diff --git a/CLAUDE.md b/CLAUDE.md index 334c57a..e2d6fef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,3 +82,35 @@ cd web/website && npm run gen:tokens - `client/lib/pangolin_theme.dart` — 只含实现层(`PangolinScheme`/`PangolinText`/`PangolinTheme`),不含 token 数值。 - `design/flutter/` 已删除;Flutter 组件 canonical 实现在 `client/lib/widgets/`,规格在 `design/preview/`。 - **禁止**再向 `design/` 提交 Dart/TS 组件代码副本(会漂移)。 + +## client/ macOS 原生隧道(PacketTunnel 系统扩展 + 内嵌 libbox) + +内嵌 sing-box(`Libbox.xcframework`)的 `NEPacketTunnelProvider` **系统扩展**(站外 Developer ID +分发)。让它能被 `sysextd` 加载并真正连通踩了一长串坑,**改这块前必读** +`docs/macos-sysext-realize-troubleshooting.html`。以下是铁律: + +**构建 / 发版** +- 一律走 `scripts/local_test.sh`(`build`/`notarize`/`copy`/`run`):Developer ID 签名 + 公证 + staple。 +- **每次构建必递增 `CFBundleVersion`**(`CURRENT_PROJECT_VERSION`)——否则 `sysextd` 视为同版本**不更新**,装上去跑的还是旧扩展。 +- SIP 开启的机器只接受**已公证**的 sysext;`client/macos/sign_libbox.sh` 在构建期以 Developer ID 重签内嵌 Libbox。 + +**系统扩展能被 realize 的硬性要求**(缺一即 `code=4` / 静默拒) +- **自包含**:`PacketTunnel` target 设 `OTHER_LDFLAGS = ""`(切断继承项目级 CocoaPods 链接标志,否则会把 `flutter_secure_storage` 链进扩展);`Libbox.xcframework` **只 Link 不 Embed**(它是静态库)。验证:`otool -L` 扩展二进制应**零 `@rpath` 外部依赖**。 +- **bundle 名 = 标识符**:`PRODUCT_NAME = com.pangolin.pangolin.PacketTunnel`。 +- 扩展 `Info.plist` 必须有 **`NSSystemExtensionUsageDescription`**(网络扩展类别强制,主 app 的不顶用)。 +- **App Group 用 macOS 原生格式 `.`**(`BYL4KQHMTN.com.pangolin.pangolin`,非 iOS 的 `group.` 前缀);`NEMachServiceName` 以其为前缀。 +- 沙箱扩展补 `network.client` / `network.server`;`get-task-allow=false` + 签名加 `--timestamp`。 + +**libbox / NetworkExtension 集成铁律**(改 `PacketTunnelProvider.swift` 注意) +- `startTunnel` **必须在后台队列**执行 libbox 启动(`DispatchQueue.global().async`)——否则 `startOrReloadService` 在 provider 队列同步阻塞,与 `openTun → setTunnelNetworkSettings` 回调**三方死锁**(隧道卡 connecting 永不完成)。 +- `startOrReloadService(options:)` **传非空** `LibboxOverrideOptions()`(传 `nil` → libbox 解引用空指针 SIGSEGV,扩展进程崩溃)。 +- `startDefaultInterfaceMonitor` 要**阻塞到首个 path 更新再返回**(否则 sing-box 启动期拿不到默认接口,报 `no available network interface`)。 + +**配置由服务端渲染,客户端不拼** +- sing-box 客户端配置由 `server/internal/httpapi/clientconfig.go::BuildClientConfig` 渲染、原样下发。 +- **TUN 模式必须有 DNS 劫持**:`route.rules` 首条 `{"action":"hijack-dns","port":[53]}`(排在 LAN 直连规则前)——否则发往隧道 DNS(172.19.0.2:53)的查询被 `172.16.0.0/12` 吞去直连,域名解析失败,**隧道连上也打不开网站**。 +- REALITY 数据口走 **节点 `endpoint` 的端口**(当前 443;受限网络常封高位端口如 11443,优先 443)。 + +**已知坑** +- 开发机若是 **macOS 26 (Tahoe)**:`sysextd` 报 `no policy, cannot allow apps outside /Applications`(app 在 /Applications 也报)是 **Apple 回归**,本机调试需关 SIP 后 `systemextensionsctl developer on`;真实用户(macOS 14/15)不受影响。 +- #5 国内分流:客户端 `smartRoute` → `?split_cn=1` 下发远程 rule-set;**TODO 改本地 `.srs` 预取**,避免启动期下载。 diff --git a/docs/macos-sysext-realize-troubleshooting.html b/docs/macos-sysext-realize-troubleshooting.html index af7e451..86eb80b 100644 --- a/docs/macos-sysext-realize-troubleshooting.html +++ b/docs/macos-sysext-realize-troubleshooting.html @@ -159,6 +159,51 @@ sysext didFinishWithResult ✓ result=0 (completed) $ systemextensionsctl list * * BYL4KQHMTN com.pangolin.pangolin.PacketTunnel (1.0/1) [activated enabled] +

