它做什么curl --interface tun0
?根据 man:Perform an operation using a specified interface. You can enter interface name, IP address or host name.
它似乎使用 SO_BINDTODEVICE 来实现这一点。这是我感到困惑的地方:根据我的理解,当程序想要将数据包发送到特定目的地时,由路由表决定使用哪个接口作为源。此选项似乎以某种方式覆盖了它。那么如果路由表中没有路由会使用该接口并到达该目的地,会发生什么?
案例一(由sing-box设置的隧道):
ip a:
tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 9000 qdisc fq state UNKNOWN group default qlen 500
link/none
inet 172.18.0.1/30 brd 172.18.0.3 scope global tun0
valid_lft forever preferred_lft forever
inet6 fe80::717d:757a:23db:789b/64 scope link stable-privacy
valid_lft forever preferred_lft forever
ip route:
172.18.0.0/30 dev tun0 proto kernel scope link src 172.18.0.1
curl example.com --interface tun0
按照预期进行操作(但是怎么做到的?)并通过 tun0 请求 example.com。
情况二(wireguard接口):
ip a:
wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
link/none
inet 10.230.116.1/24 scope global wg0
valid_lft forever preferred_lft forever
ip route:
10.230.116.0/24 dev wg0 proto kernel scope link src 10.230.116.1
结果是:connect to 142.44.215.161 port 80 failed: No route to host
它怎么知道那里实际上没有路线?
我理解这些隧道是由用户空间(或内核空间)程序操作的。这是否像 FuseFS 一样,当这些程序不喜欢目标 IP 时,它们可以简单地返回“不,不允许”(或“是的,无论如何我都可以这样做”,即使根据路由表,它们不应该发送到那里)?这是否意味着在使用 SO_BINDTODEVICE 时完全忽略路由表?如果我使用 eth0 而不是 tun0 会怎样?它是一个物理接口,它会使用路由表来确定是否可以路由此数据包吗?最后,可以这样做curl example.com --interface 172.18.0.1
。令人惊讶的是,这并不等同于--interface tun0
,并且不会成功。这是为什么?
该选项
--interface <name>
调用setsockopt(SO_BINDTODEVICE)
似乎在 Linux 上有这样的效果:仍会执行路由查找 - 仅考虑通过该接口的路由 - 如果有匹配项,则照常使用该路由。例如,如果您有两个默认路由(通过 eth0 的路由具有最低度量,并且通常是首选路由),则将
--interface wlan0
改为使用通过 wlan0 的默认路由。但是,如果没有匹配的路由,则假定为链路
0.0.0.0/0 dev wlan0
路由(例如,没有网关)。对于使用链路层(L2、MAC 层)寻址的接口和不使用链路层寻址的接口,结果会有所不同:以太网和 Wi-Fi(以及软件
tap
接口)是多路访问接口,需要 MAC 地址才能将数据包传送到某个地方(它不会自己“直接进入网关”),因此路由指定一个“网关”来将数据包导向该网关的 MAC 地址。在这种情况下,当假设在线路由时,主机将尝试发出 ARP 请求以将目标 IP 地址解析为 MAC 地址,并且大多数情况下它永远不会收到答复。
(除非它是一个非常老式的网络,其中网关代表所有内容执行代理 ARP - 思科网关过去长期默认启用代理 ARP - 在这种情况下,网关将代表 WAN IP 地址做出响应并且连接就会发生。但这种情况在当今极为罕见。)
相比之下,
tun
接口(以及wg
和ppp
接口)是点对点接口,只有一个可能的目的地,因此不使用 ARP。在这种情况下,链路上路由和网关路由之间没有区别(因为没有 MAC 寻址)——IP 数据包只是通过接口推送并“到达另一端”——并且通信将始终有效。
(但是,如果没有到对等点的“AllowedIPs”映射,或者该对等点的外部地址未知,WireGuard 内部逻辑仍可能拒绝带有错误代码的数据包。)
抱歉,这次我没有深入研究内核源代码,也没有验证我在步骤#1的猜测是否正确。
另一方面,如果您使用
--interface <ip>
,则会发生完全不同的事情:套接字不绑定到任何接口,而是使用调用绑定到该本地 IP 地址bind()
。这种影响在不同的操作系统之间差异很大,但在 Linux 上,它几乎根本不影响路由 - 即使数据包是从接口 A 的地址发送的,它仍可能通过接口 B 或 C 进行路由。
(它是“弱主机模型”,在某些情况下有其优势,但不是现在最常见的两个接口连接到完全不同的网络的模型。想象一下古老的 Unix 主机,其不同的接口仅仅是通过同一个 ARPANET 网络的不同路径。)
因此,为了使其
--interface <ip>
按预期工作,Linux 系统实际上需要策略路由规则,根据源地址选择完全不同的路由表。例如,某些 VPN 软件会添加一个(我相信 WireGuard 的 wg-quick 通常会添加),以便通过 wg0 IP 地址绑定的套接字将使用仅具有通过 wg0 的路由的不同路由表,依此类推。这些是通过 定义的ip rule
。用户空间隧道只能丢弃数据包,或者它们可以代表(虚拟的)网关发出(假的)ICMP 错误数据包,但不能追溯拒绝已经通过其接口路由和“发送”的数据包(从用户空间文件描述符读取构成接受)。
wg0
由于由内核逻辑处理,限制较少,并且可以用自己的错误代码进行响应。对于高级套接字 (TCP),并非所有这些都按原样传递;只有ping
使用“原始 IP”级 sendmsg() 之类的东西似乎能够看到它,而 TCP connect() 会看到不同的错误代码——当 WireGuard 核心返回 EDESTADDRREQ“需要目标地址”或类似内容时,确实会导致“没有到主机的路由”。