(待测试)跨境数字化完全指南:西班牙 ↔ 中国大陆

跨境数字化完全指南:西班牙 ↔ 中国大陆

适用场景: 旅居西班牙的华人回国期间,需要访问西班牙政府系统、维持远程办公,以及无人值守地运维留守在家的树莓派节点。 版本: v3.5 | 覆盖:Cloudflare Tunnel 穿透(强制 TCP + 出口节点验证 + 三层封锁对策)· Zero Trust 访客隔离 · 树莓派自愈监控(已纳入 Xray Reality 健康检查)· CF Workers 轻量转发 · Xray Reality 西班牙 IP 保证方案(住宅 IP + DDNS + Fallback 伪装 + 系统加固,含跨节点 ufw 端口清单)


目录


一、为什么不用免费 VPS?

回国前许多人第一反应是找一台免费 VPS(如甲骨文 Always Free)。这条路有两个致命缺陷:

问题说明
无西班牙区域甲骨文等大厂欧洲免费节点通常只在法兰克福、阿姆斯特丹、伦敦,不提供马德里区域免费额度
机房 IP 被拦截政府风控系统会识别 Data Center IP 并返回 403 Forbidden,住宅 IP 才能通过

结论: 最完美的西班牙住宅原生 IP 其实就在你家里的树莓派上。


二、核心方案:树莓派 + Cloudflare Tunnel

2.1 架构原理

国内设备(WARP 客户端)
        │
        ▼
Cloudflare Zero Trust 边缘网络(身份验证)
        │
        ▼
西班牙家中树莓派(cloudflared 隧道守护进程)
        │
        ▼
西班牙家庭宽带出口 → 目标网站

目标网站(如西班牙税务局 Agencia Tributaria)看到的是你家的西班牙住宅宽带 IP,100% 原生,无法被风控识别。

关键优势: 树莓派不需要公网 IP,不需要配置 DDNS,不需要路由器端口映射。cloudflared 只做纯主动出站(Outbound-Only)连接,安全且不暴露任何入站攻击面。

2.2 树莓派端部署

# 先将 Token 存入环境变量文件(避免 Token 暴露在进程列表)
echo "TUNNEL_TOKEN=你的ZeroTrustToken" > /home/pi/cf_monitor/.env
chmod 600 /home/pi/cf_monitor/.env

# 国内节点:强制 TCP 协议,规避 UDP 封锁
docker run -d \
  --name cf-tunnel \
  --restart always \
  --env-file /home/pi/cf_monitor/.env \
  cloudflare/cloudflared:latest \
  tunnel --no-autoupdate run --protocol tcp

# 西班牙节点:无需强制 TCP,可省略 --protocol 参数
docker run -d \
  --name cf-tunnel \
  --restart always \
  --env-file /home/pi/cf_monitor/.env \
  cloudflare/cloudflared:latest \
  tunnel --no-autoupdate run

Token 在 Cloudflare Zero Trust 后台 → Networks → Tunnels → 新建 Tunnel 时自动生成。

为什么国内节点要加 --protocol tcp cloudflared 默认使用 QUIC(UDP)连接 Cloudflare 边缘,国内运营商对跨境 UDP 长连接有 QoS 随机丢包或直接阻断策略。强制 TCP 后,隧道改走标准 HTTP/2 over TCP 443,与普通 HTTPS 流量特征一致,稳定性大幅提升。

自 cloudflared v2026.5.2 起,启动时会自动执行连通性预检,分别测试 UDP 和 TCP 在端口 7844 的可达性,并在 CLI 输出修复建议。国内看到 UDP 检查失败属预期行为,TCP 通道正常即可。

关于防火墙: 国内节点的 cloudflared 容器和 cf_governor.py 监控脚本都只发起出站连接,不监听任何入站端口,因此不需要配置 ufw 或开放任何端口。如果计划装 ufw,默认的 allow outgoing 策略足够,不要额外配置入站规则,否则可能误拦截 cloudflared 自身的连接重试。第七节的 ufw 加固方案仅适用于跑 Xray Reality 的西班牙节点。

2.3 开启私有网络访问(远程办公/SSH)

在 Tunnel 详情页进入 路由流量 标签页,选择 创建 CIDR 路由(旧版 UI 显示为 Private Network 选项卡,新版已合并入路由流量页面):

  • CIDR(必需): 192.168.1.0/24
  • 描述(必需): 西班牙家庭局域网

保存后流量会经过 Cloudflare Gateway 转发,国内设备需安装 WARP 客户端才能访问此网段。

配置完成后,你在国内可以直接通过内网 IP 访问家中所有设备:

# 国内直接 SSH 登录家中树莓派(如同在家里)
ssh [email protected]

# 直接访问家中 NAS 管理后台
http://192.168.1.xxx:5000

2.4 Split Tunnels 分流配置

WARP 客户端默认将私有网段排除在隧道外,需要手动修正:

  1. 进入 Zero Trust 后台 → Settings → WARP Client → Profile settings
  2. 找到默认策略,点击 Edit,滚动到底部的 Split Tunnels
  3. 若模式为 Exclude,在列表中找到 192.168.0.0/16删除,使家庭内网流量进入隧道

2.5 日常使用场景切换

场景操作
日常上网(访问百度、刷手机)关闭 WARP。WARP 开启时所有流量经 Cloudflare 边缘中转,访问国内网站反而绕远,速度变慢甚至不通
办政府业务(税务局、Cl@ve 等)⚠️ 未在国内环境验证。 此前测试是在西班牙本地网络下进行,WARP 就近接入了马德里节点,出口自然是西班牙 IP——这不代表回国后的结果。Cloudflare 在中国大陆境内同样部署了大量边缘节点,WARP 回国后可能就近接入境内节点,导致出口变成中国 IP,而非西班牙 IP。回国后必须重新验证,见下方实测方法。如果出口不是西班牙 IP,改用第七节 Xray Reality(住宅 IP 直出,结果确定)
SSH 连家里设备开启 WARP,随时可用,无需其他设置

注意: WARP 客户端本身没有"全局代理开关"按钮。切换全流量路由的正确方式是在 Zero Trust 后台 Split Tunnels 里将模式从 Exclude 改为 Include 并添加 0.0.0.0/0,或在单次使用后再切回去。

2.5.1 回国后必做:验证出口节点是否为西班牙 IP

办政府业务前,务必先确认 WARP 当前连接的出口节点位置,因为 Cloudflare 在中国大陆境内同样部署了大量边缘节点(北京、上海、广州等),WARP 按"就近接入"逻辑,回国后有较大概率连接境内节点,导致出口变成中国 IP 而非西班牙 IP:

# WARP 开启状态下执行
curl https://www.cloudflare.com/cdn-cgi/trace

输出中的 colo= 字段是三字码数据中心代号:

colo 代码含义出口 IP 是否符合预期
MAD马德里✅ 符合,西班牙 IP
PEK / SHA / CAN北京/上海/广州等境内节点❌ 不符合,中国 IP,需切换方案
其他境外代码(如 HKGNRTSIN香港/东京/新加坡等⚠️ 非西班牙 IP,同样不满足"西班牙 IP"要求

如果不是 MAD 直接改用第七节 Xray Reality 方案。该方案流量从 Pi 400 物理网卡直出,出口 IP 锁定为西班牙住宅 IP,不存在 Cloudflare 节点动态调度带来的不确定性,是这个场景下唯一能保证结果的方案。

2.6 国内严苛封锁下的连接对策

本节面向回国后 WARP 连接不稳定或完全无法使用的场景。若 2.5.1 节验证显示出口节点不是西班牙 IP,但你仍需要 WARP 用于内网访问/SSH(无需西班牙 IP 的场景),下面的对策同样适用;若需要确保西班牙 IP 出口,直接跳转第七节。

当前 GFW 封锁现状(2026 Q2)

协议状态说明
WireGuard(UDP)❌ 封锁cloudflared 默认协议,国内基本不可用
MASQUE(QUIC/UDP)❌ 封锁单个 QUIC Initial 包即可触发阻断
HTTP/2 over TCP 443✅ 目前可用cloudflared --protocol tcp 走此路径
WARP 客户端降级✅ 自动客户端自动从 WireGuard/MASQUE 降级到 TCP fallback

结论: cloudflared 强制 TCP(已在 2.2 节配置)+ WARP 客户端自动降级,是目前在国内最稳定的组合。

重要澄清:免费版 WARP 不会接入中国境内节点。 Cloudflare 确实运营着面向企业的 China Network(与 JD Cloud 合作,境内约 35 个数据中心),以及 Global Acceleration(与中国移动国际、CBC Tech、JD Cloud 合作的专线服务),但这两者都是企业付费产品,个人免费账户无法接入。回国后 WARP 能连上时,实际连接的仍是中国大陆境外最近的 Cloudflare 边缘节点(如香港、东京),通过 TCP fallback 穿透 GFW,而非真正接入了境内基础设施。这意味着连接质量始终受限于跨境链路本身,不会因为"人在国内"而自动变得更快更稳。


第一层:WARP 连接不稳定

换运营商网络测试。 三大运营商对 Cloudflare 的封锁力度差异明显,移动最严,联通和电信相对宽松。手机开热点切换网络是最快的验证方式。

确认桌面端已设置 MASQUE 协议:

Zero Trust 后台 → Settings → Network → Gateway → Client
→ 找到设备配置 → Edit → Tunnel protocol → MASQUE → 保存

注意:移动端(手机)官方不支持 MASQUE,保持 WireGuard 即可,依赖客户端自动降级。


第二层:WARP 完全无法连接,仍需 SSH 访问树莓派

放弃 WARP 客户端,改用 cloudflared access 命令行工具直接建立 SSH 连接。此方式完全走 TCP 443,与 WARP 协议无关:

第一步:在树莓派(西班牙端)配置 SSH 公共主机名

在 Tunnel 路由流量页面添加公共主机名:

  • 子域名:ssh
  • 域名:yourdomain.com(你托管在 CF 的域名)
  • 服务类型:SSH
  • URL:localhost:22

第二步:在 Zero Trust 后台添加 SSH 应用

进入 Access → Applications → Add an application → Self-hosted

  • 应用名称:Home SSH
  • 域名:ssh.yourdomain.com
  • 策略:仅允许你的邮箱

第三步:国内设备安装 cloudflared 并连接

# macOS
brew install cloudflared

# Windows(PowerShell)
winget install Cloudflare.cloudflared

# 连接(浏览器会弹出 Access 验证)
cloudflared access ssh --hostname ssh.yourdomain.com
# 或直接用 ssh ProxyCommand
ssh -o ProxyCommand="cloudflared access ssh --hostname ssh.yourdomain.com" [email protected]

此方式完全不依赖 WireGuard 或 MASQUE,TCP 443 通则必通。

备用:浏览器 Web SSH(零客户端安装)

直接在浏览器访问 https://ssh.yourdomain.com,通过 Access 邮箱验证后,Cloudflare 在浏览器内渲染一个完整终端,无需安装任何客户端,手机也可用。


第三层:TCP 443 也遭遇 SNI 定点干扰

极少数情况下,某些本地宽带对特定域名的 HTTPS 连接会触发 SNI 检测并重置。此时 cloudflared 隧道本身不受影响(走 region1/region2.v2.argotunnel.com,非你的自定义域名),但 WARP 客户端可能受影响。

对策:切换到手机移动数据网络,或更换 DNS 为 1.1.1.1(DoH),避开运营商 DNS 污染后重试。


三、进阶:临时邀请朋友并安全隔离

如果朋友也需要借用你的西班牙住宅 IP 办业务,可以在 Cloudflare 层面给他划定一个严格的"访问格子间"。

3.1 身份层放行(无需共享账号密码)

  1. 进入 Settings → Authentication,确认已开启 One-time PIN (OTP) 邮箱验证码登录
  2. 进入 Settings → WARP Client → Device enrollment rules,添加朋友邮箱(如 [email protected]
  3. 朋友下载官方 WARP 客户端,使用自己的邮箱收验证码登录即可,全程不接触你的账号密码

3.2 网络策略隔离(两条防火墙规则)

进入 Network → Policies,按顺序添加以下两条规则(顺序不可颠倒):

规则 1 — 放行精确目标设备:

字段
SelectorUser Email = [email protected] AND Destination IP in 192.168.1.100/32
ActionAllow

/32 表示精确到单台设备(你的树莓派固定 IP),只放行对它的访问。

规则 2 — 阻断整个网段:

字段
SelectorUser Email = [email protected] AND Destination IP in 192.168.1.0/24
ActionBlock

规则 1 命中后不再匹配规则 2,朋友只能访问树莓派,无法扫描你家其他设备。

3.3 SSH 层二次加固

如果朋友需要 SSH 登录树莓派,在树莓派上额外操作:

# 为朋友创建独立低权限账户
sudo adduser friend_temp

# 在 sshd_config 末尾追加(注意必须实际换行,不是 \n)
sudo tee -a /etc/ssh/sshd_config << 'EOF'

Match User friend_temp
    AllowTcpForwarding no
    X11Forwarding no
EOF

sudo systemctl restart ssh

AllowTcpForwarding no 彻底切断利用端口转发嗅探内网其他设备的可能。

事情办完后撤权: 在 Zero Trust 后台 My Team → Devices 找到朋友设备,点击 Revoke 即立即失效。


四、CF Workers 轻量级转发节点

Cloudflare Workers 可以实现轻量级 HTTP/SOCKS 转发,并可作为临时出口节点使用。在深入配置之前,有四点需要明确认识,避免对这个方案产生错误预期。

4.0 使用前须知

① 合规风险(明确禁止,非灰色地带)

Cloudflare 自 2024 年 12 月起在 Self-Serve Agreement 中明确规定:不得利用 Cloudflare 服务提供 VPN 或类似代理服务。用 Workers 搭建个人代理出口节点在条款层面属于明确禁止的行为,Cloudflare 有权暂停服务、限制功能或封禁账号。社区观察到许多个人项目仍在运行,但这是经验现象而非官方承诺,Cloudflare 随时可能调整执法策略。节点配置不要公开传播。

② 性能限制(不适合流媒体)

Workers 受请求额度、CPU 时间、连接持续时间和出口带宽管理等多重限制约束,更适合实验、临时访问或低流量场景。免费版每日 10 万次请求上限听起来宽裕,但视频流量中 HLS/DASH 分片请求、Range 请求、字幕、API 调用叠加后消耗极快。长时间 4K 视频、Plex/Jellyfin、持续代理全部家庭流量,均属于 Cloudflare 重点打击的历史场景。Workers 不应视为稳定的流媒体代理方案。

③ 出口位置不保证是美国

Workers 的执行节点由 Cloudflare Anycast 网络调度,用户在西班牙时 Worker 很可能在马德里或巴黎执行,出口 IP 不一定是美国。需要额外配合 Cloudflare Tunnel、WARP 或第三方 VPS 才能实现稳定的美国出口。Workers 本身并不天然等于美国节点。

④ 现实评估

在 2026 年的实际环境下,Workers 作为长期方案的稳定性和合规性均明显弱于一台低配美国 VPS(约 3~5 美元/月)或正规 VPN 服务。以下配置仅供短期应急或技术实验参考。

4.1 推荐项目:BPB-Worker-Panel

经代码审查,目前维护状态和代码质量最佳的项目是 bia-pain-bache/BPB-Worker-Panel(TypeScript 编写,GPL-3.0 开源,2 天前有新版本发布)。

代码安全性审查结论:

入口文件 src/worker.ts 仅 63 行,逻辑透明:收到请求后初始化全局参数,WebSocket 走 VLESS/Trojan 协议处理器,HTTP 请求按路径分发。源码中无可疑的第三方 API 外联或数据上报逻辑,构建产物可本地独立审查。

功能集:

支持 VLESS/Trojan 双协议、GUI 管理面板、订阅系统、私有 DoH 服务器、Fragment/Warp/ECH 混淆,适配 Sing-box、Clash/Mihomo、Xray 全主流客户端。对临时出口场景功能偏重,但不是缺点。

与其他项目的区别:

  • zizifn/edgetunnel(原版):原作者已停止维护,仅保留仓库作纪念
  • cmliu/edgetunnel(fork):活跃但 Issues 中存在未关闭的数据泄露安全质疑(#1097、#1135),不推荐

更诚实的替代方案评估:

对于只需要偶尔访问境外网站的场景,一台 3~5 美元/月的美国 VPS + 单文件 sing-box 或 xray,在稳定性、合规性、出口位置可控性上均优于 CF Workers 方案。Workers 方案的唯一优势是零成本。

4.2 部署步骤

方式 A:BPB-Wizard 一键脚本(最简单)

# Linux / macOS / Termux
bash <(curl -fsSL https://raw.githubusercontent.com/bia-pain-bache/BPB-Wizard/main/install.sh)

脚本自动登录 Cloudflare 账户,所有配置项均有安全默认值,直接回车即可,最后自动打开面板页面。

⚠️ 安全提示: Wizard 脚本未经证书签名,Windows 杀毒软件可能报 Trojan/Downloader 警告。这是已知误报(原作者在 README 中明确说明),但运行前需自行判断风险。如不接受,请使用方式 B 手动部署。

方式 B:手动部署(约 10 分钟)

  1. 前往 Releases 页面,下载 worker.js(如遇 1101 错误则改下载 unobfuscated-worker.js
  2. Cloudflare 后台 → Workers & Pages → Create Application → Worker,命名时避免包含"bpb"字样,上传 worker.js,点击 Save and Deploy
  3. 左侧菜单 → KV → Create,创建一个命名空间(名称随意,如 panel-kv
  4. 返回 Worker → Settings → Bindings → Add binding → KV Namespace,变量名填写 kv必须小写),选择刚建的命名空间,保存并重新部署
  5. Settings → Variables and Secrets,添加两个变量:
    • UUID(大写):访问 https://your-worker.workers.dev/ 报错页面中的链接获取
    • TR_PASS(大写):同上页面获取
  6. 访问 https://your-worker.workers.dev/panel,首次登录时设置管理员密码

后续更新: 只需下载新版 worker.js 覆盖上传即可,KV 中存储的配置会自动保留。

常见故障排查:

症状原因解决
Error 1101混淆脚本兼容性问题改用 unobfuscated-worker.js
面板无法加载KV 绑定名称错误确认绑定变量名为小写 kv
UUID/密码报错环境变量未设置确认 UUIDTR_PASS 均为大写

4.3 绑定自定义域名

进入 Worker 页面 → Settings → Triggers → Custom Domains,绑定你托管在 Cloudflare 上的域名,例如 us.yourdomain.com

必须绑定自定义域名,原因有两个:*.workers.dev 默认域名已被 GFW 封锁(DNS 污染);自定义域名同时提供 TLS 证书和 SNI 伪装,显著提升连通稳定性。

4.4 国内封锁现状与 IP 优选策略

当前国内对 Cloudflare 的封锁结构如下:

层面现状
*.workers.dev 默认域名GFW DNS 污染封锁,不可用
自定义域名目前可达,需配合优选 IP 使用
IPv4 出口部分 IP 段遭运营商 QoS 干扰,刚优选完的 IP 可能立刻超时,稳定性下降
IPv6 出口目前相对畅通,可作为 IPv4 受阻时的备用路径

IPv4 优选(主力,工具名已更新为 cfst):

# v2.3.x 起二进制名从 CloudflareSpeedTest 改为 cfst
./cfst -ip 162.159.200.0/24,108.162.192.0/18 -sl 5 -f result_v4.csv -p 5

IPv6 优选(备用,需本机有 IPv6 地址):

# Cloudflare 所有可用 IPv6 集中在 2606:4700::/32
./cfst -ip 2606:4700::/32 -sl 3 -f result_v6.csv -p 5

IPv6 当前处于"打擦边球"的缓冲期,稳定性存在地区和运营商差异(移动用户反馈更不稳定),随时可能被针对性封锁,不建议作为唯一依赖。

客户端配置:

字段IPv4 填写IPv6 填写
地址 (Address)108.162.x.x(优选结果)[2606:4700:xxxx::1](需加方括号)
端口443443
UUID你在 4.1 生成的 UUID同左
传输层WebSocketWebSocket
Host / SNIus.yourdomain.comus.yourdomain.com
TLS开启开启

IPv4 卡顿时切换到 IPv6 地址,或重新运行优选工具换 IP,Worker 端无需任何改动。


五、树莓派无人值守自愈监控体系

出发回国后,家中树莓派将无人值守运行。本节提供一套完整的监控自愈方案,覆盖从软件崩溃到物理断电的所有故障场景。

5.1 报警架构设计

原始方案让国内节点直接发 iCloud 邮件,但 smtp.mail.me.com 在中国大陆被封锁,报警会静默失败。修正后采用两级机制:

国内节点检测到故障
      │
      ├─ 隧道仍然存活 ──→ 经隧道 POST 到西班牙节点中继接口
      │                         │
      │                         ▼
      │                   西班牙节点通过 iCloud SMTP 发出邮件
      │                         │
      │                         ▼
      │                   你的手机收到推送通知
      │
      └─ 隧道完全断线 ──→ Healthchecks.io 超时未打卡
                               │
                               ▼
                         海外平台主动触发报警(Telegram / 邮件)
故障类型触发路径覆盖率
容器崩溃但网络正常中继 → iCloud 邮件
隧道断线后自愈中继 → iCloud 邮件(恢复通知)
隧道完全断线无法中继Healthchecks.io 心跳超时
整机断电 / 物理断网Healthchecks.io 心跳超时

5.2 前置准备

① 申请 iCloud App 专用密码(西班牙节点发信用)

  1. 登录 appleid.apple.com(非国区 Apple ID)
  2. 进入 登录和安全 → App 专用密码,生成并命名为 pi-monitor-es
  3. 复制 16 位密码(格式:xxxx-xxxx-xxxx-xxxx

App 专用密码与 Apple ID 主密码完全隔离,可随时单独吊销,不影响账号安全。

② 注册 Healthchecks.io 心跳频道

  1. 注册 healthchecks.io(免费套餐支持 20 个频道)
  2. 新建 Check,周期设为 15 分钟,宽限期 30 分钟
  3. 在 Integrations 绑定 Telegram 或邮件
  4. 复制打卡 URL(形如 https://hc-ping.com/你的UUID

5.3 部署文件

所有文件统一放置在两台树莓派的 /home/pi/cf_monitor/ 目录。

config.py — 公共配置(两台节点各自维护)

# /home/pi/cf_monitor/config.py

# ── 节点身份 ──────────────────────────────────────────────
NODE_ROLE = "china"          # 国内节点: "china" | 西班牙节点: "spain"

# ── 本地网络 ──────────────────────────────────────────────
LOCAL_GATEWAY          = "192.168.31.1"    # 西班牙节点改为 192.168.1.1
TUNNEL_CONTAINER_NAME  = "cf-tunnel"       # Docker 容器名

# ── 连通性检测目标(与隧道域名无关,避免循环依赖)──────────
HEALTH_CHECK_URL     = "https://www.cloudflare.com"
HEALTH_CHECK_TIMEOUT = 6

# ── 西班牙节点中继接口(国内节点填写,经隧道内网访问)──────
SPAIN_RELAY_URL = "http://192.168.1.xxx:9731/alert"  # 替换为实际内网 IP

# ── iCloud SMTP(仅西班牙节点使用)──────────────────────
SMTP_SERVER    = "smtp.mail.me.com"        # iCloud 官方 SMTP(非 163/QQ)
SMTP_PORT      = 587                       # STARTTLS 标准端口
EMAIL_USER     = "[email protected]"      # 非国区 iCloud 邮箱
EMAIL_PASSWORD = "xxxx-xxxx-xxxx-xxxx"     # App 专用密码(非主密码)
EMAIL_RECEIVER = "[email protected]"      # 接收报警的邮箱

# ── Healthchecks.io 心跳(两台节点各用独立 URL)─────────
HEALTHCHECK_PING_URL = "https://hc-ping.com/你的UUID"

# ── Cloudflare IP 优选参数(仅国内节点使用)─────────────
CF_IP_RANGES = "162.159.200.0/24,108.162.192.0/18"
CF_MIN_SPEED = 10    # MB/s

# ── Xray Reality 监控(仅西班牙节点使用,见第七节)───────
XRAY_ENABLED      = True            # 西班牙节点部署了 Xray 则设为 True,国内节点保持 False
XRAY_SERVICE_NAME = "xray"          # systemd 服务名
XRAY_LISTEN_PORT  = 443             # Xray Reality 监听端口

cf_governor.py — 主监控脚本(两台节点通用)

#!/usr/bin/env python3
"""
cf_governor.py — Cloudflare Tunnel 全自动监控与自愈脚本
通过 config.py 区分节点角色,国内/西班牙节点通用同一份代码
"""

import os, sys, time, json, subprocess
import requests, smtplib
from email.mime.text import MIMEText
from email.header import Header
import config


# ── 邮件发送(仅西班牙节点直接调用)─────────────────────────

def send_email(title, message):
    """通过 iCloud SMTP 发送 HTML 格式报警邮件"""
    full_title = f"⚠️【树莓派监测站·{config.NODE_ROLE.upper()}{title}"
    html_body = f"""
    <html><body style="font-family:-apple-system,Helvetica,Arial;color:#333;line-height:1.6">
      <h2 style="color:#ff9500;border-bottom:1px solid #eee;padding-bottom:8px">{full_title}</h2>
      <div style="background:#f5f5f7;border-radius:8px;padding:16px;margin:16px 0">
        <strong>故障详情:</strong>
        <pre style="font-family:'SF Mono',Consolas,monospace;white-space:pre-wrap;
                    background:#fff;padding:12px;border-radius:4px;
                    border:1px solid #e5e5ea;margin:8px 0 0 0">{message}</pre>
      </div>
      <p style="font-size:11px;color:#86868b;margin-top:20px">
        发生时间:{time.strftime('%Y-%m-%d %H:%M:%S')}<br>
        发送节点:{config.NODE_ROLE} · 无人值守自动化运维
      </p>
    </body></html>
    """
    msg = MIMEText(html_body, "html", "utf-8")
    msg["From"]    = Header(f"树莓派守护 <{config.EMAIL_USER}>", "utf-8")
    msg["To"]      = Header(config.EMAIL_RECEIVER, "utf-8")
    msg["Subject"] = Header(full_title, "utf-8")

    try:
        srv = smtplib.SMTP(config.SMTP_SERVER, config.SMTP_PORT, timeout=15)
        srv.ehlo()
        srv.starttls()   # STARTTLS 强制加密,国内运营商无法窥探内容
        srv.ehlo()
        srv.login(config.EMAIL_USER, config.EMAIL_PASSWORD)
        srv.sendmail(config.EMAIL_USER, [config.EMAIL_RECEIVER], msg.as_string())
        srv.quit()
        print("✅ iCloud 邮件投递成功")
        return True
    except Exception as e:
        print(f"❌ iCloud 邮件投递失败: {e}")
        return False


# ── 报警分发(根据节点角色选择路径)─────────────────────────

def notify(title, message):
    """
    报警路由逻辑:
      西班牙节点 → 直接调用 iCloud SMTP
      国内节点   → 经隧道内网 POST 到西班牙中继接口,由对端发信
                   若隧道本身已断线,POST 请求失败属预期行为,
                   此时 Healthchecks.io 心跳超时负责兜底报警
    """
    print(f"\n[ALERT] {title}\n{message}\n")

    if config.NODE_ROLE == "spain":
        send_email(title, message)

    elif config.NODE_ROLE == "china":
        payload = {"title": title, "message": message, "ts": time.time()}
        try:
            r = requests.post(config.SPAIN_RELAY_URL, json=payload, timeout=8)
            if r.status_code == 200:
                print("✅ 告警已中继至西班牙节点")
            else:
                print(f"⚠️  中继接口返回异常: {r.status_code}")
        except requests.RequestException as e:
            print(f"⚠️  中继不可达(隧道可能已断线,心跳哨兵将兜底): {e}")


# ── 健康检查(三层递进)──────────────────────────────────────

def check_local_network():
    """第一层:物理层 — ping 本地网关"""
    ret = os.system(f"ping -c 1 -W 2 {config.LOCAL_GATEWAY} > /dev/null 2>&1")
    if ret != 0:
        return False, f"无法 ping 通本地网关 {config.LOCAL_GATEWAY},疑似 Wi-Fi 断开或停电。"
    try:
        requests.get(config.HEALTH_CHECK_URL, timeout=config.HEALTH_CHECK_TIMEOUT)
        return True, "OK"
    except requests.RequestException:
        return False, "本地网关正常,但公网出口异常(宽带欠费、光猫死机或骨干网故障)。"

def check_tunnel_process():
    """第二层:进程层 — 检查 cloudflared 容器状态"""
    try:
        # 使用列表参数,避免 shell=True 的 f-string 模板注入问题
        result = subprocess.check_output(
            ["docker", "inspect", "-f", "{{.State.Running}}", config.TUNNEL_CONTAINER_NAME],
            stderr=subprocess.DEVNULL
        ).decode().strip()
        if result != "true":
            return False, f"Tunnel 容器 [{config.TUNNEL_CONTAINER_NAME}] 已停止运行。"
        return True, "OK"
    except subprocess.CalledProcessError:
        return False, f"未找到容器 [{config.TUNNEL_CONTAINER_NAME}],或 Docker 守护进程异常。"

def check_outbound_connectivity():
    """第三层:连通层 — 独立于隧道域名的出口验证"""
    try:
        r = requests.get(config.HEALTH_CHECK_URL, timeout=config.HEALTH_CHECK_TIMEOUT)
        if r.status_code < 500:
            return True, "OK"
        return False, f"出口连通性检测返回 HTTP {r.status_code}。"
    except requests.RequestException as e:
        return False, f"出口连通性检测失败: {e}"


# ── Xray Reality 监控(仅西班牙节点,见第七节)────────────────

def check_xray_process():
    """检查 Xray systemd 服务是否处于 active 状态"""
    try:
        result = subprocess.check_output(
            ["sudo", "systemctl", "is-active", config.XRAY_SERVICE_NAME],
            stderr=subprocess.DEVNULL
        ).decode().strip()
        if result != "active":
            return False, f"Xray 服务 [{config.XRAY_SERVICE_NAME}] 状态异常: {result}"
        return True, "OK"
    except subprocess.CalledProcessError:
        return False, f"Xray 服务 [{config.XRAY_SERVICE_NAME}] 未运行或不存在"

def check_xray_port():
    """验证 Xray 确实在监听配置的端口(不只是进程存在,还要真的在服务)"""
    try:
        result = subprocess.check_output(
            ["ss", "-tlnp"],
            stderr=subprocess.DEVNULL
        ).decode()
        port_str = f":{config.XRAY_LISTEN_PORT} "
        if port_str not in result:
            return False, f"Xray 进程存在,但端口 {config.XRAY_LISTEN_PORT} 未在监听,可能配置错误或崩溃半挂起"
        return True, "OK"
    except subprocess.CalledProcessError as e:
        return False, f"端口检查命令执行失败: {e}"


# ── Healthchecks.io 心跳打卡 ─────────────────────────────────

def ping_healthcheck():
    """所有检查通过后打卡;超时未打卡则由海外平台触发兜底报警"""
    try:
        requests.get(config.HEALTHCHECK_PING_URL, timeout=5)
        print("💓 Healthchecks.io 打卡成功")
    except Exception as e:
        print(f"⚠️  打卡失败(非致命): {e}")


# ── Cloudflare IP 优选(仅国内节点,按需调用)────────────────

def run_cf_speedtest():
    cmd = [
        "./cfst",          # v2.3.x 起二进制名从 CloudflareSpeedTest 改为 cfst
        "-ip", config.CF_IP_RANGES,
        "-sl", str(config.CF_MIN_SPEED),
        "-f",  "result.csv",
        "-p",  "3"
    ]
    try:
        print("⏳ 正在执行 Cloudflare 出口 IP 优选...")
        subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        if os.path.exists("result.csv") and os.path.getsize("result.csv") > 10:
            with open("result.csv") as f:
                lines = f.readlines()
            if len(lines) > 1:
                cols = lines[1].strip().split(",")
                return cols[0], cols[1], cols[2]   # IP, 延迟ms, 速度MB/s
    except Exception as e:
        print(f"优选执行失败: {e}")
    return None, None, None


# ── 主流程 ────────────────────────────────────────────────────

def main():
    print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] 开始健康检查 | 节点: {config.NODE_ROLE}")

    # 第一层:物理网络
    net_ok, net_msg = check_local_network()
    if not net_ok:
        print(f"[FATAL] 本地网络异常(无法外发报警): {net_msg}")
        sys.exit(1)

    # 第二层:容器进程
    proc_ok, proc_msg = check_tunnel_process()
    if not proc_ok:
        print(f"[WARN] 容器异常,尝试重启: {proc_msg}")
        os.system(f"docker restart {config.TUNNEL_CONTAINER_NAME}")
        time.sleep(15)
        proc_ok2, _ = check_tunnel_process()
        if not proc_ok2:
            notify("Tunnel 容器重启失败",
                   f"原因: {proc_msg}\n\n已执行 docker restart,容器仍未恢复,请手动排查。")
            sys.exit(1)
        else:
            notify("Tunnel 容器自愈成功",
                   f"原因: {proc_msg}\n\n已通过 docker restart 恢复正常运行。")

    # 第三层:出口连通性
    conn_ok, conn_msg = check_outbound_connectivity()
    if not conn_ok:
        notify("出口连通性异常", conn_msg)
        sys.exit(1)

    # 第四层:Xray Reality 服务(仅西班牙节点,需在 config.py 中开启 XRAY_ENABLED)
    if config.NODE_ROLE == "spain" and config.XRAY_ENABLED:
        xray_ok, xray_msg = check_xray_process()
        if not xray_ok:
            print(f"[WARN] Xray 服务异常,尝试重启: {xray_msg}")
            os.system(f"sudo systemctl restart {config.XRAY_SERVICE_NAME}")
            time.sleep(5)
            xray_ok2, _ = check_xray_process()
            if not xray_ok2:
                notify("Xray Reality 重启失败",
                       f"原因: {xray_msg}\n\n已执行 systemctl restart,服务仍未恢复,"
                       f"国内访问西班牙住宅 IP 的备用通道当前不可用,请手动排查。")
                sys.exit(1)
            else:
                notify("Xray Reality 自愈成功",
                       f"原因: {xray_msg}\n\n已通过 systemctl restart 恢复正常运行。")

        # 进程活着之后,再验证端口确实在监听(防止半挂起假活状态)
        port_ok, port_msg = check_xray_port()
        if not port_ok:
            os.system(f"sudo systemctl restart {config.XRAY_SERVICE_NAME}")
            time.sleep(5)
            port_ok2, _ = check_xray_port()
            if not port_ok2:
                notify("Xray 端口监听异常",
                       f"原因: {port_msg}\n\n服务进程显示运行中,但端口未监听,"
                       f"已尝试重启仍未恢复,请检查 config.json 是否有效。")
                sys.exit(1)
            else:
                notify("Xray 端口异常已自愈", f"原因: {port_msg}\n\n重启后端口恢复监听。")

    # 全部通过:打卡
    ping_healthcheck()
    print("✅ 所有检查通过")

    # 可选:强制 IP 优选
    if "--force-opt" in sys.argv and config.NODE_ROLE == "china":
        ip, latency, speed = run_cf_speedtest()
        if ip:
            print(f"🚀 最优出口: {ip} | 延迟: {latency}ms | 速度: {speed}MB/s")
        else:
            print("当前无优于阈值的 IP,保持原链路。")


if __name__ == "__main__":
    main()

Xray 集成说明: 检查逻辑与 cloudflared 容器完全对称——进程异常 → 自动重启 → 二次验证 → 失败则报警、成功则发自愈通知,走的是同一条 notify() 报警链路(西班牙节点直发邮件),不需要额外配置报警通道。仅当 config.pyNODE_ROLE == "spain"XRAY_ENABLED = True 时才会执行,国内节点不受影响。

权限准备: cf_governor.py 通常以普通用户 pi 身份运行,但 systemctl restart xray 默认需要 root 权限。需要给 pi 用户开一条不输密码的 sudo 白名单,仅限这一条命令:

echo "pi ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart xray, /usr/bin/systemctl is-active xray" | sudo tee /etc/sudoers.d/xray-monitor
sudo chmod 440 /etc/sudoers.d/xray-monitor

同步把脚本里两处调用改为 sudo systemctl restartsudo systemctl is-active,权限收得很窄,不会扩大攻击面。

alert_relay.py — 西班牙节点告警中继服务

此服务在西班牙节点常驻运行,监听来自国内节点经隧道传入的告警请求,收到后通过本地 iCloud SMTP 发出邮件。

#!/usr/bin/env python3
"""
alert_relay.py — 西班牙节点告警中继服务
仅绑定内网接口,不对公网暴露
"""

from http.server import HTTPServer, BaseHTTPRequestHandler
import json, time
import config
from cf_governor import send_email

BIND_HOST = "0.0.0.0"
BIND_PORT = 9731

class RelayHandler(BaseHTTPRequestHandler):

    def do_POST(self):
        if self.path != "/alert":
            self.send_response(404)
            self.end_headers()
            return
        length = int(self.headers.get("Content-Length", 0))
        body = self.rfile.read(length)
        try:
            data = json.loads(body)
            send_email(f"[国内节点中继] {data.get('title','未知告警')}",
                       data.get("message", ""))
            self.send_response(200)
        except Exception as e:
            print(f"中继处理失败: {e}")
            self.send_response(500)
        self.end_headers()

    def log_message(self, fmt, *args):
        print(f"[{time.strftime('%H:%M:%S')}] RELAY {fmt % args}")

if __name__ == "__main__":
    print(f"🔁 告警中继服务启动,监听 {BIND_HOST}:{BIND_PORT}")
    HTTPServer((BIND_HOST, BIND_PORT), RelayHandler).serve_forever()

防火墙配置: 该服务监听 0.0.0.0,代码层面未限制来源,安全性依赖 ufw 规则兜底。具体命令见第 7.3.1 节"系统层:ufw 收紧暴露面"——若这台西班牙节点同时部署了第七节的 Xray Reality,两者的 ufw 规则需要合并配置,避免重复执行 ufw enable 互相覆盖。

5.4 Crontab 定时任务

在两台树莓派上分别执行:

chmod +x /home/pi/cf_monitor/cf_governor.py
crontab -e

追加:

# 每 10 分钟:健康检查 + 自愈 + 心跳打卡
*/10 * * * * cd /home/pi/cf_monitor && /usr/bin/python3 cf_governor.py >> monitor.log 2>&1

# 每 6 小时(国内节点专用):Cloudflare 出口 IP 优选
0 */6 * * * cd /home/pi/cf_monitor && /usr/bin/python3 cf_governor.py --force-opt >> opt.log 2>&1

5.5 西班牙节点:中继服务开机自启

sudo tee /etc/systemd/system/cf-alert-relay.service > /dev/null << 'EOF'
[Unit]
Description=Cloudflare Tunnel Alert Relay
After=network.target

[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/cf_monitor
ExecStart=/usr/bin/python3 /home/pi/cf_monitor/alert_relay.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now cf-alert-relay

六、出行前安全检查清单

出发前逐项确认,避免出国后无法补救:

Cloudflare 账户安全

  • Cloudflare 账户已开启 2FA 双重验证
  • Tunnel Token 已安全保存,不存储在明文配置文件中

树莓派物理可靠性

  • 树莓派通过网线直连光猫(非 Wi-Fi,避免长期无人时 Wi-Fi 休眠)
  • 执行一次断电自启测试(拔插电源),确认树莓派能自动拉起 Docker 和隧道容器
  • Docker 容器已设置 --restart always,断电重启后无需人工干预

监控脚本配置

  • 西班牙节点 config.pySMTP_SERVER = "smtp.mail.me.com"
  • EMAIL_PASSWORD 已填写 App 专用密码(非 Apple ID 主密码)
  • 国内节点 SPAIN_RELAY_URL 已填写西班牙树莓派的实际内网 IP
  • 两台节点各有独立的 HEALTHCHECK_PING_URL
  • 西班牙节点 cf-alert-relay 服务已启动(systemctl status cf-alert-relay
  • 两台节点 crontab 配置可见(crontab -l
  • 手动执行 python3 cf_governor.py 确认无报错
  • 发送一条测试告警,确认邮件到达手机

CF Workers 节点(如已部署)

  • 使用 BPB-Worker-Panel,Worker 命名未包含"bpb"字样
  • KV Namespace 已创建并绑定,变量名为小写 kv
  • UUIDTR_PASS 环境变量已设置(均为大写)
  • 自定义域名已绑定并 DNS 解析正常(*.workers.dev 默认域名在国内已被封锁)
  • 本地已保存 IPv4 和 IPv6 两份优选 IP 列表,开箱即用
  • 已知悉 ToS 限制,节点配置不对外传播

Xray Reality 备用节点(如已部署,详见第七节)

  • 路由器已将 443 TCP 转发到 Pi 400,内网 IP 已绑定固定地址
  • systemctl status xray 显示 active (running)
  • ss -tlnp | grep 443 确认监听正常
  • DDNS 脚本手动执行成功,CF DNS 记录橙色云朵已关闭
  • crontab 每 5 分钟 DDNS 任务已配置(crontab -l 可见)
  • 客户端填入 PublicKey + ShortId,测试连接成功
  • 与 WARP 方案并行配置,两套节点互为备份
  • outboundstagrouting.rulesoutboundTag 命名完全一致
  • Fallback 静态页面已部署(fallback-web 服务 active,curl localhost:8080 可见 403 页面)
  • ufw status verbose 确认仅 443 对公网开放,22 和 9731(如部署 alert_relay)仅限内网网段
  • fail2ban-client status 确认 xray jail 已启用
  • unattended-upgrades 已配置,系统可自动安装安全补丁
  • UUID / privateKey / shortId 未出现在任何公开仓库或聊天记录截图中
  • config.pyXRAY_ENABLED = True(西班牙节点)已设置
  • /etc/sudoers.d/xray-monitor 免密白名单已配置,sudo -l 可见对应规则
  • 手动执行一次 python3 cf_governor.py,确认 Xray 检查通过且无报错
  • 手动 sudo systemctl stop xray 模拟故障,确认 5 分钟内收到自愈或报警邮件,再手动启动验证

七、备用方案:Pi 400 + Xray Reality(住宅 IP 直连)

定位: 凡需要保证西班牙 IP 出口的场景(如 Cl@ve、税务局等强制要求西班牙本地访问的政府系统),本节是确定性方案,优先于 WARP 使用——WARP 的出口节点由 Cloudflare 动态就近调度,回国后可能落地境内节点而非马德里,无法保证西班牙 IP;本节方案流量从 Pi 400 物理网卡直出,出口锁定为西班牙住宅 IP,结果确定。同时也是 WARP 在国内完全连不上时的独立备用通道。基于真实住宅 IP,部署 VLESS + Reality + Vision,目前抗 GFW 检测能力最强的组合。

前提条件: Pi 400 有真实公网 IPv4、O2 路由器可配置端口转发。

7.1 安全架构说明

国内设备(v2rayN / Shadowrocket / Sing-box)
        │
        │  VLESS + Reality + Vision(TCP 443)
        │  流量特征:完全模拟真实 TLS 1.3 握手
        ▼
O2 路由器(端口转发 443 → Pi 400)
        │
        ▼
Pi 400(Xray 服务端,监听 443)
        │  GFW 主动探测时,Reality 将探测流量
        │  透传给真实的 dl.google.com,无法被识别
        ▼
西班牙家庭宽带出口(住宅 IP,GFW 极少封锁)

住宅 IP 的安全优势: GFW 封锁住宅 IP 误伤代价极高,策略上极度保守,不在主动扫描目标列表内。即使触发封锁,重启路由器通常即可换 IP。这是比任何 VPS 都安全的核心资产。


7.2 路由器端口转发配置

在 O2 路由器后台(通常 192.168.1.1)配置端口转发:

字段
外部端口443
内部 IPPi 400 的局域网 IP(如 192.168.1.xxx
内部端口443
协议TCP

固定 Pi 400 内网 IP: 在路由器 DHCP 设置里,将 Pi 400 的 MAC 地址绑定固定 IP,避免重启后内网 IP 变化导致转发失效。Pi 400 MAC 地址查询:ip link show eth0 | grep ether


7.3 Xray 安装与 Reality 配置

在 Pi 400 上执行:

# 安装 Xray
sudo bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install

# 生成 Reality 密钥对(私钥留服务端,公钥给客户端)
xray x25519

# 生成 UUID
xray uuid

# 生成 shortId(8位十六进制,可多生成几个备用)
openssl rand -hex 8

将三个输出值记录下来,填入下面的配置文件:

sudo nano /usr/local/etc/xray/config.json
{
  "log": {
    "loglevel": "warning",
    "access": "/var/log/xray/access.log",
    "error": "/var/log/xray/error.log"
  },
  "inbounds": [
    {
      "listen": "0.0.0.0",
      "port": 443,
      "protocol": "vless",
      "settings": {
        "clients": [
          {
            "id": "填入xray uuid生成的UUID",
            "flow": "xtls-rprx-vision"
          }
        ],
        "decryption": "none"
      },
      "streamSettings": {
        "network": "tcp",
        "security": "reality",
        "realitySettings": {
          "show": false,
          "dest": "dl.google.com:443",
          "xver": 0,
          "serverNames": [
            "dl.google.com"
          ],
          "privateKey": "填入x25519生成的Private key",
          "shortIds": [
            "填入openssl生成的8位hex"
          ]
        }
      },
      "sniffing": {
        "enabled": true,
        "destOverride": ["http", "tls", "quic"]
      }
    }
  ],
  "outbounds": [
    {
      "protocol": "freedom",
      "tag": "direct"
    },
    {
      "protocol": "blackhole",
      "tag": "blocked"
    }
  ],
  "routing": {
    "domainStrategy": "IPIfNonMatch",
    "rules": [
      {
        "type": "field",
        "ip": ["geoip:private"],
        "outboundTag": "blocked"
      }
    ]
  }
}

⚠️ 命名一致性提醒: outbounds 里定义的 tag 名称必须与 routing.rules 里引用的 outboundTag 完全一致(此处统一用 blocked)。这是实际部署中最容易踩的坑之一——如果两处字符串不匹配,Xray 不会报错提示,但路由规则会静默失效,进而导致连接异常。修改配置后务必用 sudo xray run -test -confdir /usr/local/etc/xray/ 校验语法。

伪装目标选择说明: dl.google.com 是最佳选择——全球 CDN 覆盖广、支持 TLS 1.3 + H2、延迟低、与 CF 边缘 IP 地理位置接近,GFW 绝对不会封锁。备选:www.apple.comcdn.cloudflare.com

# 关键步骤:授予绑定特权端口(443)的能力
# Xray 默认以低权限用户 nobody 运行,未授权时绑定 443 会静默失败
# (进程正常启动、日志无报错,但端口实际未监听)
sudo setcap cap_net_bind_service=+eip /usr/local/bin/xray

# 启动并设置开机自启
sudo systemctl enable xray
sudo systemctl start xray
sudo systemctl status xray

# 验证能力已正确授予(应输出 cap_net_bind_service=eip)
getcap /usr/local/bin/xray

# 验证监听端口
sudo ss -tlnp | grep 443

⚠️ 常见坑: 如果 getcap 输出为空,说明能力未生效,端口必然起不来,且 journalctl -u xray 不会有任何报错提示——Xray 会"成功启动"但 inbound 静默失败。每次手动重新编译、替换二进制或重装 Xray 后,这个能力会被清除,需要重新执行 setcap 命令。


7.3.1 安全加固:Fallback 回落伪装 + 系统防护

443 端口暴露在公网后,会被 Shodan、Censys 等自动化扫描工具和僵尸网络持续探测。Reality 协议本身已经在密钥层面解决了"如何分辨真实客户端和探测者"的问题,本节补充两类配套加固:协议层伪装(让未授权探测者看到一个无害网页)和系统层收口(限制除 443 外的一切暴露面)。

协议层:配置 Fallback 回落

这是配合 Reality 的关键武器,未携带正确密钥的连接会被无感转发到本地的一个静态网页,而不是收到任何"代理服务"的迹象。

第一步:在 Pi 400 上准备一个极简静态网页

sudo mkdir -p /var/www/fallback
sudo tee /var/www/fallback/index.html > /dev/null << 'EOF'
<!DOCTYPE html>
<html><head><title>403 Forbidden</title></head>
<body><h1>403 Forbidden</h1><p>nginx</p></body></html>
EOF

# 用 Python 内置 HTTP 服务跑起来即可,监听本地端口,无需安装 nginx
sudo tee /etc/systemd/system/fallback-web.service > /dev/null << 'EOF'
[Unit]
Description=Fallback Static Web Server
After=network.target

[Service]
Type=simple
WorkingDirectory=/var/www/fallback
ExecStart=/usr/bin/python3 -m http.server 8080
Restart=always

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now fallback-web

第二步:在 config.json 的 inbound 里加入 fallbacks 字段

{
  "log": {
    "loglevel": "warning",
    "access": "/var/log/xray/access.log",
    "error": "/var/log/xray/error.log"
  },
  "inbounds": [
    {
      "listen": "0.0.0.0",
      "port": 443,
      "protocol": "vless",
      "settings": {
        "clients": [
          {
            "id": "填入xray uuid生成的UUID",
            "flow": "xtls-rprx-vision"
          }
        ],
        "decryption": "none",
        "fallbacks": [
          {
            "dest": 8080
          }
        ]
      },
      "streamSettings": {
        "network": "tcp",
        "security": "reality",
        "realitySettings": {
          "show": false,
          "dest": "dl.google.com:443",
          "xver": 0,
          "serverNames": ["dl.google.com"],
          "privateKey": "填入x25519生成的Private key",
          "shortIds": ["填入openssl生成的8位hex"]
        }
      },
      "sniffing": {
        "enabled": true,
        "destOverride": ["http", "tls", "quic"]
      }
    }
  ]
}

fallbacks.dest 指向上面跑起来的本地静态网页端口(8080)。效果:

探测者用浏览器直接访问 https://你的IP 或 https://你的域名
        │
        ▼
   没有携带正确的 UUID/shortId
        │
        ▼
   Reality 判定为非法访问,将连接转发到 localhost:8080
        │
        ▼
   探测者看到一个普通的 403 页面,毫无破绽
sudo systemctl restart xray

系统层:ufw 收紧暴露面

ufw 在这里不负责"识别谁是合法客户端"(这是 Reality 密钥层的工作),只负责只暴露 443 这一个端口,其余全部拒绝

重要: 如果第五节的 alert_relay.py(监听 9731,接收国内节点经隧道发来的告警)也部署在这台 Pi 400 上,下面的规则要一并加上,否则装完 ufw 后告警链路会被默认策略拦截,且不会有任何报错提示——下次国内节点故障时,你会发现报警邮件根本没收到。

sudo apt-get install -y ufw

# 默认策略:拒绝所有入站,允许所有出站
sudo ufw default deny incoming
sudo ufw default allow outgoing

# 只开放 Xray 需要的 443
sudo ufw allow 443/tcp

# SSH 只允许同一局域网访问,不对公网暴露(避免与 443 共享被扫描风险)
sudo ufw allow from 192.168.1.0/24 to any port 22

# 如果这台机器同时跑着 alert_relay.py(第五节),加上这条
# 只允许隧道内网网段访问,9731 不应该对公网暴露
sudo ufw allow from 192.168.1.0/24 to any port 9731

sudo ufw enable
sudo ufw status verbose

系统层:Fail2ban 联动封禁恶意探测

sudo apt-get install -y fail2ban

# 监控 Xray 访问日志,对短时间内大量异常请求的 IP 自动封禁
sudo tee /etc/fail2ban/jail.d/xray.conf > /dev/null << 'EOF'
[xray]
enabled  = true
filter   = xray
logpath  = /var/log/xray/error.log
maxretry = 5
findtime = 600
bantime  = 86400
action   = iptables-allports
EOF

sudo systemctl restart fail2ban
sudo fail2ban-client status

系统层:无人值守安全更新

人长期在国外,树莓派需要具备自我修补能力:

sudo apt-get install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

每晚自动检测并静默安装内核及核心库(如 OpenSSL)的安全补丁。

密钥保密提醒(比端口号本身更重要的安全边界)

Reality 协议的真正安全边界是 UUID、私钥(privateKey)、shortId 这三个值,不是端口号或证书。一旦这三者泄露,伪装机制形同虚设:

  • 不要把 config.json 提交到任何公开代码仓库
  • 不要在群聊、论坛截图中暴露这三个字段
  • 如果怀疑泄露,立即重新生成(xray x25519xray uuidopenssl rand -hex 8)并重启服务

关于非标端口和 VLAN 的说明(不采纳的两种思路)

  • 不要把外部端口改成非标端口(如 38443): Reality 的伪装逻辑依赖"看起来像是在访问标准 HTTPS 网站",非标端口本身就是一个统计学异常特征,反而比标准 443 更容易被针对性识别为代理流量。
  • VLAN 隔离非必需: 消费级路由器(包括 O2 默认款)多数不支持 VLAN 划分。上面的 ufw 规则已经做到了同等效果——即使 Pi 400 被攻破,攻击者也无法主动连接局域网内的其他设备(ufw 默认拒绝入站,不影响 Pi 400 自身发起的出站连接,但限制了别人从外部或内网横向连入 Pi 400 后再跳转的路径)。

7.4 Cloudflare DDNS 自动更新脚本

O2 家宽 IP 可能每隔数天变化一次,需要 DDNS 自动跟踪。利用已有的 Cloudflare DNS 托管,无需第三方服务:

第一步:获取 Cloudflare API Token

进入 Cloudflare 后台 → My Profile → API Tokens → Create Token

  • 模板选 Edit zone DNS
  • Zone Resources 选你的域名
  • 复制生成的 Token

第二步:创建 DDNS 更新脚本

nano /home/pi/cf_monitor/ddns_update.sh
#!/bin/bash
# Cloudflare DDNS 自动更新脚本
# 适用于 O2 西班牙家宽动态公网 IPv4

# ── 配置区 ──────────────────────────────────────
CF_API_TOKEN="你的Cloudflare API Token"
ZONE_ID="你的域名Zone ID"          # CF后台域名概览页右下角
RECORD_NAME="xray.yourdomain.com"  # 用于 Xray 的子域名
# ────────────────────────────────────────────────

# 获取当前公网 IP
CURRENT_IP=$(curl -s -4 https://ifconfig.me)
if [[ -z "$CURRENT_IP" ]]; then
    echo "[$(date)] 获取公网 IP 失败,跳过本次更新"
    exit 1
fi

# 获取 Cloudflare 上已记录的 IP
CF_RECORD=$(curl -s -X GET \
    "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?type=A&name=${RECORD_NAME}" \
    -H "Authorization: Bearer ${CF_API_TOKEN}" \
    -H "Content-Type: application/json")

CF_IP=$(echo "$CF_RECORD" | python3 -c "import sys,json; r=json.load(sys.stdin)['result']; print(r[0]['content'] if r else '')" 2>/dev/null)
RECORD_ID=$(echo "$CF_RECORD" | python3 -c "import sys,json; r=json.load(sys.stdin)['result']; print(r[0]['id'] if r else '')" 2>/dev/null)

# IP 未变化则跳过
if [[ "$CURRENT_IP" == "$CF_IP" ]]; then
    echo "[$(date)] IP 未变化(${CURRENT_IP}),无需更新"
    exit 0
fi

# 更新 DNS 记录
if [[ -z "$RECORD_ID" ]]; then
    # 记录不存在,创建新记录
    curl -s -X POST \
        "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \
        -H "Authorization: Bearer ${CF_API_TOKEN}" \
        -H "Content-Type: application/json" \
        --data "{\"type\":\"A\",\"name\":\"${RECORD_NAME}\",\"content\":\"${CURRENT_IP}\",\"ttl\":60,\"proxied\":false}"
else
    # 记录存在,更新 IP
    curl -s -X PUT \
        "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" \
        -H "Authorization: Bearer ${CF_API_TOKEN}" \
        -H "Content-Type: application/json" \
        --data "{\"type\":\"A\",\"name\":\"${RECORD_NAME}\",\"content\":\"${CURRENT_IP}\",\"ttl\":60,\"proxied\":false}"
fi

echo "[$(date)] DNS 已更新:${CF_IP:-未知}${CURRENT_IP}"

重要:DNS 记录必须关闭橙色云朵(proxied: false),Xray Reality 需要客户端直连真实 IP,不能经过 CF CDN 中转。TTL 设为 60 秒,IP 变化后最快 1 分钟生效。

# 赋权并测试
chmod +x /home/pi/cf_monitor/ddns_update.sh
/home/pi/cf_monitor/ddns_update.sh

# 加入 crontab,每 5 分钟检查一次
crontab -e

追加:

# 每 5 分钟检查公网 IP 是否变化,自动更新 Cloudflare DNS
*/5 * * * * /home/pi/cf_monitor/ddns_update.sh >> /home/pi/cf_monitor/ddns.log 2>&1

7.5 客户端配置参数

以下参数填入 v2rayN / Shadowrocket / Sing-box / Clash Meta:

参数
协议VLESS
地址xray.yourdomain.com(DDNS 域名)
端口443
UUID步骤 7.3 中生成的 UUID
Flowxtls-rprx-vision
传输层TCP
TLS 类型reality
SNIdl.google.com
Fingerprintchrome(或 safari
PublicKey步骤 7.3 中 x25519 生成的 Public key
ShortId步骤 7.3 中生成的 8 位 hex

7.6 与主方案的关系

维度Cloudflare Tunnel + WARP(主方案)Xray Reality(本节备用方案)
公网访问要求无需公网 IP需要公网 IP + 端口转发
国内连通性WARP 自动降级,目前可用TCP 443,住宅 IP,极稳定
内网穿透✅ 原生支持 192.168.1.x❌ 仅做出口代理
政府网站访问✅ 实测 Cl@ve 可用✅ 走西班牙住宅 IP 出口
抗封锁能力依赖 CF 平台,协议受限Reality 主动探测防御最强
ToS 风险无(Tunnel 合规使用)无(自建服务,无 CF ToS 约束)
维护复杂度低(CF 托管)中(需维护 DDNS + Xray)

建议使用策略: 内网访问/SSH 首选 WARP(零配置,穿透好用,出口节点不影响这类场景)。需要保证西班牙 IP 的政府业务场景(Cl@ve 等),先用 2.5.1 节方法验证 WARP 出口是否为 MAD 节点——是则可用 WARP,不是则必须用本节 Xray Reality。WARP 完全连不上时,本节也是独立备用通道。两套方案并行部署,按场景选用。


Xray Reality 节点部署检查清单

  • 路由器已将 443 TCP 端口转发到 Pi 400 内网 IP
  • Pi 400 内网 IP 已在路由器 DHCP 中绑定固定地址
  • xray x25519 密钥对已生成并填入 config.json
  • xray uuid 已生成并填入 config.json
  • systemctl status xray 显示 active (running)
  • ss -tlnp | grep 443 确认 Xray 在监听
  • Cloudflare API Token 已填入 ddns_update.sh
  • DDNS 脚本手动执行一次,DNS 记录已创建,橙色云朵已关闭
  • crontab 每 5 分钟 DDNS 任务已配置
  • 客户端用 PublicKey + ShortId 测试连接成功