linux内核协议栈 netfilter 之连接跟踪子系统核心数据结构 struct nf_conn

这篇博客深入探讨Linux内核的netfilter框架中的连接跟踪子系统,主要关注struct nf_conn数据结构及其关键组件。内容包括连接状态(status)、初始化(init_conntrack())、连接跟踪标识(nf_conntrack_tuple)及其解析、查找过程,以及struct sk_buff中的连接追踪信息。博客还讨论了tuple的不对称设计以及在数据包处理中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

1 连接跟踪信息块 struct nf_conn

1.1 "连接"状态 status

1.2 连接跟踪信息块的创建 init_conntrack()

1.3 连接跟踪标识 struct nf_conntrack_tuple

1.3.1 tuple 解析 nf_ct_get_tuple(const struct sk_buff *skb,...)

1.3.2 tuple的组织 struct nf_conntrack_tuple_hash

1.3.3 tuple的查找 nf_conntrack_find_get()

2 数据包 struct sk_buff 中的连接追踪系统 struct nf_conntrack *nfct


1 连接跟踪信息块 struct nf_conn

连接跟踪子系统中的每条“连接”就是用struct nf_conn表示,其定义如下:

struct nf_conn {
	/* Usage count in here is 1 for hash table/destruct timer, 1 per skb,
           plus 1 for any connection(s) we are `master' for */
	//连接信息块的引用计数,如注释所列,这些情况会增加引用计数
	struct nf_conntrack ct_general;

	spinlock_t lock;

	/* XXX should I move this to the tail ? - Y.K */
	/* These are my tuples; original and reply */
	//连接的标识tuple,每条连接有两个tuple,分别代表初始方向和reply方向.
	//这里是tuple_hash而不是tuple的原因见下面
	struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];

	/* Have we seen traffic both ways yet? (bitset) */
	//标识一条连接的状态,按bit使用,可设置的bit如下
	unsigned long status;

	/* If we were expected by an expectation, this will be it */
	//如果该连接跟踪信息块是某个连接的期望连接,那么master指向源连接,否则为NULL
	struct nf_conn *master;

	/* Timer function; drops refcnt when it goes off. */
	//如果一个连接上面长时间任何方向上都没有数据传输,那么应该清除该连接的连接跟踪
	//信息块,防止其继续占有系统资源。这里为每个连接维持一个定时器,定时器超时则清除
	//该连接,每当有数据传输时,会重置该定时器
	struct timer_list timeout;

#if defined(CONFIG_NF_CONNTRACK_MARK)
	u_int32_t mark;
#endif

#ifdef CONFIG_NF_CONNTRACK_SECMARK
	u_int32_t secmark;
#endif

	/* Extensions */
	//连接跟踪信息块的扩展,见"连接跟踪子系统之extend"
	struct nf_ct_ext *ext;
#ifdef CONFIG_NET_NS
	struct net *ct_net;
#endif

	/* Storage reserved for other modules, must be the last member */
	union nf_conntrack_proto proto;
};

#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
struct nf_conntrack {
	atomic_t use;
};
#endif

1.1 "连接"状态 status

连接跟踪信息块中的staus字段标识了一条连接的状态,status可取的值及其含义如下:

enum ip_conntrack_status {
	/* 如果是期望连接,设置该bit */
	IPS_EXPECTED_BIT = 0,
	IPS_EXPECTED = (1 << IPS_EXPECTED_BIT),
	/* 该连接的两个方向上都已经检测到了数据包,设置该bit */
	IPS_SEEN_REPLY_BIT = 1,
	IPS_SEEN_REPLY = (1 << IPS_SEEN_REPLY_BIT),
	/* Conntrack should never be early-expired. */
	IPS_ASSURED_BIT = 2,
	IPS_ASSURED = (1 << IPS_ASSURED_BIT),
	/* 该连接被确认后,设置该bit */
	IPS_CONFIRMED_BIT = 3,
	IPS_CONFIRMED = (1 << IPS_CONFIRMED_BIT),
	/* Connection needs src nat in orig dir.  This bit never changed. */
	IPS_SRC_NAT_BIT = 4,
	IPS_SRC_NAT = (1 << IPS_SRC_NAT_BIT),
	/* Connection needs dst nat in orig dir.  This bit never changed. */
	IPS_DST_NAT_BIT = 5,
	IPS_DST_NAT = (1 << IPS_DST_NAT_BIT),
	/* Both together. */
	IPS_NAT_MASK = (IPS_DST_NAT | IPS_SRC_NAT),
	/* Connection needs TCP sequence adjusted. */
	IPS_SEQ_ADJUST_BIT = 6,
	IPS_SEQ_ADJUST = (1 << IPS_SEQ_ADJUST_BIT),
	/* NAT initialization bits. */
	IPS_SRC_NAT_DONE_BIT = 7,
	IPS_SRC_NAT_DONE = (1 << IPS_SRC_NAT_DONE_BIT),
	IPS_DST_NAT_DONE_BIT = 8,
	IPS_DST_NAT_DONE = (1 << IPS_DST_NAT_DONE_BIT),
	/* Both together */
	IPS_NAT_DONE_MASK = (IPS_DST_NAT_DONE | IPS_SRC_NAT_DONE),
	//连接跟踪信息块被从全局链表中移除后会设置该标记,表示该连接
	//跟踪信息块即将被销毁,不应该继续被访问
	IPS_DYING_BIT = 9,
	IPS_DYING = (1 << IPS_DYING_BIT),
	//设定该标记后,该连接的超时时间将无法被更新
	IPS_FIXED_TIMEOUT_BIT = 10,
	IPS_FIXED_TIMEOUT = (1 << IPS_FIXED_TIMEOUT_BIT),
};

1.2 连接跟踪信息块的创建 init_conntrack()

当收到一个数据包,发现其不属于任何一个之前跟踪过的连接,那么这属于一个新的连接,这时会调用 init_conntrack() 新建一个连接跟踪该数据包。具体流程如下:

  1. 调用函数__nf_conntrack_alloc创建一个数据连接跟踪项
  2. 根据刚创建的连接跟踪项的原始方向的nf_conntrack_tuple变量,查找期望连接链表,看能否找到符合条件的期望连接跟踪。若查找到:则更新新创建连接跟踪项的master指针,以及调用exp->expectfn;若没有查找到:则遍历helpers链表,找到符合条件的nf_conntrack_helper变量后,则更新连接跟踪项的helper指针。
  3. 将新创建的连接跟踪项插入到net->ct.unconfirmed 链表【算是全局】中。对于连接跟踪项,是使用定时器超时机制来实现异步垃圾回收的。所以也要对连接跟踪项的垃圾回收机制进行分析下。
/* Allocate a new conntrack: we return -ENOMEM if classification
   failed due to stress.  Otherwise it really is unclassifiable. */
static struct nf_conntrack_tuple_hash *
init_conntrack(struct net *net,
	       const struct nf_conntrack_tuple *tuple,
	       struct nf_conntrack_l3proto *l3proto,
	       struct nf_conntrack_l4proto *l4proto,
	       struct sk_buff *skb,
	       unsigned int dataoff)
{
	struct nf_conn *ct;
	struct nf_conn_help *help;
	struct nf_conntrack_tuple repl_tuple;
	struct nf_conntrack_expect *exp;
	
	//根据初始方向的tuple得到reply方向的tuple,保存到repl_tuple中
	if (!nf_ct_invert_tuple(&repl_tuple, tuple, l3proto, l4proto)) {
		pr_debug("Can't invert tuple.\n");
		return NULL;
	}
	//分配一个连接跟踪信息块,并且将初始方向和reply方向的tuple设置到其tuplehash中
	ct = nf_conntrack_alloc(net, tuple, &repl_tuple, GFP_ATOMIC);
	if (ct == NULL || IS_ERR(ct)) {
		pr_debug("Can't allocate conntrack.\n");
		return (struct nf_conntrack_tuple_hash *)ct;
	}
	//一个新的连接产生了,调用L4协议的new()回调,L4协议必须提供该回调
	if (!l4proto->new(ct, skb, dataoff)) {
		nf_conntrack_free(ct);
		pr_debug("init conntrack: can't track with proto module\n");
		return NULL;
	}

	nf_ct_acct_ext_add(ct, GFP_ATOMIC);

	spin_lock_bh(&nf_conntrack_lock);
	//根据skb的tuple搜索期望连接链表,检查该新的连接是否是某个已有连接的期望连接
	exp = nf_ct_find_expectation(net, tuple);
	if (exp) {
		pr_debug("conntrack: expectation arrives ct=%p exp=%p\n",
			 ct, exp);
		/* Welcome, Mr. Bond.  We've been expecting you... */
		//是某个连接的期望连接,那么给这个新的连接设置IPS_EXPECTED_BIT标记
		__set_bit(IPS_EXPECTED_BIT, &ct->status);
		...
	} else {
		//不是期望连接,查找系统中是否有helper模块想处理这种连接
		struct nf_conntrack_helper *helper;

		helper = __nf_ct_helper_find(&repl_tuple);
		if (helper) {
			help = nf_ct_helper_ext_add(ct, GFP_ATOMIC);
			if (help)
				rcu_assign_pointer(help->helper, helper);
		}
		NF_CT_STAT_INC(net, new);
	}

	/* Overload tuple linked list to put us in unconfirmed list. */
	//无论是否是期望连接,此时一个新的连接跟踪信息块产生了,先将其加入到net->ct.unconfirmed
	//链表,skb离开连接跟踪子系统时,如果该skb没有被防火墙丢掉,那么skb会被连接跟踪
	//子系统的confirmed钩子捕获,那时会对该连接跟踪信息块确认并将其加入到全局哈希表中
	hlist_add_head(&ct->tuplehash[IP_CT_DIR_ORIGINAL].hnode,
		       &net->ct.unconfirmed);

	spin_unlock_bh(&nf_conntrack_lock);

	if (exp) {
		if (exp->expectfn)
			exp->expectfn(ct, exp);
		nf_ct_expect_put(exp);
	}

	return &ct->tuplehash[IP_CT_DIR_ORIGINAL];
}

在分配连接跟踪信息块后,只是将其加入到了一个 net->ct.unconfirmed 链表,只有被确认后,该连接跟踪信息块才会被加入到全局的哈希表中,表示该连接被正式跟踪。关于连接跟踪子系统的helper和期望连接某块参见xxxx

1.3 连接跟踪标识 struct nf_conntrack_tuple

每个连接都需要有个标识,就像TCP的五元组一样,连接跟踪子系统中,每条连接是由两个struct nf_conntrack_tuple对象唯一标识的,它们分别代表初始方向(该连接上第一个数据包的传输方向)和Reply方向。后面称该结构为tuple。

//L4协议地址
union nf_conntrack_man_proto
{
	/* Add other protocols here. */
	__be16 all;
	//对于tcp和udp,就是端口号
	struct {
		__be16 port;
	} tcp;
	struct {
		__be16 port;
	} udp;
	//对于icmp是ID
	struct {
		__be16 id;
	} icmp;
	struct {
		__be16 port;
	} sctp;
	struct {
		__be16 key;	/* GRE key is 32bit, PPtP only uses 16bit */
	} gre;
};
//L3地址,如IPv4和IPv6地址
union nf_inet_addr {
	__u32		all[4];
	__be32		ip;
	__be32		ip6[4];
	struct in_addr	in;
	struct in6_addr	in6;
};

