为 ocserv 配置 IPv6:一次由 MTU 1200 引发的隐蔽故障排查

给一台已经正常运行的 ocserv 服务器补上 IPv6,看起来只需要打开地址池、转发和防火墙。实际配置完成后,我遇到了一个很绕的问题:服务器有公网 IPv6,IPv6 转发和 NAT66 也都正常,客户端却始终只有 IPv4。

最后发现,问题不在 IPv6 地址池,也不在 OpenConnect 客户端,而是原配置中的 mtu = 1200。扣除隧道和加密开销后,有效 MTU 只剩 1114,低于 IPv6 要求的 1280。ocserv 因此主动停止向客户端下发 IPv6。

这篇文章记录完整配置和排查过程。

环境与目标

本次环境:

  • Ubuntu 20.04
  • ocserv 1.1.6
  • ocserv 监听 TCP/UDP 666
  • 服务器公网 IPv6:2406:da18:d4c:c000:1173:148b:7e09:a4fa
  • VPN IPv4 地址池:10.10.10.0/24
  • VPN IPv6 地址池:fda9:4efe:7e3b:03ea::/48
  • 出口网卡:eth0
  • UFW 管理防火墙

目标是让 VPN 客户端同时获得 IPv4 和 IPv6,并让公网 IPv6 流量从 ocserv 服务器出口访问互联网。

这里给客户端分配的是 ULA 地址 fda9:...,再通过 NAT66 转换成服务器的公网 IPv6。LinuxBabe 的 ocserv IPv6 配置示例采用了同样的结构。[1]

如果云服务商或机房向服务器路由了可供客户端使用的公网 IPv6 前缀,也可以直接路由该前缀,不必使用 NAT66。本文讨论的是服务器只有单个公网 IPv6、没有可委派前缀时的实用方案。

确认服务器的 IPv6 出口

先确认服务器自己能通过 IPv6 访问互联网:

1
2
3
ip -6 addr show scope global
ip -6 route
curl -6 https://api64.ipify.org

正常情况下应看到公网 IPv6、IPv6 默认路由,以及 curl -6 返回的服务器出口地址。

如果服务器自己都不能访问 IPv6,先处理云平台 IPv6 分配、路由表、安全组和网络 ACL。ocserv 无法替代底层网络配置。

配置 ocserv IPv6 地址池

编辑:

1
sudo nano /etc/ocserv/ocserv.conf

加入或取消注释:

1
2
ipv6-network = fda9:4efe:7e3b:03ea::/48
ipv6-subnet-prefix = 64

可以同时下发 IPv6 DNS:

1
2
dns = 2001:4860:4860::8888
dns = 2606:4700:4700::1111

本次完整的地址池相关配置如下:

1
2
3
4
5
6
7
8
9
10
device = vpns

ipv4-network = 10.10.10.0
ipv4-netmask = 255.255.255.0

ipv6-network = fda9:4efe:7e3b:03ea::/48
ipv6-subnet-prefix = 64

mtu = 1400
try-mtu-discovery = true

ipv6-network 使用 /48,ocserv 再按 ipv6-subnet-prefix = 64 为连接分配子网。修改后需要重启 ocserv,新建的 VPN 会话才会使用新参数。

开启 IPv6 转发

检查当前值:

1
sysctl net.ipv6.conf.all.forwarding

如果结果不是 1,在 /etc/sysctl.conf 或独立的 sysctl 配置文件中加入:

1
2
net.ipv6.conf.all.forwarding = 1
net.ipv6.conf.default.forwarding = 1

应用配置:

1
sudo sysctl -p

再次确认:

1
2
sysctl net.ipv6.conf.all.forwarding
sysctl net.ipv6.conf.default.forwarding

配置 UFW 转发和 NAT66

客户端使用的是 ULA 地址,访问公网 IPv6 时需要在出口网卡做 MASQUERADE。

/etc/ufw/before6.rules 的 NAT 表中加入:

1
2
3
4
*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s fda9:4efe:7e3b::/48 -o eth0 -j MASQUERADE
COMMIT

eth0 换成服务器的实际出口网卡。建议限制源地址为 VPN IPv6 网段,不要对所有 IPv6 流量无条件做 MASQUERADE。

ufw6-before-forward 链允许 VPN IPv6 网段转发:

1
2
-A ufw6-before-forward -s fda9:4efe:7e3b::/48 -j ACCEPT
-A ufw6-before-forward -d fda9:4efe:7e3b::/48 -j ACCEPT

确认 /etc/default/ufw 中启用了 IPv6:

1
IPV6=yes

应用规则后检查:

1
2
3
sudo ip6tables -t nat -vnL POSTROUTING
sudo ip6tables -vnL FORWARD
sudo ip6tables -vnL ufw6-before-forward

LinuxBabe 的教程也要求 IPv6 forwarding、UFW IPv6 转发规则和出口 MASQUERADE。[1]

第一次失败:所有客户端都只有 IPv4

最初配置完成后,服务端看起来没有问题:

  • 服务器有公网 IPv6;
  • net.ipv6.conf.all.forwarding = 1
  • UFW 已允许 fda9:4efe:7e3b::/48
  • ip6tables 已存在 MASQUERADE;
  • ocserv 配置中已经启用 IPv6 地址池。

但 Windows OpenConnect GUI 的 CONNECT 响应只有:

1
2
X-CSTP-Address: 10.10.10.108
X-CSTP-Netmask: 255.255.255.0

没有:

1
X-CSTP-Address-IP6: fda9:.../64

macOS Cisco Secure Client 提示本次 VPN 不支持 IPv6,并阻断 IPv6 流量。macOS OpenConnect 9.21 也只显示:

1
Configured as 10.10.10.71

换了三个客户端,结果完全一样,问题显然不能再简单归因于某个 GUI 或网卡驱动。

容易误导的 occtl 输出

当时执行:

1
sudo occtl show user <用户名>

服务端却能看到:

1
2
IPv4: 10.10.10.71
IPv6: fda9:4efe:7e3b:...

这很容易让人误以为 IPv6 已经下发成功。实际上,ocserv 可以先为会话生成或预留 IPv6 地址,之后再根据客户端能力和隧道条件决定是否把该地址写入 CSTP 响应。

判断客户端是否真正收到 IPv6,应该同时检查:

  1. 客户端 CONNECT 响应是否包含 X-CSTP-Address-IP6
  2. 客户端 VPN 网卡是否有 fda9: 地址;
  3. 客户端是否安装了通过 VPN 的 IPv6 路由。

只看 occtl 不够。

真正根因:有效 MTU 低于 IPv6 最低要求

原 ocserv 配置中有:

1
mtu = 1200

客户端实际收到:

1
2
X-CSTP-Base-MTU: 1200
X-CSTP-MTU: 1114

1200 还要扣除 CSTP、DTLS、IP、UDP/TCP 和加密相关开销,最终可用于隧道数据的 MTU 只有 1114。

IPv6 规定每条链路必须支持至少 1280 字节的 MTU;如果链路无法直接承载,链路层需要提供分片与重组能力。[2]

ocserv 1.1.6 在发送地址前有一段明确检查:当计算出的数据 MTU 小于 1280,并且会话原本允许 IPv6 时,ocserv 会把该会话标记为不使用 IPv6。[3]

对应逻辑可以概括为:

1
2
3
4
5
if (DATA_MTU(ws, ws->link_mtu) < 1280 &&
ws->vinfo.ipv6 &&
req->no_ipv6 == 0) {
req->no_ipv6 = 1;
}

这解释了之前所有看似矛盾的现象:

1
2
3
4
5
6
7
8
9
ocserv 已生成 fda9 地址

有效隧道 MTU 只有 1114

ocserv 禁止该会话使用 IPv6

CONNECT 响应不发送 X-CSTP-Address-IP6

所有客户端最终都只有 IPv4

