Skip to content

文档日期

东风云接网

本文给出一条完整但边界克制的链路:在一台有公网出口的 Debian 服务器上运行 yyy1mu/ustc-iwan,只把指定 USTC 内网 IP 的流量送入 iwan0;随后以这台服务器作为 SSH 跳板访问东风云,并建立反向 SOCKS transport。东风云再运行 system-wide sing-box:应用统一连接本机 HTTP/SOCKS mixed listener,由它将 USTC/private 流量直连,其余流量送入反向 SOCKS,经公网服务器访问 Internet。

全文只考虑 Debian 系系统、systemd 与 x86_64。示例目标为:

text
TARGET_IP=172.20.23.66
TARGET_SSH_PORT=4242
TARGET_USER=login
SOCKS_PORT=17890
POLICY_PROXY_PORT=17891
SING_BOX_VERSION=1.13.14

如果你的东风云实例不同,请统一替换这些值。文中的公网地址、登录密码、OIDC token 和 SSH private key 均不会写入配置示例。

一、最终拓扑与边界

text
日常使用的电脑

  │ SSH 到公网服务器

Debian 公网服务器(iWAN gateway / bastion)
  ├── 原默认路由 ───────────────────────────────→ Internet
  ├── ustc-iwan.service
  │     └── iwan0:只接管 172.20.23.66/32
  ├── ustc-iwan-keepalive.service
  ├── SSH ProxyJump ────────────────────────────→ 172.20.23.66:4242
  └── reverse dynamic SOCKS SSH client
        └── 在东风云提供 127.0.0.1:17890

东风云 Debian
  └── sing-box.service:127.0.0.1:17891(HTTP + SOCKS mixed)
        ├── private IP / *.ustc.edu.cn ─────────→ direct
        └── 其他公网 ─→ SOCKS 127.0.0.1:17890
                             └── SSH 加密隧道 → 公网服务器 → Internet

本方案刻意不做以下事情:

  • 不修改公网服务器的 default route;
  • 不把整个 172.20.0.0/16 送入 iWAN;
  • 不在公网服务器或东风云开放新的公网代理端口;
  • 不把东风云改造成透明代理网关;
  • 不让 ping、普通 UDP 或所有系统进程自动经过 explicit proxy;
  • 不把密码写入 systemd unit;
  • 不重新打包或分发 upstream binary。

二、所需前置

2.1 一台 Debian 公网服务器

本文称之为“公网服务器”或“bastion”。最低要求:

  • Debian 12/13 或兼容 Debian 的发行版;
  • x86_64;
  • 使用 systemd
  • 有稳定公网出口;
  • 可以使用 root 或 sudo
  • 内核支持 TUN,存在 /dev/net/tun
  • 允许创建 network interface 和 host route;
  • 可以访问 GitHub 与 USTC iWAN controller;
  • 有稳定的 SSH 登录入口。

如果运行在 VPS 上,虚拟化平台必须允许 TUN。OpenVZ/LXC 服务商可能需要额外开通;KVM 通常可用。

2.2 一台东风云 Debian 主机

最低要求:

  • 已知其 USTC iWAN 内网 IP;
  • Debian 系系统;
  • sshd 已启动;
  • 已安装 openssh-serveriproute2curlca-certificates 与 Debian 标准 dpkg 工具;
  • 已知 SSH 用户和端口;
  • 允许该用户维护 ~/.ssh/authorized_keys
  • sshd 的有效策略允许 remote TCP forwarding,并允许监听 127.0.0.1:17890
  • 127.0.0.1:17891 未被其他服务占用;
  • 即使它不能直接访问 Internet,也必须能经 iWAN 与公网服务器建立 SSH。

本文实例使用:

text
172.20.23.66:4242

这些软件应在东风云失去公网出口前预装,或通过 SSH/SCP 传入 Debian package 后离线安装;不要等 reverse SOCKS 建立后才发现缺少用于验收的 curlss

在东风云检查基础工具:

bash
test -x /usr/sbin/sshd
command -v ss
command -v curl

检查 sshd 的全局 forwarding policy:

bash
sudo /usr/sbin/sshd -T |
  grep -E '^(disableforwarding|allowtcpforwarding|permitlisten|gatewayports) '

至少应满足:

text
disableforwarding no
allowtcpforwarding yes 或 remote
permitlisten any 或包含 127.0.0.1:17890

gatewayports no 不构成阻碍;本文本来就只允许 loopback listener。如果 sshd_config 使用了 Match block,还必须按实际 SSH 用户和公网服务器的 iWAN source address 检查 effective config:

bash
BASTION_IWAN_IP='198.18.x.x'

sudo /usr/sbin/sshd -T \
  -C "user=login,host=ustc-iwan-bastion,addr=$BASTION_IWAN_IP" |
  grep -E '^(disableforwarding|allowtcpforwarding|permitlisten|gatewayports) '

BASTION_IWAN_IP 应替换为公网服务器连接 iWAN 后获得的真实 198.18.x.x 地址。

2.3 USTC iWAN 权限

需要:

  • 可用的 USTC 统一身份认证账号;
  • 账号已获得 iWAN 使用权限;
  • 一台能打开浏览器完成 OIDC 登录的设备;
  • 能把最终的 custom-scheme callback URL 粘贴回公网服务器终端。

2.4 日常使用的电脑

需要 OpenSSH client。它只负责登录公网服务器和通过 ProxyJump 访问东风云,不需要安装 iWAN client。

