迁移方案-脱离Cloudflare代理依赖

迁移总方案:博客 + 播客脱离共享 CDN 代理 IP 依赖

背景与目标(2026-08-29 更新)

当前问题:博客(Cloudflare Pages)和播客音频(Cloudflare R2)都跑在 Cloudflare 的代理网络上,2026/27 赛季 LaLiga 反盗版封锁几乎天天有比赛日触发,导致这两块服务频繁被 O2 误伤。

重要更正:原方案曾建议博客迁往 Vercel,但经查证,Vercel、Netlify、GitHub Pages、BunnyCDN 等主流共享 IP CDN/托管平台,同样被 LaLiga 这轮封锁波及过——本质上都是"几十万个域名挤在同一批共享 IP 上",只是被点名的频率和时间点不同,不是安全选项。因此博客的迁移目标改为自建到已有的 Linode VPS,用独立静态 IP 避开"共享大池子"这个结构性风险。

目标架构:

博客        → 自建到 Linode VPS(独立静态 IP,不与任何共享 CDN 池混用)
播客音频     → Backblaze B2(独立网络出口,目前未见被波及)
              + archive.org(可选长期镜像)
RSS feed    → 跟博客一起自建到 VPS
DNS         → Nameserver 仍留在 Cloudflare(不受影响),但相关记录改成
              灰云 DNS-only 或直接指向新服务商,不再走 Cloudflare 代理

总体原则:稳字当头

  1. 分阶段,不并行操作多个系统——一次只动一件事,出问题能立刻定位是哪一步
  2. 新旧并行运行,验证通过再切流量——旧服务在确认新服务稳定前,一律不下线、不删除
  3. 先低风险资产练手,后核心资产——博客先行,播客音频(涉及历史存量数据)放后面
  4. 每一步都有明确的验证方法,不是"感觉应该好了"就往下走
  5. 每个阶段都留回滚预案——任何一步出问题,能在几分钟内退回上一个稳定状态
  6. 不动现有的关键基础设施——VPS 上已有的 Xray Reality 代理是日常依赖的服务,迁移过程优先选不影响它的方案,即使多花点小钱或多绕一步

Phase 0:准备阶段(不影响任何线上服务,随时可以开始)

  • 确认 Linode VPS 当前的资源状况(磁盘剩余空间、443 端口已被 Xray 占用——处理方式见 Phase 1 第二步之后的说明)
  • 注册 Backblaze B2 账号,创建一个 bucket(比如 yemushi-podcast),生成 Application Key(记得勾选读写权限)
  • (可选)注册 archive.org 账号,在 archive.org/account/s3.php 生成 IAS3 密钥,留到 Phase 3 用
  • 把 Cloudflare 后台当前所有相关 DNS 记录截图或导出备份(类型、名称、值、TTL、是否代理),这是最重要的一步——出问题时靠这份记录能几分钟内手动改回原样
  • 记录当前博客和播客涉及的所有域名/子域名清单,避免迁移时漏掉某个不常用的子域名
  • 备份现有 Xray 配置cp /usr/local/etc/xray/config.json /usr/local/etc/xray/config.json.bak,不管后面选哪个方案处理 443 冲突,先留一份备份

这一步做完,什么都还没变,可以慢慢来,不用赶。


Phase 1:博客自建到 Linode VPS(先拿这个练手,风险最低)

1.1 VPS 基础检查与安全加固

# SSH 登录 VPS
ssh root@你的VPS_IP

# 更新系统
apt update && apt upgrade -y

# 检查磁盘空间,确认够用
df -h

# 确认 443 端口被谁占用(预期会看到 xray)
ss -tlnp | grep -E ':80|:443'

安全加固(比基础版更完整,因为这台机器要长期对公网暴露 80/443):

a) SSH 加固

# 配置 SSH 密钥登录(如果还没有)
ssh-keygen -t ed25519 -C "[email protected]"
ssh-copy-id root@你的VPS_IP

nano /etc/ssh/sshd_config

修改这几项:

PasswordAuthentication no
PermitRootLogin no          # 彻底禁 root 直接登录,比 prohibit-password 更严格
PubkeyAuthentication yes
MaxAuthTries 3               # 限制单次连接的认证尝试次数
ClientAliveInterval 300
ClientAliveCountMax 2         # 空闲连接自动断开
systemctl restart sshd

由于 PermitRootLogin no 需要先有一个非 root 的管理账号能 sudo,先建好:

adduser admin
usermod -aG sudo admin
mkdir -p /home/admin/.ssh
cp /root/.ssh/authorized_keys /home/admin/.ssh/
chown -R admin:admin /home/admin/.ssh
chmod 700 /home/admin/.ssh
chmod 600 /home/admin/.ssh/authorized_keys

先用 admin 账号登录测试成功之后,再改 sshd_config 禁用 root 登录,避免把自己锁在门外。

b) 防火墙(IPv4 + IPv6 分别确认)

apt install -y ufw

# 确认 IPv6 支持已开启
grep IPV6 /etc/default/ufw
# 应该看到 IPV6=yes,没有的话改成 yes

ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

# 确认规则同时覆盖了 v4 和 v6
ufw status verbose

ufw status verbose 的输出里每条规则应该同时出现 (v6) 版本,确认新的 IPv6 地址上的 80/443 确实被放行、且没有意外放开其他端口。

c) fail2ban 加固,并扩展监控 Caddy 日志

apt install -y fail2ban

# 给 sshd 单独配置更严格的封禁策略
cat > /etc/fail2ban/jail.local << 'EOF'
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3

[sshd]
enabled = true

[caddy-auth]
enabled = true
port = http,https
filter = caddy-auth
logpath = /var/log/caddy/access.log
maxretry = 10
EOF

# Caddy 默认不带 fail2ban 过滤规则,手动加一个简单版本,抓异常高频请求
cat > /etc/fail2ban/filter.d/caddy-auth.conf << 'EOF'
[Definition]
failregex = ^<HOST> .* "(GET|POST|HEAD).*" (404|444|400) .*$
ignoreregex =
EOF

systemctl restart fail2ban
fail2ban-client status

d) 自动安全更新(装完就不用天天惦记打补丁)

apt install -y unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades
# 选"是",之后系统会自动安装安全补丁

e) (强烈建议)SSH 管理入口收进你已有的 WireGuard 隧道,不对公网暴露 22 端口

你已经有 WireGuard 基础设施,这是最有效的一步——与其在公网上防守 SSH 暴力破解,不如干脆不让 22 端口对公网可见:

# 把 SSH 只允许通过 WireGuard 内网网段访问,公网防火墙直接拒绝 22
ufw delete allow OpenSSH
ufw allow in on wg0 to any port 22 proto tcp   # wg0 换成你实际的 WireGuard 接口名
ufw status verbose   # 确认 OpenSSH 那条公网规则已经不在了

这样即使全网扫描 IP 找开放的 22 端口,也扫不到你这台机器——管理入口完全收敛到你自己的 WireGuard 网络里,比任何 fail2ban 规则都更彻底。如果不方便这么做(比如临时需要在没连 WireGuard 的设备上应急登录),可以保留公网 22 但改成非标准端口,降低被扫描到的概率,两者都做也可以。

⚠️ 但要注意:这条只适用于你自己的交互式管理登录(admin 账号)。1.5 里 GitHub Actions 的自动部署是从 GitHub 的公网服务器发起连接的,连不上被收进 WireGuard 内网的 SSH。 部署用的 deploy 账号需要单独处理——继续保持公网可达,但用「强制命令」把它的权限锁死到只能跑 rsync,即使部署密钥意外泄露,攻击者能做的也仅限于同步文件到 /var/www/blog,拿不到任何 shell 权限。具体配置见 1.5.2。

