---
layout: post
title: 针对当前机场节点不稳定的一个方案-为机场订阅添加一层 VPS 中转
date: 2026-07-14 18:39:13
status: publish
author: harumonia
noThumbInfoStyle: default
outdatedNotice: 'no'
reprint: standard
thumbChoice: default
thumbStyle: default
hidden: false
email: zxjlm233@gmail.com
updated:
categories:
  - VPS
  - Network
tags:
---

# 用一台美区 VPS，给机场订阅套一个固定出口

## 问题背景

目前笔者使用的是 机场订阅 这样的一个 🪜 方案，但是今年上半年服务变得非常地不稳定，在相关的技术论坛上，这似乎是一个普遍的现象，所以笔者也就没有换其他的服务商。

但是这终究是一个问题，服务不稳定，如果使用单节点，就会面临时而可用，时而不可用的窘境；但是如果让他通过 url-test 策略自由切换，又会导致出口 IP 不停变换，笔者使用 Claude 和 GPT，这两者都是有风控的，频繁切换 IP 无疑是高风险行为，所以期望能够有一套更加稳定的访问策略。

![原先的方案](https://pic.harumonia.moe/illustration/before-clash-mihomo-plan-low.webp)

笔者有一台美区的传家宝 VPS，于是就有了本篇的一个方案改造。

核心矛盾其实很清楚：

- 机场节点**多、快、但飘**——出口 IP 随时切换；
- 传家宝 VPS**稳、IP 固定、但线路差**——直连体验很糟糕。

那能不能把两者的优点拼起来？让机场节点负责把流量安全高速地送出国，再统一从我自己的 VPS 落地。这样机场怎么抖，最终目标网站看到的都是同一个固定 IP。

目前这个方案以及平稳运行了几天，效果还不错，所以分享出来给大家做一个参考。

## 一、整体思路

答案是 Mihomo 的 `dialer-proxy`。它允许一个代理节点「不直接连出去」，而是先经过另一个代理组，再建立自己的连接。链路串起来是这样：

```text
本地应用
   │
   ▼
Mihomo / Clash Meta
   │
   ├── SUB-UPSTREAM：机场订阅节点组（url-test / fallback）
   │       ├── 香港节点
   │       ├── 日本节点
   │       └── 新加坡 / 美国节点
   │
   ▼
US-VPS-FIXED：Shadowsocks AEAD over TCP（dialer-proxy 指向 SUB-UPSTREAM）
   │
   ▼
目标网站
```

最终链路为：

```text
本地 → 机场节点 → 美国 VPS → 目标网站
```

目标网站只能看到美国 VPS 的公网 IP。机场节点在这里退化成一条「加速管道」，负责翻墙出去；真正决定出口身份的是 VPS。Mihomo 官方也把这种配置明确列为「通过订阅节点中继自己的 VPS」的典型场景，并推荐被中继的 VPS 使用简单的 Shadowsocks AEAD 或 VMess，不要再叠 TLS、混淆这些东西——因为翻墙这一段已经由机场节点完成了。

![改造的方案](https://pic.harumonia.moe/illustration/now-clash-mihomo-plan-low.webp)

> 为什么不直接用 VPS 搭建一个服务 ?
> 
> 笔者在上学时也搭过自己的 🪜 服务，但是 GFW 会根据流量特征来屏蔽 vps ip，笔者在这方面没有太深的研究，所以使用的都是网上的一些方案，流量特征都属于 “记录在案” 的那些，导致用个两天 ip 就废掉了，重新开一个能直连的 ip 又很麻烦。 所以现在干脆放专业的服务商来越过 GFW ，而笔者的 VPS 只需要处理墙外的流量即可。

## 二、几个关键设计决策

### 2.1 本地必须是 Mihomo 内核

先确认客户端跑的是 **Mihomo / Clash Meta**，而不是早已停止维护的旧版 Clash 内核。整个方案依赖：

```yaml
dialer-proxy: SUB-UPSTREAM
```

在 Mihomo 中，`dialer-proxy` 可以引用具体代理或一整个代理组，用来决定当前节点通过哪个上游建立连接。旧内核没有这个能力。

### 2.2 服务端用 Shadowsocks-rust 官方镜像

服务端选择官方镜像：

```text
ghcr.io/shadowsocks/ssserver-rust
```

它提供 `amd64`、`arm64` 等多架构镜像，挂载配置文件即可运行 `ssserver`。建议固定版本，不要长期用 `latest`。笔者实际部署时用的是一台老 VPS 上早就缓存好的镜像（后文会讲到这里踩的坑）。

### 2.3 加密算法

方案统一用：

```text
aes-256-gcm
```

经典 AEAD 算法，Mihomo 和 Shadowsocks-rust 都支持。如果你的 VPS 是没有 AES 硬件加速的低性能 ARM 机器，可以换成 `chacha20-ietf-poly1305`。**服务端和客户端的算法、密码必须完全一致。**

### 2.4 只走 TCP

服务端设 `"mode": "tcp_only"`，Docker 也只发布 TCP 端口、不发布 UDP。这样做的好处是攻击面更小、也避免 UDP 被滥用。

需要留意的是：TCP-only 意味着 QUIC / HTTP/3 走不了这条固定出口。绝大多数浏览器会在 QUIC 失败后自动回退到 HTTP/2，所以日常浏览无感；但纯 UDP 的应用（部分游戏、实时语音）会直接失败。要不要开 UDP，取决于你的实际用途——笔者后来在本地节点上把 `udp` 打开了，是因为顺手要用一些依赖 UDP 的场景，取舍自便。

## 三、VPS 服务端部署

下面的示例统一假设（请替换成你自己的真实值）：

```text
VPS 公网 IP：203.0.113.10        # 文档保留地址，占位用
Shadowsocks 端口：38443
加密算法：aes-256-gcm
部署目录：/opt/ss-exit
```

### 3.1 创建目录并收紧权限

```bash
sudo mkdir -p /opt/ss-exit/config
cd /opt/ss-exit

sudo touch compose.yaml
sudo touch config/config.json
sudo touch config/server.acl

sudo chown -R root:root /opt/ss-exit
sudo chmod 700 /opt/ss-exit
sudo chmod 700 /opt/ss-exit/config
sudo chmod 600 /opt/ss-exit/config/config.json
sudo chmod 600 /opt/ss-exit/config/server.acl
```

### 3.2 生成强随机密码

```bash
umask 077
openssl rand -base64 48
```

会得到一串类似 `XXXXXXXXXXXXXXXXXXXXXXXX...`（此处省略）的随机串。

### 3.3 服务端配置

`/opt/ss-exit/config/config.json`：

```json
{
  "servers": [
    {
      "server": "0.0.0.0",
      "server_port": 38443,
      "method": "aes-256-gcm",
      "password": "替换为上一步生成的随机密码",
      "timeout": 300,
      "acl": "/etc/shadowsocks-rust/server.acl"
    }
  ],
  "mode": "tcp_only",
  "no_delay": true,
  "keep_alive": 15,
  "nofile": 32768
}
```

要点：`server: 0.0.0.0` 监听容器地址；`tcp_only` 彻底关掉 UDP；`acl` 指向出站访问控制文件；不叠加插件 / WebSocket / TLS，保持链路简单。

### 3.4 出站 ACL：把 VPS 自己保护起来

这一步很关键。默认情况下，一旦密码泄露，攻击者可以把你的 Shadowsocks 当成 SSRF 跳板，去访问 VPS 内网服务、Docker 宿主机、甚至云平台元数据地址（`169.254.169.254`）。ACL 就是用来堵死这条路的。

`/opt/ss-exit/config/server.acl`：

```text
[accept_all]

[outbound_block_list]

# IPv4 回环、私网、链路本地和保留地址
0.0.0.0/8
10.0.0.0/8
100.64.0.0/10
127.0.0.0/8
169.254.0.0/16
172.16.0.0/12
192.0.0.0/24
192.0.2.0/24
192.168.0.0/16
198.18.0.0/15
198.51.100.0/24
203.0.113.0/24
224.0.0.0/4
240.0.0.0/4

# IPv6 回环、ULA、链路本地和组播
::1
fc00::/7
fe80::/10
ff00::/8
```

其中最要命的几条是 `127.0.0.1`、`10/8`、`172.16/12`、`192.168/16` 和 `169.254.169.254`——挡住它们，即使密码泄露，代理也进不了内网和元数据接口。

> 小提醒：手抄这份列表极易出错。笔者第一次部署时就把 `169.254.0.0/16` 敲成了 `169.254.016`、`192.0.0.0/24` 写成 `192.0/24`，CIDR 一旦写错，ACL 就形同虚设。建议直接复制，然后逐行核对。

### 3.5 Docker Compose

`/opt/ss-exit/compose.yaml`：

```yaml
services:
  server:
    image: ghcr.io/shadowsocks/ssserver-rust:v1.24.0
    container_name: ss-fixed-exit
    restart: unless-stopped
    user: nobody
    command: ["ssserver", "--log-without-time", "-c", "/etc/shadowsocks-rust/config.json"]

    ports:
      - "0.0.0.0:38443:38443/tcp"

    volumes:
      - ./config/config.json:/etc/shadowsocks-rust/config.json:ro
      - ./config/server.acl:/etc/shadowsocks-rust/server.acl:ro

    read_only: true

    tmpfs:
      - /tmp:size=16m,mode=1777

    cap_drop:
      - ALL

    security_opt:
      - no-new-privileges:true

    pids_limit: 128
    mem_limit: 256m
    cpus: 1.0

    ulimits:
      nofile:
        soft: 32768
        hard: 32768

    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
```

这套配置实现了：配置文件只读挂载、容器根文件系统只读、删除全部 Linux capabilities、禁止提权、限制 PID / 内存 / CPU、日志轮转，并且不用 `privileged`、不用 host network、不挂 Docker Socket、不映射 UDP。

### 3.6 启动与验证监听

```bash
cd /opt/ss-exit
sudo docker compose config
sudo docker compose up -d

sudo docker compose ps
sudo docker compose logs --tail=100
sudo ss -lntp | grep 38443
```

预期只看到 **TCP** 监听，不应该出现 `38443/udp`。

## 四、防火墙与端口暴露

### 4.1 优先用云厂商安全组

在云平台安全组放行 `TCP 38443`，**不要**放行对应的 UDP。

如果你的机场能提供稳定的出口 IP 列表，最安全的做法是只允许这些地址连接（源 IP 白名单）。但现实中机场出口 IP 经常变，拿不到完整列表，那就只能放开：

```text
0.0.0.0/0 → TCP 38443
```

这时安全性完全压在**高强度随机密码 + TCP-only + 出站 ACL + 速率限制 + 流量监控**上。随机高端口只能减少扫描噪声，不能替代认证。

### 4.2 小心 Docker 绕过 UFW

一个常见误区：以为 `ufw allow 38443/tcp` 就万事大吉了。实际上 Docker 发布端口时，数据包在到达 UFW 的 `INPUT` 链之前就已经被 Docker 的 NAT 转发，**UFW 规则会被绕过**。正确的做法是优先用云安全组，或者写到 Docker 专用的 `DOCKER-USER` 链上。

### 4.3 可选的连接速率限制

在公网开放端口、又拿不到白名单时，可以给单个源 IP 的异常新建连接加一道闸（阈值故意放宽，避免误伤正常网页并发）：

```bash
# 单源新建连接速率限制
sudo iptables -I DOCKER-USER 1 \
  -p tcp --dport 38443 \
  -m conntrack --ctstate NEW \
  -m hashlimit \
  --hashlimit-above 120/second \
  --hashlimit-burst 240 \
  --hashlimit-mode srcip \
  --hashlimit-name ss_new_conn \
  -j DROP

# 单源并发连接数限制
sudo iptables -I DOCKER-USER 2 \
  -p tcp --dport 38443 \
  -m connlimit \
  --connlimit-above 1024 \
  --connlimit-mask 32 \
  -j REJECT --reject-with tcp-reset
```

规则持久化到 `/etc/iptables/rules.v4`，再配一个开机 `iptables-restore` 的 systemd 服务即可自动恢复：

```ini
# /etc/systemd/system/iptables-restore.service
[Unit]
Description=Restore iptables rules
Before=docker.service
After=network.target

[Service]
Type=oneshot
ExecStart=/sbin/iptables-restore /etc/iptables/rules.v4
RemainAfterExit=yes

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

> 注意：机场节点是共享出口，同一个出口 IP 背后可能有一堆用户，速率阈值别设太小，先观察实际峰值再逐步收紧。

## 五、本地 Mihomo 配置

### 5.1 别直接改订阅文件

大多数图形客户端在订阅更新时会覆盖配置。要用客户端提供的 **Override / Mixin / 配置覆写 / 扩展配置** 功能，把下面的内容合并进去。

### 5.2 定义 VPS 节点

```yaml
proxies:
  - name: US-VPS-FIXED
    type: ss
    server: 203.0.113.10          # 换成你的 VPS IP
    port: 38443
    cipher: aes-256-gcm
    password: "与服务端完全相同的随机密码"

    udp: true                     # 需要 UDP 就开，纯稳定出口可设 false
    ip-version: ipv4

    dialer-proxy: SUB-UPSTREAM     # 关键：先走机场组，再连 VPS
```

`dialer-proxy: SUB-UPSTREAM` 这一行是整个方案的核心——它让 Mihomo 通过机场组去连 VPS，而目标网站看到的仍然是 `US-VPS-FIXED` 的公网 IP。

### 5.3 定义机场上游组

```yaml
proxy-groups:
  - name: SUB-UPSTREAM
    type: url-test
    use:
      - airport                    # 你的机场 provider 名称
    interval: 300                  # 每 300 秒重测延迟
    tolerance: 50                  # 新节点快 50ms 以上才切换，避免抖动
    lazy: true                     # 没被使用时不主动测速，省资源
    filter: "(?i)香港|日本|新加坡|美国|韩国|台湾|HK|JP|SG|US"
```

这里笔者用的是 `url-test`——因为下游 VPS 已经是固定出口，上游怎么切换都不影响最终 IP，所以让它自动挑最快的机场节点就行。

如果你更在意**连接稳定性**（比如上游节点掉线时不想频繁重连），也可以用 `fallback`：按顺序用第一个可用节点，坏了才往后切。两种都行，取舍在于「延迟最优」还是「尽量不切换」。建议把 `filter` 范围收在香港 / 日本 / 新加坡 / 美西，别让它自动选到欧洲、南美这种明显绕路的节点。

### 5.4 定义出口策略

```yaml
proxy-groups:
  - name: US-EXIT
    type: select
    proxies:
      - US-VPS-FIXED
```

需要固定出口的流量，规则里统一指向 `US-EXIT` 即可：

```yaml
rules:
  - DOMAIN-SUFFIX,openai.com,US-EXIT
  - DOMAIN-SUFFIX,anthropic.com,US-EXIT
  - DOMAIN-SUFFIX,github.com,US-EXIT

  - GEOIP,CN,DIRECT
  - MATCH,US-EXIT
```

### 5.5 关于「故障降级」的取舍

有一种很自然的想法是：VPS 挂了就自动切回机场节点直出。**但这恰恰是要避免的**——一旦自动降级，VPS 短暂抖动时对外 IP 就变了，账号风控该触发还是会触发。

本方案更推荐 **fail-closed**：VPS 不可用 → 请求直接失败 → 人工决定要不要临时切到机场直出。对登录账号、API IP 白名单、自动化任务这类对出口一致性敏感的场景来说，「失败」比「悄悄换 IP」安全得多。

## 六、验证清单

部署完拿这几条过一遍：

```bash
# 1. 服务端只监听 TCP
sudo ss -lntp | grep 38443

# 2. 本地经代理看到的是 VPS IP
curl -x http://127.0.0.1:7890 https://api.ipify.org; echo
#    预期输出：你的美国 VPS 公网 IP
```

然后手动在 `SUB-UPSTREAM` 里依次切换香港 / 日本 / 新加坡节点，每次都跑一遍上面的 `curl`——**每次都应该返回同一个 VPS IP**。切换入口时已有的 TCP 长连接可能断开重连，这是正常的，出口身份不变就对了。

最后验证 fail-closed：在 VPS 上 `docker compose stop`，本地访问应该**失败**，而不是自动露出机场 IP；再 `start` 恢复即可。

## 七、笔者踩过的坑

真机部署（一台 Ubuntu 18.04、内核 4.15 的老 VPS）时，遇到的几个问题记录在这，帮你省点时间：

| 现象 | 原因 | 处理 |
|------|------|------|
| `cap_drop: ALL` 后容器起不来 | 默认 CMD 带 `-a nobody`，需要 SETUID/SETGID | 用 `user: nobody` + 自定义 `command`，去掉 `-a nobody` 参数 |
| `config.json` 读取 Permission denied | 以 nobody 运行时，配置文件属组不对 | `chown root:nogroup` 并 `chmod 640`（宿主机上 nobody 对应的组名是 `nogroup`，不是 `nobody`） |
| 启动时 `Memory limited without swap` 警告 | 内核 4.15 不支持 cgroup swap limit | 忽略即可，不影响运行 |
| ACL 写了却没生效 | CIDR 手抄写错（如 `169.254.016`、`192.0/24`） | 逐行核对网段，别手抄 |

## 八、日常运维速查

```bash
# 状态 / 日志
cd /opt/ss-exit && docker compose ps
docker logs ss-fixed-exit --tail=50

# 重启 / 停止 / 启动
cd /opt/ss-exit && docker compose restart
cd /opt/ss-exit && docker compose down
cd /opt/ss-exit && docker compose up -d

# 确认只监听 TCP
ss -lntp | grep 38443

# 查看防火墙规则
iptables -L DOCKER-USER -n --line-numbers
```

### 密码怀疑泄露时

经典单密码 Shadowsocks AEAD 不支持平滑轮换，切换期间会有短暂中断，按顺序处理：

1. 安全组临时关掉 TCP 38443；
2. 生成新随机密码；
3. 改服务端 `config.json`，同步改本地 `US-VPS-FIXED` 的密码；
4. `docker compose up -d --force-recreate` 重启；
5. 重新开放端口，检查流量统计和异常连接；
6. 必要时连端口一起换。

## 小结

绕了这么一圈，本质上就是把「机场节点」和「自有 VPS」的角色分工：机场只管把流量高速送出国，VPS 负责统一落地给出一个固定 IP。机场再怎么飘，Claude、GPT 看到的都是同一个出口，风控这一关也就稳了。

方案不复杂，真正花时间的都在细节上——ACL 的 CIDR、老内核的 cgroup、nobody 的属组、Docker 绕过 UFW 这些坑。希望这篇能帮到苦 GFW 久矣的诸君。