2.5 已知限制

  • yyy1mu/ustc-iwan 是社区实现,不是 Panabit/USTC 官方 Linux 客户端;
  • 本文固定并实际验证的是 v2.2.0 x86_64 musl release;
  • upstream 仓库在本文写作时没有明确 LICENSE,宜直接从 upstream 下载供本人使用,不要擅自公开再分发二进制或派生作品;
  • v2.2.0 的 OIDC 交互需要人工浏览器登录,没有无人值守 refresh-token 生命周期;
  • TUN path 缺少协议级 keepalive,故本文增加独立 traffic keepalive;
  • OpenSSH dynamic SOCKS 只承载 TCP,不提供 UDP associate;
  • sing-box 本文仅作为 explicit HTTP/SOCKS policy proxy,不启用 TUN,不等价于系统级 VPN;
  • 默认公网 outbound 依赖 127.0.0.1:17890;上游断开时公网 fail closed,而 USTC/private direct 规则仍可工作。

三、参数表

部署前先确定:

参数本文示例含义
TARGET_IP172.20.23.66东风云 iWAN 内网 IP
TARGET_CIDR172.20.23.66/32唯一允许走 iWAN 的目标
TARGET_SSH_PORT4242东风云 SSH 端口
TARGET_USERlogin东风云 SSH 用户
SOCKS_PORT17890OpenSSH reverse dynamic SOCKS transport;通常只供 sing-box 使用
POLICY_PROXY_PORT17891sing-box mixed HTTP/SOCKS listener;应用统一连接此端口
SING_BOX_VERSION1.13.14本文固定并验收的 sing-box stable 版本
BASTION_HOST自行填写公网服务器域名或 IP
BASTION_SSH_PORT自行填写公网服务器 SSH 端口

release 固定值:

text
Release:      v2.2.0
Asset:        iwan-client-oidc-x86_64-musl.zip
Asset SHA256: 580c7f5fe9c8d8d496a0f617228bb96f1bfa21b9f21b43d9368c177f8b4dfb0f
Binary SHA256: 24d171567f5d6a72c2340e9a82b3e69dc1e821e1163477767e0aeef187f52bf6

upstream release binary 的 --version 仍可能显示 0.1.0。因此部署身份应由 release URL 和 SHA-256 共同确定,而不是只看 version string。

四、检查公网服务器

以下命令均在公网服务器执行:

bash
sudo apt update
sudo apt install -y \
  ca-certificates \
  curl \
  unzip \
  iproute2 \
  iputils-ping \
  openssh-client

test -c /dev/net/tun
uname -m
systemctl --version | head -n 1

预期:

text
/dev/net/tun 存在
uname -m 输出 x86_64
systemd 可用

检查安装前 route:

bash
ip -4 route get 172.20.23.66
ip -4 route get 1.1.1.1

此时二者通常都走原公网 uplink。安装后,只允许第一条改走 iwan0

五、下载并验证 iWAN release

bash
workdir="$(mktemp -d)"
cd "$workdir"

curl --fail --location --proto '=https' --tlsv1.2 \
  --output iwan-client-oidc-x86_64-musl.zip \
  https://github.com/yyy1mu/ustc-iwan/releases/download/v2.2.0/iwan-client-oidc-x86_64-musl.zip

printf '%s  %s\n' \
  '580c7f5fe9c8d8d496a0f617228bb96f1bfa21b9f21b43d9368c177f8b4dfb0f' \
  'iwan-client-oidc-x86_64-musl.zip' |
  sha256sum --check --strict

unzip -q iwan-client-oidc-x86_64-musl.zip

printf '%s  %s\n' \
  '24d171567f5d6a72c2340e9a82b3e69dc1e821e1163477767e0aeef187f52bf6' \
  'iwan-client-oidc-x86_64-musl' |
  sha256sum --check --strict

两次校验都必须显示 OK。然后安装:

bash
sudo install -d -o root -g root -m 0755 \
  /usr/local/libexec/ustc-iwan

sudo install -o root -g root -m 0755 \
  iwan-client-oidc-x86_64-musl \
  /usr/local/libexec/ustc-iwan/iwan-client-oidc

printf '%s  %s\n' \
  '24d171567f5d6a72c2340e9a82b3e69dc1e821e1163477767e0aeef187f52bf6' \
  '/usr/local/libexec/ustc-iwan/iwan-client-oidc' |
  sudo tee /usr/local/libexec/ustc-iwan/iwan-client-oidc.sha256 >/dev/null

sudo chmod 0644 \
  /usr/local/libexec/ustc-iwan/iwan-client-oidc.sha256

sudo sha256sum --check --strict \
  /usr/local/libexec/ustc-iwan/iwan-client-oidc.sha256

不要用“文件能执行”代替 digest verification。

六、完成首次 OIDC bootstrap

OIDC 应由普通用户在私密终端中完成:

bash
install -d -m 0700 "$HOME/.config/ustc-iwan-bootstrap"
umask 077

/usr/local/libexec/ustc-iwan/iwan-client-oidc \
  --config-dir "$HOME/.config/ustc-iwan-bootstrap" \
  --fetch

流程如下:

  1. 打开 CLI 输出的认证 URL;
  2. 完成 USTC 统一身份认证;
  3. https://auth.ivpn.ustc.edu.cn/callback?... 只是中间 callback,不要粘贴;
  4. 从成功页面的“打开 iWAN / 打开 APP”按钮复制最终链接;
  5. 最终链接应以 com.panabit.mobile://oauth2redirect?... 开头;
  6. 只把本次登录流程生成的完整链接粘贴回当前 CLI。

检查文件元数据,不输出文件内容:

bash
stat -c 'mode=%a owner=%U size=%s' \
  "$HOME/.config/ustc-iwan-bootstrap/servers.json"

/usr/local/libexec/ustc-iwan/iwan-client-oidc \
  --config-dir "$HOME/.config/ustc-iwan-bootstrap" \
  --list

安装 credential:

bash
sudo install -d -o root -g root -m 0700 /etc/ustc-iwan

sudo install -o root -g root -m 0600 \
  "$HOME/.config/ustc-iwan-bootstrap/servers.json" \
  /etc/ustc-iwan/servers.json

