net.ipv4.icmp_errors_use_inbound_ifaddr内核代码参数详解

net.ipv4.icmp_errors_use_inbound_ifaddr内核代码参数详解

学习icmp相关的内核参数的时候,遇到了这个内核参数,看了看官方文档的描述,感觉它描写的云里雾里的,问了问朋友,朋友也看不懂文档的描述,互联网上对这个参数的解读大多又都是直接从内核文档这里复制过来的。既然如此,那就很有必要读一读内核代码,直接从代码层面解决问题了。

📄文档描述

这里先贴出官方文档对这个参数的解释:

icmp_errors_use_inbound_ifaddr - BOOLEAN

If zero, icmp error messages are sent with the primary address of the exiting interface.

If non-zero, the message will be sent with the primary address of the interface that received the packet that caused the icmp error. This is the behaviour many network administrators will expect from a router. And it can make debugging complicated network layouts much easier.

Note that if no primary address exists for the interface selected, then the primary address of the first non-loopback interface that has one will be used regardless of this setting.

Default: 0

首先文档说,这个参数接受两种情况:

  • 如果是0,icmp的错误信息会从exiting interface带着primary address发出
  • 如果非0,错误信息会从接收到引起这个错误的包的接口带着primary address发出

首先这里我们遇到了两个陌生的词汇exiting interfaceprimary address

在网络术语中,exiting interface通常指的是数据包离开网络设备的那个网络接口。

至于什么是primary address我们来看看代码吧。

❓前提知识:什么是primary address

源码

primary address中文翻译是首要地址,在内核中你还可以见到secondary address,这个指的是次要地址。

一个地址是不是secondary address会被记录在struct in_ifaddr地址结构体中的ifa_flags中。这个flags会在地址插入的时候就决定好,因此我们来看看Linux地址插入的关键代码(下文中文部分为自己添加的注释)。

// defined in https://github.com/torvalds/linux/blob/2594faafeee2f4406ff82790604e4e3f55037d60/net/ipv4/devinet.c#L476

/*
添加ip地址
primary address添加到最后一个满足范围的primary address后面
从地址添加到整个列表最后面
若列表中存在与插入地址在同一子网的地址,则
要求ip地址不同且范围相同,并且插入地址认为是secondary address
*/
static int __inet_insert_ifa(struct in_ifaddr *ifa, struct nlmsghdr *nlh,
u32 portid, struct netlink_ext_ack *extack)
{
struct in_ifaddr __rcu **last_primary, **ifap;
struct in_device *in_dev = ifa->ifa_dev;
struct in_validator_info ivi;
struct in_ifaddr *ifa1;
int ret;

ASSERT_RTNL();

/* 地址不存在,则释放资源并退出 */
if (!ifa->ifa_local) {
inet_free_ifa(ifa);
return 0;
}

/*IFA_F_SECONDARY的值为0x1,利用二进制计算,实现将secondary address标志清除,也就是所有地址一开始默认为primary address(只有两个状态,非secondary就是primary),后面根据一些条件再认定为secondary address*/
ifa->ifa_flags &= ~IFA_F_SECONDARY;
last_primary = &in_dev->ifa_list; /* 记录最后一个scope大于当前插入地址的scope并且是primary address的位置,用于如果当前地址类型是primary address时,将当前地址插入到last_primary地址后面(所有address是一个链表) */

/* Don't set IPv6 only flags to IPv4 addresses */
ifa->ifa_flags &= ~IPV6ONLY_FLAGS;

/*ifa_list为要添加地址的网卡设备的一个地址链表的头,即这个指向了第一个地址,每一个地址结构in_ifaddr都会有一个ifa_next,指向了该接口的下一个地址*/
ifap = &in_dev->ifa_list;
/*利用Read-Copy-Update机制,实现安全的解引用指针。如果只是从功能角度理解,等于 ifa1 = *ifap */
ifa1 = rtnl_dereference(*ifap);

/*遍历地址列表,利用二级指针遍历链表*/
while (ifa1) {
/*如果是primary address并且其scope大于等于当前要插入的地址。这个if的作用就是找到符合的last_primary,参见上文last_primary作用的描述*/
if (!(ifa1->ifa_flags & IFA_F_SECONDARY) &&
ifa->ifa_scope <= ifa1->ifa_scope)
last_primary = &ifa1->ifa_next;
/*如果插入地址和当前遍历地址属于一个子网*/
if (ifa1->ifa_mask == ifa->ifa_mask &&
inet_ifa_match(ifa1->ifa_address, ifa)) {
/*如果和之前的地址重复了,报错退出*/
if (ifa1->ifa_local == ifa->ifa_local) {
inet_free_ifa(ifa);
return -EEXIST;
}
/*如果两个地址的scope不一样,报错退出*/
if (ifa1->ifa_scope != ifa->ifa_scope) {
NL_SET_ERR_MSG(extack, "ipv4: Invalid scope value");
inet_free_ifa(ifa);
return -EINVAL;
}
/*标记当前地址为secondary address。这也是这段代码中唯一一个会更改primary还是secondary的了。因此可以得出结论,如果再给一个接口添加一个同子网的address那么它就是secondary地址*/
ifa->ifa_flags |= IFA_F_SECONDARY;
}

/*链表遍历,选择下一个address*/
ifap = &ifa1->ifa_next;
ifa1 = rtnl_dereference(*ifap);
}

/*与我们目的无关*/
/* Allow any devices that wish to register ifaddr validtors to weigh
* in now, before changes are committed. The rntl lock is serializing
* access here, so the state should not change between a validator call
* and a final notify on commit. This isn't invoked on promotion under
* the assumption that validators are checking the address itself, and
* not the flags.
*/
ivi.ivi_addr = ifa->ifa_address;
ivi.ivi_dev = ifa->ifa_dev;
ivi.extack = extack;
ret = blocking_notifier_call_chain(&inetaddr_validator_chain,
NETDEV_UP, &ivi);
ret = notifier_to_errno(ret);
if (ret) {
inet_free_ifa(ifa);
return ret;
}

/* 下面添加地址规则 */
/* 主地址放在最后一个满足范围的主地址的后面 */
/* 从地址放在最后一个(从)地址的后面 */

/*判断当前插入地址是不是primary address。ifap的作用就是当前地址要插入链表的前一个node*/
if (!(ifa->ifa_flags & IFA_F_SECONDARY))
ifap = last_primary;

/*又是保证了安全的赋值,可以理解为ifa->ifa_next = *ifap*/
/*下面两行都是链表插入,但是使用了二级指针的方法(与常用教科书上的模板不一致)*/
rcu_assign_pointer(ifa->ifa_next, *ifap);
rcu_assign_pointer(*ifap, ifa);

/*下面都无关了*/
inet_hash_insert(dev_net(in_dev->dev), ifa);

cancel_delayed_work(&check_lifetime_work);
queue_delayed_work(system_power_efficient_wq, &check_lifetime_work, 0);

/* Send message first, then call notifier.
Notifier will trigger FIB update, so that
listeners of netlink will know about new ifaddr */
rtmsg_ifa(RTM_NEWADDR, ifa, nlh, portid);
blocking_notifier_call_chain(&inetaddr_chain, NETDEV_UP, ifa);

return 0;
}

补充一下scope

enum rt_scope_t {
RT_SCOPE_UNIVERSE=0, // 等于global
/* User defined values */
RT_SCOPE_SITE=200,
RT_SCOPE_LINK=253,
RT_SCOPE_HOST=254,
RT_SCOPE_NOWHERE=255
};

结论

看完代码我们可以给出结论了:

  • primary address:当前网卡上不存在与要插入地址是同一个子网的IP,则新插入地址为primary address。
  • secondary address:当前网卡上已存在与插入地址是同一个子网的IP,后插入的地址为secondary address

我们还可以看出in_device中地址链表的分布方式了:

链表分布方式

若此时我想插入一个127.0.2.1/24,结果将如下:

插入127.0.2.1

此时如果插入一个192.168.2.3/24,结果将如下:

插入192.168.2.3/24

🔎 研究icmp_errors_use_inbound_ifaddr代码

知道了什么是primary address以后,我们再来看看这个参数的内核代码

源码

// defined in https://github.com/torvalds/linux/blob/2594faafeee2f4406ff82790604e4e3f55037d60/net/ipv4/icmp.c#L587C1-L589C2

void __icmp_send(struct sk_buff *skb_in, int type, int code, __be32 info,
const struct ip_options *opt)
{
....
/*
* Construct source address and options.
*/
/*
iph为内核收到的包的相关描述,saddr是source addr的意思,发出的包将以这个做为源地址。
*/
saddr = iph->daddr;
/*rt为进来数据包的路由结构,判断是不是本地only的包,是的话就不需要做这些了*/
if (!(rt->rt_flags & RTCF_LOCAL))
/*dev将会被赋值为,内核收到这个包的接口*/
struct net_device *dev = NULL;

/*锁,保障并发安全*/
rcu_read_lock();
/*判断这个包是不是接收到的,并且我们关注的这个参数是否开启,READ_ONCE为优化宏,告诉编译器一定要去读,而不是优化成读缓存*/
if (rt_is_input_route(rt) &&
READ_ONCE(net->ipv4.sysctl_icmp_errors_use_inbound_ifaddr))
/*skb_in为收到的引发 ICMP 错误的原始包,根据进来的数据包skb_in,查找对应进来的dev*/
dev = dev_get_by_index_rcu(net, inet_iif(skb_in));

/*如果在上文中成功获得了dev*/
if (dev)
/*选择一个地址,该函数为重要函数,查看下方的详细解析*/
saddr = inet_select_addr(dev, iph->saddr,
RT_SCOPE_LINK);
else
/*赋值为0不会出现任何问题,如果值为0,会在icmp_route_lookup由路由系统负责赋值对应的值,下文详解*/
saddr = 0;
/*解锁*/
rcu_read_unlock();
}

...
/*这是发出的icmp包中,头字段的destination ip地址。可以看到无论开不开这个参数,这个值永远等于,接收到这个包的时候这个包的destination ip*/
ipc.addr = iph->saddr;
...
/*查找构造好的icmp包,该用怎样的路由发出去。这是刚刚赋值的saddr唯一被用到的地方。*/
/*其中参数
net表示当前的网络空间;
fl4包含IPv4 路由查找的信息;
skb_in收到的引发 ICMP 错误的原始包;
iph代表了接受到的包的IP头部,其中的source ip,在路由寻路查询时一定会被做为发出包的destination IP;
saddr在发出包的源IP地址,在这个函数中该函数会被赋值到fl4—>saddr中,也就是实际上发出的包的source ip。但是如果该saddr为0,路由系统会负责选择一个合适的ip填充到fl4->saddr中,如果非0,路由系统将依据fl4->saddr来路由寻址。;
tos为服务优先级;
mark为网络标记;
code为ICMP消息代码;
icmp_param包含了 ICMP 消息的各种参数。
下方详解该函数
*/
rt = icmp_route_lookup(net, &fl4, skb_in, iph, saddr, tos, mark,
type, code, &icmp_param);
...
}

inet_select_addr

// defined in https://github.com/torvalds/linux/blob/994d5c58e50e91bb02c7be4a91d5186292a895c8/net/ipv4/devinet.c#L1325
__be32 inet_select_addr(const struct net_device *dev, __be32 dst, int scope)
{
const struct in_ifaddr *ifa;
__be32 addr = 0;
/*具体有哪些scope,参照上方primary address源码探究中的标注*/
unsigned char localnet_scope = RT_SCOPE_HOST;
struct in_device *in_dev;
struct net *net = dev_net(dev);
int master_idx;

rcu_read_lock();
in_dev = __in_dev_get_rcu(dev);
if (!in_dev)
goto no_in_dev;

/*
IN_DEV_ROUTE_LOCALNET(in_dev) 宏用于检查与给定的 in_device 结构相关联的网络设备是否启用了 route_localnet 配置选项。
route_localnet 是一个布尔值,用于控制是否允许从网络设备上的非本地地址(即非 127.0.0.0/8 地址)路由到本地主机。默认情况下,这个选项是关闭的,以防止可能的安全问题。
*/
if (unlikely(IN_DEV_ROUTE_LOCALNET(in_dev)))
localnet_scope = RT_SCOPE_LINK;

/*遍历dev的所有address*/
in_dev_for_each_ifa_rcu(ifa, in_dev) {
/*如果是secondary address则跳过*/
if (ifa->ifa_flags & IFA_F_SECONDARY)
continue;
/*由函数调用可知,传入的scope为RT_SCOPE_LINK=253。
当route_localnet选项没开启时,localnet_scope=RT_SCOPE_HOST=254,此时ifa_scope必须为非NOWHERE、HOST
当route_localnet选项没开启时,localnet_scope=RT_SCOPE_LINK=253,此时恒成立,ifa_scope可以是任何值
*/
if (min(ifa->ifa_scope, localnet_scope) > scope)
continue;
/*
如果dst和当前地址在同一个子网,则直接返回这个addr
*/
if (!dst || inet_ifa_match(dst, ifa)) {
addr = ifa->ifa_local;
break;
}
/*
addr暂时等于这个值,看看有没有更好的地址,如在同一个子网的。如果没有在同一个子网的,最后的addr会是scope更低的,也就是更偏向是UNIVERSE的地址
*/
if (!addr)
addr = ifa->ifa_local;
}

/*
如果在刚刚的循环中,找到了一个地址的话,直接退出函数。
*/
if (addr)
goto out_unlock;
no_in_dev:
/*若当前接口为某个VRF的slave interface,则输出当前接口所属VRF的id,否则输出0*/
master_idx = l3mdev_master_ifindex_rcu(dev);

/* For VRFs, the VRF device takes the place of the loopback device,
* with addresses on it being preferred. Note in such cases the
* loopback device will be among the devices that fail the master_idx
* equality check in the loop below.
*/
/*上面英语为源码原来的注释,翻译过来是:对于 VRF,VRF 设备取代了环回设备,其上的地址被优先选择。请注意,在这种情况下,环回设备将是在下面循环中失败于 master_idx 相等检查的设备之一。 */
/*也就是说如果当前interface是属于某个VRF的,那么就不再是选取环回设备的地址,更别说和文档中说的优先选取了,而只会尝试获取VRF接口的地址*/
if (master_idx &&
(dev = dev_get_by_index_rcu(net, master_idx)) &&
(in_dev = __in_dev_get_rcu(dev))) {
addr = in_dev_select_addr(in_dev, scope);
if (addr)
goto out_unlock;
}

/* Not loopback addresses on loopback should be preferred
in this case. It is important that lo is the first interface
in dev_base list.
*/
/*上面英文的翻译是:在环回设备上的非环回地址将会优先选择。注意lo设备是dev_base列表中第一个元素*/
/*这里的dev_base列表是net中记录当前namespace下所有设备的列表,也就是for_each_netdev_rcu遍历的*/
for_each_netdev_rcu(net, dev) {
/*只查看和当前接口属于同一个VRF的设备。如果一个设备不属于任何VRF设备,那么l3mdev_master_ifindex_rcu将返回0。也就是说一个设备已经属于VRF了,那么该循环只会遍历同样属于那个VRF下面的接口。如果该设备不属于任何VRF,master_idx=0,因此只会遍历不属于任何VRF的接口
*/
if (l3mdev_master_ifindex_rcu(dev) != master_idx)
continue;

/*下面的函数,之前已出现过*/
in_dev = __in_dev_get_rcu(dev);
if (!in_dev)
continue;

addr = in_dev_select_addr(in_dev, scope);
if (addr)
goto out_unlock;
}
out_unlock:
rcu_read_unlock();
return addr;
}

icmp_route_lookup

这里我们主要关注上文中的saddr是怎么赋值到最终的作为发出结构体flow4上的即可

