丢失的$remote_port

最近做日志采集的时候发现,部分 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) {

这么改之后,如果受信任的来源没有提供端口,就使用当前的端口作为替换后的地址的端口。这种做法有两个好处:

  1. 不需要自己发明一套fallback(比如先尝试 $realip_remote_port,没有再尝试 $remote_port)
  2. 现有用了 $remote_port 的代码不需要更改

不过 Nginx 官方并没有合并这个 patch。他们认为这个行为已经存在很久了。而且更早之前,甚至没有办法由受信任的来源提供端口的做法。后来增加该功能时没有加“如果没有提供端口,使用当前端口作为默认”的设定,所以认为不算是 bug。

你可能感兴趣的:(nginx)