frp 内网穿透实战:原理、配置、部署与故障排查

发布时间:2026/10/28 4:08:42
frp 内网穿透实战:原理、配置、部署与故障排查

说起内网穿透,我印象最深的是前两年帮一位朋友处理的一个小需求:他家里长期放着一台旧主机做照片和资料的冷备份,人经常往外跑,想在酒店里把家里机器上的几个目录直接调出来用。麻烦的地方在于,他家宽带拨号拿到的是运营商内网地址,路由器上根本没有公网 IP,从外部无论怎么连都进不去。这类场景就是内网穿透最典型的用武之地,而 frp 是我这些年反复用、也反复踩坑的一套方案。这篇文章把 frp 的原理模型、配置文件里每个参数的含义、从零部署的完整流程、几个高频场景的具体写法,以及我实际遇到的故障和处理办法,尽量一次讲透。不管你是第一次接触内网穿透,还是已经在用 frp 但配置总是出问题,下面这些内容应该都能直接拿来抄或者对照排查。

1. 内网穿透到底在解决什么问题

1.1 没有公网 IP 的现实困境

要理解 frp 为什么存在,得先搞清楚家庭和小型办公网络的基本结构。绝大多数宽带接入方式,运营商给你的是一个内网地址,路由器做 NAT 转换之后,家里所有设备共享这一个出口。这种设计对上网没影响,因为出站连接是主动发起的,NAT 设备会记录一条会话表,回包能顺着这条表找回来。但反过来,外部想主动连进来就没有入口了,NAT 表里没有对应记录,包直接被丢弃。

有人会想到在路由器上做端口映射,把外部某个端口转到内网某台机器。这个思路本身没错,但前提是你得有一个公网 IP,而且这个 IP 还得是固定的或者至少能通过动态域名追踪到。现实情况是,很多宽带套餐已经不给公网 IP 了,打电话申请也未必给,即便给了,地址还可能变动。更麻烦的是,部分网络环境下连 UPnP 端口映射都被限制,路由器后台里那项功能形同虚设。

这就是内网穿透要解决的核心矛盾:内网设备想对外提供服务,但网络结构不允许外部主动连接。绕开这个矛盾的办法只有一个——让内网设备主动向外建立一条长连接,外部访问请求全部走这条已经建立的连接进来。frp 做的就是这件事,它的连接方向和服务方向是反的,这也是它名字里 reverse 的由来。

1.2 frp 的连接模型:谁主动连谁

frp 的架构非常简洁,只有两个角色:服务端 frps 和客户端 frpc。服务端跑在一台有公网地址的机器上,客户端跑在需要被访问的内网机器上。启动顺序上,frpc 主动向 frps 发起连接,建立一条控制通道,之后所有的映射关系都通过这条通道协商。

我用一个更直白的类比来解释。假设你在公司上班,公司前台有对外电话,你工位上没有。客户想找你,直接拨你工位电话是打不通的。但你可以在上班时先给前台打个电话说“我在,如果有个叫张三的找我,你就把电话转到我工位来”。这个“先打给前台”的动作就是 frpc 连 frps,前台记录下“张三 -> 某工位分机”的对应关系,之后客户打前台电话找张三,前台一转,就通了。整条链路里,你的工位从头到尾没有对外暴露过任何东西,对外可见的只有前台那部电话。

这个模型带来的直接好处是:内网侧完全不需要改动路由器配置,不需要公网 IP,不需要端口映射,只要内网机器能正常访问外网就行。对那些处在多层 NAT 后面、或者公司网络出口管得很严的环境,这一点非常关键。

1.3 几种常见方案的横向对比

内网穿透不止 frp 一种做法,选型的时候我心里一般会过一遍下面这张表。不同方案适合的规模和技术门槛差别很大,硬套一种方案往往会在后期维护上付出代价。

方案类型是否需要公网服务端配置复杂度适用场景主要短板
路由器端口映射需要公网 IP低有固定公网 IP 的家庭或小型办公室多数宽带已不提供公网 IP
商用穿透服务不需要极低临时演示、非技术用户带宽和连接数受限,长期成本高
frp 自建需要一台公网主机中等长期使用、多设备、需要精细控制需要自己维护服务端
组网类工具不需要低到中多设备互联、远程办公配置灵活度和可控性不如自建

