网络数据的收发和处理是操作系统的核心功能之一,一般在内核中直接实现。要修改内核中的网络处理功能,就需要对内核的网络处理逻辑进行修改,之后经过内核编译、升级,才能生效。这个过程的代价很大,显然不能用于需要频繁定制内核网络行为的场景。因此,需要一种可以对内核网络行为进行灵活定制的机制,在不用修改内核核心代码,不升级内核的前提下实现对内核网络处理逻辑的自定义修改。
在Linux内核中,netfilter和iptables就提供了这样一套内核网络定制机制。本文将简单介绍netfilter和iptables的原理、用途以及两者的关系,从而说明这套机制是如何实现通过用户态的简单配置,就能修改linux内核网络行为的。对netfilter和iptables的具体实现和用法将在后续的文章中讨论。
netfilter和iptables的应用非常广泛,linux系统上的防火墙、NAT,docker的bridge模式容器网络,都会基于这套机制实现。网上也有很多介绍netfilter和iptables机制的文章,其中一些为了简化概念,会将两者统称为iptables。但netfilter与iptables其实是有依赖关系的两套扩展机制,netfilter依赖内核提供内核态的基本网络扩展能力,iptables则基于netfilter提供了跨内核与用户态的网络定制能力。通过netfilter和iptables的组合,用户可以通过用户态的iptables配置工具下发网络行为规则,实现对网络功能的灵活定制。
上图是netfilter和iptables机制的整体结构示意图。可以看到其中主要包含了3个部分:内核协议栈中的netfilter,内核中的iptables相关模块,以及用户态的iptables工具。
如上图中所示,netfilter是内核协议栈基本功能的一部分,是不能作为模块动态加载的。内核协议栈在ip报文的处理路径上选择了5个关键位置,在这些位置上可以调用预先注册的一系列函数指针,来修改报文内容和后续处理路径。图中所画的是其中的LOCAL_IN位置,处于ip_local_deliver和ip_local_deliver_finish这两个处理函数之间。netfilter模块提供了nf_register_net_hook接口,用于注册处理函数指针。
此外,netfilter提供了在setsockopt和getsockopt系统调用中添加option和相应处理函数的接口,便于网络功能模块和用户态进行交互。
每个网络名空间netns中的netfilter是独立的,开发者需要为需要定制行为的每个netns分别调用注册接口来注册自己的回调函数。
netfilter与iptables功能和模块之间没有必然的联系。基于netfilter提供的函数注册接口,可以通过内核模块的方式实现自己的网络定制功能,而不需要和iptables有任何关系。很多厂商都会通过netfilter接口实现自己的NAT、ACL、QoS等功能,而不会通过iptables配置的方式。
但是,在内核中开发网络模块并实现和用户态的规则同步毕竟是一个非常复杂的工作。对于绝大部分应用场景(例如小流量的NAT、firewall等)来说,为了并不复杂的需求专门实现一整套内核与用户态机制显然是不现实的。iptables正是为了满足这些需求而产生的。
iptables是包含了用户态工具和内核态模块在内的一整套功能体系。用户态的iptables工具主要负责下发和获取内核的网络处理规则。iptables工具通过getsockopt/setsockopt系统调用,和内核的ip_tables模块进行交互,同步规则信息。这里使用的getsockopt/setsockopt,也是ip_tables模块使用netfilter提供的接口注册实现的。
内核态的iptables功能被解耦成了多个不同的模块来完成,解耦的原因是内核中支持的网络规则表类型在不断增加,而这些表操作的逻辑大部分是相同的,因此需要将这些公共逻辑提取出来供相关表操作共享,减少代码的重复和冗余。上图中只画了3个模块,事实上iptables(以及ip6tables等)的相关模块非常多,大致可以分为3类,图中画出的3个是其中的典型代表:
下面以上图中展示的iptable filter表的功能为例,介绍iptables的主要功能逻辑:
通过上述方式,iptables就能将用户态下发的规则通过netfilter机制作用到内核协议栈中。
本文介绍了netfilter和iptables机制存在的意义、最基本的实现原理、以及相关的模块架构。最后来回答一下文章起始时的问题作为总结: