一天调通 strongSwan:从「能连无网」到路径平面复盘

发布于 2 小时前  8 次阅读


一天调通 strongSwan:从「能连无网」到路径平面复盘

起点

现象很典型:手机 IKEv2 能 ESTABLISHED,却上不了网

环境大致是:

  • 主机 Ubuntu(4.15 内核)、上联 ens160
  • Docker network_mode: hostvimagick/strongswan
  • 客户端拿到 VIP 后 bytes_i 在涨,bytes_o 接近 0
  • 第一反应总是「再加一条 MASQUERADE / FORWARD ACCEPT」

这一天真正有用的,不是再堆规则,而是把故障拆成三层,并按顺序证明每一层:

  1. Load plane:charon 是否真的加载了 conf / secrets
  2. Tunnel plane:SA 是否 ESTABLISHED、VIP 是否分配、ESP 是否有双向字节
  3. Path plane:解密后的包是否进入 FORWARD、是否 SNAT 成主机 IP 再出去

没有这三层,任何「看起来正确」的 iptables 都只是安慰剂。

代价

按错误顺序修,这一天至少付过这些学费:

1. VIP 与主机网段重叠

最初 VIP 池落在主机 /22 里面。
结果是:SA 能建,明文看起来也有,但回程和转发语义全乱。
改成不重叠的 10.20.30.0/24 之后,至少「地址平面」不再自己绊自己。

2. 镜像自带 NAT 是死的

容器用了 bare entrypoint(ipsec start --nofork),镜像里那套 env NAT 根本不会执行
宿主机上还残留着指向不存在网卡(如 eth0)的 MASQUERADE,计数永远是 0。
这会制造一种幻觉:规则在,问题却像「客户端配置不对」。

3. 用计数器证明 path 断了,而不是继续加 ACCEPT

关键证据是一组对照:

观测点 结果
mangle PREROUTING(VIP) 有包
filter FORWARD 0
SNAT 0
tcpdump 上联 明文 VIP→公网,源地址从未变成主机 IP

结论不是「少一条 ACCEPT」,而是 转发路径根本没走进 filter FORWARD
在 legacy iptables + nf_tables / Docker 混用的主机上,这尤其容易发生:iptables -L 看起来很对,实际 filter hook 是空的。

4. 有害实验:物理口 disable_policy=1

在上联口上关 policy,会把 inbound 路径直接打穿成 XfrmInNoPolsbytes_o 更差。
disable_policy=1 只属于 VTI 设备,不属于物理上联。

5. mark + recreate 后「连 Connections 都空了」

mark=42 并 recreate 后,客户端变成 NO_PROPOSAL_CHOSENConnections: 空。
第一反应会怪 mark 语法;实际是 load plane 塌了

  • 宿主机 conf 还在
  • 容器内 /etc/ipsec.conf 0 字节
  • /etc/ipsec.secrets 是指向 volume 的 symlink,真实文件 0 字节

根因是把 bind mount 打在镜像 symlink 路径上。
Empty Connections after mark 优先查 in-container wc -c,不要先怪算法或客户端。

调整

后半段的有效动作,全部可以收成「一条权威路径」,而不是「救急脚本 + 永久脚本」两套。

Load plane:/config + entrypoint 拷贝

