一文读懂Linux下单机实现百万并发的内核黑科技:连接跟踪(Conntrack),


本文介绍连接跟踪(connection tracking,conntrack,CT)的原理,应用,及其在 Linux 内核中的实现。

代码分析基于内核 4.19。为使行文简洁,所贴代码只保留了核心逻辑,但都给出了代码 所在的源文件,如有需要请查阅。

水平有限,文中不免有错误之处,欢迎指正交流。

1 引言

连接跟踪是许多网络应用的基础。例如,Kubernetes Service、ServiceMesh sidecar、 软件四层负载均衡器 LVS/IPVS、Docker network、OVS、iptables 主机防火墙等等,都依赖 连接跟踪功能。

1.1 概念

连接跟踪(conntrack)

图 1.1. 连接跟踪及其内核位置

连接跟踪,顾名思义,就是跟踪(并记录)连接的状态。

例如,图 1.1 是一台 IP 地址为 10.1.1.2 的 Linux 机器,我们能看到这台机器上有三条 连接:

连接跟踪所做的事情就是发现并跟踪这些连接的状态,具体包括:

  •  从数据包中提取元组(tuple)信息,辨别数据流(flow)和对应的连接(connection)
  •  为所有连接维护一个状态数据库(conntrack table),例如连接的创建时间、发送 包数、发送字节数等等
  •  回收过期的连接(GC)
  •  为更上层的功能(例如 NAT)提供服务

需要注意的是,连接跟踪中所说的“连接”,概念和 TCP/IP 协议中“面向连接”( connection oriented)的“连接”并不完全相同,简单来说:

  •  TCP/IP 协议中,连接是一个四层(Layer 4)的概念。
    •  TCP 是有连接的,或称面向连接的(connection oriented),发送出去的包都要求对端应答(ACK),并且有重传机制
    •  UDP 是无连接的,发送的包无需对端应答,也没有重传机制
  •  CT 中,一个元组(tuple)定义的一条数据流(flow )就表示一条连接(connection)。
    •  后面会看到 UDP 甚至是 ICMP 这种三层协议在 CT 中也都是有连接记录的
    •  但不是所有协议都会被连接跟踪

本文中用到“连接”一词时,大部分情况下指的都是后者,即“连接跟踪”中的“连接”。

网络地址转换(NAT)

图 1.2. NAT 及其内核位置

网络地址转换(NAT),意思也比较清楚:对(数据包的)网络地址(IP + Port)进行转换。

例如,图 1.2 中,机器自己的 IP 10.1.1.2 是能与外部正常通信的,但 192.168 网段是私有 IP 段,外界无法访问,也就是说源 IP 地址是 192.168 的包,其应答包是无 法回来的。

因此当源地址为 192.168 网段的包要出去时,机器会先将源 IP 换成机器自己的 10.1.1.2 再发送出去;收到应答包时,再进行相反的转换。这就是 NAT 的基本过程。

Docker 默认的 bridge 网络模式就是这个原理 [4]。每个容器会分一个私有网段的 IP 地址,这个 IP 地址可以在宿主机内的不同容器之间通信,但容器流量出宿主机时要进行 NAT。

NAT 又可以细分为几类:

  •  SNAT:对源地址(source)进行转换
  •  DNAT:对目的地址(destination)进行转换
  •  Full NAT:同时对源地址和目的地址进行转换

以上场景属于 SNAT,将不同私有 IP 都映射成同一个“公有 IP”,以使其能访问外部网络服 务。这种场景也属于正向代理。

NAT 依赖连接跟踪的结果。连接跟踪最重要的使用场景就是 NAT。

四层负载均衡(L4 LB)

图 1.3. L4LB: Traffic path in NAT mode [3]

再将范围稍微延伸一点,讨论一下 NAT 模式的四层负载均衡。

四层负载均衡是根据包的四层信息(例如 src/dst ip, src/dst port, proto)做流量分发。

VIP(Virtual IP)是四层负载均衡的一种实现方式:

  •  多个后端真实 IP(Real IP)挂到同一个虚拟 IP(VIP)上
  •  客户端过来的流量先到达 VIP,再经负载均衡算法转发给某个特定的后端 IP

如果在 VIP 和 Real IP 节点之间使用的 NAT 技术(也可以使用其他技术),那客户端访 问服务端时,L4LB 节点将做双向 NAT(Full NAT),数据流如图 1.3。

1.2 原理

了解以上概念之后,我们来思考下连接跟踪的技术原理。

要跟踪一台机器的所有连接状态,就需要

例如,

除了以上两点功能需求,还要考虑性能问题,因为连接跟踪要对每个包进行过滤和分析 。性能问题非常重要,但不是本文重点,后面介绍实现时会进一步提及。

之外,这些功能最好还有配套的管理工具来更方便地使用。

1.3 设计:Netfilter

图 1.4. Netfilter architecture inside Linux kernel

Linux 的连接跟踪是在 Netfilter 中实现的。

Netfilter 是 Linux 内核中一个对数据 包进行控制、修改和过滤(manipulation and filtering)的框架。它在内核协议 栈中设置了若干hook 点,以此对数据包进行拦截、过滤或其他处理。

  “    说地更直白一些,hook 机制就是在数据包的必经之路上设置若干检测点,所有到达这 些检测点的包都必须接受检测,根据检测的结果决定:

    连接跟踪模块只是完成连接信息的采集和录入功能,并不会修改或丢弃数据包,后者是其 他模块(例如 NAT)基于 Netfilter hook 完成的。    ”

