一台机器突然从局域网连不上了。本机 ping 得通、出站流量正常、
sshd在监听、端口没被防火墙拦,但从别的机器 ping 过来、SSH 过来一律超时。
本地看ip addr一切正常,网卡有 IP,路由表没问题。
这种”出站正常、入站全死”的组合,排查方向通常会滑向防火墙、SSH 配置、服务状态——但真正的原因往往是一个 sysctl。
一、症状画像
先把典型特征列出来,方便对号入座:
| 观察到的现象 | 说明 |
|---|---|
| 本机 ping 自己的 IP 通 | 走 loopback 或本地路由,不经过反向路径校验 |
| 本机 ping 外网 / 主动出站正常 | 出站不受 rp_filter 限制 |
| 从 LAN 另一台机器 ping 本机 超时 | 入站包被内核在 IP 层丢弃,应用层根本没看到 |
| 换一条网络路径(走另一张网卡)就正常 | 源地址在”入站网卡”上可达时才通 |
| 重启 / 重新加载 sysctl 之后症状出现或消失 | 典型的 sysctl 时序问题 |
| 服务、端口、路由、防火墙全查了没问题 | 问题不在应用层 |
最后一条是关键:没有任何一个应用层组件能解释它。ss -lntp 显示端口在听,防火墙规则放行,ip route get 算出来的下一跳也没问题。包确实进来了,只是被内核在更早的一层丢掉了。
本文要讲的机制叫 net.ipv4.conf.*.rp_filter(反向路径过滤,RFC 3704)。
二、rp_filter 的三个取值
| 值 | 模式 | 行为 |
|---|---|---|
0 |
关闭 | 不做任何反向路径校验 |
1 |
strict(严格) | 源地址必须只能从入站网卡到达,否则丢弃 |
2 |
loose(宽松) | 源地址必须能从某张网卡到达,否则丢弃 |
Debian 为了不打断非对称路由的机器,默认给所有网卡设了 2:
# /usr/lib/sysctl.d/50-default.conf
net.ipv4.conf.default.rp_filter = 2
net.ipv4.conf.*.rp_filter = 2
2 在大多数场景下是无害的。但桥接(bridge)场景下它会变成杀手:桥接的物理网卡上没有自己的 IP,所有地址都挂在网桥接口上。当一个包从物理网卡进来、源地址属于网桥那一侧时:
- strict(1):源地址不可能”从这张物理网卡到达” → 丢
- loose(2):源地址要从这张物理网卡可达才算数 → 同样丢
所以桥接场景必须显式关掉。注意这个坑和 net.bridge.bridge-nf-call-iptables 是叠加的:桥接流量一旦被 bridge-nf 转进 IP 层,物理网卡就成了”入站网卡”,rp_filter 在它上面一查就是不合格。
三、为什么 all 和 default 不够
最直觉的写法是这两行:
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.default.rp_filter = 0
直觉的理由是:”all 管所有网卡,default 管默认值,都设 0 就全关了。”
这个直觉是错的,而且错在两个不同的地方。
3.1 all 是下限,不是上限
内核文档 ip-sysctl.txt 的原文:
conf/all/rp_filter - INTEGER
Enable rp_filter on all interfaces. This is the maximum value
from all the per-interface values. It is used as the max value
of conf/{all,interface}/rp_filter. So if (conf/interface/rp_filter
is 0), the kernel will use (conf/all/rp_filter) value.
conf/interface/rp_filter - INTEGER
The max value from conf/{all,interface}/rp_filter is used when
doing source validation on the {interface}.
翻译成公式:
某张网卡实际生效的 rp_filter = max(conf/all/rp_filter, conf/<该网卡>/rp_filter)
all 是下限,不是覆盖。某张网卡自己写着 2,那它实际生效的就是 max(0, 2) = 2。
all = 0 对它毫无作用。
3.2 default 只是”网卡出生那一刻”的初值
conf/default 是一个模板,语义是”以后新建的网卡,初始值从这里复制一份”。
它对已经存在的网卡没有任何作用。
更麻烦的是:在 Debian 上,连”新建的网卡”这一层也保不住。
四、第二层覆盖:udev 会为每张新网卡重跑 systemd-sysctl
sysctl.d(5) 手册里有一段容易被忽略的说明:
The settings configured with sysctl.d files will be applied early on boot. The network interface-specific options will also be applied individually for each network interface as it shows up in the system. (More specifically,
net.ipv4.conf.*,net.ipv6.conf.*,net.ipv4.neigh.*andnet.ipv6.neigh.*.)
对应的实现是一条 udev 规则:
# /usr/lib/udev/rules.d/99-systemd.rules
ACTION=="add", SUBSYSTEM=="net", KERNEL!="lo", \
RUN+="/usr/lib/systemd/systemd-sysctl --prefix=/net/ipv4/conf/$name \
--prefix=/net/ipv4/neigh/$name --prefix=/net/ipv6/conf/$name \
--prefix=/net/ipv6/neigh/$name"
翻译一下:每出现一张新网卡,udev 就重跑一次 systemd-sysctl,作用域限定到这张新网卡。
而重跑时,它会重新展开 50-default.conf 里那条 glob:
net.ipv4.conf.*.rp_filter = 2
于是时间线变成:
网卡创建
→ 从 conf/default 复制初值(假设 default=0,这一步是 0)
→ udev 触发 systemd-sysctl 重跑,作用域限这张网卡
→ 50-default.conf 的 glob 重新展开,命中这张网卡 → 覆盖成 2
结果:default = 0 在 Debian 上对新建网卡最终无效。
这也解释了一个容易误判的现象:机器刚重启时手工验证 default 确实是 0,新建的网卡”看起来应该继承 0″,但过一会儿网卡上的值还是 2。
五、sysctl.d 的优先级:按文件名,不按目录
这是最容易踩错的一条规则。sysctl.d(5) 原文:
All configuration files are sorted by their filename in lexicographic order, regardless of which of the directories they reside in. If multiple files specify the same option, the entry in the file with the lexicographically latest name will take precedence.
Files in
/etc/override files with the same name in/run/,/usr/local/lib/, and/usr/lib/.
拆开说:
5.1 排序只看 basename,目录不参与
/etc/sysctl.d/、/run/sysctl.d/、/usr/local/lib/sysctl.d/、/usr/lib/sysctl.d/ 里的文件混在一起按文件名排。
验证方法(不需要 root):
systemd-analyze cat-config sysctl.d | grep '^# /'
输出会按真实生效顺序列出每个文件。注意 /etc 里的文件会夹在 /usr/lib 的文件中间出现——因为决定顺序的是文件名,不是目录。
5.2 目录优先级只在”同名”时才起作用
/etc/sysctl.d/50-default.conf(同名文件)会整体替换 /usr/lib/sysctl.d/50-default.conf,不是合并。
如果确实不想让某个 vendor 文件生效,man 页给的标准做法是:
ln -s /dev/null /etc/sysctl.d/50-default.conf
5.3 “排在后面” = 逐键覆盖,不是取消别人的语句
这是最关键的一点,也是最容易误解的:
你的文件排在 vendor 文件后面,只能覆盖你写了的那些键。vendor 文件里你没覆盖到的键——尤其是 glob——照样执行。
也就是说,一个排在 50-default.conf 后面的本地文件,只要它自己没有写 net.ipv4.conf.*.rp_filter,那 50-default.conf 的 net.ipv4.conf.*.rp_filter = 2 依然会把所有网卡刷成 2。想抵消它,必须自己再写一条对应的 glob,而不是指望”我文件排在后面”。
六、同一类陷阱不止 rp_filter:接口级键的三种内核语义
前面只举了 rp_filter。实际上 net.ipv4.conf.* / net.ipv6.conf.* / net.ipv4.neigh.* / net.ipv6.neigh.* 这四个命名空间里的每一个键都有同样的结构:值分两层存(一个全局的 all/default 伪接口 + 每张网卡一份),而 all 到底是”下限”还是”回落默认值”,取决于键自己的语义。
这一点直接决定了”只写 all 到底够不够”:
| 语义 | 生效规则 | all 的角色 |
典型键 |
|---|---|---|---|
| MAX | max(all, <iface>) |
下限 | rp_filter、accept_source_route |
| OR | all \|\| <iface> |
下限(布尔意义) | promote_secondaries |
| fallback | <iface> 为 0 时回落到 all 的值 |
缺省值 | arp_announce、route_localnet |
netns 里一分钟就能验证(不需要 root):
unshare --net --map-root-user sh -c '
ip link add name t0 type dummy
echo 0 > /proc/sys/net/ipv4/conf/all/rp_filter
echo 2 > /proc/sys/net/ipv4/conf/t0/rp_filter
echo "rp_filter -> $(cat /proc/sys/net/ipv4/conf/t0/rp_filter) # MAX: all=0 压不住"
echo 0 > /proc/sys/net/ipv4/conf/all/promote_secondaries
echo 1 > /proc/sys/net/ipv4/conf/t0/promote_secondaries
echo "promote_secondaries -> $(cat /proc/sys/net/ipv4/conf/t0/promote_secondaries) # OR: 同上"
echo 2 > /proc/sys/net/ipv4/conf/all/arp_announce
echo "arp_announce -> $(cat /proc/sys/net/ipv4/conf/t0/arp_announce) # fallback: 0 表示用 all"
'
结论:MAX / OR 类的键,”只写 all” 一律不管用 —— 要关掉某张网卡的某个行为,必须落到那张网卡上(逐键写,或自己写一条 glob)。
fallback 类看着安全,但也有反直觉的地方:per-iface 的 0 不是”禁用”,而是”用 all 的值”。比如 arp_announce 的取值 0/1/2 里,0 的含义是”用默认”,不是”关掉”。
6.1 vendor 文件里的接口级 glob 就那么几条
Debian 的 vendor 文件把这类键统一用三段式写:
net.ipv4.conf.default.X = V
net.ipv4.conf.*.X = V
-net.ipv4.conf.all.X
最后一行是”把 all 重置回内核默认值”(因为 all 也被 * 匹配到了,所以要单独还原)。
以 Debian 13 的 vendor 文件为例,接口级 glob 只有三条:
| glob | vendor 值 | 语义 | 冲突风险 |
|---|---|---|---|
net.ipv4.conf.*.rp_filter |
2 |
MAX | 高 —— 管理员通常想要 0 |
net.ipv4.conf.*.accept_source_route |
0 |
MAX | 低(vendor 值恰好就是安全值) |
net.ipv4.conf.*.promote_secondaries |
1 |
OR | 低(同上) |
真会”打架”的是第一条:vendor 想要 2(宽松反向路径校验),管理员想要 0(彻底关掉)。
另外两条现在没冲突,但语义陷阱一模一样 —— 哪天想把 promote_secondaries 关掉,只写 all=0 依然是白写。
6.2 自己写的 glob 方向可能反:”只写 all+default” 只对新网卡生效
net.ipv6.conf.*.disable_ipv6 是最常见的一个例子。很多自动化脚本会写:
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
看起来”全局关了 IPv6″。实际上:
- 新建的网卡 → 从
default复制 → 关掉了 ✔ - 开机前就存在的网卡 → 没人管 → IPv6 还开着 ✘
同一个坑,只是方向相反:rp_filter 是老网卡新网卡都错,而”只写 all+default”是只对新网卡对。
而且这不是洁癖问题:IPv6 还开着,就多了一条入站路径。只按 IPv4 写的防火墙规则、ACL、安全组,对 IPv6 一律不生效;只要网卡上有一个 link-local 地址(fe80::),IPv6 邻居发现、RA 就已经在跑了。
6.3 给自己写一份审计清单
# 1) 我的本地文件里所有接口级键
grep -E '^net\.(ipv4|ipv6)\.(conf|neigh)\.' /etc/sysctl.d/*.conf
# 2) 所有目录里带 * 的行(潜在的二次覆盖源)
grep -rn -E '^[^#]*\*' /usr/lib/sysctl.d/ /etc/sysctl.d/
# 3) 运行时每个接口的实际值 —— 唯一可信的依据
for f in /proc/sys/net/ipv4/conf/*/* /proc/sys/net/ipv6/conf/*/*; do
printf '%-56s %s\n' "${f#/proc/sys/}" "$(cat "$f")"
done
第 3 条是唯一可信的依据。配置文件的直觉不可靠 —— 上面三类语义、两层伪接口、再加上 udev 的二次覆盖,任何一条都能让”我明明写了”和”实际生效”对不上。
七、正确做法
7.1 配置
在 /etc/sysctl.d/ 下建一个本地文件,文件名用 60-90 区间(man 页推荐:/usr/ 用 10-40,/etc/ 与 /run/ 用 60-90,保证本地配置排在 vendor 之后):
sudo tee /etc/sysctl.d/60-local.conf > /dev/null << 'EOF'
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.default.rp_filter = 0
net.ipv4.conf.*.rp_filter = 0
EOF
三行各有分工,缺一不可:
| 行 | 作用 | 单独够用吗 |
|---|---|---|
net.ipv4.conf.all.rp_filter = 0 |
压低下限,防止有别的文件把 all 调高 |
不够,压不住 per-iface |
net.ipv4.conf.default.rp_filter = 0 |
新建网卡的初值模板 | 不够,会被 udev 重跑覆盖 |
net.ipv4.conf.*.rp_filter = 0 |
直接命中每张网卡 | 这行才是主力 |
第三行在开机时会把当时存在的所有网卡刷成 0;之后每张新网卡出现,udev 触发的那次重跑也会命中它——跟 vendor 那条 glob 走的是完全对称的路径,所以必然覆盖成功。
如果某张网卡有特殊要求,显式写在 glob 之后:
net.ipv4.conf.*.rp_filter = 0
net.ipv4.conf.eth0.rp_filter = 1
7.2 生效
sudo sysctl --system
sysctl --system 会按文件名顺序重跑所有 sysctl.d:vendor 的 glob 先把网卡刷成 2,你 60 开头的文件后跑,再刷成 0。净结果就是 0,所以这条命令是安全的。
也可以只加载自己那一个文件:
sudo sysctl -p /etc/sysctl.d/60-local.conf
7.3 验证
# 逐张网卡看实际值
for f in /proc/sys/net/ipv4/conf/*/rp_filter; do
printf '%-40s %s\n' "${f#/proc/sys/net/ipv4/conf/}" "$(cat "$f")"
done
期望输出:每一行都是 0。只要还有一张不是 0,那张网卡就是还在被反向路径校验。
新建网卡是否真的继承了 0,可以现场验证(不需要 root,用 user namespace 起一个 netns):
unshare --net --map-root-user sh -c '
ip link add name t0 type dummy
echo "新建网卡继承: $(cat /proc/sys/net/ipv4/conf/t0/rp_filter)"
echo 2 > /proc/sys/net/ipv4/conf/default/rp_filter
ip link add name t1 type dummy
echo "default=2 后新建: $(cat /proc/sys/net/ipv4/conf/t1/rp_filter)"
echo 0 > /proc/sys/net/ipv4/conf/all/rp_filter
echo "all=0 后 t1: $(cat /proc/sys/net/ipv4/conf/t1/rp_filter)"
'
预期:default 决定新网卡的初值;all=0 不会把一张 2 的网卡拉下来(max 规则)。
八、几个容易连带踩的坑
8.1 sysctl --system 会让”修好的东西”被打回原形
sysctl --system 按顺序重跑所有文件。vendor 的 glob 先把当时存在的所有网卡刷成 2,只有你文件里显式写到的键会被改回 0。
所以:如果你只是手工 sysctl -w 改过某张网卡(没写进文件),一次 sysctl --system 就把它打回 2。所有改动都要落到文件里,不能只改运行时。
8.2 不要用安装脚本整体覆盖 /etc/sysctl.d/ 下的文件
自动化脚本里常见这种写法:
cat > /etc/sysctl.d/xxx.conf << 'EOF'
... 一大堆参数 ...
EOF
sysctl --system
> 是整体覆盖。后果有两个:
- 会冲掉管理员手工加的行(本地文件里的注释掉的行、临时调过的值全没了)
- 更隐蔽的是:
sysctl --system这条命令本身会让 vendor 的 glob 重新盖一遍所有网卡
建议:本地配置写到独立文件,用 60-90 区间的名字;或者至少用 >> 追加而不是 > 覆盖。
8.3 别用 sysctl -w 当”解决方案”
sysctl -w 改的是运行时,重启即失。要长期生效,必须落到 sysctl.d 文件里。
8.4 lo 是个例外
udev 规则里排除了 lo(KERNEL!="lo"),所以 loopback 不会被二次覆盖,只在开机时被 vendor 的 glob 命中一次。你的 glob 在开机时同样会命中它,结果一致。
九、一张图记住
┌─────────────────────────────────────────┐
│ net.ipv4.conf.*.rp_filter = 2 (vendor) │
│ 按文件名排序,先于本地文件执行 │
└───────────────────┬─────────────────────┘
│ glob 展开成每张网卡的具体键
▼
开机 ──────────────────────────► 所有现存网卡 = 2
│
新网卡出现 ──► udev 重跑 systemd-sysctl ─┘ (作用域限这张网卡) ──► 这张网卡 = 2
▲
│ 同一路径,你的 glob 也在这里生效
你的 60-local.conf ─┘ net.ipv4.conf.*.rp_filter = 0
│
▼
实际生效值 = max(all, <iface>)
│
net.ipv4.conf.all.rp_filter = 0 ──────────┘ 只能压低下限,压不住 2
一句话:all 是下限,default 是初值模板,只有 net.ipv4.conf.*.rp_filter 这种 glob 才真正逐张网卡命中;而 glob 必须在”你自己的文件”里再写一遍,因为排序靠后只能逐键覆盖,不能取消别人的语句。
附:相关内核与手册页
| 出处 | 内容 |
|---|---|
ip-sysctl.txt |
conf/all/rp_filter 是 max 下限;conf/default/rp_filter 是新建网卡的初值模板 |
sysctl.d(5) |
文件按 basename 字典序排序,目录不参与;同名时 /etc 整体替换 /usr/lib;推荐 /etc 用 60-90 区间;网络接口专属选项会在每张网卡出现时单独再应用一次 |
/usr/lib/udev/rules.d/99-systemd.rules |
上面那条”网卡出现就重跑 systemd-sysctl”的实现 |
/usr/lib/sysctl.d/50-default.conf |
Debian 默认 rp_filter = 2 的出处 |
排查这类问题时,先看实际生效值:
cat /proc/sys/net/ipv4/conf/*/rp_filter
比读配置文件的直觉可靠得多。