前文 AMD64 Linux 部署 FRP v0.65 内网穿透 已经介绍了安装的背景
但是使用过程中发现一个现象:
无论查看应用日志还是简单打印请求来源,所有访问看起来都是由 127.0.0.1 发起的
经过查询了解到,这并不是配置错误,而是 FRP 在默认工作模式下的正常行为
在当前的访问链路中,外部请求经过 FRPS 转发后,最终是由 FRPC 在本地建立连接并访问后端服务
对于被访问的服务而言,请求的直接来源就是 FRPC 本身,因此看到的源地址自然是 127.0.0.1。
根据 官方文档 说明,如果希望在使用 FRP 的场景下获取真实的客户端 IP,需要额外启用相关机制
例如通过 HTTP 场景下的 X-Forwarded-For,或使用更通用的 Proxy Protocol
针对没有备案的域名,但是又需要获取真实用户 IP 的情况,一番折腾了是少不了的了,于是乎一篇文章又双叒叕的应运而生
一、环境说明
| IP 地址及端口 | 作用与需求 | |
|---|---|---|
| FRPS 公网服务器 | 服务器ip 假设为1.1.1.1 | 接收公网请求并转发给 FRPC |
| 宿主机 FRPC | 10.211.55.2 | 连接 FRPS 并转发公网请求 启用 Proxy Protocol v2 |
| 宿主机 Spring Boot 服务 | 10.211.55.2:50000 | 提供业务 API 读取 X-Real-IP 获取真实 IP |
| 虚拟机 Nginx 服务需要,也可以使用 Docker | 10.211.55.100 | 运行 Nginx解析 Proxy Protocol 注入 X-Real-IP |
二、修改配置文件 frpc.toml
针对单端口内网穿透最简配置文件
serverAddr = "1.1.1.1"serverPort = 7000
[[proxies]]name = "my_local_server"type = "tcp"# 虚拟机上 Nginx 的 IPlocalIP = "10.211.55.100"# 虚拟机上 Nginx 的 HTTP 监听端口localPort = 8080# 公网暴露端口remotePort = 20002# 启用 Proxy Protocol v2transport.proxyProtocolVersion = "v2"手动启动 FRPC ,确保 FRPC 所在主机能访问 10.211.55.100:8080
./frpc -c frpc.toml日志中出现
[my_local_server] start proxy success即表示访问成功
三、Nginx配置
创建一个独立的配置文件,例如/etc/nginx/conf.d/web-proxy.conf,并填写以下配置
server { # 启用 Proxy Protocol v1/v2 接收(由 FRPC 发送) listen 8080 proxy_protocol;
# 严格限制 set_real_ip_from :仅来自 FRPC 所在主机的 Proxy Protocol 数据 # 替换为 FRPC 实际 IP(如 10.211.55.2) set_real_ip_from HOST_IP; # 从 Proxy Protocol 中提取真实客户端 IP real_ip_header proxy_protocol;
location / { # 透传协议信息 proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme;
# 传递真实客户端 IP(由 Proxy Protocol 解析后赋值给 $remote_addr) proxy_set_header X-Real-IP $remote_addr;
# 指定后端 Host(通常设为目标服务 IP 或保留原始 Host) # 或改为 $http_host 以保留原始 Host proxy_set_header Host 10.211.55.2;
# 超时设置(设置为0会导致响应504,建议设合理值) proxy_connect_timeout 60s; proxy_send_timeout 600s; proxy_read_timeout 600s;
# 转发到宿主机上的 Spring Boot 服务 proxy_pass http://10.211.55.2:50000; }}重载 Nginx配置
nginx -s reload虚拟机内执行
ss -tuln | grep :8080应显示监听状态
四、宿主机 Spring Boot 服务获取真实 IP 的代码示例
private static String getClientIpAddress(HttpServletRequest request) { // 1. 优先检查是否是 Cloudflare 代理 String cfIp = request.getHeader("CF-Connecting-IP"); if (cfIp != null && !cfIp.isEmpty() && !"unknown".equalsIgnoreCase(cfIp)) { return cfIp; }
// 2. 回退到标准代理头 String[] headers = { "X-Forwarded-For", "X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP" };
// 3. 遍历标准代理头获取 for (String header : headers) { String ip = request.getHeader(header); if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) { if ("X-Forwarded-For".equals(header)) { String[] ips = ip.split(","); for (String ipStr : ips) { ipStr = ipStr.trim(); if (!ipStr.isEmpty() && !"unknown".equalsIgnoreCase(ipStr)) { return ipStr; } } } else { return ip; } } }
// 4. 最终回退:直接连接的 IP (一般为 127.0.0.1 或 ::1) return request.getRemoteAddr(); }五、FRPS - FRPC - Nginx - Spring Boot 代理的全流程
-
用户访问
http://1.1.1.1:20002 -
FRPS 接收客户端连接,并通过 frp 隧道将该连接的数据转发给 FRPC
-
FRPC 与本地服务建立连接后会发送 Proxy Protocol 头
-
虚拟机 Nginx:
- 通过
listen ... proxy_protocol解析出真实 IP - 将
$remote_addr设为该真实 IP - 通过
proxy_set_header X-Real-IP $remote_addr传递给后端
- 通过
-
请求被反向代理至宿主机
10.211.55.2:50000 -
Spring Boot 通过解析标准代理头获取用户真实公网 IP
验证方法
1. 检查 Nginx 日志
tail -f /var/log/nginx/web-proxy_access.log→ $remote_addr 应为用户公网 IP,而非 10.211.55.2
2. 调用测试接口
访问 http://服务器ip:20002/test-ip,返回应为你的公网 IP
3. 通过 Spring Boot 日志打印 X-Real-IP / X-Forwarded-For / getRemoteAddr() 进行验证
六、常见问题排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
Nginx 日志中 $remote_addr 是 10.211.55.2 | Proxy Protocol 未生效 | 1. 确认 FRPC 配置 proxyProtocolVersion = "v2"2. 确认 Nginx listen 含 proxy_protocol |
| 连接被拒绝或超时 | 端口冲突或防火墙拦截 | 换用空闲端口(如 20002),检查 iptables/安全组 |
Spring Boot 拿不到 X-Real-IP | Nginx 未设置该 Header | 检查 proxy_set_header X-Real-IP $remote_addr; |
| 跨域错误 | 前端 Origin 未被允许 | 使用 allowedOriginPatterns 配置通配规则 |
七、无法使用ws长连接原因以及解决方法
在使用的过程中突然发现 WS 长连接的请求都是不成功的,但是普通 HTTP 请求都是没有问题的
修改 FRPC 直接转发不使用 Nginx ,发现 WS 的请求是没有问题的,所以问题根源还是在 Nginx 配置上
翻了些资料才搞明白,默认情况下 Nginx 反向代理会用 HTTP/1.0 协议转发请求,但 WS 长连接的建立根本离不开 HTTP/1.1 的“协议升级机制”。
HTTP/1.0 压根不支持这种升级操作,后端收不到升级信号,自然没法建立 WS 连接,所以 WS 的请求是一直没办法成功的
另外,WS 握手还有两个缺一不可的核心请求头:Upgrade: websocket 和 Connection: Upgrade。
作用是告诉后端“我要从普通 HTTP 切换到 WS 长连接了”,但如果 Nginx 配置里没明确说要透传这两个头,它就会直接把这俩关键信息丢掉。
后端没收到信号,就按普通 HTTP 请求处理,握手自然失败,WS 连接也就建不起来。
知道了原因,解决起来就简单了,核心就是补全 Nginx 配置里缺失的部分:
首先,在配置文件的 server 块上方加动态适配规则,让 Nginx 能自动区分普通 HTTP 和 WS 请求:
map $http_upgrade $connection_upgrade { default keep-alive; websocket upgrade;}然后在 server 块的 location 块里,补充这几行关键配置:
# 强制使用 HTTP/1.1,支持 WS 升级机制proxy_http_version 1.1;# 透传 WS 升级头,不丢失握手信号proxy_set_header Upgrade $http_upgrade;proxy_set_header Connection $connection_upgrade;# 拉长超时时间,避免 60 秒无数据自动断开proxy_read_timeout 86400s;proxy_send_timeout 86400s;改完这些配置重启 Nginx 再试,WS 长连接就能稳定建立了
非商业用途可以使用,但必须注明出处;
若有改编需采用相同许可协议发布。