--/\n union nf_conntrack_man_proto u; -->--\\\n \\ // include/uapi/linux/netfilter/nf_conntrack_tuple_common.h\n u_int16_t l3num; // L3 proto \\ // 协议相关的部分\n}; union nf_conntrack_man_proto {\n __be16 all;/* Add other protocols here. */\n \n struct { __be16 port; } tcp;\n struct { __be16 port; } udp;\n struct { __be16 id; } icmp;\n struct { __be16 port; } dccp;\n struct { __be16 port; } sctp;\n struct { __be16 key; } gre;\n };\n \nstruct nf_conntrack_tuple { /* This contains the information to distinguish a connection. */\n struct nf_conntrack_man src; // 源地址信息,manipulable part\n struct {\n union nf_inet_addr u3;\n union {\n __be16 all; /* Add other protocols here. */\n \n struct { __be16 port; } tcp;\n struct { __be16 port; } udp;\n struct { u_int8_t type, code; } icmp;\n struct { __be16 port; } dccp;\n struct { __be16 port; } sctp;\n struct { __be16 key; } gre;\n } u;\n u_int8_t protonum; /* The protocol. */\n u_int8_t dir; /* The direction (for tuplehash) */\n } dst; // 目的地址信息\n};\n```\n\n **Tuple 结构体中只有两个字段 src 和 dst** ,分别保存源和目的信息。 和 自身也是结构体,能保存不同类型协议的数据。以 IPv4 UDP 为例,五元组分别保存在如下字段:\n\n### CT 支持的协议\n\n从以上定义可以看到,连接跟踪模块 **目前只支持以下六种协议** :TCP、UDP、ICMP、DCCP、SCTP、GRE。\n\n **注意其中的 ICMP 协议** 。大家可能会认为,连接跟踪模块依据包的三层和四层信息做 哈希,而 ICMP 是三层协议,没有四层信息,因此 ICMP 肯定不会被 CT 记录。但 **实际上 是会的** ,上面代码可以看到,ICMP 使用了其头信息中的 ICMP 和 字段来 定义 tuple。\n\n### 3.3 :协议需要实现的方法集合\n\n支持连接跟踪的协议都需要实现 结构体 中定义的方法,例如 。\n\n\n\n```\n// include/net/netfilter/nf_conntrack_l4proto.h\n \nstruct nf_conntrack_l4proto {\n u_int16_t l3proto; /* L3 Protocol number. */\n u_int8_t l4proto; /* L4 Protocol number. */\n \n // 从包(skb)中提取 tuple\n bool (*pkt_to_tuple)(struct sk_buff *skb, ... struct nf_conntrack_tuple *tuple);\n \n // 对包进行判决,返回判决结果(returns verdict for packet)\n int (*packet)(struct nf_conn *ct, const struct sk_buff *skb ...);\n \n // 创建一个新连接。如果成功返回 TRUE;如果返回的是 TRUE,接下来会调用 packet() 方法\n bool (*new)(struct nf_conn *ct, const struct sk_buff *skb, unsigned int dataoff);\n \n // 判断当前数据包能否被连接跟踪。如果返回成功,接下来会调用 packet() 方法\n int (*error)(struct net *net, struct nf_conn *tmpl, struct sk_buff *skb, ...);\n \n ...\n};\n```\n\n### 3.4 :哈希表项\n\nconntrack 将活动连接的状态存储在一张哈希表中()。\n\n 根据 tuple 计算出一个 32 位的哈希值(key):\n\n\n\n```\n// net/netfilter/nf_conntrack_core.c\n \nstatic u32 hash_conntrack_raw(struct nf_conntrack_tuple *tuple, struct net *net)\n{\n get_random_once(&nf_conntrack_hash_rnd, sizeof(nf_conntrack_hash_rnd));\n \n /* The direction must be ignored, so we hash everything up to the\n * destination ports (which is a multiple of 4) and treat the last three bytes manually. */\n u32 seed = nf_conntrack_hash_rnd ^ net_hash_mix(net);\n unsigned int n = (sizeof(tuple->src) + sizeof(tuple->dst.u3)) / sizeof(u32);\n \n return jhash2((u32 *)tuple, n, seed ^ ((tuple->dst.u.all << 16) | tuple->dst.protonum));\n}\n```\n\n注意其中是如何利用 tuple 的不同字段来计算哈希的。\n\n 是哈希表中的表项(value):\n\n\n\n```\n// include/net/netfilter/nf_conntrack_tuple.h\n// 每条连接在哈希表中都对应两项,分别对应两个方向(egress/ingress)\n// Connections have two entries in the hash table: one for each way\nstruct nf_conntrack_tuple_hash {\n struct hlist_nulls_node hnnode; // 指向该哈希对应的连接 struct nf_conn,采用 list 形式是为了解决哈希冲突\n struct nf_conntrack_tuple tuple; // N 元组,前面详细介绍过了\n};\n```\n\n### 3.5 :连接(connection)\n\n **Netfilter 中每个 flow 都称为一个 connection** ,即使是对那些非面向连接的协议(例 如 UDP)。每个 connection 用 表示,主要字段如下:\n\n\n\n```\n// include/net/netfilter/nf_conntrack.h\n \n // include/linux/skbuff.h\n ------> struct nf_conntrack {\n | atomic_t use; // 连接引用计数?\n | };\nstruct nf_conn { |\n struct nf_conntrack ct_general;\n \n struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX]; // 哈希表项,数组是因为要记录两个方向的 flow\n \n unsigned long status; // 连接状态,见下文\n u32 timeout; // 连接状态的定时器\n \n possible_net_t ct_net;\n \n struct hlist_node nat_bysource;\n // per conntrack: protocol private data\n struct nf_conn *master; union nf_conntrack_proto {\n / /* insert conntrack proto private data here */\n u_int32_t mark; /* 对 skb 进行特殊标记 */ / struct nf_ct_dccp dccp;\n u_int32_t secmark; / struct ip_ct_sctp sctp;\n / struct ip_ct_tcp tcp;\n union nf_conntrack_proto proto; ---------->----/ struct nf_ct_gre gre;\n}; unsigned int tmpl_padto;\n };\n```\n\n **连接的状态集合 enum ip_conntrack_status** :\n\n\n\n```\n// include/uapi/linux/netfilter/nf_conntrack_common.h\n \nenum ip_conntrack_status {\n IPS_EXPECTED = (1 << IPS_EXPECTED_BIT),\n IPS_SEEN_REPLY = (1 << IPS_SEEN_REPLY_BIT),\n IPS_ASSURED = (1 << IPS_ASSURED_BIT),\n IPS_CONFIRMED = (1 << IPS_CONFIRMED_BIT),\n IPS_SRC_NAT = (1 << IPS_SRC_NAT_BIT),\n IPS_DST_NAT = (1 << IPS_DST_NAT_BIT),\n IPS_NAT_MASK = (IPS_DST_NAT | IPS_SRC_NAT),\n IPS_SEQ_ADJUST = (1 << IPS_SEQ_ADJUST_BIT),\n IPS_SRC_NAT_DONE = (1 << IPS_SRC_NAT_DONE_BIT),\n IPS_DST_NAT_DONE = (1 << IPS_DST_NAT_DONE_BIT),\n IPS_NAT_DONE_MASK = (IPS_DST_NAT_DONE | IPS_SRC_NAT_DONE),\n IPS_DYING = (1 << IPS_DYING_BIT),\n IPS_FIXED_TIMEOUT = (1 << IPS_FIXED_TIMEOUT_BIT),\n IPS_TEMPLATE = (1 << IPS_TEMPLATE_BIT),\n IPS_UNTRACKED = (1 << IPS_UNTRACKED_BIT),\n IPS_HELPER = (1 << IPS_HELPER_BIT),\n IPS_OFFLOAD = (1 << IPS_OFFLOAD_BIT),\n \n IPS_UNCHANGEABLE_MASK = (IPS_NAT_DONE_MASK | IPS_NAT_MASK |\n IPS_EXPECTED | IPS_CONFIRMED | IPS_DYING |\n IPS_SEQ_ADJUST | IPS_TEMPLATE | IPS_OFFLOAD),\n};\n```\n\n### 3.6 :进入连接跟踪\n\n

