先给你描述一个极其常见的、几乎每个程序员家里都发生过的场景:你在家或者在公司内网里有一台 Linux 机器,上面跑着很多好东西——代码仓库、数据库、自己搭的 Web 服务、别人托你管的服务器。你白天在公司、晚上在地铁上,突然想远程登录这台机器,想敲几个命令进去看看。你下意识地在另一台联网的笔记本上敲下 ssh,连的却是自己内网那群机器群里的某台?连不上。你换了宿舍的网、换了手机热点,全都连不上。

为什么连不上?因为这台机器在内网里,它没有公网 IP,而公网根本"看不见"它。这就是我们今天这一整节要啃的一块硬骨头,而解锁它的钥匙叫内网穿透(英文常叫 NAT traversal 或内网隧道)。

这节课我们不走理论空谈,直接动手:我会带你用 frp 这个工具,手动部署并测试一条完整的内网穿透链路。等这一节结束,你会真切地理解四条"为什么"——为什么内网机器公网连不上、为什么 frp 能解决、frp 自身怎么搭起来、以及怎么验证它真的通了。而博文的最后,我还埋了几个带详解答案的思考题,读完后可以自查一遍。

先把概念掰开:什么是内网穿透、公网可达、映射

在我们动手之前,先花一段把几个词讲扎实,否则后面会越看越糊涂。别急着跳进命令,地基打牢了,命令才有意义。

先讲公网可达。这个词看起来……好像有点正式,但其实意思非常朴素:一台服务,只要能让互联网上任何一个普通用户,随时用公开的 IP 和端口敲进它、拿到响应,我们就说它是"公网可达"(publicly reachable)的。打个比方,一家开在街边的店铺,只要路人推门就能进来买东西,它就叫"街上可达";而一家开在居民楼里的黑作坊,外人连楼门都进不去,它就不"街上可达"。公网可达的关键前提是机器有一个唯一的公网 IP,并且这个 IP 上对应的端口开着。internet 上随便一台电脑,只要知道这个 IP 加端口,就能直连到它。

那内网穿透是干嘛的?一句话:让一台"公网可达 = 否"的内网机器,通过某种方法,变得可以让公网用户访问到它上面的服务。注意这里有个关键区别——frp 并不是真的给内网机器分配一个公网 IP(路由器上的网络地址转换,英文 NAT,Network Address Translation,让一大群内网设备共用一个公网出口 IP,公网根本分不清是谁),而是"借道"。它把内网的服务,通过一台有公网 IP 的中转机器,当作"对外窗口"透传出去。在 frp 的语境里,"穿透"不是真的凿开 NAT 防火墙,而是绕开它。

再讲映射(也常叫端口映射,port mapping / port forwarding)。这是穿透的灵魂操作。想象两台机器的端口之间建立了一条"专用通道":公网服务器上的一个端口,和你的内网机器上的一个端口,被对接了起来。公网上的人敲服务器的 8081 端口,数据会被原封不动地"搬"到内网机器的 22 端口上。这个"把一个端口接驳到另一个端口"的动作,就叫映射。frp 配置里那几个让人眼花缭乱的 localPort、remotePort 字段,本质上全是在描述这种映射关系。记住:端口是服务的"门牌号",映射就是给内网服务在公网上另装了一扇门,门背后连着的还是原来那扇门。

难在哪:运营商 NAT、没有公网 IP,到底拦在哪一步

"内网连不上"这句话,背后的敌人到底是什么?很多人以为是"我的路由器/NAT 挡住了"。这话对了一半,但更精确的定位是这样两层:

第一层是你没独立的公网 IP。运营商的出口带宽是共享的,为了省 IP,绝大多数家庭和企业宽带,大家共用一个公网 IP。运营商不会给每个家庭都发一个独立 IP。有些地方,你觉得装宽带时"有公网 IP",其实是运营商级 NAT(Carrier-Grade NAT,简称 CGNAT)——运营商在最上游再叠了一层 NAT,你拿到的那个"公网 IP",其实是运营商内部网络里的一个私有地址(比如 100.64.x.x 这类地址段)。这意味着就算你咖喱咖喱地在家里的路由器上做端口映射、做了 DDNS(动态域名,把动态变化的公网 IP 映射到一个固定域名上),你映射到的那个 IP 可能根本不是真正属于你的公网地址。很多教程让你"去路由器上做个端口转发就能远程了",但因为你根本没有独立的公网 IP,这条路打一开始就断了。

第二层是就算你有公网 IP,NAT 也天然只放行"由内向外"的连接。我们的家用路由器默认有一种"记性":它记下内网机器往外发起的连接(谁、从哪个端口出去、要去哪),回来的数据它认得,放行;但一个从外面主动发起、想直接闯进内网某台机器的连接,它既不认识,也默认拦下。就好比一栋只有门卫守着的小区,门卫认识楼里住户刚叫的外卖员可以进;但一个陌生人(公网用户)想直接闯进某户人家,门卫会拦住不让进。

