net.ipv4.conf.all.rp_filter 这个参数,直接决定了 Linux 内核是否丢弃那些“来源地址看起来是伪造的”数据包。它不跟你绕弯子,核心逻辑就是反向路径过滤。当网卡收到一个包,内核会去查路由表,看发回这个源地址的路径是不是应该从收到包的这张网卡出去。如果不是,内核就认定这个包的源地址是伪造的,直接丢掉。这个机制设计出来就是为了防 DDoS 攻击,尤其是基于 IP 欺骗的 SYN Flood 之类。但如果你不理解它的三个值代表什么,在多网卡、多线路、负载均衡或者 Docker/K8s 环境里,它就会变成“网络不通”的罪魁祸首。

0、1、2 三个值的真实行为

很多人只看文档,以为 0 是关闭,1 是严格,2 是松散。但实际行为比这几个字复杂得多。先说 0,它确实不检查源地址合法性,包进来就收。但别以为设成 0 就万事大吉,在某些内核版本里,0 只是不做反向路径检查,如果其他 netfilter 规则或者路由策略有依赖,行为依然不可控。再说 1,这是 RFC 3704 定义的严格模式。内核不仅要求回程路由的出接口就是收包接口,还要求源地址在 FIB 里是最佳路由。如果有多条等价路由,严格模式下可能直接丢包,因为它无法确定哪条是“最佳”。最后是 2,松散模式只要求源地址在路由表里有任意一条路由可达就行,不强制出接口一致。这在非对称路由场景下是必需的,比如你的流量从 ISP-A 进来,但回程走了 ISP-B,严格模式就会把合法流量全丢掉。

all 和 default 以及具体接口的生效优先级

这是最容易踩坑的地方。net.ipv4.conf 下面有 all、default 和具体接口比如 eth0 三个层级。内核在判断是否做 rp_filter 时,用的是 all 和具体接口值之间的逻辑或,也就是取最大值。你把 eth0 的 rp_filter 设成 0,但 all 设成 1,实际生效的是 1。这个设计非常反直觉,很多人在接口上改了参数发现不生效,就是因为 all 的值还卡着。正确的做法是,如果你要全局关闭,必须同时把 all 和 default 都设成 0,然后每个接口也要设成 0。或者你明确知道自己的网络拓扑,就在 all 上设成 2,接口上根据角色微调。但要注意,default 只对之后新出现的接口生效,已经存在的接口不受影响。

多网卡多线路场景下的非对称路由问题

典型场景是服务器有两张网卡,分别接两个运营商线路,或者一张内网一张外网。流量从外网卡进来,但回程路由表指向了内网卡,严格模式 rp_filter=1 直接丢包。这时候你设成 2 就能解决。但设成 2 也有风险,它降低了防欺骗的能力。攻击者只要知道你的内网网段,构造一个源地址是内网 IP 的包从外网卡打进来,松散模式下就会放行。所以设成 2 的同时,你必须在 iptables 或 nftables 里做更精细的源地址校验,不能单靠内核参数。另外,如果你启用了 bonding 或者 teaming,rp_filter 的行为会更复杂,因为 bonding 的 slave 接口也有自己的 rp_filter 参数,而且 bond 接口本身也有。内核在处理 bond 成员口收到的包时,会先按 slave 接口的 rp_filter 判断,再按 bond 主接口的判断,中间任何一步丢包都会导致不通。

容器和 Kubernetes 环境下的特殊表现

在 Docker 和 K8s 节点上,rp_filter 参数是常见的网络故障根源。Calico、Flannel 这些 CNI 插件会创建大量 veth pair 和路由规则。Pod 发出的流量经过 veth 到达宿主机,宿主机再做路由转发。如果宿主机开启了 rp_filter=1,从 veth 口收到的 Pod 源地址,内核去查回程路由,发现回程路由的出接口可能不是这个 veth,而是另一个网桥或者物理网卡,直接丢包。这就是为什么很多 K8s 部署文档会要求你把 rp_filter 设成 0 或 2。但全设成 0 又会让节点暴露在 IP 欺骗攻击下。更合理的做法是,在宿主机物理网卡上保持严格模式,在 veth 和网桥上设成松散模式或关闭。不过由于前面说的 all 和接口值取最大值的逻辑,你必须在 all 上设成 2,然后在物理接口上通过 sysctl 单独设成 1,但这样 all 和物理接口取最大值还是 2,物理接口上的 1 根本不生效。这个死结导致很多运维只能全局设成 2 或 0,然后靠网络策略来补安全缺口。

内核版本差异带来的行为变化

不要以为 rp_filter 的行为在所有内核版本上一致;

2.6 内核时代,松散模式只是检查源地址是否可达,不检查出接口。到了 3.x 内核,松散模式增加了对源地址在 local 表的检查,如果源地址是本机 IP,但收包接口不是 loopback,也会丢;

4.x 以后,内核引入了 per-interface 的 accept_local 参数,配合 rp_filter 使用;

5.x 内核在松散模式下对多路径路由的处理也有调整,以前可能因为 ECMP 导致丢包,现在部分情况会放行。所以你在不同发行版上看到同样的 sysctl 设置,实际效果可能不同。Debian 10 用的 4.19 内核和 Debian 12 用的 6.1 内核,rp_filter 的边界行为就有差异。验证的时候不能光看文档,要用 tcpdump 抓包配合 iptables TRACE 或者 dropwatch 工具看内核到底在哪里丢了包。

如何诊断 rp_filter 导致的丢包

