yuqi-zheng

Linux rp_filter:为什么你的交易丢包找不到原因


你装了第二块网卡做冗余行情源,配好路由,开始收数据。然后有些包就消失了。没有错误、没有日志、没有重传——它们就是没了。内核在应用程序看到之前就把它们丢掉了,而罪魁祸首就是 rp_filter,Linux 的反向路径过滤器。

这篇文章解释 rp_filter 做了什么、为什么默认配置对多网卡交易机不友好、以及如何正确配置。


rp_filter 是什么?

反向路径过滤是一种安全机制,根据路由表验证入向数据包。当一个包到达某个网卡接口时,内核问:如果我要给这个源地址回包,会走同一个接口吗?

如果答案是否,这个包就是可疑的——它可能是伪造的——内核会静默丢弃。没有 ICMP 错误、没有日志条目(除非你开了调试)、也不会通知应用程序。

检查发生在内核的 fib_validate_source() 函数中,对每个入向包在 IP 输入路径(ip_rcvip_rcv_finish)上调用,在任何传输层处理之前。

sysctl 接口

# 按接口
net.ipv4.conf.eth0.rp_filter
net.ipv4.conf.eth1.rp_filter

# 所有接口(作为默认值)
net.ipv4.conf.all.rp_filter

# 新接口的默认值
net.ipv4.conf.default.rp_filter

某个接口的生效值conf.all.rp_filterconf.<ifname>.rp_filter最大值。这是一个常见陷阱:设置 eth0.rp_filter = 0all.rp_filter = 1,eth0 仍然是严格模式。

# 检查生效值
sysctl net.ipv4.conf.all.rp_filter
sysctl net.ipv4.conf.eth0.rp_filter
# 生效值 = max(all, eth0)

三种模式

0 — 不做源地址验证

禁用。每个入向包都被接受,无论源地址和到达接口。内核不执行反向路径检查。

sysctl -w net.ipv4.conf.all.rp_filter=0
sysctl -w net.ipv4.conf.eth0.rp_filter=0

1 — 严格模式(RFC 3704)

内核用源地址作为目的地做一次完整的 FIB 查找。如果查到的出接口与入接口不匹配,包被丢弃。

包从 eth1 到达,源地址 10.0.1.100
内核查找去往 10.0.1.100 的路由 → 出接口是 eth0
eth0 ≠ eth1 → 丢弃

这是最严格的模式,也是许多现代发行版的默认值(Ubuntu 20.04+、RHEL 8+、CentOS 8+)。它会在任何回复路径与到达路径不一致的拓扑上丢包——而这正是多网卡交易机的常态。

2 — 松散模式(RFC 3704)

内核用源地址做 FIB 查找,但只检查路由是否存在——不检查走哪个接口。只有当源地址完全不可达时才丢包。

包从 eth1 到达,源地址 10.0.1.100
内核查找去往 10.0.1.100 的路由 → 路由存在(经 eth0)
路由存在 → 接受(接口不匹配没关系)

松散模式能防护明目张胆的伪造地址(不可路由的地址),同时允许非对称路由。


交易机为什么容易踩坑

多网卡双路行情

典型的低延迟交易机有两块或更多网卡,每块连接不同的交易所行情源或经纪商:

┌─────────────────────────────┐
│         交易主机            │
│                             │
│  eth0 (10.0.1.10) ── 行情 A│
│  eth1 (10.0.2.10) ── 行情 B│
│                             │
└─────────────────────────────┘

默认路由走 eth0:

default via 10.0.1.1 dev eth0
10.0.1.0/24 dev eth0
10.0.2.0/24 dev eth1

从 eth1 到达的 10.0.2.x 源地址的包没问题(回复路由也走 eth1)。但如果行情 B 使用的源地址来自不同子网,或者默认路由经 eth0 匹配了该源地址,严格模式就会丢包。

Kernel-bypass 网卡

使用 Solarflare/OpenOnload 或 EFVI 时,数据面完全绕过内核。但控制面——ARP、ICMP、路由管理——仍然走内核协议栈。如果内核的 rp_filter 丢弃了 ARP 应答或 ICMP 重定向,kernel-bypass 路径也无法建立连接。

