东风云接网
本文给出一条完整但边界克制的链路:在一台有公网出口的 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。示例目标为:
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 均不会写入配置示例。
一、最终拓扑与边界
日常使用的电脑
│
│ 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-server、iproute2、curl、ca-certificates与 Debian 标准dpkg工具; - 已知 SSH 用户和端口;
- 允许该用户维护
~/.ssh/authorized_keys; sshd的有效策略允许 remote TCP forwarding,并允许监听127.0.0.1:17890;127.0.0.1:17891未被其他服务占用;- 即使它不能直接访问 Internet,也必须能经 iWAN 与公网服务器建立 SSH。
本文实例使用:
172.20.23.66:4242这些软件应在东风云失去公网出口前预装,或通过 SSH/SCP 传入 Debian package 后离线安装;不要等 reverse SOCKS 建立后才发现缺少用于验收的 curl 与 ss。
在东风云检查基础工具:
test -x /usr/sbin/sshd
command -v ss
command -v curl检查 sshd 的全局 forwarding policy:
sudo /usr/sbin/sshd -T |
grep -E '^(disableforwarding|allowtcpforwarding|permitlisten|gatewayports) '至少应满足:
disableforwarding no
allowtcpforwarding yes 或 remote
permitlisten any 或包含 127.0.0.1:17890gatewayports no 不构成阻碍;本文本来就只允许 loopback listener。如果 sshd_config 使用了 Match block,还必须按实际 SSH 用户和公网服务器的 iWAN source address 检查 effective config:
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.0x86_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_IP | 172.20.23.66 | 东风云 iWAN 内网 IP |
TARGET_CIDR | 172.20.23.66/32 | 唯一允许走 iWAN 的目标 |
TARGET_SSH_PORT | 4242 | 东风云 SSH 端口 |
TARGET_USER | login | 东风云 SSH 用户 |
SOCKS_PORT | 17890 | OpenSSH reverse dynamic SOCKS transport;通常只供 sing-box 使用 |
POLICY_PROXY_PORT | 17891 | sing-box mixed HTTP/SOCKS listener;应用统一连接此端口 |
SING_BOX_VERSION | 1.13.14 | 本文固定并验收的 sing-box stable 版本 |
BASTION_HOST | 自行填写 | 公网服务器域名或 IP |
BASTION_SSH_PORT | 自行填写 | 公网服务器 SSH 端口 |
release 固定值:
Release: v2.2.0
Asset: iwan-client-oidc-x86_64-musl.zip
Asset SHA256: 580c7f5fe9c8d8d496a0f617228bb96f1bfa21b9f21b43d9368c177f8b4dfb0f
Binary SHA256: 24d171567f5d6a72c2340e9a82b3e69dc1e821e1163477767e0aeef187f52bf6upstream release binary 的 --version 仍可能显示 0.1.0。因此部署身份应由 release URL 和 SHA-256 共同确定,而不是只看 version string。
四、检查公网服务器
以下命令均在公网服务器执行:
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预期:
/dev/net/tun 存在
uname -m 输出 x86_64
systemd 可用检查安装前 route:
ip -4 route get 172.20.23.66
ip -4 route get 1.1.1.1此时二者通常都走原公网 uplink。安装后,只允许第一条改走 iwan0。
五、下载并验证 iWAN release
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。然后安装:
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 应由普通用户在私密终端中完成:
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流程如下:
- 打开 CLI 输出的认证 URL;
- 完成 USTC 统一身份认证;
https://auth.ivpn.ustc.edu.cn/callback?...只是中间 callback,不要粘贴;- 从成功页面的“打开 iWAN / 打开 APP”按钮复制最终链接;
- 最终链接应以
com.panabit.mobile://oauth2redirect?...开头; - 只把本次登录流程生成的完整链接粘贴回当前 CLI。
检查文件元数据,不输出文件内容:
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:
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:
[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关键参数:
--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:
[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.targetTUN path 没有稳定的协议级 keepalive;实测 idle 会导致连接退出。每 30 秒向目标发一个 packet,可以维持 tunnel activity。目标暂时不回复并不妨碍它完成 traffic keepalive 的职责。
7.3 聚合 target
创建 /etc/systemd/system/ustc-iwan.target:
[Unit]
Description=USTC iWAN tunnel target
Wants=ustc-iwan.service ustc-iwan-keepalive.service
After=network-online.target
[Install]
WantedBy=multi-user.target7.4 验证并启动
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
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必须同时成立:
iwan0 为 UP
172.20.23.66 dev iwan0
1.1.1.1 仍走原公网 uplink
目标可达只看到 iwan0 UP 不足以证明分流正确;必须同时检查目标 route 和公网 route。
检查目标 SSH host key:
ssh-keyscan -T 8 -p 4242 -t ed25519 172.20.23.66 |
ssh-keygen -lf -首次部署时应通过可信渠道记录 fingerprint。此后若 fingerprint 变化,应先调查原因,不能直接使用 StrictHostKeyChecking=no 绕过。
九、把公网服务器作为 SSH 跳板
在日常使用电脑的 ~/.ssh/config 中加入:
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_* 占位符,然后:
ssh dongfengyun等价的一次性命令:
ssh \
-J BASTION_USER@BASTION_PUBLIC_IP_OR_DOMAIN:BASTION_SSH_PORT \
-p 4242 \
login@172.20.23.66ProxyJump 通过现有 SSH connection 建立 direct-tcpip channel,不要求在公网服务器新增公网 listener。
十、从公网服务器向东风云建立反向 SOCKS
本节命令必须在公网服务器上执行:
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。
不要误写成:
-R 127.0.0.1:17890:127.0.0.1:7890后一种语法要求公网服务器上已经存在 127.0.0.1:7890 proxy。若没有对应 listener,SOCKS 表面上可能监听成功,实际请求会 connection refused。
在东风云验证:
ss -lnt | grep '127.0.0.1:17890'
curl -fsS \
--proxy socks5h://127.0.0.1:17890 \
https://api.ipify.org
echo返回值应为公网服务器的出口 IP。
socks5h 的 h 表示 hostname 在公网服务器一侧解析。如果使用 socks5://,某些工具会先尝试在东风云本地解析 DNS。
十一、让反向 SOCKS 长期运行
手工 SSH 只能活到 terminal、网络连接或主机 reboot 为止。ServerAliveInterval 能发现死连接,但不能自己重新启动进程。长期运行需要:
专用 SSH key
+ authorized_keys capability restriction
+ systemd Restart=always下面在公网服务器创建专用系统用户 iwan-relay,避免让 tunnel 持有日常登录用户的 private key。
11.1 创建专用 relay 用户和 key
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_egressempty passphrase 只用于 unattended service;风险由 dedicated key 与目标端限制控制。
11.2 固定东风云 host key
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 比较。匹配后安装:
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
在公网服务器:
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:
[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验证并启动:
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=30 与 ServerAliveCountMax=3 大约在 90 秒后判死并交给 systemd 重启。
11.5 验收长期 tunnel
公网服务器:
systemctl --no-pager --full status \
ustc-iwan.target \
dongfengyun-egress-tunnel.service
journalctl \
-u dongfengyun-egress-tunnel.service \
-n 100 \
--no-pager东风云:
ss -lnt | grep '127.0.0.1:17890'
curl -fsS \
--proxy socks5h://127.0.0.1:17890 \
https://api.ipify.org
echo最后应安排一次公网服务器 reboot gate,确认:
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,并集中执行分流:
应用
│ 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:
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 与配置
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创建临时配置:
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:
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 退出。
验证并启动:
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 → 公网服务器:
curl -fsS \
--proxy http://127.0.0.1:17891 \
https://api.ipify.org
echo返回值应为公网服务器出口 IP。mixed inbound 也接受 SOCKS:
curl -fsS \
--proxy socks5h://127.0.0.1:17891 \
https://api.ipify.org
echo检查 USTC direct rule:
curl -I \
--proxy http://127.0.0.1:17891 \
--max-time 30 \
https://www.ustc.edu.cn/生产验收应至少包含:
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 请求成功,再清理测试计数:
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.service12.5 配置 shell 与 fnm
当前 shell:
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,然后:
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:
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"验证:
apt-config dump | grep -i proxy
sudo apt-get updateDocker daemon 也有独立 environment scope。修改前先检查正在运行的 containers 与 live-restore,再创建 /etc/systemd/system/docker.service.d/proxy.conf:
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"然后:
sudo systemctl daemon-reload
sudo systemctl restart docker
systemctl show docker \
-p Environment \
--no-pager
docker info
docker pull hello-world:latestDocker daemon proxy 不会自动注入 containers;bridge container 内的 127.0.0.1 指向 container 自己,不能直接复用 host loopback listener。
12.7 修改 sing-box 分流
分流配置位于:
/etc/sing-box/config.jsonsing-box route rules 从上到下匹配,先命中者生效。例如新增 direct domains:
{
"domain": [
"downloads.example.org"
],
"domain_suffix": [
"example.com"
],
"action": "route",
"outbound": "direct"
}新增 direct CIDR:
{
"ip_cidr": [
"202.38.64.0/24"
],
"action": "route",
"outbound": "direct"
}final 已经是 hostdare-socks,未命中 direct rules 的目标不需要再写 proxy rule。修改时使用 fail-safe 流程:
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 失败,不要继续使用未验证配置;执行:
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:
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。
十三、日常运维
查看状态
公网服务器:
sudo systemctl --no-pager --full status \
ustc-iwan.target \
ustc-iwan.service \
ustc-iwan-keepalive.service \
dongfengyun-egress-tunnel.service东风云:
systemctl --no-pager --full status sing-box.service
ss -lnt | grep '127.0.0.1:1789[01]'查看日志
公网服务器:
sudo journalctl \
-u ustc-iwan.service \
-u ustc-iwan-keepalive.service \
-u dongfengyun-egress-tunnel.service \
--since today \
--no-pager东风云:
sudo journalctl \
-u sing-box.service \
--since today \
--no-pager不要公开发送未经检查的 OIDC/iWAN 或 proxy 原始日志;sing-box info 日志可能包含目标 domain/IP。
启停完整链路
公网服务器控制 iWAN 与 reverse tunnel:
sudo systemctl start ustc-iwan.target
sudo systemctl stop ustc-iwan.targetreverse tunnel unit 使用 PartOf=ustc-iwan.target,停止 target 时会一起停止。东风云的 sing-box service 独立运行:17890 消失后,17891 仍监听,但默认公网 outbound fail closed;direct rules 仍可工作。
东风云单独启停 policy proxy:
sudo systemctl start sing-box.service
sudo systemctl stop sing-box.service单独重启 reverse tunnel
sudo systemctl restart dongfengyun-egress-tunnel.service单独重启 sing-box
在东风云执行:
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 的:
StandardInputText=1改为 1..4 后:
sudo systemctl daemon-reload
sudo systemctl restart ustc-iwan.service主 tunnel 中断时,reverse SSH 最迟约 90 秒发现连接失效,随后自动重建。
重新完成 OIDC
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:
- 停止
ustc-iwan.target; - 下载新 release;
- 验证新 asset digest;
- 原子替换 binary;
- 更新
.sha256; - 重启并重新执行 split-route、SSH、soak 与 cleanup 验收。
更新 sing-box release
不能沿用本文 v1.13.14 的 package/binary digest 验证其他版本。更新时:
- 从 official GitHub release 获取新 asset 与 checksum;
- 在临时目录提取 binary,不直接安装 package 自带 config/unit;
- 先用新 binary 对现有
/etc/sing-box/config.json执行check; - 备份并原子替换
/usr/local/bin/sing-box; - 重启 service,复验 HTTP、SOCKS、direct rule、默认出口与 crash recovery;
- 只有全部通过后才删除 rollback binary。
十四、常见故障
| 症状 | 主要原因 | 检查与处理 |
|---|---|---|
/dev/net/tun 不存在 | VPS 未开放 TUN | 联系服务商或换 KVM VPS |
iwan0 不存在 | client 未启动或 crash | systemctl 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 timeout | iWAN route 或线路故障 | ping target,检查 route,切换线路 |
| host-key verification failed | fingerprint 未固定或变化 | 可信渠道重新核对,不能关闭校验 |
| remote port forwarding failed | 东风云 17890 已被占用 | 停止旧手工 SSH,检查 ss -lnt |
| systemd 尝试询问密码 | dedicated key 未生效 | BatchMode=yes 下应直接失败重试 |
17890 存在而 17891 不存在 | sing-box 未启动或 config check 失败 | 检查 systemctl status 与 journalctl -u sing-box.service |
sing-box 报 subscribe route updates | unit 缺少 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 |
十五、回滚与卸载
只关闭东风云公网出口
在公网服务器执行:
sudo systemctl disable --now dongfengyun-egress-tunnel.service在东风云确认 transport listener 消失:
ss -lnt | grep ':17890' || echo 'SOCKS listener removed'此时 sing-box 的 17891 仍可监听;默认公网 outbound 应 fail closed,direct rules 仍可工作。
然后从 ~login/.ssh/authorized_keys 中只删除注释为:
dongfengyun-egress@iwan-bastion的专用 key 行,不要误删其他 key。
只卸载东风云 sing-box
先删除或改回所有指向 127.0.0.1:17891 的 consumer 配置,包括登录用户 ~/.bashrc、APT proxy 和 Docker daemon drop-in;否则 service 停止后这些程序会继续连接不存在的 proxy。
然后在东风云执行:
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
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
先在公网服务器执行:
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 时,再执行:
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-owned0600; - [ ]
ustc-iwan.targetactive; - [ ]
iwan0UP; - [ ] 只有目标
/32走iwan0; - [ ] default route 不变;
- [ ] target SSH fingerprint 已通过可信渠道固定;
- [ ] ProxyJump 成功;
- [ ] reverse tunnel 使用受限 dedicated key;
- [ ]
17890只监听东风云 loopback,并经 SOCKS 查询到公网服务器出口 IP; - [ ] sing-box
v1.13.14package 与 binary SHA-256 正确; - [ ]
sing-box.service以sing-boxsystem 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:17891,NO_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。