安全提示:

  • servers.json 即使含密文,也应按 credential 管理;
  • 不要把它放进 Git、网盘公开链接或聊天记录;
  • v2.2.0 --fetch 会向交互终端显示一段 token prefix,避免录屏、shell trace 和公开原始日志;
  • 只接受当前 PKCE/OIDC 会话生成的 callback,不复用历史 URL。

七、配置仅 /32 走 iWAN

7.1 主 iWAN service

创建 /etc/systemd/system/ustc-iwan.service

ini
[Unit]
Description=USTC iWAN release client (172.20.23.66/32 only)
Documentation=https://github.com/yyy1mu/ustc-iwan
Wants=network-online.target
After=network-online.target
PartOf=ustc-iwan.target
StartLimitIntervalSec=0
ConditionPathExists=/dev/net/tun
ConditionPathExists=/usr/local/libexec/ustc-iwan/iwan-client-oidc
ConditionPathExists=/etc/ustc-iwan/servers.json

[Service]
Type=simple
WorkingDirectory=/etc/ustc-iwan
StandardInput=data
StandardInputText=1
ExecStart=/usr/local/libexec/ustc-iwan/iwan-client-oidc --config-dir /etc/ustc-iwan --connect --tun iwan0 --proxy-ip 172.20.23.66 --encrypt 1
ExecStartPost=/usr/bin/ip -4 route get 172.20.23.66
Restart=always
RestartSec=10s
KillSignal=SIGINT
TimeoutStopSec=20s

User=root
Group=root
UMask=0077
CapabilityBoundingSet=CAP_NET_ADMIN
AmbientCapabilities=CAP_NET_ADMIN
NoNewPrivileges=yes
DevicePolicy=closed
DeviceAllow=/dev/net/tun rw
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
RestrictSUIDSGID=yes
RestrictRealtime=yes
RestrictNamespaces=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
InaccessiblePaths=-/root
ReadOnlyPaths=/etc/ustc-iwan/servers.json

[Install]
WantedBy=ustc-iwan.target

关键参数:

text
--proxy-ip 172.20.23.66

它表达一个单目标 host route,即 /32。不要改成整个 172.20.0.0/16,更不要添加 0.0.0.0/0

StandardInputText=1 选择教育网线路。本文实测的菜单为:

输入线路
1教育网
2电信
3联通
4移动

7.2 traffic keepalive

创建 /etc/systemd/system/ustc-iwan-keepalive.service

ini
[Unit]
Description=Traffic keepalive for USTC iWAN tunnel
After=ustc-iwan.service
PartOf=ustc-iwan.target

[Service]
Type=simple
DynamicUser=yes
ExecStart=/usr/bin/ping -n -i 30 172.20.23.66
Restart=always
RestartSec=5s

NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
RestrictSUIDSGID=yes
RestrictRealtime=yes
RestrictNamespaces=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_PACKET
CapabilityBoundingSet=CAP_NET_RAW
AmbientCapabilities=CAP_NET_RAW

[Install]
WantedBy=ustc-iwan.target

TUN path 没有稳定的协议级 keepalive;实测 idle 会导致连接退出。每 30 秒向目标发一个 packet,可以维持 tunnel activity。目标暂时不回复并不妨碍它完成 traffic keepalive 的职责。

7.3 聚合 target

创建 /etc/systemd/system/ustc-iwan.target

ini
[Unit]
Description=USTC iWAN tunnel target
Wants=ustc-iwan.service ustc-iwan-keepalive.service
After=network-online.target

[Install]
WantedBy=multi-user.target

7.4 验证并启动

bash
sudo systemd-analyze verify \
  /etc/systemd/system/ustc-iwan.target \
  /etc/systemd/system/ustc-iwan.service \
  /etc/systemd/system/ustc-iwan-keepalive.service

sudo systemctl daemon-reload
sudo systemctl enable --now ustc-iwan.target

为什么使用 KillSignal=SIGINT:实测 upstream 的 SIGINT path 能完整清理 TUN 与 route;普通 SIGTERM 曾留下 controller endpoint route residue。因此不要把它改回默认 SIGTERM。

八、验收 split routing

bash
systemctl --no-pager --full status \
  ustc-iwan.target \
  ustc-iwan.service \
  ustc-iwan-keepalive.service

ip -brief address show dev iwan0
ip -4 route show 172.20.23.66/32
ip -4 route get 172.20.23.66
ip -4 route get 1.1.1.1
ping -c 3 -W 2 172.20.23.66

必须同时成立:

text
iwan0 为 UP
172.20.23.66 dev iwan0
1.1.1.1 仍走原公网 uplink
目标可达

只看到 iwan0 UP 不足以证明分流正确;必须同时检查目标 route 和公网 route。

检查目标 SSH host key:

bash
ssh-keyscan -T 8 -p 4242 -t ed25519 172.20.23.66 |
  ssh-keygen -lf -

首次部署时应通过可信渠道记录 fingerprint。此后若 fingerprint 变化,应先调查原因,不能直接使用 StrictHostKeyChecking=no 绕过。

九、把公网服务器作为 SSH 跳板

在日常使用电脑的 ~/.ssh/config 中加入:

sshconfig
Host ustc-iwan-bastion
    HostName BASTION_PUBLIC_IP_OR_DOMAIN
    Port BASTION_SSH_PORT
    User BASTION_USER
    ServerAliveInterval 30
    ServerAliveCountMax 3

Host dongfengyun
    HostName 172.20.23.66
    Port 4242
    User login
    ProxyJump ustc-iwan-bastion
    ServerAliveInterval 30
    ServerAliveCountMax 3

替换三个 BASTION_* 占位符,然后:

bash
ssh dongfengyun

等价的一次性命令:

bash
ssh \
  -J BASTION_USER@BASTION_PUBLIC_IP_OR_DOMAIN:BASTION_SSH_PORT \
  -p 4242 \
  login@172.20.23.66

ProxyJump 通过现有 SSH connection 建立 direct-tcpip channel,不要求在公网服务器新增公网 listener。

十、从公网服务器向东风云建立反向 SOCKS

