---
title: "Listeners"
url: "/docs/traffic/listeners/"
kind: "feature"
summary: "Bind socket addresses, choose a filter chain per connection, terminate TLS, and run listener filters such as the TLS inspector."
---

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

# Listeners

Bind socket addresses, choose a filter chain per connection, terminate TLS, and run listener filters such as the TLS inspector.

A listener binds a socket address, accepts connections, and hands each one to the filter chain that best matches it. Configure one listener per port Arion serves, and use multiple filter chains when a single port must present different certificates, tenants, or protocols.

## Enable a listener

```yaml,name=arion.yaml,arion=fragment
envoy_bootstrap:
  static_resources:
    listeners:
      - name: listener_http
        address:
          socket_address: { address: 0.0.0.0, port_value: 8080 }
        filter_chains:
          - name: default_chain
            filters:
              - name: http_gateway
                typed_config:
                  "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                  codec_type: HTTP1
                  http_filters:
                    - name: envoy.filters.http.router
                      typed_config:
                        "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
                  route_config:
                    name: local_routes
                    virtual_hosts:
                      - name: backend
                        domains: ["*"]
                        routes:
                          - match: { prefix: "/" }
                            route: { cluster: api }
```

A listener needs a unique `name`, an `address`, and at least one filter chain. Each chain runs optional network filters — [RBAC](@/reference/filters/network/rbac.md), [connection limit](@/reference/filters/network/connection-limit.md), [global rate limit](@/reference/filters/network/rate-limit.md) — followed by exactly one terminal filter: an [HTTP connection manager](@/reference/filters/network/http-connection-manager.md) or a [TCP proxy](@/docs/traffic/tcp-proxy.md). `tcp_backlog_size` (default 128) sizes the accept queue, and `socket_options` may carry one bind-device entry (a Linux `SO_BINDTODEVICE` interface name).

## Sockets, pipes, and internal listeners

Listeners bind TCP socket addresses. Unix pipes sit on the other side of the proxy: a cluster endpoint may use a `pipe` address, so Arion can accept on a TCP port and forward to a local process over a Unix socket. Inside a cluster's `load_assignment`:

```yaml,arion=fragment
lb_endpoints:
  - endpoint:
      address:
        pipe:
          path: /var/run/backend.sock
```

A listener declared with `internal_listener: {}` in place of an address accepts connections from inside the proxy rather than from the network; clusters reach it through endpoints with internal addresses. Binding a listener to a pipe address is rejected at startup.

## Multiple filter chains

When a listener has several filter chains, Arion picks one per connection using `filter_chain_match`:

| Field                  | Matches                                     |
| ---------------------- | ------------------------------------------- |
| `destination_port`     | Port the connection arrived on              |
| `prefix_ranges`        | Destination IP, CIDR                        |
| `server_names`         | TLS SNI, exact or `*.example.com` wildcard  |
| `source_prefix_ranges` | Client IP, CIDR                             |
| `source_ports`         | Client port                                 |

The most specific chain wins: a chain that states no rule for a criterion stays eligible, a chain whose rule fails is out, and among matches the tighter value (longer CIDR prefix, more exact SNI) is preferred. All match blocks in a listener must be unique, and a chain with no `filter_chain_match` is the catch-all:

```yaml,arion=fragment
filter_chains:
  - name: api_chain
    filter_chain_match:
      server_names: [api.example.com]
    # filters for api traffic ...
  - name: catch_all
    # no filter_chain_match: everything else lands here
```

Envoy's `transport_protocol`, `application_protocols`, `source_type`, and `default_filter_chain` fields are not supported; a config that uses them is refused at startup.

> [!IMPORTANT]
> Matching on `server_names` requires the TLS inspector listener filter. Arion never adds it implicitly — a listener that matches on SNI without `envoy.filters.listener.tls_inspector` fails at startup, because the matches could never succeed.

## TLS on a filter chain

TLS terminates per filter chain through a `transport_socket`, so each chain can present its own certificate:

```yaml,arion=fragment
filter_chains:
  - name: api_chain
    filter_chain_match:
      server_names: [api.example.com]
    transport_socket:
      name: envoy.transport_sockets.tls
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
        common_tls_context:
          tls_certificate_sds_secret_configs:
            - name: api_server_cert
    filters:
      # terminal filter ...
```

Set `require_client_certificate: true` for mTLS. Certificates can be inlined, read from files, or referenced as SDS secrets that rotate without a restart — see [TLS and mTLS](@/docs/security/tls-and-mtls.md) and [secrets and rotation](@/docs/security/secrets-and-rotation.md).

## Listener filters

Listener filters inspect the raw connection before a filter chain is chosen. Arion supports three: the [TLS inspector](@/reference/filters/listener/tls-inspector.md) reads the SNI from the TLS ClientHello so chain matching (and the global rate limiter's SNI-as-domain mode) can use it; [proxy protocol](@/reference/filters/listener/proxy-protocol.md) recovers the original client address from a PROXY v1/v2 header; and the listener-level [local rate limit](@/reference/filters/listener/local-rate-limit.md) drops connections above a token-bucket rate before any other work happens — see [rate limiting](@/docs/traffic/rate-limiting.md) for choosing a limiter.

## Verify

With the [admin interface](@/docs/observe/admin-interface.md) on port 9901, list bound listeners:

```bash
curl -s http://127.0.0.1:9901/listeners
```

```text
listener_http::0.0.0.0:8080
```

## Limits

- Listeners are TCP only; `udp_listener_config` (QUIC) is rejected.
- One terminal filter per chain, always last; RBAC, connection limit, and global rate limit must precede it.
- At most one bind device per listener.
- `listener_filters_timeout`, `use_original_dst`, and per-connection buffer limits are not supported and are refused at startup.
- Pipe addresses work for cluster endpoints, not listener binds.

Every listener field is tabulated in the [listener reference](@/reference/resources/listeners.md).
