Aside0's AI Blog

纯AI生成的折腾日记,保证无手打!
  1. 首页
  2. 网络
  3. 正文

Cloudflare Tunnel 通过 Xray 出口:cloudflared、nftables 和 sniffing 排查

2026年6月17日 56点热度 0人点赞 0条评论
日期:2026-06-17

公开版说明:本文保留配置结构、排查路径和关键判断,内网地址、Xray 出站 tag、group id 等环境标识已替换为示例值。

这份文档记录的是树莓派上 `cloudflared` 的出口调整。它不是从零安装 Cloudflare Tunnel 的教程,而是解释怎么让已经能运行的 Cloudflare Tunnel 不再从树莓派家宽直连 Cloudflare edge,而是通过本机 Xray 的代理出口出去。

这次真正解决的是一个很具体的问题:

```text
cloudflared 默认从树莓派直连 Cloudflare edge
我希望它走 Xray 出口
但 cloudflared 对 TLS SNI、原始 edge IP 和 7844 端口很敏感
普通 sniff 或普通 HTTP_PROXY 很容易把 tunnel 搞坏
```

最后能稳定工作的方案是:

```text
cloudflared 进程使用专用 group
nft 只拦截这个 group 发往 Cloudflare Tunnel edge IP 的 TCP 7844
流量重定向到 Xray 的专用 dokodemo-door 入站
这个入站开启 sniff,但必须启用 routeOnly
Xray 再把流量送到默认代理出站
```

## 相关折腾记录

- [树莓派 Tailscale Exit 与 Xray 透明代理配置整理](https://aside0.me/raspberry-pi-tailscale-xray-2026-06-15/)
- [树莓派 Wi-Fi 热点接入 Xray:DNS 防污染和透明代理排查](https://aside0.me/raspberry-pi-wifi-hotspot-xray-dns-2026-06-19/)
- [树莓派 Xray DNS 防泄露和游戏下载分流](https://aside0.me/raspberry-pi-xray-dns-leak-game-split-2026-06-20/)

## 系统和环境

这台机器是树莓派,不是 OpenWrt。

```text
硬件:Raspberry Pi 5
系统:Debian GNU/Linux 13 (trixie)
内核:6.12.62+rpt-rpi-v8
架构:linux/arm64
服务管理:systemd
防火墙:nftables
Xray:26.3.27
cloudflared:2026.6.0
LAN 地址:192.168.10.20
```

相关服务和端口:

```text
Xray socks:0.0.0.0:10808
Xray HTTP proxy:127.0.0.1:18080
cloudflared 专用 Xray 入站:127.0.0.1:1061
cloudflared 运行组:cloudflared_proxy
Cloudflare Tunnel edge 端口:TCP 7844
```

这台树莓派上本来已经有 Xray 透明代理、Tailscale exit、DNS 分流等配置。本文只写 Cloudflare Tunnel 这条链路,不重复那部分。

## 最终拓扑

最终流量路径是这样:

```mermaid
flowchart LR
  cf["cloudflared<br/>Group=cloudflared_proxy"]
  out["Linux OUTPUT"]
  nft["nftables<br/>cloudflared_output_proxy"]
  redir["REDIRECT<br/>127.0.0.1:1061"]
  xray_in["Xray<br/>cloudflared-local-redir"]
  xray_out["Xray 默认代理出站"]
  proxy["远端代理节点"]
  edge["Cloudflare Tunnel edge<br/>198.41.192.0/24<br/>198.41.200.0/24"]
  cfnet["Cloudflare Tunnel"]
  origin["局域网 origin 服务"]

  cf --> out
  out --> nft
  nft --> redir
  redir --> xray_in
  xray_in --> xray_out
  xray_out --> proxy
  proxy --> edge
  edge --> cfnet
  cfnet --> origin
```

nft 规则只拦 `cloudflared_proxy` 这个 group 的流量。这样不会影响树莓派上其他 root 进程,也不会把 Xray 自己的出站绕回 Xray。

```mermaid
flowchart TD
  process["本机进程"]
  group_check{"skgid == cloudflared_proxy?"}
  cf_ip_check{"目标 IP 是<br/>198.41.192.0/24<br/>或 198.41.200.0/24?"}
  tcp_check{"TCP?"}
  redirect["redirect to :1061"]
  normal["按原本路径走"]

  process --> group_check
  group_check -->|否| normal
  group_check -->|是| cf_ip_check
  cf_ip_check -->|否| normal
  cf_ip_check -->|是| tcp_check
  tcp_check -->|否| normal
  tcp_check -->|是| redirect
```

## 为什么 sniff 会影响 cloudflared

Cloudflare Tunnel 的连接有一个容易误判的地方:

```text
TCP 目标:Cloudflare edge IP,例如 198.41.192.167:7844
TLS SNI:cftunnel.com
```

普通 HTTPS 访问里,Xray sniff 到 SNI 后把目标从 IP 改成域名,通常问题不大,甚至有利于按域名分流。但 `cloudflared` 这条链路不一样。它连接的是 Cloudflare 分配的 edge IP,TLS 握手里又带 `cftunnel.com`。如果 Xray 把原始目标 IP 改写成 sniff 到的域名,再重新解析或重路由,就容易出现:

```text
TLS handshake with edge error: EOF
```

所以这里的关键不是简单地“开 sniff”或“关 sniff”,而是:

```text
允许 sniff 看到域名
但不要让 sniff 改写 cloudflared 的真实目标地址
```

这就是 `routeOnly: true` 的作用。

```mermaid
flowchart LR
  original["原始目标<br/>198.41.192.167:7844"]
  sniff["sniff 到<br/>cftunnel.com"]
  route["用于路由判断"]
  dial["实际连接仍然拨原始 IP"]

  original --> sniff
  sniff --> route
  original --> dial
```

## 普通入站的 sniff 模板

你的普通 Xray 入站可以使用这种 sniff 写法。它适合正常透明代理或普通代理入站:

```json
{
  "sniffing": {
    "destOverride": [
      "fakedns",
      "quic",
      "tls",
      "http"
    ],
    "enabled": true,
    "routeOnly": true,
    "domainsExcluded": [
      "domain:cftunnel.com",
      "domain:argotunnel.com",
      "full:api.cloudflare.com",
      "domain:cloudflareaccess.com",
      "domain:cloudflareresearch.com"
    ]
  }
}
```

这段配置的意思是:

- 对 FakeDNS、QUIC、TLS、HTTP 做 sniff。
- sniff 结果可以用于恢复域名,让 `geosite` 规则有机会生效。
- `routeOnly: true` 让 sniff 只参与路由判断,不改写真实目标地址。
- `domainsExcluded` 只排除 Cloudflare Tunnel、API、Zero Trust Access 和相关上报域名,不把所有 Cloudflare CDN IP 都排除掉。

不要在普通入站里直接排除 `geoip:cloudflare`。很多第三方网站也在 Cloudflare CDN 后面,按整个 Cloudflare IP 段排除会让这些网站失去更细的域名分流。

### 为什么普通入站不用 geoip:cloudflare

`geoip:cloudflare` 匹配的是 Cloudflare 边缘 IP,不等于 Cloudflare 自己的服务域名。大量普通网站也把 DNS 指到 Cloudflare CDN,它们的 TLS SNI 仍然是自己的域名,不会变成 `cloudflare.com`。

如果普通透明代理入站把 `geoip:cloudflare` 整段排除,Xray 会把很多完全不同的网站都当成“Cloudflare IP 流量”处理。这样会削弱原来的 `geosite`、广告、国内外和服务类域名分流:访问一个用了 Cloudflare CDN 的第三方站点时,规则看到的重点会变成 Cloudflare edge IP,而不是这个站点自己的域名。

这次要解决的问题更窄:`cloudflared` 连接的是 Cloudflare 分配的 edge IP,但握手里会出现 `cftunnel.com`、`argotunnel.com`、`cloudflareaccess.com` 这类控制面域名。正确做法是保留原始 edge IP,同时只对这些控制面域名禁止 sniff 改写。所以普通入站用 `routeOnly` 加小范围 `domainsExcluded`,`cloudflared` 专用入站再额外保留实测 edge CIDR。

## cloudflared 专用 Xray 入站

当前能工作的入站是:

```json
{
  "tag": "cloudflared-local-redir",
  "listen": "127.0.0.1",
  "port": 1061,
  "protocol": "dokodemo-door",
  "settings": {
    "network": "tcp",
    "followRedirect": true
  },
  "sniffing": {
    "enabled": true,
    "destOverride": [
      "fakedns",
      "quic",
      "tls",
      "http"
    ],
    "routeOnly": true,
    "domainsExcluded": [
      "domain:cftunnel.com",
      "domain:argotunnel.com",
      "full:api.cloudflare.com",
      "domain:cloudflareaccess.com",
      "domain:cloudflareresearch.com"
    ],
    "ipsExcluded": [
      "geoip:cloudflare",
      "198.41.192.0/24",
      "198.41.200.0/24"
    ]
  }
}
```

几个点不能省:

- `followRedirect: true`:让 `dokodemo-door` 读取 nft REDIRECT 前的原始目标。
- `network: tcp`:正式方案只走 HTTP/2,也就是 TCP 7844。
- `routeOnly: true`:sniff 到域名,但实际拨号仍然保持原始 edge IP。
- `domainsExcluded`:覆盖 Tunnel、API、Zero Trust Access 和 Cloudflare Research 相关域名,避免这些控制面域名被 sniff 改写。
- `198.41.192.0/24` 和 `198.41.200.0/24`:这是这次 Cloudflare Tunnel edge TCP 7844 实测命中的两个网段。

这里没有把 Xray 全局 `domainStrategy` 改成 `AsIs`。当前全局仍然是:

```text
IPIfNonMatch
```

原因是树莓派还有原来的国内直连、广告拦截、DNS、Tailscale exit 分流。只要 `cloudflared-local-redir` 用 `routeOnly` 后能稳定注册,就没有必要为了这条链路改全局策略。

## nft 转发规则

### 专用 group 和 GID

这里的 `cloudflared_proxy` 不是特殊内置组,而是专门给 `cloudflared` 建的运行组。这样 nft 可以只抓这个进程组的 socket 流量,不需要按 root 用户全局拦截。

先创建系统组:

```sh
sudo groupadd -r cloudflared_proxy 2>/dev/null || true
```

没有显式指定 `-g` 时,系统会自动分配一个可用的 system GID。这个数字每台机器可能不同,所以不要直接照抄别人的 GID。创建后用 `getent` 查:

```sh
getent group cloudflared_proxy
getent group cloudflared_proxy | cut -d: -f3
```

输出类似这样:

```text
cloudflared_proxy:x:<cloudflared_proxy_gid>:
```

中间的第三段就是实际 GID。把这个数字填到 `/etc/cloudflared/output-proxy.nft` 里的 `<cloudflared_proxy_gid>`:

```nft
meta skgid <cloudflared_proxy_gid>
```

`cloudflared.service` 里写的是组名:

```ini
Group=cloudflared_proxy
```

nft 规则里写的是这个组对应的 GID。两边必须指向同一个组。服务启动后可以这样确认进程是否真的跑在这个 group 下:

```sh
ps -o pid,user,group,args -C cloudflared
```

正常情况下,`GROUP` 一列应该是 `cloudflared_proxy`。如果这里还是 `root`,说明 systemd service 没有重新加载或服务没有按新配置重启。

规则文件:

```text
/etc/cloudflared/output-proxy.nft
```

当前内容:

```nft
table inet cloudflared_output_proxy {
  chain output_nat {
    type nat hook output priority dstnat; policy accept;
    meta skgid <cloudflared_proxy_gid> meta nfproto ipv4 ip daddr { 198.41.192.0/24, 198.41.200.0/24 } meta l4proto tcp redirect to :1061
  }
}
```

这个规则的意思是:

```text
只匹配本机 OUTPUT
只匹配 cloudflared_proxy 这个 group
只匹配 IPv4
只匹配 Cloudflare Tunnel edge 的两个 /24
只匹配 TCP
命中后重定向到本机 1061
```

这比按端口或按 root 用户全局拦截安全得多。`cloudflared` 是 root 启动的,如果按 uid root 抓,本机很多系统服务都可能被误伤。按 group 抓可以把影响面限制到 `cloudflared`。

注意,这两个 /24 不是完整 Cloudflare CDN 网段,只是 Cloudflare Tunnel edge 在这次环境里使用的网段。以后如果 Cloudflare 调整 edge 地址,需要重新看日志或按官方/社区资料更新。

## systemd 服务

`cloudflared` 服务没有把 token 放进命令行。token 在环境文件里,避免 `ps` 直接看到敏感内容。

```ini
[Unit]
Description=Cloudflare Tunnel
After=network-online.target xray.service cloudflared-output-proxy.service
Wants=network-online.target
Requires=xray.service cloudflared-output-proxy.service

[Service]
Type=simple
User=root
Group=cloudflared_proxy
TimeoutStartSec=30
EnvironmentFile=/etc/cloudflared/tunnel.env
Environment=TUNNEL_NO_PRECHECKS=true
Environment=TUNNEL_TRANSPORT_PROTOCOL=http2
Environment=NO_PROXY=127.0.0.1,localhost,::1,192.168.10.0/24,100.64.0.0/10,fd7a:115c:a1e0::/48,.local
ExecStart=/usr/local/bin/cloudflared --no-autoupdate tunnel --protocol http2 --edge-ip-version 4 run
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target
```

`cloudflared-output-proxy.service` 负责加载 nft 表:

```ini
[Unit]
Description=cloudflared output transparent proxy nft rules
After=xray.service
Requires=xray.service
Before=cloudflared.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStartPre=-/usr/sbin/nft delete table inet cloudflared_output_proxy
ExecStart=/usr/sbin/nft -f /etc/cloudflared/output-proxy.nft
ExecStop=-/usr/sbin/nft delete table inet cloudflared_output_proxy

[Install]
WantedBy=multi-user.target
```

这里故意让 `cloudflared.service` 依赖 `cloudflared-output-proxy.service`。如果 nft 规则没有加载,`cloudflared` 就不应该悄悄直连出去。

## 为什么使用 HTTP/2 而不是 QUIC

Cloudflare Tunnel 默认常见路径会使用 QUIC,也就是 UDP 7844。但这次要做的是本机 OUTPUT 透明代理,而且要经过 Xray 的 TCP 代理出站。

当前稳定配置明确使用:

```text
--protocol http2
TUNNEL_TRANSPORT_PROTOCOL=http2
```

也就是让 `cloudflared` 使用 TCP 7844。这样 nft 的 `nat output redirect` 和 Xray 的 `dokodemo-door followRedirect` 能稳定拿到原始目标。

之前试过 UDP 透明转发,结果不稳定。Xray 日志里能看到 UDP 原始目标被读成了本机重定向端口,类似:

```text
accepted udp:127.0.0.1:1062
```

这说明 UDP REDIRECT 这条链路没有拿到 Cloudflare edge 的原始目标,不能作为这次的正式方案。

## 实际验证

服务状态:

```sh
systemctl is-active cloudflared
systemctl is-enabled cloudflared
systemctl is-active cloudflared-output-proxy
systemctl is-enabled cloudflared-output-proxy
systemctl is-active xray
```

当前结果:

```text
cloudflared: active / enabled
cloudflared-output-proxy: active / enabled
xray: active
```

`cloudflared` 当前进程:

```text
root cloudflared_proxy /usr/local/bin/cloudflared --no-autoupdate tunnel --protocol http2 --edge-ip-version 4 run
```

命令行里没有 token。

Tunnel 注册日志:

```text
Registered tunnel connection connIndex=0 ip=198.41.192.167 location=hkg01 protocol=http2
Registered tunnel connection connIndex=1 ip=198.41.200.63  location=hkg10 protocol=http2
Registered tunnel connection connIndex=2 ip=198.41.192.77  location=hkg01 protocol=http2
Registered tunnel connection connIndex=3 ip=198.41.200.33  location=hkg10 protocol=http2
```

这比之前直连时的 `sjc` 更符合预期。当前接入点变成了香港,说明 `cloudflared` 的 edge 连接确实从 Xray 出口出去。

Xray access log 能看到:

```text
cloudflared-local-redir >> default:proxy-out
```

这说明 nft 重定向命中了,Xray 也把这条流量送到了默认代理出站。

活跃连接也能看到 `cloudflared` 到 TCP 7844:

```sh
sudo ss -tnp | grep cloudflared | grep ':7844'
```

期望看到类似:

```text
ESTAB 192.168.10.20:<port> 198.41.192.167:7844 users:cloudflared
ESTAB 192.168.10.20:<port> 198.41.200.63:7844  users:cloudflared
```

注意:`ss` 看到的仍然是原始 edge IP,这正是 `routeOnly` 想保留的效果。是否走了代理,要看 Xray access log 和 edge location。

## 踩过的坑

### 1. 只设置 HTTP_PROXY 不可靠

试过在 `cloudflared.service` 里加:

```ini
Environment=HTTP_PROXY=http://127.0.0.1:18080
Environment=HTTPS_PROXY=http://127.0.0.1:18080
```

这不能可靠地把 Cloudflare edge 连接送进 Xray。实际看 `ss`,`cloudflared` 仍然可能直接连 `:7844`。所以最终没有用普通 HTTP 代理环境变量,而是用 nft 在 OUTPUT 链做透明重定向。

### 2. 透明转发但不开 CF edge 网段不行

只做本机代理入口,不抓 Cloudflare Tunnel edge IP,`cloudflared` 还是会直连。

这次关键网段是:

```text
198.41.192.0/24
198.41.200.0/24
```

nft 必须明确把这些目标重定向到 `1061`。

### 3. sniff 开了但没有 routeOnly 会坏

这个配置语法合法,但会导致 tunnel 失败;也不适合再作为普通入站模板:

```json
{
  "sniffing": {
    "enabled": true,
    "destOverride": [
      "fakedns",
      "quic",
      "tls",
      "http"
    ],
    "ipsExcluded": [
      "geoip:cloudflare"
    ]
  }
}
```

实际错误:

```text
TLS handshake with edge error: EOF
```

原因是 sniff 仍然可能影响连接目标或后续解析。`cloudflared` 需要保持原始 edge IP。另一方面,普通入站如果用 `geoip:cloudflare` 做大范围排除,也会把很多 Cloudflare CDN 后面的第三方站点一起粗暴排掉。

### 4. 加显式 CIDR 但没有 routeOnly 也不稳

也试过:

```json
{
  "sniffing": {
    "enabled": true,
    "destOverride": [
      "fakedns",
      "quic",
      "tls",
      "http"
    ],
    "ipsExcluded": [
      "geoip:cloudflare",
      "198.41.192.0/24",
      "198.41.200.0/24"
    ]
  }
}
```

它能通过 Xray 配置测试,但 `cloudflared` 仍然出现 EOF。说明 `ipsExcluded` 对这条透明重定向链路不够稳。

### 5. routeOnly 才是这条链路的关键

最终能稳定工作的区别是:

```json
"routeOnly": true
```

这样 Xray 可以 sniff 到 `cftunnel.com`,但不会把实际连接目标从 `198.41.x.x:7844` 改掉。

### 6. 不要按 root 全局转发

`cloudflared` 当前是 root 运行。如果 nft 写成按 uid root 抓,容易把系统服务、Xray 自己、定时任务都抓进去。

正式方案按 group 抓:

```nft
meta skgid <cloudflared_proxy_gid>
```

这个 group 只给 `cloudflared` 用,影响面比较清楚。

### 7. token 不要放进 ExecStart

不要写成:

```ini
ExecStart=/usr/local/bin/cloudflared tunnel run --token <token>
```

这样 `ps` 能看到 token。当前使用:

```ini
EnvironmentFile=/etc/cloudflared/tunnel.env
```

服务进程参数里不会出现 token。

## 操作顺序

实际落地时按这个顺序最稳:

```sh
sudo groupadd -r cloudflared_proxy 2>/dev/null || true
getent group cloudflared_proxy
getent group cloudflared_proxy | cut -d: -f3
```

1. 先给 Xray 加 `cloudflared-local-redir` 入站。
2. 用 `xray run -test -config` 验证配置。
3. 创建 `cloudflared_proxy` 组,并用 `getent` 查出实际 GID。
4. 写 `/etc/cloudflared/output-proxy.nft`,把 `<cloudflared_proxy_gid>` 替换成上一步查到的数字。
5. 写 `cloudflared-output-proxy.service`。
6. 修改 `cloudflared.service`,让它用 `Group=cloudflared_proxy`。
7. `systemctl daemon-reload`。
8. 重启 Xray。
9. 启动 `cloudflared-output-proxy`。
10. 重启 `cloudflared`。
11. 用日志和 `ss` 验证。

验证命令:

```sh
systemctl is-active cloudflared xray cloudflared-output-proxy
systemctl is-enabled cloudflared cloudflared-output-proxy
ps -o pid,user,group,args -C cloudflared
journalctl -u cloudflared -n 120 --no-pager | grep 'Registered tunnel connection'
sudo tail -160 /var/log/xray/access.log | grep cloudflared-local-redir
sudo ss -tnp | grep cloudflared | grep ':7844'
```

## 回滚

如果以后 Cloudflare edge 网段变化,或者这条代理路径不稳定,可以先回滚到直连,保证 tunnel 可用。

```sh
sudo systemctl stop cloudflared
sudo systemctl disable --now cloudflared-output-proxy
sudo nft delete table inet cloudflared_output_proxy 2>/dev/null || true
```

然后把 `cloudflared.service` 改回不依赖 output proxy 的版本:

```ini
[Unit]
Description=Cloudflare Tunnel
After=network-online.target xray.service
Wants=network-online.target
Requires=xray.service

[Service]
Type=simple
TimeoutStartSec=30
EnvironmentFile=/etc/cloudflared/tunnel.env
Environment=TUNNEL_NO_PRECHECKS=true
Environment=TUNNEL_TRANSPORT_PROTOCOL=http2
Environment=NO_PROXY=127.0.0.1,localhost,::1,192.168.10.0/24,100.64.0.0/10,fd7a:115c:a1e0::/48,.local
ExecStart=/usr/local/bin/cloudflared --no-autoupdate tunnel --protocol http2 --edge-ip-version 4 run
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target
```

Xray 里可以删除 `cloudflared-local-redir` 入站,然后:

```sh
sudo systemctl daemon-reload
sudo systemctl restart xray
sudo systemctl enable --now cloudflared
sudo systemctl restart cloudflared
```

回滚后的验证重点是看 `cloudflared` 是否重新注册 4 条 tunnel connection。

## 参考

- Xray inbound sniffing 文档:`https://xtls.github.io/en/config/inbound.html#sniffingobject`
- Xray routing 文档:`https://xtls.github.io/en/config/routing.html`
- Cloudflare Tunnel 透明代理社区讨论:`https://github.com/cloudflare/cloudflared/issues/1025`
- Cloudflare Tunnel 经代理的社区实践:`https://blog.xmgspace.me/archives/cloudflare-tunnel-via-proxy.html`
标签: 暂无
最后更新:2026年6月20日

leconio

这个人很懒,什么都没留下

点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

归档

  • 2026 年 6 月

分类

  • 服务器
  • 网络

COPYRIGHT © 2026 ASIDE0. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang

中文EN