这两层叠加起来,结论就一个字:内网服务对公网来说,是"隐形"的。

正面对决的方案:反向连接、打洞、借一台公网服务器

既然公网进不来内网,那就反过来想:内网"自己主动出去"总可以吧? NAT 对"里面往外面发的连接"是完全放行的。这就是内网穿透的基石——反向连接。

反向连接(reverse connection)这个词,要重点划一下。它核心就一句话:连接的方向颠倒过来,由内网的机器主动去连公网的机器。你平时理解的"连接",都是客户端去连服务器;这里恰恰相反,让"被访问方"(内网机器)充当发起连接的那一方,主动去连一台公网上的机器,并且把这条连接保持住、保活。因为这条连接是"从里往外"建立的,NAT 的门卫会老老实实地放行。之后,公网用户要访问内网服务,根本不用去"闯 NATA",只需要去连那台公网机器,公网机器再顺着这条它和内网之间"现成的、还活着"的通道,把流量递进内网。反向连接的精华,就是"我不去敲门,我先把门里的线拉出来"。

那打洞呢?打洞(NAT hole punching,也叫 UDP 打洞/打洞连接)是另一条流派。它不借助中转机器转发数据,而是尝试让内网机器和公网用户直接建立点对点连接(P2P)。原理是:两台机器先都主动连到一个公共的"信令服务器"上,各自借机在各自 NAT 里"凿"出一个可转发的洞(让 NAT 记住"我要和谁通信");然后双方互换各自的公网地址,之后直接互连,数据不再经过中转。打洞省带宽、省延迟,但它成功率受 NAT 类型影响,不是每次都能成功。frp 也支持一种思路近似的 xtcp 代理类型来尝试 P2P。不过这节课我们做的是稳的、一定通的方案,先不碰打洞的玄学。

于是我们选定的主方案就很清楚了:准备一台有公网 IP 的小服务器,让内网机器主动反向连到它上面,借它当中转站,把内网的端口映射到它的公网端口上。 这台中转机,是整条链路的中枢,也是内网机器在公网上的"代言人"。

认识主角 frp 和它的架构

上面这套思路,市面上有现成的工具帮我们落地,我们选 frp。frp 全名 Fast Reverse Proxy,翻译过来就是"快速反向代理",作者是 fatedier,一个用 Go 语言写的开源工具,也是国内团队里非常流行的内网穿透神器。它的仓库在 GitHub 上,叫 fatedier/frp。

frp 的架构是经典的 C/S(客户端/服务端)模型,一共两个组成部分,你最好一开始就分清楚谁是公网、谁是内网:

  • frps(s 是 server 服务端):装在有公网 IP 的那台中转服务器上。它负责"开门"——监听一个端口,等着内网机器来反向连它;同时它也负责把公网用户的访问流量,转交到内网去。
  • frpc(c 是 client 客户端):装在你想穿透的那台内网机器上。它负责"主动出击"——主动去连公网上的 frps,把自己本地某个端口"登记"给 frps,让 frps 对外开放对应的端口。

你可以把 frps 理解成一个"总机",frpc 是无数个"分机"。每个分机拨通总机后告诉它:"我这儿 22 号分线有台 ssh 服务,你帮我对外接一根 8081 的线进来。" 于是外界的电话打到总机 8081,总机就直接把这条线接向那个分机。

frp 新版本的配置格式值得一提:较新版本(大约从 v0.5x 起,例如 v0.58.1、以及搜索资料里常见的 v0.62/v0.65)已经全面转向用 TOML 格式的配置文件,也就是 frps.toml 和 frpc.toml(TOML 是一种简单易读的配置文件格式,用 key = value 和数组表的结构组织内容),网页文档推荐新项目直接用 TOML,旧的那种 INI 格式虽兼容但已不推荐使用。 我们这篇用的是 TOML 格式,跟当下主流的官网文档和你从 release 压缩包解压出来看到的示例文件保持一致。

动手前准备:一台有公网 IP 的服务器 + 下载 frp

好,概念都通了,下面进入纯手工劳动。做实验你需要的东西非常少:

  • 一台有公网 IP 的 Linux 服务器(这就是前面说的"中转机/代言人",可以是阿里云、腾讯云、华为云或者任何一台有公网 IP 的云主机,或者你自己的、有独立公网 IP 的一台机器);
  • 一台要被穿透的内网 Linux 机器(就是你在家里/公司里那台跑着 ssh、nginx 之类的机器);
  • 两者能互通网络(内网机器至少要能正常访问外网,因为它得去连公网服务器)。

