一天调通 strongSwan:从「能连无网」到路径平面复盘
起点
现象很典型:手机 IKEv2 能 ESTABLISHED,却上不了网。
环境大致是:
- 主机 Ubuntu(4.15 内核)、上联
ens160 - Docker
network_mode: host跑vimagick/strongswan - 客户端拿到 VIP 后
bytes_i在涨,bytes_o接近 0 - 第一反应总是「再加一条 MASQUERADE / FORWARD ACCEPT」
这一天真正有用的,不是再堆规则,而是把故障拆成三层,并按顺序证明每一层:
- Load plane:charon 是否真的加载了 conf / secrets
- Tunnel plane:SA 是否 ESTABLISHED、VIP 是否分配、ESP 是否有双向字节
- 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 路径直接打穿成 XfrmInNoPols,bytes_o 更差。
disable_policy=1 只属于 VTI 设备,不属于物理上联。
5. mark + recreate 后「连 Connections 都空了」
加 mark=42 并 recreate 后,客户端变成 NO_PROPOSAL_CHOSEN,Connections: 空。
第一反应会怪 mark 语法;实际是 load plane 塌了:
- 宿主机 conf 还在
- 容器内
/etc/ipsec.conf0 字节 /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=root,install_routes=no - 不要为了图省事设
install_policies=no - conf 里
mark/mark_in/mark_out与 VTIkey一致 - 设备名优先
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。
它要做的事很具体:
- 保证
nft的inet filter forward上有 live 的counter accept(空链 ≠ 活的 filter path) ip_forward=1,上联rp_filter=0,永不给上联disable_policy=1- 清掉上联上的 VIP
/32host-route - 创建/替换
ipsec0(VTI key=mark),VIP 网关地址上 VTI,route replace VIP_NET dev ipsec0 - iptables:
ipsec0双向 FORWARD + VIP SNAT 到主机 IP - 剥掉 legacy
MASQUERADE -o eth0/ 旧池规则 - secrets 保持 600
配套:
- boot:
vpn-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 实则没有。
留下的判断
- 先分层,再动手。 Load 没绿时,所有 NAT/VTI 讨论都是噪声。
- compose 绿灯 ≠ 用户有网。 charon 活着只证明 tunnel 可能活;path 在 host。
- 计数器比感觉可靠。 mangle 有、filter 无,就是 hook 问题,不是「再 ACCEPT 一次」。
- 一条权威路径。 path 脚本、boot 单元、watch 热自愈是同一条线的三个挂载点,不是三套方案。
- 密钥出现在聊天里就轮换。 权限 600 是地板,不是天花板。
- 共享 PSK 解决的是连通,不是治理。 别把
statusall误当成账号系统。 - 验收要对准用户目标。 手机能出网、path 在 docker 事件和(如需要)整机 reboot 后仍在,才叫调通;「规则看起来对」不算。
基于 jasper 主机 strongSwan roadwarrior 实战复盘整理。命令与单元名以现场权威脚本为准;平行 fix 脚本应归档,不应继续分叉。




Comments | NOTHING