ipv6源地址选择原理

本文最后更新于:2025年11月19日 下午

引言

一个接口通常有多个ipv6地址(比如可能存在多个全球地址,多个链路本地地址)。那么设备从接口发包时它会使用哪个地址作为源地址呢?

假设场景如下:
我们设备是路由器,br0有两个链路本地地址:fe80::1/64, fe80::2/64。
路由器下的PC发送dns查询,目的地址为fe80::1,路由器的dns server回包时会使用哪个源地址,fe80::1,还是fe80::2呢?
dns server发包时使用的sendto函数,而且socket也没有bind到一个特定的地址上,选择哪个源地址取决于内核的选择。
如果使用了fe80::2,那么糟糕,PC可能认为源地址与自己发送的目的地址不一样,丢包,导致无法上网。

本文主要围绕上面问题来分析地址选择,而略过其他场景下的选择。

内核选择算法

选择源地址的函数从ipv6_dev_get_saddr开始。

use_oif_addrs_only

如果目的地址是组播、链路本地地址、或者接口的use_oif_addrs_only配置为1,那么源地址只会从发包接口选择。

1
2
cat /proc/sys/net/ipv6/conf/enp3s0/use_oif_addrs_only 
0

use_oif_addrs_only默认为0,即使用全球地址通信时,源地址不一定是出接口上的IP地址。

__ipv6_dev_get_saddr

评选出接口上评分最高的地址。这是选择地址的核心。

它通过以下一系列的标准,从上往下比较两个地址的评分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* Choose an appropriate source address (RFC3484)
*/
enum {
IPV6_SADDR_RULE_INIT = 0,
IPV6_SADDR_RULE_LOCAL,
IPV6_SADDR_RULE_SCOPE,
IPV6_SADDR_RULE_PREFERRED,
#ifdef CONFIG_IPV6_MIP6
IPV6_SADDR_RULE_HOA,
#endif
IPV6_SADDR_RULE_OIF,
IPV6_SADDR_RULE_LABEL,
IPV6_SADDR_RULE_PRIVACY,
IPV6_SADDR_RULE_ORCHID,
IPV6_SADDR_RULE_PREFIX,
#ifdef CONFIG_IPV6_OPTIMISTIC_DAD
IPV6_SADDR_RULE_NOT_OPTIMISTIC,
#endif
IPV6_SADDR_RULE_MAX
};

IPV6_SADDR_RULE_LOCAL
如果目的地址和接口地址相等,那么优先

IPV6_SADDR_RULE_SCOPE
使用范围的比较,这儿实际判断比较复杂,因为地址范围有好几个。
可以先简单理解为:在源地址范围大于等于目的地址范围的情况下,地址的使用范围越小,越优先。如果源地址范围小于目的地址范围,那么分值一样。

IPV6_SADDR_RULE_PREFERRED
地址优选期没到期的高于到期的,优选期到期的地址在ip命令里面能看到deprecated标识

IPV6_SADDR_RULE_OIF
优先使用出接口的地址

IPV6_SADDR_RULE_LABEL
如果label与目的地址相同,那么优选这个地址。

IPV6_SADDR_RULE_PRIVACY
优先使用隐私地址(临时地址)。可通过proc配置接口的use_tempaddr,默认为2,默认使用隐私地址。
也可通过sockopt配置。

IPV6_SADDR_RULE_ORCHID
不清楚

IPV6_SADDR_RULE_PREFIX
前缀长度,长度越长,越优先。

1
2
3
ret = ipv6_addr_diff(&score->ifa->addr, dst->addr);
if (ret > score->ifa->prefix_len)
ret = score->ifa->prefix_len;

选谁呢

fe80::1/64, fe80::2/64这两个地址,用上面的选择算法走一遍,发现都是一样的。比较不出来。。
那么用哪个呢?用第一个,看哪个在链表最前面。
可以简单的理解为使用最近时间添加的一个,如果fe80::2/64是最近添加的,那么就用它。

接口上的地址顺序

接口上添加地址的函数如下。
地址列表是按照scope排序的。scope表示地址的使用范围,比如常见的全球地址(0x0e)和链路本地地址(0x02)。scope高的在前面,同一scope则是后填加的地址在前面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ipv6_link_dev_addr(struct inet6_dev *idev, struct inet6_ifaddr *ifp)
{
struct list_head *p;
int ifp_scope = ipv6_addr_src_scope(&ifp->addr);

/*
* Each device address list is sorted in order of scope -
* global before linklocal.
*/
list_for_each(p, &idev->addr_list) {
struct inet6_ifaddr *ifa
= list_entry(p, struct inet6_ifaddr, if_list);
if (ifp_scope >= ipv6_addr_src_scope(&ifa->addr))
break;
}

list_add_tail_rcu(&ifp->if_list, p);
}

我们通过ip命令查看到地址列表,和上面的顺序基本吻合

1
2
3
4
5
6
7
8
# ip -6 a show dev br0
23: br0: <BROADCAST,MULTICAST,ALLMULTI,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
inet6 2009::40:6002:20ff:fe11:34b0/64 scope global 全球
valid_lft forever preferred_lft forever
inet6 fe80::100/64 scope link 链路本地,后添加
valid_lft forever preferred_lft forever
inet6 fe80::6002:20ff:fe11:34b0/64 scope link 链路本地,先添加
valid_lft forever preferred_lft forever

dadfailed
添加一个ipv6地址时,默认会启动地址重复检查。如果dad检查失败,会有一条相关打印,ip命令看到的地址状态如下。

1
inet6 fe80::100/64 scope link dadfailed tentative

dadfailed的地址是无法使用的。

左右源地址优先级

还是最初的场景,要是我们无论如何都想系统使用fe80::2,而不使用fe80::1。要怎么做呢?

从上面的分析看,有两种方法:
(1)添加fe80::1时,设置它的优选期为0就行了。

1
ip a add fe80::1/64 dev eth0 preferred_lft 0

(2)缩短地址的前缀长度

1
ip a add fe80::1/10 dev eth0

参考

# IPv6源地址选择


ipv6源地址选择原理
https://leon0625.github.io/2024/01/25/d6c93a55c9e0/
作者
leon.liu
发布于
2024年1月25日
许可协议