Files
drouter/_dev/smoke-docker-108.sh
liuzhuohua 890b2b3fa6 test(smoke): 修容器冒烟的 13 条假红 —— 判据吃注释 / 路径拼错 / 结构位置错
冒烟脚本 1.0.7 就在报红,本轮首次真跑才看清:13 个失败里
**没有一条是真产品缺陷**,全是判据自身的问题。三类:

## 一、路径拼错:`/opt/douter`(少个 r)

第 13 节 4 条断言全往这个不存在的路径 grep。grep 会往 stderr 打
"No such file or directory",而 `2>&1` 把它收进了输出,于是:
- 「期望 0 次」那两条→ 错一次 == 期望值,**恒绿**
- 「期望包含 xxx」那两条 → 拿到的是报错不是内容,**恒假**

它们守的3 个 1.0.7 真缺陷于是「看起来早就修好了」。
修法:先加一条「helper 路径拼写正确」的前置断言,再谈内容。

## 二、`ck` 配 `grep -c`:数字里永远不含代码片段

第 12/13/14 节共 7 处。`grep -c` 只吐一个数字(如 `1`),
拿它去 `grep -qE 'def _vpn_env'` 匹配,**恒为假**。
而 `ckc` 只能问「恰好等于几」,「出现过没有」这个中间档没有函数。
新增 `ckg`:问「命中数 ≥1」,且非数字输出(grep 报错)判失败 ——
免得「查错了文件」又被读成「代码不在」。

## 三、判据在注释里命中

修好路径后暴露出来的:
- `sensitive=(d == '/etc/drouter')` 命中在 **11669 行的注释**里
  (那段正是在解释这个 bug 的来历)
- `'raw': d` 一处在 **3975 行注释**里,一处是**打印服务的 raw**
  (`{'cups':..., 'raw': dict(PRINT_DEFAULTS['raw'])}`),与 VPN 无关

改法:反向断言先 `grep -v` 剔注释;raw 那条用 awk 限在
`_vpn_status` 函数体内。这正是本轮在 t-107 里反复抓的同一个根因,
**同一个错在冒烟脚本里又犯了一遍**。

## 四、结构位置错:断言写在 trap 之前

「netavark 表已删除」这条断言原本在函数体末尾,而 cleanup 是
`trap cleanup EXIT` 触发的 —— 打印时容器还活着,表刚被 podman 重建,
**结构性必红**。断言移进 cleanup() 内。

连带发现:trap 在 `exit $fail` **之后**才跑,那时 $fail 已求值传给内核,
在 trap 里 `fail=$((fail+1))` 毫无作用。改用 RC_AT_EXIT 传递 + trap 里自己 exit。

## 顺带修正一条写错的需求

第 12 节原本断言三个守护「有不依赖 init 的拉起路径(Popen)」。
**这三个是oneshot 型 timer 守护**(timer 定时拉起、跑完即退),
压根不需要脱离 init 常驻;实测三个文件里Popen 均为 0。
那条断言每版必红 3 条 —— 于是有人会去改期望值「修绿」,那是把判据
迁就实现。改为验它自己的形态:有 __main__ 入口 + Popen=0。

冒烟:36/13 → **53/0**,SMOKE_EXIT=0,netavark 表确认已删。
2026-10-03 12:35:45 +08:00