\n\nFig. Netfilter 中的连接跟踪点\n\n如上图所示,Netfilter 在四个 Hook 点对包进行跟踪:\n\n1. 和 : **调用 nf_conntrack_in() 开始连接跟踪** , 正常情况下会创建一条新连接记录,然后将 conntrack entry 放到 **unconfirmed list** 。\n为什么是这两个 hook 点呢?因为它们都是 **新连接的第一个包最先达到的地方** ,\n\n2. 和 : **调用 nf_conntrack_confirm() 将 nf_conntrack_in() 创建的连接移到 confirmed list** 。\n同样要问,为什么在这两个 hook 点呢?因为如果新连接的第一个包没有被丢弃,那这 是它们 **离开 netfilter 之前的最后 hook 点** :\n\n下面的代码可以看到 **这些 handler 是如何注册到 Netfilter hook 点的** :\n\n\n\n```\n// net/netfilter/nf_conntrack_proto.c\n \n/* Connection tracking may drop packets, but never alters them, so make it the first hook. */\nstatic const struct nf_hook_ops ipv4_conntrack_ops[] = {\n {\n .hook = ipv4_conntrack_in, // 调用 nf_conntrack_in() 进入连接跟踪\n .pf = NFPROTO_IPV4,\n .hooknum = NF_INET_PRE_ROUTING, // PRE_ROUTING hook 点\n .priority = NF_IP_PRI_CONNTRACK,\n },\n {\n .hook = ipv4_conntrack_local, // 调用 nf_conntrack_in() 进入连接跟踪\n .pf = NFPROTO_IPV4,\n .hooknum = NF_INET_LOCAL_OUT, // LOCAL_OUT hook 点\n .priority = NF_IP_PRI_CONNTRACK,\n },\n {\n .hook = ipv4_confirm, // 调用 nf_conntrack_confirm()\n .pf = NFPROTO_IPV4,\n .hooknum = NF_INET_POST_ROUTING, // POST_ROUTING hook 点\n .priority = NF_IP_PRI_CONNTRACK_CONFIRM,\n },\n {\n .hook = ipv4_confirm, // 调用 nf_conntrack_confirm()\n .pf = NFPROTO_IPV4,\n .hooknum = NF_INET_LOCAL_IN, // LOCAL_IN hook 点\n .priority = NF_IP_PRI_CONNTRACK_CONFIRM,\n },\n};\n```\n\n 是 **连接跟踪模块的核心** 。\n\n\n\n```\n// net/netfilter/nf_conntrack_core.c\n \nunsigned int\nnf_conntrack_in(struct net *net, u_int8_t pf, unsigned int hooknum, struct sk_buff *skb)\n{\n struct nf_conn *tmpl = nf_ct_get(skb, &ctinfo); // 获取 skb 对应的 conntrack_info 和连接记录\n if (tmpl || ctinfo == IP_CT_UNTRACKED) { // 如果记录存在,或者是不需要跟踪的类型\n if ((tmpl && !nf_ct_is_template(tmpl)) || ctinfo == IP_CT_UNTRACKED) {\n NF_CT_STAT_INC_ATOMIC(net, ignore); // 无需跟踪的类型,增加 ignore 计数\n return NF_ACCEPT; // 返回 NF_ACCEPT,继续后面的处理\n }\n skb->_nfct = 0; // 不属于 ignore 类型,计数器置零,准备后续处理\n }\n \n struct nf_conntrack_l4proto *l4proto = __nf_ct_l4proto_find(...); // 提取协议相关的 L4 头信息\n \n if (l4proto->error != NULL) { // skb 的完整性和合法性验证\n if (l4proto->error(net, tmpl, skb, dataoff, pf, hooknum) <= 0) {\n NF_CT_STAT_INC_ATOMIC(net, error);\n NF_CT_STAT_INC_ATOMIC(net, invalid);\n goto out;\n }\n }\n \nrepeat:\n // 开始连接跟踪:提取 tuple;创建新连接记录,或者更新已有连接的状态\n resolve_normal_ct(net, tmpl, skb, ... l4proto);\n \n l4proto->packet(ct, skb, dataoff, ctinfo); // 进行一些协议相关的处理,例如 UDP 会更新 timeout\n \n if (ctinfo == IP_CT_ESTABLISHED_REPLY && !test_and_set_bit(IPS_SEEN_REPLY_BIT, &ct->status))\n nf_conntrack_event_cache(IPCT_REPLY, ct);\nout:\n if (tmpl)\n nf_ct_put(tmpl); // 解除对连接记录 tmpl 的引用\n}\n```\n\n大致流程:\n\n### 3.7 :创建新连接记录\n\n如果连接不存在(flow 的第一个包), 会调用 ,后者进而会调用 方法创建一个新的 conntrack entry。\n\n\n\n```\n// include/net/netfilter/nf_conntrack_core.c\n \n// Allocate a new conntrack\nstatic noinline struct nf_conntrack_tuple_hash *\ninit_conntrack(struct net *net, struct nf_conn *tmpl,\n const struct nf_conntrack_tuple *tuple,\n const struct nf_conntrack_l4proto *l4proto,\n struct sk_buff *skb, unsigned int dataoff, u32 hash)\n{\n struct nf_conn *ct;\n \n // 从 conntrack table 中分配一个 entry,如果哈希表满了,会在内核日志中打印\n // \"nf_conntrack: table full, dropping packet\" 信息,通过 `dmesg -T` 能看到\n ct = __nf_conntrack_alloc(net, zone, tuple, &repl_tuple, GFP_ATOMIC, hash);\n \n l4proto->new(ct, skb, dataoff); // 协议相关的方法\n \n local_bh_disable(); // 关闭软中断\n if (net->ct.expect_count) {\n exp = nf_ct_find_expectation(net, zone, tuple);\n if (exp) {\n /* Welcome, Mr. Bond. We\'ve been expecting you... */\n __set_bit(IPS_EXPECTED_BIT, &ct->status);\n \n /* exp->master safe, refcnt bumped in nf_ct_find_expectation */\n ct->master = exp->master;\n ct->mark = exp->master->mark;\n ct->secmark = exp->master->secmark;\n NF_CT_STAT_INC(net, expect_new);\n }\n }\n \n /* Now it is inserted into the unconfirmed list, bump refcount */\n // 至此这个新的 conntrack entry 已经被插入 unconfirmed list\n nf_conntrack_get(&ct->ct_general);\n nf_ct_add_to_unconfirmed_list(ct);\n \n local_bh_enable(); // 重新打开软中断\n \n if (exp) {\n if (exp->expectfn)\n exp->expectfn(ct, exp);\n nf_ct_expect_put(exp);\n }\n \n return &ct->tuplehash[IP_CT_DIR_ORIGINAL];\n}\n```\n\n每种协议需要实现自己的 方法,代码见:。 例如 TCP 协议对应的 方法是:\n\n\n\n```\n// net/netfilter/nf_conntrack_proto_tcp.c\n \n/* Called when a new connection for this protocol found. */\nstatic bool tcp_new(struct nf_conn *ct, const struct sk_buff *skb, unsigned int dataoff)\n{\n if (new_state == TCP_CONNTRACK_SYN_SENT) {\n memset(&ct->proto.tcp, 0, sizeof(ct->proto.tcp));\n /* SYN packet */\n ct->proto.tcp.seen[0].td_end = segment_seq_plus_len(ntohl(th->seq), skb->len, dataoff, th);\n ct->proto.tcp.seen[0].td_maxwin = ntohs(th->window);\n ...\n}\n```\n\n如果当前包会影响后面包的状态判断, 会设置 的 字段。面向连接的协议会用到这个特性,例如 TCP。\n\n### 3.8 :确认包没有被丢弃\n\n 创建的新 conntrack entry 会插入到一个 **未确认连接** ( unconfirmed connection)列表。\n\n如果这个包之后没有被丢弃,那它在经过 时会被 方法处理,原理我们在分析过了 3.6 节的开头分析过了。 完成之后,状态就变为了 ,并且连接记录从 **未确认列表** 移到 **正常** 的列表。\n\n之所以把创建一个新 entry 的过程分为创建(new)和确认(confirm)两个阶段 ,是因为 **包在经过 nf_conntrack_in() 之后,到达 nf_conntrack_confirm() 之前 ,可能会被内核丢弃** 。这样会导致系统残留大量的半连接状态记录,在性能和安全性上都 是很大问题。分为两步之后,可以加快半连接状态 conntrack entry 的 GC。\n\n\n\n```\n// include/net/netfilter/nf_conntrack_core.h\n \n/* Confirm a connection: returns NF_DROP if packet must be dropped. */\nstatic inline int nf_conntrack_confirm(struct sk_buff *skb)\n{\n struct nf_conn *ct = (struct nf_conn *)skb_nfct(skb);\n int ret = NF_ACCEPT;\n \n if (ct) {\n if (!nf_ct_is_confirmed(ct))\n ret = __nf_conntrack_confirm(skb);\n if (likely(ret == NF_ACCEPT))\n nf_ct_deliver_cached_events(ct);\n }\n return ret;\n}\n```\n\nconfirm 逻辑,省略了各种错误处理逻辑:\n\n\n\n```\n// net/netfilter/nf_conntrack_core.c\n \n/* Confirm a connection given skb; places it in hash table */\nint\n__nf_conntrack_confirm(struct sk_buff *skb)\n{\n struct nf_conn *ct;\n ct = nf_ct_get(skb, &ctinfo);\n \n local_bh_disable(); // 关闭软中断\n \n hash = *(unsigned long *)&ct->tuplehash[IP_CT_DIR_REPLY].hnnode.pprev;\n reply_hash = hash_conntrack(net, &ct->tuplehash[IP_CT_DIR_REPLY].tuple);\n \n ct->timeout += nfct_time_stamp; // 更新连接超时时间,超时后会被 GC\n atomic_inc(&ct->ct_general.use); // 设置连接引用计数?\n ct->status |= IPS_CONFIRMED; // 设置连接状态为 confirmed\n \n __nf_conntrack_hash_insert(ct, hash, reply_hash); // 插入到连接跟踪哈希表\n \n local_bh_enable(); // 重新打开软中断\n \n nf_conntrack_event_cache(master_ct(ct) ? IPCT_RELATED : IPCT_NEW, ct);\n return NF_ACCEPT;\n}\n```\n\n可以看到, **连接跟踪的处理逻辑中需要频繁关闭和打开软中断** ,此外还有各种锁, 这是短连高并发场景下连接跟踪性能损耗的主要原因?。\n\n## 4 Netfilter NAT 实现\n\nNAT 是与连接跟踪独立的模块。\n\n### 4.1 重要数据结构和函数\n\n **重要数据结构:** \n\n支持 NAT 的协议需要实现其中的方法:\n\n **重要函数:** \n\n### 4.2 NAT 模块初始化\n\n\n\n```\n// net/netfilter/nf_nat_core.c\n \nstatic struct nf_nat_hook nat_hook = {\n .parse_nat_setup = nfnetlink_parse_nat_setup,\n .decode_session = __nf_nat_decode_session,\n .manip_pkt = nf_nat_manip_pkt,\n};\n \nstatic int __init nf_nat_init(void)\n{\n nf_nat_bysource = nf_ct_alloc_hashtable(&nf_nat_htable_size, 0);\n \n nf_ct_helper_expectfn_register(&follow_master_nat);\n \n RCU_INIT_POINTER(nf_nat_hook, &nat_hook);\n}\n \nMODULE_LICENSE(\"GPL\");\nmodule_init(nf_nat_init);\n```\n\n### 4.3 :协议相关的 NAT 方法集\n\n\n\n```\n// include/net/netfilter/nf_nat_l3proto.h\n \nstruct nf_nat_l3proto {\n u8 l3proto; // 例如,AF_INET\n \n u32 (*secure_port )(const struct nf_conntrack_tuple *t, __be16);\n bool (*manip_pkt )(struct sk_buff *skb, ...);\n void (*csum_update )(struct sk_buff *skb, ...);\n void (*csum_recalc )(struct sk_buff *skb, u8 proto, ...);\n void (*decode_session )(struct sk_buff *skb, ...);\n int (*nlattr_to_range)(struct nlattr *tb[], struct nf_nat_range2 *range);\n};\n```\n\n## 4.4 :协议相关的 NAT 方法集\n\n\n\n```\n// include/net/netfilter/nf_nat_l4proto.h\n \nstruct nf_nat_l4proto {\n u8 l4proto; // Protocol number,例如 IPPROTO_UDP, IPPROTO_TCP\n \n // 根据传入的 tuple 和 NAT 类型(SNAT/DNAT)修改包的 L3/L4 头\n bool (*manip_pkt)(struct sk_buff *skb, *l3proto, *tuple, maniptype);\n \n // 创建一个唯一的 tuple\n // 例如对于 UDP,会根据 src_ip, dst_ip, src_port 加一个随机数生成一个 16bit 的 dst_port\n void (*unique_tuple)(*l3proto, tuple, struct nf_nat_range2 *range, maniptype, struct nf_conn *ct);\n \n // If the address range is exhausted the NAT modules will begin to drop packets.\n int (*nlattr_to_range)(struct nlattr *tb[], struct nf_nat_range2 *range);\n};\n```\n\n各协议实现的方法,见:。例如 TCP 的实现:\n\n\n\n```\n// net/netfilter/nf_nat_proto_tcp.c\n \nconst struct nf_nat_l4proto nf_nat_l4proto_tcp = {\n .l4proto = IPPROTO_TCP,\n .manip_pkt = tcp_manip_pkt,\n .in_range = nf_nat_l4proto_in_range,\n .unique_tuple = tcp_unique_tuple,\n .nlattr_to_range = nf_nat_l4proto_nlattr_to_range,\n};\n```\n\n### 4.5 :进入 NAT\n\nNAT 的核心函数是 ,它会在以下 hook 点被调用:\n\n也就是除了 之外其他 hook 点都会被调用。\n\n **在这些 hook 点的优先级** : **Conntrack > NAT > Packet Filtering** 。 **连接跟踪的优先级高于 NAT** 是因为 NAT 依赖连接跟踪的结果。\n\n

