frp + Nginx Proxy Manager 拿不到访客真实 IP?用 Proxy Protocol 不停机切换

经 frp 转发后,帐号中心和 Nginx Proxy Manager 记录的 IP 全是本机。记录用 Proxy Protocol v2 恢复访客地址的过程:并行开新端口验证,再用 frpc reload 热切换,全程只断了一次 2~3 秒,并附重启后是否会失效的检查。

0w0 用户最后更新于 2026/11/3
本文目录5 个步骤

起因

帐号中心(Logto)的审计日志里,每一条记录的 IP 都是同一个内网地址:本地服务器自己。Nginx Proxy Manager(下文简称 NPM)的访问日志也一样,看不到任何访客的真实地址。这样一来,登录记录、限流、异常排查都失去了意义。

站点的入口链路是这样的:

text
访客 → 阿里云服务器(frps)→ 本地服务器上的 frpc → NPM(Docker)→ 各个服务

难点在于这条线路一直在用,全站的服务都挂在上面。直接改 frpc 配置再重启,所有服务都会断开,所以这次的要求是:尽量不中断地完成切换。

为什么 IP 全是本机

frp 的 tcp 类型隧道只转发字节流。对 NPM 来说,连接是 frpc 从本机发起的,它当然只能看到本机地址。Logto 虽然开了 TRUST_PROXY_HEADER,会读 X-Forwarded-For,但 NPM 写进去的就是它看到的地址,也就是本机。

HTTPS 流量在 frp 这一层是加密的,frpc 没法往里插 HTTP 头。能用的办法只有 Proxy Protocol:frpc 在每个 TCP 连接的最前面附上一小段头部,里面写着访客的原始地址;接收方读出这段头部,再把它当作客户端地址。

前提是两端要同时改:frpc 发 Proxy Protocol,NPM 按 Proxy Protocol 接收。只改一边,连接会直接失败。这也是不能在原端口上直接改的原因。

思路:先开一条并行的新通道

不动原来的 443,另开一套新入口,验证没问题后再把公网端口切过去:

  1. NPM 在原有的 443 之外,额外监听一个只接收 Proxy Protocol 的端口 8443;
  2. frpc 新增一条带 Proxy Protocol 的隧道指向 8443,先挂在一个测试用的公网端口上;
  3. 测试通过后,把新旧两条隧道的公网端口对调,并用热重载生效。

第一步:NPM 额外监听 8443

NPM 支持自定义配置片段:/data/nginx/custom/server_proxy.conf 会被引入到每个代理主机的 server 块里。在宿主机上找到这个容器目录对应的挂载路径,新建文件:

nginx
listen 8443 ssl proxy_protocol;
set_real_ip_from 192.168.1.0/24;
set_real_ip_from 172.16.0.0/12;
real_ip_header proxy_protocol;
  • listen 8443 ssl proxy_protocol:给所有站点多加一个监听端口,证书沿用各站点自己的配置;
  • set_real_ip_from:只信任来自本机局域网和 Docker 网段的 Proxy Protocol 头,其他来源的不认;
  • real_ip_header proxy_protocol:用头部里的地址替换客户端地址。之后 NPM 照常把这个地址写进 X-Forwarded-For,后面的服务不需要任何改动。

原来的 443 完全没变,局域网直连 NPM 的请求也不受影响。先检查语法,再热重载:

bash
docker exec nginx-proxy-manager-app-1 nginx -t
docker exec nginx-proxy-manager-app-1 nginx -s reload

这里踩了个坑:我写好文件、重载完成之后,顺手把之前准备好的「撤销命令」也执行了,文件被删掉了。好在 nginx 已经重载过,内存里的配置还在,重新创建文件、再重载一次就好了。这个文件之后就是线上依赖,删掉后 NPM 下次重载时 8443 会消失,公网 HTTPS 就会全断。

容器里没有 netstat 和 ss,可以直接查 /proc/net/tcp 确认端口在监听(8443 的十六进制是 20FB,0A 表示 LISTEN):

bash
docker exec nginx-proxy-manager-app-1 sh -c "grep -i ':20FB 00000000:0000 0A' /proc/net/tcp /proc/net/tcp6"

第二步:让 frpc 支持热重载

