Docker 里的 PPTP 能认证却不携带流量
PPTP 需要 TCP 1723 做控制、GRE 承载载荷。容器里常常缺少 conntrack 助手,于是登录成功、其他全不通。
客户端完成认证,PPP 协商出一个地址,然后一切静止。Ping 失败,浏览器失败,而容器在整个过程中看起来都是连着的。这个现象不是路由表问题,因为路由问题通常也会弄断控制连接。问题出在载荷路径上,而在 PPTP 里,载荷与刚刚成功的那个协议并不是同一个协议。
五项检查,按最省时间的顺序排列。
检查 1:把控制通道与载荷分开
PPTP 通过 TCP 1723 认证,然后把 PPP 载荷装在 GRE(IP 协议 47)里传输。GRE 既不是 TCP 也不是 UDP,所以任何转发控制端口的规则对它都无效。放行 1723 的 NAT 或防火墙可能悄悄丢掉协议 47,会话就半建成地挂着,看上去像成功。
如果登录成功却没有数据包回来,先假定是 GRE,再去证明。
检查 2:确认宿主机内核里的助手
连接跟踪没有助手就无法把 TCP 控制连接和 GRE 流关联起来。模块是 nf_conntrack_pptp,它位于宿主机内核里,在容器镜像里加载毫无意义,因为模块是宿主机级别的。在 Docker 宿主机上执行 lsmod | grep nf_conntrack_pptp 才是检查。如果缺失,modprobe nf_conntrack_pptp 会为当前内核加载它,但对下次启动毫无保证,所以也要写进宿主机配置。
重启后消失的模块,下个月还是同一张工单。
检查 3:读一读关掉它的内核策略
即使模块在,4.7 之后的内核默认把 net.netfilter.nf_conntrack_helper 设为 0,这等于关闭了所有协议的助手自动分配。正是这个默认值让正确安装的模块依然不起作用:代码在,策略却说不许用。sysctl net.netfilter.nf_conntrack_helper 会报告当前值,运行时把它设为 1 就能让助手重新可用。如果设计允许,就通过宿主机的 sysctl 配置把这个值持久化。
自动分配让流量隐式地创建期望,这正是默认值被改掉的原因。把它限制在可信宿主机上,或者使用针对该助手的显式规则。
检查 4:单独看容器这一侧的路径
加载了助手并不等于有了路由。容器需要一条通往 PPTP 服务器的路径,宿主机的转发必须双向放行 GRE:iptables -L FORWARD -v -n 会显示协议 47 是否被接受、计数器是否在动。Docker 的 masquerade 规则只转换 TCP 和 UDP,不转换协议 47,所以桥接容器可能需要显式的 forward 放行,在某些宿主机上还需要自己的转换规则。同时确认容器具备其镜像所期望的 PPP 设备支持。
要看计数器,而不是规则文本。存在却匹配不到任何东西的规则只是装饰。
检查 5:证明 GRE 被跟踪,再决定 PPTP 的去留
让客户端保持运行,查看跟踪表。已建立的 PPTP 控制条目加上一条相关的 GRE 条目,说明助手正在关联两条流,剩下的故障在别处。只有控制条目而没有 GRE 条目,说明载荷被丢弃或未被跟踪,那就回到检查 2 和 3。
PPTP 本身已是遗留协议,MS-CHAPv2 也很弱。如果服务器同时提供 L2TP/IPsec、IKEv2 或 WireGuard,换到其中之一就能整类消除这些问题。
在必须保留 PPTP 的地方,把客户端跑在宿主机上而不是容器里可以少一层 NAT 和过滤,这往往是更便宜的决定。