范围与脱敏说明

本文记录一次真实的软路由代理接入过程。最终目标是:

``text 国内流量 → 软路由直

境外流量 → PassWall → Hysteria 2 → VPS 出口 ```

为避免把基础设施凭证公开到 Git 仓库,文中的公网 IP、订阅域名、认证密码、混淆密码和证书指纹均使用占位符:

VPS_IP
SUB_DOMAIN
AUTH_PASSWORD
OBFS_PASSWORD
CERT_PIN_SHA256

实际运行时必须替换为自己的值。任何包含 hysteria2:// 完整链接、认证密码、混淆密码或证书指纹的文件都不应提交到公开仓库。

环境基线

本次环境如下:

``text VPS Ubuntu 24.04 x86-64 / amd64 Hysteria 2 server UDP 20000-20100 Salamander obfuscation self-signed TLS certificat

软路由 LibWrt SNAPSHOT qualcommax/ipq60xx aarch64_cortex-a53 uname -m = aarch64 opkg PassWall 25.6.28-r1

客户端 iPhone / Shadowrocket Windows / Hiddify Mac / Shadowrocket 或其他兼容客户端 ```

软路由最终安装的 Hysteria 版本为:

Version:      v2.11.0
Platform:     linux
Architecture: arm64

最终结果

最终验收通过:

``text Hysteria 2 核心 /usr/bin/hysteria 可在 ARM64 软路由正常运

独立客户端测试 成功连接 VPS 出口 IP 等于 VPS 公网 IP

PassWall TCP 节点:Tokyo-HY2-Hop UDP 节点:与 TCP 节点相同 中国列表:直连 其余 TCP/UDP:代理

透明代理 Hysteria 子进程正常存活 PassWall 防火墙规则正常加载 国内网站直连 境外网站通过 VPS ```

问题的最终根因不是 Hysteria 服务端,也不是 UDP 端口跳跃,而是 PassWall 保存的证书指纹与 VPS 当前叶子证书不一致:

FATAL failed to initialize client
connect error: INTERNAL_ERROR (local):
no certificate matches the pinned hash

同步真实证书 SHA-256 指纹后,代理恢复正常。

架构选择

WireGuard 作为备用

最初使用原生 WireGuard。它的吞吐和延迟表现较好,但固定 UDP 端口在当前网络路径下偶发不可用。

WireGuard 没有被删除,而是保留为应急备用。

Tailscale Exit Node 作为第二备用

Tailscale Exit Node 能稳定工作,但相较原生 WireGuard:

延迟更高
抖动更明显
吞吐略低
链路多一层控制面与中继可能性

因此保留为低维护备用,不作为主要出口。

Hysteria 2 作为主线路

最终选择 Hysteria 2,原因是:

基于 QUIC / UDP
支持端口跳跃
支持 Salamander 混淆
移动网络兼容性较好
同一个可执行文件包含服务端与客户端
可由 PassWall 直接接管透明代理

本次采用的端口范围:

20000-20100/udp

客户端每 30 秒切换端口。

VPS 端配置模板

下面是可复现的通用模板。真实环境中的密码必须使用随机值,并通过安全渠道保存。

安装 Hysteria 2 服务端

官方安装脚本会安装二进制文件和 systemd 服务:

bash <(curl -fsSL https://get.hy2.sh/)

确认:

hysteria version
systemctl status hysteria-server.service

生成自签名证书

本文使用 hy2.local 作为 SNI:

``bash install -d -m 0755 /etc/hysteri

openssl req \ -x509 \ -newkey rsa:2048 \ -nodes \ -sha256 \ -days 3650 \ -keyout /etc/hysteria/server.key \ -out /etc/hysteria/server.crt \ -subj '/CN=hy2.local' \ -addext 'subjectAltName=DNS:hy2.local'

chmod 0600 /etc/hysteria/server.key chmod 0644 /etc/hysteria/server.crt ```

查看叶子证书 SHA-256 指纹:

openssl x509 \
  -noout \
  -fingerprint \
  -sha256 \
  -in /etc/hysteria/server.crt

输出格式类似:

sha256 Fingerprint=AA:BB:CC:...

服务端配置

/etc/hysteria/config.yaml

``yaml listen: :20000-2010

