How Linux Routes Network Packets
既然你已对 Linux 网络路由有了基本了解,现在是时候深入探讨一些利用这些知识可以实现的有趣功能了。
丢弃数据包
以下是一些丢弃数据包的示例及其效果。
静默丢弃数据包。
ip route add blackhole [destination addr/mask]
以 ICMP “主机不可达” 响应拒绝数据包。
ip route add unreachable [destination addr/mask]
以 ICMP “管理性禁止” 响应拒绝数据包。
ip route add prohibit [destination addr/mask]
以 ICMP “网络不可达” 响应拒绝数据包。
ip route add throw [destination addr/mask]
操纵源地址
src 参数会强制沿特定路由发出的出站流量,其源 IP 地址显示为指定的地址。一个典型应用场景是:将出站流量按需引导至共享的网络接口。
其格式为:
~# ip route add n.n.n.n/n.n.n.n dev[device] src n.n.n.n
例如:
ip route add 10.10.14.107 dev tun0 table vpn src 192.168.1.100
大致可理解为: “向名为 vpn 的路由表中添加一条新路由:将发往目标地址 10.10.14.107 的流量,其源地址设为 192.168.1.100,并通过 tun0 接口发送出去。”
按时间顺序拆解,也可以这样理解该命令:
| |
|---|
| |
| |
| |
| 将经过此路由的数据包的源地址修改为 192.168.1.100 |
注意:src 的使用仅对源自本服务器的数据包有效,通常不建议使用。
源地址 NAT(或非 NAT)
尽管 iproute2(即 ip route 命令)有时能模拟类似 NAT 的功能,但它并不具备真正的 NAT 能力。RPDB 仅能在数据包为新建且源自本机(localhost)时修改其源 IP 地址。根据定义,iproute 是一个无状态(stateless)的过程。它只处理原始数据包,其作用仅仅是“路由”它们(而 iptables 和 ip rule 则能理解网络连接状态)。因此,从技术上讲,iproute 的 NAT 是无状态的。虽然它看起来像是一种 NAT,但实际上并非如此。
前文提到的 src 参数可以修改数据包的源地址。但它仅对源自当前主机的数据包有效[1]。这使其区别于由 iptables 处理的源 NAT(SNAT)。
iptables 中的源 NAT(包括非常相似的 MASQUERADE 功能)仅作用于连接的第一个数据包(尽管 SNAT 更加健壮一些)。这种策略有助于确保属于已有连接的数据包不会丢失——在 iproute 能任意修改源 NAT 的时代,这种情况是可能发生的。
iproute2 处理流程受到内核限制:除非数据包是新建且源自 localhost,否则无法对其执行任何 NAT 操作。在数据包离开 localhost 后、但在进入 OUTPUT iptables 链之前,RPDB 有能力修改出站数据包的源地址。但这在技术上并非 NAT,而是一种无状态的数据包修改(mangling)。和 NAT 一样,它也仅适用于新建连接。无论如何,通常不建议这样做。只有极少数场景下,使用 RPDB 修改新建数据包的源 IP 才有意义。几乎总是更推荐在 iptables 的 POSTROUTING 链中执行源 NAT(SNAT)操作。你可以参考 iptables 数据包路由流程图,或查看此版本,其中展示了 iproute 在何时能影响数据包流。
图 6(右侧)展示了 iptables 出站数据包流程,并高亮了 iproute2 源 NAT 的“后门”注入点。
图6(右侧)展示了iptables出站数据包处理流程,其中已高亮标注了iproute2源网络地址转换后门何时使用 src NAT 是合理的
请记住,iproute 对连接状态一无所知。只有在以下两种情况下,我才建议你使用 ip route 来修改数据包的源地址。
第一种情况是:单个网络设备连接了多个 LAN。在此场景下,使用 ip route 修改源地址比使用 iptables 更合理,因为 ip route 知道数据包将走哪条出站路由,且数据包确实源自本服务器。这能解决一个潜在问题:下一跳设备尝试将回复发回服务器时,若数据包的源地址不在正确范围内,该数据包就会被丢弃,导致源服务器收不到回复。
第二种合理场景是:服务器拥有多个网络设备。当指定了某个特定网络设备来发送出站数据包时,同时设置该数据包的源 IP 地址是顺理成章的。
请注意,ip route无法用于修改数据包的目标地址(即目标 NAT 或 DNAT)。
iproute NAT 的简史
在 2004 年底 Linux 2.6.9 内核更新之前,iproute 可以对任意出站数据包执行 NAT。出于多种原因,此功能被移除,主要是为了整合现有的网络工具,防止它们相互冲突。对于 iproute NAT 而言,这是一个颇具争议的决定,因为它曾是一项强大的能力(尽管其在现实世界中的实际用途值得怀疑)。
当时这一路由变更并未引起太多关注,我怀疑绝大多数人甚至都未曾察觉。事实上,如果你查阅 2.6.9 的发布说明,几乎找不到关于 iproute2 变更的提及。该更新被描述为一个“通用网络统计”的补丁。诚然,完整的注释确实提到了连接跟踪。
对 iproute2 的这项变更,在某种程度上是连接跟踪变更的间接结果,但也是有意为之的影响。即便是完整的发布说明,也未能充分体现此次变更的重要性:“一个新的统一统计工具正在开发中,用于路由缓存、连接跟踪和邻居缓存,并将包含在 iproute2 中。”[2]
从功能上讲,这次变更使 iproute 的 NAT 能力与 iptables 保持一致,并旨在鼓励用户使用 iptables 来进行数据包 NAT 变换(使用有状态的 SNAT 和 DNAT;本文档的 iptables 章节对此有更详细的讨论)。
不出所料,iproute2 大部分 NAT 功能的丧失,直接将 iptables 推到了 NAT 的聚光灯下,因为这意味着再使用 ip route 来执行 NAT 任务已无太大意义。
有状态 vs. 无状态:它们意味着什么?
“无状态”是什么意思?你在网络术语中可能会经常听到这个词。它的含义很简单:数据包不会被跟踪,也不会与特定的连接及其状态相关联。一个使用“无状态”数据包检查的工具,不关心数据包所属的连接,它只根据数据包自身的属性进行评估。反之亦然。
例如,一个“有状态”的工具,能够感知当前数据包是否属于一个已建立的连接,并了解该数据包及其对应连接的一些信息。简而言之,它知晓连接和数据包的“状态”,无论它们具体是什么。
当你必须使用无状态 NAT 时
Linux 中许多被弃用的功能从未真正消亡。iproute2 的无状态数据包修改能力正是如此。虽然通过 ip route 命令的钩子已被移除,但其执行无状态 NAT 的能力依然存在。你只需要知道在哪里找到它以及如何使用它。tc 是流量控制器(Traffic Controller)的命令行调用工具,它是 iproute2 的一部分。对 tc 的深入解释超出了本文档的范围,但我将简要描述一下,以便感兴趣的读者能进一步了解。
除非你绝对确定自己在做什么,否则强烈不建议使用无状态 NAT。
tc
tc 在 Linux 2.2 内核中引入,并在 2.6.9 内核移除大部分无状态 NAT 功能后不久,于 2.6.24 内核中获得了额外的能力。
tc 工具常用于 QoS(服务质量),其基本功能是管理网络流量,以减少丢包和/或延迟。它的文档非常稀少。如果你想用它来实现无状态 NAT,就必须自行深入研究。
关于流量控制基本原理的总体指南,可参考 Linux 文档项目发布的《流量控制 HowTo》。你也可以浏览《Linux 高级路由与流量控制 HOWTO》文档。最后,如果你想要一个针对 Debian 系 Linux 发行版的 tc 快速入门指南,我推荐你查阅 ArchLinux Wiki 上的《高级流量控制》主题,其中提供了很好的概述。
单网络连接,多 LAN
举个例子。假设你的服务器通过一个网络设备可以访问两个 LAN。
- LAN1 的地址范围是 192.168.10.0–192.168.10.255,网关为 192.168.10.10
- LAN2 的地址范围是 10.10.10.0–10.10.10.255,没有网关
你的服务器想向地址为 10.10.10.14 的另一台设备发送数据。让我们运行 route 命令,看看路由表是什么样子:
从 route 命令的输出中,我们看到有一个网关 192.168.10.10 和两条本地路由(192.168.10.0/24 和 10.10.10.0/24)。那么,问题出在哪里?
问题在于:如何保证从本服务器发往每个 LAN 的出站数据包,其响应能成功返回?我们知道,发往 10.10.10.14 的数据包会选择最长匹配的路由前缀。但我们不知道其源地址会表明它来自 192.168.10.0/24 网络还是 10.10.10.0/24 网络。我们显然希望是后者,因为我们知道 10.10.10.0/24 网络上没有网关,这意味着任何超出该网络范围的地址都会失败,因为该网络上的其他服务器不会响应这个数据包。
问题在于,从这个显示中,我们无法判断这种保证是否存在。设置此路由表的人是否包含了 src 参数?我们无从得知。这正是 route 命令的不足之处。我们需要更多细节。不妨试试运行 ip route list,你会得到如下输出:
default via 192.168.10.10 dev eth0192.168.10.0/24 dev eth0 proto kernel scope link src 192.168.10.1110.10.10.0/24 dev eth0 proto kernel scope link src 10.10.10.11
哇!现在我们可以清楚地看到,两个 LAN 都有 src 参数,这非常好。这意味着,无论数据包发往哪个 LAN(目标地址),它都能返回到本机,因为它离开服务器时使用的地址与它所去的 LAN 属于同一网段。
多网络连接,多 LAN
更常见的场景是:服务器上有多个网络设备,每个都连接到一个独立的网络。在这种情况下,src 参数的作用类似:保证从服务器发起的新连接,其出站数据包使用指定的 IP 地址作为源地址。通过使用 src 为特定出站路由强制指定一个源 IP 地址,你可以确保返回的数据包能找到回本服务器的路径。
让我们简要看看,如果你要设置一个名为 test 的表来实现此功能,ip route 命令可能是什么样子。假设你已经创建了 test 表并可用于测试。
ip route add default via 192.168.10.10 dev eth0 table test src 192.168.10.11ip route add 192.168.10.0/24 dev eth0 table test src 192.168.10.11ip route add 10.10.10.0/24 dev eth0 table test src 10.10.10.11
你可以看到,每条路由在数据包经过时都指定了源 IP 地址。这确保了无论数据包的目的地是哪里,它都能返回到本主机。
注释
[1] Andreasson, Oskar. Iptables Tutorial 1.2.1. 第 6 章:表与链的遍历。https://www.frozentux.net/iptables-tutorial/chunkyhtml/x1237.html。[2] v2.6.8 到 v2.6.9 的变更摘要。2004 年 10 月 19 日。变更日志。https://mirrors.edge.kernel.org/pub/linux/kernel/v2.6/ChangeLog-2.6.9。
Src
https://datahacker.blog/industry/technology-menu/networking/routes-and-rules/how-linux-routes-network-packets