frp 是二进制发布的,不需要编译。去它的 GitHub Releases 页找适合你系统架构的压缩包下载即可。下面这段命令,示范的是在一台 Linux x86_64 的中转服务器上,用 wget 下载 0.58.1 版本(你也可以替换成更新、你更喜欢的版本号),然后解压:

# 下载 frp 0.58.1 的 linux/amd64 版本压缩包到当前目录(wget 是命令行下载工具)
# 注意服务器是 64 位 x86 就选 amd64;如果是 ARM 机器(如树莓派)要选 arm64 的包
wget https://github.com/fatedier/frp/releases/download/v0.58.1/frp_0.58.1_linux_amd64.tar.gz
 
# 解压这个 tar.gz 压缩包(tar 是归档工具,x=解压 z=gzip压缩 v=显示过程 f=指定文件)
tar -xzf frp_0.58.1_linux_amd64.tar.gz
 
# 进入解压出来的目录(里面就有 frps、frpc 两个可执行文件和对应的 toml 示例配置文件)
cd frp_0.58.1_linux_amd64
 
# 看一眼目录里都有啥(ls 列目录,确认 frps / frpc / *_example.toml 等文件都在)
ls -l

解压出来的目录里,你会发现里面同时有服务端和客户端两套东西(frps、frps.toml、frpc、frpc.toml)。你只需要按机器角色各取所需:公网服务器上,我们只用 frps(服务端);内网机器上,我们只用 frpc(客户端)。建议你把下载、解压操作在公网服务器和内网机器上各来一遍(内网机器选对应架构的包即可),为下面两步做准备。

小提醒:如果你在下载 GitHub 资源时感到速度很慢甚至下不动,别慌,这是 GitHub 在国内网络下的常见现象。可以借助 Watt Toolkit(原名 Steam++)这类加速工具,或者改用官方给出的镜像站下载,同样能拿到 frp 的压缩包。

第一步:在公网服务器上搭 frps 服务端(bindPort 从这里认识)

先搭建公网这个"代言人"。frps 的配置简单得惊人——新版本里,服务端最简单的情况只需要告诉它一个端口就好。这个端口叫 bindPort(bind 取自"绑定"之意,即这台服务器把自己这个端口"握在手里"监听起来)。它是 frps 用来"迎接"内网 frpc 反向连接的端口,也就是刚才比喻里"总机"那个对外接线的总号码。客户端 frpc 里也有一个对应字段,叫 serverPort,它们两个必须一样大,否则客户端连不上服务端。

我帮你写一份极简但完整可用的 frps.toml,每个字段都注释清楚,你照着抄就行:

# ============ frps.toml —— 公网服务器上的服务端配置 ============
 
# bindPort:frps 监听哪个端口,用来接收内网 frpc 的反向连接。
# 默认是 7000,可自定义;这里我们特意用一个不常见的 8888,
# 避免和默认端口撞车。注意 frpc 里的 serverPort 必须填同一个数。
bindPort = 8888

对,就这一行。不信你去解压目录里看示例配置,它默认也就这么点东西。frps 本身"只负责接客和转接",它不需要预先知道你内网有哪些服务——那些是 frpc 后来"上报"给它的。真要说服务端还能配啥,无非是加个 auth.method/auth.token 来做身份认证(我们稍后安全那一节会补上),以及可选的 Web 管理面板 webServer 之类,但那是加分项,不是必需项。

写好后,在公网服务器上启动 frps,用 -c 参数把配置文件指给它:

# 运行 frps,-c 指定配置文件(当前目录下的 frps.toml)
# 这样前台运行,方便看到启动日志;确认没问题后再考虑后台常驻(见后文)
./frps -c ./frps.toml

运行起来后,它会打印一串日志,其中就有类似 frps tcp listener started on [::]:8888 的提示,看到它,说明 frps 已经成功把 8888 端口监听起来了。

第二步:在内网机器上配 frpc 客户端(serverAddr / serverPort / 映射的完整面貌)

公网的服务端点起来了,接下来让内网机器去连它。这一步是整个配置里信息量最大的一步,我们要把一个内网服务的端口,映射(映射这个词还记得吧,就是两端端口对接)到公网上去。这里要一次认清 client 配置里的三个"端口角色":

  • serverAddr / serverPort:这是"我要去连接的 frps 在哪"。serverAddr 是公网服务器的 IP,serverPort 是 frps 监听的那个端口(在 frps 里叫 bindPort,在 client 里叫 serverPort,同一个端口、两个名字看视角,数值必须一致)。
  • localIP / localPort:这是"我内网真正跑服务的那台机器和端口"。比如 ssh 服务就监听在内网机器的 127.0.0.1:22,nginx 监听在 127.0.0.1:80。
  • remotePort:这是"我要求公网服务器对外开放哪个端口来访问我"。公网用户访问 serverAddr:remotePort,流量就顺着隧道走回内网机器的 localPort。

