Skip to content

feat(stream): support set_real_ip_from to trust an inbound PROXY protocol header#13700

Draft
nic-6443 wants to merge 3 commits into
apache:masterfrom
nic-6443:feat/stream-realip
Draft

feat(stream): support set_real_ip_from to trust an inbound PROXY protocol header#13700
nic-6443 wants to merge 3 commits into
apache:masterfrom
nic-6443:feat/stream-realip

Conversation

@nic-6443

@nic-6443 nic-6443 commented Jul 16, 2026

Copy link
Copy Markdown
Member

Description

A stream port can accept the PROXY protocol (proxy_protocol) and send it on to the upstream (proxy_protocol_to_upstream), but the two don't compose into "pass the client address through". ngx_proxy_protocol_write() builds the outbound header from c->sockaddr — the directly connected peer — and never looks at c->proxy_protocol. So with a load balancer in front, the upstream is told the load balancer's address and the client's is lost. The stream $remote_addr, the access log and address matching such as ip-restriction see the load balancer for the same reason.

nginx's answer is ngx_stream_realip_module, but there was no way to configure it: set_real_ip_from is only rendered into the http block.

This adds nginx_config.stream.real_ip_from, mirroring the existing nginx_config.http.real_ip_from:

apisix:
  proxy_mode: http&stream
  stream_proxy:
    tcp:
      - addr: 9100
        proxy_protocol: true
        proxy_protocol_to_upstream: true
nginx_config:
  stream:
    real_ip_from:
      - 192.168.1.0/24

On a connection from a trusted address that carries an inbound PROXY protocol header, the module writes the header's address into c->sockaddr, so the rebuilt outbound header — and every stream-side consumer of the client address — carries the client. The directly connected address stays available as $realip_remote_addr.

The option is empty by default, so nothing changes for existing configurations. It only takes effect on ports that accept the PROXY protocol, and only for peers it matches.

Depends on #13697

ngx_stream_realip_module is not built by default, so set_real_ip_from is an unknown directive on runtimes before 1.3.11. #13697 bumps APISIX_RUNTIME to 1.3.11, which adds --with-stream_realip_module. This PR stays in draft until that lands; CI here cannot pass before it.

Tests

t/cli/test_stream_proxy_protocol.sh grows four cases. Two cover rendering: nothing is trusted by default, and each real_ip_from entry renders one set_real_ip_from. The assertions are scoped to the stream block, since the http block renders the same directive by default.

Two are end to end against a PROXY-protocol-aware stream upstream that echoes the address it was handed, so they assert on the header APISIX actually rebuilds rather than on nginx.conf:

  • with the peer trusted, a client announcing 198.51.100.7 reaches the upstream as 198.51.100.7;
  • with nothing trusted, the same connection reaches the upstream as the direct peer, so an untrusted peer's claim is ignored.

Both were run locally against apisix-runtime 1.3.11.

Checklist

  • I have explained the need for this PR and the problem it solves
  • I have explained the changes or the new features added to this PR
  • I have added tests corresponding to this change
  • I have updated the documentation to reflect this change
  • I have verified that this change is backward compatible

nic-6443 added 3 commits July 16, 2026 17:15
…ocol header

`proxy_protocol_to_upstream` rebuilds the outbound PROXY protocol header
from `c->sockaddr`, which is the directly connected peer. When APISIX sits
behind a load balancer that speaks the PROXY protocol, that peer is the
load balancer, so the client address parsed out of the inbound header is
dropped on the way to the upstream. The stream `$remote_addr`, the access
log and address-based matching are limited to the load balancer's address
for the same reason.

Add `nginx_config.stream.real_ip_from`, mirroring `nginx_config.http.real_ip_from`,
which renders `set_real_ip_from` into the stream block. On a connection
from a trusted address carrying an inbound PROXY protocol header,
ngx_stream_realip_module writes the header's address back into
`c->sockaddr`, so the rebuilt outbound header and every stream-side
address consumer see the client.

The option is empty by default, so the client address remains the directly
connected peer unless a trusted network is configured.

Requires a runtime built with `--with-stream_realip_module`.
The http block renders set_real_ip_from by default, so an unscoped grep
over nginx.conf can never assert the stream side's default.
The tests overwrite conf/config.yaml wholesale, so the `yq` lookup depends
on whatever admin key the deployment ends up with. Pin it in the test's own
config instead, which keeps the tests hermetic and independent of the
shipped default.

Also document the combination that silently loses the client address:
accepting and re-emitting the PROXY protocol without trusting any address.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant