从不稳定的代理节点到自建 CN2 GIA 出口:搬瓦工 + Xray Reality + Clash Verge 全流程

一个下午,从「决定自建」到「Claude Code、GitHub、Maven Central 全部走新出口」,链路彻底切换完成。最终状态是:一台洛杉矶 CN2 GIA 线路的 VPS,跑 Xray 的 VLESS + Reality,本机 Clash Verge 规则分流,国内直连、海外走代理;年费约 $170,日常零维护,每季度登录一次升个级。

这篇按操作顺序拆解整个过程:选型 → 购机与加固 → 服务端部署 → 客户端接入 → 验证与切换。每个阶段给出要做什么、怎么验证做对了,以及实际撞到的坑——其中一个(Reality 握手失败)我翻遍搜索引擎没找到现成解释,最后是靠服务端的详细日志一行行读出来的。如果你正带着 handshake did not complete successfully 搜到这里,可以直接跳到阶段三的踩坑

全局:一条自建出口由哪几层组成

本机应用(终端 / 浏览器 / Claude Code)
    ↓ 环境变量 HTTPS_PROXY 或系统代理 → 127.0.0.1:7897
Clash Verge(mihomo 内核)
    ↓ 规则分流:国内域名/IP 直连,其余走 PROXY 组
    ↓ VLESS + Reality,TCP 443
海外 VPS(Xray 服务端)
    ↓ 解密后以自身 IP 直连目标
GitHub / Anthropic / Maven Central ...

动手前要想清楚的三件事:

阶段一:选型

这一阶段的目标:定下线路、服务商和协议,避免在错误的基础上折腾。

线路:优化线路 vs 普通国际线路

类型代表月费特点
普通国际线路Vultr / DigitalOcean / AWS Lightsail 洛杉矶$5–6白天还行,晚高峰绕路、丢包明显
中国优化线路(CN2 GIA / CMIN2 / 9929)搬瓦工 DC6、DMIT LAX Pro$10–20三网直连回程,晚高峰依然稳,对流式长连接友好

区域选美西洛杉矶:延迟 150 ms 左右对开发工具无感,各类海外服务全部可用;日本延迟更低,但国内到日本的线路晚高峰反而更差。

服务商:搬瓦工 vs DMIT

两家都是 CN2 GIA 线路的老牌选手,实测(2026-09)对比:

套餐配置价格差异点
搬瓦工 CN2 GIA-E 20G1 核 / 1 GB / 1 TB 月流量 / 2.5 Gbps$49.99/季、$169.99/年后台免费自助迁机房(洛杉矶 DC6/DC9、日本软银等 10+ 个),长期有货
DMIT LAX.AS3.Pro.TINY1 核 / 2 GB / 1 TB / 1 Gbps$10.90/月可月付、每月免费换一次 IP;小套餐常断货

我选了搬瓦工年付:有货、后台能迁机房、年付省心。DMIT 的差异化在月付和免费换 IP,断货时是很好的替补。

这个套餐(CN2 GIA-E 20G)值得多说两句,因为它的规格看起来寒酸,值钱的部分在线路上:

线路和大规格版本完全相同,作为个人出口,最低档刚好够。

提示:服务商官网的价格和库存变化很快,下单前以购物车页为准。DMIT 官网套了 Cloudflare 人机验证,用代理访问经常过不去,直连即可。

协议:VLESS + Reality

✅ 本阶段验证

一句话能说清「为什么是这条线路、这家服务商、这个协议」,就可以进入下一步。选型没定就动手,后面每一步都可能返工。

阶段二:购机与系统加固

这一阶段的目标:拿到一台只开 SSH 和 443、只允许密钥登录的干净 Debian。

1. 下单

搬瓦工的订单页有几个细节:

2. 重装系统

购买后 KiwiVM 面板默认装的是 AlmaLinux,走 Install new OS 重装成 Debian(当前稳定版 Debian 13)。重装前要先在 Main controls 里 Stop 虚拟机,否则按钮会报 Unable to reload a running VM。重装页会显示 SSH 端口和临时 root 密码——已经贴了公钥的话密码用不上。

3. 加固

密钥登录通了之后,一次性做完三件事:

# 只允许密钥登录
cat > /etc/ssh/sshd_config.d/10-hardening.conf <<EOF
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
EOF
sshd -t && systemctl reload ssh

# 防火墙只放 SSH 和 443
apt-get install -y nftables
cat > /etc/nftables.conf <<'EOF'
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
  chain input {
    type filter hook input priority 0; policy drop;
    ct state established,related accept
    iif lo accept
    ip protocol icmp accept
    ip6 nexthdr icmpv6 accept
    tcp dport { 22, 443 } accept
    udp dport 443 accept
  }
  chain forward { type filter hook forward priority 0; policy drop; }
  chain output { type filter hook output priority 0; policy accept; }
}
EOF
nft -f /etc/nftables.conf && systemctl enable --now nftables

# BBR
printf 'net.core.default_qdisc=fq\nnet.ipv4.tcp_congestion_control=bbr\n' > /etc/sysctl.d/99-bbr.conf
sysctl -p /etc/sysctl.d/99-bbr.conf

如果 SSH 端口不是 22,nftables 里对应改掉——先改防火墙再 reload sshd,顺序反了会把自己锁在外面。

✅ 本阶段验证

阶段三:服务端部署 Xray

这一阶段的目标:443 端口跑起 VLESS + Reality,并且先用 Xray 自己当客户端在服务器本机验证通过,再去碰任何第三方客户端。

1. 安装并生成参数

bash -c "$(curl -fsSL https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install -u root
xray uuid            # 客户端 UUID
xray x25519          # 输出 PrivateKey(服务端)和 Password/PublicKey(客户端)
openssl rand -hex 4  # shortId

新版 Xray 把 x25519 输出里的「Public key」改叫了「Password」,客户端配置里填的还是它。

2. 写配置

/usr/local/etc/xray/config.json

{
  "log": { "loglevel": "warning" },
  "inbounds": [{
    "listen": "0.0.0.0",
    "port": 443,
    "protocol": "vless",
    "settings": {
      "clients": [{ "id": "<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": "<PrivateKey>",
        "shortIds": ["<shortId>"]
      }
    },
    "sniffing": { "enabled": true, "destOverride": ["http", "tls", "quic"] }
  }],
  "outbounds": [
    { "protocol": "freedom", "tag": "direct" },
    { "protocol": "blackhole", "tag": "block" }
  ]
}
xray run -test -config /usr/local/etc/xray/config.json   # Configuration OK
systemctl enable --now xray
ss -ltnp | grep ':443 '

✅ 本阶段验证:先用 Xray 自己连自己

第三方客户端连不上时,你无法区分是服务端配置错了还是客户端兼容问题。所以在服务器上再起一个 Xray 实例当客户端,指向 127.0.0.1:443

{
  "inbounds": [{ "listen": "127.0.0.1", "port": 10808, "protocol": "socks" }],
  "outbounds": [{
    "protocol": "vless",
    "settings": { "vnext": [{ "address": "127.0.0.1", "port": 443,
      "users": [{ "id": "<UUID>", "encryption": "none", "flow": "xtls-rprx-vision" }] }] },
    "streamSettings": { "network": "tcp", "security": "reality",
      "realitySettings": { "serverName": "dl.google.com", "fingerprint": "chrome",
        "publicKey": "<Password/PublicKey>", "shortId": "<shortId>" } }
  }]
}
xray run -config /tmp/client.json &
curl --socks5-hostname 127.0.0.1:10808 https://ipinfo.io/ip   # 应输出 VPS 自己的 IP

这条自测通过,服务端就是对的;后面客户端连不上只需要查客户端。

踩坑:伪装目标的证书链太大,握手永远完不成

我最初把 dest 设成 www.microsoft.com:443——它 TLS 1.3 + HTTP/2 都支持,是很多教程里的常见选择。结果所有连接被服务端直接断开,客户端报 connect error: EOF,服务端日志只有一行:

REALITY: processed invalid connection from x.x.x.x: handshake did not complete successfully

这句话极具误导性——它让人去怀疑密钥、shortId、时间差,而这些全都核对无误,连服务器本机的 Xray 自测都失败。把 realitySettings.show 打开后,详细日志才把真相摆出来:

hs.c.ClientShortId: [...]            ← shortId 匹配
hs.c.conn == conn: true              ← 认证通过
len(s2cSaved): 4096  Server Hello: 1215
len(s2cSaved): 2881  Change Cipher Spec: 6
len(s2cSaved): 2875  Encrypted Extensions: 41
len(s2cSaved): 2834  Certificate: 8273   ← 证书消息 8273 字节,缓冲只剩 2834
hs.c.isHandshakeComplete.Load(): false

根因:Reality 服务端会先和伪装目标做一次真实握手、把对方的 ServerHello / Certificate 等消息读进一个 4096 字节的缓冲,用来模仿目标的握手形态。www.microsoft.com 的证书链有 8 KB 多,一条 Certificate 消息就装不下,握手状态永远到不了 complete,服务端便把这条已经认证通过的连接当成非法连接处理。

修复:换一个证书链小的伪装目标dl.google.com 的证书链约 2–3 KB,改完立刻通。挑 dest 时除了「支持 TLS 1.3 + H2、非国内站」,再加一条:

# 在 VPS 上量一下候选目标的证书链大小
echo | openssl s_client -connect dl.google.com:443 -servername dl.google.com 2>/dev/null \
  | openssl x509 -outform DER | wc -c

叶子证书本身不超过 2 KB、整条链不超过 4 KB 的,才是安全的选择。

阶段四:客户端接入 Clash Verge

这一阶段的目标:本机 Clash Verge 加载新节点,规则分流,混合端口 7897 起来。

1. 写 profile

Clash Verge(mihomo 内核)原生支持 VLESS + Reality,新建一个本地 profile:

mixed-port: 7897
allow-lan: false
mode: rule
ipv6: false
dns:
  enable: true
  nameserver:
    - https://223.5.5.5/dns-query
    - https://120.53.53.53/dns-query
proxies:
  - name: MY-LA
    type: vless
    server: <VPS IP>
    port: 443
    uuid: <UUID>
    network: tcp
    tls: true
    udp: true
    flow: xtls-rprx-vision
    servername: dl.google.com
    client-fingerprint: chrome
    reality-opts:
      public-key: <Password/PublicKey>
      short-id: <shortId>
proxy-groups:
  - name: PROXY
    type: select
    proxies: [MY-LA, DIRECT]
rules:
  - GEOSITE,private,DIRECT
  - GEOIP,LAN,DIRECT,no-resolve
  - GEOSITE,cn,DIRECT
  - GEOIP,CN,DIRECT,no-resolve
  - MATCH,PROXY

servername 必须和服务端 serverNames 一致;服务端换了伪装目标,这里要同步改。

规则是怎么分流的

思路只有一句话:能证明是”国内”的直连,其余一律走代理。匹配从上到下,命中即停:

顺序规则出口作用
1GEOSITE,privateDIRECTlocalhost.local 等本地域名
2GEOIP,LAN,no-resolveDIRECT局域网 IP
3GEOSITE,cnDIRECT域名在中国站点列表里(.cn 后缀、淘宝、腾讯、字节等几十万条,子域名自动包含)
4GEOIP,CN,no-resolveDIRECT目标 IP 属于中国大陆 IP 段——兜域名规则没收录的国内小站
5MATCHPROXY以上都没命中 → 走 VPS

两个数据源都不用自己维护:geosite 来自 v2fly 社区的 domain-list-community,geoip 来自 MaxMind/ipip 的国家 IP 段,随客户端版本更新。

no-resolve 这个标记值得单独理解:它的含义是「连接目标是域名时跳过本条、不去解析 IP」。浏览器和 CLI 经 HTTP 代理时传的都是域名,所以 GEOIP 规则不会为了判定去做一次 DNS 解析——既省一次解析,也避免了「国内 DNS 把海外域名解析到国内 CDN 节点、被误判直连」这类事故。

这直接关系到一个很多人关心的问题:Claude、ChatGPT 这类对代理敏感的服务,会不会有部分请求漏到直连? 在这套规则下不会。它们的域名(claude.aiapi.anthropic.comchatgpt.comopenai.com)不在 geosite:cn 里,IP 在 Cloudflare 和 AWS 美国段,域名连接又因 no-resolve 不走 GEOIP 判定,只能落到兜底走代理。可以从 Clash 的日志里取证:

[TCP] 127.0.0.1:50912 --> claude.ai:443 match Match using PROXY[MY-LA]
[TCP] 127.0.0.1:50915 --> api.anthropic.com:443 match Match using PROXY[MY-LA]
[TCP] 127.0.0.1:50921 --> chatgpt.com:443 match Match using PROXY[MY-LA]

真正会触发这类服务风控的是另外两件事:同一账号在多个设备上用不同出口(短时间内 IP 在不同国家/城市之间跳),以及手机没开代理时直接打开 App(请求从国内运营商 IP 打到对方)。自建出口天然解决前者——所有设备都用同一台 VPS、同一个出口 IP,从服务方视角你就是一个固定在洛杉矶的用户;后者靠手机端代理常开 + 开机自启来规避。

边界情况:国内公司用海外域名 + 海外 CDN 的站点会走代理(通常反而更快);海外公司在国内有 CDN 节点的(Apple、微软部分域名)会命中 GEOIP,CN 直连,都属正常。极少数走错的,在 Clash「连接」页看它匹配了哪条,加一行 DOMAIN-SUFFIX,xxx.com,DIRECT 做例外即可。

2. 在界面上做两件事

踩坑:Verge 的模式只认界面操作

我一开始图省事,用 mihomo 的控制接口 PATCH /configs {"mode":"rule"} 切了模式,测试全通;但 Verge 自己的状态文件里记录的仍是 global,它下次重启会把模式写回去。GLOBAL 组默认指向 DIRECT,等于代理静默失效。模式、开机自启这类状态,一定要在 Verge 界面上点一次让它自己持久化;如果界面已经显示「规则」高亮,先点「全局」再点回「规则」,逼它写一次。

✅ 本阶段验证

P=http://127.0.0.1:7897
curl -x $P https://ipinfo.io/json          # ip/org 应是 VPS
curl -x $P -o /dev/null -w '%{http_code} %{time_total}\n' https://www.baidu.com   # 直连,< 0.2s
curl -x $P -o /dev/null -w '%{http_code}\n' https://api.anthropic.com/            # 404 即可达
curl -x $P -o /dev/null -w '%{http_code}\n' https://github.com/                   # 200

出口 IP 显示的是本地运营商而不是 VPS,说明模式还在 global 且 GLOBAL 组选了 DIRECT,回到上一条踩坑。

阶段五:切换本机链路

这一阶段的目标:把所有指向旧代理端口的地方改到 7897,旧客户端退出但先不卸载。

先摸清现状再动手——我本机的旧链路是 ShadowsocksX-NG 开 SOCKS 1086 + 内置 Privoxy 转 HTTP 1087,而终端 rc 文件里根本没有代理设置,只有 ~/.claude/settings.jsonenv 块指向 1087。也就是说只有 Claude Code 及其子进程在走代理,git/gh/gradle 一直是直连。所以切换只有两处:

  1. ~/.claude/settings.jsonenv.http_proxy / env.https_proxy 改成 http://127.0.0.1:7897
  2. Clash Verge 开「系统代理」,它会把系统 HTTP/HTTPS/SOCKS 三项全部指向 7897(浏览器走这个)

然后退出旧客户端。留两周当冷备再卸载——新线路要跑过几次晚高峰才能确认真的稳。不要把旧节点导进 Clash 做「一键切换」:那只是往个人配置里塞一段永远不会切过去的废弃加密配置。

✅ 本阶段验证

export https_proxy=http://127.0.0.1:7897
gh api /rate_limit --jq '.resources.core.remaining'
git ls-remote --heads https://github.com/<you>/<repo>.git | head -1
networksetup -getsecurewebproxy Wi-Fi     # Enabled: Yes, Port: 7897

最后一项验收在晚上 8–11 点:用 Claude Code 跑一段长的流式输出,体感正常才算线路达标。白天 ping 通不说明任何问题。

支线:手机接入与日常维护

手机直接导入一条 vless:// 分享链接即可(Shadowrocket、sing-box 均可),格式:

vless://<UUID>@<VPS IP>:443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=dl.google.com&fp=chrome&pbk=<PublicKey>&sid=<shortId>&type=tcp#MY-LA

Android 推荐 v2rayNG(Xray 内核,扫码即用)——注意原版 Clash for Android 已归档且不支持 VLESS/Reality,Shadowsocks 客户端只认 SS 协议,两者都用不了。导入后在「路由设置」选预设**「绕过局域网及中国大陆地址」**,它和电脑那套规则是同一个思路,多了两条:geosite:google 强制代理(防个别 Google 域名解析到国内 IP 被误判),以及阻断 UDP 443(拦掉 QUIC,逼浏览器回落 TCP 走代理,否则这部分 UDP 分流不可控)。

手机端代理可以常开:命中国内规则的流量从手机自己的网络直接出去,多出的开销只是本机进程转一手,淘宝、微信、视频体验和不开时一样;功耗一天约多 1–2%。少数银行类 App 会弹”检测到 VPN”,用时关一下即可。

日常维护表:

事件处理
突然连不上nc -z <IP> 443:通则是协议层问题,换端口或伪装目标;不通则 IP 被墙,后台换 IP 或迁机房
晚高峰变差搬瓦工后台迁到 DC9 或日本机房对比,免费
例行每季度 apt upgrade + 重跑一次 Xray 安装脚本升级内核
兜底留一个几十块一年的商用订阅在 Clash 里,只在 VPS 挂掉时手动切

动手前 Checklist

坑速查表

症状根因修复章节
订单进入人工审核填的国家和下单 IP 对不上关代理后下单,信息填真实阶段二·1
Unable to reload a running VM重装需要先停机Main controls → Stop阶段二·2
客户端 connect error: EOF,服务端 handshake did not complete successfully伪装目标证书链超过 4096 字节握手缓冲换证书链小的 dest(如 dl.google.com)阶段三
Clash 出口 IP 是本地运营商模式为 global 且 GLOBAL 组指向 DIRECT界面切「规则」并让 Verge 持久化阶段四
通过 API 改的模式重启后失效Verge 只持久化界面操作在界面上点一次阶段四
curl https://claude.ai 返回 403Cloudflare 对 curl UA 的人机检查,非链路问题用浏览器验证阶段四

最后

回头看,整件事真正花时间的不是部署,而是那个 handshake did not complete successfully——一条把「认证已通过、只是缓冲装不下证书」说成「非法连接」的日志。它提醒我两件事:一是排障要先做隔离(服务端自连自己),把「谁的问题」定下来再往深挖;二是所有「教程里常见」的默认值都值得量一次,www.microsoft.com 从很多人的配置里抄过来,未必在你的版本上还成立。

至于「为什么自建」——不稳定和不可控是一对孪生问题,商用服务只解决前者的一半。一台自己控制的机器加几十行配置,换来的是接下来一年不用再想这件事。