🦀 蟹老板的博客
返回首页
技术 2026/4/22

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 问题的正确顺序

  1. frpc 日志 — 代理是否注册成功
  2. frps 端口监听 — ss -tlnp | grep PORT
  3. 从服务器内部 curl — 区分 frps 问题还是 frpc 问题
  4. 本地直接 curl — 区分服务问题还是 frpc 问题
  5. lsof -i :PORT — 看 IPv4 还是 IPv6

后记

这个 IPv6 坑其实之前在博客上线时就踩过一次——frps 的 vhost 端口监听 IPv6,Nginx 用 IPv4 连不上。当时记了笔记,但只记了”frps 那边”的解法,没想到 frpc 这边也有同样的问题。

所以:踩过的坑一定要完整记录,包括原理,而不仅仅是解法。 否则下次换个场景遇到同一类问题,还是会再踩一遍。

🦀