内网穿透实战:用 frp 把本地服务暴露到公网
记录一次从零开始部署 frp 的完整过程,包括原理理解、架构设计、踩坑排查,以及最终跑通的全流程。
背景
我有一台内网服务器,上面跑着一些本地服务(监听在 8900 端口)。同时我有一台云服务器,有公网 IP,上面已经用 Caddy 跑了一堆服务。
需求很简单:让公网能访问到我内网服务器上的服务。
方案:frp(Fast Reverse Proxy)—— 一个专注于内网穿透的反向代理工具。
frp 是什么
frp 由两个组件构成:
- frps(server):部署在有公网 IP 的服务器上
- frpc(client):部署在内网机器上
它的工作原理:
- frps 在公网服务器上启动,监听一个"控制端口"(默认 7000),等待客户端连接
- frpc 在内网机器上启动,主动向 frps 发起连接,建立一条长连接隧道
- 当外部用户访问公网服务器上的某个映射端口时,frps 通过隧道把流量转发给 frpc,frpc 再转发到本地对应的服务
关键点:控制连接只有一条,但可以在上面跑多条隧道。
用户请求 → 公网IP:映射端口 → frps → 隧道 → frpc → 本地服务端口
架构决策
方案一:frp + Caddy 联动(未采用,但值得了解)
这个方案的核心思路是:frp 映射的端口不对外暴露,藏在 127.0.0.1 后面,让 Caddy 统一对外提供 HTTPS 访问。
完整链路:
内网服务A (3000) ←→ frpc ←→ frps → 127.0.0.1:8081 ←→ Caddy (a.example.com:443)
内网服务B (8080) ←→ frpc ←→ frps → 127.0.0.1:8082 ←→ Caddy (b.example.com:443)
内网服务C (5000) ←→ frpc ←→ frps → 127.0.0.1:8083 ←→ Caddy (c.example.com:443)
具体做法:
frpc.toml(内网机器):
[[proxies]]
name = "web1"
type = "tcp"
localIP = "127.0.0.1"
localPort = 3000
remotePort = 8081
[[proxies]]
name = "web2"
type = "tcp"
localIP = "127.0.0.1"
localPort = 8080
remotePort = 8082
这里 remotePort 映射到云服务器的本地端口(8081、8082),因为 frps 默认监听 0.0.0.0,外部其实也能访问到。如果想严格限制只让本机访问,可以在 frps 配置中限制,或者用防火墙只放行 443 不放行 8081/8082。
Caddyfile(云服务器):
a.example.com {
reverse_proxy 127.0.0.1:8081
}
b.example.com {
reverse_proxy 127.0.0.1:8082
}
Caddy 自动申请 HTTPS 证书,用户通过域名访问,流量经过 Caddy → frps 本地端口 → 隧道 → 内网服务。
这个方案的优势:
- 公网只暴露 443(Caddy)和 7000(frp 控制端口),攻击面小
- HTTPS + 域名访问,体验好,证书自动续期
- 多个内网服务共用一个 443 端口,按域名分流
- 不需要记 IP + 端口号
这个方案的代价:
- frp 和 Caddy 产生了耦合,frp 挂了会影响 Caddy 反代的那些站点
- 需要有域名,并且 DNS 解析到云服务器
- 每加一个内网服务,Caddy 配置也要跟着改
- 非 HTTP 服务(SSH、数据库)没法走这条路,还是得开端口
最终没采用的原因:
我的云服务器上 Caddy 已经稳定跑了一堆服务,不想因为 frp 的不确定性影响到主服务。而且目前只是自用,IP + 端口访问完全够用,没必要引入额外复杂性。
但如果你的场景是:需要给团队或外部用户提供访问、希望有 HTTPS 和域名、追求更好的安全性,那这个方案是更优雅的选择。
方案二:直接端口映射(采用)
最简单的方案:
用户 → 公网IP:8900 → frps → frpc → 内网 127.0.0.1:8900
公网直接开端口,IP + 端口访问,Caddy 完全不动。简单粗暴,互不干扰。
端口规划
整个方案涉及的端口:
| 端口 | 位置 | 用途 |
|---|---|---|
| 7000 | 云服务器 | frp 控制端口,frpc 和 frps 之间的通信通道 |
| 8900 | 云服务器 | 对外暴露的转发端口,外部用户通过这个端口访问 |
| 8900 | 内网机器 | 本地实际运行服务的端口 |
云服务器需要在安全组/防火墙放行 7000 和 8900。
部署过程
第一步:下载 frp
去 GitHub Releases 下载对应平台的包。我用的是 v0.61.1:
# 查看系统架构
uname -m
# x86_64 → 下载 amd64 版本
# aarch64 → 下载 arm64 版本
cd /tmp
wget https://github.com/fatedier/frp/releases/download/v0.61.1/frp_0.61.1_linux_amd64.tar.gz
tar xzf frp_0.61.1_linux_amd64.tar.gz
解压后里面有 frps 和 frpc 两个二进制文件,分别放到对应的机器上。
第二步:生成认证 Token
frp 支持 token 认证,防止别人连你的 frps。用 openssl 生成一个强密码:
openssl rand -base64 32
# 输出类似:xXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx=
两边配置文件里的 token 必须完全一致。
第三步:云服务器部署 frps
配置文件 /etc/frp/frps.toml:
bindPort = 7000
auth.token = "<你生成的token>"
就两行,简洁到离谱。
创建 systemd 服务 /etc/systemd/system/frps.service:
[Unit]
Description=frps service
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/frps -c /etc/frp/frps.toml
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
启动:
systemctl daemon-reload
systemctl enable frps
systemctl start frps
验证:
# 确认 7000 端口在监听
ss -tlnp | grep 7000
第四步:内网机器部署 frpc
配置文件 frpc.toml:
serverAddr = "<云服务器公网IP>"
serverPort = 7000
auth.token = "<你生成的token>"
loginFailExit = false
[[proxies]]
name = "port-8900"
type = "tcp"
localIP = "127.0.0.1"
localPort = 8900
remotePort = 8900
配置解读:
serverAddr/serverPort:frps 的地址和控制端口auth.token:认证密码,必须和 frps 一致loginFailExit = false:连接失败时持续重试,不退出[[proxies]]:定义一条隧道name:代理名称,全局唯一type:协议类型,tcp/udplocalIP/localPort:本地服务地址remotePort:云服务器上对外暴露的端口
启动:
./frpc -c frpc.toml
看到以下日志说明成功:
login to server success
[port-8900] start proxy success
踩坑记录
坑1:旧版 frpc 占用代理名
启动新 frpc 后报错:
[port-8900] start error: proxy [port-8900] already exists
原因:本地之前有一个旧版 frpc(v0.58.1)以 systemd 服务的形式在后台运行,已经注册了同名的 proxy。
排查:
ps aux | grep frpc | grep -v grep
发现一个 root 权限的旧进程从几个月前就在跑。
解决:
# 停掉旧服务
sudo systemctl stop frpc
sudo systemctl disable frpc
# 清理旧文件
sudo rm -f /etc/systemd/system/frpc.service
sudo rm -f /usr/local/bin/frpc
sudo rm -rf /etc/frp
sudo systemctl daemon-reload
# 杀掉残留进程
sudo pkill -f frpc
坑2:Token 不匹配
token in login doesn't match token from configuration
原因:云端 frps 还在用旧 token,客户端用了新 token。
解决:确保两边 auth.token 完全一致,云端更新配置后重启 frps。
坑3:frpc 启动后命令卡住
用 nohup ./frpc -c frpc.toml & 启动时,如果 frps 还没就绪,frpc 会一直重试连接,导致 shell 看起来"卡住"。
解决:确保云端 frps 先启动好,再启动 frpc。或者用 systemd 管理 frpc,就不存在这个问题。
多端口映射
frp 支持在一个配置文件里定义多条隧道:
[[proxies]]
name = "web"
type = "tcp"
localIP = "127.0.0.1"
localPort = 8900
remotePort = 8900
[[proxies]]
name = "api"
type = "tcp"
localIP = "127.0.0.1"
localPort = 8901
remotePort = 8901
[[proxies]]
name = "debug"
type = "tcp"
localIP = "127.0.0.1"
localPort = 9000
remotePort = 9000
加完重启 frpc 就生效。云端 frps 不需要任何修改,只需要确保新端口在安全组/防火墙放行。
多台内网机器
一个 frps 可以同时接多个 frpc 客户端。第二台内网机器只需要:
- 用相同的
serverAddr、serverPort、auth.token连接 - 确保 proxy 的
name和remotePort不与其他客户端冲突
serverAddr = "<云服务器公网IP>"
serverPort = 7000
auth.token = "<token>"
loginFailExit = false
[[proxies]]
name = "machine2-web"
type = "tcp"
localIP = "127.0.0.1"
localPort = 8080
remotePort = 9080
云端 frps 依然不用动。
核心理解
整个 frp 的设计哲学可以总结为:
服务端是"傻管道",客户端是"大脑"。
frps 启动后就两件事:
- 监听控制端口,等客户端来连
- 客户端说要什么端口,它就开什么端口
所有的变更——加端口、删端口、加机器、改映射——全部在客户端完成。服务端一次部署,基本再也不用碰。
frpc 配置文件的结构也很清晰:
# === 连接凭证(核心,有这四行就能连上 frps)===
serverAddr = "<IP>"
serverPort = 7000
auth.token = "<token>"
loginFailExit = false
# === 隧道声明(想映射几个写几个)===
[[proxies]]
name = "xxx"
type = "tcp"
localIP = "127.0.0.1"
localPort = <本地端口>
remotePort = <公网端口>
上半部分是"我要连谁",下半部分是"我要映射什么"。
安全注意事项
- Token 要强:用
openssl rand -base64 32生成,不要用弱密码 - Token 要保密:泄露了别人就能往你服务器上开端口
- 按需开放端口:映射了就等于公网可达,不需要的别映射
- 敏感服务加白名单:SSH、数据库等建议配合防火墙做 IP 限制
- 版本统一:frps 和 frpc 尽量用同一版本,避免兼容性问题
最终文件清单
| 文件 | 位置 | 说明 |
|---|---|---|
| frps | 云服务器 /usr/local/bin/frps | 服务端二进制 |
| frps.toml | 云服务器 /etc/frp/frps.toml | 服务端配置 |
| frps.service | 云服务器 /etc/systemd/system/frps.service | 服务端 systemd 服务 |
| frpc | 内网机器 ~/frp/frpc | 客户端二进制 |
| frpc.toml | 内网机器 ~/frp/frpc.toml | 客户端配置 |
总结
frp 是我用过的最优雅的内网穿透工具。部署简单、配置直观、扩展方便。一个 frps 实例就能服务多台内网机器的多个端口,所有变更都在客户端搞定,服务端一劳永逸。
如果你也有"内网服务想让公网访问"的需求,frp 绝对是首选。
陕公网安备61011302002223号