问题
我遇到了一些相当奇怪的行为,一个看似不相关的默认网关路由产生了意想不到的副作用。我设法用一个最小的示例复制了这个问题。这里的目的主要是教育,我在尝试更复杂的场景时偶然发现了这一点。简而言之,我设法连接到192.168.0.3但我认为我不应该这么做。
我的笔记本电脑使用 WiFi 连接到家庭网络(192.168.0.0/24网络)。路由表如下:
kevin@kevin-UX305LA:~$ ip route
default via 192.168.0.1 dev wlp2s0 proto dhcp metric 600
169.254.0.0/16 dev wlp2s0 scope link metric 1000
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown
192.168.0.0/24 dev wlp2s0 proto kernel scope link src 192.168.0.210 metric 600
curl 192.168.0.3
和目前均可curl --interface wlp2s0 192.168.0.3
工作并给出以下响应:
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx/1.25.2</center>
</body>
</html>
现在,我继续删除与192.168.0.0/24网络,使得剩余的路线是:
kevin@kevin-UX305LA:~$ sudo ip route del default via 192.168.0.1
kevin@kevin-UX305LA:~$ sudo ip route del 192.168.0.0/24
kevin@kevin-UX305LA:~$ ip route
169.254.0.0/16 dev wlp2s0 scope link metric 1000
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown
此外,ip route get
绑定到接口和不绑定到接口的情况如下所示:
kevin@kevin-UX305LA:~$ ip route get 192.168.0.3
RTNETLINK answers: Network is unreachable
kevin@kevin-UX305LA:~$ ip route get oif wlp2s0 192.168.0.3
192.168.0.3 dev wlp2s0 src 192.168.0.210 uid 1000
cache
运行curl 192.168.0.3
结果为空curl: (7) Couldn't connect to server
,运行curl --interface wlp2s0 192.168.0.3
结果为空(curl 被阻止)。以下是 strace 显示的内容片段:
setsockopt(5, SOL_SOCKET, SO_BINDTODEVICE, "wlp2s0\0", 7) = 0
connect(5, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("192.168.0.3")}, 16) = -1 EINPROGRESS (Operation now in progress)
这很好,正如我所料,笔记本电脑不应该能够达到192.168.0.3。
现在,奇怪的部分来了。如果我添加一个虚拟默认网关(比如docker0我的路由表如下:
kevin@kevin-UX305LA:~$ sudo ip route add default via 172.17.0.2
kevin@kevin-UX305LA:~$ ip route
default via 172.17.0.2 dev docker0 linkdown
169.254.0.0/16 dev wlp2s0 scope link metric 1000
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown
运行curl 192.168.0.3
失败,但是运行curl --interface wlp2s0 192.168.0.3
成功并返回先前的 HTML 回复。
kevin@kevin-UX305LA:~$ curl 192.168.0.3
curl: (7) Failed to connect to 192.168.0.3 port 80 after 3068 ms: No route to host
kevin@kevin-UX305LA:~$ ip route get 192.168.0.3
192.168.0.3 via 172.17.0.2 dev docker0 src 172.17.0.1 uid 1000
cache
kevin@kevin-UX305LA:~$ curl --interface wlp2s0 192.168.0.3
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx/1.25.2</center>
</body>
</html>
kevin@kevin-UX305LA:~$ ip route get oif wlp2s0 192.168.0.3
192.168.0.3 dev wlp2s0 src 192.168.0.210 uid 1000
cache
我研究SO_BINDTODEVICE
过发现它仍然应该遵循路由表。为什么通过随机地址通过不同的接口添加默认网关会curl --interface wlp2s0 192.168.0.3
成功?
详细地址
以下命令列出了与接口关联的地址。我已检查过,在上述每个命令执行之后,此命令的结果始终相同。
kevin@kevin-UX305LA:~$ ip -br addr
lo UNKNOWN 127.0.0.1/8 ::1/128
wlp2s0 UP 192.168.0.210/24 fe80::7a03:3420:b8b0:4db7/64
docker0 DOWN 172.17.0.1/16
总之192.168.0.210这是笔记本电脑吗?192.168.0.3是网络上另一台托管 Web 服务器的机器,192.168.0.1是默认网关(我的路由器)。
环境
我正在运行 Linux Mint。以下是一些信息。
kevin@kevin-UX305LA:~$ uname -a
Linux kevin-UX305LA 5.15.0-84-generic #93-Ubuntu SMP Tue Sep 5 17:16:10 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
kevin@kevin-UX305LA:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Linuxmint
Description: Linux Mint 21.2
Release: 21.2
Codename: victoria
答案1
绑定到接口时,除非在此接口上找到足够的路由并重新使用(用于其网关),即使没有正确的路由,数据包也始终会发送到接口。因此,只有当不存在这样的默认路由时,这才重要。
注意:缺少网关可能会导致第 2 层接口出现问题(例如:尝试访问 8.8.8.8 将发送 8.8.8.8 的 ARP 请求。它不会导致第 3 层接口(如根本不需要网关的 IP 隧道)出现问题。由于在这个问题中,目的地无论如何都在 LAN 中,因此不需要网关,这也不会造成问题。
curl
可以通过查询内核的路由结果来验证命令的效果,如下所示ip route get
:
$ ip route get 192.168.0.3
RTNETLINK answers: Network is unreachable
# ip route get oif wlp2s0 192.168.0.3
192.168.0.3 dev wlp2s0 src 192.168.0.210 uid 0
cache
$ ip route get oif wlp2s0 8.8.8.8
8.8.8.8 dev wlp2s0 src 192.168.0.210 uid 1000
cache
正确解析后到达 192.168.0.3。
为了防止这种情况,可以添加一条策略规则,该规则仅在使用选择器绑定到接口时才选择备用路由oif
。通常,这将用于提供具有足够网关的路由,以解决上述缺少网关的问题,例如:
ip route add default via 192.168.0.1 dev wlp2s0 onlink table 1000
ip rule add oif wlp2s0 lookup table 1000
但对于这种特殊情况,目标是防止这种路线存在,这更加困难,因为oif wlp2s0
只保留dev wlp2s0
其中的路线:只需添加一个黑洞或者无法到达路由也不能包含,dev wlp2s0
所以它总是会被忽略。相反,这需要一个虚假的路由,这将使最终结果失败。选择自己的地址作为网关与选择没有网关相同:也行不通。因此,这需要在 LAN 中选择一个保证不存在的任意地址(以防止它执行 ICMP 重定向,如果它也是路由器)。让我们假设 192.168.0.4 不存在并将保留用于此用途:
# ip route add default via 192.168.0.4 onlink dev wlp2s0 table 1000
# ip rule add oif wlp2s0 lookup 1000
现在得到:
$ ip route get oif wlp2s0 192.168.0.3
192.168.0.3 via 192.168.0.4 dev wlp2s0 table 1000 src 192.168.0.210 uid 1000
cache
这将触发对 192.168.0.4 的 ARP 请求,并在约 3 秒后失败(此类尝试的标准 ARP 最大延迟),并显示EHOSTUNREACH
(没有到主机的路由):
curl: (7) Failed to connect to 192.168.0.3 port 80 after 3071 ms: Couldn't connect to server
请注意,任意 IP 地址甚至不必在 192.168.0.0/24 范围内。只要 ARP 请求最终失败,此方法同样有效:
ip route add default via 192.0.2.2 onlink dev wlp2s0 table 1000
注意:同样的技巧不适用于第 3 层接口(例如:tun 模式下的 WireGuard 或 OpenVPN),因为该接口上没有网关的概念。
更新
解决 OP 的问题
我之前没有解决 OP 遇到的问题:连接失败而不是成功,并且在其他地方添加默认路由使得尝试成功。
原因是现在只检查了一半的通信:从主机到服务器,可以成功发送数据包,而不是从服务器返回尚未考虑的主机的流量。
我们还发现(通过聊天)OP 正在使用这些rp_filter
设置:
$ sysctl net.ipv4.conf.all.rp_filter
net.ipv4.conf.all.rp_filter = 2
$ sysctl net.ipv4.conf.wlp2s0.rp_filter
net.ipv4.conf.wlp2s0.rp_filter = 2
将wlp2s0
接口设置为松散反向路径转发模式,定义在RFC 3704。
2.4. 松散的反向路径转发
松散反向路径转发(松散 RPF)是算法上的 如同严格 RPF,但不同之处在于它只检查存在路由(即使是默认路由(如适用) 不是路线指向的地方。
反向路径转发 (RPF) 正在检查反向路径:从服务器到主机的返回流量,与“正常”路径,即从主机到服务器的路由兼容。
因此,当存在默认路由(这是最常见的情况)时,松散 RPF 实际上总是会成功。如果没有默认路由或任何到目标的路由,它将失败。强制接口对于传出流量总是会成功,但这不会改变从服务器到主机接收的反向流量。此类入口流量的接收与出口流量被强制到接口的事实无关,无法解释这种情况。同样,ip route get
当任何地方都没有默认路由时,可以检查这一点。从 OP 的案例开始,没有添加规则:
# ip route get from 192.168.0.3 iif wlp2s0 to 192.168.0.210
RTNETLINK answers: Invalid cross-device link
Invalid cross-device link
是网络堆栈选择的错误,用于告知 RPF 失败:此路径无效,这意味着应该从服务器返回流量。在绑定到接口时选择隐式默认值不会影响入口流量,所以不会改变这个结果:超时(但不是由于 ARP,所以不限于 3 秒)。
同样的检查rp_filter=0
:
# sysctl -w net.ipv4.conf.all.rp_filter=0
net.ipv4.conf.all.rp_filter = 0
# sysctl -w net.ipv4.conf.all.rp_filter=0
net.ipv4.conf.all.rp_filter = 0
# ip route get from 192.168.0.3 iif wlp2s0 to 192.168.0.210
local 192.168.0.210 from 192.168.0.3 dev lo table local
cache <local> iif wlp2s0
由于不再进行检查,因此工作正常。
否则,恢复到rp_filter=2
,执行以下操作:
# ip route add 192.168.0.3/32 via 172.17.0.2
# ip route get from 192.168.0.3 iif wlp2s0 to 192.168.0.210
local 192.168.0.210 from 192.168.0.3 dev lo table local
cache <local> iif wlp2s0
也使其工作。
只需添加一个错误的192.168.0.3 的路由使其通过 Loose RPF 算法。当然,添加默认路由也会产生同样的效果:只要有 192.168.0.3 的路由,Loose RPF 就会通过。
因此,要么像上面那样完全禁用rp_filter
检查,要么无论 192.168.0.3 位于何处,都设置任何路由,包括使用其他接口的默认路由。