做网络运维这几年,IPv6的坑我踩了不少,最让我印象深刻的不是地址规划,也不是隧道配置,而是一个看起来不起眼的概念:路由条目里的src字段。简单说,IPv6 路由可以在条目里"钉死"一个优先源地址,正常情况下挺好用,但一旦这个源地址失效或被删除,路由表就成了带着脏数据的半残状态——数据包照发,对端看到的来源地址却不对,排查起来特别费劲。这篇文章我把"IPv6 路由源地址"的清除问题完整拆开讲,包括字段原理、脏数据产生场景、四种清除手法、一次完整排查复盘,以及清理之后必须注意的收尾事项,希望对搞 IPv6 网络的朋友有实际参考价值。
1. src字段的来龙去脉:为什么IPv6路由会绑定一个"源地址"
1.1 路由条目里src到底存了什么
先看一个典型的 Linux IPv6 路由表片段:
default via fe80::1 dev eth0 src 2001:db8:1::10 metric 1024 pref medium末位的src 2001:db8:1::10就是优先源地址。它的含义很直白:凡是通过这条路由出去的流量,内核优先使用这个地址作为 IPv6 报文源地址。这个机制的好处是保证源地址可控。比如一台服务器上同时有业务地址和管理地址,你完全可以通过在路由里指定src,让某段流量固定走业务地址,日志审计也方便。
坏处同样明显:如果这个地址已经不存在了,或者已经进入 deprecated 状态,内核在选源时就可能进入异常逻辑。
需要注意的是,src并不是路由条目的必填项。不带src的条目长这样:
2001:db8:1234::/64 dev eth0 proto kernel metric 256 pref medium这种条目没有钉死源地址,内核会按 RFC 6724 定义的流程自动选源。自动选源算法考虑了作用域、地址类型、退避状态等一堆规则,绝大多数情况下是靠谱的。问题恰恰出在"手动钉死"的部分——你越是想精确控制,越要警惕后续地址变化带来的联动效应。
1.2 和IPv4相比,为什么IPv6特别容易栽在这上面
IPv4 路由虽然也有src参数(ip route add ... src),但在日常运维中很少用到,因为一个接口上通常只有一个主 IPv4 地址,源地址没什么好纠结的。IPv6 完全不同:
- 一个接口上可以同时存在多个全局单播地址;
- 手动配置的地址、SLAAC 自动生成的地址、DHCPv6 获取的地址可能并存;
- 临时隐私地址(RFC 8981)还会周期性轮换,旧的被废弃、新的自动生成。
这种多地址并存的局面,让"源地址选择"成了 IPv6 运维里绕不开的话题。很多老手为了避免源地址飘来飘去,习惯在路由里显式写src。这个习惯本身没错,但它埋下了一颗雷:地址是有生命周期的,删除一个地址时,内核并不会同步清理所有引用它的路由条目。于是路由表里就出现了引用空地址的"僵尸 src"。
换句话说,IPv6 的源地址管理是动态的,而路由条目里的 src 是静态快照。两者一旦脱节,脏数据就产生了。
2. 哪些情况下路由里的src会变成"脏数据"
2.1 地址被删了但路由没跟着清理
这是最高频的场景。举个例子,你原来给 eth0 配了2001:db8:1::10/64,并手动加了一条默认路由:
ip -6 addr add 2001:db8:1::10/64 dev eth0 ip -6 route add default via fe80::1 dev eth0 src 2001:db8:1::10后来因为地址段重新规划,你执行了:
ip -6 addr del 2001:db8:1::10/64 dev eth0此时接口上已经没有2001:db8:1::10了,但那条默认路由的src 2001:db8:1::10还在。内核发流量时会试图使用一个不存在的地址作为源。不同内核版本的表现并不一致:有些会直接选一个替代地址,有些则会在连接建立时报错,表现就是"能通又不完全通"。
类似的场景还有 Docker 网桥、Kubernetes 节点、虚拟机热迁移之后,网卡被重建,地址被替换,路由表里的老src却没跟着刷新。迁移后第一反应通常是通不通,很少有人先去看路由表里是不是已经有脏字段。
2.2 RA自动学习出的路由带着过时源地址
第二个高频场景来自 SLAAC。现在的发行版默认在物理网卡上开启了接收路由器通告(accept_ra)。路由器发 RA 时,内核会动态学习路由。这个过程中,内核给新路由填写的src是从接口当前已有地址里选出来的。
问题在于:IPv6 临时地址是变化的。假设某台机器开机的瞬间,接口上第一个全局地址是临时隐私地址2001:db8:1::abcd:1234,内核从 RA 里学到了默认路由,并把src钉成这个临时地址。过了一段时间,临时地址过期被自动废弃,新临时地址2001:db8:1::abcd:9999生成,但 RA 学到的路由条目并不会立刻更新src。
于是你看到的现象就是:机器能拿到新地址,也能 ping 通外网网关,但出去的流量源地址还是那个已经废弃的老地址。在老地址进入 deprecated 状态后,内核尝试以它为源建立新连接时会被拒绝,表现非常诡异。
2.3 多地址和临时地址导致的"源打架"
还有一种情况不在路由表本身,而在地址生命周期。假设接口上有两个全局地址:
| 地址 | 状态 | 用途 |
|---|---|---|
| 2001:db8:1::10 | preferred | 固定业务地址 |
| 2001:db8:1::abcd:1234 | deprecated | 隐私地址,已轮换 |
如果某条路由的src指向的是 deprecated 的地址,内核源地址选择时会有意避开它,但你路由里显式钉了它,规则冲突就会导致连接不稳定。这时候"清除路由源地址"的实际操作,要么是把路由里的src改到 preferred 地址,要么干脆删掉src让内核重新按 RFC 6724 做选择。
这种"源打架"在纯 IPv4 环境里几乎不存在,但在 IPv6 环境里每周都可能遇上一次。排查这类问题,核心思路就一条:先看路由表里的 src,再看地址状态,两者对不上就是根源。
3. 清除路由源地址的实操方法:四条路子按需选
3.1 方法一:删了重建,最直接
这是最彻底、也最不容易出意外的做法。先删掉带脏src的路由,再不带src重新添加:
# 删除指定路由 ip -6 route del default via fe80::1 dev eth0 # 重新添加,不指定 src ip -6 route add default via fe80::1 dev eth0 # 如果是前缀路由,同理 ip -6 route del 2001:db8:1234::/64 dev eth0 ip -6 route add 2001:db8:1234::/64 dev eth0这里有个细节值得说明:ip -6 route change在修改已有路由时,未指定的属性默认保留原值。也就是说你执行ip -6 route change default via fe80::1 dev eth0,很可能发现src还在。这不是 bug,是 change 的语义就是"改我指定的、留我没提的"。所以想彻底去掉src,删了重建是最可靠的,永远不要指望 change 帮你抹掉字段。
3.2 方法二:replace原地替换,适合做脚本
如果你不想让流量有哪怕一瞬的中断,可以用replace:
ip -6 route replace default via fe80::1 dev eth0replace的语义和change不同:它会用你给定的参数整体替换原条目,未指定的属性恢复默认值。所以上面的命令执行完,这条默认路由的src字段会被清掉,而且路由表里始终存在对应的条目,不会出现删除和添加之间的空窗期。
需要提醒的是,replace在路由不存在时会直接新建一条,这通常没问题,但如果你本意是"必须改的是已有条目",建议先ip -6 route show确认一下。脚本化场景下,先 replace 后验证是标准操作。
3.3 方法三:针对RA动态路由的处理
如果脏src来自 RA 自动学习,光删不解决问题,因为下一个 RA 周期又会给你装回来。这时候要从源头处理:
# 先关闭接口的 RA 接收 sysctl -w net.ipv6.conf.eth0.accept_ra=0 # 删除动态学来的路由 ip -6 route del default via fe80::1 dev eth0 # 或者更暴力一点,清空默认路由表(谨慎使用,会断开所有 IPv6 连接) ip -6 route flush table main清完之后,你可以选择手动配静态路由,也可以重新打开 accept_ra:
sysctl -w net.ipv6.conf.eth0.accept_ra=1重新打开后,内核会重新学习 RA,并用当前有效的接口地址填充src。实测下来,这个办法在路由器刚改完 RA 参数、或者接口地址整体换代时特别管用。注意 flush 操作在远程机器上要格外小心,先确认你有别的通道能连上去,不然清完默认路由,SSH 会话可能直接断。
3.4 方法四:调整地址状态让src自动失效
还有一种思路是反其道而行之:不直接动路由,而是让地址状态恢复。比如由于隐私地址轮换导致src指向了 deprecated 地址,你可以重新生成一个 preferred 地址,再更新路由里的src:
# 查看当前地址状态 ip -6 addr show eth0 # 删除废弃地址 ip -6 addr del 2001:db8:1::abcd:1234/64 dev eth0 # 想让内核自动刷新,可临时关闭再开启临时地址机制 sysctl -w net.ipv6.conf.eth0.use_tempaddr=0 sysctl -w net.ipv6.conf.eth0.use_tempaddr=2这个方法的优势是不用打断路由表,缺点是不可控:你没法保证内核刷新路由的时机。所以我的建议是,方法四只作为辅助手段,真正要清除 src,还是回到方法一或方法二。
四种方法选哪个,取决于你对"中断"的容忍度和src的来源:
| 方法 | 适用场景 | 优点 | 风险 |
|---|---|---|---|
| 删了重建 | 静态路由带脏 src | 最彻底,可控 | 有瞬间空窗期 |
| replace 替换 | 脚本化、在线调整 | 无空窗期 | 条目不存在时会新建 |
| 处理 RA 路由 | RA 动态学习导致的脏 src | 从源头解决 | 需要动 sysctl,影响范围大 |
| 调整地址状态 | 地址 deprecated 导致选源异常 | 不打断路由 | 时机不可控,不彻底 |
4. 一次完整排查复盘:从"能ping通但源地址不对"开始
4.1 问题现象与初始判断
场景还原一下:一套多节点服务集群,某节点 A 需要访问另一个网段的节点 B。现象是 A 能 ping 通 B 的 IPv6 地址,但 B 上收到的 ICMPv6 ping 报文源地址不是 A 当前配的2001:db8:1::20,而是一个已经不存在的老地址2001:db8:1::10。B 上的防火墙策略是针对新地址段放的,老地址段早已停用,于是回包被拦,表现为"能 ping 通但连接建立不起来"。
这种"通一半"的问题最容易被误判为防火墙、路由环路、MTU 问题。我当时的判断顺序是:先确认物理连通性,再检查防火墙,最后才去看路由表。后来发现,前两步都是白费功夫,真正的根因就藏在路由表里。
4.2 逐步定位到src的完整链路
第一步,看接口地址:
$ ip -6 addr show eth0 inet6 2001:db8:1::20/64 scope global dynamic接口上只有2001:db8:1::20,老的2001:db8:1::10已经不在了。
第二步,看路由表:
$ ip -6 route show default via fe80::1 dev eth0 src 2001:db8:1::10 metric 1024 pref medium能明显看到默认路由的src指向老地址,问题基本锁定了。这里有个小技巧:不要只看ip -6 route show,一定要配合ip -6 route get看内核实际选源结果——
$ ip -6 route get 2001:db8:2::50 2001:db8:2::50 from :: via fe80::1 dev eth0 src 2001:db8:1::10 metric 1024 pref mediumroute get是排查源地址问题的神器。它直接告诉你:如果现在要发一个到2001:db8:2::50的包,内核会用什么源地址。看到src 2001:db8:1::10的那一刻,问题就不再是猜谜,而是变成了"怎么改掉它"。
第三步,为了确认影响范围,再用抓包验证一下实际发出的报文:
tcpdump -i eth0 -nn icmp6抓包结果里,出去的 ping 请求源地址确实是2001:db8:1::10,与分析一致。
4.3 清除后的验证与抓包确认
定位完成后,我用 replace 一步处理:
ip -6 route replace default via fe80::1 dev eth0再确认:
$ ip -6 route show default via fe80::1 dev eth0 metric 1024 pref mediumsrc消失了。继续用route get验证选源结果:
$ ip -6 route get 2001:db8:2::50 2001:db8:2::50 from :: via fe80::1 dev eth0 src 2001:db8:1::20 metric 1024 pref medium源地址已经变成了当前有效的2001:db8:1::20。再执行 ping 和业务连通性测试,一切正常。整个过程从最开始看到现象到定位到src,大概花了四十分钟,其中大部分时间浪费在怀疑防火墙上。如果一开始就查路由表的src,十分钟内就能解决。
5. 清理之后的收尾经验:持久化、自动化与避坑提醒
5.1 路由配置的持久化方案
手动执行ip -6 route命令只对当前运行态生效,重启后一切回到默认。要持久化,必须把路由配置写进系统网络管理配置里。
用 systemd-networkd 的话,在网卡配置里写:
[Route] Destination=2001:db8:1234::/64 # 不要写 PreferredSource=,或者写当前有效的那个地址用 NetworkManager 的话,通过 nmcli 修改路由属性,同样要注意ipv6.route列表里不要携带已经失效的src。
很多人在迁移地址段时,只改了地址不改路由配置,网络一重启,路由表里又重新冒出老的src,就是因为这些配置还躺在网卡配置文件里。改地址的时候,一定要顺手把静态路由里的 src 一起改了,这句不是套话,是我踩了不止一次后的真实教训。
5.2 自动化检查:让"脏src"无处遁形
源地址这类问题平时不冒头,一冒头就是生产事故。与其被动救火,不如写个脚本定期检查。思路非常简单:用ip -6 route get探测一个固定目标的选源结果,判断src是否落在当前接口地址范围内。
#!/bin/bash # 检测默认路由的 src 是否仍然有效 GW="2001:db8:2::1" dev="eth0" expected=$(ip -6 addr show dev "$dev" | awk '/inet6/ && /global/ {print $2}' | cut -d/ -f1) got=$(ip -6 route get "$GW" | sed -n 's/.*src \([0-9a-fA-F:]*\).*/\1/p') if ! echo "$expected" | grep -q "$got"; then echo "[WARN] route src $got is stale, fixing..." ip -6 route replace default via fe80::1 dev "$dev" fi把这个脚本丢进 cron 里每五分钟跑一次,源地址漂移问题基本能在几分钟内被自动纠正。我在一个长期运营的节点上挂了类似脚本,半年内自动修复了三次,每次都是临时地址轮换导致的脏src。
5.3 三个容易忽略的坑
第一,清理 src 不等于清理整个路由表。远程操作时千万不要手滑执行ip -6 route flush table main,那会把所有 IPv6 默认路由和业务路由全部清掉,SSH 会话可能瞬间断开。真要 flush,也要精确指定路由特征,或者先加一条兜底路由再 flush。
第二,RA 自动路由会"复活"。如果你只是删掉了 RA 学来的那条路由,而没有动 accept_ra 参数,下一次 RA 通告到达时,路由会被重新创建,src又变回老样子。所以对 RA 场景,一定要在 sysctl 层面处理,而不是只删路由。
第三,对端设备可能有基于源地址的 ACL。你清理了src之后,内核自动选出的新源地址,未必是对端白名单里的地址。如果清理完发现连接还是不通,先检查对端是否只允许特定前缀的源地址,必要时要把正确的src显式写回路由里——注意,是写当前有效的地址,而不是直接清掉。
最后再分享一个小技巧:处理这类问题,养成"先ip -6 route get再ip -6 route show"的习惯。前者告诉你内核当前会怎么选,后者告诉你路由表里写了什么,两者一对照,脏src立刻现形。IPv6 网络本来就比 IPv4 多一层源地址选择的复杂度,把这条链路彻底理解透,很多看似诡异的问题都会变得有迹可循。