改隧道要让 frpc 重新读取配置。systemctl restart 会断开全部隧道;frpc 自带的 frpc reload 只重建有变化的隧道,但它依赖管理接口 webServer。原配置里没开,所以这里必须重启一次。这是整个过程中唯一一次全站中断,大约 2~3 秒。

在 /etc/frp/frpc.toml 的最上面(所有 [[proxies]] 之前)加上:

toml
webServer.addr = "127.0.0.1"
webServer.port = 7400

同时加上新的测试隧道。localIP 填的是 NPM 容器的 IP,因为 8443 没有映射到宿主机,宿主机可以直接访问容器 IP:

toml
[[proxies]]
name = "web-https-pp"
type = "tcp"
localIP = "172.20.0.2"
localPort = 8443
remotePort = 8443
transport.proxyProtocolVersion = "v2"

改之前先备份,改完先校验,再重启:

bash
sudo cp /etc/frp/frpc.toml /etc/frp/frpc.toml.bak-$(date +%Y%m%d%H%M%S)
sudo frpc verify -c /etc/frp/frpc.toml
sudo systemctl restart frpc

在阿里云安全组临时放行 8443,然后从外网测试:

bash
curl -sS -o /dev/null -w '%{http_code}\n' --resolve account.0w0.online:8443:<阿里云公网IP> https://account.0w0.online:8443/api/status

返回 204,NPM 访问日志里的 Client 变成了发起请求那台机器的公网地址,说明整条链路已经通了。

frpc 日志里会时不时出现 work connection pool is full, discarding,只是连接池满了丢弃多余的预建连接,可以忽略。

第三步:对调端口,热重载切换

把旧隧道 web-https 的 remotePort 从 443 改成一个备用端口(这里用 18443),新隧道从 8443 改成 443:

toml
[[proxies]]
name = "web-https"
type = "tcp"
localIP = "192.168.1.20"
localPort = 443
remotePort = 18443

[[proxies]]
name = "web-https-pp"
type = "tcp"
localIP = "172.20.0.2"
localPort = 8443
remotePort = 443
transport.proxyProtocolVersion = "v2"

然后热重载:

bash
sudo frpc verify -c /etc/frp/frpc.toml && sudo frpc reload -c /etc/frp/frpc.toml

只有这两条 HTTPS 隧道被重建,SSH 和 80 端口的隧道完全没断。再用正常的 443 访问一次:

bash
curl -sS -o /dev/null -w '%{http_code}\n' https://account.0w0.online/api/status

返回 204,NPM 日志里记录的是真实公网 IP。切换完成。

旧隧道暂时保留在 18443,用来回滚:把两条隧道的 remotePort 对调回去,再执行一次 frpc reload,就能恢复原来的行为。

重启会不会掉

  • frpc 配置在磁盘上,systemctl is-enabled frpc 是 enabled,开机自动启动;
  • server_proxy.conf 放在 NPM 挂载到宿主机的数据目录里,容器重启甚至重建都会重新加载;
  • 开机时 frpc 可能比 Docker 先起来。这没有关系:frpc 每来一个访客才去连一次 NPM,NPM 起来之前的几秒连不上,之后就正常了。

唯一的隐患是 localIP 写死了容器 IP 172.20.0.2。查了一下,NPM 所在的 Docker 网络里只有它一个容器,所以单纯重启主机或容器,IP 都不会变。但如果以后有别的容器加入这个网络,或者网络被重建(比如 docker compose down 后再 up、升级 NPM),IP 就可能变,公网 HTTPS 会直接断开。

根治的办法是下次升级 NPM 时顺手给它加一个 8443:8443 端口映射,再把 localIP 改成宿主机的局域网地址,以后就不用再管容器 IP 了。

小结

  • frp 的 TCP 隧道会丢掉访客 IP;HTTPS 场景下只能用 Proxy Protocol 传递,而且收发两端必须同时支持;
  • 生产线路不要原地改:先开一个并行的新端口,测试通过再切换;
  • 先启用 frpc 的 webServer,以后改隧道一律用 frpc reload,只影响有变化的隧道;
  • 局域网内直连 NPM 的请求不经过 frp,日志里显示内网地址是正常的,比如产品后端调用帐号中心的机器对机器请求。

Conversation

评论区

0 条讨论

还没有评论,来做第一个发言的人吧。