我自己的判断标准很朴素:如果只是临时用一两天,商用服务最省事;如果是要长期跑、设备超过三台、并且对端口和访问控制有明确要求,那就自建 frp。自建的成本其实不高,一台入门配置的云主机就够,frp 本身资源占用很小,主要开销在流量上。

需要说明的是,无论用哪种方式把内网服务暴露到公网,都应当确认用途符合所在地的相关规定以及所使用网络服务商的条款,这一点在动手之前就该想清楚,而不是部署完了再回头补。

2. 配置文件里的每个参数都值得说清楚

2.1 服务端 frps.toml 核心配置

frp 从 0.52 版本开始把配置格式从 INI 换成了 TOML,老教程里那些[common]开头的写法在新版本上会直接报错,这是很多人照着旧文档配置失败的第一个原因。我下面全部以 TOML 为准,如果你手上的版本比较老,参数名对应关系需要单独查一下。

一个能用的最小服务端配置长这样:

bindPort = 7000 auth.method = "token" auth.token = "换成你自己的长随机串"

bindPort是 frps 监听客户端连接的端口,不是最终对外提供服务的端口,这两个概念别混。auth.token是身份校验,不设的话任何知道地址的人都能连上来挂映射,等于把门开着。token 建议用 32 位以上的随机字符串,用密码管理器生成一个就行,别用生日或者简单的英文单词。

实际部署时我一般会再加几项,让服务端更好管:

bindPort = 7000 auth.method = "token" auth.token = "换成你自己的长随机串" webServer.addr = "127.0.0.1" webServer.port = 7500 webServer.user = "admin" webServer.password = "另一个强密码" log.to = "/var/log/frps.log" log.level = "info" log.maxDays = 7 allowPorts = [ { start = 6000, end = 6100 } ]

webServer是 frps 自带的管理面板,能看到当前有哪些客户端在线、每个映射的流量情况。我把它绑在 127.0.0.1 上,需要看的时候通过 SSH 隧道转发到本地浏览器,不直接对公网开放。这不是多余的小心,管理面板一旦暴露,攻击者拿到面板就等于拿到了所有映射关系的控制权。

allowPorts是我强烈建议加上的一项。它限制了客户端能申请的服务端端口范围。不加的话,客户端可以申请任意端口,万一 token 泄露,对方能把 80、443 这类敏感端口占掉,甚至影响同机器上别的服务。限定一个区间,等于给映射上了个笼子。

2.2 客户端 frpc.toml 核心配置

客户端配置是日常改动最频繁的部分,结构上分成两段:连接服务端的基础信息,和一个个具体的映射项。基础部分:

serverAddr = "你的公网主机地址" serverPort = 7000 auth.method = "token" auth.token = "和服务端保持一致" log.to = "/var/log/frpc.log" log.level = "info" log.maxDays = 3

映射项在 TOML 里用数组表表示,每个映射一个[[proxies]]块。以最常用的 TCP 类型为例,把内网机器的 SSH 端口映射出去:

[[proxies]] name = "home-ssh" type = "tcp" localIP = "127.0.0.1" localPort = 22 remotePort = 6000

这里几个参数的含义要分清楚。localIP和localPort是内网侧被访问的目标,通常就是本机,写 127.0.0.1 即可;如果目标在同一内网的另一台机器上,就写那台机器的内网地址。remotePort是服务端对外开放的端口,外部访问时连的是服务端地址:6000,而不是内网机器的 22。

name这个字段看似不起眼,实际上是映射的唯一标识。同一个客户端下 name 不能重复,服务端也靠它来区分不同的映射。我见过有人把多个映射的 name 都写成test,结果只有最后一个生效,前面几个全被覆盖,排查了半天才发现是这个原因。

2.3 三种常用映射模式的取舍

frp 支持多种映射类型,用得最多的是 tcp、http、stcp 三种,它们的差别直接影响你怎么设计端口和访问方式。

