最近做日志采集的时候发现,部分 Nginx access log 里面的 $remote_port
字段为空。$remote_port
字段提供的是客户端发起连接时使用的端口地址。理论上,如果请求是通过 unix socket 来的,没有端口自然不足为奇。不过我们的请求都是走 TCP 过来的,没有端口便是一件奇事。
好在这些$remote_port
字段为空的请求有一个共同点:它们都经过 ngx_http_realip_module 的处理,更换过实际的 IP 地址。写了个最小可复现的例子:
worker_processes 1;
events {
worker_connections 4096;
}
http {
server {
listen 8888;
location / {
set_real_ip_from 127.0.0.1;
content_by_lua_block {
ngx.say(ngx.var.remote_port)
}
}
}
}
测试方式:
$ curl 127.0.0.1:8888
43948
$ curl 127.0.0.1:8888 -H "X-Real-IP: 1.1.1.1"
确实可以看到经过替换后,$remote_port
就变为空了。既然能够通过简单的例子稳定复现,那么就算不上什么难事,悬起的心可以放下了
我怀疑是 Nginx 替换 IP 的时候,对端口地址有什么特殊的处理。遇事不决,当用gdb。用 gdb 跟踪 ngx_http_realip_module
这个 ngx_http_realip_module
入口函数的执行逻辑,最后我发现它会调用一个名为 ngx_http_get_forwarded_addr_internal
的公共函数。这个函数负责根据若干参数重写客户端地址。而如果参数里不提供端口地址,这个函数不会保留原有的端口地址。举个例子,如果入参是 1.1.1.1:9292
,那么重写后的地址,IP 是 1.1.1.1
,而端口是 9292. 但如果入参是 1.1.1.1
,那么重写后的地址,IP 是 1.1.1.1
,而端口就是空无一物。
通常借助诸如 X-Forwarded-For
等方式传递真实客户端地址时,是不会把原始的端口号一并附上的。既然没有附上端口后,Nginx 就把 $remote_port
给置空了。(由于 X-Forwarded-For
是事实上的标准,没有官方的 RFC,所以无从确认该报头是否应该提供端口。不过 MDN 上的说明和所陈列的几个例子都没有提供加端口的事情)
解决这个问题也很简单,改下对应的代码就可以了。我给 Nginx 官方发了个 patch:https://forum.nginx.org/read....
diff --git src/http/ngx_http_core_module.c src/http/ngx_http_core_module.c
index a603e09c..ff5013ad 100644
--- src/http/ngx_http_core_module.c
+++ src/http/ngx_http_core_module.c
@@ -2691,6 +2691,12 @@ ngx_http_get_forwarded_addr_internal(ngx_http_request_t *r, ngx_addr_t *addr,
return NGX_DECLINED;
}
+ in_port_t port = ngx_inet_get_port(paddr.sockaddr);
+ if (port == 0) {
+ port = ngx_inet_get_port(addr->sockaddr);
+ ngx_inet_set_port(paddr.sockaddr, port);
+ }
+
*addr = paddr;
if (recursive && p > xff) {
这么改之后,如果受信任的来源没有提供端口,就使用当前的端口作为替换后的地址的端口。这种做法有两个好处:
- 不需要自己发明一套fallback(比如先尝试 $realip_remote_port,没有再尝试 $remote_port)
- 现有用了 $remote_port 的代码不需要更改
不过 Nginx 官方并没有合并这个 patch。他们认为这个行为已经存在很久了。而且更早之前,甚至没有办法由受信任的来源提供端口的做法。后来增加该功能时没有加“如果没有提供端口,使用当前端口作为默认”的设定,所以认为不算是 bug。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。