Cloudflare Tunnel:没有公网 IP,照样把内网服务暴露到互联网

前言

家里服务器跑了一堆自托管服务:AFFiNE、LobeChat、Beszel、Vaultwarden…… 但问题是 —— 我没有公网 IP。

以前的做法是 FRP 内网穿透,租一台公网 VPS 做中转。能用,但多一层跳板就多一层延迟,而且 FRP 服务端本身也是攻击面。

后来换成了 Cloudflare Tunnel。原理很简单:在内网服务器上跑一个 cloudflared 进程,它主动向 Cloudflare 边缘网络建立一条加密隧道。外部用户访问你的域名 → Cloudflare 全球网络 → 隧道 → 你的内网服务。全程只需要出站连接,不需要开放任何入站端口。

最关键的是 —— 免费。

为什么不用其他方案?

坦率讲,内网穿透的选择很多:

方案费用公网 IP安全复杂度
Cloudflare Tunnel免费不需要高(TLS 1.3 + DDoS 防护)
FRP需中转 VPS中转服务器需要
Ngrok免费有限额不需要
Tailscale Funnel免费不需要
端口转发免费需要

Cloudflare Tunnel 的核心优势在于:你不光获得了穿透能力,还白嫖了 Cloudflare 全球 CDN 的 DDoS 防护和边缘缓存。FRP 和 Ngrok 只是把流量传进来,Cloudflare 是在传之前先帮你挡一波攻击。

当然也有代价:所有流量经过 Cloudflare,理论上他们能看到你的明文 HTTP 请求(HTTPS 下只能看到 SNI)。对隐私极度敏感的场景,Tailscale 的点对点加密更合适。

前置准备

三样东西:

  1. Cloudflare 账号(免费注册)
  2. 域名托管在 Cloudflare(把域名的 NS 记录指向 Cloudflare)
  3. 一台内网服务器(能上网就行,不需要公网 IP)

5 分钟部署(Docker 版)

官方推荐的 Token 方式最简单。先去 Cloudflare Zero Trust 控制台 → Networks → Tunnels → Create a tunnel,选 Docker 环境,会给你一个 token。

拿到 token 后:

Terminal window
docker run -d \
--name cloudflared \
--restart unless-stopped \
cloudflare/cloudflared:latest \
tunnel --no-autoupdate run --token YOUR_TUNNEL_TOKEN

就这样。你的内网服务器已经有一条到 Cloudflare 的加密隧道了。

还没完 —— 隧道建立了,但还没告诉 Cloudflare「哪个域名走隧道访问哪个内网服务」。回控制台,在 Tunnel 的 Public Hostname 里加规则:

subdomain.yourdomain.com → http://localhost:3000

Cloudflare 自动帮你配好 DNS 记录(CNAME 指向 xxx.cfargotunnel.com),不需要手动改。

进阶:YAML 配置文件管理多服务

Token 方式适合快速上手,但管理多个服务的时候不够灵活。推荐用配置文件:

~/.cloudflared/config.yml
tunnel: YOUR_TUNNEL_UUID
credentials-file: /etc/cloudflared/credentials.json
ingress:
# HTTP 服务
- hostname: affine.yourdomain.com
service: http://localhost:3010
- hostname: chat.yourdomain.com
service: http://localhost:3210
- hostname: dash.yourdomain.com
service: http://localhost:8080
# SSH 访问
- hostname: ssh.yourdomain.com
4 collapsed lines
service: ssh://localhost:22
# 兜底规则(必须放在最后)
- service: http_status:404

最后一条 http_status:404 是兜底规则:匹配不到前面 hostname 的请求一律 404。不加这条 cloudflared 会报配置错误。

Docker 挂载配置文件启动:

Terminal window
docker run -d \
--name cloudflared \
--restart unless-stopped \
-v /opt/cloudflared:/etc/cloudflared \
cloudflare/cloudflared:latest \
tunnel --config /etc/cloudflared/config.yml run

远程 SSH:不用记 IP 了

配好 ssh.yourdomain.com → ssh://localhost:22 之后,在任何装了 cloudflared 的机器上:

Terminal window
cloudflared access ssh --hostname ssh.yourdomain.com

这会在本地 1234 端口开一个代理,然后正常 SSH:

Terminal window
ssh user@localhost -p 1234

或者一条命令搞定:

Terminal window
ssh -o ProxyCommand="cloudflared access ssh --hostname %h" user@ssh.yourdomain.com

不暴露 SSH 端口、不用记 IP、流量全程加密。

搭配 Cloudflare Access:加一道身份锁

Tunnel 解决了穿透问题,但如果想让某些服务只有你自己能访问(比如管理面板),可以叠加 Cloudflare Access。配置之后,用户打开网页会先弹一个 Cloudflare 登录页,验证通过才放行。

支持的验证方式:Google OAuth、GitHub OAuth、邮箱验证码、甚至硬件 Key。免费版也支持。

这个结合对于暴露管理类服务特别有用 ——AFFiNE 管理后台、1Panel、Portainer 这些,没人想在公网上裸奔。

踩坑记录

兜底规则忘了写

ingress 里最后一行必须是 service: http_status:404。忘了写的话,cloudflared 启动直接报错。报错信息很友好,一眼就能看出来,但第一次配容易漏。

DNS 设置注意

创建 Tunnel 的时候如果选了「自动配置 DNS」,Cloudflare 会自动给你的子域名加 CNAME 记录。但如果你的域名原本就有 A 记录指向某个 IP,需要手动删掉旧的,否则 DNS 冲突。

WebSocket 支持

很多自托管服务用到 WebSocket(AFFiNE 的实时协作、Beszel Hub-Agent 通信、LobeChat)。好消息是 Cloudflare Tunnel 原生支持 WebSocket,不需要额外配置。坏消息是免费版有 100 条 / 秒的并发限制,对个人够用,企业场景要注意。

cloudflared 版本更新

Docker 的 cloudflare/cloudflared:latest 标签经常更新。建议定期 docker compose pull && docker compose up -d,或者固定到具体版本号避免意外的 breaking change。

总结

Cloudflare Tunnel 解决了一个很具体的问题 ——「我没有公网 IP,但我想让外面访问我的服务」。做这件事的方式还很安全:不需要开端口、不需要维护跳板机、流量全程加密。

如果你正在用 FRP 或者端口转发暴露内网服务,花十分钟切到 Cloudflare Tunnel,你会发现以前的方案都太折腾了。

而且全程免费。对于一个白嫖 Cloudflare 全球网络 + DDoS 防护的方案来说,这已经超出预期了。