\n\nFig. NAT\n\n\n\n```\nunsigned int\nnf_nat_inet_fn(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)\n{\n ct = nf_ct_get(skb, &ctinfo);\n if (!ct) // conntrack 不存在就做不了 NAT,直接返回,这也是我们为什么说 NAT 依赖 conntrack 的结果\n return NF_ACCEPT;\n \n nat = nfct_nat(ct);\n \n switch (ctinfo) {\n case IP_CT_RELATED:\n case IP_CT_RELATED_REPLY: /* Only ICMPs can be IP_CT_IS_REPLY. Fallthrough */\n case IP_CT_NEW: /* Seen it before? This can happen for loopback, retrans, or local packets. */\n if (!nf_nat_initialized(ct, maniptype)) {\n struct nf_hook_entries *e = rcu_dereference(lpriv->entries); // 获取所有 NAT 规则\n if (!e)\n goto null_bind;\n \n for (i = 0; i < e->num_hook_entries; i++) { // 依次执行 NAT 规则\n if (e->hooks[i].hook(e->hooks[i].priv, skb, state) != NF_ACCEPT )\n return ret; // 任何规则返回非 NF_ACCEPT,就停止当前处理\n \n if (nf_nat_initialized(ct, maniptype))\n goto do_nat;\n }\nnull_bind:\n nf_nat_alloc_null_binding(ct, state->hook);\n } else { // Already setup manip\n if (nf_nat_oif_changed(state->hook, ctinfo, nat, state->out))\n goto oif_changed;\n }\n break;\n default: /* ESTABLISHED */\n if (nf_nat_oif_changed(state->hook, ctinfo, nat, state->out))\n goto oif_changed;\n }\ndo_nat:\n return nf_nat_packet(ct, ctinfo, state->hook, skb);\noif_changed:\n nf_ct_kill_acct(ct, ctinfo, skb);\n return NF_DROP;\n}\n```\n\n首先查询 conntrack 记录,如果不存在,就意味着无法跟踪这个连接,那就更不可能做 NAT 了,因此直接返回。\n\n如果找到了 conntrack 记录,并且是 、 或 状态,就去获取 NAT 规则。如果没有规则,直接返回 ,对包不 做任何改动;如果有规则,最后执行 ,这个函数会进一步调用 完成对包的修改,如果失败,包将被丢弃。\n\n### Masquerade\n\nNAT 模块\n\nMasquerade 优缺点:\n\n### 4.6 :执行 NAT\n\n\n\n```\n// net/netfilter/nf_nat_core.c\n \n/* Do packet manipulations according to nf_nat_setup_info. */\nunsigned int nf_nat_packet(struct nf_conn *ct, enum ip_conntrack_info ctinfo,\n unsigned int hooknum, struct sk_buff *skb)\n{\n enum nf_nat_manip_type mtype = HOOK2MANIP(hooknum);\n enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);\n unsigned int verdict = NF_ACCEPT;\n \n statusbit = (mtype == NF_NAT_MANIP_SRC? IPS_SRC_NAT : IPS_DST_NAT)\n \n if (dir == IP_CT_DIR_REPLY) // Invert if this is reply dir\n statusbit ^= IPS_NAT_MASK;\n \n if (ct->status & statusbit) // Non-atomic: these bits don\'t change. */\n verdict = nf_nat_manip_pkt(skb, ct, mtype, dir);\n \n return verdict;\n}\n```\n\n\n\n\n\n\n```\nstatic unsigned int nf_nat_manip_pkt(struct sk_buff *skb, struct nf_conn *ct,\n enum nf_nat_manip_type mtype, enum ip_conntrack_dir dir)\n{\n struct nf_conntrack_tuple target;\n \n /* We are aiming to look like inverse of other direction. */\n nf_ct_invert_tuplepr(&target, &ct->tuplehash[!dir].tuple);\n \n l3proto = __nf_nat_l3proto_find(target.src.l3num);\n l4proto = __nf_nat_l4proto_find(target.src.l3num, target.dst.protonum);\n if (!l3proto->manip_pkt(skb, 0, l4proto, &target, mtype)) // 协议相关处理\n return NF_DROP;\n \n return NF_ACCEPT;\n}\n```\n\n## 5. 配置和监控\n\n### 5.1 查看/加载/卸载 nf_conntrack 模块\n\n\n\n```\n$ modinfo nf_conntrack\nfilename: /lib/modules/4.19.118-1.el7.centos.x86_64/kernel/net/netfilter/nf_conntrack.ko\nlicense: GPL\nalias: nf_conntrack-10\nalias: nf_conntrack-2\nalias: ip_conntrack\nsrcversion: 4BBDB5BBEF460DF5F079C59\ndepends: nf_defrag_ipv6,libcrc32c,nf_defrag_ipv4\nretpoline: Y\nintree: Y\nname: nf_conntrack\nvermagic: 4.19.118-1.el7.centos.x86_64 SMP mod_unload modversions\nparm: tstamp:Enable connection tracking flow timestamping. (bool)\nparm: acct:Enable connection tracking flow accounting. (bool)\nparm: nf_conntrack_helper:Enable automatic conntrack helper assignment (default 0) (bool)\nparm: expect_hashsize:uint\n```\n\n卸载:\n\n\n\n```\n$ rmmod nf_conntrack_netlink nf_conntrack\n```\n\n重新加载:\n\n\n\n```\n$ modprobe nf_conntrack\n \n# 加载时还可以指定额外的配置参数,例如:\n$ modprobe nf_conntrack nf_conntrack_helper=1 expect_hashsize=131072\n```\n\n### 5.2 sysctl 配置项\n\n\n\n```\n$ sysctl -a | grep nf_conntrack\nnet.netfilter.nf_conntrack_acct = 0\nnet.netfilter.nf_conntrack_buckets = 262144 # hashsize = nf_conntrack_max/nf_conntrack_buckets\nnet.netfilter.nf_conntrack_checksum = 1\nnet.netfilter.nf_conntrack_count = 2148\n... # DCCP options\nnet.netfilter.nf_conntrack_events = 1\nnet.netfilter.nf_conntrack_expect_max = 1024\n... # IPv6 options\nnet.netfilter.nf_conntrack_generic_timeout = 600\nnet.netfilter.nf_conntrack_helper = 0\nnet.netfilter.nf_conntrack_icmp_timeout = 30\nnet.netfilter.nf_conntrack_log_invalid = 0\nnet.netfilter.nf_conntrack_max = 1048576 # conntrack table size\n... # SCTP options\nnet.netfilter.nf_conntrack_tcp_be_liberal = 0\nnet.netfilter.nf_conntrack_tcp_loose = 1\nnet.netfilter.nf_conntrack_tcp_max_retrans = 3\nnet.netfilter.nf_conntrack_tcp_timeout_close = 10\nnet.netfilter.nf_conntrack_tcp_timeout_close_wait = 60\nnet.netfilter.nf_conntrack_tcp_timeout_established = 21600\nnet.netfilter.nf_conntrack_tcp_timeout_fin_wait = 120\nnet.netfilter.nf_conntrack_tcp_timeout_last_ack = 30\nnet.netfilter.nf_conntrack_tcp_timeout_max_retrans = 300\nnet.netfilter.nf_conntrack_tcp_timeout_syn_recv = 60\nnet.netfilter.nf_conntrack_tcp_timeout_syn_sent = 120\nnet.netfilter.nf_conntrack_tcp_timeout_time_wait = 120\nnet.netfilter.nf_conntrack_tcp_timeout_unacknowledged = 300\nnet.netfilter.nf_conntrack_timestamp = 0\nnet.netfilter.nf_conntrack_udp_timeout = 30\nnet.netfilter.nf_conntrack_udp_timeout_stream = 180\n```\n\n### 5.3 监控\n\n### 丢包监控\n\n 下面有一些关于 conntrack 的详细统计:\n\n\n\n```\n$ cat /proc/net/stat/nf_conntrack\nentries searched found new invalid ignore delete delete_list insert insert_failed drop early_drop icmp_error expect_new expect_create expect_delete search_restart\n000008e3 00000000 00000000 00000000 0000309d 001e72d4 00000000 00000000 00000000 00000000 00000000 00000000 000000ee 00000000 00000000 00000000 000368d7\n000008e3 00000000 00000000 00000000 00007301 002b8e8c 00000000 00000000 00000000 00000000 00000000 00000000 00000170 00000000 00000000 00000000 00035794\n000008e3 00000000 00000000 00000000 00001eea 001e6382 00000000 00000000 00000000 00000000 00000000 00000000 00000059 00000000 00000000 00000000 0003f166\n...\n```\n\n此外,还可以用 命令:\n\n\n\n```\n$ conntrack -S\ncpu=0 found=0 invalid=743150 ignore=238069 insert=0 insert_failed=0 drop=195603 early_drop=118583 error=16 search_restart=22391652\ncpu=1 found=0 invalid=2004 ignore=402790 insert=0 insert_failed=0 drop=44371 early_drop=34890 error=0 search_restart=1225447\n...\n```\n\n### conntrack table 使用量监控\n\n可以定期采集系统的 conntrack 使用量,\n\n\n\n```\n$ cat /proc/sys/net/netfilter/nf_conntrack_count\n257273\n```\n\n并与最大值比较:\n\n\n\n```\n$ cat /proc/sys/net/netfilter/nf_conntrack_max\n262144\n```\n\n## 6. 常见问题\n\n### 6.1 连接太多导致 conntrack table 被打爆\n\n### 现象\n\n业务层(应用层)现象\n\n网络层现象\n\n操作系统层现象\n\n内核日志中有如下报错:\n\n\n\n```\n$ demsg -T\n[Tue Apr 6 18:12:30 2021] nf_conntrack: nf_conntrack: table full, dropping packet\n[Tue Apr 6 18:12:30 2021] nf_conntrack: nf_conntrack: table full, dropping packet\n[Tue Apr 6 18:12:30 2021] nf_conntrack: nf_conntrack: table full, dropping packet\n...\n```\n\n另外, 或 能看到有 drop 统计。\n\n### 确认 conntrack table 被打爆\n\n遇到以上现象,基本就是 conntrack 表被打爆了。确认:\n\n\n\n```\n$ cat /proc/sys/net/netfilter/nf_conntrack_count\n257273\n \n$ cat /proc/sys/net/netfilter/nf_conntrack_max\nnet.netfilter.nf_conntrack_max = 262144\n```\n\n如果有 conntrack count 监控会看的更清楚,因为我们命令行查看时,高峰可能过了。\n\n### 解决方式\n\n优先级从高到低:\n\n1.调大 conntrack 表\n\n运行时配置(经实际测试, **不会对现有连接造成影响** ):\n\n\n\n```\n $ sysctl -w net.netfilter.nf_conntrack_max=524288\n $ sysctl -w net.netfilter.nf_conntrack_buckets=131072 # 推荐配置 hashsize=nf_conntrack_count/4\n```\n\n持久化配置:\n\n\n\n```\n$ echo \'net.netfilter.nf_conntrack_max = 524288\' >> /etc/sysctl.conf\n$ echo \'net.netfilter.nf_conntrack_buckets = 131072\' >> /etc/sysctl.conf\n```\n\n影响:连接跟踪模块 **会多用一些内存** 。具体多用多少内存,可参考 \n\n[附录](https://arthurchiao.art/blog/conntrack-design-and-implementation-zh/%23ch_8.2)。\n\n2.减小 GC 时间\n\n还可以调小 conntrack 的 GC(也叫 timeout)时间,加快过期 entry 的回收。\n\n 针对不同 TCP 状态(established、fin_wait、time_wait 等)的 entry 有不同的 GC 时间。\n例如, **默认的 established 状态的 GC 时间是 423000s(5 天)** 。设置成这么长的 **可能原因** 是:TCP/IP 协议中允许 established 状态的连接无限期不发送任何东西(但仍然活着) [8],协议的具体实现(Linux、BSD、Windows 等)会设置各自允许的最大 idle timeout。为防止 GC 掉这样长时间没流量但实际还活着的连接,就设置一个足够保守的 timeout 时间。[8] 中建议这个值不小于 2 小时 4 分钟(作为对比和参考, **Cilium 自己实现的 CT 中,默认 established GC 是 6 小时** )。 但也能看到一些厂商推荐比这个小得多的配置,例如 20 分钟。\n如果对自己的网络环境和需求非常清楚,那可以将这个时间调到一个合理的、足够小的值; 如果不是非常确定的话,还是 **建议保守一些,例如设置 6 个小时** —— 这已经比默认值 5 天小多了。\n\n$ sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established = 21600\n\n持久化:\n\n$ echo \'net.netfilter.nf_conntrack_tcp_timeout_established = 21600\' >> /etc/sysctl.conf\n\n其他几个 timeout 值(尤其是 ,默认 )也可以适当调小, 但还是那句话: **如果不确定潜在后果,千万不要激进地调小** 。\n\n## 7. 总结\n\n连接跟踪是一个非常基础且重要的网络模块,但只有在少数场景下才会引起普通开发者的注意。\n\n例如,L4LB 短时高并发场景下,LB 节点每秒接受大量并发短连接,可能导致 conntrack table 被打爆。此时的现象是:\n\n此时的原因可能是 conntrack table 太小,也可能是 GC 不够及 时,甚至是 GC 有bug。\n\n## 8. 附录\n\n### 8.1 第一个 SYN 包的重传间隔计算(Linux 4.19.118 实现)\n\n调用路径:。\n\n\n\n```\n// net/ipv4/tcp_output.c\n/* Do all connect socket setups that can be done AF independent. */\nstatic void tcp_connect_init(struct sock *sk)\n{\n inet_csk(sk)->icsk_rto = tcp_timeout_init(sk);\n ...\n}\n \n// include/net/tcp.h\nstatic inline u32 tcp_timeout_init(struct sock *sk)\n{\n // 获取 SYN-RTO:如果这个 socket 上没有 BPF 程序,或者有 BPF 程序但执行失败,都返回 -1\n // 除非用户自己编写 BPF 程序并 attach 到 cgroup/socket,否则这里都是没有 BPF 的,因此这里返回 -1\n timeout = tcp_call_bpf(sk, BPF_SOCK_OPS_TIMEOUT_INIT, 0, NULL);\n \n if (timeout <= 0) // timeout == -1,接下来使用默认值\n timeout = TCP_TIMEOUT_INIT; // 宏定义,等于系统的 HZ 数,也就是 1 秒,见下面\n return timeout;\n}\n \n// include/net/tcp.h\n#define TCP_RTO_MAX ((unsigned)(120*HZ))\n#define TCP_RTO_MIN ((unsigned)(HZ/5))\n#define TCP_TIMEOUT_MIN (2U) /* Min timeout for TCP timers in jiffies */\n#define TCP_TIMEOUT_INIT ((unsigned)(1*HZ)) /* RFC6298 2.1 initial RTO value */\n```\n\n### 8.2 根据 nf_conntrack_max 计算 conntrack 模块所需的内存\n\n\n\n```\n$ cat /proc/slabinfo | head -n2; cat /proc/slabinfo | grep conntrack\nslabinfo - version: 2.1\n# name
: tunables : slabdata \nnf_conntrack 512824 599505 320 51 4 : tunables 0 0 0 : slabdata 11755 11755 0\n```\n\n其中的 **objsize 表示这个内核对象** (这里对应的是 )的大小, 单位是 **字节** ,所以以上输出表明 **每个 conntrack entry 占用 320 字节的内存空间** 。\n\n如果忽略内存碎片(内存分配单位为 slab),那 **不同 size 的 conntrack table 占用的内存** 如下:\n\n
\n\n[https://zhuanlan.zhihu.com/p/703880283](https://zhuanlan.zhihu.com/p/703880283)
\n -->