用我们熟悉的服务来具象化:内网机器上的 ssh 服务在 22 号端口,我们想让它通过公网服务器的 8081 端口对外可访问。那么 localPort = 22、remotePort = 8081。这句话翻译成人话就是:把内网 22 号端口的 ssh,映射到公网的 8081 端口上。

[[proxies]] 这个写法也要认识一下:它在 TOML 里是一个"数组表",英文叫 array-of-tables。你可以把一对 [[proxies]] 看成一个"条目/一段配置块",里面写一个代理服务的完整描述。它的妙处在于:一个 frpc.toml 里可以写多组 [[proxies]],每组对应一个要穿透的内网服务。比如你要同时穿透 ssh 和 nginx,那就写两段 [[proxies]],每段各自有 name、type、localPort、remotePort。下面的配置我暂且只写 ssh 这一段,nginx 那段留到后面测试时加进去:

# ============ frpc.toml —— 内网机器上的客户端配置 ============
 
# serverAddr:公网 frps 服务器的公网 IP(替换成你自己的)
serverAddr = "x.x.x.x"
 
# serverPort:frps 监听的端口,必须和 frps.toml 里的 bindPort 一致(这里是 8888)
serverPort = 8888
 
# 下面开始定义我们要穿透的代理服务,[] 表示一个数组条目,可写多组
[[proxies]]
name = "ssh-service"        # 这个代理的名字,自己起,唯一即可,方便在日志里辨认
type = "tcp"                # 代理类型:tcp 表示透明地透传一个 TCP 端口,ssh 属于这种
localIP = "127.0.0.1"       # 内网服务所在机器的地址;因为 frpc 就装在这台机上,所以是回环地址
localPort = 22              # 内网服务监听的端口,这里是 ssh 的 22 号端口
remotePort = 8081           # 要求公网服务器对外开放的端口,公网用户通过它访问我们的 22 号服务

字符串要加引号(TOML 里字符串用双引号包起来),数字不加引号,这一点看着小但特别容易写错,写反了 frp 会直接报配置错误。上面的 127.0.0.1 用的是"回环地址"——也就是机器自己。因为 frpc 和 ssh 服务担当两个角色但住在同一台内网机器上,frpc 访问本机 22 号端口,地址写 127.0.0.1 就是对的了。如果你的 frpc 打算装在 A 上、但想映射 B 上的服务,那 localIP 就得写 B 的内网 IP(比如 192.168.1.100)。

配置写好后在内网机器上启动 frpc,命令和服务端几乎一样,自动补上这个配套环节:

# 运行 frpc,-c 指定客户端配置文件,frpc 会主动去连公网上的 frps
./frpc -c ./frpc.toml

启动后 frpc 会打印日志,重点盯这一句——类似 start proxy success 或 login to server success 的提示,看到它说明内网客户端已经和公网服务端成功握手,一条隧道建起来了。 如果这里有报错(比如登录失败、连接被拒),八成是端口没对上、或者后面的防火墙/安全组把端口拦了。别急,这个我们专门有一节讲常见坑。

测试一:通过穿透,远程 SSH 登录内网机器

隧道建好了,现在是见证奇迹的时刻。我们换个思路验证:此时你手上有一台"公网上的普通电脑"(可以是你自己的笔记本,注意它现在连接的网络是公网/另一个网络,反正不在这台内网机器所在的局域网里就行)。在这台公网侧电脑上,我们用 ssh 去连公网服务器的 8081 端口:

# 从公网侧电脑 ssh 登录内网机器
# 注意:连接的目标端口是公网服务器的 8081,而不是 22
# 后面那个 user 换成内网机器上真实存在的用户名,@ 后面是公网 frps 服务器的 IP
ssh -p 8081 user@x.x.x.x

如果一切正常,你会像往常登录一台正常机器一样,弹出密码验证,输对密码后就能进入内网机器的 shell。此时你离这台"在公网看来隐身"的内网机器,只剩一个 SSH 会话的距离,而真正经过的链路是:

你的笔记本 ──(公网)──> 公网服务器:8081 ──(frps 转发)──> 隧道 ──(frpc 接收)──> 内网机器:22(ssh)

这条链路你最好在心里过一遍:外侧的笔记本永远只跟公网服务器说话(它根本不知道也不关心内网机器的存在,它只知道自己连上了"公网服务器的 8081");公网服务器再通过它和内网 frpc 之间那条"由内向外建立、一直保活的反向连接",把流量原路递进内网。命令本身和平时 ssh 没任何区别,只是端口从 22 换成了 8081——这就是 frp 最高明的地方,对上层应用完全透明:ssh 协议本身毫不知情,它只以为自己在连一台正常的服务器。

测试二:通过穿透,访问内网的 nginx 网页

SSH 打通了,我们再让"宝宝巴士"另一位成员也上车——把内网的 nginx Web 服务也穿透出去。这能让你更全面地看到一个 frpc 配置文件如何承载"多个服务"。