Netfilter 是最古老的内核框架之一,1998 年开始开发,2000 年合并到 2.4.x 内 核主线版本 [5]。

1.4 设计:进一步思考

现在提到连接跟踪(conntrack),可能首先都会想到 Netfilter。但由 1.2 节的讨论可知, 连接跟踪概念是独立于 Netfilter 的,Netfilter 只是 Linux 内核中的一种连接跟踪实现。

换句话说,只要具备了 hook 能力,能拦截到进出主机的每个包,完全可以在此基础上自 己实现一套连接跟踪。

图 1.5. Cilium's conntrack and NAT architectrue

云原生网络方案 Cilium 在 1.7.4+ 版本就实现了这样一套独立的连接跟踪和 NAT 机制 (完备功能需要 Kernel 4.19+)。其基本原理是:

因此,即便卸载掉 Netfilter ,也不会影响 Cilium 对 Kubernetes ClusterIP、NodePort、ExternalIPs 和 LoadBalancer 等功能的支持 [2]。

由于这套连接跟踪机制是独立于 Netfilter 的,因此它的 conntrack 和 NAT 信息也没有 存储在内核的(也就是 Netfilter 的)conntrack table 和 NAT table。所以常规的 conntrack/netstats/ss/lsof 等工具是看不到的,要使用 Cilium 的命令,例如:

  1. $ cilium bpf nat list  
  2. $ cilium bpf ct list global 

配置也是独立的,需要在 Cilium 里面配置,例如命令行选项 --bpf-ct-tcp-max。

另外,本文会多次提到连接跟踪模块和 NAT 模块独立,但出于性能考虑,具体实现中 二者代码可能是有耦合的。例如 Cilium 做 conntrack 的垃圾回收(GC)时就会顺便把 NAT 里相应的 entry 回收掉,而非为 NAT 做单独的 GC。

以上是理论篇,接下来看一下内核实现。

2 Netfilter hook 机制实现

Netfilter 由几个模块构成,其中最主要的是连接跟踪(CT) 模块和网络地址转换(NAT)模块。

CT 模块的主要职责是识别出可进行连接跟踪的包。CT 模块独立于 NAT 模块,但主要目的是服务于后者。

2.1 Netfilter 框架

5 个 hook 点

图 2.1. The 5 hook points in netfilter framework

如上图所示,Netfilter 在内核协议栈的包处理路径上提供了 5 个 hook 点,分别是:

  1. // include/uapi/linux/netfilter_ipv4.h  
  2. #define NF_IP_PRE_ROUTING    0 /* After promisc drops, checksum checks. */  
  3. #define NF_IP_LOCAL_IN       1 /* If the packet is destined for this box. */  
  4. #define NF_IP_FORWARD        2 /* If the packet is destined for another interface. */  
  5. #define NF_IP_LOCAL_OUT      3 /* Packets coming from a local process. */  
  6. #define NF_IP_POST_ROUTING   4 /* Packets about to hit the wire. */  
  7. #define NF_IP_NUMHOOKS       5 

用户可以在这些 hook 点注册自己的处理函数(handlers)。当有数据包经过 hook 点时, 就会调用相应的 handlers。

  “    另外还有一套 NF_INET_ 开头的定义,include/uapi/linux/netfilter.h。这两套是等价的,从注释看,NF_IP_ 开头的定义可能是为了保持兼容性。

  1. enum nf_inet_hooks {  
  2.      NF_INET_PRE_ROUTING,  
  3.      NF_INET_LOCAL_IN,  
  4.      NF_INET_FORWARD,  
  5.      NF_INET_LOCAL_OUT,  
  6.      NF_INET_POST_ROUTING,  
  7.      NF_INET_NUMHOOKS  
  8.  }; 

    ”

hook 返回值类型

hook 函数对包进行判断或处理之后,需要返回一个判断结果,指导接下来要对这个包做什 么。可能的结果有:

  1. // include/uapi/linux/netfilter.h  
  2. #define NF_DROP   0  // 已丢弃这个包  
  3. #define NF_ACCEPT 1  // 接受这个包,继续下一步处理  
  4. #define NF_STOLEN 2  // 当前处理函数已经消费了这个包,后面的处理函数不用处理了  
  5. #define NF_QUEUE  3  // 应当将包放到队列  
  6. #define NF_REPEAT 4  // 当前处理函数应当被再次调用 

hook 优先级

每个 hook 点可以注册多个处理函数(handler)。在注册时必须指定这些 handlers 的优先级,这样触发 hook 时能够根据优先级依次调用处理函数。

2.2 过滤规则的组织

iptables 是配置 Netfilter 过滤功能的用户空间工具。为便于管理, 过滤规则按功能分为若干 table:

  •  raw
  •  filter
  •  nat
  •  mangle

这不是本文重点。更多信息可参考 (译) 深入理解 iptables 和 netfilter 架构

3 Netfilter conntrack 实现

连接跟踪模块用于维护可跟踪协议(trackable protocols)的连接状态。也就是说, 连接跟踪针对的是特定协议的包,而不是所有协议的包。稍后会看到它支持哪些协议。

3.1 重要结构体和函数

重要结构体:

  •    
    1. struct nf_conntrack_tuple {} 

     : 定义一个 tuple。

  •         struct nf_conntrack_man_proto {}:manipulable part 中协议相关的部分。
  •        
    1. struct nf_conntrack_man {} 

      :tuple 的 manipulable part。

  •  struct nf_conntrack_l4proto {}: 支持连接跟踪的协议需要实现的方法集(以及其他协议相关字段)。
  •  struct nf_conntrack_tuple_hash {}:哈希表(conntrack table)中的表项(entry)。
  •  struct nf_conn {}:定义一个 flow。

