为什么个人站长需要一条自己的内网穿透通道
做个人站的人迟早会遇到同一个尴尬:手上有一台跑着开发环境的笔记本、一台放在家里的旧主机、或者一台内网 NAS,你想把上面的服务开放出去给同事预览、给自己远程访问、或者临时做演示,但家里的宽带没有公网 IP,运营商给的还是大内网地址(100.64.0.0/10 段),路由器上做端口映射根本无从下手。
这时候有两条路。一条是买商业内网穿透服务,把服务的可达性交给第三方,端口、域名、流量都受限于对方的套餐;另一条是自己在一台有公网 IP 的 VPS 上起一个隧道服务端,本地机器做客户端连上去,把内网端口映射到公网服务器的某个端口上。后者就是本文要讲的 frp。
frp 是一个用 Go 写的反向代理工具,单文件、无依赖、跨平台,服务端 frps 跑在公网 VPS 上,客户端 frpc 跑在内网机器上。它的核心价值不是「能连上」,而是「连得可控」:你可以精确控制哪个内网服务暴露成哪个公网端口,可以给面板加认证,可以强制走 TLS,可以按需临时开关。对个人站长来说,它比商业服务更便宜(一台最便宜的 VPS 一年几十块),也更容易排查故障。
拓扑与端口规划:先把账算清楚
在动手之前必须先把端口规划清楚,因为 frp 的故障里有相当一部分是端口冲突或端口没放行引起的。
假设公网 VPS 的 IP 是 203.0.113.10,内网要暴露的服务是一个跑在 3000 端口的后台管理系统。一次典型的部署涉及三个端口面:
- 控制端口:frpc 连 frps 用的端口,默认 7000(新版叫 bindPort)。这个端口只需要对内网客户端开放,绝对不该对整个互联网开放。
- 面板端口:frps 的 dashboard,默认 7500。用于在浏览器里看有哪些代理在线、流量多少。同样不该裸奔在公网。
- 业务端口:frpc 在服务端注册的 remotePort,比如 6000,访问
203.0.113.10:6000就等于访问内网 3000 端口。
规划原则是:控制端口和面板端口做来源限制,业务端口按需开放。云厂商的安全组是第一道闸,VPS 本机的防火墙是第二道闸,两道都要配,只配一道是新手最常见的漏洞。
# 云安全组放行规则 7000/tcp 仅允许内网出口 IP(或先临时放开,配完再收紧) 7500/tcp 仅允许你自己的常用 IP 6000/tcp 按需对外 80/443/tcp 对外(如果用域名访问,走 Nginx 反代)
服务端部署:frps 的最小可用配置
frp 官方在 GitHub Releases 提供预编译的 tar.gz 包,也可以用一键脚本。生产环境更推荐手动下载固定版本,因为脚本装的版本会随更新变化,出问题时不好复现。以 Linux AMD64 为例:
cd /usr/local wget https://github.com/fatedier/frp/releases/download/v0.61.0/frp_0.61.0_linux_amd64.tar.gz tar -zxvf frp_0.61.0_linux_amd64.tar.gz mv frp_0.61.0_linux_amd64 frp cd /usr/local/frp cp frps.toml frps.toml.bak
新版 frp(0.52 以后)已经把配置格式从 ini 换成了 TOML,网上大量老教程还是 frps.ini,直接照抄会报错,这是第一个高频坑。一个够用的 frps.toml 长这样:
bindPort = 7000
# 只监听内网客户端来源不明,用防火墙限制
auth.method = "token"
auth.token = "换成一串足够长的随机字符串"
# 面板
webServer.addr = "127.0.0.1"
webServer.port = 7500
webServer.user = "admin"
webServer.password = "换成强密码"
# 允许客户端申请的端口范围,避免客户端随便占端口
allowPorts = [
{ start = 6000, end = 6100 }
]
# 日志
log.to = "/var/log/frps.log"
log.level = "info"
log.maxDays = 7这里有几个设计取舍值得说明。第一,面板绑定 127.0.0.1 而不是 0.0.0.0,这样即使防火墙配错,面板也只在服务器本地可见,要用的时候通过 SSH 端口转发访问,安全性高一个量级。第二,auth.method = "token" 是最基础的认证方式,稍后会讲到更强的 TLS 双向认证。第三,allowPorts 限制客户端可申请的端口范围,防止客户端配置写错占用了服务器上其他服务的端口。
用 systemd 托管 frps:别再用 nohup 裸跑
很多人用 nohup ./frps -c frps.toml & 启动,结果服务器一重启服务就没了,或者 SSH 断开被 SIGHUP 杀掉。正确做法是写一个 systemd unit:
[Unit] Description=frp server After=network.target [Service] Type=simple User=nobody Restart=on-failure RestartSec=5s ExecStart=/usr/local/frp/frps -c /usr/local/frp/frps.toml LimitNOFILE=1048576 ExecReload=/bin/kill -HUP $MAINPID [Install] WantedBy=multi-user.target
三个细节:Restart=on-failure 保证进程崩溃后自动拉起;LimitNOFILE 调大文件描述符上限,frp 的连接数一多很容易撞到默认的 1024;用 User=nobody 而不是 root,万一 frps 有漏洞也不至于直接拿到 root。存到 /etc/systemd/system/frps.service 之后 systemctl daemon-reload && systemctl enable --now frps,然后 systemctl status frps 确认 active (running) 即可。
客户端配置:把内网服务映射出去
内网机器上安装同样的 frp 包,配置 frpc.toml:
serverAddr = "203.0.113.10" serverPort = 7000 auth.method = "token" auth.token = "与服务端完全一致" # 传输层加密与压缩 transport.tls.enable = true [[proxies]] name = "dev-admin" type = "tcp" localIP = "127.0.0.1" localPort = 3000 remotePort = 6000
name 必须在同一服务端内唯一,重名会导致后连接的客户端注册失败。transport.tls.enable = true 让 frpc 与 frps 之间的隧道走 TLS,防止 token 和数据在公网以明文传输——这一步很多人省掉,等于把内网服务的访问凭证直接暴露在链路上。
进阶:用域名 + Nginx 访问,而不是裸 IP 加端口
IP 加端口的方式不好记也难做 HTTPS。更规范的做法是让 frpc 注册 HTTP 类型代理,再由服务端 Nginx 按域名分发:
[[proxies]] name = "web-admin" type = "http" localPort = 3000 customDomains = ["dev.example.com"] # 服务端 frps.toml 需要加 # vhostHTTPPort = 8080
此时 frps 会在 8080 端口接收 HTTP 请求并按 Host 头转发到对应的 frpc 隧道。Nginx 侧只需把 dev.example.com 反向代理到 127.0.0.1:8080,证书用 certbot 正常签发即可。这样对外暴露的只有 80/443,内网服务的端口号完全不外露。
这里有个反复踩的坑:Nginx 反代到 frps 的 vhost 端口时,必须把原始 Host 头透传过去,否则 frps 无法判断该转发给哪个客户端。配置里要保留 proxy_set_header Host $host;,不能写成 proxy_set_header Host 127.0.0.1;。
安全加固:三步把暴露面收干净
内网穿透本质上是把内网服务搬到了公网上,安全配置一旦松懈,等于给攻击者开了一扇直达内网的门。三个必须做的事:
第一步,给 frps 加上 TLS 强制。在 frps.toml 中配置 transport.tls.force = true,只接受走 TLS 的客户端连接。否则攻击者可以用匿名 TLS 关闭的方式尝试连接,或者伪造客户端注册代理。
第二步,把面板藏起来。面板绑定 127.0.0.1 之后,本地访问用 ssh -L 7500:127.0.0.1:7500 root@203.0.113.10 建立隧道,浏览器打开 http://127.0.0.1:7500 即可。这样面板的认证信息永远不会出现在公网流量里。
第三步,内网服务自身也要有认证。frp 只负责「让请求到达」,不负责鉴权。把内网后台直接映射出去而不加登录,任何扫到端口的人都能操作你的系统。最省事的做法是在隧道前面套一层 Nginx basic auth,或者给内网服务本身加上强口令。
故障排查:四个最常见的现象与判读
现象一:frpc 日志报 login to server failed: authorization failed。几乎总是 token 不一致,检查两端 auth.token 是否逐字符相同,注意配置文件里的引号和空格。如果 token 里有特殊字符,务必用双引号包起来。
现象二:连接成功但访问公网端口不通。按顺序排查三层:服务端 ss -lntp | grep 6000 看 frps 是否真的监听了该端口(没有则说明 remotePort 被 allowPorts 限制或客户端注册失败);服务端防火墙和云安全组是否放行 6000;从服务端本地 curl 127.0.0.1:6000 验证隧道本身是否通。本地通公网不通,一定是防火墙问题。
现象三:客户端偶尔断开,重连很慢。多半是网络抖动或运营商 NAT 超时。配置里加上 transport.heartbeatInterval 和 transport.heartbeatTimeout,让心跳更频繁地维持连接映射;同时确认 frpc 也是由 systemd 托管,崩溃后能自动重启。
现象四:frps 占用内存持续增长。检查是否有大量短连接没有正常关闭,用面板看各代理的当前连接数。必要时开启 transport.maxPoolCount 限制连接池,并注意 LimitNOFILE 是否已调大。
不适合用 frp 的场景
最后说点反面的:frp 不是万能。如果你要暴露的是全站生产流量、有高并发需求、或者希望有多节点容灾,那正确方案是备案域名加正经的负载均衡或 CDN 回源,而不是让所有流量绕一个单点 frps。frp 的定位是「临时、可控、低流量」的内网服务可达性方案。把它用在开发预览、远程调试、临时演示这些场景上最合适;一旦变成生产入口,就要重新评估它的单点风险和带宽瓶颈。
另外,暴露内网服务前请确认合规性:单位网络、公司内网通常有明确的外联管理规定,把内网端口映射到公网可能违反安全制度。技术可行不等于制度允许,这一点对做技术的人尤其要自觉。