内网穿透:Cloudflare Tunnel
内网服务要让公网访问,传统做法是在路由器上做端口映射,再配一层反向代理。但国内家用宽带普遍没有公网 IP,即便有,80 和 443 也大多被运营商封禁。反向代理那条路线的应对办法是把服务挂到非标端口上,访问地址变成 xxx.newzone.top:9003 这种带端口的形式,能用,但不够干净。
Cloudflare Tunnel 换了个思路:不再等外部连进来,而是由内网的 cloudflared 容器主动向 Cloudflare 建立一条出站长连接。公网请求先抵达 Cloudflare 边缘节点,再沿这条已建立的连接回到内网服务。整个过程不需要公网 IP,不需要开放任何入站端口,路由器上一条映射规则都不用加。
两条路线各有取舍。Tunnel 给出的是干净的 xxx.newzone.top,不带端口,代价是流量必须绕经 Cloudflare 边缘,因而受制于它的体积和超时限制,大文件传输不适合走它。反向代理在局域网内是直连,速度不受外部网络影响,但域名要带端口。
两者并不互斥。我的做法是内网访问走反向代理,确实需要在外面用的少数服务才发布到隧道上。
创建隧道
- 登录 Cloudflare 控制台,进入 Networking > Tunnels,选择 Create a tunnel。
- 填写隧道名称。一个隧道可以承载多个服务,按机器命名(例如
nas)比按服务命名更合适。 - 选择 Create Tunnel 保存,页面会进入连接器安装环节。选择操作系统后,Cloudflare 会给出一条带 token 的安装命令,把 token 复制出来备用,命令本身不必执行,下一节改用 Compose 部署。
部署 cloudflared 容器
官方镜像是 cloudflare/cloudflared。本站其他服务统一用 Docker Compose 管理,隧道也一样,详见 Docker Compose 部署教程。
services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
network_mode: host
command:
- tunnel
- --no-autoupdate
- --protocol
- http2
- run
- --token
- YOUR_TOKENnetwork_mode: host 让容器直接使用宿主机网络,后面填写内网服务地址时可以直接写 http://localhost:端口,省去处理容器间网络互通的麻烦。--restart unless-stopped 保证容器异常退出或宿主机重启后自动拉起,带着原有参数重连。
--protocol http2 是这份配置里最关键的一项。cloudflared 默认走 QUIC,底层是 UDP,而家用路由器、运营商和旁路网关对 UDP 的处理普遍不友好,实测在 NAS 上表现为隧道能连上却反复断线。改用基于 TCP 的 HTTP/2 后四条高可用连接全部稳定注册,日志无告警。写成环境变量 TUNNEL_TRANSPORT_PROTOCOL: http2 是等效的。
cloudflared 自 2026.5.2 起会在每次启动时跑一遍连通性预检,但预检报 PASS 不代表 QUIC 可用。实测预检的两项 UDP 检查都通过、还建议以 quic 为首选协议,真正建连时四条连接却只成功三条,剩下一条以 timeout: no recent network activity 终止。预检只是一次瞬时握手,QUIC 的问题出在持续传输上,两者测的不是一回事。
只想快速验证的话,也可以用 docker run:
docker run -d \
--name cloudflared \
--restart unless-stopped \
--network host \
cloudflare/cloudflared:latest \
tunnel --no-autoupdate --protocol http2 run --token YOUR_TOKEN容器启动后回到控制台,隧道状态会变为 HEALTHY。
发布服务到子域名
隧道本身只是一条通道,还需要告诉它哪个域名对应哪个内网服务。
- 进入 Networking > Tunnels,点开刚创建的隧道,切到 Routes 标签页。
- 选择 Add route,类型选 Published application。
- Subdomain 填写子域名,Domain 从下拉菜单里选择一个已托管在 Cloudflare 的域名。
- Service URL 填写内网服务的协议和地址,例如
http://localhost:5230。
保存后 Cloudflare 会自动创建 DNS 记录并签发证书,不需要手动配置。同一个隧道可以反复添加 route,把多个服务分别挂到不同子域名下。
需要留意多级子域名的问题。如果域名形如 a.b.newzone.top,默认的通配符证书覆盖不到,访问时会报 SSL 错误,需要额外购买 Advanced Certificate。规划域名时尽量保持单级子域名。
加一层登录鉴权
隧道发布出去的地址是公开的,任何人拿到域名都能访问。memos、qBittorrent 这类存有数据的服务裸奔在公网上并不合适,可以用 Cloudflare Access 在前面挡一道登录。
- 进入 Zero Trust > Access controls > Applications,选择 Create new application。
- 类型选 Self-hosted and private,然后 Add public hostname,填入隧道已经发布的那个域名。
- 在 Access policies 中添加策略。Access 默认拒绝所有请求,必须有一条 Allow 策略匹配才会放行。
- 如果用一次性密码(One-time PIN)作为登录方式,务必在 Include 规则里限定具体邮箱或邮箱域名。只写 OTP 而不限定邮箱等于对所有人开放,因为任何邮箱都能收到验证码。
配置完成后,访问该域名会先跳转到 Cloudflare 的登录页,验证通过才进入服务。
如果服务需要在代码层面校验 Access 身份,而不只是依赖这层跳转,还要用到 AUD 与团队域名两个值,取值方式见 OpenClaw 部署教程 中的 Access 配置部分。
OpenClash 旁路网关下的配置
软路由启用 OpenClash 的 fake-ip 模式后,cloudflared 拿到的是 198.18.x.x 这类假 IP。隧道通常仍能跑起来,但会退化:可用的不同地址有限,四条高可用连接只建得起两条,日志提示 You requested 4 HA connections but I can give you at most 2。要恢复完整状态,需要在两个地方分别放行。
cloudflared 实际连接的是 region1.v2.argotunnel.com 和 region2.v2.argotunnel.com 的 7844 端口,HTTP/2 走 TCP,QUIC 走 UDP;SNI 相关的域名在 cftunnel.com 下;可选的接口调用与版本更新走 api.cloudflare.com 和 update.argotunnel.com 的 443 端口。
先在「覆写设置 > DNS 设置」的 fake-ip 黑名单中加入这些域名,让它们解析到真实 IP:
+.argotunnel.com
+.cftunnel.com
+.cloudflare.com必须用 +. 而不是 *.。Clash 的 * 只匹配一级子域,而 region1.v2.argotunnel.com 在 argotunnel.com 之下有两级,*.argotunnel.com 匹配不到,域名照样会拿到假 IP。
再在「覆写设置 > 规则设置 > 自定义设置」中让对应流量直连:
rules:
- DOMAIN-SUFFIX,argotunnel.com,DIRECT
- DOMAIN-SUFFIX,cftunnel.com,DIRECT
- DOMAIN-SUFFIX,cloudflare.com,DIRECT
- IP-CIDR,198.41.192.0/24,DIRECT,no-resolve
- IP-CIDR,198.41.200.0/24,DIRECT,no-resolve
- IP-CIDR,198.18.0.0/15,DIRECT,no-resolve两处分工不同。域名规则决定流量走不走代理,是隧道能通的关键,即便拿到假 IP,OpenClash 也会在建连时把它反查回域名再匹配规则;黑名单决定的是连接质量,生效后 cloudflared 才能拿到完整的边缘地址池,四条连接全部建成。
配完看 cloudflared 启动日志里 Registered tunnel connection 那行的 ip= 字段自查:地址落在 198.41.192.x 或 198.41.200.x、且 connIndex 从 0 到 3 齐全,说明两处都生效;仍是 198.18.x.x 则是黑名单没匹配上。
三条 IP-CIDR 的 no-resolve 不能省。IP 类规则默认会为域名目标先发起一次 DNS 解析再比对,一条规则就足以让所有域名连接多一次解析,既拖慢速度也可能造成 DNS 泄漏。
末尾的 198.18.0.0/15 跟隧道无关,单独说明一下。这一段是 Clash 的 fake-ip 池,域名进了黑名单就不会落到这里。它兜的是映射丢失的情况:OpenClash 重启后旧的假 IP 成了死地址,此时没有域名可供匹配,判 DIRECT 让连接立刻失败、触发重新解析,比交给代理干等超时更好。网上不少教程把这一条当作 Cloudflare Tunnel 连不上的解药,归因是错的。
另外,DOMAIN-SUFFIX,cloudflare.com,DIRECT 会连 dash.cloudflare.com 一起直连,国内开后台可能变慢。只想放行 cloudflared 用到的接口,可以收窄为 DOMAIN,api.cloudflare.com,DIRECT。
实测坑位
与宝塔面板的 Docker 冲突
cloudflared 容器与宝塔的官方 Docker 镜像存在冲突,两者不要同时使用。NAS 上装了宝塔的话,需要先确认这一点再部署。
单个请求体积上限 100 MB
Cloudflare 免费版和 Pro 版对单个请求的体积上限均为 100 MB,Business 版为 200 MB,超出会返回 413。这个限制作用在边缘节点上,与内网服务自身的配置无关,改服务端参数没有用。往 Castopod 这类需要上传大音频的服务传文件时很容易撞上。
源站响应超时 125 秒
边缘节点默认等待源站响应 125 秒,超时返回 524。耗时较长的导入导出任务会被这条规则打断。
这两条限制可以用同一个办法绕开:操作前把服务的对外地址临时改为内网 IP 直连,跳过 Cloudflare 边缘,完成后再切回隧道域名。Castopod 的具体做法见 Castopod。
启动日志里的两条警告
cloudflared 可能报两条 WARN,都不影响隧道工作,按上面的配置通常也不会出现。QUIC 缓冲区不足(failed to sufficiently increase receive buffer size)只在保留默认 QUIC 时出现,指定 http2 即无;ICMP 代理被禁用只影响通过隧道 ping 内网主机,发布 HTTP 服务用不到。确实要消掉的话,在宿主机上调整内核参数,容器内的 /proc/sys 是只读的:
sudo sysctl -w net.core.rmem_max=8388608
sudo sysctl -w net.core.wmem_max=8388608
sudo sysctl -w net.ipv4.ping_group_range="0 2147483647"sysctl -w 只在本次开机内有效,要持久生效就把对应的键值写入 /etc/sysctl.d/99-cloudflared.conf,再执行 sudo sysctl -p。