This is an automated email from the ASF dual-hosted git repository.
nic-6443 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new 874572e54 feat: support per-port PROXY protocol for stream TCP proxy
(#13561)
874572e54 is described below
commit 874572e54e972c71d9b0995f3ccf36b4e61ca3c3
Author: Nic <[email protected]>
AuthorDate: Tue Jun 16 16:13:18 2026 +0800
feat: support per-port PROXY protocol for stream TCP proxy (#13561)
---
apisix/cli/ngx_tpl.lua | 12 +-
apisix/cli/ops.lua | 46 ++++++-
apisix/cli/schema.lua | 6 +
conf/config.yaml.example | 13 +-
docs/en/latest/stream-proxy.md | 36 ++++++
docs/zh/latest/stream-proxy.md | 36 ++++++
t/cli/test_stream_proxy_protocol.sh | 231 ++++++++++++++++++++++++++++++++++++
7 files changed, 368 insertions(+), 12 deletions(-)
diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua
index 2e9811719..d95046efe 100644
--- a/apisix/cli/ngx_tpl.lua
+++ b/apisix/cli/ngx_tpl.lua
@@ -219,15 +219,16 @@ stream {
}
}
+ {% for _, server_group in ipairs(stream_proxy.servers or {}) do %}
server {
- {% for _, item in ipairs(stream_proxy.tcp or {}) do %}
- listen {*item.addr*} {% if item.tls then %} ssl {% end %} {% if
enable_reuseport then %} reuseport {% end %} {% if proxy_protocol and
proxy_protocol.enable_tcp_pp then %} proxy_protocol {% end %};
+ {% for _, item in ipairs(server_group.tcp) do %}
+ listen {*item.addr*} {% if item.tls then %} ssl {% end %} {% if
enable_reuseport then %} reuseport {% end %} {% if item.proxy_protocol then %}
proxy_protocol {% end %};
{% end %}
- {% for _, addr in ipairs(stream_proxy.udp or {}) do %}
+ {% for _, addr in ipairs(server_group.udp) do %}
listen {*addr*} udp {% if enable_reuseport then %} reuseport {% end %};
{% end %}
- {% if tcp_enable_ssl then %}
+ {% if server_group.tcp_enable_ssl then %}
ssl_certificate {* ssl.ssl_cert *};
ssl_certificate_key {* ssl.ssl_cert_key *};
@@ -240,7 +241,7 @@ stream {
}
{% end %}
- {% if proxy_protocol and proxy_protocol.enable_tcp_pp_to_upstream then
%}
+ {% if server_group.proxy_protocol_to_upstream then %}
proxy_protocol on;
{% end %}
@@ -260,6 +261,7 @@ stream {
apisix.stream_log_phase()
}
}
+ {% end %}
}
{% end %}
diff --git a/apisix/cli/ops.lua b/apisix/cli/ops.lua
index b14d612e5..36a2d3bca 100644
--- a/apisix/cli/ops.lua
+++ b/apisix/cli/ops.lua
@@ -605,7 +605,6 @@ Please modify "admin_key" in conf/config.yaml .
yaml_conf.apisix.ssl.ssl_cert = "cert/ssl_PLACE_HOLDER.crt"
yaml_conf.apisix.ssl.ssl_cert_key = "cert/ssl_PLACE_HOLDER.key"
- local tcp_enable_ssl
-- compatible with the original style which only has the addr
if enable_stream and yaml_conf.apisix.stream_proxy and
yaml_conf.apisix.stream_proxy.tcp then
local tcp = yaml_conf.apisix.stream_proxy.tcp
@@ -616,9 +615,6 @@ Please modify "admin_key" in conf/config.yaml .
if not ok then
util.die("invalid stream_proxy.tcp entry: ", verr, "\n")
end
- if item.tls then
- tcp_enable_ssl = true
- end
table_insert(normalized_tcp, item)
else
local ok, verr = validate_port_or_range(item)
@@ -641,6 +637,47 @@ Please modify "admin_key" in conf/config.yaml .
end
end
+ -- Split stream listens into nginx server blocks.
`proxy_protocol_to_upstream`
+ -- (sending the PROXY protocol toward the upstream) is a server-level
directive,
+ -- so TCP listens that enable it need a dedicated server block. The
accept-side
+ -- `proxy_protocol` and `ssl` are per-listen directives and coexist in one
block.
+ -- Per-listen settings fall back to the global `proxy_protocol` options;
UDP never
+ -- sends the PROXY protocol upstream and always joins the plain block.
+ if enable_stream and yaml_conf.apisix.stream_proxy then
+ local stream_proxy = yaml_conf.apisix.stream_proxy
+ local pp = yaml_conf.apisix.proxy_protocol or {}
+ local plain = {
+ tcp = {}, udp = stream_proxy.udp or {},
+ proxy_protocol_to_upstream = false, tcp_enable_ssl = false,
+ }
+ local to_upstream = {
+ tcp = {}, udp = {},
+ proxy_protocol_to_upstream = true, tcp_enable_ssl = false,
+ }
+ for _, item in ipairs(stream_proxy.tcp or {}) do
+ if item.proxy_protocol == nil then
+ item.proxy_protocol = pp.enable_tcp_pp
+ end
+ local up = item.proxy_protocol_to_upstream
+ if up == nil then
+ up = pp.enable_tcp_pp_to_upstream
+ end
+ local group = up and to_upstream or plain
+ if item.tls then
+ group.tcp_enable_ssl = true
+ end
+ table_insert(group.tcp, item)
+ end
+ local servers = {}
+ if #plain.tcp > 0 or #plain.udp > 0 then
+ table_insert(servers, plain)
+ end
+ if #to_upstream.tcp > 0 then
+ table_insert(servers, to_upstream)
+ end
+ stream_proxy.servers = servers
+ end
+
local dubbo_upstream_multiplex_count = 32
if yaml_conf.plugin_attr and yaml_conf.plugin_attr["dubbo-proxy"] then
local dubbo_conf = yaml_conf.plugin_attr["dubbo-proxy"]
@@ -691,7 +728,6 @@ Please modify "admin_key" in conf/config.yaml .
enabled_stream_plugins = enabled_stream_plugins,
dubbo_upstream_multiplex_count = dubbo_upstream_multiplex_count,
status_server_addr = status_server_addr,
- tcp_enable_ssl = tcp_enable_ssl,
admin_server_addr = admin_server_addr,
control_server_addr = control_server_addr,
prometheus_server_addr = prometheus_server_addr,
diff --git a/apisix/cli/schema.lua b/apisix/cli/schema.lua
index 8a70bda8b..6673e565a 100644
--- a/apisix/cli/schema.lua
+++ b/apisix/cli/schema.lua
@@ -165,6 +165,12 @@ local config_schema = {
},
tls = {
type = "boolean",
+ },
+ proxy_protocol = {
+ type = "boolean",
+ },
+ proxy_protocol_to_upstream = {
+ type = "boolean",
}
},
required = {"addr"}
diff --git a/conf/config.yaml.example b/conf/config.yaml.example
index a4f5c6f1e..2360647e8 100644
--- a/conf/config.yaml.example
+++ b/conf/config.yaml.example
@@ -38,8 +38,13 @@ apisix:
# proxy_protocol: # PROXY Protocol configuration
# listen_http_port: 9181 # APISIX listening port for HTTP
traffic with PROXY protocol.
# listen_https_port: 9182 # APISIX listening port for HTTPS
traffic with PROXY protocol.
- # enable_tcp_pp: true # Enable the PROXY protocol when
stream_proxy.tcp is set.
- # enable_tcp_pp_to_upstream: true # Enable the PROXY protocol.
+ # enable_tcp_pp: true # Accept the PROXY protocol on every
stream_proxy.tcp port.
+ # # Acts as the default; override per
port with the
+ # # `proxy_protocol` field on a
stream_proxy.tcp entry.
+ # enable_tcp_pp_to_upstream: true # Send the PROXY protocol to the
upstream on every
+ # # stream_proxy.tcp port. Acts as the
default; override
+ # # per port with
`proxy_protocol_to_upstream` on a
+ # # stream_proxy.tcp entry.
enable_server_tokens: true # If true, show APISIX version in the
`Server` response header.
extra_lua_path: "" # Extend lua_package_path to load
third-party code.
@@ -79,6 +84,10 @@ apisix:
# - addr: 9100 # Set the TCP proxy listening ports.
# tls: true
# - addr: "127.0.0.1:9101"
+ # proxy_protocol: true # Accept the PROXY protocol on
this port only.
+ # proxy_protocol_to_upstream: true # Send the PROXY protocol to the
upstream on this
+ # # port only. Both override the
global
+ # # proxy_protocol.enable_tcp_pp*
defaults.
# - "2000-2100" # port range (nginx native support)
# - addr: "3000-3100" # port range in table form
# - addr: "127.0.0.1:4000-4100" # address with port range in table form
diff --git a/docs/en/latest/stream-proxy.md b/docs/en/latest/stream-proxy.md
index 1fb9ac0bb..54fe67cf4 100644
--- a/docs/en/latest/stream-proxy.md
+++ b/docs/en/latest/stream-proxy.md
@@ -241,3 +241,39 @@ curl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H
"X-API-KEY: $admin_ke
By setting the `scheme` to `tls`, APISIX will do TLS handshake with the
upstream.
When the client is also speaking TLS over TCP, the SNI from the client will
pass through to the upstream. Otherwise, a dummy SNI `apisix_backend` will be
used.
+
+## PROXY protocol
+
+APISIX can accept the [PROXY
protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) on TCP
stream ports and forward it to the upstream.
+
+The `apisix.proxy_protocol` options set the default for **all** TCP stream
ports:
+
+```yaml
+apisix:
+ proxy_protocol:
+ enable_tcp_pp: true # accept the PROXY protocol from the
client
+ enable_tcp_pp_to_upstream: true # send the PROXY protocol to the upstream
+ proxy_mode: http&stream
+ stream_proxy:
+ tcp:
+ - 9100
+ - 9101
+```
+
+To control the PROXY protocol per port, set `proxy_protocol` and/or
`proxy_protocol_to_upstream` on a `stream_proxy.tcp` entry. The per-port value
overrides the global default for that port:
+
+```yaml
+apisix:
+ proxy_protocol:
+ enable_tcp_pp: true # default for ports that don't set
`proxy_protocol`
+ proxy_mode: http&stream
+ stream_proxy:
+ tcp:
+ - addr: 9100 # accepts the PROXY protocol
(inherits the global default)
+ - addr: 9101
+ proxy_protocol: false # opt this port out of accepting
the PROXY protocol
+ - addr: 9102
+ proxy_protocol_to_upstream: true # also send the PROXY protocol to
the upstream
+```
+
+The accept side (`proxy_protocol`) is a per-listen directive, so ports with
different settings can share one listener. The upstream side
(`proxy_protocol_to_upstream`) is a server-level directive, so APISIX renders
ports that send the PROXY protocol upstream into a separate `server` block. UDP
listens never send the PROXY protocol upstream, so they always stay in the
plain `server` block.
diff --git a/docs/zh/latest/stream-proxy.md b/docs/zh/latest/stream-proxy.md
index eb0cd36be..308e56385 100644
--- a/docs/zh/latest/stream-proxy.md
+++ b/docs/zh/latest/stream-proxy.md
@@ -232,3 +232,39 @@ curl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H
"X-API-KEY: $admin_ke
通过设置 `scheme` 为 `tls`,APISIX 将与上游进行 TLS 握手。
当客户端也使用基于 TCP 的 TLS 上游时,客户端发送的 SNI 将传递给上游。否则,将使用一个假的 SNI `apisix_backend`。
+
+## PROXY 协议
+
+APISIX 可以在 TCP stream 端口上接收 [PROXY
协议](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt),并将其转发给上游。
+
+`apisix.proxy_protocol` 选项为**所有** TCP stream 端口设置默认值:
+
+```yaml
+apisix:
+ proxy_protocol:
+ enable_tcp_pp: true # 接收来自客户端的 PROXY 协议
+ enable_tcp_pp_to_upstream: true # 向上游发送 PROXY 协议
+ proxy_mode: http&stream
+ stream_proxy:
+ tcp:
+ - 9100
+ - 9101
+```
+
+如需按端口控制 PROXY 协议,可在 `stream_proxy.tcp` 条目上设置 `proxy_protocol` 和/或
`proxy_protocol_to_upstream`。端口级别的设置会覆盖该端口的全局默认值:
+
+```yaml
+apisix:
+ proxy_protocol:
+ enable_tcp_pp: true # 未设置 `proxy_protocol` 的端口的默认值
+ proxy_mode: http&stream
+ stream_proxy:
+ tcp:
+ - addr: 9100 # 接收 PROXY 协议(继承全局默认值)
+ - addr: 9101
+ proxy_protocol: false # 该端口不接收 PROXY 协议
+ - addr: 9102
+ proxy_protocol_to_upstream: true # 该端口同时向上游发送 PROXY 协议
+```
+
+接收侧(`proxy_protocol`)是 listen
级别的指令,因此设置不同的端口可以共用一个监听块。上游侧(`proxy_protocol_to_upstream`)是 server 级别的指令,因此
APISIX 会把向上游发送 PROXY 协议的端口渲染到单独的 `server` 块中。UDP 监听永远不会向上游发送 PROXY
协议,因此始终保留在普通的 `server` 块中。
diff --git a/t/cli/test_stream_proxy_protocol.sh
b/t/cli/test_stream_proxy_protocol.sh
new file mode 100755
index 000000000..a71ad6ba4
--- /dev/null
+++ b/t/cli/test_stream_proxy_protocol.sh
@@ -0,0 +1,231 @@
+#!/usr/bin/env bash
+
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+. ./t/cli/common.sh
+
+# Print the stream server{} block that contains "listen <port>", isolating
+# it by brace-counting from each "server {".
+block_with_listen() {
+ awk -v port="$1" '
+ /server[ \t]*\{/ { depth = 0; block = ""; inblock = 1 }
+ inblock {
+ block = block $0 "\n"
+ depth += gsub(/\{/, "{")
+ depth -= gsub(/\}/, "}")
+ if (depth == 0) {
+ if (block ~ ("listen " port "[ ;]")) { printf "%s", block }
+ inblock = 0
+ }
+ }
+ ' conf/nginx.conf
+}
+
+# === Default: no PROXY protocol anywhere ===
+echo '
+apisix:
+ proxy_mode: "http&stream"
+ stream_proxy:
+ tcp:
+ - 9100
+ udp:
+ - 9200
+' > conf/config.yaml
+make init
+
+if grep -E "listen 9100[ ;].*proxy_protocol" conf/nginx.conf > /dev/null; then
+ echo "failed: tcp port should not accept PROXY protocol by default"
+ exit 1
+fi
+if grep -E "proxy_protocol on;" conf/nginx.conf > /dev/null; then
+ echo "failed: PROXY protocol to upstream should be off by default"
+ exit 1
+fi
+echo "passed: default has no PROXY protocol"
+
+# === Global enable_tcp_pp applies to every tcp port (accept side) ===
+echo '
+apisix:
+ proxy_mode: "http&stream"
+ proxy_protocol:
+ enable_tcp_pp: true
+ stream_proxy:
+ tcp:
+ - 9100
+ - 9101
+ udp:
+ - 9200
+' > conf/config.yaml
+make init
+
+if ! grep -E "listen 9100[ ;].*proxy_protocol" conf/nginx.conf > /dev/null;
then
+ echo "failed: global enable_tcp_pp should add proxy_protocol to 9100"
+ exit 1
+fi
+if ! grep -E "listen 9101[ ;].*proxy_protocol" conf/nginx.conf > /dev/null;
then
+ echo "failed: global enable_tcp_pp should add proxy_protocol to 9101"
+ exit 1
+fi
+if grep -E "listen 9200 udp.*proxy_protocol" conf/nginx.conf > /dev/null; then
+ echo "failed: udp listen must not carry proxy_protocol"
+ exit 1
+fi
+echo "passed: global enable_tcp_pp on every tcp port"
+
+# === Per-port accept override beats the global default ===
+echo '
+apisix:
+ proxy_mode: "http&stream"
+ proxy_protocol:
+ enable_tcp_pp: true
+ stream_proxy:
+ tcp:
+ - addr: 9100
+ - addr: 9101
+ proxy_protocol: false
+' > conf/config.yaml
+make init
+
+if ! grep -E "listen 9100[ ;].*proxy_protocol" conf/nginx.conf > /dev/null;
then
+ echo "failed: 9100 should inherit global enable_tcp_pp"
+ exit 1
+fi
+if grep -E "listen 9101[ ;].*proxy_protocol" conf/nginx.conf > /dev/null; then
+ echo "failed: 9101 should opt out of PROXY protocol"
+ exit 1
+fi
+echo "passed: per-port accept override"
+
+# === Per-port proxy_protocol_to_upstream splits into its own server block ===
+echo '
+apisix:
+ proxy_mode: "http&stream"
+ stream_proxy:
+ tcp:
+ - addr: 9100
+ - addr: 9101
+ proxy_protocol_to_upstream: true
+ udp:
+ - 9200
+' > conf/config.yaml
+make init
+
+if [ "$(grep -c "proxy_protocol on;" conf/nginx.conf)" != "1" ]; then
+ echo "failed: expected exactly one 'proxy_protocol on;' server block"
+ exit 1
+fi
+if ! block_with_listen 9101 | grep -E "proxy_protocol on;" > /dev/null; then
+ echo "failed: 9101 should be in the proxy_protocol-to-upstream block"
+ exit 1
+fi
+if block_with_listen 9100 | grep -E "proxy_protocol on;" > /dev/null; then
+ echo "failed: 9100 should be in the plain block"
+ exit 1
+fi
+# 9100 and the udp 9200 share the same plain block
+if ! block_with_listen 9100 | grep -E "listen 9200 udp" > /dev/null; then
+ echo "failed: plain tcp and udp listens should share one server block"
+ exit 1
+fi
+echo "passed: per-port proxy_protocol_to_upstream server block split"
+
+# === Global enable_tcp_pp_to_upstream keeps udp out of the proxy_protocol
block ===
+echo '
+apisix:
+ proxy_mode: "http&stream"
+ proxy_protocol:
+ enable_tcp_pp_to_upstream: true
+ stream_proxy:
+ tcp:
+ - 9100
+ - 9101
+ udp:
+ - 9200
+' > conf/config.yaml
+make init
+
+if [ "$(grep -c "proxy_protocol on;" conf/nginx.conf)" != "1" ]; then
+ echo "failed: expected exactly one 'proxy_protocol on;' server block"
+ exit 1
+fi
+if ! block_with_listen 9100 | grep -E "proxy_protocol on;" > /dev/null; then
+ echo "failed: 9100 should send PROXY protocol upstream"
+ exit 1
+fi
+if ! block_with_listen 9101 | grep -E "proxy_protocol on;" > /dev/null; then
+ echo "failed: 9101 should send PROXY protocol upstream"
+ exit 1
+fi
+if block_with_listen 9200 | grep -E "proxy_protocol on;" > /dev/null; then
+ echo "failed: udp 9200 must not be in the proxy_protocol-to-upstream block"
+ exit 1
+fi
+echo "passed: global enable_tcp_pp_to_upstream keeps udp separate"
+
+# === Per-port proxy_protocol_to_upstream=false beats a global true default ===
+echo '
+apisix:
+ proxy_mode: "http&stream"
+ proxy_protocol:
+ enable_tcp_pp_to_upstream: true
+ stream_proxy:
+ tcp:
+ - addr: 9100
+ - addr: 9101
+ proxy_protocol_to_upstream: false
+' > conf/config.yaml
+make init
+
+if ! block_with_listen 9100 | grep -E "proxy_protocol on;" > /dev/null; then
+ echo "failed: 9100 should inherit global enable_tcp_pp_to_upstream"
+ exit 1
+fi
+if block_with_listen 9101 | grep -E "proxy_protocol on;" > /dev/null; then
+ echo "failed: 9101 should opt out of PROXY protocol to upstream"
+ exit 1
+fi
+echo "passed: per-port proxy_protocol_to_upstream=false override"
+
+# === A TLS port in the to-upstream group renders ssl in that block only ===
+echo '
+apisix:
+ proxy_mode: "http&stream"
+ stream_proxy:
+ tcp:
+ - addr: 9100
+ - addr: 9101
+ tls: true
+ proxy_protocol_to_upstream: true
+' > conf/config.yaml
+make init
+
+if ! block_with_listen 9101 | grep -E "ssl_certificate " > /dev/null; then
+ echo "failed: tls port in the to-upstream block should render
ssl_certificate"
+ exit 1
+fi
+if ! block_with_listen 9101 | grep -E "proxy_protocol on;" > /dev/null; then
+ echo "failed: 9101 should send PROXY protocol upstream"
+ exit 1
+fi
+if block_with_listen 9100 | grep -E "ssl_certificate " > /dev/null; then
+ echo "failed: the plain block must not render ssl_certificate"
+ exit 1
+fi
+echo "passed: per-group ssl follows the TLS port into its server block"
+
+echo "All stream PROXY protocol tests passed."