Proxy Transport Layer (L4) Traffic
By default, APISIX operates as an application layer (L7) proxy. APISIX also supports the handling of transport layer (L4) TCP and UDP traffic, either dedicated or on top of the handling of application layer (L7) traffic.
This guide will show you how to configure APISIX to proxy transport layer (L4) traffic and configure a stream route to establish a connection with MySQL server.
Prerequisite(s)
- Install Docker.
- Install cURL to send requests to the services for validation.
- Install jq to create resources containing PEM certificates without breaking JSON escaping.
- Install OpenSSL to create the sample mTLS certificates.
- Follow the Getting Started tutorial to start a new APISIX instance in Docker or on Kubernetes.
- Install MySQL Shell to initiate connections with MySQL server.
Start a MySQL Server
Start a MySQL instance as a sample upstream service and configure the root password to be my-secret-pw:
docker run -d \
--name mysql \
--network=apisix-quickstart-net \
-e MYSQL_ROOT_PASSWORD=my-secret-pw \
mysql:9.4
Enable Transport Layer (L4) Proxy
By default, APISIX only has application layer (L7) proxy enabled. To also proxy transport layer (L4) traffic, configure proxy_mode and stream_proxy.
Update the config.yaml configuration file as follows:
docker exec apisix-quickstart /bin/sh -c "echo '
apisix:
enable_control: true
control:
ip: 0.0.0.0
port: 9092
proxy_mode: http&stream
stream_proxy:
tcp:
- 9100
# You can configure additional ports or port ranges.
# - 9200
# - "9300-9310"
# udp:
# - 9400
# - "9500-9510"
deployment:
role: traditional
role_traditional:
config_provider: etcd
admin:
admin_key_required: false
allow_admin:
- 0.0.0.0/0
plugin_attr:
prometheus:
export_addr:
ip: 0.0.0.0
port: 9091
' > /usr/local/apisix/conf/config.yaml"
❶ proxy_mode: accept both transport layer (L4) and application layer (L7) traffic.
❷ stream_proxy: configure the interface for transport layer (L4) proxy.
Configure Stream Listeners
The tcp and udp arrays accept individual ports, bound addresses, and port ranges. A TCP entry can also be an object with addr and tls fields when the listener should terminate TLS.
apisix:
proxy_mode: http&stream
stream_proxy:
tcp:
- 9100
- "127.0.0.1:9101"
- "[::1]:9102"
- "9200-9210"
- addr: "127.0.0.1:9300-9310"
tls: true
udp:
- 9400
- "127.0.0.1:9401"
- "9500-9510"
Port values must be integers from 1 through 65535. For a range, the start port must not exceed the end port. Quote range values such as "9200-9210" so YAML preserves the range as a string. You can mix individual ports, addresses, IPv6 addresses, ranges, and object entries in the same configuration.
Expose every configured listener through the container runtime, Kubernetes Service, host firewall, and any external load balancer. For example, a Docker deployment that uses ports 9100 and 9200 through 9210 needs both -p 9100:9100 and -p 9200-9210:9200-9210 when the container is created. Adding a listener to config.yaml does not publish it outside the container automatically.
Configure the PROXY Protocol
APISIX can accept the PROXY protocol from a TCP load balancer and send a new PROXY header to a compatible upstream. The global settings provide defaults for all TCP listeners:
apisix:
proxy_mode: http&stream
proxy_protocol:
enable_tcp_pp: true
enable_tcp_pp_to_upstream: true
stream_proxy:
tcp:
- addr: 9100
proxy_protocol: false
proxy_protocol_to_upstream: false
- 9200
Use object entries to override either direction for an individual port. An explicit true or false takes precedence over the corresponding global default:
apisix:
proxy_mode: http&stream
proxy_protocol:
enable_tcp_pp: true
stream_proxy:
tcp:
- addr: 9100
proxy_protocol: false
- addr: 9200
- addr: "[::]:9201"
proxy_protocol_to_upstream: true
| Setting | Direction | Effect |
|---|---|---|
proxy_protocol | Client or load balancer to APISIX | Adds the proxy_protocol parameter to the TCP listener so APISIX consumes the inbound header. |
proxy_protocol_to_upstream | APISIX to upstream | Sends a PROXY header before the proxied stream data. |
These examples reserve port 9200 for PROXY protocol traffic and explicitly keep the guide's port 9100 listener compatible with the direct MySQL and HTTP clients used later. Before using port 9200, publish it from the gateway container, bind a Stream Route to it, and use a load balancer and upstream that support the enabled PROXY protocol directions. Do not enable either direction on an existing listener until its clients and upstreams are compatible.
These settings apply only to TCP. UDP listeners never accept or send the PROXY protocol.
Enable proxy_protocol_to_upstream only when the upstream expects the PROXY protocol. Otherwise, the upstream interprets the plaintext PROXY line as application data and usually closes the connection. A TLS upstream cannot parse that line as a TLS record unless it explicitly accepts the PROXY protocol before starting TLS.
By default, APISIX does not trust the client address claimed in an inbound PROXY header. Configure nginx_config.stream.real_ip_from with only the load balancer addresses or networks that are allowed to supply the client address:
apisix:
proxy_mode: http&stream
stream_proxy:
tcp:
- addr: 9200
proxy_protocol: true
proxy_protocol_to_upstream: true
nginx_config:
stream:
real_ip_from:
- 192.168.1.0/24
- 2001:db8:1234::/48
For a connection from a trusted peer, $remote_addr, Stream access logs, and address-based plugins use the address from the inbound header. $realip_remote_addr retains the directly connected peer address.
If the peer is not trusted, APISIX consumes the inbound header but retains the peer address. A PROXY header rebuilt toward the upstream therefore identifies the untrusted peer, not its claimed client.
Trusting a network allows any peer in that network to claim an arbitrary client address. Keep the list limited to load balancers you control.
Reload APISIX for configuration changes to take effect:
docker exec apisix-quickstart apisix reload
If you started APISIX in Docker with Getting Started quickstart, port 9100 is already mapped (-p 9100:9100).
Create a Stream Route
Create a stream route and configure MySQL server to be the upstream service.
curl "http://127.0.0.1:9180/apisix/admin/stream_routes" -X PUT -d '
{
"id": "stream-route-mysql",
"server_port": 9100,
"upstream": {
"nodes": {
"mysql:3306": 1
},
"type": "roundrobin"
}
}'
Verify
Connect with the MySQL server as root and key in the password my-secret-pw once prompted:
mysql --host=127.0.0.1 --port=9100 -u root -p
If successful, you should see a welcome text similar to the following:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 9
Server version: 9.4.0 MySQL Community Server - GPL
Copyright (c) 2000, 2025, Oracle and/or its affiliates.
Authenticate to a TLS Upstream with mTLS
For a Stream upstream that requires a client certificate, set the upstream scheme to tls and provide the certificate and private key either inline or through a client SSL resource. APISIX-Runtime must include the Stream upstream mTLS module.
Generate a sample CA and server and client certificates:
mkdir -p stream-mtls-certs
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
-subj "/CN=stream-mtls-ca" \
-keyout stream-mtls-certs/ca.key \
-out stream-mtls-certs/ca.crt
openssl req -newkey rsa:2048 -nodes \
-subj "/CN=mtls-upstream" \
-keyout stream-mtls-certs/server.key \
-out stream-mtls-certs/server.csr
openssl x509 -req -days 365 \
-in stream-mtls-certs/server.csr \
-CA stream-mtls-certs/ca.crt \
-CAkey stream-mtls-certs/ca.key \
-CAcreateserial \
-out stream-mtls-certs/server.crt
openssl req -newkey rsa:2048 -nodes \
-subj "/CN=apisix-stream-client" \
-keyout stream-mtls-certs/client.key \
-out stream-mtls-certs/client.csr
openssl x509 -req -days 365 \
-in stream-mtls-certs/client.csr \
-CA stream-mtls-certs/ca.crt \
-CAkey stream-mtls-certs/ca.key \
-CAcreateserial \
-out stream-mtls-certs/client.crt
Start an upstream on the APISIX Docker network that requires a client certificate signed by the sample CA:
docker run -d --name mtls-upstream \
--network=apisix-quickstart-net \
-v "${PWD}/stream-mtls-certs:/certs:ro" \
alpine/openssl \
s_server -accept 9443 \
-cert /certs/server.crt \
-key /certs/server.key \
-CAfile /certs/ca.crt \
-Verify 1 \
-www
To keep the key out of the Upstream resource, create a client SSL resource from PEM files:
jq -n \
--rawfile cert stream-mtls-certs/client.crt \
--rawfile key stream-mtls-certs/client.key \
'{type: "client", cert: $cert, key: $key}' | \
curl "http://127.0.0.1:9180/apisix/admin/ssls/stream-client" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
--data-binary @-
Create the TLS Upstream and reference the client SSL resource:
curl "http://127.0.0.1:9180/apisix/admin/upstreams/stream-mtls" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"scheme": "tls",
"type": "roundrobin",
"nodes": {
"mtls-upstream:9443": 1
},
"tls": {
"client_cert_id": "stream-client"
}
}'
Temporarily update the existing Stream Route to use the mTLS Upstream. The later traffic-split example replaces this Route configuration with the MySQL upstream again.
curl "http://127.0.0.1:9180/apisix/admin/stream_routes/stream-route-mysql" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"server_port": 9100,
"upstream_id": "stream-mtls"
}'
When the upstream trusts the CA that signed client.crt, the TLS handshake succeeds and Stream traffic reaches the upstream. APISIX rejects malformed inline certificate and key pairs during resource validation, and closes the Stream session if a referenced client SSL resource is missing or has the wrong type. If no client certificate is configured, or the upstream does not trust the presented certificate, the upstream rejects the handshake and the client receives no proxied response. Check both the APISIX and upstream TLS error logs to distinguish configuration and parsing errors from upstream verification failures.
Send a plain HTTP request through the TCP listener. APISIX establishes the TLS connection to the sample upstream and presents its client certificate:
curl -i "http://127.0.0.1:9100/"
You should receive a response beginning with:
HTTP/1.0 200 ok
Content-type: text/html
You can also configure upstream.tls.client_cert and upstream.tls.client_key inline on an Upstream or an inline Stream Route upstream. When data encryption is enabled, APISIX encrypts an inline client_key before storage. A Stream Route GET response does not return that key in plaintext.
To use $secret://... or $env://... certificate references, put the references in the cert and key fields of a client SSL resource and select it with client_cert_id. Stream workers resolve those fields before the upstream handshake.
Split Stream Traffic
The traffic-split plugin can select an alternate upstream for a stream route. Stream routes expose variables such as route_id to rule expressions, and a weighted upstream can reference a separate upstream object by ID.
Start a second MySQL instance with a different root password:
docker run -d \
--name mysql-canary \
--network=apisix-quickstart-net \
-e MYSQL_ROOT_PASSWORD=canary-secret-pw \
mysql:9.4
Create an alternate upstream for the canary instance:
curl "http://127.0.0.1:9180/apisix/admin/upstreams/mysql-canary" -X PUT \
-d '{
"type": "roundrobin",
"nodes": {
"mysql-canary:3306": 1
}
}'
Update the stream route so requests matching its route ID use the canary upstream. A weight-only entry represents the stream route's default upstream. The 1:0 weights below send every matching connection to the canary for deterministic verification.
curl "http://127.0.0.1:9180/apisix/admin/stream_routes/stream-route-mysql" -X PUT \
-d '{
"server_port": 9100,
"plugins": {
"traffic-split": {
"rules": [
{
"match": [
{
"vars": [["route_id", "==", "stream-route-mysql"]]
}
],
"weighted_upstreams": [
{
"upstream_id": "mysql-canary",
"weight": 1
},
{
"weight": 0
}
]
}
]
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"mysql:3306": 1
}
}
}'
To send approximately 10% of matching connections to the canary, keep its weight at 1 and change the weight-only default-upstream entry to 9. Both entries are required for a split; changing the weight of a single entry does not retain traffic on the route's default upstream.
Connect to port 9100 again and enter canary-secret-pw when prompted:
mysql --host=127.0.0.1 --port=9100 -u root -p
A successful connection confirms that the rule selected mysql-canary. If no rule matches, APISIX uses the stream route's default upstream.
Next Steps
APISIX also supports TLS over TCP connections as a transport layer (L4) proxy when accepting requests from downstream clients or proxying to upstream services. For related TLS and stream routing concepts, see SSL Certificates and Stream Routes.