第一步,先确保内网机器上有 nginx 并且已经启动。不同发行版安装命令略有区别,我两种主流的都给你:

# 若内网机器是 CentOS / RHEL / Fedora 这类 yum 系发行版,用 yum 安装 nginx
sudo yum install -y nginx
 
# 若内网机器是 Ubuntu / Debian 这类 apt 系发行版,用 apt 安装 nginx
sudo apt install -y nginx
 
# 启动 nginx 服务(nginx 默认在前台加后台守护进程的方式运行,直接执行即可)
nginx
 
# 如果想停止它,用 -s stop(-s 表示发送一个 stop 信号)
nginx -s stop

nginx 装好后,首页默认内容泛泛地放在一个 ded/fault 位置,Ubuntu/Debian 系的默认首页一般在 /var/www/html 下,文件名通常叫 index.nginx-debian.html。也就是说,内网这台机器在 80 号端口上已经能提供网页了。你可以先用一条 curl 在本机确认一下(这条命令我们到后面测试还会再用,先热身):

# curl 发起一次 HTTP 请求,-I 只取响应头,验证本机 nginx 已经在 80 端口提供服务
curl -I http://127.0.0.1/

看到返回 HTTP/1.1 200 OK 这样的一行,就说明内网的 Web 服务是活的。接下来,我们给 frpc.toml 再追加一段 [[proxies]],把 nginx 也映射出去。这是一份"同时穿透 ssh 和 nginx 两个服务"的完整客户端配置,注意看我怎么在里面放了两组 [[proxies]]:

# ============ frpc.toml —— 内网机器上的客户端配置(同时穿透 ssh 和 nginx) ============
 
# serverAddr:公网 frps 服务器的公网 IP(替换成你自己的)
serverAddr = "x.x.x.x"
# serverPort:frps 监听端口,必须和 bindPort 一致(8888)
serverPort = 8888
 
# 第一组代理:ssh,把内网 22 映射到公网 8081
[[proxies]]
name = "ssh-service"        # 代理名字,唯一
type = "tcp"                # 透传 tcp 端口,适合 ssh
localIP = "127.0.0.1"       # ssh 就在本机 22
localPort = 22
remotePort = 8081           # 公网服务器对外开放 8081
 
# 第二组代理:nginx,把内网 80 映射到公网 8082
[[proxies]]
name = "nginx-service"      # 代理名字,唯一(和 ssh 那组区分开)
type = "tcp"                # nginx 是 http 应用,但底层仍是 tcp,用 tcp 透传最简单可靠
localIP = "127.0.0.1"       # nginx 监听本机 80 端口
localPort = 80
remotePort = 8082           # 公网服务器对外开放 8082,访问这里即可命中内网的 nginx

改完配置后,记得重启 frpc 让新配置生效:先 Ctrl+C 停掉之前那个前台运行的 frpc(如果它还开着),再重新跑一遍 ./frpc -c ./frpc.toml。然后,在公网侧那台电脑上,用浏览器或者 curl 去访问 http://公网服务器IP:8082:

# 从公网侧电脑访问内网的 nginx,用 -p 指定 IP 那不用,这里是 URL;直接访问公网 IP:8082
curl http://x.x.x.x:8082

这次如果通了,你会在终端看到内网 nginx 返回的那一整套 HTML 页面内容(就是内网机器上 index.nginx-debian.html 的那些标签)。也就是说,你在任何一个公网电脑的浏览器地址栏输入 http://公网IP:8082,看到的就是"藏在内网里的那台 nginx"渲染的页面。 至此两个测试全部通过:SSH 穿透成功,Web 穿透也成功。

到这里,你可能已经感觉到了:对 frp 来说,穿透 ssh 和穿透 nginx,用的是同一个 type = "tcp" 的思路——任何基于 TCP 的服务(ssh、mysql、Web、redis……)都能用这套 tcp 透传方式搬出去。frp 提供 http/https/udp 等更多类型,但 tcp 是最万能、也最适合理解"端口映射"本质的。优先把它吃透,其余都是锦上添花。

让 frp 可靠地跑在后台:nohup 是优雅的后台常驻姿势

上面的实验里,frps 和 frpc 都是在前台跑的。前台跑有个明显的毛病:只要你把那个终端窗口一关、或者按一下 Ctrl+C 中断,frp 进程就跟着没了,穿透立刻凉。 可我们要的是隧道 7×24 小时常驻,谁也不愿意为了保持一条穿透守着个终端不放。所以我们要让 frp 在后台运行、并且跟终端解除绑定——即使关掉终端,进程也照常活着。

