一个客户端需要访问另一个客户端的 SSH 服务,而地址池的其余部分应保持与今天相同的隔离状态。诱人的开关是 client-to-client,它一次性为所有人打开对等访问,并把决定权交给 OpenVPN 而不是内核。更窄的答案是保持该开关关闭,让服务器自身的路由和防火墙恰好承载一条流。

机制比规则文本更重要,因为两种做法把数据包放在不同位置。知道自己选了哪一种,就能同时解释那条看起来什么都没做的规则,以及不用它也能连上的对等端。

client-to-client 把数据包移到了哪里

启用 client-to-client 时,服务器在内部中继对等端的数据包,而不是从隧道设备再发出去,因此数据包永远不会进入内核的转发路径。这很方便,也意味着你的防火墙永远看不到本该由它监管的那条流。保持该选项关闭,服务器会把数据包从同一接口再发出去,这就把它放到了 netfilter 面前。同接口进、同接口出,FORWARD 链因此有话事权。

交换是明确的:你得到一个策略点,同时接过捷径替你做的路由与转发工作。对单条流而言这点工作量很小,但你每加一个例外它就会增长。现在就把例外数清楚,因为对等策略真正住的地方是规则集。

对等端数据包实际走过的路径

假设 Linux Community 的 OpenVPN 2.6 服务,采用用户态 TUN 处理而非 DCO,子网拓扑,tun0,以及 10.44.0.0/24 的地址池,客户端 A 位于 10.44.0.21,客户端 B 位于 10.44.0.37。在子网拓扑下,A 收到的地址和掩码已经覆盖整个池,因此 A 无需额外的推送路由就把数据包发往隧道设备。要在 A 上核实实际路由,而不是假设存在一条。

在服务器上,数据包到达时目的地址是 B,而这不是本地地址,因此被转发。要让这件事发生,IPv4 转发必须开启;路由查找必须把数据包从 tun0 再发出去。于是防火墙看到的是一条两侧接口相同的转发流,而这正是大多数规则集漏掉的细节。这里不需要源地址转换,保持 A 的地址不变,才让 B 能应用它自己的基于源的规则。

服务器上的两条规则,按此顺序

整个授权由三条规则承担:转发、成对例外,以及针对隧道对的默认丢弃。接受规则点名了这对地址和服务,因此地址池扩大时授权依然成立。把它放在任何一刀切的隧道到隧道流量拒绝之前,因为先匹配者胜:

sysctl -w net.ipv4.ip_forward=1
iptables -A FORWARD -i tun0 -o tun0 -s 10.44.0.21 -d 10.44.0.37 -p tcp --dport 22 -j ACCEPT
iptables -A FORWARD -i tun0 -o tun0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A FORWARD -i tun0 -o tun0 -j DROP

这些命令只是示意,属于你主机用来持久化规则的机制;运行时插入的规则会在下次启动时消失。正是有状态规则让 B 的回复和 TCP 握手无需第二条镜像放行就能返回。

事后要看计数器而不是规则列表,因为从未命中的规则看起来和生效的规则一模一样。如果计数器一直为零,数据包并没有按你以为的路径走。

B 仍然必须放行什么

SSH 服务由 B 运行,所以由 B 决定隧道地址上是否有东西在监听,以及它自己的主机防火墙是否接受该源。服务器上的网络放行并不等于 B 上任何账户的许可,主机密钥校验和用户认证仍与从任何其他地方来时一样适用。

先测正向用例:从 A 到 B 隧道地址的新 SSH 连接,应以预期的主机密钥被接受。然后测另一个对等端或另一个端口,并确认被拒绝,因为一条悄悄放行一切的授权,在真正要紧之前与有效授权毫无区别。这个测试的两半都必要,而人们跳过的正是后一半。

记录分配的地址、两台客户端上的实际路由,以及服务器和 B 上的确切规则。能 ping 通网关或服务器上有开放端口,都证明不了这些中的任何一条,因为在两端都成立的同时,对等转发可能仍被阻断。趁隧道还开着就把这些数字写下来,因为 客户端重连后拿到的地址可能已经不同。

相关文章