5.5 加载之后:从"扩展能起"到"真正连通"(运行时)

+

系统扩展能 realize/激活只是第一步。让内嵌的 libbox(sing-box)真正建起隧道、能上网,又踩了四个坑(均在 PacketTunnelProvider.swift / 服务端配置):

+ +
+

运行时 ① libbox startOrReloadService(options:) 传 nil → 空指针崩溃 扩展进程 SIGABRT

+

扩展进程启动 360ms 后自杀(SIGABRT)。把 stderr 重定向到 App Group 容器文件后抓到 Go panic:

+
panic: runtime error: invalid memory address or nil pointer dereference
+libbox.(*CommandServer).StartOrReloadService(server, {config}, 0x0)  command_server.go:175
+

此版本 libbox 会解引用第三个 options 参数(虽标 _Nullable)。修复:传非空 LibboxOverrideOptions() 而非 nil

+
+ +
+

运行时 ② 默认接口监控异步返回 → no available network interface 启动报错

+

startDefaultInterfaceMonitor 启动 NWPathMonitor 后立即返回,但首个 path 回调是异步晚到的;sing-box 紧接着的网络操作拿到的默认接口索引还是 -1。修复:用信号量**阻塞到首个 path 更新再返回**(带 5s 超时),对齐 sing-box-for-apple。

+
+ +
+

运行时 ③ provider 队列三方死锁 → 隧道永远卡 connecting 最隐蔽

+

openTun 打点显示卡在 setTunnelNetworkSettings 的信号量等待,回调永不触发:

+
    +
  • NE 在 provider 队列上调 startTunnel → 同步调 startOrReloadService(阻塞该队列)
  • +
  • sing-box 在 Go 线程调 openTun → sem.wait()setTunnelNetworkSettings 完成
  • +
  • 而该完成回调**正要在被阻塞的 provider 队列上投递** → 死锁
  • +
+

修复:把 libbox 启动整段放进后台队列(DispatchQueue.global().async),startTunnel 立即返回、provider 队列腾出来投递回调。

+
+ +
+

运行时 ④ 缺 DNS 劫持 → 隧道连上但打不开网站 最后一关

+

隧道起来了、TCP 能经 REALITY 出海,但域名打不开。box.log 显示发往隧道 DNS 的查询走了直连:

+
inbound packet connection to 172.19.0.2:53
+router: match ip_cidr=[..172.16.0.0/12..] => route(direct)   # DNS 被 LAN 规则吞去直连 → 解析失败
+

隧道 DNS 地址 172.19.0.2 落在 LAN 直连规则 172.16/12 内,被路由成直连(发往不存在的主机)→ 解析全失败。修复(服务端 clientconfig.go):route.rules 首条加 {"action":"hijack-dns","port":[53]}(排在 LAN 规则之前),把 :53 查询交给 sing-box DNS 模块。

+

注:sing-box 1.13 的 protocol:"dns" 匹配需先 sniff;按目的端口 53 匹配最稳、不依赖 sniff。

+
+ +
+

另一个发版必知:每次构建必递增 CFBundleVersion sysextd 按(标识符, 版本)去重;同版本号重装**不会替换**已激活的旧扩展,跑的还是旧代码(排查时极易被误导)。

+
+ +

连通验证(从命令行):

+
$ curl https://api.ipify.org          → 103.119.13.48   # 出口=节点 IP,流量走隧道
+$ curl -o/dev/null -w '%{http_code}' https://github.com   → 200
+box.log: router: match[0] port=53 => hijack-dns → dns: exchanged A github.com 140.82.116.3
+

6. 给后人的排查 checklist(Developer ID 网络系统扩展)

  1. 先找同模型的能用实现对照(Tailscale 独立版 / Mullvad),在同一台机器上验证环境没问题,把范围锁到自己的包。
  2. @@ -178,8 +223,18 @@ $ systemextensionsctl list systemextensionsctl developer on,或在 15/14 机器上验证。

    +

    8. 运行时 checklist(libbox + NE 集成)

    +
      +
    1. libbox startOrReloadService(options:) 传**非空** LibboxOverrideOptions(),别传 nil。
    2. +
    3. startTunnel 里的 libbox 启动放**后台队列**(DispatchQueue.global().async),别在 provider 队列同步跑(死锁)。
    4. +
    5. startDefaultInterfaceMonitor **阻塞到首个 path 更新再返回**。
    6. +
    7. TUN 配置必须有 **DNS 劫持**(action:hijack-dns,按 port 53),排在 LAN/分流规则之前。
    8. +
    9. **每次构建递增 CFBundleVersion**,否则 sysextd 不更新已激活的扩展。
    10. +
    11. 看 libbox 自身日志:配置 log.output 指向容器文件(writeLogs 回调启动期不触发);Go fatal 走 stderr,需把扩展 stderr 重定向到文件才看得到。
    12. +
    +
    -

    改动文件:client/macos/PacketTunnel/Info.plist · client/macos/PacketTunnel/PacketTunnel.entitlements · client/macos/Runner/Release.entitlements · client/macos/PacketTunnel/PacketTunnelProvider.swift · client/macos/Runner.xcodeproj/project.pbxproj(OTHER_LDFLAGS/PRODUCT_NAME/移除 Embed Libbox/加 timestamp)。验证机:cara,干净 macOS 15.3.2 Intel。

    +

    改动文件:扩展 Info.plist/entitlements/main.swift/PacketTunnelProvider.swift · Runner/Release.entitlements · Runner.xcodeproj/project.pbxproj(OTHER_LDFLAGS/PRODUCT_NAME/移除 Embed Libbox/--timestamp/版本号) · scripts/local_test.sh · client/macos/sign_libbox.sh · 服务端 server/internal/httpapi/clientconfig.go(DNS 劫持)。验证机:cara,干净 macOS 15.3.2 Intel,出口 IP=节点、国外站可达。

    diff --git a/docs/vpn-testing-research.md b/docs/vpn-testing-research.md new file mode 100644 index 0000000..068f7d4 --- /dev/null +++ b/docs/vpn-testing-research.md @@ -0,0 +1,124 @@ +# VPN 测试调研(测什么 / 怎么测 / Pangolin 测试工具设计) + +> 目的:在动手写测试工具前,先搞清楚 VPN 该测哪些维度、行业与中国/GFW 场景怎么测, +> 再据此设计 Pangolin 自己的测试工具。日期 2026-06-22。 + +--- + +## TL;DR + +VPN 测试 = **连得上 + 连得对 + 连得快 + 连得稳 + 不泄漏 + (中国场景)穿得过且不被封**。 +六大类:**连通性 / 正确性(出口·DNS·分流)/ 泄漏 / 性能(延迟·吞吐·丢包)/ 稳定性(掉线·重连·killswitch)/ 抗封锁(GFW)**。 +Pangolin 是面向大陆、REALITY+sing-box、macOS 自研客户端,**抗封锁 + 国内分流正确性 + 晚高峰吞吐**是和通用评测最不同、最该重点测的。 + +--- + +## 1. 行业通用 VPN 评测怎么测 + +主流评测站(Security.org、Cloudwards、PCMag 等)的共识方法论: + +| 维度 | 测什么 | 怎么测 / 工具 | 通过标准 | +|---|---|---|---| +| **速度/吞吐** | 下载、上传、相对基线的衰减 | Ookla/iperf,**多服务器距离**(本地/美/英/亚)、**多时段**(早/晚高峰)、先测裸连基线再测开 VPN | 近端衰减 <10–20% 算优;远端单独看 | +| **延迟** | RTT(ping)、首包延迟 | ping / curl `time_connect` | 越低越好;近端 +几十 ms 可接受 | +| **DNS 正确性** | 域名能否解析、解析延迟 | `dig`/`nslookup`、curl `time_namelookup` | 能解析、不超时 | +| **IP 泄漏** | 真实 IP 是否暴露(IPv4/IPv6) | 访问 ipify/ifconfig.me/ipleak.net,对比开关 VPN 出口 | 出口必须是节点 IP,**不能是本机真实 IP** | +| **DNS 泄漏** | DNS 查询是否走了本地 ISP | dnsleaktest.com、Wireshark 抓 53 端口去向 | 查询只经隧道/指定 DNS | +| **WebRTC 泄漏** | 浏览器 WebRTC 暴露真实 IP | browserleaks.com/webrtc | 不暴露 | +| **IPv6 泄漏** | IPv6 绕过隧道 | test-ipv6.com | 无 v6 泄漏(或 v6 也走隧道) | +| **Kill Switch** | 隧道意外断开时是否阻断流量 | 强杀扩展/断网,看是否还能裸连出网 | 断开瞬间流量被掐 | +| **流媒体/可达性** | 能否访问目标站点 | 实访 Netflix/YouTube/Google 等 | 能正常打开/播放 | +| **稳定性** | 长时连接掉线率、重连 | 长跑数小时,周期性请求 | 不频繁掉线;掉了能自动重连 | +| **抓包审计** | 是否有明文/异常外联 | Wireshark/tcpdump | 无明文、无意外外联 | + +要点:**先测基线(不开 VPN)再测开 VPN 做对比**;**多次/多时段**取分布而非单点。 + +--- + +## 2. 中国 / GFW 场景特有的测法(Pangolin 重点) + +通用评测不覆盖、但对面向大陆的 VPN 是生死线: + +- **抗 GFW 封锁(核心)** + - GFW 一旦判定某连接是代理,会对 **(client IP, server IP, server port) 三元组封 120–180 秒**(期间丢所有包)。测试:连一段时间后看是否突然全断、过几分钟又能连——这是被三元组封的特征。 + - **主动探测(active probing)**:GFW 会主动连你的端口探测。REALITY 的防御是把探测流量转发给真实伪装站(如 apple.com)。测试:用普通 TLS 客户端(curl/openssl)连节点的 REALITY 端口,应表现得**像真实伪装站**(返回真站证书/内容),而不是暴露代理特征。 + - **全加密流量检测**:GFW 对"高熵全加密"流量有启发式封锁。REALITY/VLESS 借真证书规避。测试:长期存活率(连续几天能否一直连)。 +- **晚高峰吞吐**:国内出海带宽晚高峰(20:00–24:00)严重劣化。**必须按时段测吞吐**(白天 vs 晚高峰),单测白天没意义。 +- **MTU / 分片**:运营商(尤其移动 5G)对大包/分片敏感,会借此识别。业界经验 **MTU clamp 到 1350(IPv6 1280)** 降低被识别和卡顿。测试:大包传输是否卡顿、丢包率。⚠️ 我们 TUN 配的是 `mtu 9000`(sing-box 默认),需评估对 REALITY-over-TCP 的实际影响。 +- **国内分流正确性(#5)**:`geoip-cn`/`geosite-cn` 命中应**直连**(不走隧道),国外才走隧道。测试:访问国内站(如 baidu)出口应是**本地 IP**,国外站出口是**节点 IP**——分流错了要么国内变慢、要么国外漏走直连。 +- **协议可达性矩阵**:REALITY(TCP 443)、Hysteria2(UDP 443)分别在不同网络(电信/联通/移动、家宽/5G)下的可达性。 + +参考:GFW 对全加密/QUIC 的检测与封锁机制见 gfw.report 的 USENIX 论文。 + +--- + +## 3. Pangolin 该测什么(结合架构分层) + +架构:macOS 客户端(NEPacketTunnelProvider 系统扩展 + 内嵌 libbox/sing-box)→ REALITY(TCP)/Hy2(UDP)→ 节点 → 出海。控制面 :8080 下发配置。 + +### A. 连通性(能不能起来) +- 系统扩展 `activated enabled`(`systemextensionsctl list`)。 +- 隧道接口起来(utun + `172.19.0.1`)、NEVPNStatus = connected。 +- 控制面可达(:8080)、REALITY 数据口可达(节点 endpoint 端口)。 +- **首次连接耗时**(含批准)、**重连耗时**。 + +### B. 正确性(连得对) +- **出口 IP = 节点 IP**(`curl ipify`),证明真走隧道。 +- **DNS 解析正常**(域名能开;`hijack-dns` 生效——这次的坑)。 +- **国内分流**:国内站出口=本地、国外站出口=节点(开 smartRoute 时)。 +- 无 **IP/DNS/IPv6 泄漏**。 + +### C. 性能(连得快) +- 延迟:到节点 RTT、隧道内到常见站 RTT。 +- 吞吐:下载/上传速度,**白天 vs 晚高峰**,对比裸连基线。 +- 丢包率、DNS 解析延迟。 + +### D. 稳定性(连得稳) +- 长跑掉线率、自动重连。 +- Kill switch(`strict_route` 已开):强杀扩展后流量是否被掐。 +- 节点 agent 热重载(SIGHUP)后客户端是否平滑。 + +### E. 抗封锁(穿得过) +- REALITY 端口被动探测表现(伪装站特征)。 +- 长期存活率(多日)。 +- 三元组封锁特征监测。 + +--- + +## 4. 测试工具设计建议 + +分三层,**先做能自动化、回报最高的"客户端侧黑盒探测脚本"**: + +### 工具一:`scripts/vpn_test.sh`(客户端侧黑盒,优先做) +在已连接的机器(本机/cara)上跑,一条命令出报告。覆盖 B+C 大部分: +- **出口 IP**:`curl ipify`,判定 = 节点 / = 本地 / 超时。 +- **DNS**:解析 github/google + `time_namelookup`;开/关分流分别测国内外站出口。 +- **可达性矩阵**:对一组目标(google/youtube/github/baidu…)测 HTTP 码 + 耗时。 +- **延迟**:`curl time_connect` 到节点和若干站。 +- **吞吐**:下载固定大小文件测 `speed_download`(用 cloudflare speed / 自建测速点)。 +- **泄漏**:IPv6 出口、DNS 出口对比。 +- 输出:表格 + 时间戳,可重复跑做时段对比。 +- 复用本会话已验证的 `ssh cara '...'` 远程跑法。 + +### 工具二:节点侧探针(可选) +- REALITY 端口被动探测模拟(openssl 连 443 看是否像伪装站)。 +- 三元组封锁监测(从国内 vantage 持续发包看存活)。 + +### 工具三:客户端内置自检(产品化,后续) +- app 内"诊断"按钮:跑连通/出口/DNS 自检,给用户一键报告(也利于支持排障)。 + +### 输出与判定 +- 每项给 **PASS/WARN/FAIL** + 实测值 + 阈值。 +- 支持 **基线对比**(先 `--baseline` 裸连存一份,再开 VPN 对比衰减)。 +- 支持 **多时段**(cron 跑,产出趋势)。 + +--- + +## 5. 参考来源 + +- Security.org《Best VPN Services of 2026: 50+ VPNs Tested》— 速度/泄漏/流媒体方法论:https://www.security.org/vpn/best/ +- Cloudwards《VPN Test: DNS Leaks, IP Address Leaks, Speed Issues》:https://www.cloudwards.net/vpn-test-guide/ +- DoVPN《IP Leak Test Guide》:https://dovpn.com/ip-leak-test-guide/ +- gfw.report — GFW 对全加密流量的检测(USENIX'23):https://gfw.report/publications/usenixsecurity23/en/ +- gfw.report — GFW 对 QUIC/SNI 的封锁(USENIX'25):https://gfw.report/publications/usenixsecurity25/en/ +- VLESS-REALITY 部署/绕过实践:https://greatfirewallguide.com/lab/vless-reality-vision diff --git a/scripts/vpn_test.sh b/scripts/vpn_test.sh new file mode 100755 index 0000000..de2d04b --- /dev/null +++ b/scripts/vpn_test.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +# vpn_test.sh —— Pangolin 客户端侧黑盒连通/正确性/性能测试(调研文档「工具一」) +# +# 在"已连接 VPN 的机器"上跑(本机或 ssh 到 cara),一条命令出报告。覆盖: +# 出口 IP / DNS / 可达性矩阵 / 延迟 / 吞吐 / IPv6 泄漏。 +# 每项给 PASS/WARN/FAIL + 实测值。支持基线对比(先 --baseline 裸连存一份)。 +# +# 用法: +# bash scripts/vpn_test.sh # 完整测试(假定 VPN 已连) +# NODE_IP=103.119.13.48 bash scripts/vpn_test.sh +# bash scripts/vpn_test.sh --quick # 跳过吞吐(快) +# bash scripts/vpn_test.sh --baseline # 记基线(裸连,不开 VPN 时跑) +# 远程:scp scripts/vpn_test.sh cara@HOST:/tmp/ && ssh cara@HOST 'bash /tmp/vpn_test.sh' +# +# 设计:见 docs/vpn-testing-research.md。零依赖(curl + 系统自带),macOS/Linux 通用。 + +set -u + +# ── 配置 ───────────────────────────────────────────────────────────── +NODE_IP="${NODE_IP:-103.119.13.48}" # 期望的隧道出口(节点 IP) +TIMEOUT="${TIMEOUT:-12}" # 单请求超时(秒) +DL_BYTES="${DL_BYTES:-5000000}" # 吞吐测试下载字节数(默认 5MB) +QUICK=0; MODE="vpn" +for a in "$@"; do + case "$a" in + --quick) QUICK=1 ;; + --baseline) MODE="baseline" ;; + esac +done + +# 国外目标(应经隧道):站点 +FOREIGN=("https://www.google.com/generate_204" "https://www.youtube.com/generate_204" \ + "https://github.com" "https://www.gstatic.com/generate_204") +# 国内目标(开分流应直连、快):站点 +DOMESTIC=("https://www.baidu.com" "https://www.qq.com") + +# ── 输出辅助 ───────────────────────────────────────────────────────── +if [ -t 1 ]; then G=$'\033[32m'; Y=$'\033[33m'; R=$'\033[31m'; B=$'\033[1m'; N=$'\033[0m' +else G=""; Y=""; R=""; B=""; N=""; fi +pass=0; warn=0; fail=0 +P(){ printf " ${G}PASS${N} %-22s %s\n" "$1" "$2"; pass=$((pass+1)); } +W(){ printf " ${Y}WARN${N} %-22s %s\n" "$1" "$2"; warn=$((warn+1)); } +F(){ printf " ${R}FAIL${N} %-22s %s\n" "$1" "$2"; fail=$((fail+1)); } +H(){ printf "\n${B}== %s ==${N}\n" "$1"; } + +cget(){ curl -s --max-time "$TIMEOUT" "$@"; } # 静默 + 超时 + +echo "${B}Pangolin VPN 测试${N} $(date '+%Y-%m-%d %H:%M:%S') 模式=${MODE} 期望出口=${NODE_IP}" + +# ── 1. 连通性:本机隧道接口 / 系统扩展(仅本机 macOS 有意义)───────── +H "1. 连通性" +if command -v ifconfig >/dev/null 2>&1; then + if ifconfig 2>/dev/null | grep -q "inet 172.19.0.1"; then P "tun 接口" "172.19.0.1 已起" + else W "tun 接口" "未见 172.19.0.1(VPN 没连?或非本机)"; fi +fi +if command -v systemextensionsctl >/dev/null 2>&1; then + st=$(systemextensionsctl list 2>/dev/null | grep -i pangolin | grep -oE "activated (enabled|waiting for user)" | head -1) + [ -n "$st" ] && P "系统扩展" "$st" || W "系统扩展" "未激活/未安装" +fi + +# ── 2. 出口 IP(核心:是否真走隧道)────────────────────────────────── +H "2. 出口 IP" +EGRESS="" +for u in "https://api.ipify.org" "https://ifconfig.me/ip" "https://ipinfo.io/ip"; do + EGRESS=$(cget "$u" | tr -dc '0-9.'); [ -n "$EGRESS" ] && break +done +if [ -z "$EGRESS" ]; then + F "出口 IP" "拿不到(国外不可达 / DNS 挂 / 未连)" +elif [ "$EGRESS" = "$NODE_IP" ]; then + P "出口 IP" "$EGRESS(= 节点,确实走隧道)" +else + W "出口 IP" "$EGRESS(≠ 节点 $NODE_IP;可能是本地/其他 VPN/直连)" +fi + +# ── 3. DNS 解析 ────────────────────────────────────────────────────── +H "3. DNS 解析" +dnsok=0 +for d in github.com www.google.com; do + t=$(cget -o /dev/null -w "%{time_namelookup}" "https://$d" 2>/dev/null) + code=$(cget -o /dev/null -w "%{http_code}" "https://$d" 2>/dev/null) + if [ "${code:-000}" != "000" ]; then P "DNS $d" "解析+连通 ${t}s (HTTP $code)"; dnsok=$((dnsok+1)) + else F "DNS $d" "解析/连接失败(HTTP 000;hijack-dns 没生效?)"; fi +done + +# ── 4. 可达性矩阵(国外应通)────────────────────────────────────────── +H "4. 可达性(国外,应经隧道)" +for u in "${FOREIGN[@]}"; do + read -r code tt < <(cget -o /dev/null -w "%{http_code} %{time_total}" "$u" 2>/dev/null || echo "000 -") + host=$(echo "$u" | sed -E 's#https?://([^/]+).*#\1#') + if [ "${code:-000}" != "000" ]; then P "$host" "HTTP $code, ${tt}s" + else F "$host" "不可达(HTTP 000)"; fi +done + +# ── 5. 国内直连(开分流时应直连且快)───────────────────────────────── +H "5. 国内站(开分流应直连)" +for u in "${DOMESTIC[@]}"; do + read -r code tt < <(cget -o /dev/null -w "%{http_code} %{time_total}" "$u" 2>/dev/null || echo "000 -") + host=$(echo "$u" | sed -E 's#https?://([^/]+).*#\1#') + if [ "${code:-000}" != "000" ]; then P "$host" "HTTP $code, ${tt}s" + else W "$host" "不可达(分流/DNS 异常?)"; fi +done + +# ── 6. 延迟(连接握手耗时)──────────────────────────────────────────── +H "6. 延迟" +for u in "https://$NODE_IP:8080" "https://www.gstatic.com/generate_204"; do + tc=$(cget -k -o /dev/null -w "%{time_connect}" "$u" 2>/dev/null) + [ -n "$tc" ] && [ "$tc" != "0.000000" ] && P "connect $u" "${tc}s" || W "connect $u" "无响应/超时" +done + +# ── 7. 吞吐(下载速度)──────────────────────────────────────────────── +if [ "$QUICK" = 0 ]; then + H "7. 吞吐(下载 ${DL_BYTES}B)" + read -r sz spd tt < <(cget -o /dev/null -w "%{size_download} %{speed_download} %{time_total}" \ + "https://speed.cloudflare.com/__down?bytes=${DL_BYTES}" 2>/dev/null || echo "0 0 -") + if [ "${sz:-0}" -gt 0 ] 2>/dev/null; then + mbps=$(awk "BEGIN{printf \"%.2f\", ${spd:-0}*8/1000000}") + P "下载速度" "${mbps} Mbps (${sz}B in ${tt}s)" + else F "下载速度" "下载失败"; fi +fi + +# ── 8. IPv6 泄漏 ───────────────────────────────────────────────────── +H "8. IPv6 泄漏" +# 用 v6-only 端点;无 v6 时 curl -6 失败返回空。仅当结果含冒号(真 v6 地址)才算有 v6。 +v6=$(cget -6 "https://api6.ipify.org" 2>/dev/null) +if echo "$v6" | grep -q ':'; then W "IPv6" "v6 出口 $v6(确认是否走隧道,否则泄漏)" +else P "IPv6" "无 v6 出口(不泄漏)"; fi + +# ── 汇总 ───────────────────────────────────────────────────────────── +H "汇总" +printf " ${G}PASS %d${N} ${Y}WARN %d${N} ${R}FAIL %d${N}\n" "$pass" "$warn" "$fail" +[ "$fail" -gt 0 ] && exit 1 || exit 0