最近把家里设备的内网穿透完整跑通了一遍,趁记忆还热乎,整理成这篇文章,给自己留个档,也给有同样需求的朋友一份参考。这篇文章我会从计算机网络的底层原理讲起,先把「为什么外网访问不到家里」这件事彻底讲透,再落到 frp 的部署实践,希望你看完后不仅会用,更明白它为什么能工作。
一、认识 NAT:外网无法访问内网设备的原因
要理解内网穿透,绕不开一个底层概念——NAT(Network Address Translation,网络地址转换)。它是「外网进不来」的根因,也是「穿透」要解决的问题本身。
1.1 公网地址与私网地址
IPv4 地址是 32 位的,理论上最多约 42 亿个。这个数量放在今天的设备规模下早已不够用,因此地址被划分为两类:
| 类别 | 地址段 | 规范 | 特性 |
| 公网地址 | 全球唯一 | — | 可直接在公网被路由,是「街道门牌号」 |
| 私网地址 | 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16 | RFC 1918 | 仅在局域网内有效,公网不路由 |
| 运营商大内网(CGNAT) | 100.64.0.0/10 | RFC 6598 | 运营商给用户分发的「套娃」私网地址 |
于是现实中的家用网络,往往是层层嵌套的:运营商用 CGNAT 把你家整体包进一个大内网,你家路由器再做一层 NAT,把设备分到 192.168.x.x。你的设备手里拿到的,从来不是公网地址。
公网 ⇄ 运营商 CGNAT(100.64.x.x)⇄ 家用路由器 NAT ⇄ 你的设备(192.168.x.x)
1.2 NAT 的地址转换机制
NAT 的核心,是路由器上维护的一张地址转换表(在 Linux 里称为 conntrack,连接跟踪表)。它把「内网地址:端口」和「公网地址:端口」一一对应起来,从而让多个内网设备共用一个公网 IP 上网。
来看一次出站访问的完整过程:
内网设备 NAT 路由器 公网服务器
192.168.1.10:50000 内网口 192.168.1.1 8.8.8.8:443
公网口 1.2.3.4
│ │ │
│ ① 出站 SYN │ │
│ src=192.168.1.10:50000│ │
│ dst=8.8.8.8:443 │ │
│───────────────────────▶│ │
│ │ ② SNAT(改写源地址) │
│ │ 建立映射表项: │
│ │ 192.168.1.10:50000 │
│ │ ⇋ 1.2.3.4:40000 │
│ │ src=1.2.3.4:40000 │
│ │ dst=8.8.8.8:443 │
│ │─────────────────────────────▶│
│ │ │
│ │ ③ 服务器回包 │
│ │ src=8.8.8.8:443 │
│ │ dst=1.2.3.4:40000 │
│ │◀─────────────────────────────│
│ ④ DNAT(查表还原) │ │
│ dst=192.168.1.10:50000│ │
│◀───────────────────────│ │
这里有两个关键动作:
- SNAT(Source NAT,源地址转换):出站时把「源地址」从内网地址改成公网地址,并记录映射关系。
- DNAT(Destination NAT,目的地址转换):回包时把「目的地址」从公网地址还原成内网地址,靠的就是之前那张表。
1.3 入站连接失败的原因
注意一个细节:上面那张映射表,是由「出站连接」动态创建的。也就是说,只有在你的设备先主动向外发起连接后,表里才会有对应的表项。
反过来,当一个外网设备想「主动」连进你家时,情况是这样的:
外网设备 NAT 路由器(查无此表项) 内网设备
1.2.3.5 192.168.1.10:22
│ │ │
│ SYN dst=1.2.3.4:22 │ │
│───────────────────────▶│ │
│ │ ✗ 地址转换表中没有 22 端口 │
│ │ 的映射,无法确定该转发给哪台 │
│ │ 内网设备,直接丢弃该包 │
│ │ │
│ ◀── 永远等不到 SYN-ACK ──(连接超时)
这就是「外网进不来」的底层原因:入站连接在 NAT 表里找不到映射,路由器不知道该把数据交给谁,只能丢弃。它不是某个配置错误,而是 NAT 机制本身的限制。
1.4 NAT 的类型(延伸)
顺带一提,NAT 按行为可分为完全锥形、受限锥形、端口受限锥形、对称型等几类。这主要影响 UDP 打洞这类依赖「猜测端口」的穿透技术能否成功——不同的 NAT 类型,打洞难度差别很大。
但本文要讲的 frp 走的是另一条路:它只依赖一条普通的「出站 TCP 长连接」,这条连接天然能被任何类型的 NAT 放行,因此几乎不受 NAT 类型限制。理解了这一点,你就能明白为什么 frp 比 UDP 打洞更通用、更稳定。
二、内网穿透的原理:反向代理
2.1 核心思路:既然入站不行,就让内网主动出站
上一节已经论证:出站连接可以被 NAT 放行,入站连接会被丢弃。frp(fast reverse proxy)正是抓住了这个不对称性——让家里的设备主动向外发起一条到公网服务器的连接,把「入站」的问题,转换成「出站」。
这条由内网主动建立的、长期保持的连接,就成了一条隧道。隧道一旦建立,双向数据都能顺着它走,因为对 NAT 来说,这本质上就是一条已经建立好的出站 TCP 连接。
外地设备 公网服务器 frps 家里设备 frpc
(手机) 监听 7000(控制) + 6000(转发) 本机 sshd :22
│ │ │
│ │ ◀── ① frpc 主动出站连 7000 ──│
│ │ (出站连接,NAT 放行) │
│ │ === 长连接隧道建立 === │
│ │ │
│ ② ssh -p 6000 服务器 │ │
│──────────────────────▶│ │
│ │ ③ 请求沿隧道转发 │
│ │────────────────────────────▶│
│ │ │ ④ frpc 交给本地 22
2.2 角色与数据流拆解
| 角色 | 程序 | 部署位置 | 职责 |
| 服务端 | frps | 有公网 IP 的服务器 | 接收客户端隧道、监听公网端口、转发请求 |
| 客户端 | frpc | 家里的设备 | 主动连接服务端、注册隧道、转发到本地服务 |
一次完整的访问,分五步走:
- 服务端就绪:
frps监听控制端口 7000(供客户端连)和转发端口 6000(供外网访问)。 - 建立隧道:
frpc主动连到 frps 的 7000,用 token 认证,双方建立长连接。 - 注册转发规则:frpc 告知 frps「外网访问你的 6000,就转发给我本地的 22(SSH)」。
- 外网访问:外地设备执行
ssh -p 6000 服务器IP,请求先到达 frps 的 6000。 - 反向转发:frps 把请求封装后沿隧道推给 frpc,frpc 再交给本地 sshd,响应原路返回。
2.3 从 TCP/IP 层次看 frp
如果站到 TCP/IP 协议栈的角度,frp 的原理会更清晰。它本质上是在应用层做了一层「TCP 连接的封装转发」:外网到 frps 是一段 TCP,frpc 到本地服务是另一段 TCP,两段 TCP 通过「隧道」这条长连接在应用层衔接起来。
外地设备 frps frpc 家里 sshd
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ SSH 客户端│ │ 应用层代理│ │ 应用层代理│ │ SSH 服务端│
│ (应用层) │ │ │ │ │ │ (应用层) │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
│ TCP(公网) │ TCP(隧道,一条长连接) │ TCP(本地) │
│◀════════════════════▶│◀═══════════════════════════════▶│◀════════════════▶│
这种「在一条连接里承载多个逻辑连接」的做法,本质是多路复用——frp 给每条转发的连接分配一个 ID,隧道里跑的每个数据包都带着 ID,frpc 收到后按 ID 还原成独立的本地连接。这和 SSH 的 -R 反向端口转发是同一个思路,只是 frp 把它做成了更通用、更好管理的服务。
2.4 端口规划
| 端口 | 位置 | 作用 |
| 7000 | 服务器 | frps 控制端口,frpc 通过它连服务器建立隧道 |
| 6000 | 服务器 | 转发端口,外网访问此端口 → 转发到家里 22 |
| 22 | 家里 | 本机 SSH 服务端口(转发目标) |
三、服务器端:部署 frps
3.1 下载 frp
# 服务器执行 —— 查看最新版本号
curl -s https://api.github.com/repos/fatedier/frp/releases/latest | grep -oP '"tag_name":\s*"v\K[^"]+'
# 服务器执行 —— 下载并解压(v0.61.1 换成上一步看到的版本)
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
sudo mv frp_0.61.1_linux_amd64 /usr/local/frp
ls /usr/local/frp # 应看到 frps、frpc 及对应的 .toml 模板
3.2 配置 frps.toml
# 服务器执行
cat > /usr/local/frp/frps.toml <<'EOF'
bindPort = 7000
auth.method = "token"
auth.token = "请替换为你的强密码"
EOF
| 配置项 | 作用 |
| bindPort = 7000 | 服务端监听端口,客户端通过此端口建立隧道 |
| auth.method = “token” | 启用 token 认证 |
| auth.token | 认证密钥,服务端与客户端必须一致,用于防止未授权连接 |
3.3 启动 frps
# 服务器执行
/usr/local/frp/frps -c /usr/local/frp/frps.toml
启动后若看到 frps started successfully 即表示运行正常。-c 参数用于指定配置文件路径。
3.4 放行端口(安全组)
四、客户端:部署 frpc
4.1 下载 frp
# 家里设备执行(与服务端同版本)
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
sudo mv frp_0.61.1_linux_amd64 /usr/local/frp
4.2 配置 frpc.toml
# 家里设备执行
cat > /usr/local/frp/frpc.toml <<'EOF'
serverAddr = "160.202.47.132"
serverPort = 7000
auth.method = "token"
auth.token = "请替换为你的强密码"
[[proxies]]
name = "ssh"
type = "tcp"
localIP = "127.0.0.1"
localPort = 22
remotePort = 6000
EOF
配置项 作用 serverAddr 服务端地址,即你的公网服务器 IP serverPort = 7000 连接服务端控制端口,与 frps 的 bindPort 对应 auth.token 与服务端一致 name = “ssh” 隧道名称,可自定义 type = “tcp” 转发协议类型,SSH 属于 TCP localIP = “127.0.0.1” 本地服务地址 localPort = 22 本地服务端口(转发目标,这里是 SSH) remotePort = 6000 服务端对外暴露端口,外网访问它即转发到本地 22
4.3 启动 frpc
# 家里设备执行
/usr/local/frp/frpc -c /usr/local/frp/frpc.toml
看到 start proxy success 即隧道建立成功。
五、连接验证
# 任意外网设备执行(手机、公司电脑等)
ssh -p 6000 家里用户名@160.202.47.132
| 参数 | 作用 |
| -p 6000 | 指定端口,连接服务器的 6000(而非默认 22) |
| 家里用户名 | 家里设备上的登录账号 |
| 160.202.47.132 | 服务器 IP |
六、进程守护与开机自启(systemd)
目前 frps / frpc 都是前台运行,关闭终端即中断。通过 systemd 托管后,可实现开机自启、异常自动重启。
6.1 服务端 frps.service
# 服务器执行
sudo cat > /etc/systemd/system/frps.service <<'EOF'
[Unit]
Description=frp server
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/frp/frps -c /usr/local/frp/frps.toml
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now frps
sudo systemctl status frps
其中 ExecStart 为服务启动命令,Restart=on-failure 表示异常退出自动重启,enable –now 表示立即启动并设为开机自启。
6.2 客户端 frpc.service
# 家里设备执行
sudo cat > /etc/systemd/system/frpc.service <<'EOF'
[Unit]
Description=frp client
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/frp/frpc -c /usr/local/frp/frpc.toml
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now frpc
sudo systemctl status frpc
七、安全加固
- 使用强 token:避免弱口令被扫描利用。
- 优先密钥登录:家里设备的 SSH 建议关闭密码登录、仅保留公钥,降低爆破风险。
- 端口选择冷门值:转发端口不必用 6000,可换为高位端口以降低被扫描概率。
- 最小化暴露:云安全组仅放行必要端口,其余保持关闭。
关闭家里设备 SSH 密码登录(可选,操作前确保密钥登录已可用):
# 家里设备执行
sudo sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo systemctl restart sshd
八、常见问题排查
| 现象 | 原因与处理 |
| frpc 报 connection refused | 服务端 7000 端口未放行,或 frps 未启动 |
| frpc 报 authorization failed | 两端 auth.token 不一致 |
| SSH 提示 Permission denied | 确认使用的是家里设备的用户名与密码,而非服务器账号 |
| 连接 6000 超时 | 云安全组未放行 6000,或本地 sshd 未运行 |
| 隧道中断 | 查看 systemctl status frpc,网络波动会自动重连 |
实时查看日志:
sudo journalctl -u frps -f # 服务端
sudo journalctl -u frpc -f # 客户端
九、写在最后
内网穿透的核心,就是抓住「出站连接不受 NAT 限制」这一特性,让内网设备主动连出、建立反向隧道。理解了这一点,frp、ngrok、tailscale 等工具背后的逻辑都是相通的。
本文以 SSH 为例,但同样思路可以穿透任意 TCP 服务——把 localPort 换成 NAS 的网页端口,就能在外网访问家里的管理界面;换成 Jellyfin 的端口,就能在外面看家里的影视库。希望这篇文章能帮你少走弯路。