tcp是最通用的,不管内网跑的是什么协议,只要能走 TCP 就能映射。缺点是每个映射都要占服务端一个端口,映射多了端口管理会很乱,而且服务端防火墙要为每个端口单独放行。

http类型则只占服务端一个端口,靠域名来区分不同的映射,配置大致如下:

[[proxies]] name = "web-app" type = "http" localPort = 8080 customDomains = ["app.你的域名.com"]

这种模式适合映射 Web 服务,尤其是要挂多个站点的场景。服务端需要配置vhostHTTPPort,比如设成 8080,然后所有 http 类型的映射都通过这个端口进来,frps 根据请求头里的域名转发到对应的客户端。用这个模式的前提是你得有一个域名,并且把这个域名解析到服务端地址。

stcp是这三者里最保守的,它的特点是映射出去之后,外网不能直接访问,必须有一个同样知道密钥的访问端先建立连接才能进来。配置上客户端这边:

[[proxies]] name = "secret-ssh" type = "stcp" secretKey = "一段足够长的随机密钥" localIP = "127.0.0.1" localPort = 22

访问端那边要配一个对应的 visitor:

[[visitors]] name = "secret-ssh-visitor" type = "stcp" serverName = "secret-ssh" secretKey = "和上面一致" bindAddr = "127.0.0.1" bindPort = 6000

然后访问端在本机连 127.0.0.1:6000 就行。这种模式适合那种“只有我自己能访问”的场景,比如把家里的 SSH 或数据库端口映射出来,但完全不想让它在公网上有入口。密钥泄露的风险比 token 泄露要小得多,因为即便有人知道服务端地址,没有 secretKey 也连不上。

选型上的经验:Web 服务优先用 http 类型,省端口也方便挂证书;非 Web 服务但只有自己用,优先 stcp;确实需要给不特定的第三方访问,才用 tcp 直接暴露,并且一定要配合防火墙限制来源。这个顺序是我踩过几次端口被扫之后固定下来的。

3. 从零开始的部署实战

3.1 环境准备与版本选择

服务端我一般选一台入门配置的云主机,1 核 1G 足够,frp 本身的常驻内存占用在几十 MB 级别,真正吃资源的是转发的流量。操作系统选常见的 Linux 发行版即可,我习惯用 Debian 系,下面的命令也都以它为例,其他发行版的差异主要在包管理命令上。

版本选择上有个细节值得注意:frp 的版本迭代比较频繁,新旧版本之间的配置文件格式和部分参数名会变。服务端和客户端的版本最好保持一致,至少大版本号要对齐,否则可能出现客户端连上了但映射建立不起来的情况。我遇到过服务端是 0.5x、客户端还是 0.4x 的组合,日志里只报了一句很含糊的握手失败,查了半天才发现是版本不匹配。

下载方式上,直接去项目的发布页面拿对应架构的压缩包最省事:

uname -m

先确认架构,x86_64 就是 amd64,aarch64 就是 arm64。下载解压后把frps和frpc两个可执行文件分别放到/usr/local/bin/下,配置文件放到/etc/frp/下,这个目录结构不是强制的,但按这个来后面写服务单元文件会顺很多。

3.2 服务端安装与守护进程配置

服务端的手动启动命令是:

/usr/local/bin/frps -c /etc/frp/frps.toml

但这个只能验证配置是否正确,真正部署要交给 systemd 托管,否则 SSH 一断开服务就没了。写一个单元文件放到/etc/systemd/system/frps.service:

[Unit] Description=frp server After=network.target [Service] Type=simple User=nobody Restart=on-failure RestartSec=5s ExecStart=/usr/local/bin/frps -c /etc/frp/frps.toml [Install] WantedBy=multi-user.target

几个地方值得说明。User=nobody是降权运行,frps 不需要 root 权限,用 root 跑等于给自己埋雷。Restart=on-failure保证进程意外退出后自动拉起,RestartSec=5s避免退出后立刻重启导致的循环。

启用和查看状态:

systemctl daemon-reload systemctl enable frps systemctl start frps systemctl status frps