tls: cert: /etc/hysteria/server.crt key: /etc/hysteria/server.key

auth: type: password password: AUTH_PASSWORD

obfs: type: salamander salamander: password: OBFS_PASSWORD ```

重启并查看日志:

``bash systemctl enable --now hysteria-server.service systemctl restart hysteria-server.servic

systemctl status hysteria-server.service journalctl --no-pager -e -u hysteria-server.service ```

云安全组必须允许:

UDP 20000-20100

若系统本地启用了 UFW,可额外放行:

ufw allow 20000:20100/udp

不要在没有确认当前防火墙实现的情况下混用 UFW、iptables 和 nftables。

先做独立客户端验证

在接入 PassWall 之前,应先证明以下链路成立:

软路由 Hysteria 二进制
→ Hysteria 服务端
→ VPS 出口

如果独立客户端都无法连接,就不应继续调试 PassWall、DNS 或透明代理。

通用客户端配置:

``yaml server: "VPS_IP:20000-20100" auth: "AUTH_PASSWORD

tls: sni: "hy2.local" insecure: true pinSHA256: "CERT_PIN_SHA256"

obfs: type: salamander salamander: password: "OBFS_PASSWORD"

transport: type: udp udp: hopInterval: 30s

http: listen: 127.0.0.1:18080 ```

启动:

/usr/bin/hysteria client -c /root/hy2-client.yaml

另开终端测试:

``sh http_proxy="http://127.0.0.1:18080" \ wget -qO- http://api.ipify.or

echo ```

成功时输出 VPS 公网 IP。

用 VPS 中转 ARM64 核心

软路由无法稳定访问软件源和 GitHub,因此不再强行让 opkg 下载 Hysteria,而是:

VPS 下载官方 ARM64 二进制
→ 软路由通过 SSH 从 VPS 读取
→ 在软路由本地安装

最终正确版脚本

下面脚本在软路由上运行。

它不会让 amd64 VPS 执行 ARM64 文件,只在 VPS 上下载和计算哈希,然后把文件传给软路由。

``sh cat >/tmp/install-hysteria-from-vps.sh <<'SCRIPT' #!/bin/sh set -

VPS_IP="VPS_IP" REMOTE_FILE="/root/hysteria-linux-arm64" LOCAL_TMP="/tmp/hysteria-linux-arm64" LOCAL_BIN="/usr/bin/hysteria"

fail() { echo "失败:$*" >&2 return 1 }

echo "[1/5] 检查软路由架构" ARCH="$(uname -m)" echo "uname -m: $ARCH"

case "$ARCH" in aarch64|arm64) ;; *) fail "当前设备不是 ARM64,不能安装 hysteria-linux-arm64" exit 1 ;; esac

echo echo "[2/5] 让 VPS 下载官方 ARM64 文件" echo "需要输入 VPS root 密码"

ssh "root@${VPS_IP}" " set -e curl -fL \ https://download.hysteria.network/app/latest/hysteria-linux-arm64 \ -o '${REMOTE_FILE}' chmod 0755 '${REMOTE_FILE}' sha256sum '${REMOTE_FILE}' " || { fail "VPS 下载失败" exit 1 }

echo echo "[3/5] 从 VPS 传到软路由"

ssh "root@${VPS_IP}" \ "cat '${REMOTE_FILE}'" \ > "$LOCAL_TMP" || { rm -f "$LOCAL_TMP" fail "传输失败" exit 1 }

chmod 0755 "$LOCAL_TMP"

echo echo "[4/5] 在 ARM64 软路由执行版本检查"

"$LOCAL_TMP" version || { rm -f "$LOCAL_TMP" fail "二进制无法在当前软路由执行" exit 1 }

echo echo "[5/5] 安装"

install -m 0755 "$LOCAL_TMP" "$LOCAL_BIN" || { fail "安装失败" exit 1 }

rm -f "$LOCAL_TMP"

echo echo "安装完成" ls -l "$LOCAL_BIN" "$LOCAL_BIN" version SCRIPT