本节命令必须在公网服务器上执行:

bash
ssh -NT \
  -o ExitOnForwardFailure=yes \
  -o ServerAliveInterval=30 \
  -o ServerAliveCountMax=3 \
  -R 127.0.0.1:17890 \
  -p 4242 \
  login@172.20.23.66

这里刻意省略了 -R 的 destination。对现代 OpenSSH,这表示 remote dynamic forwarding:在 SSH server 一侧,也就是东风云,创建 SOCKS listener。

不要误写成:

text
-R 127.0.0.1:17890:127.0.0.1:7890

后一种语法要求公网服务器上已经存在 127.0.0.1:7890 proxy。若没有对应 listener,SOCKS 表面上可能监听成功,实际请求会 connection refused。

在东风云验证:

bash
ss -lnt | grep '127.0.0.1:17890'

curl -fsS \
  --proxy socks5h://127.0.0.1:17890 \
  https://api.ipify.org
echo

返回值应为公网服务器的出口 IP。

socks5hh 表示 hostname 在公网服务器一侧解析。如果使用 socks5://,某些工具会先尝试在东风云本地解析 DNS。

十一、让反向 SOCKS 长期运行

手工 SSH 只能活到 terminal、网络连接或主机 reboot 为止。ServerAliveInterval 能发现死连接,但不能自己重新启动进程。长期运行需要:

text
专用 SSH key
+ authorized_keys capability restriction
+ systemd Restart=always

下面在公网服务器创建专用系统用户 iwan-relay,避免让 tunnel 持有日常登录用户的 private key。

11.1 创建专用 relay 用户和 key

bash
sudo useradd \
  --system \
  --create-home \
  --home-dir /var/lib/iwan-relay \
  --shell /usr/sbin/nologin \
  iwan-relay

sudo install -d -o iwan-relay -g iwan-relay -m 0700 \
  /var/lib/iwan-relay/.ssh \
  /var/lib/iwan-relay/.ssh/known_hosts.d

sudo -u iwan-relay ssh-keygen \
  -q \
  -t ed25519 \
  -N '' \
  -C 'dongfengyun-egress@iwan-bastion' \
  -f /var/lib/iwan-relay/.ssh/id_ed25519_dongfengyun_egress

empty passphrase 只用于 unattended service;风险由 dedicated key 与目标端限制控制。

11.2 固定东风云 host key

bash
tmp_known_hosts="$(mktemp)"

ssh-keyscan -T 8 -p 4242 -t ed25519 172.20.23.66 \
  >"$tmp_known_hosts" 2>/dev/null

ssh-keygen -lf "$tmp_known_hosts"

把输出与首次通过可信渠道记录的 fingerprint 比较。匹配后安装:

bash
sudo install -o iwan-relay -g iwan-relay -m 0600 \
  "$tmp_known_hosts" \
  /var/lib/iwan-relay/.ssh/known_hosts.d/dongfengyun

rm -f "$tmp_known_hosts"

11.3 在东风云安装受限 public key

在公网服务器:

bash
public_key="$(sudo cat /var/lib/iwan-relay/.ssh/id_ed25519_dongfengyun_egress.pub)"
restrictions='command="/usr/bin/false",restrict,port-forwarding,permitlisten="127.0.0.1:17890"'
restricted_line="$restrictions $public_key"

printf '%s\n' "$restricted_line" |
  ssh \
    -p 4242 \
    login@172.20.23.66 \
    'set -eu
     umask 077
     install -d -m 0700 "$HOME/.ssh"
     touch "$HOME/.ssh/authorized_keys"
     chmod 0600 "$HOME/.ssh/authorized_keys"
     IFS= read -r line
     grep -qxF -- "$line" "$HOME/.ssh/authorized_keys" ||
       printf "%s\n" "$line" >>"$HOME/.ssh/authorized_keys"'

unset public_key restrictions restricted_line

这条 authorized-key policy 的含义:

  • command="/usr/bin/false":拒绝拿这把 key 打开 shell;
  • restrict:关闭 PTY、agent forwarding、X11 forwarding 等能力;
  • port-forwarding:只重新启用 TCP forwarding;
  • permitlisten="127.0.0.1:17890":只允许该 remote listener。

11.4 创建长期 tunnel unit

如果第十节的手工 tunnel 仍在运行,先在该窗口按 Ctrl-C,避免 17890 被占用。

创建 /etc/systemd/system/dongfengyun-egress-tunnel.service

ini
[Unit]
Description=Dongfengyun reverse dynamic SOCKS via USTC iWAN bastion
Documentation=man:ssh(1)
Requires=ustc-iwan.target
After=network-online.target ustc-iwan.target
PartOf=ustc-iwan.target
StartLimitIntervalSec=0

[Service]
Type=simple
User=iwan-relay
Group=iwan-relay
UMask=0077
ExecStartPre=/usr/bin/test -r /var/lib/iwan-relay/.ssh/id_ed25519_dongfengyun_egress
ExecStartPre=/usr/bin/test -r /var/lib/iwan-relay/.ssh/known_hosts.d/dongfengyun
ExecStart=/usr/bin/ssh -NT -i /var/lib/iwan-relay/.ssh/id_ed25519_dongfengyun_egress -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/var/lib/iwan-relay/.ssh/known_hosts.d/dongfengyun -o ExitOnForwardFailure=yes -o ConnectTimeout=15 -o ConnectionAttempts=1 -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o TCPKeepAlive=yes -R 127.0.0.1:17890 -p 4242 login@172.20.23.66
Restart=always
RestartSec=10s
TimeoutStopSec=15s

NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=read-only
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
RestrictSUIDSGID=yes
RestrictRealtime=yes
RestrictNamespaces=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6

[Install]
WantedBy=ustc-iwan.target

验证并启动:

bash
sudo systemd-analyze verify \
  /etc/systemd/system/ustc-iwan.target \
  /etc/systemd/system/ustc-iwan.service \
  /etc/systemd/system/ustc-iwan-keepalive.service \
  /etc/systemd/system/dongfengyun-egress-tunnel.service

sudo systemctl daemon-reload
sudo systemctl enable --now dongfengyun-egress-tunnel.service

如果 boot 时 iWAN route 尚未 ready,SSH 会失败退出;Restart=always 每 10 秒重试,route ready 后自动恢复。连接黑洞时,ServerAliveInterval=30ServerAliveCountMax=3 大约在 90 秒后判死并交给 systemd 重启。

11.5 验收长期 tunnel

公网服务器:

bash
systemctl --no-pager --full status \
  ustc-iwan.target \
  dongfengyun-egress-tunnel.service

journalctl \
  -u dongfengyun-egress-tunnel.service \
  -n 100 \
  --no-pager

东风云:

bash
ss -lnt | grep '127.0.0.1:17890'

curl -fsS \
  --proxy socks5h://127.0.0.1:17890 \
  https://api.ipify.org
echo

最后应安排一次公网服务器 reboot gate,确认:

text
ustc-iwan.target active
iwan0 UP
172.20.23.66 dev iwan0
dongfengyun-egress-tunnel.service active
东风云 127.0.0.1:17890 listening
东风云 ipify 返回公网服务器出口 IP

这一步只验收底层 transport。完成下一节后,日常应用应统一连接 127.0.0.1:17891,不再直接依赖 17890 的 SOCKS-only interface。

十二、东风云上的代理配置

127.0.0.1:17890 是 transport,不再作为应用的默认入口。东风云额外运行 system-wide sing-box,在 127.0.0.1:17891 提供 mixed HTTP/SOCKS listener,并集中执行分流:

text
应用
  │ HTTP 或 SOCKS

127.0.0.1:17891  sing-box
  ├── private IP / *.ustc.edu.cn ─────────→ direct
  └── 其他公网 ─→ SOCKS 127.0.0.1:17890
                       └── SSH → 公网服务器 → Internet

这样可以兼容只支持 HTTP proxy 的程序,例如未启用 reqwest socks feature 的 fnm;同时避免让 shell、APT、Docker 各自维护不同的 domain bypass 规则。

12.1 固定并提取官方 sing-box binary

本文固定 sing-box v1.13.14。官方 Debian package 自带的示例配置会在 [::]:8080 创建 Shadowsocks listener,官方 unit 还授予当前 explicit-proxy 场景不需要的 capabilities 与 polkit DNS 权限。因此这里只校验并提取 package 中的官方 binary,不执行 dpkg -i

以下命令在东风云执行;下载请求先显式经过已经验证的 17890 SOCKS:

bash
version=1.13.14
asset="sing-box_${version}_linux_amd64.deb"
workdir="$(mktemp -d)"

cd "$workdir"

curl --fail --location --proto '=https' --tlsv1.2 \
  --proxy socks5h://127.0.0.1:17890 \
  --output "$asset" \
  "https://github.com/SagerNet/sing-box/releases/download/v${version}/${asset}"

printf '%s  %s\n' \
  '320523f9586877c4cb244df753d848356787e15f2f4e23a00908af2422206542' \
  "$asset" |
  sha256sum --check --strict

mkdir extracted
dpkg-deb --extract "$asset" extracted

printf '%s  %s\n' \
  '4ea794fddcb2ad84532adeab979a9b0d7b2052822bb3439dfb321c33c941da19' \
  'extracted/usr/bin/sing-box' |
  sha256sum --check --strict

sudo install -o root -g root -m 0755 \
  extracted/usr/bin/sing-box \
  /usr/local/bin/sing-box

/usr/local/bin/sing-box version

cd /
rm -rf "$workdir"

两次 hash check 都必须为 OK,且 version 必须为 1.13.14

12.2 创建专用 system user 与配置

bash
getent group sing-box >/dev/null ||
  sudo groupadd --system sing-box

id sing-box >/dev/null 2>&1 ||
  sudo useradd \
    --system \
    --gid sing-box \
    --home-dir /var/lib/sing-box \
    --no-create-home \
    --shell /usr/sbin/nologin \
    --comment 'sing-box local policy proxy' \
    sing-box

创建临时配置:

bash
config_tmp="$(mktemp)"

cat >"$config_tmp" <<'JSON'
{
  "log": {
    "level": "info",
    "timestamp": true
  },
  "inbounds": [
    {
      "type": "mixed",
      "tag": "local-mixed",
      "listen": "127.0.0.1",
      "listen_port": 17891
    }
  ],
  "outbounds": [
    {
      "type": "direct",
      "tag": "direct"
    },
    {
      "type": "socks",
      "tag": "hostdare-socks",
      "server": "127.0.0.1",
      "server_port": 17890,
      "version": "5",
      "network": "tcp"
    }
  ],
  "route": {
    "rules": [
      {
        "ip_is_private": true,
        "action": "route",
        "outbound": "direct"
      },
      {
        "domain_suffix": [
          "ustc.edu.cn"
        ],
        "action": "route",
        "outbound": "direct"
      }
    ],
    "final": "hostdare-socks"
  }
}
JSON

/usr/local/bin/sing-box check -c "$config_tmp"

sudo install -d -o root -g sing-box -m 0750 \
  /etc/sing-box

sudo install -o root -g sing-box -m 0640 \
  "$config_tmp" \
  /etc/sing-box/config.json

rm -f "$config_tmp"

这里不配置自动 direct fallback。若 17890 消失,默认公网必须 fail closed,不能静默从东风云原始出口泄漏。

12.3 创建 least-privilege systemd unit

创建临时 unit:

bash
unit_tmp="$(mktemp)"

cat >"$unit_tmp" <<'UNIT'
[Unit]
Description=Dongfengyun local policy proxy (HTTP/SOCKS to bastion egress)
Documentation=https://sing-box.sagernet.org/
Wants=network-online.target
After=network-online.target
StartLimitIntervalSec=0

[Service]
Type=simple
User=sing-box
Group=sing-box
UMask=0027
StateDirectory=sing-box
StateDirectoryMode=0750
ExecStartPre=/usr/local/bin/sing-box check -c /etc/sing-box/config.json
ExecStart=/usr/local/bin/sing-box run -c /etc/sing-box/config.json
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
TimeoutStopSec=15s
LimitNOFILE=65536
TasksMax=256

NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=
PrivateDevices=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
ProtectProc=invisible
ProcSubset=pid
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
SystemCallArchitectures=native
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK

[Install]
WantedBy=multi-user.target
UNIT

sudo install -o root -g root -m 0644 \
  "$unit_tmp" \
  /etc/systemd/system/sing-box.service

rm -f "$unit_tmp"

AF_NETLINK 不能删除:sing-box 即使不启用 TUN,也会订阅 Linux route updates;缺少它时会以 subscribe route updates: address family not supported by protocol 退出。

验证并启动:

bash
sudo /usr/local/bin/sing-box check \
  -c /etc/sing-box/config.json

sudo systemd-analyze verify \
  /etc/systemd/system/sing-box.service

sudo systemctl daemon-reload
sudo systemctl enable --now sing-box.service

systemctl is-enabled sing-box.service
systemctl is-active sing-box.service
ss -lnt | grep '127.0.0.1:1789[01]'

systemd-analyze verify 可能同时报告其他历史 unit 的 warning;必须区分无关 warning 与 sing-box.service 自身错误。

12.4 验收 mixed proxy 与集中分流

默认公网经 sing-box → 17890 → 公网服务器:

bash
curl -fsS \
  --proxy http://127.0.0.1:17891 \
  https://api.ipify.org
echo

返回值应为公网服务器出口 IP。mixed inbound 也接受 SOCKS:

bash
curl -fsS \
  --proxy socks5h://127.0.0.1:17891 \
  https://api.ipify.org
echo

检查 USTC direct rule:

bash
curl -I \
  --proxy http://127.0.0.1:17891 \
  --max-time 30 \
  https://www.ustc.edu.cn/

生产验收应至少包含:

text
127.0.0.1:17890 listening
127.0.0.1:17891 listening
HTTP proxy 查询到公网服务器出口 IP
SOCKS proxy 查询到公网服务器出口 IP
USTC domain 可达

可选 crash-recovery gate:记录 MainPID,向 main process 发送一次 SIGKILL,确认 PID 改变、listener 恢复、真实 HTTPS 请求成功,再清理测试计数:

bash
systemctl show sing-box.service -p MainPID -p NRestarts

sudo systemctl kill \
  --kill-whom=main \
  --signal=KILL \
  sing-box.service

systemctl show sing-box.service \
  -p MainPID \
  -p NRestarts \
  -p ActiveState \
  -p SubState

curl -fsS \
  --proxy http://127.0.0.1:17891 \
  https://api.ipify.org
echo

sudo systemctl reset-failed sing-box.service

12.5 配置 shell 与 fnm

当前 shell:

bash
export all_proxy=http://127.0.0.1:17891
export ALL_PROXY="$all_proxy"

export http_proxy="$all_proxy"
export HTTP_PROXY="$http_proxy"
export https_proxy="$all_proxy"
export HTTPS_PROXY="$https_proxy"

export no_proxy=127.0.0.1,localhost,::1
export NO_PROXY="$no_proxy"

需要持久化时,把同一 block 写入实际登录用户的 ~/.bashrc,然后:

bash
source ~/.bashrc

fnm install 24
node --version
npm --version

不要继续把 ustc.edu.cn 或 private CIDR 放进应用层 NO_PROXY:命中 NO_PROXY 后,请求会完全绕过 sing-box,它就无法统一分流。也不要把 17890 伪装成 http://;它是 SOCKS,不是 HTTP proxy。

12.6 配置 APT 与 Docker daemon

APT 不读取普通用户的 .bashrc。创建 /etc/apt/apt.conf.d/80proxy

bash
apt_proxy_tmp="$(mktemp)"

cat >"$apt_proxy_tmp" <<'APT'
Acquire::http::Proxy "http://127.0.0.1:17891";
Acquire::https::Proxy "http://127.0.0.1:17891";
APT

sudo install -o root -g root -m 0644 \
  "$apt_proxy_tmp" \
  /etc/apt/apt.conf.d/80proxy

rm -f "$apt_proxy_tmp"

验证:

bash
apt-config dump | grep -i proxy
sudo apt-get update

Docker daemon 也有独立 environment scope。修改前先检查正在运行的 containers 与 live-restore,再创建 /etc/systemd/system/docker.service.d/proxy.conf

bash
docker_proxy_tmp="$(mktemp)"

cat >"$docker_proxy_tmp" <<'UNIT'
[Service]
Environment="HTTP_PROXY=http://127.0.0.1:17891"
Environment="HTTPS_PROXY=http://127.0.0.1:17891"
Environment="NO_PROXY=127.0.0.1,localhost,::1"
UNIT

sudo install -d -o root -g root -m 0755 \
  /etc/systemd/system/docker.service.d

sudo install -o root -g root -m 0644 \
  "$docker_proxy_tmp" \
  /etc/systemd/system/docker.service.d/proxy.conf

rm -f "$docker_proxy_tmp"

然后:

bash
sudo systemctl daemon-reload
sudo systemctl restart docker

systemctl show docker \
  -p Environment \
  --no-pager

docker info
docker pull hello-world:latest

Docker daemon proxy 不会自动注入 containers;bridge container 内的 127.0.0.1 指向 container 自己,不能直接复用 host loopback listener。

12.7 修改 sing-box 分流

分流配置位于:

text
/etc/sing-box/config.json

sing-box route rules 从上到下匹配,先命中者生效。例如新增 direct domains:

json
{
  "domain": [
    "downloads.example.org"
  ],
  "domain_suffix": [
    "example.com"
  ],
  "action": "route",
  "outbound": "direct"
}

新增 direct CIDR:

json
{
  "ip_cidr": [
    "202.38.64.0/24"
  ],
  "action": "route",
  "outbound": "direct"
}

final 已经是 hostdare-socks,未命中 direct rules 的目标不需要再写 proxy rule。修改时使用 fail-safe 流程:

bash
sudo cp --preserve=all \
  /etc/sing-box/config.json \
  /etc/sing-box/config.json.bak

sudoedit /etc/sing-box/config.json

sudo /usr/local/bin/sing-box check \
  -c /etc/sing-box/config.json

sudo systemctl restart sing-box.service
systemctl is-active sing-box.service

若 check 或 restart 失败,不要继续使用未验证配置;执行:

bash
sudo cp --preserve=all \
  /etc/sing-box/config.json.bak \
  /etc/sing-box/config.json

sudo /usr/local/bin/sing-box check \
  -c /etc/sing-box/config.json

sudo systemctl restart sing-box.service

不要通过扩大 NO_PROXY 替代 sing-box route rules。

12.8 为什么 ping 仍可能失败

本文使用 explicit proxy,不修改东风云 kernel default route:

text
curl / Git / fnm / HTTPS     可以通过 17891
ping                          不经过 HTTP/SOCKS proxy
普通 UDP                      不经过 OpenSSH dynamic SOCKS

因此 ping 公网域名无响应,不足以否定代理链路;应使用显式 curl --proxy http://127.0.0.1:17891 ... 验收。

HTTPS 仍由应用与目标站点端到端加密;东风云到公网服务器之间还有 SSH encryption。sing-box 和 SOCKS 只负责转发,不终止目标 TLS。

十三、日常运维

查看状态

公网服务器:

bash
sudo systemctl --no-pager --full status \
  ustc-iwan.target \
  ustc-iwan.service \
  ustc-iwan-keepalive.service \
  dongfengyun-egress-tunnel.service

东风云:

bash
systemctl --no-pager --full status sing-box.service
ss -lnt | grep '127.0.0.1:1789[01]'

查看日志

公网服务器:

bash
sudo journalctl \
  -u ustc-iwan.service \
  -u ustc-iwan-keepalive.service \
  -u dongfengyun-egress-tunnel.service \
  --since today \
  --no-pager

东风云:

bash
sudo journalctl \
  -u sing-box.service \
  --since today \
  --no-pager

不要公开发送未经检查的 OIDC/iWAN 或 proxy 原始日志;sing-box info 日志可能包含目标 domain/IP。

启停完整链路

公网服务器控制 iWAN 与 reverse tunnel:

bash
sudo systemctl start ustc-iwan.target
sudo systemctl stop ustc-iwan.target

reverse tunnel unit 使用 PartOf=ustc-iwan.target,停止 target 时会一起停止。东风云的 sing-box service 独立运行:17890 消失后,17891 仍监听,但默认公网 outbound fail closed;direct rules 仍可工作。

东风云单独启停 policy proxy:

bash
sudo systemctl start sing-box.service
sudo systemctl stop sing-box.service

单独重启 reverse tunnel

bash
sudo systemctl restart dongfengyun-egress-tunnel.service

单独重启 sing-box

在东风云执行:

bash
sudo /usr/local/bin/sing-box check \
  -c /etc/sing-box/config.json

sudo systemctl restart sing-box.service
systemctl is-active sing-box.service

切换 iWAN 线路

编辑 /etc/systemd/system/ustc-iwan.service 的:

ini
StandardInputText=1

改为 1..4 后:

bash
sudo systemctl daemon-reload
sudo systemctl restart ustc-iwan.service

主 tunnel 中断时,reverse SSH 最迟约 90 秒发现连接失效,随后自动重建。

重新完成 OIDC

bash
sudo systemctl stop ustc-iwan.target

bootstrap_dir="$(mktemp -d "$HOME/.config/ustc-iwan-bootstrap.XXXXXX")"
chmod 0700 "$bootstrap_dir"
umask 077

/usr/local/libexec/ustc-iwan/iwan-client-oidc \
  --config-dir "$bootstrap_dir" \
  --fetch

sudo install -o root -g root -m 0600 \
  "$bootstrap_dir/servers.json" \
  /etc/ustc-iwan/servers.json

sudo systemctl start ustc-iwan.target

更新 iWAN release

更新版本时必须重新获取并核对新 release 的 digest,不能沿用本文 v2.2.0 hash:

  1. 停止 ustc-iwan.target
  2. 下载新 release;
  3. 验证新 asset digest;
  4. 原子替换 binary;
  5. 更新 .sha256
  6. 重启并重新执行 split-route、SSH、soak 与 cleanup 验收。

更新 sing-box release

不能沿用本文 v1.13.14 的 package/binary digest 验证其他版本。更新时:

  1. 从 official GitHub release 获取新 asset 与 checksum;
  2. 在临时目录提取 binary,不直接安装 package 自带 config/unit;
  3. 先用新 binary 对现有 /etc/sing-box/config.json 执行 check
  4. 备份并原子替换 /usr/local/bin/sing-box
  5. 重启 service,复验 HTTP、SOCKS、direct rule、默认出口与 crash recovery;
  6. 只有全部通过后才删除 rollback binary。

十四、常见故障