重要函数:

  •  hash_conntrack_raw():根据 tuple 计算出一个 32 位的哈希值(hash key)。
  •  nf_conntrack_in():连接跟踪模块的核心,包进入连接跟踪的地方。
  •  resolve_normal_ct() -> init_conntrack() -> l4proto->new():创建一个新的连接记录(conntrack entry)。
  •  nf_conntrack_confirm():确认前面通过 nf_conntrack_in() 创建的新连接。

3.2 struct nf_conntrack_tuple {}:元组(Tuple)

Tuple 是连接跟踪中最重要的概念之一。

一个 tuple 定义一个单向(unidirectional)flow。内核代码中有如下注释:

  “    //include/net/netfilter/nf_conntrack_tuple.h

  A tuple is a structure containing the information to uniquely identify a connection. ie. if two packets have the same tuple, they are in the same connection; if not, they are not.    ”

结构体定义

  1. //include/net/netfilter/nf_conntrack_tuple.h  
  2. // 为方便 NAT 的实现,内核将 tuple 结构体拆分为 "manipulatable" 和 "non-manipulatable" 两部分  
  3. // 下面结构体中的 _man 是 manipulatable 的缩写  
  4.                                                // ude/uapi/linux/netfilter.h  
  5.                                                union nf_inet_addr {  
  6.                                                    __u32            all[4];  
  7.                                                    __be32           ip;  
  8.                                                    __be32           ip6[4];  
  9.                                                    struct in_addr   in;  
  10.                                                    struct in6_addr  in6;  
  11. /* manipulable part of the tuple */         /  };  
  12. struct nf_conntrack_man {                  /  
  13.     union nf_inet_addr           u3; -->--/  
  14.     union nf_conntrack_man_proto u;  -->--\  
  15.                                            \   // include/uapi/linux/netfilter/nf_conntrack_tuple_common.h  
  16.     u_int16_t l3num; // L3 proto            \  // 协议相关的部分  
  17. };                                            union nf_conntrack_man_proto {  
  18.                                                   __be16 all;/* Add other protocols here. */ 
  19.                                                   struct { __be16 port; } tcp;  
  20.                                                   struct { __be16 port; } udp;  
  21.                                                   struct { __be16 id;   } icmp;  
  22.                                                   struct { __be16 port; } dccp;  
  23.                                                   struct { __be16 port; } sctp;  
  24.                                                   struct { __be16 key;  } gre;  
  25.                                               };  
  26. struct nf_conntrack_tuple { /* This contains the information to distinguish a connection. */  
  27.     struct nf_conntrack_man src;  // 源地址信息,manipulable part  
  28.     struct {  
  29.         union nf_inet_addr u3;  
  30.         union {  
  31.             __be16 all; /* Add other protocols here. */  
  32.             struct { __be16 port;         } tcp;  
  33.             struct { __be16 port;         } udp;  
  34.             struct { u_int8_t type, code; } icmp;  
  35.             struct { __be16 port;         } dccp;  
  36.             struct { __be16 port;         } sctp;  
  37.             struct { __be16 key;          } gre;  
  38.         } u;  
  39.         u_int8_t protonum; /* The protocol. */  
  40.         u_int8_t dir;      /* The direction (for tuplehash) */  
  41.     } dst;                       // 目的地址信息  
  42. }; 

Tuple 结构体中只有两个字段 src 和 dst,分别保存源和目的信息。src 和 dst 自身也是结构体,能保存不同类型协议的数据。以 IPv4 UDP 为例,五元组分别保存在如下字段:

  •  dst.protonum:协议类型
  •  src.u3.ip:源 IP 地址
  •  dst.u3.ip:目的 IP 地址
  •  src.u.udp.port:源端口号
  •  dst.u.udp.port:目的端口号

CT 支持的协议

从以上定义可以看到,连接跟踪模块目前只支持以下六种协议:TCP、UDP、ICMP、DCCP、SCTP、GRE。

注意其中的 ICMP 协议。大家可能会认为,连接跟踪模块依据包的三层和四层信息做 哈希,而 ICMP 是三层协议,没有四层信息,因此 ICMP 肯定不会被 CT 记录。但实际上 是会的,上面代码可以看到,ICMP 使用了其头信息中的 ICMP type和 code 字段来 定义 tuple。

3.3 struct nf_conntrack_l4proto {}:协议需要实现的方法集合

支持连接跟踪的协议都需要实现 struct nf_conntrack_l4proto {} 结构体 中定义的方法,例如 pkt_to_tuple()。

  1. // include/net/netfilter/nf_conntrack_l4proto.h  
  2. struct nf_conntrack_l4proto {  
  3.     u_int16_t l3proto; /* L3 Protocol number. */  
  4.     u_int8_t  l4proto; /* L4 Protocol number. */  
  5.     // 从包(skb)中提取 tuple  
  6.     bool (*pkt_to_tuple)(struct sk_buff *skb, ... struct nf_conntrack_tuple *tuple);  
  7.     // 对包进行判决,返回判决结果(returns verdict for packet)  
  8.     int (*packet)(struct nf_conn *ct, const struct sk_buff *skb ...);  
  9.     // 创建一个新连接。如果成功返回 TRUE;如果返回的是 TRUE,接下来会调用 packet() 方法  
  10.     bool (*new)(struct nf_conn *ct, const struct sk_buff *skb, unsigned int dataoff);  
  11.     // 判断当前数据包能否被连接跟踪。如果返回成功,接下来会调用 packet() 方法  
  12.     int (*error)(struct net *net, struct nf_conn *tmpl, struct sk_buff *skb, ...); 
  13.     ...  
  14. }; 