最轻量、最符合我们"手动部署"气质的方式,是 nohup。nohup 的名字拆开是 "no hang up(不挂起)",它专门负责让命令在终端退出后依然存活。它还有个绝配小伙伴:把命令放到后台执行的 & 符号(末尾一个 &,命令就会立刻回到你眼前,转而在后台默默执行)。组合起来是典型的后台常驻模板:

# 在公网服务器上,让 frps 脱离终端、后台常驻运行
# nohup:终端关闭后进程不退出;./frps 是启动命令;-c 指定配置
# &> /dev/null:把标准输出(stdout)和标准错误(stderr)都重定向到 /dev/null
# /dev/null 是个"黑洞"设备文件,写入即丢弃;这句的作用是忽略所有输出,不让日志刷屏
# 行尾的 & 让整个命令在后台运行,命令结束后立刻回到 shell 提示符
nohup ./frps -c ./frps.toml &> /dev/null &
 
# 在内网机器上,让 frpc 同样脱离终端、后台常驻运行(逻辑完全一样)
nohup ./frpc -c ./frpc.toml &> /dev/null &

这两条命令,是本节你要存入"肌肉记忆"的黄金组合。我逐段给你拆明白,尤其那个 &> /dev/null:

  • nohup:让后面的命令忽略"终端挂断"信号,终端关了它也不退出;
  • &> /dev/null:这个 &> 是"把标准输出和标准错误一锅端都重定向"的意思,都送进 /dev/null 这个特殊设备文件。/dev/null 是什么?它是一个名副其实的"黑洞"设备:你往它里面写任何东西,都会被立刻丢弃;你读它,会立刻得到"文件结束"(EOF,End of File)。所以这句的实际效果是:frp 在终端里啰嗦的日志,全部静音扔掉,眼不见心不烦。
  • 行尾那个 &:把整条命令扔到后台去执行,终端不阻塞,马上继续干别的。

一个小建议:开发尾声你自己跑着玩的时候,可以把 &> /dev/null 暂时改成 &>> frps.log,把日志写进文件(>> 是追加重定向,&>> 是 stdout 和 stderr 都追加)。因为 frp 真的很爱打印日志,遇到连不上之类的问题时,翻日志是排查的第一手资料(这个后面排查会用到)。等全部调通了,再回到 &> /dev/null 的清爽版本也不迟。

补充一句:nohup ... & 属于"轻量后台",适合实验和小规模使用。真正生产环境要更可靠,一般会用 systemd 或者 supervisor 这类进程守护工具来做开机自启、崩溃自动拉起。不过那是另一个话题了,这节课守住"手动部署"的边界,先把 nohup 这条最朴素的路走通。

别裸奔:给 frp 上下引身份认证 token

到目前为止,我们的 frps 是"裸奔"的——它来者不拒,任何一个人只要知道你的公网 IP 和 8888 端口,都能往你的 frps 上注册代理、把自己内网的其他端口映射到你的公网端口上去。往小了说这是"蹭你带宽",往大了说这叫 frp 未授权滥用 / 开放代理风险,可能被坏人用来架代理、发灰产流量,非常危险。所以给 frp 加一把锁、做身份认证,不是"可选项",是"必须项"。

frp 默认的认证方式就叫 token(令牌)。思路极其朴素:在 frps 和 frpc 两边的配置里,各自写上同一个自定义字符串(这个字符串就是"口令",两边对上了才认)。 用一个人的比方就是总机和分机的"暗号"——分机接通总机后先对暗号,对上了,总机才肯帮他接业务;对不上,直接请走。

在 frps.toml 和服务端配置文件里,加两行:

# ============ frps.toml —— 公网服务器上的服务端配置(加上了 token 认证) ============
 
# bindPort:frps 监听端口,用来接收 frpc 的反向连接
bindPort = 8888
 
# 身份认证:method 固定用 "token",token 是一个自定义的复杂"口令"
# 客户端 frpc 端必须填一模一样的 token 才能连上服务端
auth.method = "token"
auth.token = "Yu0iE_8f7!_Secure_Token_2025"

在 frpc.toml 里,加上完全一致的 token 两行(auth.method 保持 "token",auth.token 写同一串口令):

# ============ frpc.toml —— 内网机器上的客户端配置(加了 token 后) ============
 
serverAddr = "x.x.x.x"
serverPort = 8888
 
# 身份认证:和服务端保持一致,token 必须完全相同,否则 server login failed
auth.method = "token"
auth.token = "Yu0iE_8f7!_Secure_Token_2025"
 
[[proxies]]
name = "ssh-service"
type = "tcp"
localIP = "127.0.0.1"
localPort = 22
remotePort = 8081