1.2 安装 Caddy

apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt update
apt install -y caddy

systemctl status caddy
mkdir -p /var/www/blog
chown -R www-data:www-data /var/www/blog

1.3 处理 443 端口冲突(用 IPv6,零成本、不改 Xray)

Reality 协议的伪装原理是:不匹配 Reality 密钥的流量会被原样转发给配置里的 dest(伪装目标),由 dest 自己完成真实的 TLS 握手。Xray 进程独占了原 IPv4 的 443 监听权,不能简单叠加另一个服务。

采用方案:给 VPS 用免费分配的 IPv6 地址,Caddy 绑定这个新地址的 443,Xray 完全不动。

  • 确认 Linode 已经给这台实例分配了 IPv6(Linode 默认每台实例免费带一个 /64 段):
ip -6 addr show

看到类似 2600:xxxx:xxxx:xxxx::1/64 这样的地址就是可用的公网 IPv6,记下这个具体地址。

  • 在 Caddyfile 里显式绑定这个 IPv6 地址:
nano /etc/caddy/Caddyfile
博客域名.com {
    bind 2600:xxxx:xxxx:xxxx::1
    root * /var/www/blog
    file_server {
        hide .*          # 不暴露隐藏文件
    }
    encode gzip

    # 安全响应头,见下方 1.3.1
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "strict-origin-when-cross-origin"
        -Server            # 去掉响应头里的 Server 标识,减少信息暴露
    }
}
systemctl reload caddy
  • Cloudflare DNS 里博客的记录改成 AAAA 记录,指向这个 IPv6 地址,代理状态灰云 DNS-only

注意:只加 AAAA、不加 A 记录,意味着只有支持 IPv6 的网络才能访问你的博客。 目前西班牙主流运营商(Movistar、Vodafone、Orange 等)以及绝大多数移动网络都已支持 IPv6,覆盖率很高,但极少数老旧网络环境可能访问不了。如果不放心,可以在确认稳定后,再考虑要不要额外申请一个付费 IPv4(Linode 一般 $1-2/月)做双栈兜底,这一步不急,可以观察一段时间再决定。

这个方案安全的原因需要说准确:不是"IPv6 技术上封不了"(详见文末附注 2,运营商确实可以精确封锁具体 IPv6 地址),而是这个 IPv6 地址是你独享的,不会因为跟别人共享而被连坐。 只要不把这台 VPS 上的服务和任何共享 CDN/托管平台混用,这个地址就没有理由出现在封锁清单上。

Xray 那边完全不用动,它继续用原来的 IPv4 地址监听 443,两边互不干扰,直接进入 1.3.1。

1.3.1 新 IPv6 地址本身也要单独确认防火墙范围

因为 ufw 的规则默认是端口级别、不区分具体 IP,前面 1.1 里配置的 ufw allow 80/tcp / 443/tcp 会同时对 IPv4 和 IPv6 生效,这正是我们想要的(博客要在新 IPv6 上提供服务)。但要确认没有把 SSH 或其他内部服务意外暴露在这个新 IPv6 上

# 确认新 IPv6 地址上只有 80/443 在监听,没有其他服务
ss -tlnp | grep '2600:xxxx'   # 换成你实际的 IPv6 前缀

# 确认 SSH 已经按前面 1.1(e) 收进 WireGuard,公网 IPv4/IPv6 都看不到 22 端口
nmap -6 -p 22 2600:xxxx:xxxx:xxxx::1   # 从外部机器测试,预期结果是 filtered/closed

方案 B(备选,需谨慎,会改动 Xray 配置):把博客设为 Reality 的伪装目标

核心思路:把 Xray Reality 的 dest 改成指向本机 Caddy 监听的内部端口,让"伪装身份"变成你自己的真实博客。

# 务必先确认已经备份过(Phase 0 提到的那步)
cat /usr/local/etc/xray/config.json.bak > /dev/null || echo "还没备份,先去备份!"

编辑 config.json,找到 realitySettings 部分:

"realitySettings": {
    "dest": "127.0.0.1:8443",
    "serverNames": ["你的博客域名.com"]
}

Caddy 需要改成监听 8443 并自己完成 TLS termination(因为 dest 收到的是真实的 TLS 连接,必须有真实证书):

你的博客域名.com:8443 {
    root * /var/www/blog
    file_server
    encode gzip
}
systemctl restart xray
systemctl reload caddy

# 立刻测试 Xray 本身是否还正常工作(比如从客户端连一下),确认没有搞挂代理

如果测试后发现 Xray 连接异常,立刻用备份恢复:

cp /usr/local/etc/xray/config.json.bak /usr/local/etc/xray/config.json
systemctl restart xray

1.4 先用测试子域名跑通整条链路(不动线上域名)

1.4.1 编辑 Caddy 配置

nano /etc/caddy/Caddyfile
test-blog.你的域名.com {
    bind 2600:xxxx:xxxx:xxxx::1   # 换成你实际的 IPv6 地址
    root * /var/www/blog
    file_server {
        hide .*
    }
    encode gzip
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "strict-origin-when-cross-origin"
        -Server
    }
}
systemctl reload caddy

1.4.2 在 Cloudflare DNS 后台加一条测试记录

新建 AAAA 记录:test-blog → 你的 IPv6 地址,代理状态设为灰云(DNS only),TTL 设短一点(比如 300 秒)。

1.4.3 先手动放一个测试页面验证

echo "<h1>Hello from VPS</h1>" > /var/www/blog/index.html

访问 https://test-blog.你的域名.com,能看到内容且证书有效,说明 Caddy 的自动 HTTPS 和静态文件服务、以及端口共存方案都跑通了。

1.5 配置 GitHub Actions 自动构建 + 部署

1.5.1 在 VPS 上建一个部署专用用户

adduser deploy
usermod -aG www-data deploy
mkdir -p /home/deploy/.ssh

1.5.2 生成一对专门用于 GitHub Actions 部署的密钥,并用强制命令锁死权限

ssh-keygen -t ed25519 -f ./deploy_key -N ""
# 在 VPS 上,装 rrsync(rsync 官方自带的受限包装脚本,专门用于这种场景)
apt install -y rsync
find / -name rrsync 2>/dev/null   # 找到 rrsync 脚本的位置,通常在 /usr/share/doc/rsync/ 或 /usr/bin
cp /usr/share/doc/rsync*/scripts/rrsync /usr/local/bin/rrsync   # 路径根据实际找到的调整
chmod +x /usr/local/bin/rrsync

# authorized_keys 里用 command= 强制限定这把密钥只能执行 rrsync,且只能写入指定目录
cat >> /home/deploy/.ssh/authorized_keys << 'EOF'
command="/usr/local/bin/rrsync -wo /var/www/blog/",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAA...(把deploy_key.pub的实际内容粘贴在这里)
EOF

chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys

chown -R deploy:www-data /var/www/blog
chmod -R 775 /var/www/blog

加了 command= 前缀之后,这把密钥不管被拿去做什么,SSH 服务端都会强制只执行 rrsync -wo /var/www/blog/-wo 表示只写、不能读取和删除目录外的内容),no-pty 等选项禁止了交互式 shell、端口转发这些额外能力。即使 GitHub Secrets 意外泄露,攻击者用这把密钥能做的也仅限于往 /var/www/blog 目录写文件,拿不到 shell,进不了系统其他部分。

1.5.3 把私钥加进 GitHub 仓库的 Secrets

仓库 → Settings → Secrets and variables → Actions,新建:

  • DEPLOY_SSH_KEYdeploy_key 私钥完整内容
  • DEPLOY_HOST:你的 VPS 公网 IP(部署用的是公网 IPv4/原有管理 IP,不是 Caddy 绑定的新 IPv6,只要能连上 22 端口即可,两者不冲突)
  • DEPLOY_USERdeploy

更彻底的做法(推荐):用自托管 Runner,完全不用为部署开放 22 端口

上面这套「强制命令锁死密钥」的方案本身没问题,但它要求 22 端口必须留给公网的 GitHub 服务器连进来,跟 1.1(e) 里「SSH 完全收进 WireGuard」这个更彻底的加固思路是冲突的——鱼与熊掌不能兼得。如果不想在这两者之间妥协,更干净的做法是:在 VPS 上装一个 GitHub Actions 自托管 Runner,让部署流程反过来变成"VPS 主动连出去问 GitHub 有没有新任务",而不是"GitHub 连进来",这样就完全不需要为部署开放任何入站端口,跟你现有的 WireGuard/Xray 这类"只出不进"的架构风格也更一致:

# 在 VPS 上,用 deploy 账号创建 Runner 工作目录
sudo -u deploy bash
mkdir ~/actions-runner && cd ~/actions-runner

# 去 GitHub 仓库 → Settings → Actions → Runners → New self-hosted runner
# 按页面提示下载对应版本并配置(会给你一段专属的 token 和命令)
tar xzf ./actions-runner-linux-x64-x.x.x.tar.gz
./config.sh --url https://github.com/你的用户名/你的仓库 --token 从页面复制的token

# 注册成系统服务,开机自启、断线自动重连
exit   # 退回 root
cd /home/deploy/actions-runner
./svc.sh install deploy
./svc.sh start

对应的 workflow 里把 runs-on: ubuntu-latest 改成 runs-on: self-hosted,构建和部署(直接 cp/rsync 到本地 /var/www/blog,不需要再走网络 SSH)都在这台机器本地完成:

jobs:
  deploy:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: |
          # hugo --minify
          echo "替换成你自己的构建命令"
      - name: Deploy locally
        run: |
          rsync -avzr --delete public/ /var/www/blog/

这样一来,1.1(e) 的「SSH 完全收进 WireGuard、公网 22 端口彻底不开放」就不再和部署流程冲突了,可以放心按最严格的方式配置。前面写的「强制命令锁死密钥」方案作为备选保留——如果你不想在 VPS 上常驻一个 Runner 进程(毕竟多一个需要维护的服务),也可以选那条路,两种方式选一种即可,不用都做。

1.5.4 编写 GitHub Actions workflow

.github/workflows/deploy.yml(构建命令和产物目录换成你实际用的静态生成器对应的):

name: Deploy Blog

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build
        run: |
          # hugo --minify
          echo "替换成你自己的构建命令"

      - name: Deploy via rsync
        uses: burnett01/[email protected]
        with:
          switches: -avzr --delete
          path: public/   # 换成你实际的构建产物目录
          remote_path: /var/www/blog/
          remote_host: ${{ secrets.DEPLOY_HOST }}
          remote_user: ${{ secrets.DEPLOY_USER }}
          remote_key: ${{ secrets.DEPLOY_SSH_KEY }}

1.5.5 验证自动部署

改动一篇文章,git push,去 Actions 标签页确认构建和 rsync 都成功,刷新测试子域名确认改动生效。

1.6 正式切换域名

1.6.1 修改 Caddy 配置,加上正式域名

nano /etc/caddy/Caddyfile
你的正式博客域名.com {
    bind 新IP地址   # 方案A需要;方案B不需要
    root * /var/www/blog
    file_server
    encode gzip
}
systemctl reload caddy

1.6.2 修改 Cloudflare DNS

正式博客域名的记录改成指向对应 IP,代理状态设为灰云(DNS only)

⚠️ 先不要删除 Cloudflare Pages 项目,保留至少一到两周作为回滚后备。

1.7 验证清单

  • curl -I https://你的正式博客域名 返回 200,响应头显示 Caddy 而不是 Cloudflare
  • 家宽直连和手机流量各测一次
  • 挑一个当天有比赛的时段专门测一次
  • 改一篇文章 push 上去,确认自动部署链路在正式域名上也生效
  • 重点:确认 Xray 代理服务依然正常工作,不管用的是方案 A 还是方案 B
  • 检查 HTTPS 证书是否正常

1.8 观察期

稳定运行 3-7 天,覆盖至少 2-3 个比赛日。确认无异常后再考虑清理旧的 Cloudflare Pages 项目。

回滚预案

  • 博客本身:把 Cloudflare DNS 记录改回原来指向 Cloudflare Pages 的配置即可,几分钟内恢复,旧项目全程没删除
  • 如果用了方案 B 且 Xray 出问题:cp /usr/local/etc/xray/config.json.bak /usr/local/etc/xray/config.json && systemctl restart xray 立刻恢复

Phase 2:播客音频从 R2 迁移到 B2(核心资产,最谨慎的一步)

这一步涉及历史存量数据,是整个方案里唯一"有数据丢失风险"的环节,按下面的顺序一步都不要跳:

2.1 工具准备

curl https://rclone.org/install.sh | sudo bash
rclone config

分别把 R2 和 B2 的 Access Key / Secret Key 配置成两个独立的 remote(r2-sourceb2-target)。

2.2 先做 dry-run

rclone sync r2-source:你的bucket名 b2-target:yemushi-podcast --dry-run -v

仔细核对文件列表和数量。这一步只打印计划,不会真的传输。

2.3 正式同步(只拷贝,不删除 R2 原文件)

rclone copy r2-source:你的bucket名 b2-target:yemushi-podcast -v --progress

copy 不用 sync,更安全。

2.4 抽样校验

  • 随机挑 3-5 集对比文件大小
  • rclone check r2-source:你的bucket名 b2-target:yemushi-podcast 做哈希校验

2.5 Pipeline 代码改成双写

upload_to_r2(file_path)   # 保留原有逻辑
upload_to_b2(file_path)   # 新增

双写至少维持 2-4 周。

2.6 RSS feed 切换

先只对新发布的单集试用 B2 链接,观察 Apple Podcasts / 小宇宙抓取是否正常,稳定后再考虑批量改历史链接。

2.7 观察期

覆盖至少 2-3 个比赛密集的周末,确认稳定后再考虑停止双写、清理 R2。

回滚预案

RSS 链接改回 R2 地址即可,因为全程没删除 R2 文件。


Phase 3(可选):archive.org 长期镜像

优先级最低,可在 Phase 2 稳定后慢慢配置:

  • 用 IAS3 密钥把历史音频批量上传到 archive.org
  • 新集数发布时 pipeline 里加一步同步上传

Phase 4:DNS 收尾检查

  • 梳理 Cloudflare DNS 后台所有记录,逐条确认哪些还是橙云
  • 不再需要走 Cloudflare 代理的记录改成灰云 DNS-only
  • Nameserver 留在 Cloudflare 不用动

建议时间线

时间任务
第 1 周Phase 0 准备 + Phase 1.1-1.4(VPS 加固、装 Caddy、处理 443 冲突、测试子域名跑通)
第 2 周Phase 1.5-1.7(GitHub Actions 自动部署、正式切换域名、验证)
第 3 周Phase 1 观察期 + Phase 2 准备(rclone 同步、抽样校验)
第 4 周Phase 2 正式双写上线
第 5-8 周Phase 2 观察期,覆盖多个比赛日
之后Phase 3 archive.org 镜像 + Phase 4 DNS 收尾

哪个阶段观察下来还有疑虑,就多留观察期,不用赶进度。


通用验证清单

  1. curl -I <新地址>,确认返回 200 且响应头显示自建服务标识
  2. 家宽直连和手机流量各测一次
  3. 专门挑比赛日测试
  4. 检查旧服务是否还保持可用,作为回滚后路
  5. 涉及 VPS 改动的步骤,额外确认 Xray 代理服务未受影响

附注:为什么放弃 Vercel/Netlify,选择自建 VPS

经查证,LaLiga 这轮反盗版 IP 封锁自 2025 年初开始已陆续波及 Cloudflare、Vercel、Netlify、GitHub Pages、BunnyCDN 等主流共享 IP 的 CDN/托管平台,本质原因是这些平台都是"大量域名共享少量 IP"的结构,非法 IPTV 转播服务混在同一批 IP 里,导致误伤范围随之扩大。Vercel 官方甚至专门发博客说明此事,CEO 也公开表示即便开通了专门的举报通道,还是持续被无差别封锁。

相比之下,自建到独立静态 IP 的 VPS,不与任何大型共享 IP 池混用,被"连坐"的概率结构性更低——虽然不是 100% 的官方保证,但目前是几个选项里风险最低的一个。


附注 2:重新核实——IPv6 是否天然躲得开这类封锁?(2026-08-30 补充分析)

在最终确定用 IPv6 方案之前,专门查证了一个关键问题:运营商是不是技术上没法把 IPv6 地址全部封掉,所以 IPv6 天然更安全? 查证结果需要纠正一个想当然的假设——答案是否定的,IPv6 一样会被精确封锁,安全性的来源不是"协议层面封不了",而是别的原因

实际情况:西班牙网友论坛上已经有人明确抱怨过这个问题——原文大意是"为什么 IPv6 也被封?理论上每个服务不是应该能有自己独立的 IPv6 地址、不用像 Cloudflare 的 IPv4 那样挤在共享池里吗?“这条留言本身就证实了两件事:第一,运营商确实在封锁清单里精确点名封禁具体的 IPv6 地址,技术上完全做得到(对防火墙来说封一个 IPv6 地址和封一个 IPv4 地址没有本质难度差异,不存在"地址空间太大封不过来"这种技术豁免);第二,IPv6 真正的优势不是"封不了”,而是"每个服务可以有自己独立的地址,不必和别人共享"——这跟我们选它的理由其实是同一个逻辑,只是需要把"封不了"这个错误理解纠正成"没有理由被封"。

这对你的方案意味着什么:

  1. 你的 Linode IPv6 地址安全,不是因为它是 IPv6,而是因为它是你独享的、跟任何 Cloudflare/Vercel 服务完全无关的地址——这跟之前的核心判断完全一致,只是要澄清"protocol 本身天然安全"这个说法不准确,真正起作用的是"地址不共享、不会被别的网站牵连"
  2. 不会无缘无故被封,但也不是绝对不可能——封锁清单本质是 LaLiga 举报 + 运营商执行的动态名单,只要你的 IPv6 地址不出现在任何被举报的服务列表里(这是大概率事件,因为你的 VPS 只是自己用),就不会被点名。真正的风险不是"IPv6 遲早被扫描到",而是"万一将来 VPS 上又混进了某个跟别人共享/容易被牵连的服务",这也是为什么方案里反复强调"不要把博客和任何共享 CDN 混用"这个原则比"用 IPv4 还是 IPv6"本身更关键
  3. 规则的扩张趋势值得留意:查到的最新信息显示,法院授权的封锁范围已经从最初判决书列出的 123 个 IP,扩大到目前号称最多同时封锁约 3000 个 IP(其中 35-45% 属于 Cloudflare),而且已经开始出现"不局限于比赛日"的封锁案例,2026 年 3 月还有一份新判决把范围扩大到了欧冠、网球、高尔夫等其他赛事期间。这说明这套机制本身还在扩张,不是收敛趋势——独立 IP 这个策略眼下有效,但不代表以后完全不用再关注这个问题

结论:不用改变已经定下的 IPv6 方案,这仍然是目前几个选项里最合理的一个;只是把决策依据修正为准确的——“独享、不共享"才是关键,而不是"IPv6 协议本身封不了”。