防火墙这边要放行bindPort,以及你实际用到的服务端口。如果用了 http 类型,还要放行vhostHTTPPort。放行规则尽量精确到端口,别图省事直接开放全部。

注意:allowPorts里配置的端口区间,必须和防火墙实际放行的端口一致。我见过配置里允许 6000-6100,但防火墙只开了 6000,结果 6001 之后的映射全部连不上,日志里服务端一切正常,问题卡在防火墙上,这类问题最容易让人怀疑人生。

3.3 客户端配置与开机自启

客户端这边先手动跑一次,确认能连上服务端:

/usr/local/bin/frpc -c /etc/frp/frpc.toml

前台能看到连接日志,成功的话会打印出每个映射的启动信息。确认无误之后再做成服务。Linux 下的单元文件和服务端几乎一样,只改名字和执行文件:

[Unit] Description=frp client After=network.target [Service] Type=simple Restart=on-failure RestartSec=5s ExecStart=/usr/local/bin/frpc -c /etc/frp/frpc.toml [Install] WantedBy=multi-user.target

Windows 下的处理方式不太一样。frpc.exe本身不能直接注册成系统服务,需要用 winsw 之类的包装工具,把它包成一个服务。具体做法是下载 winsw 的可执行文件,改名成frpc-service.exe,旁边放一个同名的 xml 配置文件,里面写清楚可执行文件路径、参数和启动方式,然后用frpc-service.exe install注册。这一步很多人在 Windows 上卡住,以为 frpc 不支持开机自启,其实是缺了这层包装。

还有一类场景是客户端在内网某台不能装服务的设备上,比如某些嵌入式设备或者只能跑脚本的环境。这时候可以考虑把 frpc 跑在容器里,用--restart=always保证它随宿主启动。容器方式的配置挂载要仔细,配置文件路径和容器内路径要对上,否则容器起来就退出。

3.4 安全加固:别让服务暴露成一扇敞开的门

内网穿透这个东西,配置正确和配置安全是两件事。我见过太多人跑通之后就不再动配置,结果几个月后发现自己映射出去的端口天天被人扫。下面这几条是我每次部署都会过一遍的。

第一条,token 必须设,而且必须够长。这不是形式主义,frp 的 token 校验是在建立连接阶段做的,没有 token 或者 token 太弱,等于任何人都能把你的服务端当成自己的中转站。

第二条,映射出去的端口尽量少,能不暴露就不暴露。SSH 这类管理端口我现在基本不用 tcp 直连,改用 stcp 模式。Web 服务如果只给自己用,也可以在 frps 前面加一层访问控制,或者干脆用 stcp 配合本地端口访问。

第三条,服务端的管理面板和 SSH 端口不要对公网开放,改成只监听本地,需要时通过已有的连接转发访问。这一步多花十分钟,能省掉后面很多麻烦。

第四条,日志保留要有,出事时能查。log.maxDays设成 7 到 14 天比较合适,太短了查不到历史,太长了占空间。

第五条,定期看管理面板里的连接列表。如果发现有自己不认识的映射项,或者某个映射的流量异常大,要立刻查。frp 面板上能看到每个映射的来源地址和流量统计,这是最直接的异常发现手段。

4. 几个典型场景的落地配置

4.1 家庭 NAS 与远程桌面的访问

这是我用得最多的场景。家里一台低功耗主机跑着文件共享和远程桌面,出门在外想访问。这里有个设计上的选择:是把 SMB 端口直接映射出去,还是只映射一个 SSH 端口,通过 SSH 隧道再访问 SMB。

我现在的做法是后者。直接映射 SMB 的 445 端口出去风险太高,这个端口是扫描重灾区,而且 SMB 协议本身历史上出过不少漏洞。改成只映射 SSH,然后在本地建一条隧道,把远端的 445 转到本地,用起来体验一样,暴露面小得多。

配置上客户端只需要一个 TCP 映射:

[[proxies]] name = "home-ssh" type = "tcp" localIP = "127.0.0.1" localPort = 22 remotePort = 6000

本地访问时先建隧道:

ssh -L 1445:127.0.0.1:445 用户名@服务端地址 -p 6000

