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."

Reply via email to