Cloudflare Tunnel
Cloudflare Tunnel(守护进程 cloudflared)与 Cloudflare 网络建立出站连接,
在没有公网 IP、也不开放 80 和 443 端口的情况下发布你的服务。宿主机上
既不需要带证书的反向代理,也不需要公网地址:
流量通过已建立的隧道从 Cloudflare 到达,TLS 在
Cloudflare 一侧终止。
1. 隧道适用的场景
隧道适用于以下情况:
- 服务器没有静态公网 IP,或运营商封锁入站端口;
- 需要隐藏 origin 地址以防扫描;
- 你已经在使用 Cloudflare 做 DNS 和攻击过滤。
隧道不适用于以下情况:
- 面板必须在完全不依赖第三方服务的情况下工作;
- 你无法接受 Cloudflare 能看到全部明文流量;
- 需要到节点的稳定 SSH 终端:隧道中断空闲 WebSocket 连接比直连代理明显更激进。
流量路径如下:浏览器通过 HTTPS 访问 Cloudflare,Cloudflare 边缘
通过已打开的隧道把请求传给宿主机,而 cloudflared
以 HTTP 把它交给本地服务。宿主机上既不需要证书,也不需要
开放端口 —— 对外只有守护进程的出站连接。
Quick Tunnel(地址形如 *.trycloudflare.com)只适合测试:它
有并发连接数限制、随机域名,也没有可用性
保证。生产环境请使用命名隧道和自己的域名。
2. 安装 cloudflared
守护进程不需要入站端口:它自己与最近的 Cloudflare 节点建立
出站连接。防火墙中只需允许到端口 7844 的出站
连接(quic 协议用 UDP,http2 用 TCP);端口
443 仅用于检查更新。如果运营商封锁 UDP,
请在启动命令中加上 --protocol http2 标志。
mkdir -p /opt/xlada/cloudflared && cd /opt/xlada/cloudflared
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \
-o /usr/share/keyrings/cloudflare-main.gpg
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] \
https://pkg.cloudflare.com/cloudflared any main" \
> /etc/apt/sources.list.d/cloudflared.list
apt update && apt install -y cloudflared
cloudflared --version
3. 授权与创建隧道
# 会打开一个链接:在浏览器中确认对域名的访问权限
cloudflared tunnel login
# 创建命名隧道。密钥 JSON 文件会出现在 ~/.cloudflared/
cloudflared tunnel create xlada
# 查看 UUID —— config.yml 中会用到
cloudflared tunnel list
4. DNS 记录
cloudflared tunnel route dns xlada panel.example.com
cloudflared tunnel route dns xlada sub.example.com
这些命令会创建指向隧道地址的 CNAME 记录,无需手动添加任何 内容。代理(橙云)会自动启用。请把 SSL/TLS 模式设为 Full 或 Full (strict),并启用 Always Use HTTPS,这样访问者就无法通过 http 访问面板。
5. 隧道配置
# 来自 `cloudflared tunnel list` 输出的 UUID
tunnel: 00000000-0000-0000-0000-000000000000
# 由 cloudflared tunnel create 命令创建的密钥 JSON 文件
credentials-file: /root/.cloudflared/00000000-0000-0000-0000-000000000000.json
# 日志和隧道本地指标页面
loglevel: info
logfile: /var/log/cloudflared.log
metrics: 127.0.0.1:49312
ingress:
# 订阅页面:流量经过本地 8081 代理
- hostname: sub.example.com
service: http://127.0.0.1:8081
originRequest:
# 保留原始 Host,否则应用会看到 127.0.0.1
httpHostHeader: sub.example.com
# 面板:第 6 节中的本地 8080 代理
- hostname: panel.example.com
service: http://127.0.0.1:8080
originRequest:
httpHostHeader: panel.example.com
connectTimeout: 30s
# 必需的兜底规则:所有未匹配的请求
- service: http_status:404
两个服务都指向本地代理,而不是直接指向 3000 和 3010
端口。这样请求头始终由同一个节点生成,面板就能得到
可预测的值,不受 cloudflared 行为的影响。
启动前检查配置 —— 该命令会指出规则中的拼写错误:
cloudflared tunnel ingress validate
cloudflared tunnel ingress rule https://panel.example.com/
6. 请求头与本地代理
如果请求到达时没有
X-Forwarded-For 和 X-Forwarded-Proto: https,面板和订阅页面会断开连接。请弄清这些请求头
在隧道中的行为:
- Cloudflare 接收请求,并用访问者所使用的协议
重写
X-Forwarded-Proto。启用 Always Use HTTPS 时它 始终是https; - Cloudflare 对
X-Forwarded-For是追加而不是替换:如果客户端发来了 自己的请求头,链中排在最前面的会是它填入的值,而 真实地址在最后。不能依赖最左边的值; cloudflared会原样把请求传给originRequest服务,无法 强制设置X-Forwarded-Proto: https。如果访问者通过 http 访问,面板会拒绝这样的请求。
因此可靠的方案是在面板前放一个小型代理,由它
自己设置请求头。下面是两个本地 origin 服务器的完整配置:
面板在 127.0.0.1:8080,订阅页面在 127.0.0.1:8081。
# Cloudflare Tunnel 的本地 origin 服务器。
map $http_upgrade $cf_connection_upgrade {
default upgrade;
'' close;
}
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types
application/javascript
application/json
application/manifest+json
image/svg+xml
text/css
text/javascript
text/plain
text/xml;
# ─── XLADA 面板:cloudflared 把 panel.example.com 交给这里 ─────────
server {
listen 127.0.0.1:8080;
server_name panel.example.com;
client_max_body_size 32m;
# 面板构建产物静态资源:文件名中的哈希允许缓存它
location ^~ /assets/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Connection "";
expires 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
# WebSocket —— 到节点的 SSH 终端
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $cf_connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 只保留客户端真实地址,不带追加的链
proxy_set_header X-Forwarded-For $remote_addr;
# 关键行:面板期望 https
proxy_set_header X-Forwarded-Proto https;
proxy_connect_timeout 10s;
proxy_send_timeout 86400s;
proxy_read_timeout 86400s;
}
}
# ─── 订阅页面:cloudflared 把 sub.example.com 交给这里 ──────────────
server {
listen 127.0.0.1:8081;
server_name sub.example.com;
client_max_body_size 10m;
location ^~ /assets/ {
proxy_pass http://127.0.0.1:3010;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Connection "";
expires 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}
location / {
proxy_pass http://127.0.0.1:3010;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
# WebSocket:目前未使用,但配置已为它准备好
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $cf_connection_upgrade;
proxy_connect_timeout 10s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
}
ln -sf /etc/nginx/sites-available/xlada-origin /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
两个 server_name 都与 config.yml 规则中的 httpHostHeader 一致,因此
面板和订阅页面会收到原始域名,而不是 127.0.0.1。
7. 以容器运行
services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
# 隧道向外建立,不需要入站端口
network_mode: host
command: tunnel --no-autoupdate --config /etc/cloudflared/config.yml run
volumes:
- ./config.yml:/etc/cloudflared/config.yml:ro
- ./credentials.json:/etc/cloudflared/credentials.json:ro
# 把 ~/.cloudflared/ 中的密钥 JSON 文件复制到 compose 文件旁边
cp /root/.cloudflared/00000000-0000-0000-0000-000000000000.json \
/opt/xlada/cloudflared/credentials.json
docker compose up -d && docker compose logs -f
在容器中,config.yml 文件只有隧道密钥路径不同。
这种方式的完整文件如下:
# 来自 `cloudflared tunnel list` 输出的 UUID
tunnel: 00000000-0000-0000-0000-000000000000
# 隧道密钥位于 docker-compose.yml 旁边
credentials-file: /etc/cloudflared/credentials.json
# 日志和隧道本地指标页面
loglevel: info
logfile: /var/log/cloudflared.log
metrics: 127.0.0.1:49312
ingress:
# 订阅页面:本地 8081 代理
- hostname: sub.example.com
service: http://127.0.0.1:8081
originRequest:
httpHostHeader: sub.example.com
# 面板:本地 8080 代理
- hostname: panel.example.com
service: http://127.0.0.1:8080
originRequest:
httpHostHeader: panel.example.com
connectTimeout: 30s
# 必需的兜底规则:所有未匹配的请求
- service: http_status:404
8. 作为服务运行
mkdir -p /etc/cloudflared
cp config.yml /etc/cloudflared/config.yml
cp /root/.cloudflared/*.json /etc/cloudflared/
cloudflared service install
systemctl enable --now cloudflared
systemctl status cloudflared
9. 面板的额外防护
面板的公开地址人人可见,因此仅靠密码不够。有两种方式 关闭入口:
- Cloudflare Access —— 应用前的 Zero Trust 策略:只有登录你的
identity provider 后才能打开面板。订阅页面
可以保持公开,也可以单独保护它;为此在
.env中 提供了变量CLOUDFLARE_ZERO_TRUST_CLIENT_ID和CLOUDFLARE_ZERO_TRUST_CLIENT_SECRET; - WAF 规则,按国家或地址列表限制,前提是 管理员从已知网络工作。
这两种机制都是对面板自身设置的补充,而不是替代:请启用 双因素认证,并且不要关闭登录尝试次数限制。
10. 限制
- 客户端真实 IP。 Cloudflare 会添加带有客户端地址的
CF-Connecting-IP请求头;推荐的事实来源正是它,而不是X-Forwarded-For。如果面板前有 nginx,只信任 Cloudflare 网段。完整的/etc/nginx/conf.d/real-ip.conf文件如下:
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
real_ip_header CF-Connecting-IP;
real_ip_recursive on;
Cloudflare 的网段列表会定期更新 —— 请与 最新版本核对。
- WebSocket 与 SSH 终端。 WebSocket 通过 Cloudflare 可以工作,但 空闲连接会被中断。对于到节点的终端,更可靠的做法是 直接连接面板,而通过隧道只提供订阅页面。
- 限额。 免费套餐对上传文件大小和并发连接数 有限制;具体数值会定期变化。
- SSL/TLS。 Flexible 模式会导致循环重定向并丢失 请求头。请使用 Full 或 Full (strict)。
- 端口。 隧道只对外开放 Cloudflare 的标准端口
(
443、80以及部分备用端口)。像3010这样的内部地址 仍保持本地。
11. 检查
cloudflared tunnel info xlada
cloudflared tunnel ingress validate
curl -sI https://panel.example.com | head -3
curl -s -o /dev/null -w '%{http_code}\n' https://sub.example.com/
journalctl -u cloudflared -n 30
tail -n 30 /var/log/cloudflared.log
面板根路径应返回登录页面,而不是 502,也不是关于
请求头的错误。出现 502 时,请检查 config.yml 中的 service,以及本地
代理是否监听了正确的端口。