test(macos): local_test.sh 加 killswitch fail-closed 漏测子命令
ks-baseline:VPN 断开下记录真实公网 IP 基线 ks-test:VPN 连接(killswitch 开)下 sudo 杀扩展进程,在 on-demand 重连 窗口内反复探 IP —— 任何一次拿到真实 IP 即判泄漏(FAIL),全程被阻断/ 仅隧道 IP 即 fail-closed 生效(PASS);随后验 on-demand 自愈 ks-status:看 VPN 状态 + 统一日志里 killSwitch/includeAllNetworks 打点 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+82
-1
@@ -23,6 +23,9 @@ SE_PROFILE_NAME="Pangolin PacketTunnel DevID" # PacketTunnel 的描述文件
|
||||
TEST_EMAIL="wang880812@gmail.com" # 联调测试账号
|
||||
TEST_PASSWORD="pangolin2026"
|
||||
NOTARY_PROFILE="pangolin-notary" # notarytool 凭据(已存入钥匙串)
|
||||
EXT_NAME="PacketTunnel" # System Extension 可执行名(killswitch 测试用)
|
||||
VPN_NAME="Pangolin" # NETunnelProviderManager.localizedDescription
|
||||
IP_SVC="https://api.ipify.org" # 返回纯文本公网 IP
|
||||
# 注:DevID profile 已含 system-extension.install + NE(-systemextension 变体);
|
||||
# app/sysext entitlements 与之对齐(见 write_entitlements)。
|
||||
|
||||
@@ -155,6 +158,81 @@ cmd_run(){
|
||||
exec /Applications/pangolin_vpn.app/Contents/MacOS/pangolin_vpn
|
||||
}
|
||||
|
||||
# ─────────── KillSwitch fail-closed 验证 ───────────
|
||||
# 原理:killswitch 开 → NE 设了 includeAllNetworks + enforceRoutes(L2,OS 级强制)
|
||||
# + NEOnDemandRule 常开(L3)。验证点:隧道数据进程(sysext)被杀的瞬间,流量必须
|
||||
# 被 OS 挡住、真实 IP 不泄漏;随后 on-demand 把隧道自动拉回。
|
||||
get_ip(){ curl -fsS --max-time "${2:-5}" "$IP_SVC" 2>/dev/null; }
|
||||
vpn_status(){ scutil --nc status "$VPN_NAME" 2>/dev/null | head -1; }
|
||||
ext_pid(){ pgrep -x "$EXT_NAME" 2>/dev/null || pgrep -f "$EXT_NAME.systemextension" 2>/dev/null || true; }
|
||||
|
||||
# 第 1 步:VPN 断开下记录「真实公网 IP」基线(后续用来判断是否泄漏)。
|
||||
cmd_ks_baseline(){
|
||||
log "KillSwitch 基线:记录真实公网 IP(请确保此刻 VPN 已断开)"
|
||||
local real; real="$(get_ip real 6)"
|
||||
[ -n "$real" ] || die "拿不到公网 IP(断网?)。请连上普通网络、断开 VPN 再跑。"
|
||||
echo "$real" > "$WORK/ks_real_ip"
|
||||
echo " 真实公网 IP = $real(已存 $WORK/ks_real_ip)"
|
||||
echo " 默认路由接口:"; route -n get default 2>/dev/null | grep -E 'interface:' || true
|
||||
}
|
||||
|
||||
# 第 2 步:VPN 连接(killswitch 开)下做漏测 —— 杀扩展进程,在重连窗口内狂探 IP。
|
||||
cmd_ks_test(){
|
||||
[ -f "$WORK/ks_real_ip" ] || die "先跑 ks-baseline 记录真实 IP。"
|
||||
local real; real="$(cat "$WORK/ks_real_ip")"
|
||||
log "KillSwitch 漏测(真实 IP=$real)"
|
||||
|
||||
echo " 当前 VPN 状态: $(vpn_status)"
|
||||
vpn_status | grep -qi Connected || die "VPN 未连接。请在 app 内连接(killswitch 保持开)后再跑。"
|
||||
|
||||
local vpnip; vpnip="$(get_ip vpn 6)"
|
||||
echo " 连接中出口 IP = ${vpnip:-<取不到>}"
|
||||
[ -n "$vpnip" ] && [ "$vpnip" = "$real" ] && die "出口 IP 等于真实 IP —— VPN 没真正接管,测试无意义。"
|
||||
|
||||
local pid; pid="$(ext_pid)"
|
||||
[ -n "$pid" ] || die "找不到扩展进程 $EXT_NAME(它以 root 运行,确认隧道在跑)。"
|
||||
echo " 扩展进程 PID = $pid,准备 sudo 杀掉模拟崩溃…(可能要输密码)"
|
||||
sudo kill -9 $pid 2>/dev/null || sudo pkill -x "$EXT_NAME" 2>/dev/null || true
|
||||
|
||||
# 杀掉后立刻在重连窗口内反复探测:任何一次拿到「真实 IP」即判定泄漏。
|
||||
log "重连窗口漏测(8 次 × ~1s)"
|
||||
local leaked=0 reachable=0 i ip
|
||||
for i in $(seq 1 8); do
|
||||
ip="$(get_ip probe 1)"
|
||||
if [ -z "$ip" ]; then
|
||||
echo " #$i 流量被阻断(无响应)✓ fail-closed"
|
||||
elif [ "$ip" = "$real" ]; then
|
||||
echo " #$i ❌ 泄漏!拿到真实 IP=$ip"
|
||||
leaked=1
|
||||
else
|
||||
echo " #$i 仍走隧道 IP=$ip(on-demand 已拉回)"
|
||||
reachable=1
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
log "on-demand 自愈检查(等 5s)"
|
||||
sleep 5
|
||||
echo " VPN 状态: $(vpn_status)"
|
||||
echo " 此刻出口 IP = $(get_ip after 6)(应为隧道 IP、且 != $real)"
|
||||
|
||||
echo ""
|
||||
if [ "$leaked" = 1 ]; then
|
||||
die "FAIL —— 检测到真实 IP 泄漏,fail-closed 未生效(检查 includeAllNetworks/enforceRoutes)。"
|
||||
fi
|
||||
printf '\033[1;32m✅ PASS —— 扩展被杀期间真实 IP 未泄漏(fail-closed 生效)。\033[0m\n'
|
||||
echo " 补充手测:在 app 内点「断开」→ 跑 ks-status,应稳定 Disconnected 不被 on-demand 拉回(验 stop() 处理)。"
|
||||
}
|
||||
|
||||
# 辅助:看 NE 是否真把 killswitch 配置写进去了(读统一日志我加的打点)。
|
||||
cmd_ks_status(){
|
||||
log "VPN 状态"; vpn_status
|
||||
echo ""; log "最近 5 分钟 killswitch 相关日志(app + 扩展打点)"
|
||||
log show --last 5m --style compact \
|
||||
--predicate 'eventMessage CONTAINS[c] "killSwitch" OR eventMessage CONTAINS "includeAllNetworks"' \
|
||||
2>/dev/null | tail -20 || echo " (无,确认已连接过一次)"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
build) cmd_build ;;
|
||||
sign) cmd_sign ;; # 旧:手动 Developer ID 重签(现已由 Xcode 工程配置直接签,通常不需要)
|
||||
@@ -163,5 +241,8 @@ case "${1:-}" in
|
||||
run) cmd_run ;;
|
||||
# Xcode 工程已配 Developer ID 手动签名(Release 配置),build 即出 DevID 包,无需 sign。
|
||||
all) cmd_build; cmd_notarize; cmd_copy; cmd_run ;;
|
||||
*) echo "用法: $0 {all|build|sign|notarize|copy|run}"; exit 2 ;;
|
||||
ks-baseline) cmd_ks_baseline ;; # ① VPN 断开下记录真实公网 IP
|
||||
ks-test) cmd_ks_test ;; # ② VPN 连接(killswitch 开)下杀扩展做漏测
|
||||
ks-status) cmd_ks_status ;; # 看 VPN 状态 + killswitch 配置打点日志
|
||||
*) echo "用法: $0 {all|build|sign|notarize|copy|run|ks-baseline|ks-test|ks-status}"; exit 2 ;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user