Windows 日志中反复出现的报文丢弃也指向同一问题:

1
Drop oversized packet retrieved from Wintun adapter (1141 > 1114)

它不是单独的 Wintun 故障,而是低 MTU 已经开始影响正常数据传输。

修复 MTU

不能简单把 mtu 从 1200 改成 1280,因为扣除协议开销后,有效 MTU 仍然会低于 1280。

本次环境中:

1
2
3
基础 MTU:1200
有效 MTU:1114
估算开销:约 86 字节

要让有效 MTU 达到 1280,基础值理论上至少需要约 1366。最终采用:

1
2
mtu = 1400
try-mtu-discovery = true

重启:

1
sudo systemctl restart ocserv

重连后,服务端 VPN 接口 MTU 变成:

1
MTU: 1334

有效值已经高于 1280。客户端随即收到:

1
Configured as 10.10.10.70 + fda9:4efe:7e3b:.../64

Windows OpenConnect GUI 和 macOS Cisco Secure Client 也都能正常显示服务器的 IPv6 出口地址。

1400 不是适用于所有网络的固定答案。正确原则是:

  • 扣除隧道开销后的有效 MTU 必须不低于 1280;
  • 还要考虑客户端到服务器之间的真实路径 MTU;
  • 调整后观察 DTLS/CSTP 日志中是否还有 oversized packet、分片或重传问题。

验证清单

服务端

1
2
3
4
5
6
7
ip -6 addr show scope global
ip -6 route
sysctl net.ipv6.conf.all.forwarding
sudo ip6tables -t nat -vnL POSTROUTING
sudo ip6tables -vnL ufw6-before-forward
sudo occtl show users
sudo occtl show user <用户名>

VPN 接口应出现:

1
inet6 fda9:4efe:7e3b::1/128

并有客户端子网路由:

1
fda9:4efe:7e3b:<子网>::/64 dev vpnsX

客户端

OpenConnect 日志应包含:

1
X-CSTP-Address-IP6: fda9:.../64

或者连接摘要:

1
Configured as 10.10.10.x + fda9:.../64

检查地址和出口:

1
2
ifconfig
curl -6 https://api64.ipify.org

IP 检测网站显示的 IPv6 应变成 ocserv 服务器的公网 IPv6,而不是客户端本地运营商分配的地址。

观察计数器

连接客户端并执行一次 curl -6 后,在服务器观察:

1
2
sudo ip6tables -vnL ufw6-before-forward
sudo ip6tables -t nat -vnL POSTROUTING

来自 fda9:4efe:7e3b::/48 的转发和 NAT 计数应增加。如果客户端已经有 fda9: 地址,但计数始终为零,通常是客户端没有安装 IPv6 VPN 路由。

小结

一套可用的 ocserv IPv6 配置至少包括:

  1. ipv6-networkipv6-subnet-prefix
  2. Linux IPv6 forwarding;
  3. UFW/iptables 转发规则;
  4. 使用 ULA 时的 NAT66;
  5. 不低于 IPv6 要求的有效隧道 MTU;
  6. 客户端 IPv6 地址和路由验证。

这次最耗时间的地方,是服务器内部已经能看到 fda9: 地址,让排查方向一度转向客户端。真正有用的线索是 X-CSTP-MTU: 1114,以及 ocserv 源码中小于 1280 就禁用 IPv6 的判断。

以后再遇到“ocserv 配了 IPv6,但客户端始终只有 IPv4”,除了地址池、防火墙和 NAT,也应该第一时间看有效 MTU。

Sources

[1] https://www.linuxbabe.com/linux-server/ocserv-openconnect-vpn-advanced — Ocserv Advanced: Enable IPv6
[2] https://www.rfc-editor.org/rfc/rfc8200 — RFC 8200: Internet Protocol, Version 6
[3] https://gitlab.com/openconnect/ocserv/-/blob/1.1.6/src/worker-vpn.c — ocserv 1.1.6 worker-vpn.c MTU check