410 lines
22 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# 容器成品镜像冒烟(第二段):登录 + Web 终端建会话 + 接口抽查
# 1.0.7 新增备份 / 告警 / 配额三个守护,容器形态同样要验:
# ① 容器里没有 systemd,三个新守护仍能被拉起(_shelld_spawn 同款回退路径)
# ② /dev/ptmx 存在,PTY 能开(信号量上限后新建会话要回 BUSY 而不是挂死)
# ③ 自签证书的 SAN 取自本机地址,不得再出现作者写死的内网 IP
# (1.0.4 修复;过去直接硬编码 192.168.7.3,任何机器上签出来都带着它)
# ④ sync-isp-dns.sh(1.0.6 新增,PPPoE 上游 DNS 同步)必须在镜像里
# ⑤ rescue token / SNMP 净化(1.0.6 修)都在 backend,要能被 import
# ⑥ 1.0.7 修的 7 个缺陷:备份敏感排除、VPN 私钥不外泄、ip_network 3.13 判定
# ⑦ 1.0.8 新增:VPN 四态诊断(_vpn_env)+ /etc/modules-load.d/ 开机自启,
# 容器里 wireguard 模块多半加载不了 → 这正好是「unsupported」那一档,
# 验的是**别误报成别的态**、以及 fix 按钮该消失时消失
# 用 --network none:不碰宿主网络,只在容器内自测 127.0.0.1。
set -u
TAR=/tmp/drouter-108-final.tar
NAME=drouter-smoke108
IMG=drouter:1.0.8
BASE=https://127.0.0.1:8443
PODMAN="podman"
pass=0; fail=0
ck() { # ck <描述> <期望正则> <实际输出>
local desc="$1" want="$2" got="$3"
if grep -qE -- "$want" <<<"$got"; then
echo " ✔ $desc"; pass=$((pass+1))
else
echo " ✘ $desc"
echo " 期望包含: $want"
echo " 实际 : $(head -c 200 <<<"$got")"
fail=$((fail+1))
fi
}
ckc() { # ckc <描述> <期望命中次数> <实际命中次数>
# 专用于「grep -c 的数字」型断言。反向断言(期望 0 次)用这个,
# 不要拿数字去 ck 里匹配 —— 那是在问「数字里含不含这段代码」,
# 恒为假,1.0.6 首次冒烟的 5 个红全是栽在这里。
local desc="$1" want="$2" got="$3"
if [ "$got" = "$want" ]; then
echo " ✔ $desc($got)"; pass=$((pass+1))
else
echo " ✘ $desc"
echo " 期望命中: $want 次,实际: $got"
fail=$((fail+1))
fi
}
ckg() { # ckg <描述> <期望出现的关键字> <grep -c 的输出>
# 问的是「命中数 ≥1 吗」而不是「输出里含不含这个关键字」。
# ⛔ 别用 ck 配 `grep -c` —— grep -c 只吐一个数字,数字里永远不会
# 含代码片段,所以那种写法**恒为假**。ckc 又只能问「恰好等于几」,
# 「出现过没有」这个中间档没有对应函数,于是 1.0.7 冒烟里 4 条断言
# 全栽在这儿(其中 3 条还是恒绿:路径拼错时 grep 往 stderr 打的
# 是错误信息,`2>&1` 把它收了进来,「期望 0 次」照样满足)。
# 顺带:非数字输出(grep 的报错)也判失败,
# 免得「查错了文件」又被读成「代码不在」。
local desc="$1" key="$2" got="$3"
if [[ "$got" =~ ^[0-9]+$ ]] && [ "$got" -ge 1 ]; then
echo " ✔ $desc(命中 $got 次)"; pass=$((pass+1))
else
echo " ✘ $desc"
echo " 期望命中 ≥1 次,关键词: $key"
echo " 实际 : $(head -c 200 <<<"$got")"
fail=$((fail+1))
fi
}
cleanup() {
$PODMAN rm -f "$NAME" >/dev/null 2>&1 && echo " 容器已删除"
# podman 用 netavark 驱动,跑过一次容器就会在宿主留下 `table inet netavark`
# (空隔离链 + 只匹配 meta mark 0x2000 的 masquerade)。无容器时它匹配不到
# 任何流量,但既然是我们引入的,就顺手还回去,保持宿主规则集与跑之前一致。
if nft list table inet netavark >/dev/null 2>&1; then
nft list table inet netavark > /root/netavark-table-backup.nft 2>&1
nft delete table inet netavark 2>/dev/null \
&& echo " 已移除 podman 的 netavark 表(备份 /root/netavark-table-backup.nft)"
fi
# 基线还原的**断言必须写在 cleanup 里面**。
# 之前它被放在函数体末尾、cleanup 之前 —— 而 cleanup 是 trap EXIT 触发的,
# 那时容器还活着、netavark 表当然还在(podman 每次 run 都会重建),
# 于是这条断言**结构性必红**。顺序错了,判据再对也没用。
#
#⛔ 但 trap 在 `exit $fail` **之后**才跑,此时 $fail 已经求值传给内核了,
# 在这里 `fail=$((fail+1))` 改计数毫无作用(1.0.8 首次就踩了)。
# 想让「基线未还原」真的让脚本红,只能在 trap 里自己 exit。
if nft list table inet netavark >/dev/null 2>&1; then
echo " ✘ podman 的 netavark 表未能删除(基线未还原)"
BASELINE_BAD=1
else
echo " ✔ podman 的 netavark 表已删除(基线还原)"
fi
if [ "${BASELINE_BAD:-0}" = 1 ]; then
exit $(( ${RC_AT_EXIT:-0} + 1 ))
fi
exit ${RC_AT_EXIT:-0}
}
trap cleanup EXIT
$PODMAN rm -f "$NAME" >/dev/null 2>&1
echo "=== 加载镜像 ==="
$PODMAN load -i "$TAR" 2>&1 | tail -2
echo "=== 启动容器(--network none)==="
$PODMAN run -d --name "$NAME" --network none "$IMG" >/dev/null 2>&1
for i in $(seq 1 20); do
$PODMAN exec "$NAME" curl -sk --max-time 5 "$BASE/api/health" >/dev/null 2>&1 && break
sleep 2
done
echo " 已就绪"
echo
echo "=== 1. 登录 ==="
RAW=$($PODMAN exec "$NAME" curl -sk --max-time 8 -X POST "$BASE/api/login" \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"admin123"}' 2>/dev/null)
TOKEN=$(sed -n 's/.*"token": *"\([^"]*\)".*/\1/p' <<<"$RAW")
echo " token 长度 = ${#TOKEN}"
ck "登录返回 token" '"ok": *true' "$RAW"
echo
echo "=== 2. 容器内没有 systemd(前提确认)==="
NSD=$($PODMAN exec "$NAME" sh -c 'command -v systemctl >/dev/null 2>&1 && echo 有 || echo 无; ls /run/systemd/system 2>/dev/null | head -1' 2>&1)
echo " $NSD"
echo
echo "=== 3. /dev/ptmx 与 PTY ==="
PTMX=$($PODMAN exec "$NAME" sh -c 'ls -l /dev/ptmx 2>&1; echo "pts=$(ls /dev/pts 2>/dev/null | wc -l)"' 2>&1)
echo " $PTMX"
ck "/dev/ptmx 存在" "/dev/ptmx" "$PTMX"
echo
echo "=== 4. Web 终端建会话(关键:容器内守护能否拉起)==="
CONN=$($PODMAN exec "$NAME" curl -sk --max-time 25 -X POST "$BASE/api/webshell/connect" \
-H "X-Token: $TOKEN" -H 'Content-Type: application/json' \
-d '{"user":"root","cols":100,"rows":30}' 2>&1)
echo " $(head -c 300 <<<"$CONN")"
ck "终端建立成功(ok=true)" '"ok": *true' "$CONN"
ck "没有 NO_SHELLD" '' "$(grep -o NO_SHELLD <<<"$CONN" || echo '')"
SID=$(sed -n 's/.*"sid": *"\([^"]*\)".*/\1/p' <<<"$CONN")
echo " sid = $SID"
echo
echo "=== 5. 终端守护进程确实在跑 ==="
PS=$($PODMAN exec "$NAME" sh -c 'pgrep -af shelld 2>&1 | head -3' 2>&1)
echo " $PS"
ck "drouter-shelld 进程存在" "shelld" "$PS"
SOCK=$($PODMAN exec "$NAME" sh -c 'ls -l /run/drouter/shell.sock 2>&1' 2>&1)
echo " $SOCK"
ck "shell.sock 已创建" "shell.sock" "$SOCK"
echo
echo "=== 6. 真正跑一条命令,读回显 ==="
if [ -n "$SID" ]; then
$PODMAN exec "$NAME" curl -sk --max-time 10 -X POST "$BASE/api/webshell/write" \
-H "X-Token: $TOKEN" -H 'Content-Type: application/json' \
-d "{\"op\":\"write\",\"sid\":\"$SID\",\"data\":\"echo DROUTER-$RANDOM-OK\\n\"}" >/dev/null 2>&1
OUT=$($PODMAN exec "$NAME" curl -sk --max-time 15 -X POST "$BASE/api/webshell/read" \
-H "X-Token: $TOKEN" -H 'Content-Type: application/json' \
-d "{\"op\":\"read\",\"sid\":\"$SID\",\"wait\":2}" 2>&1)
echo " $(head -c 300 <<<"$OUT")"
ck "读回显成功" '"ok": *true' "$OUT"
ck "回显含命令输出" 'DROUTER-' "$OUT"
echo
echo "=== 7. 断开会话 ==="
DISC=$($PODMAN exec "$NAME" curl -sk --max-time 10 -X POST "$BASE/api/webshell/disconnect" \
-H "X-Token: $TOKEN" -H 'Content-Type: application/json' \
-d "{\"op\":\"disconnect\",\"sid\":\"$SID\"}" 2>&1)
echo " $(head -c 200 <<<"$DISC")"
ck "断开成功" '"ok": *true' "$DISC"
fi
echo
echo "=== 8. 接口抽查 ==="
for ep in sysinfo ifaces metrics; do
printf ' %-9s ' "$ep"
R=$($PODMAN exec "$NAME" curl -sk --max-time 10 -H "X-Token: $TOKEN" "$BASE/api/$ep" 2>&1)
grep -qE '"ok": *true' <<<"$R" && { echo "ok ✔"; pass=$((pass+1)); } || { echo "✘ $(head -c 120 <<<"$R")"; fail=$((fail+1)); }
done
echo
echo "=== 9. 自签证书 SAN(不得再写死作者内网 IP)==="
CERT=$($PODMAN exec "$NAME" sh -c 'C=$(ls /opt/drouter/certs/server.crt 2>/dev/null); [ -n "$C" ] || exit 1; openssl x509 -in "$C" -noout -text 2>/dev/null' 2>&1)
if [ -n "$CERT" ]; then
echo " SAN: $(grep -A1 -i 'Subject Alternative Name' <<<"$CERT" | tail -1 | head -c 200)"
ck "证书 SAN 含 loopback 127.0.0.1" '127\.0\.0\.1' "$CERT"
# 反向断言:写死的作者内网 IP 绝不能出现(ck 只能做「包含」判断,这里手写)
if grep -q '192\.168\.7\.3' <<<"$CERT"; then
echo " ✘ 证书里仍写着硬编码的 192.168.7.3"; fail=$((fail+1))
else
echo " ✔ 证书 SAN 不含写死的 192.168.7.3"; pass=$((pass+1))
fi
else
echo " ✘ 读不到 /opt/drouter/certs/server.crt($CERT)"; fail=$((fail+1))
fi
echo
echo "=== 10. sync-isp-dns.sh 在镜像里且可执行(1.0.6 新增)==="
# 1.0.6 修 PPPoE 断网:render_ppp 写 ip-up-script 指向这个脚本,
# 缺了它 dns_mode=isp/both 时 dnsmasq 上游全空 —— 而 PPPoE 页看不出异常。
# 路径必须是 scripts/:deb / 镜像 / deploy.sh 三条路都装到 $OPT/scripts/。
SYNC=/opt/drouter/scripts/sync-isp-dns.sh
if $PODMAN exec "$NAME" test -x "$SYNC"; then
echo " ✔ 脚本在且可执行"; pass=$((pass+1))
else
echo " ✘ 缺失或不可执行:$($PODMAN exec "$NAME" ls -l "$SYNC" 2>&1)"
fail=$((fail+1))
fi
ck "脚本把运营商 DNS 写向 dnsmasq 的 resolv-file" \
'generated/isp-dns\.conf' "$($PODMAN exec "$NAME" cat "$SYNC" 2>&1)"
# 容器里没有 systemd,只能 SIGHUP;早先版本只 systemctl reload,
# 在容器内等于什么都不做(DNS 文件更新了但 dnsmasq 仍用旧上游)。
ck "无 systemd 时回退 SIGHUP 重载 dnsmasq" \
'kill -HUP .*pidof dnsmasq' "$($PODMAN exec "$NAME" cat "$SYNC" 2>&1)"
# render_ppp 写的路径必须与实际安装位置一致,否则 pppd 静默拿不到 DNS。
# -A60 而不是小窗口:ip-up-script 距 def 行有 46 行(docstring + 5 行解释
# 为什么要这个钩子),窗口窄了正好切在它前面,断言恒假。
ck "render_ppp 的 ip-up-script 路径与安装位置一致" \
'ip-up-script /opt/drouter/scripts/sync-isp-dns\.sh' \
"$($PODMAN exec "$NAME" grep -A60 'def render_ppp' /opt/drouter/backend/render.py 2>&1)"
# 反向断言:不能再有指向 bin/ 的旧路径(那是不存在的位置)
ckc "render_ppp 不再指向不存在的 bin/ 路径" 0 \
"$($PODMAN exec "$NAME" grep -c 'ip-up-script /opt/drouter/bin/' /opt/drouter/backend/render.py 2>&1)"
echo
echo "=== 11. backend 关键修复在镜像里(1.0.6)==="
# 抽查几处最容易「打包漏掉」的地方:rescue token、SNMP 净化、静态租约字段名。
# 注意用「内容匹配」而不是 grep -c 的数字 —— 数字拿去 ck 匹配代码文本
# 是把「命中了几个」当成了「命中了什么」,恒失败(1.0.6 首次冒烟栽在这)。
ck "rescue 服务端有令牌校验" '_token_ok' \
"$($PODMAN exec "$NAME" grep '_token_ok' /opt/drouter/backend/drouter-rescue.py 2>&1 | head -2)"
# 只断言「净化那几行在」,不硬写正则字面量:ck 内部走 grep -E,
# `[\r\n]` 这类字符类再经一层 shell 转义后极易对不上(1.0.6 首次冒烟栽过)。
# 真正要防的是「三个字段只有截长度、不过滤控制字符」,用净化后的特征串断言。
ck "SNMP 三个字段过滤换行" "re\.sub\(r, ' ', str\(s\[k\]\)\)|replace\('#', ' '\)" \
"$($PODMAN exec "$NAME" grep -A10 "for k in ('contact', 'location', 'sysname')" /opt/drouter/backend/drouter-helper.py 2>&1)"
ck "静态租约写 enabled+name" "'enabled': True, 'mac': mac" \
"$($PODMAN exec "$NAME" grep "'enabled': True, 'mac': mac" /opt/drouter/backend/drouter-helper.py 2>&1 | head -2)"
ck "readings 不再遮蔽 POST 写分支" '_has_post_branch' \
"$($PODMAN exec "$NAME" grep '_has_post_branch' /opt/drouter/backend/drouter-web.py 2>&1 | head -2)"
ckc "sync-isp-dns.sh 写盘失败非零退出(不能静默 rc=0)" 2 \
"$($PODMAN exec "$NAME" grep -c 'exit 1' /opt/drouter/scripts/sync-isp-dns.sh 2>&1)"
echo
echo "=== 12. 1.0.7 新增的三个守护在镜像里且语法正确 ==="
# 容器形态没有 systemd。这里只验「文件在 + 能编译 + 被 deploy.sh 安装」,
# 真跑起来要等 sync.sh 那轮真机验证。
#
#⛔ 不要在这里断言「有 Popen / start_new_session」——
# 那是**常驻服务**(shelld 那类)的需求,而这三个是 **oneshot 型timer**:
# timer 定时把它拉起来、脚本自己跑完就退出,压根不需要脱离 init 常驻。
# 1.0.7 的冒烟里写了这三条断言,三个守护一个都没有 Popen →
# 每次冒烟必红 3 条,于是 1.0.8 有人把期望从 1 改成别的数字来「修绿」,
# 那是把判据迁就实现。判据该问的是它自己的形态对不对。
for d in backupd alertd quotad; do
ck "drouter-$d.py 存在" "drouter-$d\.py" \
"$($PODMAN exec "$NAME" ls /opt/drouter/backend/drouter-$d.py 2>&1)"
ckc "drouter-$d.py 语法正确" 0 \
"$($PODMAN exec "$NAME" python3 -c "import ast,sys;ast.parse(open(sys.argv[1],encoding='utf-8').read())" /opt/drouter/backend/drouter-$d.py 2>&1; echo $?)"
# 形态自证:oneshot 守护的入口是 main(),跑完即退。
# 常驻守护才有「起一个子进程」的需求,出现 Popen 反而要警惕它跑不飞。
ckg "drouter-$d.py 是 oneshot(有 main 入口、无常驻子进程)" '__main__' \
"$($PODMAN exec "$NAME" grep -c '__main__' /opt/drouter/backend/drouter-$d.py 2>&1)"
ckc "drouter-$d.py 不含常驻子进程(Popen=0,oneshot 不需要)" 0 \
"$($PODMAN exec "$NAME" grep -c 'Popen' /opt/drouter/backend/drouter-$d.py 2>&1)"
done
ck "三个新守护在 deploy.sh 里被安装" 'drouter-backupd\.py' \
"$($PODMAN exec "$NAME" grep -oE 'drouter-(backupd|alertd|quotad)\.py' /opt/drouter/scripts/deploy.sh 2>&1 | head -3)"
# 三个都要在。上面那条 ck 只要求「至少含一个」—— 用计数把「漏装一个」钉死。
ckc "deploy.sh 里三个新守护齐全" 3 \
"$($PODMAN exec "$NAME" grep -oE 'drouter-(backupd|alertd|quotad)\.py' /opt/drouter/scripts/deploy.sh 2>&1 | sort -u | wc -l | tr -d ' ')"
echo
echo "=== 13. 1.0.7 修的三个隐蔽缺陷在镜像里 ==="
# ⓪ 先确认文件在位。下面每一条都往这个路径 grep —— 路径拼错时 grep 会往
# stderr 打 "No such file or directory",**而这个字符串被 2>&1 收进输出**,
# 于是「期望 0 次」那几条照样绿(错一次 == 期望值),
# 「期望包含 xxx」那几条则因为拿到的是报错而非内容而恒假。
# 一条 1.0.7 冒烟就栽在这里:/opt/douter 少个r,4 条断言一直是假红,
# 而它们本该守的 3 个真缺陷「看起来」早就修好了。
H2=/opt/drouter/backend/drouter-helper.py
ckc "helper 路径拼写正确(先验路径再谈内容)" 0 \
"$($PODMAN exec "$NAME" sh -c "test -f $H2" 2>&1; echo $?)"
# ① 默认备份不再整目录排除 /etc/drouter
# ⛔ 反向断言必须**排除注释**。这个字面量就写在解释 bug 来历的注释里
#(helper 11669 行:「早先的版本写了 sensitive=(d == '/etc/drouter')」),
# 直接 grep -c 必然命中 1 → 每次冒烟假红。
# 用 `grep -v` 先剔掉注释行;行首锚定 `^[^#]*` 兜住缩进后的注释。
ckc "备份不再整目录标敏感(sensitive=(d == ...) 已消失)" 0 \
"$($PODMAN exec "$NAME" grep -v '^[[:space:]]*#' $H2 2>&1 | grep -c "sensitive=(d ==")"
#ⓐ ckc 比数字,ckc 才问「数字等于几」;问「含不含 xxx」要用 ckg(见下)
ckg "备份有逐文件敏感判定" '_bk_is_sensitive' \
"$($PODMAN exec "$NAME" grep -c '_bk_is_sensitive' $H2 2>&1 | head -1)"
# ② VPN status 不再返回含私钥的 raw
#⛔ 同一个坑 + 一个更隐蔽的:helper 里另有一处 'raw'(打印服务的
# `cfg = {'cups': ..., 'raw': dict(PRINT_DEFAULTS['raw'])}`),
# 与 VPN 毫无关系。所以既要去注释,还得把范围限在 _vpn_status 函数内。
ckc "_vpn_status 不再返回 'raw': d" 0 \
"$($PODMAN exec "$NAME" sh -c "awk '/^def _vpn_status/,/^def [^_]/' $H2 | grep -v '^[[:space:]]*#' | grep -c \"'raw':\"" 2>&1)"
ckg "_vpn_mask 仍在(脱敏没被一起删掉)" '_vpn_mask' \
"$($PODMAN exec "$NAME" grep -c '_vpn_mask' $H2 2>&1 | head -1)"
# ③ ip_network 成员判断必须用对象(3.13 回归)
ckg "ip_network 归属判断用对象" 'aobj not in net' \
"$($PODMAN exec "$NAME" grep -cE 'aobj not in net' $H2 2>&1)"
# ④ wireguard 登记进 DEPS
ckg "DEPS 登记 wireguard-tools" "wireguard-tools" \
"$($PODMAN exec "$NAME" grep -c 'wireguard-tools' $H2 2>&1 | head -1)"
# ⑤ Python 版本必须是 3.13 —— 1.0.7 修的 ip_network 缺陷正是 3.13 才暴露的
ck "镜像 Python 是 3.13(3.12 上这条断言无意义)" 'Python 3\.13' \
"$($PODMAN exec "$NAME" python3 -V 2>&1)"
echo
echo "=== 14. 1.0.8 VPN 四态诊断在容器形态下的表现 ==="
# 容器里wireguard 模块多半加载不了(内核没暴露模块或 /lib/modules 缺失)。
# 关键不是「能不能装上」,而是**别判错态** ——
# 1.0.7 之前只分「有/无」,本轮才拆成四态并给出可执行的下一步。
H="/opt/drouter/backend/drouter-helper.py"
# ckg 而不是 ck:grep -c 只吐数字,问「数字含不含 def _vpn_env」恒为假。
ckg "_vpn_env 四态函数在镜像里" 'def _vpn_env' \
"$($PODMAN exec "$NAME" grep -c 'def _vpn_env' $H 2>&1 | head -1)"
# ⛔ 判据不能只 grep 字面量:注释/docstring 里也会写这些词。
# 所以下面真把函数跑起来,看它在**本机**返回哪一态。
# 探针文件位置:可用环境变量覆盖。写死本机路径的话,换台机器跑就会
# 直接判「探针不存在」—— 而真实原因只是路径不对,误报成产品问题。
PROBE="${PROBE:-/tmp/vpn-env-probe.py}"
if [ ! -f "$PROBE" ]; then
echo " ✘ 探针脚本不存在:$PROBE"
fail=$((fail+1))
else
$PODMAN cp "$PROBE" "$NAME:/tmp/vpn-env-probe.py" >/dev/null 2>&1
VENV_OUT="$($PODMAN exec "$NAME" python3 /tmp/vpn-env-probe.py 2>&1)"
if grep -q 'IMPORTERR' <<<"$VENV_OUT"; then
echo " ✘ _vpn_env 能在镜像里 import 并运行"
echo "$VENV_OUT" | head -5
fail=$((fail+1))
else
echo " ✔ _vpn_env 能在镜像里 import 并运行"
pass=$((pass+1))
for k in STATE VIRT FIXABLE AUTOLOAD KERNEL; do
v="$(grep "^$k=" <<<"$VENV_OUT" | head -1 | cut -d= -f2-)"
echo " $k = ${v:-(空)}"
done
st="$(grep '^STATE=' <<<"$VENV_OUT" | cut -d= -f2-)"
# 判据不能写死某一态:不同内核/容器能力下合法结果不同。
# 真正要钉死的是「返回值落在四态之内」—— 判错态才是 1.0.7 那个 bug。
if grep -qE '^(ready|need_module|need_tool|unsupported)$' <<<"$st"; then
echo " ✔ state 落在四态之内($st)"
pass=$((pass+1))
else
echo " ✘ state 必须是四态之一,实际「${st}」"
fail=$((fail+1))
fi
# unsupported 时不得声称「可一键修复」—— 那会把用户引向一条必然失败的操作
if [ "$st" = "unsupported" ]; then
if grep -q '^FIXABLE=False' <<<"$VENV_OUT"; then
echo " ✔ unsupported 时 fixable=False"
pass=$((pass+1))
else
echo " ✘ unsupported 时 fixable 必须为 False"
fail=$((fail+1))
fi
fi
# 模块文件那一级是本轮新增的探测;查不到也要正常返回而不是抛异常
if grep -qE '^KERNEL=\S' <<<"$VENV_OUT"; then
echo " ✔ kernel 版本取到了(四态诊断没因为 /lib/modules 缺失而崩)"
pass=$((pass+1))
else
echo " ✘ kernel 版本应能取到"
fail=$((fail+1))
fi
fi
fi
# 开机自启文件由 fix 写,镜像里不该预先存在。
#⛔ `test ! -e` 成功时退出码是 **0**,这里原来写期望 1 → 必红。
# 另外别用 ls:它失败时把错误信息打到 stdout,而
# `2>&1 >/dev/null` 那个顺序也不对(2>&1 在前,重定向的是已复制的旧 fd),
# echo $? 拿到的根本不是 ls 的退出码。
ckc "镜像里不预置 modules-load.d 配置" 0 \
"$($PODMAN exec "$NAME" sh -c 'test ! -e /etc/modules-load.d/drouter-wireguard.conf' >/dev/null 2>&1; echo $?)"
# 前端:老后端(没有 env 字段)必须显示 unknown 而不是 unsupported。
#⛔ 又一个 grep -c 配 ck 的恒假 —— 前两条 1.0.8 首次跑就是红的。
ckg "前端老后端兜底是 unknown" "state: 'unknown'" \
"$($PODMAN exec "$NAME" grep -c "state: 'unknown'" /opt/drouter/web/app.js 2>&1 | head -1)"
ckg "前端有 VPN 一键修复按钮" 'id="vp-fix"' \
"$($PODMAN exec "$NAME" grep -c 'id="vp-fix"' /opt/drouter/web/app.js 2>&1 | head -1)"
echo
echo "================================================"
echo " 容器冒烟:通过 $pass / 失败 $fail"
echo "================================================"
echo
echo "=== 宿主侧安全基线 ==="
# ⚠️ 这里**只能读**,不能断言。cleanup 是 trap EXIT 触发的,在这几行之后
# 才执行;此时容器还在、netavark 表已经被 podman 重建了 ——
# 基线断言放在这里必然红。断言在 cleanup() 内部做。
echo " ip_forward = $(cat /proc/sys/net/ipv4/ip_forward)"
echo " nft 表 = $(nft list tables 2>/dev/null | tr '\n' ' ')"
echo " 默认路由 = $(ip route | grep '^default' | head -1)"
# 5900 是 RealVNC,本项目**绝不触碰**。这里只读不改,
# 出现监听是正常的(用户自己在用 VNC)。
echo " 5900 监听 = $(ss -lnt 2>/dev/null | grep -c ':5900' )(只读,从不干预)"
# RC_AT_EXIT 给 cleanup() 里的 trap 用 —— 它要在这个exit 之后决定最终退出码。
RC_AT_EXIT=$fail
exit $fail