大家好,我是飞哥!iptables这个工具的应用似乎越来越广了。不仅出现在传统的防火墙、NAT等功能中,在如今流行的Docker、Kubernetes、Istio等项目中也时常出现。正因为如此,深入了解iptables的工作原理是非常有价值的。Linux内核网络栈是纯内核态,与用户级函数天然隔离。但是内核为了迎合各个用户层的不同需求,开放了一些用户干预的口子。用户层可以通过一些配置改变内核的工作模式,以达到特殊的要求。Linux将netfilter过滤器放置在内核网络组件的许多关键位置。iptables是基于netfilter实现的。因此,术语iptables和netfilter在本文中有时可以互换使用。飞哥在网上也看了很多关于netfilter的技术文章,但是我觉得都不够清楚。所以让我们卷起袖子自己写一个吧。Netfilter的实现可以简单概括为四表五链。下面我们来详细了解一下四表五链是什么意思。一、Iptables中的五个链Linux下的Netfilter在内核协议栈的每个重要层次都埋下了五个钩子。每个钩子对应一系列的规则,这些规则以链表的形式存在,所以俗称五链。当网络数据包流向协议栈中的这些检查点时,注册在这些钩子上的各种规则会依次执行,进而实现对网络数据包的各种处理。要想理解好Wulian,飞哥认为最重要的还是要分别来看内核接收、发送、转发这三个过程。1.1接收过程Linux在IP层接收网络包的入口函数是ip_rcv。网络在这里遇到的第一个HOOK是PREROUTING。当钩子上的规则都处理完了,就会进行路由。如果发现是设备的网络包,输入ip_local_deliver,这里又会遇到INPUThook。我们来看详细代码,先看ip_rcv。//file:net/ipv4/ip_input.cintip_rcv(structsk_buff*skb,...){...returnNF_HOOK(NFPROTO_IPV4,NF_INET_PRE_ROUTING,skb,dev,NULL,ip_rcv_finish);}NF_HOOK这个函数会执行在各种注册的各种规则iptables中pre_routing中的表。处理完成后,输入ip_rcv_finish。路由将在此函数中完成。这就是PREROUTING链名称的由来,因为它在路由之前执行。//file:net/ipv4/ip_input.cstaticintip_rcv_finish(structsk_buff*skb){...if(!skb_dst(skb)){interr=ip_route_input_noref(skb,iph->daddr,iph->saddr,iph->tos,skb->dev);...}...returndst_input(skb);}如果发现在本地设备上接收到,则进入ip_local_deliver函数。然后它会再次执行LOCAL_IN钩子,也就是我们所说的INPUT链。//file:net/ipv4/ip_input.cintip_local_deliver(structsk_buff*skb){...returnNF_HOOK(NFPROTO_IPV4,NF_INET_LOCAL_IN,skb,skb->dev,NULL,ip_local_deliver_finish);}简单总结一下接收数据的处理流程是:PREROUTING链->路由判断(本机)->INPUT链->...1.2发送过程Linux在发送网络包的过程中,首先是发送的路由选择,然后遇到的第一个HOOK是OUTPUT,然后继续进入POSTROUTING链。让我们简单回顾一下源代码。网络层发送的入口函数是ip_queue_xmit。//file:net/ipv4/ip_output.cintip_queue_xmit(structsk_buff*skb,structflowi*fl){//路由选择过程//选择后记录路由信息到skbrt=(structrtable*)__sk_dst_check(sk,0);if(rt==NULL){//如果没有缓存,寻找路由项rt=ip_route_output_ports(...);sk_setup_caps(sk,&rt->dst);}skb_dst_set_noref(skb,&rt->dst);...//sendip_local_out(skb);}这里先进行路由选择,然后在发送时进入IP层函数__ip_local_out。//file:net/ipv4/ip_output.cint__ip_local_out(structsk_buff*skb){structiphdr*iph=ip_hdr(skb);iph->tot_len=htons(skb->len);ip_send_check(iph);returnnf_hook(NFPROTO_IPV4,NF_INET_LOCAL_OUT,skb,NULL,skb_dst(skb)->dev,dst_output);}上面的NF_HOOK会将数据包发送到NF_INET_LOCAL_OUT(OUTPUT)链。执行后进入dst_output。//file:include/net/dst.hstaticinlineintdst_output(structsk_buff*skb){returnskb_dst(skb)->output(skb);}这里获取之前的路由选择,并调用选择的输出进行发送。将进入ip_output。//file:net/ipv4/ip_output.cintip_output(structsk_buff*skb){...//再交给netfilter,回调ip_finish_outputreturnNF_HOOK_COND(NFPROTO_IPV4,NF_INET_POST_ROUTING,skb,NULL,dev,ip_finish_output,!(IPCB(skb)->flags&IPSKB_REROUTED));}总结起来,发送数据包的过程是:路由->OUTPUT链->POSTROUTING链->...1.3转发过程其实除了接收和发送过程,Linux内核也可以像路由器一样工作。它会接收网络数据包(不是自己的),然后根据路由表选择合适的网卡设备进行转发。在这个过程中,首先经历接收数据的前半部分。经过ip_rcv中的PREROUTING链,然后发现不是路由后设备的包,再进入ip_forward函数进行转发,这里又会遇到FORWARD链。最后会进入ip_output进行真正的发送,遇到POSTROUTING链。我们看一下源码,首先进入IP层入口ip_rcv,在这里遇到PREROUTING链。//file:net/ipv4/ip_input.cintip_rcv(structsk_buff*skb,...){...returnNF_HOOK(NFPROTO_IPV4,NF_INET_PRE_ROUTING,skb,dev,NULL,ip_rcv_finish);}链上PREROUTING规则处理后,输入ip_rcv_finish,这里选择routing,然后输入dst_input。//file:include/net/dst.hstaticinlineintdst_input(structsk_buff*skb){returnskb_dst(skb)->input(skb);}转发过程的这些步骤和接收过程完全一样。但是,内核路径将与上面的输入法调用不同。非本地设备不会进入ip_local_deliver,但会进入ip_forward。//file:net/ipv4/ip_forward.cintip_forward(structsk_buff*skb){...returnNF_HOOK(NFPROTO_IPV4,NF_INET_FORWARD,skb,skb->dev,rt->dst.dev,ip_forward_finish);}在ip_forward_finish中会被发送到IP层的发送函数ip_output。//file:net/ipv4/ip_output.cintip_output(structsk_buff*skb){...//再交给netfilter,回调ip_finish_outputreturnNF_HOOK_COND(NFPROTO_IPV4,NF_INET_POST_ROUTING,skb,NULL,dev,ip_finish_output,!(IPCB(skb)->flags&IPSKB_REROUTED));}在ip_output中会遇到POSTROUTING链。后续过程与发送过程的后半部分相同。总结一下转发数据的过程:PREROUTING链->路由判断(不是设备,寻找下一跳)->FORWARD链->POSTROUTING链->...1.4经过iptables总结理解接收、发送和发送三个过程转发,我们把上面三个过程放在一起。数据接收的过程是1和2,发送的过程是4、5,转发的过程是1、3、5。有了这张图,我们可以更清楚地理解iptables和内核的关系。2.iptables的四大表在上一节中,我们介绍了iptables中的五条链。每个链可能由许多规则组成。当对这条链执行NF_HOOK时,规则会按照优先级逐一传递。如果存在满足条件的规则,则执行该规则对应的动作。而这些规则根据不同的目的可以是raw、mangle、nat和filter。行表的作用是对命中规则的报文跳过其他表的处理,其优先级最高。mangle表的作用是按照规则修改数据包的一些标志位。例如TTLnat表的作用就是实现网络地址转换。过滤表的作用是过滤某些数据包,它是防火墙的基础。比如在PREROUTING链中的规则中,可以分别执行row、mangle和nat三个函数。再来说说,为什么四张桌子都不全。这是由于功能不同,并非所有功能都会使用所有五个链。Raw表项的目的是为了跳过其他表,所以只需要在接收和发送这两个主要流程开始的时候检查一下,所以只需要用到PREROUTING和OUTPUT这两个钩子。Mangle表可能在任何位置修改网络数据包,因此它使用所有挂钩位置。NAT分为SNAT(SourceNAT)和DNAT(DestinationNAT),可能工作在四个位置:PREROUTING、INPUT、OUTPUT、POSTROUTING。Filter只工作在INPUT、OUTPUT和FORWARD这三个步骤就够了。整体来看,四个链接和五个表的关系如下图所示。这里多说一点,每个命名空间都有自己独立的iptables规则。我们以NAT为例。内核遍历NAT规则时,是从net(命名空间变量)的ipv4.nat_table中取出来的。NF_HOOK最终会执行到nf_nat_rule_find函数。//file:net/ipv4/netfilter/iptable_nat.cstaticunsignedintnf_nat_rule_find(...){structnet*net=nf_ct_net(ct);unsignedintret;//重要!!!!!!nat_table存储在命名空间ret=ipt_do_table(skb,hooknum,in,out,net->ipv4.nat_table);if(ret==NF_ACCEPT){if(!nf_nat_initialized(ct,HOOK2MANIP(hooknum)))ret=alloc_null_binding(ct,hooknum);}returnret;}Docker容器基于命名空间工作,因此每个Docker容器都可以配置自己独立的iptables规则。3、Iptables使用示例看完前面两节,大家已经明白四表五链是如何实现的了。那么我们就通过几个实用的功能来看看iptables是如何在实践中使用的吧。3.1nat如果我们有一台Linux,它的eth0的IP是10.162.0.100,通过这个IP就可以访问其他服务器。现在我们已经在本机上创建了一个Docker虚拟网络环境net1,其网卡veth1的IP为192.168.0.2。如果想让192.168.0.2访问外网,就需要host网络命名空间下的设备帮助它转发网络包。由于这是一个私有地址,只有这个Linux知道,所以它不能访问外部服务器。这时如果想让net1正常访问10.162.0.101,就必须在转发时进行SNAT——源地址替换。SNAT在路由之后和网络数据包发送之前工作,也就是POSTROUTING链。我们将以下iptables规则添加到主机的命名空间。该规则判断如果源是192.168.0网段,目的不是br0,则进行所有源IP替换判断。#iptables-tnat-APOSTOUTING-s192.168.0.0/24!-obr0-jMASQUERADE有了这个规则,我们来看看整个签约过程。数据包发出去的时候,先从veth发送到br0。由于br0在主机的命名空间中,这将执行到POSTROUTING链中。在这条链中有我们刚刚配置的snat规则。根据这个规则,内核将网络包中的192.168.0.2(外界无法识别)替换为母机的IP10.162.0.100(外界可以识别)。还要跟踪链接状态。然后主机根据自己的路由表判断,选择默认发送设备从eth0网卡发送数据包,直到发送到10.162.0.101。接下来,10.162.0.100将收到来自10.162.0.101的响应数据包。由于上一步记录了链路跟踪,主机可以知道回复包是针对192.168.0.2的。然后反转替换,通过br0返回正确的veth。这样net1环境下的veth1就可以访问外网服务了。3.2DNAT目的地址替换按照上节的例子,假设我们要在192.168.0.2上提供80端口的服务。同样,外部服务器也无法访问该地址。这时,DNAT目的地址应该被替换。需要在数据包进来的时候将目的地址替换为192.168.0.2:80。DNAT工作在内核收到网络包的第一条链上,也就是PREROUTING。我们添加一条DNAT规则,具体配置如下。#iptables-tnat-APREROUTING!-ibr0-ptcp-mtcp--dport8088-jDNAT--to-destination192.168.0.2:80当来自外界的网络数据包到达eth0时。由于eth0在父机器的命名空间中,因此执行PREROUTING链。该规则判断如果端口是8088的TCP请求,则将目的地址替换为192.168.0.2:80。然后通过br0(192.168.0.1)转发数据包,数据包会到达实际提供服务的192.168.0.2:80。DNAT中也会有链路跟踪记录,所以返回包中的源地址从192.168.0.2到10.162.0.101会被替换为10.162.0.100:8088。10.162.0.101收到数据包后,一直以为自己真的在和10.162.0.100:8088通信。这样net1环境下的veth1也可以对外网提供服务了。实际上,单机Docker通过这两节介绍的SNAT和DNAT配置进行网络通信。3.3filter过滤表主要实现对网络数据包的过滤。如果发现有恶意IP疯狂请求我们的服务器,就会影响服务。然后我们可以使用过滤器来禁用它。它的工作原理是在接收到的数据包的INPUT链的位置进行判断,如果发现是恶意请求,会第一时间杀死,不做任何处理。避免到上层继续浪费CPU开销。具体配置方法如下:#iptables-IINPUT-s1.2.3.4-jDROP//Block#iptables-DINPUT-s1.2.3.4-jDROP//Unblock当然你也可以屏蔽某个IP段。#iptables-IINPUT-s121.0.0.0/8-jDROP//Block#iptables-IINPUT-s121.0.0.0/8-jDROP//Unblock再举个例子,假设你不想让别人登录你的带有任意ssh的服务器,只允许您的IP访问。然后只放开你自己的IP,其他都禁用。#iptables-tfilter-IINPUT-s1.2.3.4-ptcp--dport22-jACCEPT#iptables-tfilter-IINPUT-ptcp--dport22-jDROP3.4rawRaw表中的规则可以绕过其他表的处理。在nat表中,为了保证双向流量能够正常完成地址替换,会跟踪记录链路状态。每个连接都会生成相应的记录。使用下面两个命令查看。#conntrack-L#cat/proc/net/ip_conntrack但是在高流量的情况下,可能会出现连接跟踪记录满的问题。我曾经遇到过在单机测试百万并发连接时由于连接数超过nf_conntrack_max而无法建立新连接的问题。#ip_conntrack:tablefull,droppingpacket但实际上如果不使用NAT功能,可以关闭链接跟踪功能,例如。#iptables-traw-APREROUTING-d1.2.3.4-ptcp--dport80-jNOTRACK#iptables-AFORWARD-mstate--stateUNTRACKED-jACCEPT3.5mangle路由器转发网络包时,ttl值会减1,即当设置为0时,最后一个路由器将停止转发这个数据包。如果不想让这个路由影响ttl,可以在mangel表中加1补上。#ptables-tmangle-APREROUTING-ieth0-jTTL--ttl-inc1所有从eth0接口进来的数据包的ttl值加1,抵消路由转发默认扣1。总结Iptables是一个很常用也很重要的工具。Linux上的firewall、nat等基本功能都是基于它实现的。它也经常出现在流行的Docker、Kubernetes和Istio项目中。正因为如此,深入了解iptables的工作原理是非常有价值的。今天我们先从第一节内核的接收、发送、转发三个不同的过程来了解五链的位置。然后根据iptables的另一个维度的描述,从功能的角度,表。每个表在多个挂钩位置注册自己的规则。当数据包被处理时,规则被触发并执行。整体来看,四个链接和五个表的关系如下图所示。最后给出了raw、mangle、nat、filter表的简单应用示例。希望大家通过今天的学习,能够彻底的整合iptables。相信这会对您的工作有很大的帮助!
