Skip to content

Commit 1bc1dca

Browse files
committed
change(core): keep injecting the X-Forwarded-* headers, append instead of replace
Review asked for the request-header visibility to be preserved rather than traded away. It can be, and most of the speedup survives. The fast path no longer returns without writing the headers. Route `vars`, `ngx.req.get_headers()`, and the exits that copy `r->headers_in` instead of reading `$var_x_forwarded_*` -- `@grpc_pass`, `@dubbo_pass`, the mirror subrequest -- all observe them, and skipping the write changed what every one of those saw. On a default install that is not an edge case: with no `trusted_addresses` configured every peer is untrusted, so the fast path is the common path. What is still worth skipping is the cost of *replacing* a header. `set_header` walks the header list to find the old entry and remove it before inserting; on this path the entry is known absent, since that is the condition for being here, so `add_header` is enough. The rest of the old slow path is a no-op on this input anyway -- every `original_x_forwarded_*` value is nil, and clearing `Forwarded` or `X-Forwarded-For` has nothing to clear -- so it stays skipped. Eight rounds, alternating, single worker, wrk2 at saturation, worker on one P-core and the load generator on two others (the earlier number in this PR was taken with the worker and the load generator on hyperthread siblings and is not comparable): | variant | mean | range | |---------------------------|--------|---------------| | master | 79,345 | 77,772-80,806 | | this commit | 86,145 | 84,368-87,417 | | previous commit (skipped) | 89,726 | 87,945-91,423 | +8.6% instead of +13.1%, with the three ranges disjoint across all eight rounds. The only behaviour left that differs from master is the one that was always a fix: a plugin-set X-Forwarded-* now reaches the upstream, because `set_upstream_x_forwarded_headers` reads `ngx.var` instead of the `ctx.var` entry that a stale invalidation left behind. `core.request.set_header` invalidates `ctx.var` under the hyphenated header name while `core/ctx.lua` caches the underscored one; that cause deserves its own fix. `t/plugin/proxy-mirror4.t` TEST 1 pinned the behaviour being given up here and now asserts the values master produces. `t/core/trusted-addresses.t` TEST 17 and TEST 18 pin the two visibility consumers that have no other coverage: a plugin reading the whole header table, and a route matching on `http_x_forwarded_proto`.
1 parent 990aef5 commit 1bc1dca

3 files changed

Lines changed: 96 additions & 25 deletions

File tree

apisix/init.lua

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -706,27 +706,32 @@ local function handle_x_forwarded_headers(api_ctx)
706706
local xf_for = ngx_var.http_x_forwarded_for
707707
local forwarded = ngx_var.http_forwarded
708708

709-
-- Nothing forgeable was sent, so there is nothing to neutralize: the request
710-
-- headers are left untouched, which skips four `ngx.req.set_header()` calls.
711-
-- The values handed to the upstream must not change though, and the
712-
-- `set $var_x_forwarded_*` defaults in ngx_tpl.lua resolve to
713-
-- $scheme/$host/$server_port, so they drop an explicit port carried by the
714-
-- Host header -- which is the port the client actually used, and the only
715-
-- correct X-Forwarded-Port on a port-mapped deployment. Write the variables
716-
-- only in the cases where those defaults would lose information; `$scheme`
717-
-- never needs one.
709+
-- Nothing forgeable was sent, so there is nothing to neutralize -- but the
710+
-- headers still have to be there. Route `vars`, `ngx.req.get_headers()`, and
711+
-- the exits that copy `r->headers_in` rather than reading `$var_x_forwarded_*`
712+
-- (`@grpc_pass`, `@dubbo_pass`, the mirror subrequest) all observe them, so
713+
-- skipping the write would change what those see.
714+
--
715+
-- What can be skipped is the cost of *replacing* a header. `set_header` walks
716+
-- the header list to find the old entry and removes it; here the entry is
717+
-- known absent -- that is the condition for being on this path -- so appending
718+
-- is enough. The rest of the slow path is a no-op on this input anyway: the
719+
-- `original_x_forwarded_*` values are all nil, and clearing `Forwarded` or
720+
-- `X-Forwarded-For` has nothing to clear.
718721
if not (xf_proto or xf_host or xf_port or xf_for or forwarded) then
719-
local http_host = ngx_var.http_host
720-
if http_host then
721-
local _, port_from_host = core.utils.parse_addr(http_host)
722-
if port_from_host then
723-
api_ctx.var.var_x_forwarded_host = http_host
724-
api_ctx.var.var_x_forwarded_port = port_from_host
725-
elseif http_host ~= api_ctx.var.host then
726-
-- `$host` is lower-cased, the Host header is not
727-
api_ctx.var.var_x_forwarded_host = http_host
728-
end
729-
end
722+
local proto = api_ctx.var.scheme
723+
local http_host = ngx_var.http_host or api_ctx.var.host
724+
-- parse_addr handles IPv6 literals and bracketed host:port correctly.
725+
local _, port_from_host = core.utils.parse_addr(http_host)
726+
local port = port_from_host or api_ctx.var.server_port
727+
728+
core.request.add_header(api_ctx, "X-Forwarded-Proto", proto)
729+
core.request.add_header(api_ctx, "X-Forwarded-Host", http_host)
730+
core.request.add_header(api_ctx, "X-Forwarded-Port", port)
731+
732+
api_ctx.var.http_x_forwarded_proto = proto
733+
api_ctx.var.http_x_forwarded_host = http_host
734+
api_ctx.var.http_x_forwarded_port = port
730735
return
731736
end
732737

