首先,这个问题看起来好像是关于Linux的,但在我看来,它是关于基本路由概念的。
我碰巧有以下配置:
我想要做的是确保服务器(CentOS 7)上的对称路由,以便来自它的传入和传出流量对于任何一对节点(使用两个网络接口)都采用相同的路径。
假设我192.168.0.210/24
为eno1
和设置了静态 IP 地址(和与其他 Linux 发行版中的和相同)。192.168.1.210/24
eno2
eno1
eno2
eth0
eth1
然后我创建了2个路由表(每个网络接口一个)/etc/iproute2/rt_tables
:
...
101 net1
102 net2
然后,我在每个路由表中创建了路由,并创建了策略路由规则,以将出站流量引导到适当的路由表,如下所示:
$ ip route show table net1
default via 192.168.0.1 dev eno1
192.168.0.0/24 dev eno1 scope link
$ ip route show table net2
default via 192.168.1.1 dev eno2
192.168.1.0/24 dev eno2 scope link
$ ip rule show
0: from all lookup local
101: from 192.168.0.0/24 lookup net1
102: from 192.168.1.0/24 lookup net2
32766: from all lookup main
32767: from all lookup default
这些是我做的第一批测试(效果符合预期):
$ ip route get 192.168.100.100 from 192.168.0.210
192.168.100.100 from 192.168.0.210 via 192.168.0.1 dev eno1
cache
$ ip route get 192.168.100.100 from 192.168.1.210
192.168.100.100 from 192.168.1.210 via 192.168.1.1 dev eno2
cache
$ ip route get 192.168.100.100
192.168.100.100 via 192.168.1.1 dev eno2 src 192.168.1.210
cache
最后,tshark
我使用该工具开始监控网络接口eno1
并eno2
通过每个接口发出请求,例如:
$ curl --interface eno1 https://google.com
$ curl --interface eno2 https://google.com
$ traceroute -i eno1 google.com
$ traceroute -i eno2 google.com
$ ping -I eno1 -c 2 google.com
$ ping -I eno2 -c 2 google.com
前 4 个命令按预期工作(tshark
每个网络接口上都能正确捕获传入和传出流量),但其他ping
命令则不然。以下是tshark
这些ping
命令的输出:
如您所见,ping
仅适用于eno2
。 经过反复试验,我意识到 仅ping
适用于与通用默认网关关联的网络接口:
$ route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.1.1 0.0.0.0 UG 0 0 0 eno2
169.254.0.0 0.0.0.0 255.255.0.0 U 1002 0 0 eno1
169.254.0.0 0.0.0.0 255.255.0.0 U 1003 0 0 eno2
192.168.0.0 0.0.0.0 255.255.255.0 U 0 0 0 eno1
192.168.1.0 0.0.0.0 255.255.255.0 U 0 0 0 eno2
据我理解,ping
即使没有设置通用默认网关,命令也应该有效,因为已经设置了默认网关net1
并且net2
应该使用,这是正确的吗?
为什么会发生这种情况?这与工作方式有关吗ping
?为什么前 4 个命令有效?
答案1
两者之间是有区别的绑定到接口和绑定到 IP 地址。虽然 2 个工作案例确实绑定到接口,但它们避免了ping
遇到的问题(稍后解释)。让我们从修复开始ping
。我复制了 OP 的设置以帮助给出说明。
路由查找时仅有的绑定到接口不是:
ip route get from 192.168.1.210
但:
# ip route get oif eno2 to 192.168.100.100
192.168.100.100 dev eno2 src 192.168.1.210 uid 0
cache
这里不涉及表 101 和 102,因为查找中没有指定本地源地址。此外,主路由表中没有 192.168.100.100 的默认路由。但由于接口被强制eno2
,因此会自动创建此类默认路由...无网关. 可见的症状是,将会有从 192.168.1.210 到 192.168.100.100 发出的 ARP 请求,因为虚假路由告诉 192.168.100.100 是可直接访问的。
OP 还添加了(通常无用的)具有更高度量的附加默认路由,例如:
ip route add default via 192.168.1.1 dev eno2 metric 101
然后:
# ip route get oif eno2 to 192.168.100.100
192.168.100.100 via 192.168.1.1 dev eno2 src 192.168.1.210 uid 0
cache
现在,由于已经有一个匹配的默认路由,因此eno2
被选中,并且网关正确。ping
现在可以正常工作。路由表 102 仍然不涉及。
绑定接口的路由规则选择器
实际正确的方法是使用表 102 中定义的路由oif
IP 规则中的选择器:
oif
姓名选择要匹配的传出设备。传出接口仅适用于源自来自绑定到设备的本地套接字。
让我们使用它(并删除第二条默认路线以表明它不再需要):
ip route delete default via 192.168.1.1 dev eno2 metric 101
ip rule add oif eno1 lookup 101
ip rule add oif eno2 lookup 102
现在查找将匹配并变为:
# ip route get oif eno2 to 192.168.100.100
192.168.100.100 via 192.168.1.1 dev eno2 table 102 src 192.168.1.210 uid 0
cache
这次,由于选择器匹配,因此使用了正确的路由表,并使用网关定义了默认值。
这才是必须要做的事。
注意:ping
还接受绑定到 IP 地址(ping -I 192.168.1.210 -c 2 google.com
)或甚至两者(ping -I eno2 -I 192.168.1.210 -c 2 google.com
)。这些情况无需额外的路由规则即可工作,如前所述。
前两个案例为什么会成功呢?
(删除上面的更正然后...)
正如在先前的错误路由解析中所看到的:
# ip route get oif eno2 to 192.168.100.100 192.168.100.100 dev eno2 src 192.168.1.210 uid 0 cache
正确的 IP 源地址仍会被选中。只要 TCP 必须发出数据包,其路由查找就会显示源地址 192.168.1.210。此情况匹配规则首选项 102 中的选择器:
# ip route get from 192.168.1.210 oif eno2 to 192.168.100.100
192.168.100.100 from 192.168.1.210 via 192.168.1.1 dev eno2 table 102 uid 0
cache
表 102 仍然被规则首选项 102 选中。一旦选择了合适的表,无论为什么选择它,都会发生正确的路由。
对于 UDP 来说,情况稍微复杂一些,因为这取决于 UDP 客户端是否使用connect(2)
然后其行为类似于之前的 TCP 情况:将使用源地址,或选择不使用connect(2)
。例如,如果没有缺少路由规则,此命令将无法正确路由,因为它不使用connect(2
),并且将在没有源的情况下查询路由堆栈(即:使用 INADDR_ANY = 0.0.0.0):
echo test | socat udp4-datagram:203.0.113.1:8888,so-bindtodevice=eno2 -
而这个会成功,因为它使用了connect(2)
:
echo test | socat udp4:203.0.113.1:8888,so-bindtodevice=eno2 -
当然,一旦添加了两个缺失的路由规则,两者都可以正常工作。
可以connect
使用以下命令检查 traceroute 是否使用strace
:
...
socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) = 3
setsockopt(3, SOL_SOCKET, SO_BINDTODEVICE, "eno2\0", 5) = 0
...
connect(3, {sa_family=AF_INET, sin_port=htons(33434), sin_addr=inet_addr("203.0.113.1")}, 28) = 0
...