3.4 struct nf_conntrack_tuple_hash {}:哈希表项

conntrack 将活动连接的状态存储在一张哈希表中(key: value)。

hash_conntrack_raw() 根据 tuple 计算出一个 32 位的哈希值(key):

  1. // net/netfilter/nf_conntrack_core.c  
  2. static u32 hash_conntrack_raw(struct nf_conntrack_tuple *tuple, struct net *net)  
  3. {  
  4.     get_random_once(&nf_conntrack_hash_rnd, sizeof(nf_conntrack_hash_rnd));  
  5.     /* The direction must be ignored, so we hash everything up to the  
  6.      * destination ports (which is a multiple of 4) and treat the last three bytes manually.  */  
  7.     u32 seed = nf_conntrack_hash_rnd ^ net_hash_mix(net);  
  8.     unsigned int n = (sizeof(tuple->src) + sizeof(tuple->dst.u3)) / sizeof(u32);  
  9.     return jhash2((u32 *)tuple, n, seed ^ ((tuple->dst.u.all << 16) | tuple->dst.protonum));  

注意其中是如何利用 tuple 的不同字段来计算哈希的。

nf_conntrack_tuple_hash 是哈希表中的表项(value):

  1. // include/net/netfilter/nf_conntrack_tuple.h  
  2. // 每条连接在哈希表中都对应两项,分别对应两个方向(egress/ingress)  
  3. // Connections have two entries in the hash table: one for each way  
  4. struct nf_conntrack_tuple_hash {  
  5.     struct hlist_nulls_node   hnnode;   // 指向该哈希对应的连接 struct nf_conn,采用 list 形式是为了解决哈希冲突  
  6.     struct nf_conntrack_tuple tuple;    // N 元组,前面详细介绍过了  
  7. }; 

3.5 struct nf_conn {}:连接(connection)

Netfilter 中每个 flow 都称为一个 connection,即使是对那些非面向连接的协议(例 如 UDP)。每个 connection 用 struct nf_conn {} 表示,主要字段如下:

  1. // include/net/netfilter/nf_conntrack.h  
  2.                                                   // include/linux/skbuff.h  
  3.                                         ------>   struct nf_conntrack {  
  4.                                         |             atomic_t use;  // 连接引用计数?  
  5.                                         |         };  
  6. struct nf_conn {                        |  
  7.     struct nf_conntrack            ct_general;  
  8.     struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX]; // 哈希表项,数组是因为要记录两个方向的 flow  
  9.     unsigned long status; // 连接状态,见下文  
  10.     u32 timeout;          // 连接状态的定时器  
  11.     possible_net_t ct_net;  
  12.     struct hlist_node    nat_bysource;  
  13.                                                         // per conntrack: protocol private data  
  14.     struct nf_conn *master;                             union nf_conntrack_proto {  
  15.                                                             /* insert conntrack proto private data here */  
  16.     u_int32_t mark;    /* 对 skb 进行特殊标记 */            struct nf_ct_dccp dccp;  
  17.     u_int32_t secmark;                                      struct ip_ct_sctp sctp;  
  18.                                                             struct ip_ct_tcp tcp;  
  19.     union nf_conntrack_proto proto; ---------->----->       struct nf_ct_gre gre;  
  20. };                                                          unsigned int tmpl_padto;  
  21.                                                         }; 

连接的状态集合 enum ip_conntrack_status:

  1. // include/uapi/linux/netfilter/nf_conntrack_common.h  
  2. enum ip_conntrack_status {  
  3.     IPS_EXPECTED      = (1 << IPS_EXPECTED_BIT),  
  4.     IPS_SEEN_REPLY    = (1 << IPS_SEEN_REPLY_BIT),  
  5.     IPS_ASSURED       = (1 << IPS_ASSURED_BIT),  
  6.     IPS_CONFIRMED     = (1 << IPS_CONFIRMED_BIT),  
  7.     IPS_SRC_NAT       = (1 << IPS_SRC_NAT_BIT),  
  8.     IPS_DST_NAT       = (1 << IPS_DST_NAT_BIT),  
  9.     IPS_NAT_MASK      = (IPS_DST_NAT | IPS_SRC_NAT),  
  10.     IPS_SEQ_ADJUST    = (1 << IPS_SEQ_ADJUST_BIT),  
  11.     IPS_SRC_NAT_DONE  = (1 << IPS_SRC_NAT_DONE_BIT),  
  12.     IPS_DST_NAT_DONE  = (1 << IPS_DST_NAT_DONE_BIT),  
  13.     IPS_NAT_DONE_MASK = (IPS_DST_NAT_DONE | IPS_SRC_NAT_DONE),  
  14.     IPS_DYING         = (1 << IPS_DYING_BIT),  
  15.     IPS_FIXED_TIMEOUT = (1 << IPS_FIXED_TIMEOUT_BIT),  
  16.     IPS_TEMPLATE      = (1 << IPS_TEMPLATE_BIT),  
  17.     IPS_UNTRACKED     = (1 << IPS_UNTRACKED_BIT),  
  18.     IPS_HELPER        = (1 << IPS_HELPER_BIT),  
  19.     IPS_OFFLOAD       = (1 << IPS_OFFLOAD_BIT),  
  20.     IPS_UNCHANGEABLE_MASK = (IPS_NAT_DONE_MASK | IPS_NAT_MASK |  
  21.                  IPS_EXPECTED | IPS_CONFIRMED | IPS_DYING |  
  22.                  IPS_SEQ_ADJUST | IPS_TEMPLATE | IPS_OFFLOAD),  
  23. }; 