最直接的方法是看内核的统计信息。用 netstat -s 或者 cat /proc/net/netstat,找 IpReasmFails、TcpExtListenDrops 这些计数器,如果数值异常增长,可能就是 rp_filter 在干活。更精确的是用 nstat -az 看 TcpExtIPReversePathFilter 这个计数器,它就是专门统计因为反向路径过滤被丢弃的包数。如果你看到这个值在涨,就说明有流量被 rp_filter 干掉了。还可以用 perf 工具动态追踪内核函数,比如 fib_validate_source 的返回值,但这需要一定的内核调试经验。实际排障时,先看接口计数,再用 tcpdump 在多个点抓包对比,比如在物理网卡抓到了,但在应用层没收到,中间又没有 iptables 规则,那大概率就是 rp_filter 丢的。

生产环境的最佳实践

单网卡简单服务器,直接保持默认的 rp_filter=1,这是最安全的选择。多网卡但路由对称的场景,也可以保持 1。一旦涉及非对称路由、多线路、策略路由、VRRP 或者 Keepalived 这种 HA 方案,就必须评估是否要改成 2。改成 2 之后,一定要在防火墙层面补上源地址校验规则。比如在 iptables 的 raw 表里,对每个接口做 uRPF 检查,或者用 nftables 的 fib 表达式做更灵活的判断。对于容器环境,建议在宿主机层面用 NetworkPolicy 或者 Cilium 这种 eBPF 方案来做细粒度的源地址验证,而不是单纯依赖 rp_filter。另外,所有修改都要通过配置管理工具下发,不要手动 echo 到 /proc,因为重启网络服务或者重启机器后,/etc/sysctl.conf 或者 /etc/sysctl.d/ 下的文件才会被加载。Debian 系统尤其要注意,有些云镜像会在 /etc/sysctl.d/ 下放自己的配置文件,可能覆盖你的设置。

和其他内核参数的联动关系

rp_filter 不是孤立的。net.ipv4.conf.all.accept_source_route 如果开启,会允许源路由选项,这可能绕过部分 rp_filter 检查。net.ipv4.conf.all.forwarding 开启时,主机的路由行为会变化,rp_filter 的判断依据也会跟着变。还有 arp_filter、arp_ignore、arp_announce 这些 ARP 相关参数,在多网卡同网段场景下会影响邻居发现,间接影响回程路由的确定,从而影响 rp_filter 的判断结果。如果你用了 VRF,每个 VRF 实例有自己的路由表,rp_filter 检查时会查对应 VRF 的路由表,而不是主表。这些联动关系在复杂网络架构里必须通盘考虑,单独调一个 rp_filter 往往解决不了问题,还会引入新坑。

常见误区澄清

第一个误区是以为 rp_filter 只影响入站流量。实际上,对于本机发出的包,如果开启了本地生成的包的源地址校验,rp_filter 也会影响。不过这个行为由 net.ipv4.conf.all.rp_filter 和 net.ipv4.conf.all.log_martians 配合控制,默认不会丢本机发出的包。第二个误区是以为设成 2 就完全不做检查。松散模式只是不检查出接口,源地址完全不可达的包依然会被丢。第三个误区是以为这个参数只在 IPv4 下有效。IPv6 有对应的 net.ipv6.conf.all.rp_filter,但行为不完全一致,IPv6 的松散模式在 Linux 上的实现更宽松一些。第四个误区是以为改了 sysctl 立即对所有已建立的连接生效。rp_filter 只针对新收到的包做判断,已建立的连接不受影响,所以改参数不会让现有 TCP 连接断开,但新的握手包可能会被处理。

深入理解 FIB 校验机制

内核在收到包时,调用 fib_validate_source 函数,这个函数会查 FIB 表。严格模式下,它要求 fib_lookup 返回的出接口等于 skb 进来的接口,而且 fib 结果类型必须是 RTN_UNICAST 或 RTN_LOCAL。如果路由返回的是 RTN_MULTICAST 或者 RTN_BROADCAST,严格模式也会丢。松散模式下,只要 fib_lookup 能找到一条路由,不管类型和出接口,就放行。但这里有个细节,如果源地址是组播地址或者广播地址,不管什么模式都会丢,因为这类地址不能作为源地址出现在公网或内网传输中。另外,如果启用了 martian 日志,被 rp_filter 丢弃的包会记录到内核日志,你可以通过 dmesg 看到类似 "IPv4: martian source" 的信息,这能帮你快速定位是哪张卡、哪个源地址被丢了。

在 Debian 上的持久化配置方法

Debian 系统推荐在 /etc/sysctl.d/ 下创建自定义配置文件,比如 99-network-tuning.conf,内容写 net.ipv4.conf.all.rp_filter=2 这样的格式。然后用 sysctl -p /etc/sysctl.d/99-network-tuning.conf 加载。注意不要直接编辑 /etc/sysctl.conf 然后依赖 sysctl -p 不带参数,因为不带参数默认只读 /etc/sysctl.conf,不会读 /etc/sysctl.d/ 下的文件。Debian 的 procps 包在系统启动时会遍历 /etc/sysctl.d/ 下的文件,所以放那里是最可靠的。如果你用了 NetworkManager 或者 systemd-networkd,它们也有自己的 sysctl 配置机制,可能会覆盖你的设置,需要检查对应的 .link 或 .network 文件里的 [Link] 和 [Network] 段。对于云环境,cloud-init 的网络配置也可能在初始化时修改这些参数,务必在 user-data 或者云平台的自定义脚本里做最终设置。

理解 net.ipv4.conf.all.rp_filter 不能停留在背参数值的层面。你得知道你的流量路径长什么样,哪些接口在收发数据,回程路由怎么走,有没有策略路由或多路径。然后根据实际路径决定用 0、1 还是 2,并且要验证 all 和具体接口的最终生效值。最后用防火墙规则弥补关闭或放松 rp_filter 带来的安全缺口。这个参数调对了,网络静悄悄;调错了,排查起来就是一场噩梦。