// defined in https://github.com/torvalds/linux/blob/815fb87b753055df2d9e50f6cd80eb10235fe3e9/net/ipv4/icmp.c#L476
static struct rtable *icmp_route_lookup(struct net *net,
struct flowi4 *fl4,
struct sk_buff *skb_in,
const struct iphdr *iph,
__be32 saddr, u8 tos, u32 mark,
int type, int code,
struct icmp_bxm *param)
{
struct net_device *route_lookup_dev;
struct rtable *rt, *rt2;
struct flowi4 fl4_dec;
int err;

memset(fl4, 0, sizeof(*fl4));
/*发出的ip包的destination address*/
fl4->daddr = (param->replyopts.opt.opt.srr ?
param->replyopts.opt.opt.faddr : iph->saddr);
/*发出的ip包的source address*/
fl4->saddr = saddr;
fl4->flowi4_mark = mark;
fl4->flowi4_uid = sock_net_uid(net, NULL);
fl4->flowi4_tos = RT_TOS(tos);
fl4->flowi4_proto = IPPROTO_ICMP;
fl4->fl4_icmp_type = type;
fl4->fl4_icmp_code = code;
/*下两行只与VRF设备有关,普通设备fl4->flowi4_oif最终为0。这并不很影响路由查询,icmp的路由查询主要还是依靠fl4->daddr*/
route_lookup_dev = icmp_get_route_lookup_dev(skb_in);
fl4->flowi4_oif = l3mdev_master_ifindex(route_lookup_dev);

/*用于处理安全的上下文*/
security_skb_classify_flow(skb_in, flowi4_to_flowi_common(fl4));
/*
根据刚刚对fl4设置的地址来进行路由查询。
如果此时的fl4->saddr为0,寻路的时候会在ip_route_output_key_hash_rcu函数中调用的fib_select_path函数中,将saddr赋值为路由上设置的ip,即我们使用ip route查询路由时,输出的类似:default via 192.168.114.1 dev wlp0s20f3 proto dhcp src 192.168.114.110 metric 20600中的src后面的内容
*/
rt = ip_route_output_key_hash(net, fl4, skb_in);
if (IS_ERR(rt))
return rt;
/*下文主要是xfrm安全系统的一些处理逻辑,这里我们可以不关注*/
...
}

✅结论

我们理一下逻辑:

  • 只有当一个输入的包,是转发包时(目的地非本机),该参数才会起作用

  • 如果开启了该参数,只会影响saddr的值的选择,saddr的值会被被设置为输入接口的相关地址,其中:

    • 如果输入地址是primary ip

      • 那么遇到第一个和输入包的源地址是同一个子网的primary ip时会立即选择它为saddr
      • 如果没有那就是输入接口的地址链表中最后的一个primary ip,这个ip的scope是所有primary ip中最大的
    • 会忽略所有的secondary ip

    • 如果在当前接口上找不到任何primary ip

      • 判断当前设备是否是VRF的从设备或主设备,如果是就尝试先获取VRF主设备的地址,如果主设备没有合法地址,会尝试获取同一个VRF下面其它子设备是否有合法地址
      • 如果当前设备不是VRF设备,会尝试遍历所有不属于VRF设备的其它设备,从他们上面获取一个合法IP,由于遍历的链表的第一个元素是lo接口,所以在此情况下,是符合文档中的,找不到任何地址,会用第一个回环地址的地址来做为源地址
  • 如果没开启参数,saddr的值为0

    • 依靠路由系统根据构造出包的ip destination address来确定saddr的值
      • 路由系统确定出的值,一般就是匹配到的路由,后面的src标记,如default via 192.168.114.1 dev wlp0s20f3 proto dhcp src 192.168.114.110 metric 20600中就是192.168.114.110

所以开不开这个参数,只会对转发包有影响。在一般情况下,我们完全没有必要开启这个函数,只有我们有很复杂的路由规则的时候,对于来自同一个地址的ip,输入和输出时的路由不一致,导致从不同接口出去的时候,这才会带来影响。