//tuple的可变部分,即src
struct nf_conntrack_man
{
	//u3为L3协议地址,u为L4协议地址
	union nf_inet_addr u3;
	union nf_conntrack_man_proto u;
	//L3协议编号,通常就是协议族,如AF_INET
	u_int16_t l3num;
};
//从定义上看,tuple就是要用src和dst来一起标识一个连接,只是结构嵌套较多而已
struct nf_conntrack_tuple
{
	//src部分是可变的,netfilter的其它模块可能会修改这部分内容,比如NAT
	struct nf_conntrack_man src;
	//dst部分,这部分地址一旦确定就不会再改变
	struct {
		//L3协议地址
		union nf_inet_addr u3;
		//L4协议地址,不同的L4协议使用不同的字段
		union {
			__be16 all;
			struct {
				__be16 port;
			} tcp;
			struct {
				__be16 port;
			} udp;
			struct {
				u_int8_t type, code;
			} icmp;
			struct {
				__be16 port;
			} sctp;
			struct {
				__be16 key;
			} gre;
		} u;
		//L4协议号,根据协议号可以知道上面的u到底应该按照哪一个协议处理
		u_int8_t protonum;
		//tuple表示的方向
		u_int8_t dir;
	} dst;
};

仔细观察上面的src和dst会发现,这两个地址并不对称,目前还不理解为何这么设计,src和dst有如下不同:

  1. 在src中有L3协议编号,dst中存储的却是L4协议编号;
  2. 在dst中有dir成员,但是在src中却没有。

1.3.1 tuple 解析 nf_ct_get_tuple(const struct sk_buff *skb,...)

tuple是从skb中解析出来的,解析函数为:

int nf_ct_get_tuple(const struct sk_buff *skb, unsigned int nhoff, unsigned int dataoff,
		u_int16_t l3num, u_int8_t protonum, struct nf_conntrack_tuple *tuple
		const struct nf_conntrack_l3proto *l3proto, const struct nf_conntrack_l4proto *l4proto)
{
	//tuple内容清零
	NF_CT_TUPLE_U_BLANK(tuple);
	tuple->src.l3num = l3num;
	//如果L3协议就能初始化tuple,转换失败,结束处理过程
    //对于ipv4调用ipv4_pkt_to_tuple() 设置tuple结构的源、目的ip地址。
	if (l3proto->pkt_to_tuple(skb, nhoff, tuple) == 0)
		return 0;
	tuple->dst.protonum = protonum;
	//默认设置为初始方向
	tuple->dst.dir = IP_CT_DIR_ORIGINAL;
	//调用L4协议继续初始化tuple内容
    //调用四层函数l4_pkt_to_tuple,设置tuple结构中的四层相关的源、目的值。
    //对于tcp协议来说,则是调用函数tcp_pkt_to_tuple
    //对于udp协议来说,则是调用函数udp_pkt_to_tuple
	return l4proto->pkt_to_tuple(skb, dataoff, tuple);
}
EXPORT_SYMBOL_GPL(nf_ct_get_tuple);

1.3.2 tuple的组织 struct nf_conntrack_tuple_hash

连接跟踪子系统将系统中所有的tuple组织到一个hash表中,哈希表中保存的元素为struct nf_conntrack_tuple_hash,后续我们称该元素为tuple_hash。

//全局的hash表
struct hlist_head *nf_conntrack_hash __read_mostly;
EXPORT_SYMBOL_GPL(nf_conntrack_hash);
//nf_conntrack_hash哈希表中保存的元素
struct nf_conntrack_tuple_hash
{
	struct hlist_node hnode;
	struct nf_conntrack_tuple tuple;
};

从init_conntrack()函数中可以看得出来,tuple_hash实际上就是struct nf_conn结构中的tuplehash[]成员,所以说哈希表nf_conntrack_hash中保存的虽然是tuple_hash,但同时也保存的是连接跟踪信息块(通过container_of宏很方便的实现tuple_hash到连接跟踪信息块的转换)。