chmod 0700 /tmp/install-hysteria-from-vps.sh sh /tmp/install-hysteria-from-vps.sh ```

为什么不用随机 IPK

在 Snapshot 固件上安装来源不明或版本不匹配的 IPK,可能造成:

ABI 不兼容
依赖不完整
包数据库污染
LuCI 与核心版本错配
系统升级困难

单文件 Hysteria 官方 ARM64 二进制更容易验证、回滚和替换。

PassWall 节点配置

本次 PassWall 版本为:

25.6.28-r1

节点字段:

``text 节点备注 Tokyo-HY2-Ho

类型 Hysteria2

协议 UDP

地址 VPS_IP

端口 20000

端口跳跃范围 20001-20100

端口跳跃时间 30s

认证密码 AUTH_PASSWORD

混淆密码 OBFS_PASSWORD

域名 / SNI hy2.local

允许不安全连接 开启

PinSHA256 CERT_PIN_SHA256

快速打开 关闭

延迟启动 关闭

禁用 MTU 检测 关闭

最大上行 / 最大下行 留空

QUIC 接收窗口 保持默认 ```

这里的主端口与跳跃范围会合并为:

VPS_IP:20000,20001-20100

基本设置

``text 主开关 开

TCP 节点 Tokyo-HY2-Hop

UDP 节点 与 TCP 节点相同

中国列表 直连

TCP 代理模式 代理

UDP 代理模式 代理

直连列表 开启

代理列表 开启

GFW 列表 开启

IPv6 代理 初期关闭 ```

DNS 在首次接入时尽量保持 PassWall 默认流程,不要同时修改系统 DHCP、dnsmasq、WAN DNS 和客户端手动 DNS,否则会增加变量数量。

让 PassWall 找到手动安装的核心

手动复制 /usr/bin/hysteria 后,PassWall 不一定自动识别路径。

对 PassWall 25.6.28,可明确设置:

``sh uci set passwall.@global_app[0].hysteria_file='/usr/bin/hysteria' uci commit passwal

/etc/init.d/passwall restart ```

验证:

uci -q get passwall.@global_app[0].hysteria_file

应输出:

/usr/bin/hysteria

PassWall 正确的诊断入口

不要只查 `logread`

本次最初执行:

logread | grep -iE 'passwall|hysteria|hy2'

只看到 ucitrack,没有实际启动错误。

PassWall 25.6.28 的主要日志在:

/tmp/log/passwall.log

应查看:

tail -n 150 /tmp/log/passwall.log

进程检查

ps w | grep '[h]ysteria' || echo "没有 Hysteria 进程"

监听端口不要猜

最初误以为 SOCKS 端口一定是 1070,但本次透明代理实际使用:

TCP REDIRECT: 1041
UDP TPROXY:   1041

应以 PassWall 日志为准:

grep -E '监听端口|REDIRECT|TPROXY' /tmp/log/passwall.log

直接运行 PassWall 生成的配置

当 PassWall 显示“运行完成”,但 Hysteria 进程不存在时,应直接运行它生成的 JSON,以取得核心的真实错误:

``sh CFG="

for F in $(find /tmp/etc/passwall -type f -name '*.json' 2>/dev/null); do if grep -q '"server"' "$F" 2>/dev/null && grep -q '"auth"' "$F" 2>/dev/null; then CFG="$F" break fi done

echo "配置:${CFG:-未找到}"

if [ -n "$CFG" ]; then /usr/bin/hysteria \ -c "$CFG" \ client \ >/tmp/hy2-passwall-direct.log 2>&1 &

PID=$! sleep 5

if kill -0 "$PID" 2>/dev/null; then echo "进程正常存活" kill "$PID" wait "$PID" 2>/dev/null else echo "启动后立即退出" fi

