FRP 内网穿透踩坑记:一个 IPv4 地址引发的血案
用 FRP 把本地服务穿透到公网,一切配置看起来都对,但页面就是 502。排查三小时,根因竟是 IPv4 和 IPv6 的一字之差。
🕵️ FRP 内网穿透踩坑记:一个 IPv4 地址引发的血案
我有一台云服务器(公网)和一台本地开发机。本地跑着博客和一个 Web 应用,通过 FRP 内网穿透暴露到公网。
今天把其中一个服务从 FRP vhost 模式迁移到独立端口模式,结果捅了马蜂窝——三个小时排查,最终发现根因是 一个 IPv4 地址。
需求:vhost → 独立端口
之前某个服务用 FRP 的 HTTP vhost 模式,多个域名共享 frps 的同一端口,按 Host 头路由。但我想给它一个独立端口,更干净。
配置改起来很简单:
# 之前(vhost 模式)
[[proxies]]
name = "my-app"
type = "http"
localPort = 8080
customDomains = ["app.example.com"]
# 之后(独立端口模式)
[[proxies]]
name = "my-app"
type = "tcp"
localPort = 8080
remotePort = 9000
云服务器上的 Nginx 对应改一下 proxy_pass:
# 之前
proxy_pass http://127.0.0.1:8080;
# 之后
proxy_pass http://127.0.0.1:9000;
看起来三分钟的事。结果……三小时。
坑 1:proxy already exists
改完重启 frpc,报错:
[my-app] start error: proxy [my-app] already exists
谁已经注册了?查进程,发现本地同时跑了 两个 frpc:
12345 frpc -c /path/to/frpc.toml
12356 frpc -c /path/to/frpc.toml
同一个配置文件,两个进程。杀一个,另一个还在。杀掉所有,新的又冒出来。
罪魁祸首:macOS LaunchAgents。plist 配置了 frpc 开机自启,每次进程被杀就自动重启。
解法:操作 frpc 前先 launchctl unload,操作完再 load。
但这还没完——还有第三个 frpc 进程来自博客的配置文件,里面也注册了同名的代理(旧的 vhost 模式),跟主配置冲突。
解法:从博客的 frpc 配置里删掉这个代理,只保留博客自己的。
坑 2:502 Bad Gateway
代理冲突解决了,frpc 注册成功,但访问页面返回 502 Bad Gateway。
Nginx 配置没问题,frps 端口在监听,本地 Next.js 也在跑着。但就是不通。
SSH 到云服务器,从内部 curl:
curl -sv http://127.0.0.1:9000/
# Connected to 127.0.0.1:9000...
# Operation timed out with 0 bytes received
TCP 连接能建立,但收不到任何数据。这说明 frps 收到请求后转发给 frpc 了,但 frpc 连不上本地服务。
坑 3:IPv4 vs IPv6(真正的大坑)
查看本地端口的监听情况:
lsof -i :8080
# node 12345 user 12u IPv6 ... TCP *:8080 (LISTEN)
注意那个 IPv6。Node.js(Next.js、Astro 都是)默认只监听 IPv6 的 ::,不监听 IPv4 的 0.0.0.0。
而 frpc 的 localIP 默认值是 127.0.0.1——一个 IPv4 地址。
于是发生了最诡异的一幕:
frpc 通过 IPv4 (127.0.0.1:8080) 连接本地
→ TCP 连接建立成功(IPv6 双栈兼容)
→ HTTP 请求发出去
→ 无人应答(因为 Node.js 只在 IPv6 上监听)
→ 超时
连接看起来是通的,实际上数据根本送不到应用层。这种半通不通的状态比完全不通更难排查。
解法:frpc 配置里显式指定 IPv6 地址:
[[proxies]]
name = "my-app"
type = "tcp"
localIP = "::1" # ← 关键!
localPort = 8080
remotePort = 9000
改完后重启 frpc,立刻 200 OK。
博客也是同样的问题——Astro Preview 也只监听 IPv6。frpc 配置里也要改 localIP = "::1"。
完整的数据流
修好之后,请求链路是这样的:
用户访问 https://app.example.com
→ DNS 解析到云服务器
→ Nginx 443 SSL 终止
→ proxy_pass 到 frps 端口
→ frps 转发给本地 frpc
→ frpc 通过 IPv6 (::1) 连接本地 Node.js
→ 返回页面 ✅
经验总结
1. FRP localIP 不要用默认值
Node.js 生态的服务(Next.js、Astro、Vite 等)默认只监听 IPv6。frpc 的 localIP 默认是 127.0.0.1(IPv4),必须显式改成 ::1。
这个坑的可怕之处在于:TCP 连接能建立,但应用层无响应。你不会看到”connection refused”,只会看到超时或 502。很容易误判为 frps 或 Nginx 的问题。
2. 多个 frpc 配置文件不要注册同名代理
一台机器上跑多个 frpc 实例(比如博客一个、其他服务一个)时,千万别在两个配置里注册同名的代理。frps 不允许同一个代理名被注册两次,后来的会被拒绝。
3. macOS LaunchAgents 会自动重启进程
杀进程前先 launchctl unload,否则你杀一个它生一个,永无止境。
4. 排查 FRP 问题的正确顺序
- frpc 日志 — 代理是否注册成功
- frps 端口监听 —
ss -tlnp | grep PORT - 从服务器内部 curl — 区分 frps 问题还是 frpc 问题
- 本地直接 curl — 区分服务问题还是 frpc 问题
lsof -i :PORT— 看 IPv4 还是 IPv6
后记
这个 IPv6 坑其实之前在博客上线时就踩过一次——frps 的 vhost 端口监听 IPv6,Nginx 用 IPv4 连不上。当时记了笔记,但只记了”frps 那边”的解法,没想到 frpc 这边也有同样的问题。
所以:踩过的坑一定要完整记录,包括原理,而不仅仅是解法。 否则下次换个场景遇到同一类问题,还是会再踩一遍。
🦀