这种情况尤其隐蔽,因为 kernel-bypass 应用永远看不到被丢弃的包;它只是收不到数据。通常的调试手段——tcpdump——也帮不上忙,因为 tcpdump 在 rp_filter 检查之前抓包。

托管机房的非对称路由

在交易所托管环境中,网络拓扑往往不在你控制之内。上游交换机可能通过不同路径路由回程流量,尤其在 ECMP 或冗余交换机场景下。从内核视角看,回复接口与到达接口不匹配,严格模式就会丢包。

Bonding / LAG 与 L3+ 哈希

当网卡用 LACP 做绑定且哈希策略为 layer3+4 时,每条流被固定到特定的从接口。如果交换机因哈希不匹配或故障切换把某个流的包发到了不同的从接口,严格模式可能丢弃它们。


静默丢包问题

rp_filter 丢包默认是不可见的。包在 fib_validate_source() 中被 kfree_skb() 丢弃,只递增 IPSTATS_MIB_INADDRERRORS 计数器——没有 ICMP、没有日志、没有 socket 错误。

检测丢包

检查接口统计:

cat /proc/net/snmp | grep InAddrErrors
# Ip: Forwarding DefaultTTL InReceives InHdrErrors InAddrErrors ...
# Ip: 1 64 12345678 0 42 ...

InAddrErrors 持续增长说明有 rp_filter 丢包。但这个计数器也会因其他原因递增(无效目的地址),所以不是确凿证据。

nstat 获取更详细的数据:

nstat -az | grep InAddrErrors

开启内核反向路径日志:

# 临时开启(开销大,仅调试用)
echo 1 > /proc/sys/net/ipv4/conf/eth1/log_martians
# 查看 dmesg
dmesg | grep "martian"
# 示例输出:
# IPv4: martian source 10.0.1.100 from 10.0.2.1, on dev eth1

log_martians sysctl 让内核记录每个未通过 rp_filter 检查的包。在高包速率下开销很大,应只在调试时使用。

用 eBPF 追踪:

# 追踪 fib_validate_source 中的丢包
bpftrace -e 'kprobe:fib_validate_source { @src = ntop(arg0); @if = arg1; }'

或者用 dropwatch

dropwatch -l kas
# 显示丢包发生的确切内核函数

tcpdump 陷阱: tcpdump 在原始套接字层抓包,在 rp_filter 检查之前。一个在 tcpdump 中可见但应用程序收不到的包,是 rp_filter(或 netfilter/iptables)丢包的强烈信号。


交易机的正确配置

安全默认值:松散模式

对于任何多网卡主机,全局设置松散模式:

# /etc/sysctl.d/99-trading.conf
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.rp_filter = 2

然后验证每个接口:

for iface in $(ls /proc/sys/net/ipv4/conf/ | grep -v all | grep -v default); do
    sysctl -w net.ipv4.conf.$iface.rp_filter=2
done

松散模式仍然能防护来自不可路由源的伪造包,但不会破坏非对称路由。

何时用严格模式

仅在单网卡主机上使用严格模式(1),且你控制整个网络路径并需要最大的伪造防护。交易机很少满足这个条件。

何时完全禁用 rp_filter

以下情况设为 0:

  • 你使用 kernel-bypass(EFVI、DPDK),内核的 FIB 查找对 bypass 接口返回了错误结果。
  • 你有复杂的策略路由,rp_filter 逻辑无法正确处理。
  • 你验证过松散模式仍然会丢弃合法包。

代价是失去内核的源地址验证。如果你同时运行 iptables/nftables 并使用 rpfilter 匹配,可以以更灵活的方式复刻过滤:

# nftables 等价写法,支持逐规则控制
nft add rule ip filter input fib saddr . iif type unreachable drop
# 对特定源地址放行
nft add rule ip filter input ip saddr 10.0.0.0/8 accept
nft add rule ip filter input fib saddr . iif type unreachable drop

Kernel-bypass 专用配置

对于 Solarflare EFVI 与 sfc 驱动:

# 内核驱动处理 ARP/路由(内核路径)
# EFVI 流量完全绕过内核
# 在 Solarflare 接口上设置松散模式,避免
# 丢弃来自交易所交换机的 ARP 应答
net.ipv4.conf.sfc_bp0.rp_filter = 2   # 或 0

LACP Bonding 配置

# Bond 接口从 conf.all 继承
# 在启动 bond 之前确保松散模式
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.bond0.rp_filter = 2

all 与按接口设置的陷阱

conf.all.rp_filterconf.<ifname>.rp_filter 之间的交互是最常见的错误配置。内核取两者的最大值

all.rp_filter = 1, eth0.rp_filter = 0  → 生效 = 1(严格!)
all.rp_filter = 0, eth0.rp_filter = 2  → 生效 = 2(松散)
all.rp_filter = 2, eth0.rp_filter = 0  → 生效 = 2(松散)
all.rp_filter = 2, eth0.rp_filter = 2  → 生效 = 2(松散)

这意味着设置 all.rp_filter = 1(许多发行版的默认值)会覆盖任何按接口的 0 设置。你必须把 all 设为与期望值相同或更低的值。

推荐做法:

# 把 all 设为最低需求值,然后按接口覆盖
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.rp_filter = 2
# 按接口:只在需要严格模式时才覆盖
#(交易机上很少需要)

检查生效值

#!/bin/bash
# 显示每个接口的 rp_filter 生效值
for iface in $(ls /proc/sys/net/ipv4/conf/ | grep -v all | grep -v default); do
    all=$(cat /proc/sys/net/ipv4/conf/all/rp_filter)
    per=$(cat /proc/sys/net/ipv4/conf/$iface/rp_filter)
    effective=$((all > per ? all : per))
    echo "$iface: all=$all per=$per effective=$effective"
done

与策略路由的交互

使用策略路由(ip rule / ip route get)时,rp_filter 检查默认使用主路由表,而不是策略选定的表。这意味着被策略规则正确路由的包,仍可能因为主表中源地址的路由指向不同接口而未通过 rp_filter 检查。

示例

# 主表
default via 10.0.1.1 dev eth0
10.0.1.0/24 dev eth0

# 策略路由表 100
default via 10.0.2.1 dev eth1
10.0.2.0/24 dev eth1

# 规则:来自 10.0.2.10 的包使用表 100
ip rule add from 10.0.2.10 table 100

一个从 eth1 到达、源地址 10.0.2.100 的包:rp_filter 检查在主表中查找 10.0.2.100,找到经 eth0 的路由(不是 eth1),然后丢弃——尽管策略路由会正确地通过 eth1 路由回复。

变通方法

在受影响的接口上设置 rp_filter = 02,或确保主表中有与策略路由一致的路由。


排错清单

  1. 包在 tcpdump 中可见但应用收不到? → 检查 rp_filter。
  2. InAddrErrors 计数器在增长? → 检查 rp_filter + 开启 log_martians。
  3. 只有部分源 IP 受影响? → 可能是严格模式丢弃了来自路由走不同接口的子网的包。
  4. 添加网卡后问题出现? → 新接口可能改变了已有源地址的 FIB 查找结果。
  5. Kernel-bypass 应用收不到数据但内核协议栈正常? → 检查内核侧控制面(ARP/路由)是否被 rp_filter 阻断。

快速诊断

# 1. 检查 rp_filter 生效值
for i in /proc/sys/net/ipv4/conf/*/rp_filter; do echo "$i: $(cat $i)"; done

# 2. 检查地址错误计数
nstat -az | grep InAddrErrors

# 3. 临时开启日志
sysctl -w net.ipv4.conf.all.log_martians=1
dmesg -w | grep martian

# 4. 测试特定源地址的路由查找
ip route get 10.0.2.100 from 10.0.2.10 iif eth1

总结

模式检查内容丢非对称包?适用场景
禁用0Kernel-bypass、复杂策略路由
松散2源地址路由是否存在?多网卡交易机(推荐)
严格1回复是否走同一接口?仅单网卡主机

交易机配置建议: 设置 net.ipv4.conf.all.rp_filter = 2net.ipv4.conf.default.rp_filter = 2。用上面的脚本验证。永远不要在多网卡主机上保留 all.rp_filter = 1


参考