t/core/trusted-addresses.t

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,3 +641,69 @@ x-forwarded-host: LOCALHOST
641641
x-forwarded-port: 1984
642642
x-forwarded-proto: http
643643
x-real-ip: 127.0.0.1
644+
645+
646+
647+
=== TEST 17: a request carrying no X-Forwarded-* still gets them in r->headers_in
648+
--- yaml_config
649+
apisix:
650+
node_listen: 1984
651+
enable_admin: false
652+
deployment:
653+
role: data_plane
654+
role_data_plane:
655+
config_provider: yaml
656+
--- apisix_yaml
657+
routes:
658+
-
659+
id: 1
660+
uri: /old_uri
661+
plugins:
662+
serverless-pre-function:
663+
phase: rewrite
664+
functions:
665+
- return function(conf, ctx) local h = ngx.req.get_headers(); ngx.log(ngx.WARN, "header-table xfp=", tostring(h["x-forwarded-proto"]), " xfh=", tostring(h["x-forwarded-host"]), " xfport=", tostring(h["x-forwarded-port"])) end
666+
upstream:
667+
nodes:
668+
"127.0.0.1:1980": 1
669+
type: roundrobin
670+
#END
671+
--- request
672+
GET /old_uri
673+
--- error_log
674+
header-table xfp=http xfh=localhost xfport=1984
675+
676+
677+
678+
=== TEST 18: a route matching on http_x_forwarded_proto still matches without one
679+
--- yaml_config
680+
apisix:
681+
node_listen: 1984
682+
enable_admin: false
683+
deployment:
684+
role: data_plane
685+
role_data_plane:
686+
config_provider: yaml
687+
--- apisix_yaml
688+
routes:
689+
-
690+
id: 1
691+
uri: /old_uri
692+
vars:
693+
- ["http_x_forwarded_proto", "==", "http"]
694+
upstream:
695+
nodes:
696+
"127.0.0.1:1980": 1
697+
type: roundrobin
698+
#END
699+
--- request
700+
GET /old_uri
701+
--- error_code: 200
702+
--- response_body
703+
uri: /old_uri
704+
host: localhost
705+
x-forwarded-for: 127.0.0.1
706+
x-forwarded-host: localhost
707+
x-forwarded-port: 1984
708+
x-forwarded-proto: http
709+
x-real-ip: 127.0.0.1

t/plugin/proxy-mirror4.t

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
# The mirror subrequest, `@grpc_pass` and `@dubbo_pass` copy `r->headers_in`
1818
# instead of reading `$var_x_forwarded_*` the way `location /` does through
1919
# `proxy_set_header`. They therefore observe whether `handle_x_forwarded_headers`
20-
# wrote the X-Forwarded-* request headers, which it only does for a request that
21-
# carried a forgeable value. The mirror stands in for all three here because it
22-
# is the one that can be driven over plain HTTP.
20+
# wrote the X-Forwarded-* request headers at all, which makes them the regression
21+
# test for the fast path continuing to write them. The mirror stands in for all
22+
# three here because it is the one that can be driven over plain HTTP.
2323
use t::APISIX 'no_plan';
2424

2525
repeat_each(1);
@@ -59,7 +59,7 @@ run_tests;
5959

6060
__DATA__
6161
62-
=== TEST 1: a request carrying no X-Forwarded-* leaves the mirror with none either
62+
=== TEST 1: a request carrying no X-Forwarded-* still reaches the mirror with them
6363
--- yaml_config
6464
apisix:
6565
node_listen: 1984
@@ -86,7 +86,7 @@ GET /hello
8686
--- response_body
8787
hello world
8888
--- error_log
89-
mirror x-forwarded-proto: nil, x-forwarded-host: nil, x-forwarded-port: nil
89+
mirror x-forwarded-proto: http, x-forwarded-host: localhost, x-forwarded-port: 1984
9090
9191
9292

0 commit comments

Comments
 (0)