症状主要原因检查与处理
/dev/net/tun 不存在VPS 未开放 TUN联系服务商或换 KVM VPS
iwan0 不存在client 未启动或 crashsystemctl status ustc-iwan.service
target 仍走默认路由--proxy-ip 未生效ip route get 172.20.23.66
普通公网走 iWAN误加 default route立即停止并检查 unit/route
TUN 数分钟后退出缺少 traffic keepalive检查 keepalive service
target SSH timeoutiWAN route 或线路故障ping target,检查 route,切换线路
host-key verification failedfingerprint 未固定或变化可信渠道重新核对,不能关闭校验
remote port forwarding failed东风云 17890 已被占用停止旧手工 SSH,检查 ss -lnt
systemd 尝试询问密码dedicated key 未生效BatchMode=yes 下应直接失败重试
17890 存在而 17891 不存在sing-box 未启动或 config check 失败检查 systemctl statusjournalctl -u sing-box.service
sing-box 报 subscribe route updatesunit 缺少 AF_NETLINK恢复 RestrictAddressFamilies 中的 AF_NETLINK
17891 存在,但普通公网失败而 USTC 可达reverse SOCKS 上游断开检查 17890 与公网服务器 tunnel unit
fnm 报 reqwest proxy builder error仍把 SOCKS URL 写进 HTTP proxy env所有 shell proxy env 改为 http://127.0.0.1:17891
新增 direct rule 不生效请求被 NO_PROXY 绕过,或应用先解析为 IP收窄 NO_PROXY;必要时增加 ip_cidr rule
HTTP/SOCKS curl 成功但 ping 失败ICMP 不经过 explicit proxy预期行为
IP 查询不是公网服务器出口应用未使用 17891,或目标命中 direct rule显式传 --proxy 并检查 sing-box log/rules

十五、回滚与卸载

只关闭东风云公网出口

在公网服务器执行:

bash
sudo systemctl disable --now dongfengyun-egress-tunnel.service

在东风云确认 transport listener 消失:

bash
ss -lnt | grep ':17890' || echo 'SOCKS listener removed'

此时 sing-box 的 17891 仍可监听;默认公网 outbound 应 fail closed,direct rules 仍可工作。

然后从 ~login/.ssh/authorized_keys 中只删除注释为:

text
dongfengyun-egress@iwan-bastion

的专用 key 行,不要误删其他 key。

只卸载东风云 sing-box

先删除或改回所有指向 127.0.0.1:17891 的 consumer 配置,包括登录用户 ~/.bashrc、APT proxy 和 Docker daemon drop-in;否则 service 停止后这些程序会继续连接不存在的 proxy。

然后在东风云执行:

bash
sudo systemctl disable --now sing-box.service
sudo systemctl reset-failed sing-box.service

sudo rm -f \
  /etc/systemd/system/sing-box.service \
  /etc/apt/apt.conf.d/80proxy \
  /etc/systemd/system/docker.service.d/proxy.conf

sudo systemctl daemon-reload

sudo rm -rf /etc/sing-box
sudo rm -rf /var/lib/sing-box
sudo rm -f /usr/local/bin/sing-box
sudo userdel sing-box
sudo groupdel sing-box 2>/dev/null || true

如果删除了 Docker drop-in,还应重启 Docker;先检查 running containers 与 live-restore,避免无计划中断。

停止 iWAN

bash
sudo systemctl stop ustc-iwan.target

ip link show iwan0 2>/dev/null || echo 'iwan0 removed'
ip -4 route show 172.20.23.66/32

完整卸载 owned artifacts

先在公网服务器执行:

bash
sudo systemctl disable --now \
  dongfengyun-egress-tunnel.service \
  ustc-iwan.target

sudo rm -f \
  /etc/systemd/system/dongfengyun-egress-tunnel.service \
  /etc/systemd/system/ustc-iwan.service \
  /etc/systemd/system/ustc-iwan-keepalive.service \
  /etc/systemd/system/ustc-iwan.target

sudo systemctl daemon-reload
sudo systemctl reset-failed

只有明确决定删除 credential、binary 与 dedicated key 时,再执行:

bash
sudo rm -rf /etc/ustc-iwan
sudo rm -rf /usr/local/libexec/ustc-iwan
sudo userdel --remove iwan-relay

然后按上一小节在东风云删除 sing-box 与 consumer proxy 配置。两侧 artifacts 的 ownership 不同,不要把公网服务器 unit path 与东风云 sing-box path 混在同一条远程命令里。

十六、最终验收清单

  • [ ] 公网服务器为 Debian 系、x86_64、systemd,且 /dev/net/tun 可用;
  • [ ] release asset SHA-256 正确;
  • [ ] binary SHA-256 正确;
  • [ ] /etc/ustc-iwan 为 root-only;
  • [ ] servers.json 为 root-owned 0600
  • [ ] ustc-iwan.target active;
  • [ ] iwan0 UP;
  • [ ] 只有目标 /32iwan0
  • [ ] default route 不变;
  • [ ] target SSH fingerprint 已通过可信渠道固定;
  • [ ] ProxyJump 成功;
  • [ ] reverse tunnel 使用受限 dedicated key;
  • [ ] 17890 只监听东风云 loopback,并经 SOCKS 查询到公网服务器出口 IP;
  • [ ] sing-box v1.13.14 package 与 binary SHA-256 正确;
  • [ ] sing-box.servicesing-box system user、空 capability set 运行;
  • [ ] 17891 只监听东风云 loopback,并同时接受 HTTP 与 SOCKS;
  • [ ] 东风云经 17891 查询到公网服务器出口 IP;
  • [ ] USTC domain 与 private IP 经 sing-box direct;其他公网 fail closed to 17890,没有 direct fallback;
  • [ ] shell 使用 http://127.0.0.1:17891NO_PROXY 只保留 loopback;
  • [ ] fnm install 24、Node 与 npm 经 HTTP proxy 正常;
  • [ ] 已理解 ping 不经过 explicit proxy;
  • [ ] 手工 SSH tunnel 无残留;
  • [ ] sing-box 被 SIGKILL 后 systemd 能自动恢复 listener 与真实请求;
  • [ ] 公网服务器与东风云 reboot 后,reverse tunnel 和 sing-box 都能自动恢复;
  • [ ] 停止 ustc-iwan.target 后,TUN、目标 route 与 17890 均消失,而 17891 对默认公网 fail closed。

十七、参考