1.3.3 tuple的查找 nf_conntrack_find_get()

在数据包进入skb子系统后,将skb转换成tuple后,首先要看看该tuple是否已经在全局的tuple_hash哈希表中,如果已经在了,说明该skb属于一个已有连接,否则就属于一个新的连接,根据tuple查询tuple_hash哈希表可以通过完成。

/* Find a connection corresponding to a tuple. */
struct nf_conntrack_tuple_hash *
nf_conntrack_find_get(const struct nf_conntrack_tuple *tuple)
{
	struct nf_conntrack_tuple_hash *h;
	struct nf_conn *ct;

	rcu_read_lock();
	//真正的查找函数
	h = __nf_conntrack_find(tuple);
	if (h) {
		//找到后还需要看一下该连接跟踪信息块的引用计数是否已经为0了,如果是则返回NULL,
		//因为这种情况说明该连接跟踪信息块即将被删除了,不应该被继续引用
		ct = nf_ct_tuplehash_to_ctrack(h);
		if (unlikely(!atomic_inc_not_zero(&ct->ct_general.use)))
			h = NULL;
	}
	rcu_read_unlock();
	return h;
}

struct nf_conntrack_tuple_hash *
__nf_conntrack_find(const struct nf_conntrack_tuple *tuple)
{
	struct nf_conntrack_tuple_hash *h;
	struct hlist_node *n;
	//根据tuple计算hash值
	unsigned int hash = hash_conntrack(tuple);

	/* Disable BHs the entire time since we normally need to disable them
	 * at least once for the stats anyway.
	 */
	local_bh_disable();
	//遍历hash值索引的冲突链,找到一致的tuple,nf_ct_tuple_equla()不再展开
	hlist_for_each_entry_rcu(h, n, &nf_conntrack_hash[hash], hnode) {
		if (nf_ct_tuple_equal(tuple, &h->tuple)) {
			NF_CT_STAT_INC(found);
			local_bh_enable();
			return h;
		}
		NF_CT_STAT_INC(searched);
	}
	local_bh_enable();
	return NULL;
}
EXPORT_SYMBOL_GPL(__nf_conntrack_find);

2 数据包 struct sk_buff 中的连接追踪系统 struct nf_conntrack *nfct

数据包经过连接跟踪子系统处理后,会将相关的连接跟踪信息记录到skb中,相关字段如下:

struct sk_buff {
...
	__u8		local_df:1,
				cloned:1,
				ip_summed:2,
				nohdr:1,
				//指示skb所属“连接”的状态
				nfctinfo:3;
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
	//指向连接跟踪信息块的第一个成员ct_general,即其引用计数,
	//通过该字段可以获取到连接跟踪信息块
	struct nf_conntrack	*nfct;
	struct sk_buff		*nfct_reasm;
#endif
...
};

上面的字段nfctinfo是一个枚举值,代表了连接跟踪子系统处理了该数据包后,所处状态,可取的值有(注释已经解释的非常清楚了):

/* Connection state tracking for netfilter.  This is separated from,
   but required by, the NAT layer; it can also be used by an iptables
   extension. */
enum ip_conntrack_info
{
	//属于初始方向,Orign和Reply方向都有收到数据包
	IP_CT_ESTABLISHED=0,
	//期望连接的第一个包,即期望连接的New
	IP_CT_RELATED=1,
	//初始方向的第一个包
	IP_CT_NEW=2,
	//这是一个分界值,并没有skb会单但属于这个状态
	IP_CT_IS_REPLY=3,
	//IP_CT_IS_REPLY+IP_CT_ESTABLISHED:连接态,Reply方向
	//IP_CT_IS_REPLY+IP_CT_REPLATED:期望连接(还不知道什么情况会处于这个状态)
	/* Number of distinct IP_CT types (no NEW in reply dirn). */
	IP_CT_NUMBER = IP_CT_IS_REPLY * 2 - 1 //5
};

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值