linux服务器经常被用来提供防火墙、路由器、NAT等功能,在这些场景下,linux内核需要将网卡上收到的报文转发给其他网络设备。linux内核提供了ip_forward参数用于开关内核的报文转发功能,只有这个开关被打开时,内核才会执行报文的转发。网上能找到不少文章介绍ip_forward参数的基本用途和配置方式,但没有什么文章具体介绍这个参数配置后具体会对内核的网络行为产生哪些影响,以及在内核中是如何实现的。而这些问题的答案对转发功能的使用方式会产生很大的影响。
本文将详细分析ip_forward参数的配置方式和影响,以及这个参数在内核中的实现逻辑。分析基于5.9.11版本的内核。
问题
- ip_forward配置在什么位置配置?能按什么粒度配置?
- ip_forward打开或关闭会对内核的网络处理流程产生哪些影响?
- ip_forward配置除了影响转发功能外,是否还会对其他网络配置或功能产生副作用?
- ip_forward的配置开关在内核中是如何实现的?
配置
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的逻辑大体如下:
- 通过读写ipv4_devconf.data[IPV4_DEVCONF_FORWARDING - 1],完成sysctl配置文件的基本读写操作。每个配置文件都有自己对应的ipv4_devconf.data,在ctl_table中维护。如果是读操作,那么操作过程已经结束。如果是写操作并修改了配置的值,那么还要执行后续的操作。
- 如果配置的是default,则跳到第5步。
- 如果配置的是all,则执行inet_forward_change,将default和所有设备的forwarding配置全部修改。并且会将all/accept_redirects改成与forwarding相反的值。
- 如果打开了ip_forward,则调用dev_disable_lro,关闭相应网络设备的LRO功能。
- 调用inet_netconf_notify_devconf,为每个被修改的配置项发送rtnetlink消息,通知关注forwarding配置的组件配置已经发生了变化。
- 调用rt_cache_flush,更新netns的路由配置序号,标记路由配置发生了变化。
可以看到上述实现和前面的配置操作是基本对应的。但是要注意到forwarding配置还会产生两个副作用:
- 配置all/forwarding会同步修改all/accept_redirects为相反值。accept_redirects配置决定内核是否接收ICMP redirect消息。
- 打开forwarding会关闭对应设备的LRO功能。LRO会造成报文被聚合,可能会让报文过大或者checksum错误从而无法被转发,因此需要在转发功能打开时关闭LRO。这个操作可能显著影响内核网络的报文接收性能。
影响
在内核协议栈中,通过IN_DEV_FORWARD(in_dev)宏来获取当前设备的forwarding配置。通过检索内核代码,可以看到有5处逻辑会通过forwarding配置决定后续流程。
- br_nf_pre_routing_finish函数,位于net/bridge/br_netfilter_hooks.c
这是网桥设备的报文处理流程,大体上是说如果在打开了forwarding的情况下ip_route_input查询路由仍然返回了EHOSTUNREACH失败,则不再继续处理skb。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;
- bpf_ipv4_fib_lookup函数,位于net/core/filter.c
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。
- arp_process函数,位于net/ipv4/arp.c
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代理应答。
- ip_error函数,位于net/ipv4/route.c
这是路由查找失败后的错误处理函数,会在ip_route_input_slow中被设置为非本地报文的后续处理函数,在报文处理的主路径函数ip_rcv_finish中调用。这个函数会将报文skb释放,结束处理流程。函数内会根据是否启用了forwarding来决定如何处理路由失败,如果没有启用forwarding,那么这就是个错误路由到本地的包,直接丢弃即可;如果启用了forwarding,那么就是本机作为网关无法完成报文路由,会向报文发送端反馈一个ICMP_HOST_UNREACH的ICMP报文。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函数,位于net/ipv4/route.c
这是内核协议栈路由查找的核心逻辑。在报文的路由查找没有找到目的地址位置,或者目的地址不是本地设备地址时,就需要根据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,也就是转发功能的核心函数。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; }
总结
本文详细分析了linux内核中常用的网络配置项ip_forward的用法、作用和实现。最后看一下相关问题是否已经被解答:
- ip_forward配置在什么位置配置?能按什么粒度配置?
ip_forward相关配置在ipv4和ipv6的sysctl配置中有独立的配置项,按网络设备粒度配置生效。真正起影响的配置项为forwarding。 - ip_forward打开或关闭会对内核的网络处理流程产生哪些影响?
ip_forward配置开关会对内核网络处理的多处逻辑产生影响,其中最重要的影响是决定目的地址不是本地设备的报文能否被转发。具体影响的逻辑参见上文。 - ip_forward配置除了影响转发功能外,是否还会对其他网络配置或功能产生副作用?
除了影响内核转发功能开关,ip_forward的开关还会影响accept_redirects功能和LRO功能的开关,需要注意对相关功能和网络性能的影响。此外,ipv6的forwarding开关还会影响accept_ra的功能。 - ip_forward的配置开关在内核中是如何实现的?
每个sysctl配置项都有对应的处理函数,由sysctl文件系统的read/write实现函数来调用。ip_forward的处理函数为devinet_sysctl_forward,这个函数主要通过读写设备的forwarding配置项来使配置生效。