sed -n '1,160p' /tmp/hy2-passwall-direct.log fi ```

本次就是通过这一步得到关键错误:

no certificate matches the pinned hash

一次性同步真实证书指纹

证书指纹必须来自 VPS 当前实际使用的叶子证书,而不是旧 URI、旧截图、旧订阅或旧参数文件。

下面脚本:

备份 PassWall 配置
从 VPS 读取当前证书
计算 SHA-256 指纹
更新当前 Hysteria2 节点
固定核心路径
重启 PassWall
自动检查进程与出口

脚本在软路由运行。

``sh cat >/tmp/fix-passwall-hy2-pin.sh <<'SCRIPT' #!/bin/s

VPS_IP="VPS_IP" CERT="/etc/hysteria/server.crt" CORE="/usr/bin/hysteria"

fail() { echo echo "修复失败:$*" echo "未修改 WAN、LAN、无线或 DHCP。" exit 1 }

echo "[1/6] 检查节点和核心"

NODE="$(uci -q get passwall.@global[0].tcp_node)" [ -n "$NODE" ] || fail "未找到 PassWall TCP 节点"

NODE_TYPE="$(uci -q get "passwall.${NODE}.type")" [ "$NODE_TYPE" = "Hysteria2" ] || fail "当前 TCP 节点不是 Hysteria2:${NODE_TYPE:-未知}"

[ -x "$CORE" ] || fail "$CORE 不存在或没有执行权限"

echo "节点:$NODE" echo "核心:$CORE"

echo echo "[2/6] 备份 PassWall 配置"

TS="$(date +%Y%m%d-%H%M%S)" BACKUP="/root/passwall-before-pin-fix-${TS}.conf"

cp -a /etc/config/passwall "$BACKUP" || fail "无法备份 /etc/config/passwall"

echo "备份:$BACKUP"

echo echo "[3/6] 从 VPS 获取当前叶子证书指纹" echo "需要输入一次 VPS root 密码"

RAW="$( ssh -o ConnectTimeout=15 "root@${VPS_IP}" \ "openssl x509 -noout -fingerprint -sha256 -in '${CERT}'" )" SSH_RC=$?

[ "$SSH_RC" -eq 0 ] || fail "无法从 VPS 获取证书指纹"

PIN="$( printf '%s\n' "$RAW" | sed -n 's/^[^=]*=//p' | head -n 1 | tr -d '\r\n ' )"

HEX="$(printf '%s' "$PIN" | tr -d ':')"

[ "${#HEX}" -eq 64 ] || fail "证书指纹长度异常"

if printf '%s' "$HEX" | grep -q '[^0-9A-Fa-f]'; then fail "证书指纹格式异常" fi

echo "指纹格式验证通过,不在终端显示。"

echo echo "[4/6] 更新 PassWall 节点"

uci set "passwall.${NODE}.hysteria2_tls_pinSHA256=${PIN}" uci set "passwall.${NODE}.tls_serverName=hy2.local" uci set "passwall.${NODE}.tls_allowInsecure=1"

uci set "passwall.${NODE}.address=${VPS_IP}" uci set "passwall.${NODE}.port=20000" uci set "passwall.${NODE}.protocol=udp" uci set "passwall.${NODE}.hysteria2_hop=20001-20100" uci set "passwall.${NODE}.hysteria2_hop_interval=30s"

uci set "passwall.@global_app[0].hysteria_file=${CORE}"

uci commit passwall || fail "保存 PassWall 配置失败"

echo echo "[5/6] 重启 PassWall"

rm -f /tmp/log/passwall.log /etc/init.d/passwall restart sleep 12

echo echo "[6/6] 验收"

if ps w | grep -q '[h]ysteria'; then echo "Hysteria 进程:已启动" else echo "Hysteria 进程:未启动" echo tail -n 120 /tmp/log/passwall.log 2>/dev/null fail "Hysteria 子进程未存活" fi

OUT="$( wget -qO- --timeout=20 https://api.ipify.org 2>/dev/null )"

if [ "$OUT" = "$VPS_IP" ]; then echo echo "修复成功" echo "Hysteria 进程:正常" echo "代理出口:$OUT" else echo echo "出口测试返回:${OUT:-空}" echo tail -n 120 /tmp/log/passwall.log 2>/dev/null fail "出口验收未通过" fi SCRIPT

chmod 0700 /tmp/fix-passwall-hy2-pin.sh sh /tmp/fix-passwall-hy2-pin.sh ```

PassWall 版本差异

本文针对:

luci-app-passwall 25.6.28-r1

该版本使用:

hysteria2_tls_pinSHA256
hysteria2_hop
hysteria2_hop_interval
hysteria_file

新版本 PassWall 的内部字段可能不同。升级后应先查看对应版本源码或 LuCI 保存结果,不要直接复制旧版 UCI 字段。

安全回滚

PassWall 配置失败时,不应立刻恢复出厂,也不应重启整个网络服务。

先关闭 PassWall:

/etc/init.d/passwall stop
/etc/init.d/firewall restart
/etc/init.d/dnsmasq restart

这组命令的目标是:

停止代理进程
清除代理防火墙状态
恢复 DNS 服务
保留 WAN / LAN / Wi-Fi 配置

不要优先执行:

/etc/init.d/network restart

网络整体重启可能断开当前管理连接,使排障难度进一步增加。

若要恢复 PassWall 配置:

``sh cp -a \ /root/passwall-before-pin-fix-YYYYMMDD-HHMMSS.conf \ /etc/config/passwal

/etc/init.d/passwall restart ```

本次踩过的坑

坑 1:在 amd64 VPS 上执行 ARM64 二进制

错误代码:

/root/hysteria-linux-arm64 version

VPS 是 amd64,因此返回:

cannot execute binary file: Exec format error

正确思路是:

VPS 只负责下载和中转
ARM64 文件只在 ARM64 软路由执行

检查架构:

uname -m
file /path/to/binary

坑 2:`sed` 替换字符串中的 `&`

错误修复命令试图把一行改成:

sha256sum /root/hysteria-linux-arm64 >&2

但在 sed replacement 中,& 表示“整个匹配内容”。未转义时,最终生成了错误重定向,出现:

22: Bad file descriptor

安全做法:

sed -i '/需要删除的完整模式/d' file

或在 replacement 中转义:

\&

自动修改 shell 脚本后,必须先查看目标行再执行:

sed -n '1,80p' script.sh

坑 3:BusyBox 命令能力与桌面 Linux 不同

软路由上的 BusyBox 没有:

timeout

nc 也只支持:

nc IP PORT

不支持:

nc -w 10 IP PORT

因此不能假设 OpenWrt/LibWrt 具备 GNU coreutils 和完整版 netcat。

运行前先检查:

command -v timeout
nc --help
busybox

坑 4:手机热点不等于自动共享代理

iPhone 连接 Shadowrocket 后开启个人热点,并不代表热点客户端自动继承 Shadowrocket 隧道。

还需要 Shadowrocket 的显式“代理共享 / 局域网共享”。

本次软路由能 Ping 通:

172.20.10.1

但访问共享端口返回:

Connection refused

这说明:

热点链路正常
手机网关正常
目标 TCP 端口没有监听

最终放弃手机反向代理,改用 VPS 中转,路径更短、变量更少。

坑 5:恢复出厂会清空配置,但不一定符合预期

软路由恢复出厂后:

自定义 WAN / LAN 配置可能丢失
Wi-Fi SSID 与密码恢复默认
后装包可能被清除或禁用
Web 终端插件可能消失
SSH 主机密钥可能变化

恢复后应先建立最小基线:

设置管理密码
确认 WAN 获取地址
确认 LAN 管理可用
确认国内网站可访问
再安装代理核心

不要在基础网络未验收前叠加 PassWall、DNS、WireGuard 或手机热点接口。

坑 6:地址输入错误

节点地址曾被手动输入为错误 IP。

排障时先检查最基础字段:

地址
端口
认证
SNI
混淆
证书指纹

不要直接跳到复杂 DNS 或 nftables。

坑 7:PassWall 显示“运行完成”不等于核心正常

PassWall 主日志显示:

防火墙规则加载完成
运行完成

但 Hysteria 子进程实际上已经退出。

因此必须同时检查:

ps w | grep '[h]ysteria'
tail -n 120 /tmp/log/passwall.log

必要时直接运行生成的 JSON。

坑 8:查错日志位置

logread 没有显示核心错误,不代表没有错误。

本次真正有用的日志是:

/tmp/log/passwall.log
/tmp/hy2-passwall-direct.log

坑 9:误把 1070 当成固定监听端口

1070 是 PassWall 某些 SOCKS 场景的默认端口,不代表透明代理一定监听它。

本次实际是:

1041

应以当前日志和配置为准。

坑 10:手动安装核心后没有绑定 PassWall 路径

独立运行成功,但 PassWall 没有启动核心,原因之一是:

passwall.@global_app[0].hysteria_file

未指向 /usr/bin/hysteria

手动安装二进制后,应用层仍需要知道它的位置。

坑 11:证书指纹过期或来源错误

旧 URI 或旧参数文件中的指纹,与 VPS 当前证书不同。

Hysteria 2.6.4 之后,pinSHA256 只匹配叶子证书指纹。证书更新后必须同步新指纹。

正确来源:

openssl x509 \
  -noout \
  -fingerprint \
  -sha256 \
  -in /etc/hysteria/server.crt

不要从截图手抄,不要使用 CA 指纹,不要沿用已经更换证书前的值。

坑 12:粘贴脚本中的 `exit` 会关闭当前 SSH

在交互式 SSH 会话中直接粘贴:

[ condition ] || {
  echo "错误"
  exit 1
}

会退出当前 shell,并关闭 SSH。

长脚本应写入文件后执行:

``sh cat >/tmp/task.sh <<'SCRIPT' #!/bin/sh ... SCRIP

sh /tmp/task.sh ```