3.6 nf_conntrack_in():进入连接跟踪

Fig. Netfilter 中的连接跟踪点

如上图所示,Netfilter 在四个 Hook 点对包进行跟踪:

  1.  PRE_ROUTING 和 LOCAL_OUT:调用 nf_conntrack_in() 开始连接跟踪,正常情况 下会创建一条新连接记录,然后将 conntrack entry 放到 unconfirmed list。

  为什么是这两个 hook 点呢?因为它们都是新连接的第一个包最先达到的地方,

  • PRE_ROUTING 是外部主动和本机建连时包最先到达的地方
  • LOCAL_OUT 是本机主动和外部建连时包最先到达的地方

  2.  POST_ROUTING 和 LOCAL_IN:调用 nf_conntrack_confirm() 将 nf_conntrack_in() 创建的连接移到 confirmed list。

  同样要问,为什么在这两个 hook 点呢?因为如果新连接的第一个包没有被丢弃,那这 是它们离开 netfilter 之前的最后 hook 点:

  •  外部主动和本机建连的包,如果在中间处理中没有被丢弃,LOCAL_IN 是其被送到应用(例如 nginx 服务)之前的最后 hook 点
  •  本机主动和外部建连的包,如果在中间处理中没有被丢弃,POST_ROUTING 是其离开主机时的最后 hook 点

下面的代码可以看到这些 handler 是如何注册的:

  1. // net/netfilter/nf_conntrack_proto.c  
  2. /* Connection tracking may drop packets, but never alters them, so make it the first hook.  */  
  3. static const struct nf_hook_ops ipv4_conntrack_ops[] = {  
  4.  {  
  5.   .hook  = ipv4_conntrack_in,       // 调用 nf_conntrack_in() 进入连接跟踪  
  6.   .pf  = NFPROTO_IPV4,  
  7.   .hooknum = NF_INET_PRE_ROUTING,     // PRE_ROUTING hook 点  
  8.   .priority = NF_IP_PRI_CONNTRACK,  
  9.  },  
  10.  {  
  11.   .hook  = ipv4_conntrack_local,    // 调用 nf_conntrack_in() 进入连接跟踪  
  12.   .pf  = NFPROTO_IPV4,  
  13.   .hooknum = NF_INET_LOCAL_OUT,       // LOCAL_OUT hook 点  
  14.   .priority = NF_IP_PRI_CONNTRACK,  
  15.  },  
  16.  {  
  17.   .hook  = ipv4_confirm,            // 调用 nf_conntrack_confirm()  
  18.   .pf  = NFPROTO_IPV4,  
  19.   .hooknum = NF_INET_POST_ROUTING,    // POST_ROUTING hook 点  
  20.   .priority = NF_IP_PRI_CONNTRACK_CONFIRM,  
  21.  },  
  22.  {  
  23.   .hook  = ipv4_confirm,            // 调用 nf_conntrack_confirm()  
  24.   .pf  = NFPROTO_IPV4,  
  25.   .hooknum = NF_INET_LOCAL_IN,        // LOCAL_IN hook 点  
  26.   .priority = NF_IP_PRI_CONNTRACK_CONFIRM,  
  27.  },  
  28. }; 

