---
title: "TCP proxy"
url: "/docs/traffic/tcp-proxy/"
kind: "feature"
summary: "Forward raw TCP streams to a cluster, split them across weighted clusters, and route by TLS SNI without terminating TLS."
---

> Arion site index: [/llms.txt](/llms.txt)

# TCP proxy

Forward raw TCP streams to a cluster, split them across weighted clusters, and route by TLS SNI without terminating TLS.

The `tcp_proxy` network filter forwards a connection's bytes to an upstream cluster without interpreting them. Use it for anything that is not HTTP — databases, message brokers, plain TLS passthrough — or when you want L4 forwarding with none of the HTTP machinery.

## Enable it

`tcp_proxy` is the terminal filter of a filter chain, in place of the HTTP connection manager:

```yaml,name=arion.yaml,arion=fragment
listeners:
  - name: listener_tcp
    address:
      socket_address: { address: 0.0.0.0, port_value: 8081 }
    filter_chains:
      - name: tcp_chain
        filters:
          - name: tcp
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
              cluster: backend
```

The filter takes a cluster and, optionally, [access logs](@/docs/observe/access-logs.md). Everything about the upstream side — endpoints, load balancing, health checks, upstream TLS — comes from the [cluster](@/docs/traffic/upstreams-and-load-balancing.md).

## Weighted clusters

Split connections across clusters by weight instead of naming one:

```yaml,arion=fragment
typed_config:
  "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
  weighted_clusters:
    clusters:
      - name: backend-v1
        weight: 90
      - name: backend-v2
        weight: 10
```

Each entry carries only `name` and `weight`; the split is per connection, not per request.

## Route by SNI

Because [filter chain selection](@/docs/traffic/listeners.md) happens before any filter runs, a listener with the TLS inspector can steer TLS connections to different clusters by SNI — without terminating TLS. Each chain omits a `transport_socket`, so the encrypted stream passes through untouched:

```yaml,arion=fragment
listeners:
  - name: listener_tls_passthrough
    address:
      socket_address: { address: 0.0.0.0, port_value: 8443 }
    listener_filters:
      - name: envoy.filters.listener.tls_inspector
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector
          enable_ja3_fingerprinting: false
    filter_chains:
      - name: api_chain
        filter_chain_match:
          server_names: [api.example.com]
        filters:
          - name: tcp
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
              cluster: api
      - name: default_chain
        filters:
          - name: tcp
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
              cluster: backend
```

Add a `transport_socket` to a chain to terminate TLS at Arion instead, and combine `tcp_proxy` with the RBAC, connection limit, or global rate limit network filters ahead of it. The [Route TCP at L4 guide](@/guides/proxy/route-tcp-at-l4.md) builds a complete L4 front end; its production notes cover the SNI split.

## TCP or HTTP?

Reach for `tcp_proxy` when Arion should not parse the stream: non-HTTP protocols, end-to-end TLS where the backend must see the client hello, or minimum-overhead port forwarding. Use the [HTTP connection manager](@/docs/traffic/routing.md) whenever you need per-request behaviour — routing by path or header, retries, HTTP filters, per-request logs.

## Verify

With an echo server as the upstream:

```bash
printf 'ping\n' | nc -q1 127.0.0.1 8081
```

```text
ping
```

## Limits

- One `cluster` or `weighted_clusters` per filter; the choice is fixed per chain.
- Idle and connection-duration timeouts on the filter are not supported — bound connections with the cluster's settings and the [connection limit](@/reference/filters/network/connection-limit.md) filter.
- CONNECT tunneling (`tunneling_config`), `max_connect_attempts`, and hash-based endpoint selection are not supported and are refused at startup.
- Per-entry `metadata_match` on weighted clusters is not supported.

The field table is in the [TCP proxy reference](@/reference/filters/network/tcp-proxy.md).