这条命令的意思是,把本地的 1445 端口通过 SSH 转到服务端那台机器视角下的 127.0.0.1:445,也就是内网 NAS 的 SMB 端口。之后在本地文件管理器里访问smb://127.0.0.1:1445即可。整个过程 SMB 端口从来没有在公网上出现过。

远程桌面同理,映射 3389 或者 VNC 的 5900 都行,但我依然倾向于走 SSH 隧道,理由一样。

4.2 本地开发调试与回调接收

开发阶段经常遇到一个需求:本地跑着的服务,需要接收外部平台发来的回调通知。比如对接某个第三方接口,对方要求提供一个可访问的回调地址。本地开发机没有公网地址,用 frp 映射出去最直接。

这种场景我更推荐用 http 类型,因为它能根据域名区分不同项目,不用为每个项目单独分配端口:

[[proxies]] name = "dev-api" type = "http" localPort = 8080 customDomains = ["dev.你的域名.com"]

服务端这边配上vhostHTTPPort = 8080,域名解析到服务端地址,访问http://dev.你的域名.com就打到本地 8080 了。改项目的时候只要改 localPort,域名不变,回调地址也不用跟着改,这一点在反复调试时很省心。

有个细节要留意:很多回调平台会校验请求来源,或者对 HTTPS 有要求。如果对方强制 HTTPS,那就在服务端配一个反向的 Web 服务器处理证书,frp 只负责把请求转过来,不要在 frp 层面硬搞证书,那样配置会变得很复杂。

另外,如果只是想临时接收一次回调,用完就关,那就别做成 systemd 服务,直接前台跑 frpc,用完 Ctrl+C 结束。常驻服务适合长期存在的映射,临时调试没必要。

4.3 多设备管理:用配置模板减少重复劳动

设备一多,配置文件的管理就成了问题。我手上有好几台不同位置的机器需要映射,每台的服务端地址、token 都一样,只有映射项不同。如果每台都写一份完整的配置文件,改 token 的时候就得改好几处,漏一处就出问题。

frp 支持配置文件的包含机制,可以把公共部分抽出来。基础配置文件frpc-common.toml放连接信息和认证:

serverAddr = "你的公网主机地址" serverPort = 7000 auth.method = "token" auth.token = "公共的 token"

然后每个设备一份自己的映射配置frpc-device-a.toml,里面只写代理项。启动时用includes把公共部分拉进来:

includes = ["/etc/frp/frpc-common.toml"] [[proxies]] name = "device-a-ssh" type = "tcp" localPort = 22 remotePort = 6001

这样改 token 只改一处,所有设备统一生效。命名上我建议带上设备标识,比如device-a-ssh、office-nas-web,服务端面板上一眼看过去就知道是哪个设备哪项服务,比清一色的ssh1、ssh2强太多。

端口分配这块也建议提前做个表,哪个设备用哪段端口,写下来贴在配置文件注释里。我自己是按 6000-6099 分给家庭设备,6100-6199 分给办公设备,每台设备内部再细分。不提前规划的话,设备一多必然撞端口,撞了之后的报错信息又不明显,很难第一时间想到是端口冲突。

5. 故障排查实录与常见问题速查

5.1 连不上:从日志里找线索

frp 连不上分两种情况,一种是客户端压根没连上服务端,另一种是连上了但映射没建立。这两种的排查方向完全不同,先要分清是哪一种。

客户端启动后如果日志里一直刷connect to server error,说明控制通道没建立。按这个顺序查:服务端进程在不在(systemctl status frps)、服务端bindPort有没有被防火墙挡住(从客户端机器上用telnet 服务端地址 7000试)、服务端地址和端口写对没有。这三步能覆盖八成的情况。

如果日志显示login to server success但某个映射报start error,说明控制通道通了,问题在映射本身。常见原因有:name 重复、remotePort超出了服务端allowPorts的范围、remotePort已经被别的映射占用。日志里一般会明确写出是哪一项失败,仔细看那一行,不要被其他正常映射的日志干扰。

还有一种容易忽略的情况是 token 不匹配。新版本 frp 在 token 错误时的报错信息比较直接,老版本可能只报一句握手失败。遇到含义模糊的报错,先把服务端和客户端的 token 逐字符比对一遍,包括有没有多余的空格。