这样 exit 只退出子脚本,不会直接关闭父级 SSH 会话。

坑 13:截图会泄露节点密钥

PassWall 节点编辑页可能直接显示:

混淆密码
认证密码
证书指纹

公开截图前必须打码。完整 hysteria2:// 链接应视为凭证。

最终验收清单

软路由基础网络

ip -4 route
ping -c 3 223.5.5.5
nslookup www.baidu.com 127.0.0.1

核心

ls -l /usr/bin/hysteria
/usr/bin/hysteria version

PassWall 路径

uci -q get passwall.@global_app[0].hysteria_file

进程

ps w | grep '[h]ysteria'

日志

tail -n 150 /tmp/log/passwall.log

境外出口

wget -qO- --timeout=20 https://api.ipify.org
echo

应返回 VPS 公网 IP。

国内直连

从接入软路由的客户端测试:

百度正常
国内 DNS 正常
国内延迟不经过 VPS

境外代理

Google / YouTube 正常
出口为 VPS
客户端自身的 VPN / Hiddify / Shadowrocket 已关闭

客户端自身代理必须关闭,否则无法判断流量究竟经过软路由还是本机代理。

运维建议

证书更新后同步指纹

每次更换 /etc/hysteria/server.crt 后执行一次指纹同步脚本。

更新核心仍通过 VPS 中转

在软路由无法稳定访问官方源时,重复使用:

VPS 下载
→ SSH 传输
→ 本机 version 验证
→ install 覆盖
→ 重启 PassWall

覆盖前备份:

cp -a \
  /usr/bin/hysteria \
  "/root/hysteria.backup.$(date +%Y%m%d-%H%M%S)"

保留备用链路

建议保留:

主线路:Hysteria 2
备用一:WireGuard
备用二:Tailscale Exit Node

但不要同时让多套透明代理接管默认路由和 DNS。

不执行全量 `opkg upgrade`

Snapshot 固件不应无差别执行:

opkg upgrade

包和内核模块版本错配可能导致系统不可启动或网络模块失效。

先验证一层,再进入下一层

推荐顺序:

WAN / LAN
→ VPS 服务端
→ 软路由独立 Hysteria 客户端
→ PassWall 核心路径
→ PassWall 节点
→ 透明代理
→ DNS 分流
→ 客户端验收

每一层通过后再继续。不要一次修改网络、无线、DHCP、DNS、代理和防火墙。

参考资料

  • Hysteria 2 安装:<https://v2.hysteria.network/docs/getting-started/Installation/>
  • Hysteria 2 客户端与证书固定:<https://v2.hysteria.network/docs/getting-started/Client/>
  • Hysteria 2 完整客户端配置:<https://v2.hysteria.network/docs/advanced/Full-Client-Config/>
  • Hysteria 2 端口跳跃:<https://v2.hysteria.network/docs/advanced/Port-Hopping/>
  • Hysteria 2 Changelog:<https://v2.hysteria.network/docs/Changelog/>
  • PassWall 25.6.28 Hysteria2 配置生成器:<https://github.com/Openwrt-Passwall/openwrt-passwall/blob/25.6.28-1/luci-app-passwall/luasrc/passwall/util_hysteria2.lua>
  • PassWall 25.6.28 Hysteria2 LuCI 字段:<https://github.com/Openwrt-Passwall/openwrt-passwall/blob/25.6.28-1/luci-app-passwall/luasrc/model/cbi/passwall/client/type/hysteria2.lua>