关于 token 的三条铁律,写在下面,希望你别踩:

  • 两边必须同一串字符,差一个字符都握手失败,frpc 日志会出现 login to server failed 之类的报错。搜了一下 frp 官方文档,token 认证就是这样——服务端和客户端各自配置相同的 token 即可,这也是 frp 默认采用的认证方式。
  • token 要够复杂、够随机,别用 123456、frp 这种一猜就中的。理想情况是一长串掺着大小写、数字、下划线/分隔符的乱码(上面的例子就是这种风格),并且把它当成密码一样保管好,不要明文贴在公开的代码仓库里。
  • 改 token 时必须两端一起改、然后都重启,否则会出现"我以为配好了但连不上"的诡异现象。

把 token 配上后,你的 frps 就不再是谁都能白嫖的开放代理了——只有持有正确口令的 frpc 才能接入。

连接为什么能一直"活着":心跳保活机制

前面我们反复提到"由内向外建立的反向连接要一直保活"。你可能会追问:凭什么这条连接不自己断掉?NAT 不是会记不住、超时清除吗? 这个问题问得极好,答案在心跳(heartbeat)这套机制里。

先认识心跳保活这个词。它的意思很形象:连接双方为了让一条连接不被网络设备"当作闲置连接而回收",会没话找话地、周期性地互发一个小包,类似"我还活着,别掐我"的打招呼。这些周期性的小信使就叫心跳包。

为什么要这么麻烦?因为家用 NAT / 运营商 CGNAT 为了保证端口资源不浪费,大体有一个"连接空闲超时"策略:一条连接如果很久没有任何流量经过,NAT 就会把它对应的映射记录删掉,当作"这条连接已经死了"。删掉之后,外面的流量就再也送不进来了——即使物理上网络没断。这是一种"表项过期"造成的假性断连(NAT 的空闲超时常见于几十秒到几分钟的量级,依赖具体设备和配置,范围通常在 60 到 300 秒上下)。

frp 是怎么对抗的?frpc 会周期性地(默认大概每 30 秒)朝 frps 发一个心跳包,告诉服署端"我还连着呢"。这个心跳包维持了隧道里有"周期性流量",恰好让 NAT 的映射记录一直处于"新鲜、没超时"的状态,于是那条"由内向外"建立的反向连接就能被长期维持住。你可以把心跳理解成"给 NAT 喂饭"——只要定时喂,NAT 就以为这条连接一直很活跃,舍不得掐掉。这也就是为什么即使你的内网 IP 或公网 IP 长期不变、哪怕断网重连,frp 都能在恢复后把隧道重新搭起来:frp 自己也有一套断线自动重连的回退机制,配合心跳,能让你的穿透"懒人式"地长期在线。

常踩的坑与一手排查指南

实验做到这里,理论、配置、测试应该都顺了。但十次动手九次会栽跟头,我把我认为最坑的几个点集中给你排一遍,按"影响面从大到小"排:

坑一:防火墙 / 云安全组没放行端口,这是新手最容易失败、又最隐蔽的一环。 你在 frpc 里配置了 remotePort = 8081,公网用户也来访问 8081 了,结果连不上——你以为是你配错了,其实是公网服务器那侧的防火墙/安全组压根没把 8081 放行进来(云服务器除了操作系统里的防火墙,通常还有一个云安全组在"第一道门"拦着)。我需要提醒你的是:你需要放行的端口不止"业务端口",还有 frps 的 bindPort 本身。 具体地讲,至少要保证这些端口在公网服务器的防火墙和云安全组里是放行(TCP 入方向)的:

  • 8888(bindPort,frpc 连上来要用);
  • 8081(ssh 的 remotePort);
  • 8082(nginx 的 remotePort)。

如果是在非云服务器上,可能还要过一层操作系统的防火墙,常见的有 firewalld 或 iptables,用 firewall-cmd --permanent --add-port=端口/tcp 之类把它放行并重载。排查时用一条 ss -lnt(看看服务器监听哪些端口)和一条在公网侧执行的 telnet 公网IP 8081(测某个端口通不通)能快速定位到底卡没卡在防火墙。

坑二:serverPort 和 bindPort 不一致。 客户端里 serverPort 说的是"我要连你 frps 的哪个端口",服务端里 bindPort 说的是"我 frps 监听哪个端口"。这俩必须相等(我们统一都是 8888)。曾经有人服务端配 8888、客户端填 7000(默认值忘了改),日志反复报连接失败,还以为是防火墙。先核实端口数字。

坑三:token 不一致,导致"登录失败"。 加了 token 后,两端字符哪怕差一个空格都连不上,frpc 日志会直接告诉你认证失败。改 token 后记得两端都要重启进程才生效。

坑四:改配置忘了重启,以为配置没生效。 frpc.toml 改了(比如新增了 nginx 的那组 [[proxies]]),但进程还是旧的,导致新代理没起来。改成新配置后必须重跑一次 frpc(先停旧进程再启新的)。

