[{"content":" 朋友坐在我家客厅，MacBook 上的 OpenVPN Connect 死活连不上我 NAS 的 VPN。我没碰她的电脑，只把一条 ssh 命令丢给了 AI agent（Claude Code）。\n一个小时后：VPN 通了；她的设备在 NAS 上有了专属的出口策略；桌面上多了三个带图标的小程序，双击就能用。\n这篇文章记两件事：故障本身是怎么一层层剥开的，以及有了 AI 之后，这类工作和以前到底哪里不一样了。\n文中所有公网 IP、密码、商业服务商品牌、个人域名与真实用户名均已抹去或改写，只保留可复用的结构。内网地址与真实环境一致（192.168.1.0/24，网段约定同前两篇），VPN 段记作 172.x.y.0/24，对外端口记作 \u0026lt;VPN端口\u0026gt;。\n一、TL;DR 一次\u0026quot;连不上\u0026quot;背后叠了四层互不相关的问题，每剥掉一层症状就变一次：\n层 症状 根因 1 Server poll timeout（TCP） 客户端 .ovpn 写的是 proto tcp-client，服务端只开 UDP 2 改成 UDP 仍超时 人在家里局域网，拨的却是公网域名 —— 路由器 hairpin NAT 不通 3 加了内网 remote 仍只打公网 IP OpenVPN Connect 把服务器地址缓存在自己的数据库里，改磁盘上的 .ovpn 不生效 4 UDP send exception: send: Broken pipe 同机的另一个商业 VPN（WireGuard）kill-switch 在拦包；断开后残留状态仍拦，换了一条网络链路才消失 连通之后又追加了两件事：\n仅这一台设备的流量不走家里的旁路网关 OpenWrt（其他 VPN 用户不变）：client-config-dir 固定 IP + 一条 pref 0 的 ip rule + 写进 DSM 定时任务持久化。 给一个不懂命令行的用户交付桌面工具：一键代理开关（带测速、退出恢复）、一个只走代理的 Gemini 独立浏览器、一个远程桌面入口。方案迭代了三版。 全程 20:59 → 22:05，一个会话，同时操作三台机器（我的 Mac、她的 Mac、NAS）。\n二、背景 服务端：群晖 NAS 上的 OpenVPN Server，proto udp，端口 \u0026lt;VPN端口\u0026gt;，地址池 172.x.y.0/24，topology subnet，账号密码认证（username-as-common-name + duplicate-cn，所有人共用一个账号）。 拓扑：NAS 上有一张策略路由表 —— from 172.x.y.0/24 lookup 100，table 100 的默认路由指向旁路网关 OpenWrt（192.168.1.254），VPN 用户的流量由它分流出境；NAS 自身走主路由 192.168.1.1。这套东西的来龙去脉见《家庭网络分层拓扑》。 客户端：朋友的 MacBook（macOS 15），OpenVPN Connect 3.8，.ovpn 是从 DSM 导出后别人手改过的。机器上还装着另一个商业 VPN、一个代理工具、一个企业 VDI 客户端和远程控制软件。 我的角色：坐在旁边，不碰她的电脑。所有操作通过 sshpass ... ssh 由 AI 执行；NAS 也是 ssh + sudo。 三、排障时间线 按真实顺序写，每一步都是\u0026quot;看到什么 → 怀疑什么 → 用什么命令证实/证伪\u0026quot;。\n3.1 第一层：协议不对 先看客户端日志（~/Library/Application Support/OpenVPN Connect/log/ovpn.log）：\nContacting \u0026lt;公网IP\u0026gt;:\u0026lt;VPN端口\u0026gt; via TCP EVENT: WAIT Server poll timeout, trying next remote entry... 再看 .ovpn：\nremote \u0026lt;ddns域名\u0026gt; \u0026lt;VPN端口\u0026gt; proto tcp-client 而服务端在 NAS 上：\n$ netstat -lnu | grep \u0026lt;VPN端口\u0026gt; udp6 0 0 :::\u0026lt;VPN端口\u0026gt; :::* $ nc -vz 192.168.1.4 \u0026lt;VPN端口\u0026gt; # TCP Connection refused TCP 永远握不上。 这一步 AI 是先查了我的记忆库（后面第六章会讲），已经知道\u0026quot;服务端是 UDP \u0026lt;VPN端口\u0026gt;\u0026quot;，所以第一眼就盯住了 proto 行。\n3.2 第二层：在家里打公网 IP 改成 UDP 后还是超时。这时注意到一个细节：\n$ curl ifconfig.me # 在她的 Mac 上 \u0026lt;和我家一样的公网IP\u0026gt; 她连的是我家 Wi-Fi，拨的却是 DDNS 域名 —— 从内网打自己路由器的公网 IP，靠的是 hairpin NAT，而我家路由器不支持。这个坑在我记忆库里也有一条：\u0026ldquo;公网 DDNS 在家 hairpin 不通会静默卡死\u0026rdquo;。\n修法直觉上很简单：在 .ovpn 里加一行内网地址，让它优先：\nremote 192.168.1.4 \u0026lt;VPN端口\u0026gt; udp remote \u0026lt;ddns域名\u0026gt; \u0026lt;VPN端口\u0026gt; udp 3.3 第三层：客户端不读你改的文件 改完再连，日志里依然只有公网 IP，192.168.1.4 一次都没出现过。\nOpenVPN Connect 3 导入 profile 时，会把解析出来的 remoteHost / serverList 存进自己的 config.json（一个 redux persist 状态）。之后连接用的是这份缓存，磁盘上的 .ovpn 只是个备份。所以：\n改 OpenVPN Connect 的配置，必须在 app 里删掉 profile 重新导入，而不是改文件。\n重新导入后，日志终于出现 Contacting 192.168.1.4:\u0026lt;VPN端口\u0026gt; via UDP。然后症状变了。\n3.4 第四层：Broken pipe Connecting to [192.168.1.4]:\u0026lt;VPN端口\u0026gt; via UDP UDP send exception: send: Broken pipe UDP send exception: send: Broken pipe ... 包在本机就没发出去。 查她 Mac 上的网络状态：\n$ scutil --nc list * (Connected) ... \u0026#34;\u0026lt;商业VPN\u0026gt; (Wireguard)\u0026#34; $ netstat -rn | grep default default link#19 UCSg utun4 另一个商业 VPN 正处于连接状态，utun4 抢了默认路由，它的 kill-switch 会丢掉一切不走隧道的流量。断开它 —— 依然 Broken pipe。\n接下来是一段真正的\u0026quot;排障\u0026quot;：\npfctl -s info → pf 是 Disabled sysctl net.cfil → 内容过滤器 active_count: 0 systemextensionsctl list → 那个商业 VPN 的系统扩展处于 activated waiting for user（半激活） 系统日志里发现 OpenVPN Connect 进程自己发的 TCP SYN 也没有回应（SYN in/out: 0/11），但同一台机器上 curl、Python 的 UDP socket 都正常 —— 这是针对单个进程的现象 机器上还跑着企业 VDI 客户端（root 权限的网络服务）、远程控制软件、四个残留的 utun 接口 到这里我叫停了 AI。它给出的候选根因都有道理，但每验证一个要五分钟，而我们的目标是\u0026quot;让 VPN 通\u0026quot;，不是\u0026quot;给这台机器写病历\u0026quot;。\n人做的决策：让她把网络切到手机热点（同时 Wi-Fi 还连着我家、但那个 Wi-Fi 没网关），用 DDNS 域名走公网进来 —— 这才是她日常真正的使用场景。切完之后：\nEVENT: CONNECTED vpn@\u0026lt;ddns域名\u0026gt;:\u0026lt;VPN端口\u0026gt; via /UDP on utun4/172.x.y.2/ 通了。 EPIPE 的最终根因没有钉死（候选：半激活的系统扩展残留、VDI 客户端的网络钩子、en0 无网关时 ovpnagent 计算 bypass route 出错）。这里诚实地记一笔：不是每个故障都值得追到底，换链路后消失是一个足够的工程结论。\n3.5 教训小结 日志里的\u0026quot;超时\u0026quot;和\u0026quot;Broken pipe\u0026quot;是两种完全不同的失败：前者包出去了没回应，后者包根本没出去。看清这一点能省一半时间。 客户端 GUI 有自己的状态库时，改文件不等于改配置。 一台机器上同时装着 3 个会接管网络栈的软件（商业 VPN、代理、VDI），任何一个的残留都可能拦别人的包。排障前先 scutil --nc list + ifconfig | grep utun。 在服务端也要看：NAS 上 netstat -lnu、openvpn.conf、连接日志（synovpnlog.db）三样合起来才能说\u0026quot;服务端没问题\u0026quot;。 四、第二阶段：仅这一台设备不走 OpenWrt VPN 通了之后，新需求：这位朋友的流量不要经过旁路网关 OpenWrt，直接从主路由出去；其他 VPN 用户保持不变。\n4.1 为什么不能直接做 策略路由按源地址匹配。现在所有人共用一个账号（duplicate-cn），拿到的 IP 是动态的 .2/.3/.4，从 NAS 看根本分不清谁是谁。所以第一步是让这台设备有一个稳定的源地址。\n4.2 三个动作 ① 专属账号 + client-config-dir 固定 IP\nDSM 里给她建一个独立用户（授予 VPN Server 权限），然后在 OpenVPN 配置里启用 per-client 配置目录：\n# /usr/syno/etc/packages/VPNCenter/openvpn/openvpn.conf 追加 client-config-dir /usr/syno/etc/packages/VPNCenter/openvpn/ccd # ccd/\u0026lt;用户名\u0026gt; ifconfig-push 172.x.y.200 255.255.255.0 push-remove dhcp-option # 不给她推旁路网关的 DNS push \u0026#34;dhcp-option DNS 192.168.1.1\u0026#34; # 改推主路由 两个细节：\n用 push-remove 而不是 push-reset。push-reset 会把服务端自动生成的 topology subnet、route-gateway、ping/ping-restart 一起清掉，客户端会拿到一个残缺的配置。push-remove 只精确摘掉一项（OpenVPN ≥ 2.4）。 ccd 文件名 = 客户端 CN。因为服务端开了 username-as-common-name，所以文件名就是用户名。 ② 一条排在前面的 ip rule\nip rule add from 172.x.y.200/32 lookup main pref 0 0: from all lookup local 0: from 172.x.y.200 lookup main ← 新加，查 main 表 → default via 192.168.1.1 1: from 172.x.y.0/24 lookup 100 ← 原有，其他 VPN 用户 → OpenWrt 为什么是 pref 0：Linux 内核插入同优先级规则时，排在已有同优先级规则之后（fib_nl_newrule 找的是第一条 pref \u0026gt; new 的位置）。写 pref 1 会落在那条 /24 后面，永远匹配不到；写 pref 0 则正好落在 local 之后、/24 之前。\n回程不用担心：NAS 回给 .200 的包源地址是 172.x.y.1，仍命中 /24 规则查 table 100，而 table 100 里有 172.x.y.0/24 dev tun0（上一篇就是修这条的）。\n③ 持久化\nip rule 重启 NAS 就没了。我家 NAS 上已经有一个每 5 分钟跑一次、维护 table 100 的 DSM 定时任务（上一篇的产物），最合理的做法是把新规则加进同一个脚本，而不是再开一个。\nDSM 的定时任务不在 crontab 里，是存在它自己的库里的。用 synowebapi 读出来、改、写回去：\nsynowebapi --exec api=SYNO.Core.TaskScheduler method=get version=4 id=7 # ...在 script 字段里插入下面这段，再 method=set 写回 DIRECT_HOSTS=\u0026#34;172.x.y.200\u0026#34; for h in $DIRECT_HOSTS; do ip rule list | grep -q \u0026#34;from ${h} lookup main\u0026#34; || ip rule add from \u0026#34;${h}/32\u0026#34; lookup main pref 0 done 写回后立刻 get 一次核对字段确实在 —— 这类\u0026quot;写了但没生效\u0026quot;的坑上一篇踩过。\n4.3 一个顺手的技巧：不登录也能验证密码 朋友不确定新账号的密码。DSM 的登录 API 对\u0026quot;密码错\u0026quot;和\u0026quot;密码对但没有登录权限\u0026quot;返回不同的错误码：\ncurl -G http://127.0.0.1:5000/webapi/entry.cgi \\ --data-urlencode api=SYNO.API.Auth --data-urlencode version=6 \\ --data-urlencode method=login --data-urlencode account=\u0026lt;用户\u0026gt; \\ --data-urlencode passwd=\u0026lt;待验证密码\u0026gt; # {\u0026#34;error\u0026#34;:{\u0026#34;code\u0026#34;:402}} ← 密码对，只是这个账号没有 DSM 桌面权限 # {\u0026#34;error\u0026#34;:{\u0026#34;code\u0026#34;:400}} ← 账号或密码错 一次请求就知道答案，不用去改密码。\n4.4 完成后的拓扑 朋友的 MacBook │ 底层：手机热点（或任意外网） │ OpenVPN / UDP \u0026lt;VPN端口\u0026gt; → \u0026lt;ddns域名\u0026gt;，专属账号 ▼ utun 172.x.y.200/24 ──────────────► NAS tun0 172.x.y.1 │ ip rule ───────────┤ pref 0 from .200 → main ──────►│ default via 192.168.1.1 ──► 主路由直出 ← 她 pref 1 from /24 → table 100 ─►│ default via 192.168.1.254 ► OpenWrt 分流 ← 其他人 五、第三阶段：给不懂命令行的人交付工具 她的真实需求其实只有一句话：\u0026ldquo;我要能用 Gemini。\u0026rdquo; 出境流量在我家是 OpenWrt 上的一个 SOCKS 端口（192.168.1.254:1080）负责的，所以要做的是：让她的浏览器在需要时走这个 SOCKS。\n方案迭代了三版，每一版的否决理由都值得记。\n5.1 第一版：系统代理开关（被否） 一个 AppleScript 小程序，双击后用 networksetup -setsocksfirewallproxy 把系统 SOCKS 代理指过去，弹一个窗；点\u0026quot;关闭并退出\u0026quot;或 ⌘Q 时恢复。\n做对的部分：\n开启前把每个网络服务的原始值（启用状态/服务器/端口）记到 ~/Library/Application Support/\u0026lt;工具\u0026gt;/state.psv，退出时逐项还原，并在她机器上实测 on→off 一轮，四个服务全部精确回到 Enabled: No / Server 空 / Port 0。 上次被强杀没恢复的话，下次启动不覆盖已有记录，退出仍回到最初状态。 启动先 nc -z 探测代理可达，不通就提示\u0026quot;先连 Wi-Fi 或 VPN\u0026quot;，不改任何东西。 踩的坑：\nnetworksetup -setsocksfirewallproxy \u0026lt;svc\u0026gt; \u0026quot;\u0026quot; 0 会把服务器设成字面量 0；要清空得写 \u0026quot;\u0026quot; \u0026quot;\u0026quot;。 状态文件用 Tab 分隔，IFS=$'\\t' read 会把连续的 Tab 合并（Tab 是空白类 IFS），空字段导致列错位。换成 | 分隔。 AppleScript 里 POSIX path of (path to me) \u0026amp; \u0026quot;…\u0026quot; 偶发 -1700（can\u0026rsquo;t make … into type），拆成两句赋值就稳了。 osacompile 生成的小程序里带了一个 Assets.car，macOS 15 优先从它读图标，换 applet.icns 不生效；删掉 Assets.car、去掉 Info.plist 的 CFBundleIconName、重新签名才行。 被否的理由（用户原话）：\u0026quot;太危险了。\u0026quot; 系统级代理影响所有应用，忘了关、或者小程序崩了，整台机器的流量就都在走那个口。对一个不看菜单栏的用户，这个风险不对称。\n5.2 第二版：拆成\u0026quot;代理开关\u0026quot;和\u0026quot;Gemini\u0026quot; 代理开关保留，做成 VPN 工具那种小白模式：开启 → 自动跑一轮测试 → 弹窗显示\n✅ Google 可达，延迟 580 ms ⬇️ 下载速度约 18.9 Mbps 🌍 出口：US Los Angeles [重新测试] [关闭代理并退出] Gemini 单独一个图标，双击时检查系统代理没开就先拉起开关、等它开好，再开浏览器。但这没有解决\u0026quot;系统代理\u0026quot;本身的风险，只是把它藏得更深了。\n5.3 第三版：Gemini 专用的隔离 Chrome（最终） 正确的模型是：代理只作用于一个进程，且这个进程的生命周期和窗口一致。 Chrome 恰好都能做到：\n\u0026#34;/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\u0026#34; \\ --user-data-dir=\u0026#34;$HOME/Library/Application Support/GeminiChrome\u0026#34; \\ # 独立 profile，和她日常的 Chrome 互不干扰 --proxy-server=\u0026#34;socks5://192.168.1.254:1080\u0026#34; \\ # 只有这个实例走代理 --remote-debugging-port=9333 \\ # 给看门狗用 --no-first-run --no-default-browser-check --disable-sync \\ --app=https://gemini.google.com/app # app 模式，无地址栏 三个要点：\n系统代理一个字节都不碰。 scutil --proxy 全程 SOCKSEnable: 0。 DNS 不泄漏。 Chrome 对 socks5:// 代理是把域名交给代理端解析的。一开始还加了 --host-resolver-rules=\u0026quot;MAP * ~NOTFOUND, EXCLUDE 192.168.1.254\u0026quot; 做双保险，但 Chrome 会为它弹\u0026quot;不受支持的命令行标记\u0026quot;横幅，去掉了。 关窗即退出。 macOS 上 Chrome 关掉最后一个窗口不会退出进程。解决办法是一个看门狗：每 2 秒查 http://127.0.0.1:9333/json/list，\u0026quot;type\u0026quot;: \u0026quot;page\u0026quot; 数量为 0 就 kill 这个实例。不需要辅助功能权限，不需要 UI 脚本。 端到端验证的方式也值得记：不是\u0026quot;看起来关了\u0026quot;，而是通过 DevTools 协议模拟关窗（/json/close/\u0026lt;id\u0026gt;），然后确认进程 4 秒内消失、launcher.pid 被清理。\n5.4 最终桌面 图标 做什么 Gemini（官方图标） 隔离的代理 Chrome；关窗 = 一切消失 代理开关（盾牌+锁） 可选：其他应用也要走代理时用；带测速；退出恢复 远程桌面 VDI（客户端原版图标） Safari 打开企业 VDI 登录页，Touch ID 自动填密码 OpenVPN Connect / 商业 VPN 替身 图标来源：Gemini 用 App Store 的 1024px 原图按 macOS 圆角规范裁；VDI 直接拿客户端 bundle 里的 .icns；代理开关用 PIL 画了一个盾牌 + 锁。\n六、有了 AI 以后，变化在哪 这一节是写这篇的真正原因。上面的技术内容放在三年前我也能做出来，但我不会去做 —— 尤其是第四、五章。变化不在\u0026quot;能不能\u0026quot;，在边际成本。\n6.1 排障半径：从\u0026quot;我的机器\u0026quot;到\u0026quot;我能 ssh 到的一切\u0026quot; 整个会话里 AI 同时操作三台机器：我的 Mac（跑 agent）、她的 Mac（sshpass ssh）、NAS（ssh + sudo）。每一次\u0026quot;看客户端日志 → 看服务端监听 → 看 NAS 路由表 → 回到客户端\u0026quot;的跨机器往返，以前是我在三个终端窗口之间切换、复制粘贴、在脑子里拼图；现在是一次工具调用并行发出，结果并排回来。\n第三章里客户端说 Broken pipe、同时服务端确认 UDP \u0026lt;VPN端口\u0026gt; 在听、同时我自己的机器 nc 测端口 —— 三个视角一分钟内对齐。并行是最直接的加速。\n6.2 外置记忆：AI 比我更记得我家的网络 会话一开始，AI 先去查了我的记忆库（一个跨客户端共享的 MCP memory），搜到了四条相关记录：\n服务端是 UDP \u0026lt;VPN端口\u0026gt;，不是 TCP table 100 的设计与\u0026quot;回程路由\u0026quot;的坑 DDNS 在家 hairpin 不通 DSM 定时任务存在两个地方，只查一个会误判 这四条每一条都在这次派上了用场。第一层故障几乎是秒判；第四章改定时任务时直接走了正确的存储位置。这些东西我自己写过、也在博客里发过，但如果是我亲手排查，大概率要重新想一遍\u0026quot;服务端到底是 UDP 还是 TCP 来着\u0026quot;。\n博客顶上那句 The brain has cache expiration too 是写给未来的我的；现在多了一个读者，而且它真的读。\n6.3 假设-证据循环更快，错得也更快 —— 人负责叫停 3.4 节是这次最值得反思的段落。AI 判断\u0026quot;另一个 VPN 的 kill-switch 是根因\u0026quot;—— 证据充分（默认路由被抢、utun4 在线），结论合理，但断开后故障依旧。它接着给出下一个候选、再下一个：pf、内容过滤器、系统扩展、NECP、flow divert、VDI 客户端……每一个都有对应的验证命令，每一个都能再花五分钟。\n它不会自己停。这不是缺陷，是分工：agent 的默认模式是穷尽假设空间，而\u0026quot;这个问题值不值得继续追\u0026quot;是目标层面的判断，属于人。我叫停之后换了链路，问题消失，我们接受了\u0026quot;根因未钉死\u0026quot;这个结论继续往前走。\n反过来说，如果我一个人排查，我可能在第二个候选就放弃了 —— 也可能在第五个候选上耗掉一晚上。AI 把\u0026quot;继续追\u0026quot;的成本压得很低，这让\u0026quot;停不停\u0026quot;这个决策变得更重要、也更清晰。\n6.4 需求变更的成本趋近于一句话 回看这次的需求流：\n\u0026ldquo;帮我看下为什么连不上\u0026rdquo; \u0026ldquo;改成公网 IP\u0026rdquo; \u0026ldquo;现在用的是手机热点，用 DDNS 域名解决\u0026rdquo; \u0026ldquo;有办法不要流量往 OpenWrt 发吗？仅这个设备\u0026rdquo; \u0026ldquo;用已有的 ho 用户\u0026rdquo; \u0026ldquo;写一个工具，点击设置代理，关闭恢复如初\u0026rdquo; \u0026ldquo;把 OpenVPN 快捷方式放桌面上\u0026rdquo; \u0026ldquo;拆成代理开关和 Gemini 两个\u0026rdquo; \u0026ldquo;图标换成真的\u0026rdquo; \u0026ldquo;太危险了，做成 Chrome app，关闭就清掉\u0026rdquo; \u0026ldquo;去掉那个横幅\u0026rdquo; 十一次变更，每次一句话，每次都在几分钟内落地并验证。以前这种对话发生在我和自己之间，落地的成本是\u0026quot;再花一个晚上\u0026quot;，于是大部分变更根本不会发生 —— 我会在第 1 步修好 VPN 之后说\u0026quot;行了，能用了\u0026quot;。\nAI 没有改变我想要什么，改变的是\u0026quot;想要\u0026quot;和\u0026quot;得到\u0026quot;之间的距离。 第 4、6、9、10 步是以前绝对不会为朋友做的事。\n6.5 从\u0026quot;修好\u0026quot;到\u0026quot;交付\u0026quot; 以前帮朋友修完东西，交付物是一段口头说明或一份 README。这次交付物是三个带图标的 .app，而且每一个都在她的机器上做了真实的端到端验证：\n代理开关：on → 查 scutil --proxy → off → 逐项比对四个服务恢复原值 Gemini：启动 → 查 DevTools 目标列表里有 gemini.google.com/app → 模拟关窗 → 确认进程退出、系统代理从未变过 VDI：启动 → 读 Safari 前台标签 URL 验证不是额外步骤，是每一步的一部分。 这是 agent 工作方式里我最想保留到自己手工操作中的习惯。\n6.6 代价：权限、边界与备份 诚实地列一下这次 AI 拿到了什么：她 Mac 的登录密码、NAS 的 sudo 密码、她新账号的密码、三台机器的 root 级操作。\n对应的约束也要写清楚，这些是人来定的：\n改 NAS 配置前先 cp openvpn.conf openvpn.conf.bak-\u0026lt;时间戳\u0026gt;；改她的 .ovpn 前先备份到桌面。 所有写入 NAS 的脚本必须幂等（grep -q || ip rule add），可以反复跑。 有不可逆或影响他人的操作时（建账号、重启 VPN Server 会踢掉现有用户），先问再做。这次有两处它停下来问了，都是对的。 会话结束后，密码文件删掉。 AI 让\u0026quot;做\u0026quot;变得便宜，也让\u0026quot;做错\u0026quot;变得便宜。备份和幂等不是形式，是这套工作方式的安全带。\n6.7 时间线 时刻 事件 20:59 她第一次点 Connect，TCP 超时 21:04 定位 proto tcp-client，改 UDP + 内网 remote 21:12 发现客户端缓存，重新导入 21:15 Broken pipe；发现另一个 VPN 在线 21:20 换手机热点 21:22 CONNECTED 21:28 ccd + ip rule + 定时任务写回，VPN Server 重启 21:31 以专属账号连接，拿到 .200 21:36 第一版代理开关上线 21:44 桌面替身 21:54 三个工具 + 图标 22:00 Chrome 隔离实例版 Gemini 22:05 去掉横幅，收工 66 分钟。其中真正卡住的只有 3.4 节那十分钟。\n七、可复用的排查清单 OpenVPN 客户端连不上，按这个顺序：\n客户端日志：是 poll timeout（包出去没回应）还是 send exception（包没出去）？ 服务端：netstat -lnu / -lnt 确认协议和端口；配置文件里的 proto / port。 客户端 .ovpn 的 proto 是否与服务端一致。 客户端和服务端是否在同一个 LAN（curl ifconfig.me 对比公网 IP）→ hairpin。 如果改了 .ovpn 没效果：GUI 客户端是否有自己的状态缓存，需要删除重导入。 scutil --nc list、ifconfig | grep utun、netstat -rn | grep default：有没有别的 VPN/代理在线或残留。 换一条网络链路（热点）再试，区分\u0026quot;这台机器的问题\u0026quot;和\u0026quot;这条链路的问题\u0026quot;。 NAS 侧单设备例外，按这个顺序：\n独立账号 + client-config-dir + ifconfig-push 固定 IP。 push-remove 精确摘掉不想推的选项，不要 push-reset。 ip rule add from \u0026lt;ip\u0026gt;/32 lookup main pref 0（注意同 pref 排序规则）。 写进已有的自愈定时任务；写回后 get 一次核对。 提醒未来的自己：DSM 界面一旦保存 VPN 设置，client-config-dir 会被抹掉。 给非技术用户交付 macOS 小工具：\n能不碰系统级设置就不碰；优先给单个进程传参数。 生命周期要和用户看得见的东西（窗口）绑定。 每一个\u0026quot;恢复原状\u0026quot;都要在目标机器上实测一轮 on→off 并比对。 osacompile 的图标：删 Assets.car、去 CFBundleIconName、codesign --force --deep -s -。 八、结语 这次最大的收获不是修好了一个 VPN。是清楚地看到了一种新的工作形态：人提供意图、约束和叫停，agent 提供并行执行、外置记忆和逐步验证。 十一句话，三台机器，一个小时，交付物是朋友能双击的东西。\n三年前同样的下午，结局大概率是：\u0026ldquo;我把 proto 改成 udp 了，你回家再试试。\u0026rdquo;\n","permalink":"https://jiwenkang.com/posts/2026/ai-assisted-vpn-troubleshooting/","summary":"\u003cblockquote\u003e\n\u003cp\u003e朋友坐在我家客厅，MacBook 上的 OpenVPN Connect 死活连不上我 NAS 的 VPN。我没碰她的电脑，只把一条 \u003ccode\u003essh\u003c/code\u003e 命令丢给了 AI agent（Claude Code）。\u003c/p\u003e\n\u003cp\u003e一个小时后：VPN 通了；她的设备在 NAS 上有了专属的出口策略；桌面上多了三个带图标的小程序，双击就能用。\u003c/p\u003e\n\u003cp\u003e这篇文章记两件事：\u003cstrong\u003e故障本身是怎么一层层剥开的\u003c/strong\u003e，以及\u003cstrong\u003e有了 AI 之后，这类工作和以前到底哪里不一样了\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e文中所有公网 IP、密码、商业服务商品牌、个人域名与真实用户名均已抹去或改写，只保留可复用的结构。内网地址与真实环境一致（\u003ccode\u003e192.168.1.0/24\u003c/code\u003e，网段约定同\u003ca href=\"/posts/2026/home-network-topology/\"\u003e前两篇\u003c/a\u003e），VPN 段记作 \u003ccode\u003e172.x.y.0/24\u003c/code\u003e，对外端口记作 \u003ccode\u003e\u0026lt;VPN端口\u0026gt;\u003c/code\u003e。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"远程给朋友修 VPN：有了 AI 之后，一次运维排障是怎么进行的"},{"content":" 上一篇《家庭网络分层拓扑：从远端笔记本到双出口的完整链路》讲了这套拓扑该怎么搭。这一篇讲它怎么坏的——一个只错了一行、却让人绕了整整一晚的故障。\n最讽刺的地方在于：上一篇里已经写对了那一行，落地时却没抄进去。 而上一篇紧接着的另一句解释，恰好让那一行看起来可以省。\n文中所有公网 IP、商业服务商品牌、个人域名与主机名均已抹去，只保留可复用的结构。\n一、TL;DR VPN 客户端可以正常上网、可以访问家里任何一台内网机器，唯独访问不了 VPN 网关 172.x.y.1 本身——ping 不通，全端口静默。\n排查过程中先后怀疑并证伪了：客户端防火墙、DSM 防火墙（把整个防火墙关掉照样不通）、\u0026ldquo;网关和 NAS 不是同一台机器\u0026rdquo;。\n真正的根因是 NAS 上这张表：\nip rule: 1: from 172.x.y.0/24 lookup 100 table 100: default via 192.168.1.254 dev ovs_eth0 ← 只有这一条 NAS 回复 VPN 客户端时，回包的源地址是 172.x.y.1，它落在 172.x.y.0/24 里。 于是回包命中这条规则、去查 table 100、只匹配到那条 default，被送去了旁路网关。而旁路网关没有回 VPN 网段的路由，包就此消失。\n请求进得来，回包出不去。修复是补回上一篇里就写着的那条回程路由：\nip route replace 172.x.y.0/24 dev tun0 src 172.x.y.1 table 100 二、症状 远端笔记本通过 OpenVPN 拨回家，拿到 172.x.y.2/24，网关 172.x.y.1。一切正常：\n默认路由走隧道，出口 IP 是境外节点 ✅ 内网 192.168.1.0/24 全通，NAS 的 DSM、SSH、SMB 都能用 ✅ 唯独 ping 172.x.y.1 100% 丢包，22/80/443/5000/5001 全部无响应 ❌ 隧道本身健康得很——tun0 上已经跑了 1.4 GB。就是网关这一个地址，像不存在一样。\n三、排查：三个被证伪的假设 这次排查真正的价值不在结论，而在排除的过程。我依次相信过三个假设，全是错的。\n3.1 假设一：客户端侧的问题 第一反应总是查本机。全部排除：\n检查项 结果 tun0 状态 UP，累计 RX 1.4 GB / TX 838 MB 默认路由 ip route get 8.8.8.8 → via 172.x.y.1 dev tun0 本机防火墙 ufw-user-input 里有 iifname \u0026quot;tun0\u0026quot; accept，已放行 346 个包 rp_filter = 2（loose），不会丢反向路径 tcpdump -i tun0 确认 ICMP echo request 确实从客户端发出去了，零回包。问题在对端。\n3.2 假设二：NAS 防火墙 —— 证伪 这是最自然的猜测，而且证据看起来严丝合缝：群晖 DSM 的防火墙规则是按网络界面分档的，VPN 接口那一栏如果没有放行规则就落到默认拒绝；而转发流量走 FORWARD + NAT，不受 INPUT 规则影响——完美解释了\u0026quot;能上网但访问不了网关本身\u0026quot;。\n于是把整个防火墙关掉。\n结果：一模一样，还是不通。\n这个假设死得干干净净。后来上机验尸也确认了它从头到尾就是无辜的：\nChain INPUT (policy ACCEPT) Chain FORWARD (policy ACCEPT) Chain OUTPUT (policy ACCEPT) Chain DOS_PROTECT (1 references) DROP icmp -- ovs_eth0 * ... ← 全部限定 in ovs_eth0，tun0 流量根本不匹配 DROP tcp -- ovs_eth0 * ... Chain MAILSERVER_PLUS (1 references) DROP tcp -- * * multiport dports 5230,5252,8500:8507,... ← 只 DROP 邮件端口 三条主链全是 policy ACCEPT，两个自定义链一个只管 ovs_eth0、一个只管邮件端口。没有任何一条规则能碰到发往 172.x.y.1 的包。\n教训一：一个假设能解释所有已知现象，不代表它是对的。 它只是还没被证伪而已。要证伪它，就得去做那个能区分真假的实验——这里就是\u0026quot;把防火墙整个关掉\u0026quot;，而不是继续在规则里微调。\n3.3 假设三：搞错了机器 —— 证伪 既然 NAS 的 LAN 地址 192.168.1.4 一切正常，而 172.x.y.1 全死，那会不会它俩根本不是同一台机器？也许 VPN 服务跑在别的设备上，我一直在错误的机器上关防火墙。\n用 TTL 判定：\n$ ping -c 2 -t 1 192.168.1.4 64 bytes from 192.168.1.4: icmp_seq=1 ttl=64 time=7.37 ms ← TTL=1 就到了 $ ping -c 1 192.168.1.244 64 bytes from 192.168.1.244: icmp_seq=1 ttl=63 time=12.2 ms ← 对照：后面还有一跳 TTL=1 的包穿不过任何路由器（路由器会把它减到 0 然后丢弃）。它能被 192.168.1.4 收到并回复，只有一种解释：这个地址就在隧道的对端本机上。假设三出局。\n3.4 转折点一：filtered 还是 closed？ /dev/tcp 这种土办法只能告诉你\u0026quot;连不上\u0026quot;，区分不了被丢弃和没人监听。nmap 可以：\n$ nmap -Pn --reason -sS -p 22,80,443,5000,8080 172.x.y.1 22/tcp filtered ssh no-response 80/tcp filtered http no-response 443/tcp filtered https no-response 5000/tcp filtered upnp no-response 8080/tcp filtered http-proxy no-response $ nmap -Pn --reason -sS -p 22,80,443,5000,8080 192.168.1.4 # 同一台机器 22/tcp open ssh syn-ack ttl 64 80/tcp open http syn-ack ttl 64 443/tcp open https syn-ack ttl 64 5000/tcp open upnp syn-ack ttl 64 8080/tcp closed http-proxy reset ttl 64 ← 注意这一格 192.168.1.4:8080 回了 RST。这说明内核在正常处理发往那个地址的包，没监听的端口就规规矩矩拒绝。\n而 172.x.y.1 上，连 22/80/443/5000 这些已经证明开着的端口都零响应。sshd 和 DSM 都是 bind 在 0.0.0.0 上的，只要包带着一个本机地址进了内核，它们必然应答。\n结论：包没进到 TCP 协议栈。\n3.5 转折点二：在服务端抓包 到这一步只能上 NAS 了。同时 ping 两个地址，在 NAS 的 tun0 上抓：\n$ tcpdump -nli tun0 \u0026#34;icmp or tcp port 5000\u0026#34; 172.x.y.2 \u0026gt; 172.x.y.1: ICMP echo request, seq 1 ← 到了 172.x.y.2 \u0026gt; 172.x.y.1: ICMP echo request, seq 2 ← 到了 172.x.y.2 \u0026gt; 172.x.y.1: ICMP echo request, seq 3 ← 到了，一个回包都没有 172.x.y.2.59692 \u0026gt; 172.x.y.1.5000: Flags [S] ← 到了 172.x.y.2.59692 \u0026gt; 172.x.y.1.5000: Flags [S] ← 到了，没有 SYN-ACK 172.x.y.2 \u0026gt; 192.168.1.4: ICMP echo request, seq 1 192.168.1.4 \u0026gt; 172.x.y.2: ICMP echo reply, seq 1 ← 同一接口，秒回 172.x.y.2.40308 \u0026gt; 192.168.1.4.5000: Flags [S] 192.168.1.4.5000 \u0026gt; 172.x.y.2: Flags [S.] ← SYN-ACK 请求包完好无损地到达了内核，OpenVPN 交付无误，iptables 是 ACCEPT，但内核一声不吭。\n这一刻方向变了：问题不在入方向，在出方向。\n3.6 定位：ip route get Linux 的路由查找是可以直接问的。把三种情况并排一比，答案就跳出来了：\n# 回包（源地址 = VPN 网关自己） $ ip route get 172.x.y.2 from 172.x.y.1 172.x.y.2 from 172.x.y.1 via 192.168.1.254 dev ovs_eth0 table 100 ← 走局域网了！ # 对照：源地址 = NAS 的 LAN 地址 $ ip route get 172.x.y.2 from 192.168.1.4 172.x.y.2 from 192.168.1.4 dev tun0 ← 正常 # 对照：不指定源地址 $ ip route get 172.x.y.2 172.x.y.2 dev tun0 src 172.x.y.1 ← 正常 找到了。\n四、根因 4.1 机制 上一篇里，为了让 VPN 客户端享受和家里设备一致的分流体验，做了源地址策略路由：凡是源地址属于 VPN 网段的流量，默认路由换成旁路网关。\n问题在于，NAS 有两个地址：LAN 侧 192.168.1.4，以及 tun0 上的 172.x.y.1。当它回复一个发往 172.x.y.1 的请求时，回包的源地址必须是 172.x.y.1（否则四元组对不上）。而 172.x.y.1 就在 172.x.y.0/24 里——它命中了那条规则：\n回包 src=172.x.y.1 dst=172.x.y.2 → ip rule 1: from 172.x.y.0/24 ✓ 命中 → 查 table 100 → 表里只有 default via 192.168.1.254 → 回包被从 ovs_eth0 发给旁路网关 → 旁路网关没有回 172.x.y.0/24 的路由 → 丢弃 再往深一层：策略路由表一旦被规则命中，就自成体系了。 main 表里那条 172.x.y.0/24 dev tun0 完全帮不上忙，因为查找压根不会走到 main。而 table 100 里只有一条默认路由——一张只有 default 的路由表，意味着\u0026quot;任何目的地址都往下一跳扔\u0026quot;，包括那些本该直连的目的地。\n4.2 真正的教训：文档是对的，落地漏了一行 这才是这次故障最值得记的部分。\n上一篇原文给的命令是两条：\n# 1. 独立路由表：VPN 客户端的\u0026#34;默认路由\u0026#34;指向旁路网关（而不是上游网关） ip route add default via 192.168.1.254 dev \u0026lt;内网接口\u0026gt; table 100 ip route add 172.x.y.0/24 dev tun0 table 100 # 回程路由也要在这张表里 第二行就是修复本身。 它当时就写在那里，还带着注释。\n而实际落地到 NAS 开机任务里的脚本，是这样的：\n#!/bin/sh sleep 30 ip route replace default via 192.168.1.254 table 100 ip rule show | grep -q \u0026#34;from 172.x.y.0/24 lookup 100\u0026#34; || ip rule add from 172.x.y.0/24 table 100 回程路由那一行没了。\n为什么会漏？因为上一篇紧接着的解释是这么写的：\n命中 from 172.x.y.0/24 的（= 公司笔记本），默认路由被替换成\u0026quot;下一跳旁路网关\u0026quot;。 NAS 自己的流量源地址是内网地址，不命中，继续查 main 表直连国内——NAS 的下载、备份、内网服务完全不受影响。 这段话让人相信：table 100 是给客户端用的，NAS 自己永远不会查它。 既然如此，往一张\u0026quot;客户端专用表\u0026quot;里放一条 VPN 网段的路由，看起来就像是冗余——客户端本来就在那个网段里，它发给同网段的包根本不会走三层。于是抄写脚本时顺手省掉了。\n问题是那句话不完整：NAS 自己的流量里，有一类的源地址不是内网地址，而是 172.x.y.1。这类流量恰好就是\u0026quot;所有对 VPN 客户端的应答\u0026quot;。\n教训二：一条配置为什么存在，比这条配置是什么更重要。 上一篇写了\u0026quot;回程路由也要在这张表里\u0026quot;，但没写\u0026quot;给谁回程\u0026quot;。注释解释了 What，没解释 Why，于是在落地时被当成冗余删掉了。凡是看起来冗余的配置，要么补上它存在的理由，要么它迟早会被某个人（包括三个月后的你自己）删掉。\n教训三：策略路由表不会 fallback 到 main。 一张只有 default 的表会把所有流量都往下一跳扔，包括本机的应答。凡是这张表的使用者需要访问的直连网段，都得显式写进去。\n4.3 为什么 192.168.1.4 一直是好的 因为它命中的是另一条规则（DSM 自己维护的）：\n7: from 192.168.1.4 lookup ovs_eth0-table 而 ovs_eth0-table 里只有 192.168.1.0/24，匹配不到 172.x.y.2。策略路由的规则链在\u0026quot;查表未命中\u0026quot;时会继续往下走，于是落到 32766: from all lookup main，main 表里有 172.x.y.0/24 dev tun0 —— 正确出隧道。\n同一台机器，两个源地址，两条完全不同的命运。\n4.4 最精彩的细节：它能主动发包，却收不了包 排查中期有个现象一度让我判断失误——172.x.y.1 能给客户端发 ICMP：\n$ ping -t 1 8.8.8.8 From 172.x.y.1 icmp_seq=1 Time to live exceeded ← 它活着，而且能发包给我 TTL 耗尽时，NAS 用 172.x.y.1 作源地址发回了 time-exceeded，一路顺利到达客户端。既然它能发，为什么 echo reply 发不出来？\n区别在于源地址是什么时候被绑定的：\nICMP 差错报文（time-exceeded）：触发它的原包目的地是 8.8.8.8，不是本机。在 icmp_errors_use_inbound_ifaddr=0（默认）下，内核先做路由查找、再从出接口挑源地址。查找时源地址是未指定的，from 172.x.y.0/24 这条按源匹配的规则自然不命中 → 落到 main 表 → 出 tun0 → 然后才把源地址填成出接口的主地址 172.x.y.1。✅\nICMP echo reply / TCP SYN-ACK：源地址在路由查找之前就被钉死了（必须等于请求的目的地址 172.x.y.1），于是规则命中 → table 100 → 走岔。❌\n这正是 ip route get 172.x.y.2（不带 from）返回 dev tun0、而 ip route get 172.x.y.2 from 172.x.y.1 返回 via 192.168.1.254 的原因。\n教训四：出入不对称是源地址策略路由的指纹。 一个地址\u0026quot;能主动发、不能被访问\u0026quot;，几乎必然是出方向的源地址路由问题，而不是入方向的过滤问题。下次先跑 ip route get \u0026lt;对端\u0026gt; from \u0026lt;本机该地址\u0026gt;，能省几小时。\n五、修复 table 100 必须补全直连路由：\nip route replace 172.x.y.0/24 dev tun0 src 172.x.y.1 table 100 # 关键 ip route replace 192.168.1.0/24 dev ovs_eth0 src 192.168.1.4 table 100 # 建议 ip route replace default via 192.168.1.254 dev ovs_eth0 table 100 第一条是修复本身。第二条是顺手优化：在此之前，VPN 客户端访问家里其他机器要先绕到旁路网关再拐回来（实测多一跳，回包 ttl=63），还要白白穿一遍透明代理的 netfilter 路径；加上直连路由后走直连。\n完整的落地脚本（群晖「控制面板 → 任务计划 → 用户定义的脚本」，用户选 root）：\n#!/bin/sh VPN_NET=172.x.y.0/24 TUN_IP=172.x.y.1 TUN_DEV=tun0 LAN_NET=192.168.1.0/24 LAN_IP=192.168.1.4 LAN_DEV=ovs_eth0 PROXY_GW=192.168.1.254 RT=100 PREF=1 # ---- 不依赖 tun0，立刻可做 ---- ip route replace default via \u0026#34;$PROXY_GW\u0026#34; dev \u0026#34;$LAN_DEV\u0026#34; table \u0026#34;$RT\u0026#34; ip route replace \u0026#34;$LAN_NET\u0026#34; dev \u0026#34;$LAN_DEV\u0026#34; src \u0026#34;$LAN_IP\u0026#34; table \u0026#34;$RT\u0026#34; # 显式指定 pref。不写的话由内核自动分配，那是运气不是设计。 ip rule list | grep -q \u0026#34;from ${VPN_NET}\u0026#34; || ip rule add from \u0026#34;$VPN_NET\u0026#34; table \u0026#34;$RT\u0026#34; pref \u0026#34;$PREF\u0026#34; # ---- 依赖 tun0：等它真正带上地址，最多 60 秒 ---- # 不能用固定 sleep：早了 tun0 还没起，下面这条会失败；晚了纯属白等。 n=0 until ip addr show \u0026#34;$TUN_DEV\u0026#34; 2\u0026gt;/dev/null | grep -q \u0026#34;inet ${TUN_IP}/\u0026#34;; do n=$((n + 1)) [ \u0026#34;$n\u0026#34; -ge 60 ] \u0026amp;\u0026amp; exit 0 sleep 1 done # 修复点：回程路由。少了它，NAS 从 172.x.y.1 发出的回包会命中上面那条 rule， # 在 table 100 里只匹配到 default，被甩给旁路网关，再也回不到隧道。 ip route replace \u0026#34;$VPN_NET\u0026#34; dev \u0026#34;$TUN_DEV\u0026#34; src \u0026#34;$TUN_IP\u0026#34; table \u0026#34;$RT\u0026#34; ip route flush cache exit 0 相比最初那版，有几处值得单独说：\n改动 原因 补回 172.x.y.0/24 dev tun0 本文的全部内容 sleep 30 → 轮询等 tun0 带上地址 固定 sleep 是赌运气：早了 ip route ... dev tun0 src 172.x.y.1 直接失败（源地址还不是本机地址），晚了白等。改成轮询后，脚本才具备\u0026quot;可反复执行\u0026quot;的前提 ip rule add 显式写 pref 不写 pref 时由内核自动分配。我这次恰好拿到 1（排在其他规则之前），换个加载顺序落到别的位置，匹配行为就变了 补 exit 0 如果这脚本被用作 OpenVPN 的 route-up 钩子，返回非 0 会让 OpenVPN 直接退出 grep -q \u0026quot;from ${VPN_NET}\u0026quot; 只匹配源前缀 不匹配 lookup 部分。否则一旦给 table 100 在 /etc/iproute2/rt_tables 里起了名字，输出会从 lookup 100 变成 lookup \u0026lt;name\u0026gt;，幂等判断失效，重复执行会不断累积规则 验证：\n$ ping -c 3 172.x.y.1 3 packets transmitted, 3 received, 0% packet loss rtt min/avg/max/mdev = 8.150/8.496/9.049/0.395 ms $ ip route get 172.x.y.2 from 172.x.y.1 172.x.y.2 from 172.x.y.1 dev tun0 table 100 ← 留在隧道里了 防火墙重新启用后复测，不受影响。\n六、实测：route-up 在 server 模式下根本不触发 修好之后还剩一个问题：这条回程路由该挂在哪里才不会再丢？\nNAS 上原本挂着一个 OpenVPN 的 route-up 钩子脚本，内容正是那条回程路由命令。如果它可靠，那就够了，不需要别的。但故障发生时它加的路由并不在表里——所以它到底有没有被执行过，必须实测。\n实验设计 一次重启，同时回答两个问题：钩子有没有被调用、路由会不会自己回来。\n给 route-up.sh 插两条互相独立的埋点：写文件 + logger 进 syslog。任何一条留下记录，就证明它被执行过。 删掉 table 100 里的回程路由。 重启 VPN 服务。 每秒采样一次 table 100，连续 150 秒，记录路由首次出现的精确秒数。 因为重启会掐断隧道（而我正是从这条隧道连进 NAS 的），整个流程写成脚本用 setsid nohup 脱离终端执行，结果落盘，等隧道恢复后再回来取。\n结果 [01:21:15] del rc=0 ; table100 条数=2 ← 回程路由已删除 [01:21:15] \u0026gt;\u0026gt;\u0026gt; synopkg restart VPNCenter restart package [VPNCenter] successfully [01:22:21] \u0026lt;\u0026lt;\u0026lt; restart rc=0 ← 服务确实重启,openvpn 新 PID @ 01:22:00 [01:24:53] *** 150 秒内路由从未出现 *** --- route-up 自证日志 --- (空) --- syslog 中的埋点 --- (空) 两条独立埋点全部为空。而同一个脚本手工执行完全正常：\n[01:19:46] route-up FIRED dev=tun0 ifconfig_local=172.x.y.1 [01:19:46] route-up ip-route rc=0 所以不是脚本坏了，是 OpenVPN 压根没有调用它。\n原因大概率在于 --route-up 的语义是\u0026quot;路由添加完成后执行\u0026quot;。一个 --server 实例并不为自己添加客户端路由，这个钩子自然就没有触发点。文档里它看起来像个通用的\u0026quot;隧道起来后执行\u0026quot;钩子，实际上不是。\n教训五：没验证过的兜底，比没有兜底更危险。 这个脚本躺在那儿好几天，看起来像一层保险，实际从未执行过一次。它唯一的作用是让人误以为问题已经被兜住了——包括写它的人自己。任何自动恢复机制，都必须真的触发一次给你看。\n插曲：我自己也踩了一次同样的坑 测试过程中出现过一个当时无法解释的现象：另一轮 5 秒粒度的测试里，路由曾在重启后约 28 秒自己出现过一次。我当时排查了一圈，得出\u0026quot;没有任何东西在轮询\u0026quot;的结论，理由是这两条：\n查 DSM 的任务库 esynoscheduler.db，里面只有一个开机触发的任务； 把路由删掉后静置 30 秒观察，它没有回来。 两条都是错的。\n第一条错在只查了一半。群晖把任务分成两处存：事件触发的（开机/关机）在 esynoscheduler.db，而定时的在 /usr/syno/etc/synoschedule.d/\u0026lt;用户\u0026gt;/\u0026lt;id\u0026gt;.task。我只查了前者，于是把一个一直在跑的 5 分钟定时任务判成了\u0026quot;不存在\u0026quot;。\n第二条错在观测窗口比被观测周期还短。用 30 秒的观察去否定一个 5 分钟周期的进程，本来就不可能看见它。\n而那轮\u0026quot;150 秒内路由从未出现\u0026quot;的严格测试，同样栽在窗口上——采样在 01:24:53 结束，定时任务的下一个刻度是 01:25:00，差 7 秒。\n对上时间线，一切都吻合：\n时刻 事件 01:14:15 测试删除回程路由 01:15:00 定时任务触发，恢复路由 01:15:32 采样到\u0026quot;路由回来了\u0026quot; → 当时判为无法解释 01:21:15 严格测试删除回程路由 01:24:53 采样窗口结束，报告\u0026quot;150 秒内从未出现\u0026quot; 01:25:00 定时任务触发——比窗口晚 7 秒 教训六：负面结论比正面结论更难成立。 \u0026ldquo;我看到 X 发生了\u0026quot;只需要一次观测；\u0026ldquo;X 不会发生\u0026quot;却要求你的观测覆盖了它可能发生的全部时空。周期性行为的观测窗口必须长于它的周期，而\u0026quot;我在某处没找到\u0026quot;永远不等于\u0026quot;它不存在\u0026rdquo;——先问清楚还有没有别的地方要找。\nroute-up 的结论不受影响 上面这个错误没有动摇 route-up 的判定，因为那次判定不依赖于窗口长度：\nopenvpn 于 01:22:00 启动，采样从 01:22:21 开始。如果 route-up 在服务启动时触发过，路由在采样的第一秒就应该已经在表里了——而它全程缺失，两条独立埋点也全为空。定时任务的刻度在窗口之外，不构成干扰。\n顺带排除的其他可能：\n可能性 排除方式 表号撞车（DSM 也在用 100） /etc/iproute2/rt_tables 里只有 2/3/5/7/8，没有 100 别的脚本在写这张表 全盘 grep 172.x.y、VPN 套件启停脚本、synocrond 全部内置任务，均无结果 工程结论 route-up 这条路走不通，所以：\nVPN 服务重启会销毁重建 tun0，Linux 内核在网卡消失时会清掉所有引用它的路由——自定义表也不例外。 群晖的「触发的任务」只有开机/关机两种事件，没有\u0026quot;服务启动\u0026quot;钩子。 因此\u0026quot;开机跑一次\u0026quot;不够：只要在面板里改一次 VPN 设置触发重启，回程路由就没了，一直到下次重启 NAS 或手动补一次为止。 唯一的自愈手段是轮询：把第五节那个幂等脚本再注册一个「计划的任务 → 每天，重复间隔 5 分钟」。脚本本身设计成可反复执行就是为了这个。\n这一条是实测过的——按教训五的要求，让它真的触发一次给我看：\n[T+0] 删除 table 100 的回程路由 [T+140s] 路由自动恢复,ping 172.x.y.1 → 0% 丢包, 8.7ms 140 秒，正好落在下一个 5 分钟刻度上。\n那个从未执行过的 route-up.sh，连同配置里的 route-up 和 script-security 2 两行，已经一并删掉了。留着只会让下一个排查的人（很可能就是几个月后的自己）以为有兜底。\n七、可复用的排查清单 按这个顺序走，大部分\u0026quot;某个地址访问不了\u0026quot;的问题都能收敛：\n# 1. 包出去了吗？（客户端） tcpdump -ni \u0026lt;出接口\u0026gt; host \u0026lt;目标\u0026gt; # 2. 被丢弃还是没人监听？filtered vs closed 是完全不同的两件事 nmap -Pn --reason -sS -p \u0026lt;ports\u0026gt; \u0026lt;目标\u0026gt; # 3. 目标和你以为的是同一台机器吗？TTL=1 打不穿任何路由器 ping -t 1 \u0026lt;目标\u0026gt; # 4. 包到达对端内核了吗？（服务端） tcpdump -ni \u0026lt;入接口\u0026gt; host \u0026lt;源\u0026gt; # 到了却没回包 ⇒ 问题在出方向，别再查防火墙的 INPUT 了 # 5. 回包会往哪走？—— 决定性的一步 ip route get \u0026lt;对端\u0026gt; from \u0026lt;本机应答用的源地址\u0026gt; ip route get \u0026lt;对端\u0026gt; # 对照：不指定源地址 ip rule show # 看有没有按源匹配的规则 ip route show table \u0026lt;被命中的表\u0026gt; # 看那张表是不是\u0026#34;残表\u0026#34; 第 5 步里\u0026quot;带 from 和不带 from 结果不一样\u0026quot;就是决定性证据。\n八、结语 这个故障的技术根因只有一行路由，但它能活下来，靠的是四层叠加：\n文档解释了 What，没解释 Why——\u0026ldquo;回程路由也要在这张表里\u0026quot;写了，但没写这条路由是给谁回程的。 旁边一句不完整的断言给了错误的安全感——\u0026ldquo;NAS 自己的流量源地址是内网地址，不命中\u0026rdquo;，对主动流量成立，对应答流量不成立。 落地脚本和设计文档没有对账——两条命令只抄了一条，而且没人复核。 兜底机制从未被验证过——那个 route-up 钩子看起来能补回丢失的路由，实测证明它一次都没执行过。它没有修复任何问题，只是让人以为问题已经被修复了。 分层解耦是好设计，但每一层的边界条件都得自己守住。这次踩的坑，只有当一台机器同时是网关和主机时才会出现——它既要转发别人的包，又要用那个网关地址回自己的包，而策略路由只认源地址，分不清这两者。\n","permalink":"https://jiwenkang.com/posts/2026/vpn-gateway-vanished/","summary":"\u003cblockquote\u003e\n\u003cp\u003e上一篇\u003ca href=\"/posts/2026/home-network-topology/\"\u003e《家庭网络分层拓扑：从远端笔记本到双出口的完整链路》\u003c/a\u003e讲了这套拓扑\u003cstrong\u003e该怎么搭\u003c/strong\u003e。这一篇讲它\u003cstrong\u003e怎么坏的\u003c/strong\u003e——一个只错了一行、却让人绕了整整一晚的故障。\u003c/p\u003e\n\u003cp\u003e最讽刺的地方在于：\u003cstrong\u003e上一篇里已经写对了那一行，落地时却没抄进去。\u003c/strong\u003e 而上一篇紧接着的另一句解释，恰好让那一行看起来可以省。\u003c/p\u003e\n\u003cp\u003e文中所有公网 IP、商业服务商品牌、个人域名与主机名均已抹去，只保留可复用的结构。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"一tldr\"\u003e一、TL;DR\u003c/h2\u003e\n\u003cp\u003eVPN 客户端可以正常上网、可以访问家里任何一台内网机器，唯独\u003cstrong\u003e访问不了 VPN 网关 \u003ccode\u003e172.x.y.1\u003c/code\u003e 本身\u003c/strong\u003e——ping 不通，全端口静默。\u003c/p\u003e","title":"家庭网络分层拓扑（二）：一条策略路由如何让 VPN 网关人间蒸发"},{"content":" 本文记录一次在已承担生产出口的 OpenWrt 路由器上，安全地接入第二个境外 WireGuard 出口，并按域名分流的完整思路与方法。\n核心诉求：主出口是全家的生命线，绝对不能被第二出口覆盖或拖垮；第二出口只用来把 YouTube、Spotify 这类大流量按域名单独引走。\n文中所有服务器 IP、商业 VPN 服务商品牌、个人域名均已抹去，只保留可复用的方法。\n全流程 Step-by-Step 总览 赶时间的话，照这个顺序走就行；每一步的详细说明见对应章节。顺序本身就是安全保证——先只读、再旁路、再最小上线、最后扩展。\nStep 0｜看清现状（第一、二章）：判断主出口是\u0026quot;路由\u0026quot;还是\u0026quot;透明代理\u0026quot;，只读命令调查，认出防泄漏规则。 Step 1｜定架构（第三章）：确认第二出口放在代理平面，立下\u0026quot;绝不覆盖主出口\u0026quot;的铁律。 Step 2｜备份 + 回滚脚本（第九章 §1-2）：动手前先能退回去。 Step 3｜取 WireGuard 配置（第四章）：用服务商会员后台的配置生成器，不要跑官方一键脚本。 Step 4｜旁路验证（第五章）：独立 sing-box 实例 + 额外 SOCKS 端口，测直连/链式两条底层路径，不碰生产。 Step 5｜最小规则上线（第六章前半）：建节点 + 分流节点，先只放一个\u0026quot;金丝雀\u0026quot;域名，重启代理验证。 Step 6｜扩展分流域名（第六章后半）：确认无误后把 YouTube/Spotify 等批量加进同一条规则。 Step 7｜多节点冗余 + 自动切换（第七章）：下载节点池 + cron 故障切换脚本。 Step 8｜定 Fail-safe 策略（第八章）：第二出口挂＝DROP；主出口挂＝绝不自动换国家。 Step 9｜收尾验证（第九章）：核对路由表/规则未变、防泄漏仍在、两个出口 IP 正确。 一、为什么\u0026quot;加一条路由\u0026quot;这个直觉是错的 大多数人想到\u0026quot;给路由器加第二个出口\u0026quot;，脑子里的图是这样的：\n+--\u0026gt; 默认路由 --\u0026gt; 主出口 终端 --\u0026gt; OpenWrt ---| +--\u0026gt; 第二条路由 --\u0026gt; 新出口 于是去研究 ip route / ip rule / PBR（策略路由）/ mwan3。但动手前必须先搞清楚：你现有的\u0026quot;主出口\u0026quot;到底是不是路由层实现的。\n我的这台机器调查下来发现：所谓\u0026quot;主出口\u0026quot;根本不是路由层的 default route，而是一套全局透明代理（TPROXY）。它的真实数据流是：\n终端流量 / 路由器本机流量 │ （被防火墙 mangle 表打上 mark） ▼ ip rule: fwmark 0x1 → 独立路由表 ▼ TPROXY 重定向到本机代理内核（xray）监听端口 ▼ 代理协议封装 → 境外服务器 → Internet 也就是说：所有流量在进入 main 路由表决策之前，就已经被防火墙 mark + TPROXY 劫走了。 main 表里那条 default via 网关 只是给代理内核自己连境外服务器用的底层链路，终端流量根本不经过它做决策。\n这个发现直接推翻了\u0026quot;加第二条路由\u0026quot;的方案。因为：\n你在 main 表 / PBR 里怎么折腾第二条路由，普通流量都感知不到——它们早被 TPROXY 截走了。 想按域名分流更是无从谈起：路由层只认 IP，不认域名。 结论：如果你的主出口是透明代理实现的，那么\u0026quot;第二出口\u0026quot;正确的位置不在路由层，而在代理平面里——把它做成代理内核里的第二个 outbound 节点，用代理软件自带的分流（shunt）按域名区分。\n这一步\u0026quot;先搞清楚现状再动手\u0026quot;是整件事里最重要的判断。不要照搬教程直接装 PBR 包。\n二、调查阶段：只读，不改 在动任何配置之前，先把现状摸清楚。全部是只读命令，不会影响正在跑的生产流量：\nubus call system board # 系统/内核/架构 ip addr ; ip route show # 接口与 main 表 ip route show table all # 所有路由表（关键！能看出有没有独立表） ip rule show ; ip -6 rule show # 策略路由规则（关键！fwmark 规则暴露 TPROXY） nft list ruleset # 防火墙全量规则（mangle/TPROXY/mark 都在这） wg show # 有没有现成 WireGuard uci show network ; uci show firewall opkg list-installed | grep -iE \u0026#39;wireguard|pbr|mwan|xray|sing|passwall\u0026#39; 重点看三样东西，它们能直接告诉你主出口是不是透明代理：\nip rule 里有没有 fwmark ... lookup \u0026lt;表号\u0026gt;：有，基本就是 TPROXY 分流。 nft list ruleset 里有没有 mangle 链 + tproxy / meta mark set 动作：这是透明代理的指纹。 ip route show table all 里除了 main 有没有独立小表（如 local default dev lo table 100）：TPROXY 的配套。 我的机器上还发现一条自制\u0026quot;防泄漏\u0026quot;规则（在 forward 链上 DROP 掉内网直连外网的流量），意思是\u0026quot;代理挂了宁可断网也不许裸奔\u0026quot;。这种规则要认出来并保留，它是你现有安全模型的一部分，别手滑删了。\n三、目标架构 ┌─ 默认（未命中规则）──► 主出口（原有，保持不动） 终端/本机 ─ TPROXY ─► 代理内核分流 ─┤ └─ 命中域名规则 ───────► 第二出口（WireGuard 境外节点） 几条铁律：\nmain 路由表、IPv4 ip rule、防火墙 zone、防泄漏规则——一个字节都不改。第二出口永远不可能变成系统默认出口。 第二出口挂掉 → 只有命中分流规则的域名受影响（表现为连接失败），主出口完全无关。 主出口挂掉 → 金融、默认流量不会自动切到第二出口（宁可暂时不通，也不要突然换国家 IP）。 不装 PBR / mwan3，不重启整个 network（避免 SSH 断线），唯一的服务中断点是重启代理服务（秒级）。 四、取得 WireGuard 配置的正确姿势 很多商业 VPN 服务商提供官方的 OpenWrt 客户端脚本，但大多数这类脚本会无条件接管默认路由——典型做法是往 main 表注入两条 /1 路由：\n0.0.0.0/1 dev \u0026lt;wg\u0026gt; 128.0.0.0/1 dev \u0026lt;wg\u0026gt; 两条 /1 合起来覆盖整个 0.0.0.0/0，而且比默认路由更\u0026quot;具体\u0026quot;（前缀更长），会压过你原有的默认路由。很多客户端还会同时改 DNS、装 kill switch，且没有开关可以关掉这些行为。\n所以：不要跑服务商的官方一键脚本。 只需要它的\u0026quot;静态 WireGuard 配置\u0026quot;——现在正规服务商的会员后台基本都有一个 WireGuard 配置生成器：浏览器本地生成密钥对，私钥不上传（只把公钥注册到账号），当场生成标准的 [Interface] / [Peer] 配置文件。\n生成的配置长这样（敏感信息已抹去）：\n[Interface] Address = 10.x.x.x/32, fd00:.../128 DNS = 10.x.x.1, fd00:...::1 PrivateKey = \u0026lt;私钥，只存本地\u0026gt; [Peer] PublicKey = \u0026lt;服务器公钥\u0026gt; AllowedIPs = 0.0.0.0/0, ::/0 Endpoint = \u0026lt;服务器地址\u0026gt;:\u0026lt;端口\u0026gt; PersistentKeepalive = 20 我们只取里面的 PrivateKey / PublicKey / Endpoint / Address / DNS，AllowedIPs 里的 0.0.0.0/0 只是 WireGuard 协议字段，不会真的注入系统路由表——因为我们不把它交给内核 WireGuard 接口，而是喂给代理内核当一个 outbound。\n把配置传到路由器专门的目录并收紧权限，私钥全程不要打印到终端或日志：\nssh root@路由器 \u0026#39;mkdir -p /etc/hideout/pool \u0026amp;\u0026amp; chmod 700 /etc/hideout/pool\u0026#39; scp 配置.conf root@路由器:/etc/hideout/pool/nodeA.conf ssh root@路由器 \u0026#39;chmod 600 /etc/hideout/pool/*.conf\u0026#39; # 传完立刻删掉本地下载的副本 五、旁路验证：先别碰生产 在把节点接进生产代理之前，先起一个独立的 sing-box 实例，用一个额外的 SOCKS 端口单独测这条隧道。这样验证过程完全不影响正在跑的主出口。\n关键点：WireGuard 走 UDP，从境内直连境外 UDP 端口有时会被干扰。所以要测两条底层路径：\n直连：WireGuard 的 Endpoint 直接从本地网络出去。 链式（前置代理）：让 WireGuard 的握手和数据先走你现有的主出口代理，再到境外节点（sing-box 的 detour 字段）。 sing-box 测试配置骨架（用户态 WireGuard endpoint + 两个 SOCKS 入口分别对应两条路径）：\n{ \u0026#34;endpoints\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;wireguard\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;wg-direct\u0026#34;, \u0026#34;address\u0026#34;: [\u0026#34;10.x.x.x/32\u0026#34;, \u0026#34;fd00:.../128\u0026#34;], \u0026#34;private_key\u0026#34;: \u0026#34;...\u0026#34;, \u0026#34;mtu\u0026#34;: 1380, \u0026#34;peers\u0026#34;: [{ \u0026#34;address\u0026#34;: \u0026#34;\u0026lt;endpoint\u0026gt;\u0026#34;, \u0026#34;port\u0026#34;: \u0026lt;端口\u0026gt;, \u0026#34;public_key\u0026#34;: \u0026#34;...\u0026#34;, \u0026#34;allowed_ips\u0026#34;: [\u0026#34;0.0.0.0/0\u0026#34;,\u0026#34;::/0\u0026#34;], \u0026#34;persistent_keepalive_interval\u0026#34;: 20 }] }, { \u0026#34;type\u0026#34;: \u0026#34;wireguard\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;wg-chained\u0026#34;, \u0026#34;detour\u0026#34;: \u0026#34;socks-main\u0026#34;, /* 其余同上，握手经主出口 */ } ], \u0026#34;inbounds\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;socks\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;in-direct\u0026#34;, \u0026#34;listen\u0026#34;: \u0026#34;127.0.0.1\u0026#34;, \u0026#34;listen_port\u0026#34;: 10808 }, { \u0026#34;type\u0026#34;: \u0026#34;socks\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;in-chained\u0026#34;, \u0026#34;listen\u0026#34;: \u0026#34;127.0.0.1\u0026#34;, \u0026#34;listen_port\u0026#34;: 10809 } ], \u0026#34;outbounds\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;socks\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;socks-main\u0026#34;, \u0026#34;server\u0026#34;: \u0026#34;127.0.0.1\u0026#34;, \u0026#34;server_port\u0026#34;: \u0026lt;主出口本地SOCKS端口\u0026gt; }, { \u0026#34;type\u0026#34;: \u0026#34;direct\u0026#34;, \u0026#34;tag\u0026#34;: \u0026#34;direct\u0026#34; } ], \u0026#34;route\u0026#34;: { \u0026#34;rules\u0026#34;: [ { \u0026#34;inbound\u0026#34;: [\u0026#34;in-direct\u0026#34;], \u0026#34;outbound\u0026#34;: \u0026#34;wg-direct\u0026#34; }, { \u0026#34;inbound\u0026#34;: [\u0026#34;in-chained\u0026#34;], \u0026#34;outbound\u0026#34;: \u0026#34;wg-chained\u0026#34; } ], \u0026#34;final\u0026#34;: \u0026#34;direct\u0026#34; } } 验证命令（看到的 IP 应该是境外节点，而不是主出口）：\nsing-box check -c test.json \u0026amp;\u0026amp; sing-box run -c test.json \u0026amp; curl --socks5-hostname 127.0.0.1:10808 https://ifconfig.co/ip # 直连路径 curl --socks5-hostname 127.0.0.1:10809 https://ifconfig.co/ip # 链式路径 curl https://ifconfig.co/ip # 现网：应仍是主出口 我实测下来直连 UDP 通且速度不错（几十 Mbps 级别），就用直连；如果你那边直连不通，就用链式。测完把测试实例杀掉、临时文件删掉，生产还是干干净净的。\n顺带记一个坑：主出口代理的本地 SOCKS 如果 UDP relay 不通，链式路径会失败——因为 WireGuard 是 UDP。这种情况要么开启 SOCKS 的 UDP 支持，要么老实用直连。\n六、接入生产代理 + 域名分流 验证通过后，把这个 WireGuard 节点正式加进生产代理（这里以 PassWall2 为例，它的 xray 后端原生支持 WireGuard 协议节点）。用 uci 脚本从 .conf 里提取字段、建节点：\nCONF=/etc/hideout/pool/nodeA.conf PK=$(sed -n \u0026#39;s/^PrivateKey *= *//p\u0026#39; $CONF) PUB=$(sed -n \u0026#39;s/^PublicKey *= *//p\u0026#39; $CONF) ADDR=$(sed -n \u0026#39;s/^Address *= *//p\u0026#39; $CONF); A4=${ADDR%%,*}; A6=${ADDR##*,} EP=$(sed -n \u0026#39;s/^Endpoint *= *//p\u0026#39; $CONF) uci set 代理.nodeA=nodes uci set 代理.nodeA.type=\u0026#39;Xray\u0026#39; uci set 代理.nodeA.protocol=\u0026#39;wireguard\u0026#39; uci set 代理.nodeA.address=\u0026#34;${EP%:*}\u0026#34; uci set 代理.nodeA.port=\u0026#34;${EP##*:}\u0026#34; uci set 代理.nodeA.wireguard_public_key=\u0026#34;$PUB\u0026#34; uci set 代理.nodeA.wireguard_secret_key=\u0026#34;$PK\u0026#34; uci add_list 代理.nodeA.wireguard_local_address=\u0026#34;$A4\u0026#34; uci add_list 代理.nodeA.wireguard_local_address=\u0026#34;$A6\u0026#34; uci set 代理.nodeA.wireguard_mtu=\u0026#39;1380\u0026#39; uci set 代理.nodeA.wireguard_keepAlive=\u0026#39;20\u0026#39; 再建一个分流（shunt）节点：默认走主出口，命中规则的域名走第二出口。先只加一条最小可验证的规则（比如一个 IP 查询站点当\u0026quot;金丝雀\u0026quot;），确认整条链路通了再扩：\n# 分流规则：初期只放一个金丝雀域名 uci set 代理.MediaRule=shunt_rules uci set 代理.MediaRule.remarks=\u0026#39;MediaRule\u0026#39; uci set 代理.MediaRule.network=\u0026#39;tcp,udp\u0026#39; uci set 代理.MediaRule.domain_list=\u0026#39;ifconfig.co\u0026#39; # 分流节点：默认=主出口，MediaRule=第二出口 uci set 代理.split=nodes uci set 代理.split.protocol=\u0026#39;_shunt\u0026#39; uci set 代理.split.default_node=\u0026#39;\u0026lt;主出口节点ID\u0026gt;\u0026#39; uci set 代理.split.MediaRule=\u0026#39;nodeA\u0026#39; uci set 代理.@global[0].node=\u0026#39;split\u0026#39; # 全局切到分流节点 uci commit 代理 /etc/init.d/代理 restart # 唯一的秒级中断点 验证（默认仍是主出口，金丝雀域名走第二出口）：\ncurl https://ifconfig.me # 应为主出口 IP curl https://ifconfig.co/ip # 应为第二出口（境外节点）IP 确认无误后，把真正要分流的域名批量加进同一条规则（用 geosite 分类最省事）：\nuci set 代理.MediaRule.domain_list=\u0026#39;geosite:youtube geosite:spotify domain:googlevideo.com domain:ytimg.com domain:youtubei.googleapis.com domain:scdn.co domain:spotifycdn.com\u0026#39; uci commit 代理 \u0026amp;\u0026amp; /etc/init.d/代理 restart 金融、代码托管、AI 服务、工作相关域名——不写任何规则，它们自然落到默认（主出口），符合\u0026quot;这些服务绝不自动换国家 IP\u0026quot;的要求。\n⚠️ 一个重要的坑：不要让多条分流规则指向同一个 WireGuard 节点。 用户态 WireGuard outbound 每条规则会建一个独立会话，同一把密钥的多个并发会话在服务端会互相打架（endpoint 漂移、丢包）。所有域名都塞进同一条规则即可。\n七、多节点冗余 + 自动故障切换 单节点有个明显风险：那台境外服务器一挂，分流的域名就全崩。解决办法是下载同一服务商的一批节点组成\u0026quot;节点池\u0026quot;。\n关键认知：同一个 WireGuard 密钥对，在服务商后台可以对每个位置分别生成配置——它们共享同一把私钥和同一个隧道内网地址，只是 Endpoint 和服务器公钥不同。所以下载一批（我下了十来个欧洲位置），逐个建成代理节点即可。\n然后写一个故障切换脚本，用 cron 定时探测，节点挂了就沿\u0026quot;节点环\u0026quot;轮转到下一个。设计要点：\n双金丝雀探测：用两个独立的 IP 查询站点（都走第二出口规则），避免单站点抽风误判。 连续失败才切：连续 2 次探测失败（约 6 分钟确认窗口）才动，避免瞬时抖动导致频繁重启代理。 整环走完仍失败就停手：说明不是单节点问题（可能是金丝雀站点挂了、或上游/审查层面的问题），继续轮转只会无意义地反复重启，不如保持现状等恢复。 永不回落主出口：切换只在节点池内部进行，跟\u0026quot;宁可断也不换国家\u0026quot;的原则一致。 脚本骨架：\n#!/bin/sh RING=\u0026#34;nodeA nodeB nodeC ... nodeJ\u0026#34; # 节点环 STATE=/tmp/failover.state probe() { # 双金丝雀，任一成功即算通 curl -4 -s --max-time 10 https://金丝雀1/ip \u0026amp;\u0026amp; return 0 curl -4 -s --max-time 10 https://金丝雀2 \u0026amp;\u0026amp; return 0 return 1 } FAILS=0; ROT=0; [ -f \u0026#34;$STATE\u0026#34; ] \u0026amp;\u0026amp; . \u0026#34;$STATE\u0026#34; if probe; then echo \u0026#34;FAILS=0\u0026#34;$\u0026#39;\\n\u0026#39;\u0026#34;ROT=0\u0026#34; \u0026gt; \u0026#34;$STATE\u0026#34;; exit 0; fi # 恢复即归零 FAILS=$((FAILS+1)) [ \u0026#34;$FAILS\u0026#34; -lt 2 ] \u0026amp;\u0026amp; { echo \u0026#34;FAILS=$FAILS\u0026#34;$\u0026#39;\\n\u0026#39;\u0026#34;ROT=$ROT\u0026#34; \u0026gt; \u0026#34;$STATE\u0026#34;; exit 0; } # 未连续失败2次，先忍 NODECOUNT=$(echo $RING | wc -w) [ \u0026#34;$ROT\u0026#34; -ge \u0026#34;$NODECOUNT\u0026#34; ] \u0026amp;\u0026amp; exit 0 # 整环走完仍失败，停手保持现状 # 取环中下一个节点，改分流映射并重启代理 CUR=$(uci -q get 代理.split.MediaRule) # ...在 RING 里找到 CUR 的下一个 NEXT... uci set 代理.split.MediaRule=\u0026#34;$NEXT\u0026#34;; uci commit 代理; /etc/init.d/代理 restart echo \u0026#34;FAILS=0\u0026#34;$\u0026#39;\\n\u0026#39;\u0026#34;ROT=$((ROT+1))\u0026#34; \u0026gt; \u0026#34;$STATE\u0026#34; logger -t failover \u0026#34;switched: $CUR -\u0026gt; $NEXT\u0026#34; 挂到 cron：\necho \u0026#39;*/3 * * * * /etc/hideout/failover.sh\u0026#39; \u0026gt;\u0026gt; /etc/crontabs/root /etc/init.d/cron restart 日志用 logread | grep failover 看切换动态。\n八、Fail-safe：两种取舍 第二出口挂掉时，命中规则的流量怎么办？ 两个方案：\nA. DROP（推荐，零配置）：分流规则固定指向第二出口节点，节点死则那些域名连接失败。零风险、绝不泄漏到别的出口。配合上面的自动切换，通常还没等你发现就已经切到别的节点了。 B. 回落主出口：让规则节点自动切回主出口。但这意味着 YouTube/Spotify 会在你不知情时突然变回主出口的国家 IP，未必是你想要的。 我选 A。\n主出口挂掉时：默认流量（金融、工作等）不会自动切到第二出口——因为它们压根没有分流规则，默认节点死了就是不通。这正是我们想要的：\u0026ldquo;宁可这些服务暂时不可访问，也不要突然换一个国家的 IP 去登录银行。\u0026rdquo;\n九、收尾验证清单 ip rule show # 应与备份一致：只有 local / fwmark→表 / main / default ip route show # main 表默认路由未变 nft list chain inet fw4 \u0026lt;防泄漏链\u0026gt; # 自制防泄漏规则仍在 curl https://ifconfig.me # 主出口 IP（未变） curl https://ifconfig.co/ip # 第二出口 IP（境外节点） logread | grep failover | tail # 故障切换正常 再补几个安全习惯：\n动手前先全量备份：/etc/config/{network,firewall,dhcp,代理}、/etc/nftables.d/，以及 ip route show table all / ip rule / nft list ruleset 的快照，全部塞进 /root/网络备份-时间戳/。 写一个一键回滚脚本：恢复上述配置 + 重启代理服务（不要 network restart，会断 SSH），确保配置出错能立刻退回原状。 优先 ifup/ifdown \u0026lt;接口\u0026gt; 而不是 /etc/init.d/network restart，避免 SSH 断线。 私钥永不落终端/日志，配置文件 chmod 600。 十、小结 这次的核心不是\u0026quot;怎么配 WireGuard\u0026quot;，而是三个判断：\n先摸清主出口的真实实现（路由 vs 透明代理），它决定了第二出口该放在哪一层——照搬教程装 PBR 是南辕北辙。 第二出口做成代理平面的一个 outbound 节点，用软件自带的域名分流，天生不碰系统路由表，也天然用无污染的远程 DNS。 主出口是 PRIMARY，第二出口是 SECONDARY：后者的存在、故障、切换，都不允许以任何方式影响前者。 沿着\u0026quot;只读调查 → 旁路验证 → 最小规则上线 → 逐步扩展 → 加冗余与 fail-safe\u0026quot;这个顺序走，全程对生产链路零破坏，SSH 不断线，随时可一键回滚。\n","permalink":"https://jiwenkang.com/posts/2026/openwrt-dual-egress/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本文记录一次在\u003cstrong\u003e已承担生产出口\u003c/strong\u003e的 OpenWrt 路由器上，安全地接入第二个境外 WireGuard 出口，并按域名分流的完整思路与方法。\u003c/p\u003e\n\u003cp\u003e核心诉求：主出口是全家的生命线，\u003cstrong\u003e绝对不能被第二出口覆盖或拖垮\u003c/strong\u003e；第二出口只用来把 YouTube、Spotify 这类大流量按域名单独引走。\u003c/p\u003e\n\u003cp\u003e文中所有服务器 IP、商业 VPN 服务商品牌、个人域名均已抹去，只保留可复用的方法。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"OpenWrt 双出口分流：在不动主链路的前提下增加第二个境外出口"},{"content":" 上一篇《OpenWrt 双出口分流》讲的是旁路网关内部怎么做第二出口。这一篇把镜头拉远，讲整张网络的分层拓扑：一台远端的公司笔记本，是怎么穿过 NAS 的 VPN、被两段策略路由接力、最终从境外出口上网的；以及家里每一类设备各自走哪条路、为什么。\n文中所有公网 IP、商业服务商品牌、个人域名与主机名均已抹去，只保留可复用的结构。\n一、全景图 先上完整的数据流图，后面逐层拆解：\n┌──────────────────────────────────────────────┐ 公司笔记本（远端，任意网络） │ 家庭内网 192.168.1.0/24 │ │ │ │ │ VPN 隧道（加密） │ ┌─────────── NAS (.4) ────────────┐ │ └───────────────────────────┼──►│ VPN Server → tun0 172.x.y.0/24 │ │ │ │ │ │ │ │ ip rule: from 172.x.y.0/24 │ │ │ │ lookup 100 │ │ │ │ table 100: default via .254 ────┼──┐ │ │ │ │ │ │ │ │ NAS 自身流量: default via .1 ───┼──┼──► │──► 国内家宽（直连） │ └─────────────────────────────────┘ │ │ │ ▼ │ 家里普通设备（手机/电视/PC）───────┼──────────────────────────────► 旁路网关 (.254)│ │ OpenWrt 虚拟机 │ │ │ │ │ TPROXY 全局透明代理 │ │ │ fwmark 0x1 → lookup 100 → 代理内核 │ │ │ │ │ ┌───────────┴───────────┐ │ │ ▼ ▼ │ │ 默认（未命中规则） 命中媒体域名 │ │ │ │ │ └────────┼───────────────────────┼─────────────┘ ▼ ▼ 主出口（境外A） 第二出口（境外B， 金融/工作/默认 WireGuard 节点池 + 自动故障切换） 三个层次一句话概括：\n接入层（NAS）：把远端设备\u0026quot;拉进\u0026quot;家庭内网，并用策略路由决定它的流量交给谁。 代理层（旁路网关）：把到达的流量透明地劫进代理内核，按域名分流到两个境外出口。 出口层：主出口（生命线）+ 第二出口（媒体流量、多节点冗余）。 二、四类终端，四种出口画像 同一张家庭网络，不同设备的流量走完全不同的路——这是整个设计的出发点：\n终端 路径 最终出口 为什么 公司笔记本（远端） VPN → NAS → 策略路由 → 旁路网关 → 代理 境外（主出口/分流） 人在外面，也要和在家一样的网络环境 家里普通设备 默认网关指向旁路网关 → 代理 境外（主出口/分流） 全家默认出海，域名分流自动生效 NAS 本机 默认路由直指上游网关 国内直连 NAS 跑的是下载/备份/内网服务，走国内又快又稳，也不占代理带宽 开发用笔记本 自己的独立 VPN 隧道 境外（独立于本拓扑） 故障隔离：排查家庭网络时，这台机器不受影响，反之亦然 注意最后一行的价值：留一台不依赖这套拓扑的机器。旁路网关重启、代理挂掉时，你需要一台还能上网、还能 SSH 进各设备排障的\u0026quot;生命线终端\u0026quot;。\n三、第一棒：NAS 上的接入与策略路由 远端的公司笔记本通过 VPN 拨进 NAS（很多 NAS 系统自带 VPN Server 套件），拿到一个 VPN 子网的地址（本文记作 172.x.y.0/24，NAS 侧接口是 tun0）。\n默认情况下，VPN 客户端的流量会跟着 NAS 自己的默认路由走——也就是国内直连。但我们想让它走代理。在 NAS 上加两条东西即可：\n# 1. 独立路由表：VPN 客户端的\u0026#34;默认路由\u0026#34;指向旁路网关（而不是上游网关） ip route add default via 192.168.1.254 dev \u0026lt;内网接口\u0026gt; table 100 ip route add 172.x.y.0/24 dev tun0 table 100 # 回程路由也要在这张表里 # 2. 策略路由：只有源地址来自 VPN 子网的流量才查这张表 ip rule add from 172.x.y.0/24 lookup 100 生效后的 ip rule 长这样：\n0: from all lookup local 1: from 172.x.y.0/24 lookup 100 ← 只有 VPN 客户端命中 32766: from all lookup main ← NAS 自身流量照旧 32767: from all lookup default 这一段的精髓是按源地址切割路由决策：\n命中 from 172.x.y.0/24 的（= 公司笔记本），默认路由被替换成\u0026quot;下一跳旁路网关\u0026quot;。 NAS 自己的流量源地址是内网地址，不命中，继续查 main 表直连国内——NAS 的下载、备份、内网服务完全不受影响。 别忘了打开转发和 NAT/放行（各系统写法不同，思路一致）：VPN 子网 → 内网接口允许 FORWARD；如果旁路网关上没有回程路由，就在 NAS 上对这段做 SNAT，让回包能找回来。\n四、第二棒：旁路网关上的透明代理 流量到达旁路网关后，进入上一篇讲过的代理平面，这里只画重点。\n旁路网关是台 OpenWrt 虚拟机，单臂部署（只有一个桥接口，既收内网流量，也从同一条线出去）。它不做路由层分流，而是全局透明代理：\n到达的转发流量 / 本机流量 │ 防火墙 mangle 链打上 fwmark 0x1 ▼ ip rule: from all fwmark 0x1 lookup 100 ▼ table 100: local default dev lo ← 流量被\u0026#34;扣留\u0026#34;在本机 ▼ TPROXY 重定向 → 代理内核监听端口 ▼ 按域名分流： ├─ 默认（金融/工作/一切未命中）──► 主出口（境外A） └─ geosite:youtube 等媒体域名 ───► 第二出口（境外B，WireGuard 节点池） 配套机制（详见上一篇）：\nDNS 全量劫持到代理内核的 DNS 端口，远程解析，无污染——域名分流因此可靠。 防泄漏（kill switch）：自制 nft 规则 DROP 掉\u0026quot;绕过代理直连外网\u0026quot;的转发——代理挂了宁可断网也不裸奔。 第二出口节点池 + cron 自动故障切换：单节点挂了沿节点环轮转，绝不回落主出口。 五、两段 ip rule 的接力：各管一段，互不知情 这套拓扑里最容易混淆的，是 NAS 和旁路网关上各有一条 lookup 100 的 ip rule，但它们是完全不同的两回事：\nNAS 上的规则 旁路网关上的规则 规则 from 172.x.y.0/24 lookup 100 from all fwmark 0x1 lookup 100 匹配依据 源地址（谁发的） 防火墙标记（被 mangle 链盖过章的） table 100 内容 default via 旁路网关 local default dev lo 作用 把 VPN 客户端流量送到网关 把打了标的流量扣进代理 谁维护 手工/开机脚本 代理服务自动增删 表号都叫 100 纯属巧合（两台机器各有各的路由表空间），但正好提醒我们:策略路由是逐跳生效的。NAS 只负责\u0026quot;下一跳给谁\u0026quot;，它不知道也不需要知道网关后面是代理；网关只管\u0026quot;到达我的流量按域名分流\u0026quot;，它不在乎流量是家里设备发的还是 VPN 穿进来的。两段规则通过\u0026quot;把包送到对方门口\u0026quot;完成接力，没有任何共享状态。\n这种分层解耦的直接好处：加一台新的接入设备（比如再来一路 VPN），只动 NAS 的规则；换一个出口节点，只动网关的代理配置——互不牵连。\n六、故障域分析：每一层挂了会怎样 设计时把\u0026quot;谁挂了影响谁\u0026quot;想清楚，出问题时就不会慌：\n故障 影响范围 表现与恢复 旁路网关重启 家里设备 + 公司笔记本断网 代理内核拉起需要几十秒，期间\u0026quot;打不开\u0026quot;，自愈，不用管 第二出口节点挂 只有媒体域名 自动故障切换约 6 分钟内换下一个节点；期间媒体域名连接失败，其余一切正常 主出口挂 默认流量（金融/工作） 不会自动切到第二出口——宁可暂时不通，也不换国家 IP 去登录银行 NAS 挂 只有公司笔记本 家里设备不经过 NAS，完全无感；笔记本重连 VPN 即可 上游家宽挂 全部 物理层问题，谁也救不了 代理挂但网关活着 家里设备 + 笔记本 kill switch 兜底：断网而不是裸奔直连 两个值得强调的设计决策：\n主出口挂 ≠ 切第二出口。金融和工作服务的 IP 归属地突变，轻则风控验证，重则封号。所以默认流量没有任何 fallback 规则——这是故意的。 重启网关前心里有数。接入层（NAS 的 ip rule）是静态的，重启网关不影响它；恢复顺序永远是\u0026quot;网关代理拉起 → 全链路自愈\u0026quot;，不需要人工干预任何一层。 七、一个运维细节：会悄悄残留的重复 ip rule 旁路网关重启后，偶尔会看到 fwmark 规则出现两条一模一样的副本：\n32764: from all fwmark 0x1 lookup 100 32765: from all fwmark 0x1 lookup 100 翻代理服务的脚本能找到原因——它的启动/停止逻辑是不对称的：\n# start 时： ip rule add fwmark 1 lookup 100 # 加一条 # stop 时： ip rule del fwmark 1 lookup 100 # 只删一条（del 一次只删一个实例） 开机过程中 start 逻辑被触发了两次（没有经过 stop），就叠成两条。之后每次 restart 都是\u0026quot;删一条、加一条\u0026quot;，净数不变——这个重复副本不会自愈，会永久残留。\n处理原则：\n功能上完全无害：两条规则字节级相同，匹配等价，零开销。不处理也没任何问题。 想清理的话，ip rule del fwmark 1 lookup 100 恰好只删一个实例，剩下那条继续工作，流量无感知。 但绝不能删光——这条规则是整个代理平面的命脉，删光则所有代理流量（包括穿过 NAS 进来的公司笔记本）全断，直到代理服务下次重启才恢复。删之前先 ip rule show 数清楚还剩几条。 八、小结 回看整张图，这套拓扑的骨架就三句话：\n接入归接入，代理归代理。NAS 用\u0026quot;源地址策略路由\u0026quot;决定谁的流量交给网关，网关用\u0026quot;fwmark + TPROXY\u0026quot;决定流量怎么出海——两段 ip rule 各管一段，通过下一跳接力，零共享状态。 出口画像按设备定制。远端笔记本全量出海、家里设备默认出海、NAS 直连国内、开发机独立隧道互为备份——同一张网，四种画像。 故障域预先划清。每一层挂掉影响谁、会不会自愈、要不要 fallback，都是设计时定好的——尤其是\u0026quot;主出口挂了绝不自动换国家\u0026quot;这条底线。 分层的代价是多一跳、多一处配置；换来的是每一层都可以独立变更、独立排障、独立演进。对一张要长期运行的家庭网络来说，这笔账很划算。\n","permalink":"https://jiwenkang.com/posts/2026/home-network-topology/","summary":"\u003cblockquote\u003e\n\u003cp\u003e上一篇\u003ca href=\"/posts/2026/openwrt-dual-egress/\"\u003e《OpenWrt 双出口分流》\u003c/a\u003e讲的是\u003cstrong\u003e旁路网关内部\u003c/strong\u003e怎么做第二出口。这一篇把镜头拉远，讲整张网络的\u003cstrong\u003e分层拓扑\u003c/strong\u003e：一台远端的公司笔记本，是怎么穿过 NAS 的 VPN、被两段策略路由接力、最终从境外出口上网的；以及家里每一类设备各自走哪条路、为什么。\u003c/p\u003e\n\u003cp\u003e文中所有公网 IP、商业服务商品牌、个人域名与主机名均已抹去，只保留可复用的结构。\u003c/p\u003e\n\u003c/blockquote\u003e","title":"家庭网络分层拓扑：从远端笔记本到双出口的完整链路"},{"content":"Services \u0026amp; Digital Products 这个博客主要记录我的技术学习、工作经验和个人项目。随着内容越来越多，我把其中一部分可以被复用、交付或进一步咨询的内容，整理到了独立页面：\n🛒 Ji Wenkang 技术服务与数字资源 它不是一个传统意义上的“商品店”，更像是这个博客的延伸：\n博客：记录学习路径、技术文章、项目索引和个人经历； Buy 页面：整理可以预约、购买、交付或进一步展开的服务与资源； Prompt Library / Vibe Coding：沉淀可复用的 Prompt 包、项目手册和自动化工具； Red Hat / KVM / Virtio-Win / OpenShift：沉淀技术咨询、排障笔记和实验环境方案。 适合访问 Buy 页面的场景：\n想系统学习 Linux / KVM / OpenShift / Red Hat 认证； 想请我帮忙看一个虚拟化、容器、网络或 Homelab 问题； 想获取 Prompt、脚本、部署手册或实验资源； 想搭建 NAS、软路由、AI API、内网穿透或个人数字基础设施； 想定制自动化脚本、数据监控工具或个人 AI Agent。 服务咨询与联系\n咨询前可先浏览上方的服务与资源介绍，来信时请简要说明需求；也可以直接预约一次 30 分钟线上沟通。\n预约线上沟通：calendly.com/wenkangji 个人服务咨询：wenkangji@gmail.com Red Hat 工作事务：wji@redhat.com Domain Names: Ji Wenkang - 季文康, Wenkang Ji - 文康季, ITxdm - IT 兄弟盟\n本博客挂在 Github Pages 上进行分布式版本控制，配合 Cloudflare 实现动态防护。国内访问很慢，需魔法。\n","permalink":"https://jiwenkang.com/services/","summary":"\u003ch2 id=\"services--digital-products\"\u003eServices \u0026amp; Digital Products\u003c/h2\u003e\n\u003cp\u003e这个博客主要记录我的技术学习、工作经验和个人项目。随着内容越来越多，我把其中一部分可以被复用、交付或进一步咨询的内容，整理到了独立页面：\u003c/p\u003e\n\u003ch4 id=\"-ji-wenkang-技术服务与数字资源\"\u003e🛒 \u003ca href=\"https://buy.jiwenkang.com/\"\u003eJi Wenkang 技术服务与数字资源\u003c/a\u003e\u003c/h4\u003e\n\u003cp\u003e它不是一个传统意义上的“商品店”，更像是这个博客的延伸：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e博客\u003c/strong\u003e：记录学习路径、技术文章、项目索引和个人经历；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eBuy 页面\u003c/strong\u003e：整理可以预约、购买、交付或进一步展开的服务与资源；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePrompt Library / Vibe Coding\u003c/strong\u003e：沉淀可复用的 Prompt 包、项目手册和自动化工具；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRed Hat / KVM / Virtio-Win / OpenShift\u003c/strong\u003e：沉淀技术咨询、排障笔记和实验环境方案。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e适合访问 Buy 页面的场景：\u003c/p\u003e","title":"服务"},{"content":"About me Wenkang Ji 【从 2016 年 05 月 10 日 到 2022 年 05 月 14 日】拥有 博客园 6 年撰稿经验，50w 阅读量技术博主。\n九九后，在沪，前 linux.cn 开发组成员，前腾科网络技术、交大慧谷红帽课程讲师。详见 Bilibili：文康的读书经历\n【从 2018 年到 2023 年，红帽政策原则无法继续】一线城市培训机构 5 年讲师\n已培训超千名工程师，遍布各大高校、电信、金融、电商等相关行业。 【从 2019 年 5 月 29 日 到 2023 年 2 月 18 日】三年半兔兔铲屎官 🐰\nTips：兔子是非常聪明的宠物，饲养需要多喂水，大量供草，尽早绝育。 【2026 年 06 月 15 日至今】Red Hat Customer Success Executive（CSE），上海，APAC SA \u0026amp; Adoption 团队。\n帮助客户制定并推进客户成功计划，跟踪产品采用情况与业务目标；协调销售、技术、支持和交付团队，推动阻塞问题解决、主动管理风险，并收集客户反馈。 【从 2022 年 02 月 至 2026 年 06 月】Red Hat 虚拟化团队，研发与质量工程（R\u0026amp;D/QE）\n质量工程师的旅程：从学习者到领导者\n在虚拟化团队期间参与的开源项目：\nOur Goal: Virtio-Win 性能架构解析\nVirtio-Win 开源项目：kvm-guest-drivers-windows\nDownload latest or stable binary drivers Virtio-Win 安装包：virtio-win-guest-tools-installer\n测试框架：avocado-framework\n测试用例：tp-qemu\nHonours Patches\n[Jun 23, 2016] 人生发出的第一个 Patch：Linux.CN 社区：文章翻译\n[Aug 28, 2022] 人生首个开源 Patch：K8S 社区：OVN 文档修复\n[Apr 6, 2023] 人生首次多人协作 Patch：OpenPLC 社区：OpenPLC 对于 RT 支持\n关于 OpenPLC 介绍：OpenPLC RealTime tests 本人具有 RHCA、ISTQB、HCIP 等多项职业认证，红帽认证多数均满分通过。\nOthers IT 梦想的起点：\n2015 年全国职业院校技能大赛网络搭建与应用竞赛 - 赛题分享 带我 Linux 入门的师傅们：饶 \u0026amp; 贺老师\n上海科管 — 饶老师，带我进入 Linux 世界的引导者。 红帽 GLS 团队 — 贺老师，领我进入 Red Hat 公司的指路人。 在红帽虚拟化团队工作期间遇到的部分同事，在此感谢：\nProduct Owner: Yan Vugenfirer Manager: Qianqian Zhu Nini Gu, Menghuan Li, Xiaoling Gao, Kostiantyn Kostiuk, Vadim Rozenfeld, Yuri Benditovich Qiao Zhao ","permalink":"https://jiwenkang.com/about/","summary":"\u003ch2 id=\"about-me\"\u003eAbout me\u003c/h2\u003e\n\u003cscript src=\"https://platform.linkedin.com/badges/js/profile.js\" async defer type=\"text/javascript\"\u003e\u003c/script\u003e\n\u003cdiv class=\"badge-base LI-profile-badge\" data-locale=\"en_US\" data-size=\"medium\" data-theme=\"light\" data-type=\"VERTICAL\" data-vanity=\"wenkang-ji-b2120324a\" data-version=\"v1\"\u003e\u003ca class=\"badge-base__link LI-simple-link\" href=\"https://cn.linkedin.com/in/wenkang-ji-b2120324a?trk=profile-badge\"\u003eWenkang Ji\u003c/a\u003e\u003c/div\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e【从 2016 年 05 月 10 日 到 2022 年 05 月 14 日】拥有 \u003ca href=\"https://www.cnblogs.com/itxdm\"\u003e博客园\u003c/a\u003e 6 年撰稿经验，50w 阅读量技术博主。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e九九后，在沪，前 \u003ca href=\"https://linux.cn\"\u003elinux.cn\u003c/a\u003e 开发组成员，前腾科网络技术、交大慧谷红帽课程讲师。详见 Bilibili：\u003ca href=\"https://www.bilibili.com/video/BV1iR4y1c7o4\"\u003e文康的读书经历\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e【从 2018 年到 2023 年，红帽政策原则无法继续】一线城市培训机构 5 年讲师\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e已培训超千名工程师，遍布各大高校、电信、金融、电商等相关行业。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e【从 2019 年 5 月 29 日 到 2023 年 2 月 18 日】三年半兔兔铲屎官 🐰\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eTips：兔子是非常聪明的宠物，饲养需要多喂水，大量供草，尽早绝育。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e【2026 年 06 月 15 日至今】Red Hat Customer Success Executive（CSE），上海，APAC SA \u0026amp; Adoption 团队。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e帮助客户制定并推进客户成功计划，跟踪产品采用情况与业务目标；协调销售、技术、支持和交付团队，推动阻塞问题解决、主动管理风险，并收集客户反馈。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e【从 2022 年 02 月 至 2026 年 06 月】Red Hat 虚拟化团队，研发与质量工程（R\u0026amp;D/QE）\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"/redhat/%E8%B4%A8%E9%87%8F%E5%B7%A5%E7%A8%8B%E5%B8%88%E7%9A%84%E6%97%85%E7%A8%8B%EF%BC%9A%E4%BB%8E%E5%AD%A6%E4%B9%A0%E8%80%85%E5%88%B0%E9%A2%86%E5%AF%BC%E8%80%85.mp4\"\u003e质量工程师的旅程：从学习者到领导者\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e在虚拟化团队期间参与的开源项目：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003eOur Goal: \u003ca href=\"/redhat/Virtio-win%20%E6%80%A7%E8%83%BD%E6%9E%B6%E6%9E%84%E8%A7%A3%E6%9E%90.png\"\u003eVirtio-Win 性能架构解析\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eVirtio-Win 开源项目：\u003ca href=\"https://github.com/virtio-win/kvm-guest-drivers-windows\"\u003ekvm-guest-drivers-windows\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://docs.fedoraproject.org/en-US/quick-docs/creating-windows-virtual-machines-using-virtio-drivers/index.html\"\u003eDownload latest or stable binary drivers\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eVirtio-Win 安装包：\u003ca href=\"https://github.com/virtio-win/virtio-win-guest-tools-installer\"\u003evirtio-win-guest-tools-installer\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e测试框架：\u003ca href=\"https://github.com/avocado-framework/avocado-vt/\"\u003eavocado-framework\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e测试用例：\u003ca href=\"https://github.com/autotest/tp-qemu/\"\u003etp-qemu\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e","title":"关于"},{"content":"Tech Knowledges General Linux 文件系统基础版 Linux 的启动流程图 Linux 下 grub 引导修复 如何将 Linux 系统转移至 LVM 卷 Linux 意外操作后如何进行数据抢救 Linux Commond Line Linux 下 find 命令 Linux 下 OpenSSH 高级运用两则 C/Shell Programing 一文让你明白C语言中的指针 C 语言中指针的指针究竟是个啥\nC 语言 struct 结构体\nC 语言之函数访问 Linux 下 Shell 脚本几种基本命令替换区别\nLinux 下 Shell 脚本的基础/高级编程\nStandard Network 网络基础：IP地址子网划分 记一次在黑盒环境下使用网络设备寻找主机 OpenShift Network Slides OpenShift CNV Videos OpenWrt 双出口分流：在不动主链路的前提下增加第二个境外出口 家庭网络分层拓扑：从远端笔记本到双出口的完整链路 家庭网络分层拓扑（二）：一条策略路由如何让 VPN 网关人间蒸发 远程给朋友修 VPN：有了 AI 之后，一次运维排障是怎么进行的 Qemu-KVM Virtualization 虚拟化介绍 虚拟化文字稿 OpenShift Introduce Videos VM live migration deep dive in OCP-V KVM 基座能力来支持OpenShift Virtualization Slide Virtio-Win Virtio-win 性能架构解析 Virtio-Win 驱动程序及相关虚拟化技术组件概览 NetKVM 软件之桥：Windows_如何在_Linux_云上运行 NetKVM Feature Presentation VirtIO-Win-NetKVM - UDP Segmentation Offload Virtio_Windows_KVM_Enterprise_Drivers FWcfg Fwcfg \u0026amp; vioinput transfer AI part 腾讯研究院 AIGC 发展趋势报告2023.pdf Instruct Lab \u0026amp; Granite Model .pdf CS146S: The Modern Software Developer Deep Learning codechangeai 远程给朋友修 VPN：有了 AI 之后，一次运维排障是怎么进行的 Prompt Library 把我本地已经在用、以及已经归档过的 prompt 资源拆开收纳。这里不混成一坨，而是按“单条 prompt / prompt 源码项目 / system prompt 归档”分开挂，同时在首页直接放一批可读入口。\nLocal Workflow Prompts ChatGPT System Prompt Snapshot RHEL / avocado / tp-qemu Debug User Prompt Microsoft Docs MCP Prompt oh-my-opencode Setup Prompt 去 AI 特征 Prompt WJI Local Rules Prompt - Sanitized Prompt Source Projects CC-Haha Prompt Source Index System Prompt Archives OpenAI / ChatGPT / Codex System Prompt Archive OpenAI 直接阅读：Codex, ChatGPT GPT-5 Agent Mode, GPT-4.1, GPT-4o Anthropic / Claude System Prompt Archive Anthropic 直接阅读：Claude Code, Claude Sonnet 4, Claude 3.7 Full Tools Google / Gemini System Prompt Archive Gemini 直接阅读：Gemini 2.5 Pro Webapp, Gemini 2.0 Flash Webapp, Gemini Diffusion Other AI System Prompt Archive 其他直接阅读：xAI Grok 4, Perplexity Voice Assistant, Warp 2.0 Agent, Proton Luma Vibe Coding 把我自己已经跑通过、并且能继续交给 AI 复现的项目整理成一个手册库。这里不只放一个项目，而是持续收纳 Gemini、Cursor、语音输入、图像生成、中转服务之类的 Vibe Coding 项目，每个项目再进入自己的执行手册。\n前台文本发送助手 Xfyun Wayland 实时语音输入 Nanobanana AI 时尚工作室 Gemini API 中转站 Document General Help Manpages BashShell 基础 Red Hat Links 所有红帽产品和文档\n上下游预览 操作系统下载 Red Hat 软件包 证书查询\n付费订阅查询\nFunny 红帽创造营第一期 - 多才多艺的开源人 清华武汉籍女生英文演讲：我三岁会唱国歌，一场疫情才理解了其中真正的含义 Wake up Links Friend Links CCAV - 创业哥们 h0u5er - 黑客好友 fishstar - 大学室友 xiaoming - 大学室友 Alberthua - GLS 华老师 Tool Links 手写风格画图 ","permalink":"https://jiwenkang.com/links/","summary":"\u003ch2 id=\"tech-knowledges\"\u003eTech Knowledges\u003c/h2\u003e\n\u003ch4 id=\"general-linux\"\u003eGeneral Linux\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.cnblogs.com/itxdm/p/filesystem_base_version.html\"\u003e文件系统基础版\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"/redhat/typora/202208281647545.png\"\u003eLinux 的启动流程图\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"/redhat/typora/202208221147727.png\"\u003eLinux 下 grub 引导修复\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://linux.cn/article-7718-1.html\"\u003e如何将 Linux 系统转移至 LVM 卷\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.cnblogs.com/itxdm/p/linuxdate_recover.html\"\u003eLinux 意外操作后如何进行数据抢救\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"linux-commond-line\"\u003eLinux Commond Line\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.cnblogs.com/itxdm/p/5936907.html\"\u003eLinux 下 find 命令\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://linux.cn/article-7475-1.html\"\u003eLinux 下 OpenSSH 高级运用两则\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"cshell-programing\"\u003eC/Shell Programing\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://www.cnblogs.com/itxdm/p/c_pointer2.html\"\u003e一文让你明白C语言中的指针 \u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://www.cnblogs.com/itxdm/p/c_pointer_of_pointer.html\"\u003eC 语言中指针的指针究竟是个啥\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://www.cnblogs.com/itxdm/p/C_language_struct_structure.html\"\u003eC 语言 struct 结构体\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://www.cnblogs.com/itxdm/p/c_visiting_from_function.html\"\u003eC 语言之函数访问 \u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://www.cnblogs.com/itxdm/p/something_of_shellscirpt.html\"\u003eLinux 下 Shell 脚本几种基本命令替换区别\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"/BashShell/index.html\"\u003eLinux 下 Shell 脚本的基础/高级编程\u003c/a\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e","title":"知识库"}]