5.2 连上了但访问不了:端口与防火墙

这个阶段最典型的现象是:客户端日志一切正常,映射显示已启动,但从外部访问就是连不上或者超时。问题基本都在链路的某一环上。

我一般按这个顺序排查:先确认外部访问的目标地址和端口写对了没有,remotePort是服务端的端口,不是内网的端口,很多人在这里搞混;然后在服务端本机上用curl或者telnet访问127.0.0.1:remotePort,如果能通说明 frp 转发没问题,问题在防火墙;如果不通,问题在内网目标服务或者 frp 配置。

防火墙这块要查两层:云服务商的安全组和机器本身的防火墙。两层都要放行才是真的放行,只开一层是很多人卡住的原因。安全组改完通常立即生效,机器防火墙改完记得 reload。

http 类型还有一类特有问题:域名解析和vhostHTTPPort对不上。访问时 Host 头里的域名必须是配置里customDomains写的那一个,用 IP 直接访问是不会命中的。这个特性一开始容易让人困惑,但理解了之后反而觉得合理,因为它就是靠 Host 头来区分站点的。

5.3 速度慢、连接断:性能与超时调优

frp 默认配置下,长时间空闲的连接可能会被中间网络设备清理掉,表现为用着用着突然断一下,过一会儿又恢复。这种情况可以通过心跳参数缓解:

transport.heartbeatInterval = 30 transport.heartbeatTimeout = 90

heartbeatInterval是客户端发送心跳的间隔,heartbeatTimeout是服务端判定超时的时间。前者要明显小于后者,留出足够的容忍空间。设得太激进会增加不必要的流量,设得太宽松则断线恢复慢,30 和 90 这组值在我这里跑了很久没出过问题。

速度快慢主要受服务端带宽和两端网络质量影响,frp 本身不是瓶颈。如果传输大文件觉得慢,可以先确认服务端的带宽上限,再看客户端到服务端这一段的网络质量。有个容易被忽略的点是transport.useCompression,开启压缩对文本类数据有帮助,但对已经压缩过的数据(比如图片、视频)反而会增加 CPU 开销且没有收益,不建议全局打开。

连接池参数transport.poolCount在一些高并发场景下有提升,但默认值对绝大多数个人用途已经够用,没必要一开始就调。调参的原则是先找到瓶颈在哪,再针对性地改,盲目调大参数往往适得其反。

5.4 常见问题速查表

下面这张表是我这些年遇到过的、被问得最多的问题,按现象归类,方便对照排查。

现象常见原因处理方向
客户端提示连接服务端失败地址或端口写错、服务端未启动、防火墙拦截先用 telnet 测端口连通性,再查进程状态
映射启动报端口相关错误remotePort 超出允许范围或被占用检查 allowPorts 配置和服务端端口占用情况
多个映射只有一个生效name 字段重复逐个检查 name 唯一性
显示已连接但外部访问超时云安全组或机器防火墙未放行两层防火墙都要确认
http 映射用 IP 访问不通未使用配置的域名访问确认 Host 头与 customDomains 一致
用一段时间后连接中断空闲超时被中间设备清理配置心跳间隔和超时时间
传输大文件速度慢服务端带宽上限、压缩配置不当确认带宽,关闭不必要的压缩
老旧配置文件启动报错使用了新版本不支持的 INI 格式迁移到 TOML 格式
Windows 下无法开机自启未用 winsw 包装成服务用 winsw 注册系统服务
服务端重启后客户端未自动重连客户端未配置自动重连检查 systemd 的 Restart 设置

最后再分享一个我自己的习惯:每次改完配置,先在前台手动跑一遍,看着日志确认所有映射都起来了,再交给 systemd 托管。直接改完就重启服务,出问题时日志被覆盖一部分,排查起来会麻烦不少。另外,配置文件和证书这类东西,我习惯在本地留一份带注释的副本,机器重装的时候直接拷过去,比凭记忆重写要可靠得多。踩过几次配置丢失的坑之后,这个习惯就一直保留下来了。