家用服务器上跑着十几个服务:博客、API、面板、监控……但家里是内网,公网 IP 是个"动态且可能不存在"的东西。怎么让外网安全地访问这些服务?

这个问题我折腾了三年,方案换了两代。这篇文章把演进过程和每代的坑都写下来。

第一代:frp + VPS 中转

最初的架构很经典:

公网客户端 → VPS (固定IP, frps) → 内网 n7 (frpc) → 本地服务

frp 本身非常成熟,配置也就十几行。但用了一年之后,痛点越来越明显:

  1. 端口管理失控。每加一个服务就要开一个新端口,VPS 的防火墙规则越写越长,哪个端口对应什么服务全靠注释回忆。
  2. TLS 自己管。每个端口要么裸 HTTP(不安全),要么各自配证书(Nginx 配置爆炸)。
  3. 单点故障。VPS 挂了,所有服务全部失联。frps 的进程守护、自动重启都要自己写。
  4. 带宽瓶颈。所有流量都过 VPS 中转,家里上传 100M,实际体验可能只有 VPS 带宽的一半。

最致命的一次事故:frpc 升级后配置格式变了,服务静默失联三天——因为内网没人用,公网访问直接超时,我根本没收到任何告警。

第二代:Cloudflare Tunnel

转折点是我把博客放上了 Cloudflare。既然 DNS 和 CDN 都在 CF 了,为什么不让流量直接走 CF 的边缘网络,而不是先打到 VPS 再转内网?

Cloudflare Tunnel(cloudflared)的架构:

公网客户端 → CF 边缘节点 (自动TLS) ──隧道──→ 内网 n7 (cloudflared daemon) → 本地服务

关键区别:隧道是内网主动向外建立的。家里不需要公网 IP、不需要开任何入站端口——防火墙可以关得死死的。

部署

# 1. 安装 cloudflared(systemd 服务)
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared
chmod +x /usr/local/bin/cloudflared

# 2. 用账号令牌认证(比 tunnel token 更细粒度的权限控制)
cloudflared service --token <TUNNEL_TOKEN>

在 CF 控制台配置 ingress 规则,把域名路径映射到内网地址:

主机名 路径 目标
blog.example.com /* http://127.0.0.1:3000
api.example.com /v1/* http://127.0.0.1:3001/v1/*

解决第一代的所有痛点

  • 端口管理:全部收敛到域名+路径,防火墙零入站规则
  • TLS:CF 边缘自动签发和续期证书,内网全程 HTTP 即可
  • 可用性:cloudflared 是 systemd 服务,挂了自动重启;CF 侧还有多节点冗余
  • 带宽:流量走 CF 的骨干网,不再受 VPS 带宽限制

仍然踩的坑

  1. 本地调试绕不开。开发时要直连内网端口,隧道只对公网生效。解决:hosts 文件 + 内网域名双套配置。
  2. 流式响应要小心。SSE、大文件下载这类长连接,CF 默认超时是 100 秒。需要在 ingress 里显式配置 noTLSOrigins 和超时参数。
  3. 日志分散。请求日志一半在 CF 控制台(Analytics),一半在 cloudflared 本地。排障时要两边对照。

安全模型的变化

两代方案的安全边界完全不同:

  • frp 时代:安全靠"VPS 防火墙 + 端口不暴露"。一旦 VPS 被打穿,内网拓扑直接暴露。
  • Tunnel 时代:安全靠"CF 边缘 + 域名鉴权"。内网没有任何入站入口,攻击面从"一个 VPS"缩小到"CF 的 WAF 策略"。

配合 CF 的 WAF、速率限制、Bot 管理,安全性不是"自己想办法",而是站在一个全球 CDN 的肩膀上

成本账

  • frp 方案:VPS 约 ¥50/月(还要为带宽升级买单)
  • Tunnel 方案:CF 免费额度(个人博客流量远够用),VPS 退化为纯备份节点

省钱是结果,不是目的。 真正的收益是架构简化——少一个中转节点,就少一类故障。

结语

内网穿透这件事,工具会一直变(frp、nps、Tailscale、Tunnel……),但原则不变:

  1. 入站端口能不开就不开
  2. TLS 交给专业系统管
  3. 架构每多一跳,故障面就多一分

三年两代方案,最大的收获不是某个工具,而是对"安全边界应该画在哪里"的理解。