nf_conntrack_in 函数是连接跟踪模块的核心。

  1. // net/netfilter/nf_conntrack_core.c  
  2. unsigned int  
  3. nf_conntrack_in(struct net *net, u_int8_t pf, unsigned int hooknum, struct sk_buff *skb)  
  4. {  
  5.   struct nf_conn *tmpl = nf_ct_get(skb, &ctinfo); // 获取 skb 对应的 conntrack_info 和连接记录  
  6.   if (tmpl || ctinfo == IP_CT_UNTRACKED) {        // 如果记录存在,或者是不需要跟踪的类型  
  7.       if ((tmpl && !nf_ct_is_template(tmpl)) || ctinfo == IP_CT_UNTRACKED) {  
  8.           NF_CT_STAT_INC_ATOMIC(net, ignore);     // 无需跟踪的类型,增加 ignore 计数  
  9.           return NF_ACCEPT;                       // 返回 NF_ACCEPT,继续后面的处理  
  10.       }  
  11.       skb->_nfct = 0;                             // 不属于 ignore 类型,计数器置零,准备后续处理  
  12.   }  
  13.   struct nf_conntrack_l4proto *l4proto = __nf_ct_l4proto_find(...);    // 提取协议相关的 L4 头信息  
  14.   if (l4proto->error != NULL) {                   // skb 的完整性和合法性验证  
  15.       if (l4proto->error(net, tmpl, skb, dataoff, pf, hooknum) <= 0) { 
  16.            NF_CT_STAT_INC_ATOMIC(net, error);  
  17.           NF_CT_STAT_INC_ATOMIC(net, invalid);  
  18.           goto out;  
  19.       }  
  20.   }  
  21. repeat:  
  22.   // 开始连接跟踪:提取 tuple;创建新连接记录,或者更新已有连接的状态  
  23.   resolve_normal_ct(net, tmpl, skb, ... l4proto);  
  24.   l4proto->packet(ct, skb, dataoff, ctinfo); // 进行一些协议相关的处理,例如 UDP 会更新 timeout  
  25.   if (ctinfo == IP_CT_ESTABLISHED_REPLY && !test_and_set_bit(IPS_SEEN_REPLY_BIT, &ct->status))  
  26.       nf_conntrack_event_cache(IPCT_REPLY, ct);  
  27. out:  
  28.   if (tmpl)  
  29.       nf_ct_put(tmpl); // 解除对连接记录 tmpl 的引用  

大致流程:

3.7 init_conntrack():创建新连接记录

如果连接不存在(flow 的第一个包),resolve_normal_ct() 会调用 init_conntrack ,后者进而会调用 new() 方法创建一个新的 conntrack entry。

  1. // include/net/netfilter/nf_conntrack_core.c  
  2. // Allocate a new conntrack  
  3. static noinline struct nf_conntrack_tuple_hash *  
  4. init_conntrack(struct net *net, struct nf_conn *tmpl,  
  5.         const struct nf_conntrack_tuple *tuple,  
  6.         const struct nf_conntrack_l4proto *l4proto,  
  7.         struct sk_buff *skb, unsigned int dataoff, u32 hash)  
  8. {  
  9.  struct nf_conn *ct;  
  10.  ct = __nf_conntrack_alloc(net, zone, tuple, &repl_tuple, GFP_ATOMIC, hash);  
  11.  l4proto->new(ct, skb, dataoff); // 协议相关的方法  
  12.  local_bh_disable();             // 关闭软中断  
  13.  if (net->ct.expect_count) {  
  14.   exp = nf_ct_find_expectation(net, zone, tuple);  
  15.   if (exp) {  
  16.    /* Welcome, Mr. Bond.  We've been expecting you... */  
  17.    __set_bit(IPS_EXPECTED_BIT, &ct->status);  
  18.    /* exp->master safe, refcnt bumped in nf_ct_find_expectation */  
  19.    ct->master = exp->master; 
  20.    ct->mark = exp->master->mark;  
  21.    ct->secmark = exp->master->secmark;  
  22.    NF_CT_STAT_INC(net, expect_new);  
  23.   }  
  24.  }  
  25.  /* Now it is inserted into the unconfirmed list, bump refcount */  
  26.  nf_conntrack_get(&ct->ct_general);  
  27.  nf_ct_add_to_unconfirmed_list(ct);  
  28.  local_bh_enable();              // 重新打开软中断  
  29.  if (exp) {  
  30.   if (exp->expectfn)  
  31.    exp->expectfn(ct, exp);  
  32.   nf_ct_expect_put(exp);  
  33.  }  
  34.  return &ct->tuplehash[IP_CT_DIR_ORIGINAL];  

每种协议需要实现自己的 l4proto->new() 方法,代码见:net/netfilter/nf_conntrack_proto_*.c。

如果当前包会影响后面包的状态判断,init_conntrack() 会设置 struct nf_conn 的 master 字段。面向连接的协议会用到这个特性,例如 TCP。

3.8 nf_conntrack_confirm():确认包没有被丢弃

nf_conntrack_in() 创建的新 conntrack entry 会插入到一个 未确认连接( unconfirmed connection)列表。

如果这个包之后没有被丢弃,那它在经过 POST_ROUTING 时会被 nf_conntrack_confirm() 方法处理,原理我们在分析过了 3.6 节的开头分析过了。nf_conntrack_confirm() 完成之后,状态就变为了 IPS_CONFIRMED,并且连接记录从 未确认列表移到正常的列表。

之所以要将创建一个合法的新 entry 的过程分为创建(new)和确认(confirm)两个阶段 ,是因为包在经过 nf_conntrack_in() 之后,到达 nf_conntrack_confirm() 之前 ,可能会被内核丢弃。这样会导致系统残留大量的半连接状态记录,在性能和安全性上都 是很大问题。分为两步之后,可以加快半连接状态 conntrack entry 的 GC。

  1. // include/net/netfilter/nf_conntrack_core.h  
  2. /* Confirm a connection: returns NF_DROP if packet must be dropped. */  
  3. static inline int nf_conntrack_confirm(struct sk_buff *skb)  
  4. {  
  5.  struct nf_conn *ct = (struct nf_conn *)skb_nfct(skb);  
  6.  int ret = NF_ACCEPT;  
  7.  if (ct) {  
  8.   if (!nf_ct_is_confirmed(ct))  
  9.    ret = __nf_conntrack_confirm(skb);  
  10.   if (likely(ret == NF_ACCEPT))  
  11.    nf_ct_deliver_cached_events(ct);  
  12.  }  
  13.  return ret;  

confirm 逻辑,省略了各种错误处理逻辑:

  1. // net/netfilter/nf_conntrack_core.c  
  2. /* Confirm a connection given skb; places it in hash table */  
  3. int  
  4. __nf_conntrack_confirm(struct sk_buff *skb)  
  5. {  
  6.  struct nf_conn *ct;  
  7.  ct = nf_ct_get(skb, &ctinfo);  
  8.  local_bh_disable();               // 关闭软中断  
  9.  hash = *(unsigned long *)&ct->tuplehash[IP_CT_DIR_REPLY].hnnode.pprev;  
  10.  reply_hash = hash_conntrack(net, &ct->tuplehash[IP_CT_DIR_REPLY].tuple);  
  11.  ct->timeout += nfct_time_stamp;   // 更新连接超时时间,超时后会被 GC  
  12.  atomic_inc(&ct->ct_general.use);  // 设置连接引用计数?  
  13.  ct->status |= IPS_CONFIRMED;      // 设置连接状态为 confirmed  
  14.  __nf_conntrack_hash_insert(ct, hash, reply_hash);  // 插入到连接跟踪哈希表  
  15.  local_bh_enable();                // 重新打开软中断  
  16.  nf_conntrack_event_cache(master_ct(ct) ? IPCT_RELATED : IPCT_NEW, ct);  
  17.  return NF_ACCEPT;  

可以看到,连接跟踪的处理逻辑中需要频繁关闭和打开软中断,此外还有各种锁, 这是短连高并发场景下连接跟踪性能损耗的主要原因?。

4 Netfilter NAT 实现

NAT 是与连接跟踪独立的模块。

4.1 重要数据结构和函数

重要数据结构:

支持 NAT 的协议需要实现其中的方法:

  •  struct nf_nat_l3proto {}
  •  struct nf_nat_l4proto {}

重要函数:

  •  nf_nat_inet_fn():NAT 的核心函数是,在除 NF_INET_FORWARD 之外的其他 hook 点都会被调用。

4.2 NAT 模块初始化

  1. // net/netfilter/nf_nat_core.c  
  2. static struct nf_nat_hook nat_hook = {  
  3.  .parse_nat_setup = nfnetlink_parse_nat_setup,  
  4.  .decode_session  = __nf_nat_decode_session,  
  5.  .manip_pkt  = nf_nat_manip_pkt,  
  6. }; 
  7. static int __init nf_nat_init(void)  
  8. {  
  9.  nf_nat_bysource = nf_ct_alloc_hashtable(&nf_nat_htable_size, 0);  
  10.  nf_ct_helper_expectfn_register(&follow_master_nat);  
  11.  RCU_INIT_POINTER(nf_nat_hook, &nat_hook);  
  12. }  
  13. MODULE_LICENSE("GPL");  
  14. module_init(nf_nat_init); 

4.3 struct nf_nat_l3proto {}:协议相关的 NAT 方法集

  1. // include/net/netfilter/nf_nat_l3proto.h  
  2. struct nf_nat_l3proto {  
  3.     u8    l3proto; // 例如,AF_INET  
  4.     u32     (*secure_port    )(const struct nf_conntrack_tuple *t, __be16);  
  5.     bool    (*manip_pkt      )(struct sk_buff *skb, ...);  
  6.     void    (*csum_update    )(struct sk_buff *skb, ...);  
  7.     void    (*csum_recalc    )(struct sk_buff *skb, u8 proto, ...);  
  8.     void    (*decode_session )(struct sk_buff *skb, ...);  
  9.     int     (*nlattr_to_range)(struct nlattr *tb[], struct nf_nat_range2 *range);  
  10. }; 

4.4 struct nf_nat_l4proto {}:协议相关的 NAT 方法集

  1. // include/net/netfilter/nf_nat_l4proto.h  
  2. struct nf_nat_l4proto {  
  3.     u8 l4proto; // Protocol number,例如 IPPROTO_UDP, IPPROTO_TCP  
  4.     // 根据传入的 tuple 和 NAT 类型(SNAT/DNAT)修改包的 L3/L4 头  
  5.     bool (*manip_pkt)(struct sk_buff *skb, *l3proto, *tuple, maniptype);  
  6.     // 创建一个唯一的 tuple  
  7.     // 例如对于 UDP,会根据 src_ip, dst_ip, src_port 加一个随机数生成一个 16bit 的 dst_port  
  8.     void (*unique_tuple)(*l3proto, tuple, struct nf_nat_range2 *range, maniptype, struct nf_conn *ct);  
  9.     // If the address range is exhausted the NAT modules will begin to drop packets. 
  10.      int (*nlattr_to_range)(struct nlattr *tb[], struct nf_nat_range2 *range);  
  11. }; 

各协议实现的方法,见:net/netfilter/nf_nat_proto_*.c。例如 TCP 的实现:

  1. // net/netfilter/nf_nat_proto_tcp.c  
  2. const struct nf_nat_l4proto nf_nat_l4proto_tcp = {  
  3.  .l4proto  = IPPROTO_TCP,  
  4.  .manip_pkt  = tcp_manip_pkt,  
  5.  .in_range  = nf_nat_l4proto_in_range,  
  6.  .unique_tuple  = tcp_unique_tuple,  
  7.  .nlattr_to_range = nf_nat_l4proto_nlattr_to_range,  
  8. }; 

4.5 nf_nat_inet_fn():进入 NAT

NAT 的核心函数是 nf_nat_inet_fn(),它会在以下 hook 点被调用:

  •  NF_INET_PRE_ROUTING
  •  NF_INET_POST_ROUTING
  •  NF_INET_LOCAL_OUT
  •  NF_INET_LOCAL_IN

也就是除了 NF_INET_FORWARD 之外其他 hook 点都会被调用。

在这些 hook 点的优先级:Conntrack > NAT > Packet Filtering。连接跟踪的优先 级高于 NAT 是因为 NAT 依赖连接跟踪的结果。

Fig. NAT

  1. unsigned int  
  2. nf_nat_inet_fn(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)  
  3. {  
  4.     ct = nf_ct_get(skb, &ctinfo);  
  5.     if (!ct)    // conntrack 不存在就做不了 NAT,直接返回,这也是为什么说 NAT 依赖 conntrack 的结果  
  6.         return NF_ACCEPT;  
  7.     nat = nfct_nat(ct);  
  8.     switch (ctinfo) {  
  9.     case IP_CT_RELATED:  
  10.     case IP_CT_RELATED_REPLY: /* Only ICMPs can be IP_CT_IS_REPLY.  Fallthrough */  
  11.     case IP_CT_NEW: /* Seen it before? This can happen for loopback, retrans, or local packets. */  
  12.         if (!nf_nat_initialized(ct, maniptype)) {  
  13.             struct nf_hook_entries *e = rcu_dereference(lpriv->entries); // 获取所有 NAT 规则  
  14.             if (!e)  
  15.                 goto null_bind;  
  16.             for (i = 0; i < e->num_hook_entries; i++) { // 依次执行 NAT 规则  
  17.                 if (e->hooks[i].hook(e->hooks[i].priv, skb, state) != NF_ACCEPT )  
  18.                     return ret;                         // 任何规则返回非 NF_ACCEPT,就停止当前处理  
  19.                 if (nf_nat_initialized(ct, maniptype))  
  20.                     goto do_nat; 
  21.              }  
  22. null_bind:  
  23.             nf_nat_alloc_null_binding(ct, state->hook);  
  24.         } else { // Already setup manip  
  25.             if (nf_nat_oif_changed(state->hook, ctinfo, nat, state->out))  
  26.                 goto oif_changed;  
  27.         }  
  28.         break;  
  29.     default: /* ESTABLISHED */  
  30.         if (nf_nat_oif_changed(state->hook, ctinfo, nat, state->out))  
  31.             goto oif_changed;  
  32.     }  
  33. do_nat:  
  34.     return nf_nat_packet(ct, ctinfo, state->hook, skb);  
  35. oif_changed:  
  36.     nf_ct_kill_acct(ct, ctinfo, skb);  
  37.     return NF_DROP;  

首先查询 conntrack 记录,如果不存在,就意味着无法跟踪这个连接,那就更不可能做 NAT 了,因此直接返回。

如果找到了 conntrack 记录,并且是 IP_CT_RELATED、IP_CT_RELATED_REPLY 或 IP_CT_NEW 状态,就去获取 NAT 规则。如果没有规则,直接返回 NF_ACCEPT,对包不 做任何改动;如果有规则,最后执行 nf_nat_packet,这个函数会进一步调用 manip_pkt 完成对包的修改,如果失败,包将被丢弃。

Masquerade

NAT 模块一般配置方式:Change IP1 to IP2 if matching XXX。

此次还支持一种更灵活的 NAT 配置,称为 Masquerade:Change IP1 to dev1's IP if matching XXX。与前面的区别在于,当设备(网卡)的 IP 地址发生变化时,这种方式无 需做任何修改。缺点是性能比第一种方式要差。

4.6 nf_nat_packet():执行 NAT

  1. // net/netfilter/nf_nat_core.c  
  2. /* Do packet manipulations according to nf_nat_setup_info. */  
  3. unsigned int nf_nat_packet(struct nf_conn *ct, enum ip_conntrack_info ctinfo,  
  4.       unsigned int hooknum, struct sk_buff *skb)  
  5. {  
  6.  enum nf_nat_manip_type mtype = HOOK2MANIP(hooknum);  
  7.  enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);  
  8.  unsigned int verdict = NF_ACCEPT;  
  9.  statusbit = (mtype == NF_NAT_MANIP_SRC? IPS_SRC_NAT : IPS_DST_NAT)  
  10.  if (dir == IP_CT_DIR_REPLY)     // Invert if this is reply dir  
  11.   statusbit ^= IPS_NAT_MASK;  
  12.  if (ct->status & statusbit)     // Non-atomic: these bits don't change. */  
  13.   verdict = nf_nat_manip_pkt(skb, ct, mtype, dir);  
  14.  return verdict;  
  15. }  
  16. static unsigned int nf_nat_manip_pkt(struct sk_buff *skb, struct nf_conn *ct,  
  17.          enum nf_nat_manip_type mtype, enum ip_conntrack_dir dir)  
  18. {  
  19.  struct nf_conntrack_tuple target;  
  20.  /* We are aiming to look like inverse of other direction. */  
  21.  nf_ct_invert_tuplepr(&target, &ct->tuplehash[!dir].tuple); 
  22.  l3proto = __nf_nat_l3proto_find(target.src.l3num);  
  23.  l4proto = __nf_nat_l4proto_find(target.src.l3num, target.dst.protonum);  
  24.  if (!l3proto->manip_pkt(skb, 0, l4proto, &target, mtype)) // 协议相关处理  
  25.   return NF_DROP;  
  26.  return NF_ACCEPT;  

5. 总结

连接跟踪是一个非常基础且重要的网络模块,但只有在少数场景下才会引起普通开发者的注意。

例如,L4LB 短时高并发场景下,LB 节点每秒接受大量并发短连接,可能导致 conntrack table 被打爆。此时的现象是:

  •  客户端和 L4LB 建连失败,失败可能是随机的,也可能是集中在某些时间点。
  •  客户端重试可能会成功,也可能会失败。
  •  在 L4LB 节点抓包看,客户端过来的 TCP SYNC 包 L4LB 收到了,但没有回 ACK。即,包 被静默丢弃了(silently dropped)。

此时的原因可能是 conntrack table 太小,也可能是 GC 不够及 时,甚至是 GC 有bug。

  “    原文链接:http://arthurchiao.art/blog/conntrack-design-and-implementation-zh/    ”

相关内容