不要再 bind 到镜像自带的 symlink 路径。稳定模式是:

  • 把 conf/secrets 挂到 /config/*.ro
  • entrypoint 里 cp 到真实 /etc/ipsec.d/.../etc/ipsec.conf
  • secrets chmod 600(宿主机与容器内一致)
  • 启动日志硬门槛:BOOT conf=<nonzero> secrets=<nonzero>,且 Connections: 非空、loaded IKE secret 出现

Tunnel / VTI plane:官方模型,不发明第二套

  • charon:user/group=rootinstall_routes=no
  • 不要为了图省事设 install_policies=no
  • conf 里 mark / mark_in / mark_out 与 VTI key 一致
  • 设备名优先 ipsec0(部分 iproute2 对 vti* 名有特殊路径)
  • disable_policy=1 只打在 ipsec0
  • 若 inbound ESP state 缺 mark,需要按主机 ip xfrm 语法重建;错误写法(空格 mask、slash 形式)会直接失败

Path plane:host 单元,而不是 compose

这是最容易反复踩坑的分界:

平面 谁负责 docker restart 后还在吗 主机 reboot 后还在吗
conf/secrets/charon compose + entrypoint 视 volume 视 docker 自启
ipsec0 / VIP 路由 / SNAT / nft forward host 脚本/单元 否,除非再跑 仅当 systemd 启用

权威脚本只留一条:/root/ipsec-vpn/vpn-path-persist.sh
它要做的事很具体:

  1. 保证 nftinet filter forward 上有 live 的 counter accept(空链 ≠ 活的 filter path)
  2. ip_forward=1,上联 rp_filter=0永不给上联 disable_policy=1
  3. 清掉上联上的 VIP /32 host-route
  4. 创建/替换 ipsec0(VTI key=mark),VIP 网关地址上 VTI,route replace VIP_NET dev ipsec0
  5. iptables:ipsec0 双向 FORWARD + VIP SNAT 到主机 IP
  6. 剥掉 legacy MASQUERADE -o eth0 / 旧池规则
  7. secrets 保持 600

配套:

  • bootvpn-path.service(oneshot,After docker/network)
  • 热自愈vpn-path-watch.service(短轮询 + docker events 的 start/restart)
  • 观测:定时写 status 日志;诊断用的 LOG 规则用完就剥,避免把生产路径变成噪声场

旧的 MASQ-only 单元(vpn-nat 一类)直接退役。Jay 的反双轨要求在这里不是风格问题,是唯一能让「下次别再修一遍」成立的条件。

用最小探针验收 path,而不是让客户端一直 ping

服务端对照一张表就够:

mangle FWD filter FWD SNAT 判断
>0 0 0 filter hook 死了 → 先修 nft forward
>0 >0 0 SNAT 出接口 / 顺序
>0 >0 >0 path 活了

手机侧成功定义也很窄:

  • ESTABLISHED
  • VIP 上 FWD/SNAT 有计数
  • bytes_o 持续增长

不是「又加了一条看起来很对的规则」。

现在

落成后的状态可以这样描述:

  • Load/config + cp,Connections 与 secret 可回读
  • Path:唯一脚本 vpn-path-persist.sh;boot 单元 + watch 热自愈
  • 观测:status 日志安静;临时 LOG 不常驻
  • 密钥:secrets 600;聊天里出现过的 PSK 已轮换,旧聊天密钥作废
  • 自测:删 ipsec0 → 自愈;docker restart → path 再挂上;ping 时 FWD/SNAT > 0

还没做、也不该假装做完的:

  • 全局 PSK + rightid=%any 不是客户端管理平面
  • ipsec statusall 只能观察,不能吊销单设备
  • 真要管设备,下一阶是每设备 PSK / EAP / 证书

另外,若目标是「断电也不挂」,只验证 docker 热重启不够。必须整机 reboot,用 uptime/boot time 证明真的重启过;有的主机 unattended-upgrades 会拖住 shutdown,表面像 reboot 实则没有。

留下的判断

  1. 先分层,再动手。 Load 没绿时,所有 NAT/VTI 讨论都是噪声。
  2. compose 绿灯 ≠ 用户有网。 charon 活着只证明 tunnel 可能活;path 在 host。
  3. 计数器比感觉可靠。 mangle 有、filter 无,就是 hook 问题,不是「再 ACCEPT 一次」。
  4. 一条权威路径。 path 脚本、boot 单元、watch 热自愈是同一条线的三个挂载点,不是三套方案。
  5. 密钥出现在聊天里就轮换。 权限 600 是地板,不是天花板。
  6. 共享 PSK 解决的是连通,不是治理。 别把 statusall 误当成账号系统。
  7. 验收要对准用户目标。 手机能出网、path 在 docker 事件和(如需要)整机 reboot 后仍在,才叫调通;「规则看起来对」不算。

基于 jasper 主机 strongSwan roadwarrior 实战复盘整理。命令与单元名以现场权威脚本为准;平行 fix 脚本应归档,不应继续分叉。


或许明日太阳西下倦鸟已归时