坑五:误把日志全扔进 /dev/null,出了问题抓瞎。 调试阶段建议保留日志,用 nohup ./frpc -c ./frpc.toml &>> frpc.log & 写到文件,出问题去翻日志——frp 的日志已经把"哪一步失败"写得非常直白(是登录失败、端口冲突、还是没权限)。等全通了再回到静音模式。

坑六:把"公网可达"和"内网可访问"搞混。 最后一个容易自我怀疑的点是:如果在你内网机器自己所在的局域网里测访问 公网IP:8082 有时会"回环拥塞"或受网络环境干扰,最好直接从真正的公网侧(手机流量、另一台非本网段的机器)测试,下单前先想清楚"我是从哪条链路发起请求的"。

把这六个坑提前背下来,你的 frp 生涯就能少一半的掉队感。

思考题(附详解答案)

照老规矩,每节留几道思考题,下面全带详解。强烈建议你先自己想一遍,再对答案。

思考题 1:为什么同样是在公网上访问,内网机器的 ssh(22) 连不上,而把它映射到公网服务器 8081 端口后却能连上?两者差在哪一步?

答案:差的不是"服务"本身,而是"到达路径"。内网机器的 22 号端口,它所在的 NAT/CGNAT 只允许"由内向外"的连接;公网用户想主动直连它的 22 端口时,这一条"由外向内"的连接在 NAT 那一关就被拦下了,公网也根本不知道 22 端口属于哪台内网机器(它甚至没有一个对外唯一可达的地址)。而映射到公网服务器 8081 后,公网用户访问的其实是"公网服务器的 8081",这一步在公网侧完全合法;公网服务器再通过那条由内网 frpc"反向连接 + 保活着"的隧道,把数据递进内网机器的 22 号端口。相当于我们给 22 号服务在公网侧另开了一个合法入口,而这个入口和真正的服务之间,早就用一条"从里往外、NAT 放行"的活连接打通了。想证明这一点,可以试试只配 ssh 那组代理、不配 nginx 那组,然后访问公网的 8082——会发现连不上,因为公网服务器的 8082 上压根没有服务在监听。端口代理是"按需申请"的,public 只开你让 frpc 开的那些。

思考题 2:frpc.toml 里的 localIP / localPort / serverAddr / serverPort / remotePort 五个字段,分别描述了"哪两次连接"的哪一端?

答案:它描述了两段连接的"两端"。第一段是"内网机器 → 公网服务器"的注册/隧道连接:serverAddr 是公网服务器的地址,serverPort 是 frps 监听那个端口(=frps 的 bindPort)。第二段是"公网用户 → 公网服务器 → 内网本地服务"的数据通路:公网用户访问 serverAddr:remotePort(remotePort 是公网服务器对外开的端口),frps 收到后经隧道把流量转给 frpc,frpc 再去连内网服务的 localIP:localPort。所以 localIP/localPort 是"frpc 要打到本地哪个服务",remotePort 是"公网服务器对外开哪扇门",serverAddr/serverPort 是"frpc 去哪找 frps"。一句话总结:serverXxx 管"frpc 找 frps",localXxx 管"frpc 找本地服务",remotePort 管"公网用户找 frps"。

思考题 3:为什么要配 token?如果忘了配,最坏会被怎样利用?

答案:token 是 frps 和 frpc 之间的身份口令,防止"任何人"都能连上你的 frps 并注册代理。如果忘了配,你的 frps 就是一个开放代理:任何知道 公网IP:bindPort 的人都能向它注册任意代理,把自己内网的端口映射到你这台公网服务器的端口上。后果包括:别人蹭走你公网服务器的带宽和流量;更危险的是被坏人用作匿名/中转服务器去对外发起连接,一旦被用于盗刷、灰黑产,最后追查到的流量出口 IP 是你这台机的,你可能被冠以"代理滥用"带来麻烦。所以配置里务必给 frps/frpc 填上一致的、足够复杂的 token,并在公网侧只开放你真正要用到的端口(bindPort + 你需要的 remotePort)。

思考题 4:为什么 frp 需要在 keepalive 下"定时发心跳"?一旦 NAT 的空闲超时到期会发生什么?

答案:NAT/CGNAT 会为空闲的连接记录设置一个过期时间(常见大约几十秒到几分钟)。当一条连接持续没有流量时,NAT 会把它对应的端口映射记录删除,视同"这条连接已死"。frp 通过周期性地从 frpc 向 frps 发心跳包,让隧道里始终有细小的周期性流量,从而让 NAT 的映射记录一直"新鲜",未被回收,隧道得以长期维持。如果心跳停了、或者心跳周期长于 NAT 空闲超时,NAT 就会把该连接删掉,随之而来的是:外面的公网数据再也递不进内网,表现为"穿透瞬间 20 秒(乃至更久)连不上/卡死",而 frp 自身未必报错——它会尝试重连恢复,但那段"假死"间隙你确实访问不到服务。这也是为什么长期稳定穿透离不开心跳机制的原因。