linux服务器经常被用来提供防火墙、路由器、NAT等功能;在这些场景下;linux内核需要将网卡上收到的报文转发给其他网络设备。linux内核提供了ip_forward参数用于开关内核的报文转发功能;只有这个开关被打开时;内核才会执行报文的转发。网上能找到不少文章介绍ip_forward参数的基本用途和配置方式;但没有什么文章具体介绍这个参数配置后具体会对内核的网络行为产生哪些影响;以及在内核中是如何实现的。而这些问题的答案对转发功能的使用方式会产生很大的影响。
本文将详细分析ip_forward参数的配置方式和影响;以及这个参数在内核中的实现逻辑。分析基于5.9.11版本的内核。
ip_forward功能的配置开关有三个位置;
1. /proc/sys/net/ipv4/ip_forward
2. /proc/sys/net/ipv4/conf/{all/default/devname}/forwarding
3. /proc/sys/net/ipv6/conf/{all/default/devname}/forwarding
其中第一个配置是许多文章中提到的ip报文转发开关;这个配置开关是linux早期版本中定义的;它只能控制IPv4报文的转发功能;其功能和取值都等价于/proc/sys/net/ipv4/conf/all/forwarding。此外;实际上真正决定报文能否被转发的;是conf/devname/forwarding;这个配置在每个网卡设备的粒度控制这个网卡上收到的ipv4/ipv6报文能否被转发。
上面讲到;ip_forward等价于ipv4/conf/all/forwarding;而真正有效的是conf/devname/forwarding。那么conf/all/forwarding和conf/default/forwarding又是用来干嘛的?
首先;conf/default/forwarding用于控制新建设备的配置;如果在配置这个参数后创建一个新的网络设备;例如veth pair;;那么新创建设备的conf/devname/forwarding值就会等于default配置。
conf/all/forwarding则可以配置当前所有设备的forwarding参数;例如将all参数配置从0修改为1;则包括default在内的所有forwarding配置都将被改成1。要注意的是all配置只有在值被修改时才有效;重复写入all当前值不会对其他forwarding配置产生任何影响。
另外要说明的是all/forwarding配置只对当前net namespace生效;每个netns有自己的独立配置。
关于conf/下的其他配置项;还有一点需要注意的是每项配置的取值逻辑是不同的;例如;forwarding的配置取的是devname/forwarding的值;而mc_forwarding的实际配置值是all/mc_forwarding&&devname/mc_forwarding;accept_local的实际配置值是all/accept_local||devname/accept_local。要知道每项配置的实际取值方式;只能通过include/linux/inetdevice.h中的定义来了解;在内核文档Documentation/networking/ip-sysctl.rst中对各项配置的含义做了介绍;但是对取值逻辑的介绍还不完善。这个接口定义方式显然非常不友好;用户不可能记得所有选项的取值逻辑;而且这些取值逻辑还会随着内核版本而变化。对用户来说;需要记得在使用这些配置前查阅对应的内核代码;来确认这些配置的使用方法。
ip_forward配置是sysctl选项;也就是/proc/sys/文件系统中的虚拟文件。这类文件的操作实现是在fs/proc/proc_sysctl.c中实现的。每个sysctl配置文件都有一个对应的ctl_table数据结构;这个结构中指定的proc_handler回调函数负责实现对应文件的读写操作。
ipv4的ip_forward和forwarding配置对应的处理函数在net/ipv4/devinet.c中定义;是devinet_sysctl_forward。而ipv6的forwarding函数在net/ipv6/addrconf.c中定义;是addrconf_sysctl_forward。我们以ipv4的devinet_sysctl_forward为例分析实现。
devinet_sysctl_forward的逻辑大体如下;
可以看到上述实现和前面的配置操作是基本对应的。但是要注意到forwarding配置还会产生两个副作用;
在内核协议栈中;通过IN_DEV_FORWARD(in_dev)宏来获取当前设备的forwarding配置。通过检索内核代码;可以看到有5处逻辑会通过forwarding配置决定后续流程。
if ((err = ip_route_input(skb, iph->daddr, iph->saddr, iph->tos, dev))) {
struct in_device *in_dev = __in_dev_get_rcu(dev);
/* If err equals -EHOSTUNREACH the error is due to a
* martian destination or due to the fact that
* forwarding is disabled. For most martian packets,
* ip_route_output_key() will fail. It won;t fail for 2 types of
* martian destinations: loopback destinations and destination
* 0.0.0.0. In both cases the packet will be dropped because the
* destination is the loopback device and not the bridge. */
if (err != -EHOSTUNREACH || !in_dev || IN_DEV_FORWARD(in_dev))
goto free_skb;
rt = ip_route_output(net, iph->daddr, 0,
RT_TOS(iph->tos), 0);
if (!IS_ERR(rt)) {
/* - Bridged-and-DNAT;ed traffic doesn;t
* require ip_forwarding. */
if (rt->dst.dev == dev) {
skb_dst_set(skb, &rt->dst);
goto bridged_dnat;
}
ip_rt_put(rt);
}
free_skb:
kfree_skb(skb);
return 0;
这是网桥设备的报文处理流程;大体上是说如果在打开了forwarding的情况下ip_route_input查询路由仍然返回了EHOSTUNREACH失败;则不再继续处理skb。 dev = dev_get_by_index_rcu(net, params->ifindex);
if (unlikely(!dev))
return -EnodeV;
/* verify forwarding is enabled on this interface */
in_dev = __in_dev_get_rcu(dev);
if (unlikely(!in_dev || !IN_DEV_FORWARD(in_dev)))
return BPF_FIB_LKUP_RET_FWD_DISABLED;
这是一个内核的bpf helper函数;用于支持ebpf程序查找转发表。如果forwarding没有打开;则查找失败;返回BPF_FIB_LKUP_RET_FWD_DISABLED。
if (addr_type == RTN_LOCAL) {
...
} else if (IN_DEV_FORWARD(in_dev)) {
if (addr_type == RTN_UNICAST &&
(arp_fwd_proxy(in_dev, dev, rt) ||
arp_fwd_pvlan(in_dev, dev, rt, sip, tip) ||
(rt->dst.dev != dev &&
pneigh_lookup(&arp_tbl, net, &tip, dev, 0)))) {
...
这是内核处理arp报文的逻辑。如果收到了ARP请求报文;而且请求的目标不是本地IP;那么只有在forwarding打开时才有可能为其提供ARP代理应答。
net = dev_net(rt->dst.dev);
if (!IN_DEV_FORWARD(in_dev)) {
switch (rt->dst.error) {
case EHOSTUNREACH:
__IP_INC_STATS(net, IPSTATS_MIB_INADDRERRORS);
break;
case ENETUNREACH:
__IP_INC_STATS(net, IPSTATS_MIB_INNOROUTES);
break;
}
goto out;
}
这是路由查找失败后的错误处理函数;会在ip_route_input_slow中被设置为非本地报文的后续处理函数;在报文处理的主路径函数ip_rcv_finish中调用。这个函数会将报文skb释放;结束处理流程。函数内会根据是否启用了forwarding来决定如何处理路由失败;如果没有启用forwarding;那么这就是个错误路由到本地的包;直接丢弃即可;如果启用了forwarding;那么就是本机作为网关无法完成报文路由;会向报文发送端反馈一个ICMP_HOST_UNREACH的ICMP报文。 err = fib_lookup(net, &fl4, res, 0);
if (err != 0) {
if (!IN_DEV_FORWARD(in_dev))
err = -EHOSTUNREACH;
goto no_route;
}
if (res->type == RTN_BROADCAST) {
if (IN_DEV_BFORWARD(in_dev))
goto make_route;
/* not do cache if bc_forwarding is enabled */
if (IPV4_DEVCONF_ALL(net, BC_FORWARDING))
do_cache = false;
goto brd_input;
}
if (res->type == RTN_LOCAL) {
err = fib_validate_source(skb, saddr, daddr, tos,
0, dev, in_dev, &itag);
if (err < 0)
goto martian_source;
goto local_input;
}
if (!IN_DEV_FORWARD(in_dev)) {
err = -EHOSTUNREACH;
goto no_route;
}
这是内核协议栈路由查找的核心逻辑。在报文的路由查找没有找到目的地址位置;或者目的地址不是本地设备地址时;就需要根据forwarding开关来确定后续的操作。如果forwarding开关没有打开;会设置路由结果fib_result.type = RTN_UNREACHABLE;rtable.dst.input= ip_error;rtable.dst.error=EHOSTUNREACH。之后执行上面介绍的ip_error函数;将报文丢弃。如果forwarding打开;就会执行ip_mkroute_input准备后续的路由转发工作;其中就会设置rtable.dst.input=ip_forward;也就是转发功能的核心函数。本文详细分析了linux内核中常用的网络配置项ip_forward的用法、作用和实现。最后看一下相关问题是否已经被解答;