连接跟踪子系统之核心实现

本文深入探讨了Netfilter连接跟踪子系统的核心实现,包括nf_conntrack_in()入口函数,其中resolve_normal_ct()用于检查数据包是否属于现有连接或创建新连接;nf_conntrack_confirm()出口函数,确认连接的有效性;以及连接超时机制,详细阐述了定时器的初始化、超时回调和更新过程。

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

如上一篇笔记连接跟踪子系统之AF_INET协议族钩子函数所介绍,协议族的钩子函数几乎都是直接调用框架的接口实现的处理,这篇笔记就来看看连接跟踪子系统框架部分的核心代码实现,涉及文件主要有:

代码路径说明
net/netfilter/nf_conntrack_core.c连接跟踪子系统框架代码的实现文件

1. 连接跟踪入口: nf_conntrack_in()

数据包skb就是通过该函数进入连接跟踪子系统的,对于发送报文,从LOCAL_OUT点进入,对于接收报文,从PRE_ROUTING点进入。

unsigned int nf_conntrack_in(int pf, unsigned int hooknum, struct sk_buff *skb)
{
	struct nf_conn *ct;
	enum ip_conntrack_info ctinfo;
	struct nf_conntrack_l3proto *l3proto;
	struct nf_conntrack_l4proto *l4proto;
	unsigned int dataoff;
	u_int8_t protonum;
	int set_reply = 0;
	int ret;

	//之前已经跟踪过了,直接返回
	if (skb->nfct) {
		NF_CT_STAT_INC_ATOMIC(ignore);
		return NF_ACCEPT;
	}
	//从全局数组nf_conntrack_l3protos[]中找到该协议族注册的L3协议
	l3proto = __nf_ct_l3proto_find((u_int16_t)pf);
	//调用L3协议的get_l4proto()回调解析skb,获取L4协议
	//编号protonum和L4协议报文首部距skb起始位置的偏移量dataoff
	ret = l3proto->get_l4proto(skb, skb_network_offset(skb), &dataoff, &protonum);
	if (ret <= 0) {
		pr_debug("not prepared to track yet or error occured\n");
		NF_CT_STAT_INC_ATOMIC(error);
		NF_CT_STAT_INC_ATOMIC(invalid);
		return -ret;
	}
	//查找全局数组nf_conntrack_protos[],寻找L4协议
	l4proto = __nf_ct_l4proto_find((u_int16_t)pf, protonum);
	//如果L4协议提供了校验回调error(),对skb进行校验
	if (l4proto->error != NULL &&
	    (ret = l4proto->error(skb, dataoff, &ctinfo, pf, hooknum)) <= 0) {
		NF_CT_STAT_INC_ATOMIC(error);
		NF_CT_STAT_INC_ATOMIC(invalid);
		return -ret;
	}
	//核心函数,获取该skb所属的连接跟踪信息块nf_conn,见下文。
	//如果数据包skb属于reply方向,set_reply会被设置为1,否则为0
	ct = resolve_normal_ct(skb, dataoff, pf, protonum, l3proto, l4proto,
			       &set_reply, &ctinfo);
	if (!ct) {
		/* Not valid part of a connection */
		NF_CT_STAT_INC_ATOMIC(invalid);
		return NF_ACCEPT;
	}
	if (IS_ERR(ct)) {
		//连接跟踪子系统本意不会过滤数据包,但是在一些异常情况也会丢包
		NF_CT_STAT_INC_ATOMIC(drop);
		return NF_DROP;
	}
	NF_CT_ASSERT(skb->nfct);
	//调用L4协议的packet回调函数决定连接跟踪子系统给Netfilter框架的返回值
	ret = l4proto->packet(ct, skb, dataoff, ctinfo, pf, hooknum);
	if (ret < 0) {
		/* Invalid: inverse of the return code tells
		 * the netfilter core what to do */
		pr_debug("nf_conntrack_in: Can't track with proto module\n");
		nf_conntrack_put(skb->nfct);
		skb->nfct = NULL;
		NF_CT_STAT_INC_ATOMIC(invalid);
		return -ret;
	}
	//如果是reply方向的第一个数据包,更新事件缓冲,并向外发送通知(应用场景?)
	if (set_reply && !test_and_set_bit(IPS_SEEN_REPLY_BIT, &ct->status))
		nf_conntrack_event_cache(IPCT_STATUS, skb);
	return ret;
}
EXPORT_SYMBOL_GPL(nf_conntrack_in);

梳理下关键步骤:

  1. 根据skb找到能够处理该skb的L3协议和L4协议;
  2. 如果有,调用L4协议的error()回调进行报文校验,校验通过继续,否则结束;
  3. 调用resolve_normal_ct()查询该skb是否属于某个已有连接,没有则创建一个;
  4. 调用L4协议的packet()回调,该回调的返回值将作为该Netfilter钩子的返回值,一般应该都是NF_ACCEPT。

1.1 连接跟踪信息块的获取:resolve_normal_ct()

对于连接跟踪子系统来讲,每个数据包都应该归属某一条“连接”。如上,数据包通过一些检查后,就会调用该函数检查数据包是否属于已有的连接,如果该数据包不属于任何一个连接,那么它是一个“新连接”的数据包,这时该函数就会创建一个“连接”对该数据包进行跟踪,具体实现细节如下:

@dataoff: L4报文的偏移量
@set_reply: 输出参数,
/* On success, returns conntrack ptr, sets skb->nfct and ctinfo */
static inline struct nf_conn* resolve_normal_ct(struct sk_buff *skb,
		unsigned int dataoff, u_int16_t l3num, u_int8_t protonum,
		struct nf_conntrack_l3proto *l3proto, struct nf_conntrack_l4proto *l4proto,
		int *set_reply, enum ip_conntrack_info *ctinfo)
{
	struct nf_conntrack_tuple tuple;
	struct nf_conntrack_tuple_hash *h;
	struct nf_conn *ct;
	//将skb转换为tuple,具体实现见笔记"Netfilter之连接跟踪子系统核心数据结构"
	if (!nf_ct_get_tuple(skb, skb_network_offset(skb),
			dataoff, l3num, protonum, &tuple, l3proto, l4proto)) {
		pr_debug("resolve_normal_ct: Can't get tuple\n");
		return NULL;
	}
	//从全局的哈希表nf_conntrack_hash中寻找是否有一致的tuple_hash
	h = nf_conntrack_find_get(&tuple);
	if (!h) {
		//如果没找到,说明这是一个新的连接,新创建一个连接,顺带也就创建了tuple_hash
		h = init_conntrack(&tuple, l3proto, l4proto, skb, dataoff);
		if (!h)
			return NULL;
		if (IS_ERR(h))
			return (void *)h;
	}
	//tuple_hash就在连接跟踪信息块中,这里由tuple_hash获取连接跟踪信息块。
	//注意:连接跟踪信息块的分配是在init_conntrack()中完成的,这里只是转换
	ct = nf_ct_tuplehash_to_ctrack(h);
	//特别注意下面ctinfo的设定,它最后会被设置到skb->nfctinfo中,
	//表示当处理完该skb后,skb所属连接的状态
	if (NF_CT_DIRECTION(h) == IP_CT_DIR_REPLY) {
		//收到的是reply方向的数据包,记录状态为ESTABLISHED
		*ctinfo = IP_CT_ESTABLISHED + IP_CT_IS_REPLY;
		/* Please set reply bit if this packet OK */
		*set_reply = 1;
	} else {
		//初始方向的数据包,但是在该连接的reply方向上也已经收到过了数据包
		//那么skb依然是属于ESTABLISHED连接的
		if (test_bit(IPS_SEEN_REPLY_BIT, &ct->status)) {
			pr_debug("nf_conntrack_in: normal packet for %p\n", ct);
			*ctinfo = IP_CT_ESTABLISHED;
		} else if (test_bit(IPS_EXPECTED_BIT, &ct->status)) {
			pr_debug("nf_conntrack_in: related packet for %p\n", ct);
			*ctinfo = IP_CT_RELATED;
		} else {
			//初始方向的第一个数据包
			pr_debug("nf_conntrack_in: new packet for %p\n", ct);
			*ctinfo = IP_CT_NEW;
		}
		*set_reply = 0;
	}
	//设置skb中的引用计数和连接跟踪状态
	skb->nfct = &ct->ct_general;
	skb->nfctinfo = *ctinfo;
	//返回连接跟踪信息块指针
	return ct;
}

2. 连接跟踪子系统出口:nf_conntrack_confirm()

/* Confirm a connection: returns NF_DROP if packet must be dropped. */
static inline int nf_conntrack_confirm(struct sk_buff *skb)
{
	//skb所属连接的连接跟踪信息块
	struct nf_conn *ct = (struct nf_conn *)skb->nfct;
	int ret = NF_ACCEPT;

	if (ct) {
		if (!nf_ct_is_confirmed(ct) && !nf_ct_is_dying(ct))
			ret = __nf_conntrack_confirm(skb);
		//向外部模块发送缓存的事件
		nf_ct_deliver_cached_events(ct);
	}
	return ret;
}

并不是每一个skb都需要执行确认,一条连接只有初始方向上的第一个skb需要被确认,并且连接必须是有效的才需要确认。

//检查该连接是否已经被确认过了
static inline int nf_ct_is_confirmed(struct nf_conn *ct)
{
	return test_bit(IPS_CONFIRMED_BIT, &ct->status);
}
//检查连接是否依然有效
static inline int nf_ct_is_dying(struct nf_conn *ct)
{
	return test_bit(IPS_DYING_BIT, &ct->status);
}

2.1 __nf_conntrack_confirm()

/* Confirm a connection given skb; places it in hash table */
int __nf_conntrack_confirm(struct sk_buff *skb)
{
	unsigned int hash, repl_hash;
	struct nf_conntrack_tuple_hash *h;
	struct nf_conn *ct;
	struct nf_conn_help *help;
	struct hlist_node *n;
	enum ip_conntrack_info ctinfo;

	ct = nf_ct_get(skb, &ctinfo);
	/* ipt_REJECT uses nf_conntrack_attach to attach related
	   ICMP/TCP RST packets in other direction.  Actual packet
	   which created connection will be IP_CT_NEW or for an
	   expected connection, IP_CT_RELATED. */
	//只处理初始方向skb
	if (CTINFO2DIR(ctinfo) != IP_CT_DIR_ORIGINAL)
		return NF_ACCEPT;
	//计算初始方向和reply方向的tuple的哈希值
	hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
	repl_hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_REPLY].tuple);

	spin_lock_bh(&nf_conntrack_lock);
	/* See if there's one in the list already, including reverse:
	   NAT could have grabbed it without realizing, since we're
	   not in the hash.  If there is, we lost race. */
	//检查全局的hash表中是否已经保存了对应的tuple或者reply_tuple,
	//如果已经保存了,那么是一种异常,结束处理过程
	hlist_for_each_entry(h, n, &nf_conntrack_hash[hash], hnode)
		if (nf_ct_tuple_equal(&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple, &h->tuple))
			goto out;
	hlist_for_each_entry(h, n, &nf_conntrack_hash[repl_hash], hnode)
		if (nf_ct_tuple_equal(&ct->tuplehash[IP_CT_DIR_REPLY].tuple, &h->tuple))
			goto out;
	//将初始方向上的tuple_hash从unconfirmed链表中删除,添加的时候也只添加了初始方向的tuple
	hlist_del(&ct->tuplehash[IP_CT_DIR_ORIGINAL].hnode);
	//将tuple和reply_tuple都加入到全局的nf_conntrack_hash中,完成一条连接的跟踪
	__nf_conntrack_hash_insert(ct, hash, repl_hash);
	/* Timer relative to confirmation time, not original
	   setting time, otherwise we'd get timer wrap in
	   weird delay cases. */
	ct->timeout.expires += jiffies;
	//激活超时定时器
	add_timer(&ct->timeout);
	atomic_inc(&ct->ct_general.use);
	//标记该连接已经被确认过了
	set_bit(IPS_CONFIRMED_BIT, &ct->status);
	NF_CT_STAT_INC(insert);
	spin_unlock_bh(&nf_conntrack_lock);
	help = nfct_help(ct);
	if (help && help->helper)
		nf_conntrack_event_cache(IPCT_HELPER, skb);
#ifdef CONFIG_NF_NAT_NEEDED
	if (test_bit(IPS_SRC_NAT_DONE_BIT, &ct->status) ||
	    test_bit(IPS_DST_NAT_DONE_BIT, &ct->status))
		nf_conntrack_event_cache(IPCT_NATINFO, skb);
#endif
	nf_conntrack_event_cache(master_ct(ct) ? IPCT_RELATED : IPCT_NEW, skb);
	return NF_ACCEPT;
out:
	NF_CT_STAT_INC(insert_failed);
	spin_unlock_bh(&nf_conntrack_lock);
	return NF_DROP;
}
EXPORT_SYMBOL_GPL(__nf_conntrack_confirm);

3. 连接超时机制

一个连接被建立后,如果长时间没有数据交互,那么应该将该连接从连接跟踪子系统中清除,因为毕竟内存是有限的,不可能长时间跟踪这些空闲的连接。

为了实现这个目的,连接跟踪子系统为每个连接维护了一个定时器,一旦该定时器超时,那么就将该连接从系统中清除。

定时器的初始化

定时器是在连接跟踪信息块的分配函数nf_conntrack_alloc()中初始化的。

struct nf_conn *nf_conntrack_alloc(const struct nf_conntrack_tuple *orig,
				   const struct nf_conntrack_tuple *repl)
{
...
	/* Don't set timer yet: wait for confirmation */
	setup_timer(&ct->timeout, death_by_timeout, (unsigned long)ct);
...
}

如上所见,定时器是在nf_conntrack_confirmed()中激活的。

定时器超时回调

定时器超时回调函数为death_by_timeout(),其实现就是将连接跟踪信息块从系统中删除。

static void death_by_timeout(unsigned long ul_conntrack)
{
	struct nf_conn *ct = (void *)ul_conntrack;
	struct nf_conn_help *help = nfct_help(ct);
	struct nf_conntrack_helper *helper;
	//如果该连接有helper模块,先调用该helper模块的销毁函数
	if (help) {
		rcu_read_lock();
		helper = rcu_dereference(help->helper);
		if (helper && helper->destroy)
			helper->destroy(ct);
		rcu_read_unlock();
	}
	spin_lock_bh(&nf_conntrack_lock);
	/* Inside lock so preempt is disabled on module removal path.
	 * Otherwise we can get spurious warnings. */
	NF_CT_STAT_INC(delete_list);
	//将连接跟踪信息块、可能存在的期望连接一并从系统中删除
	clean_from_lists(ct);
	spin_unlock_bh(&nf_conntrack_lock);
	//递减连接信息跟踪块的引用计数,当引用计数变为0后会被其
	nf_ct_put(ct);
}

具体的删除由clean_from_llists()实现:

static void clean_from_lists(struct nf_conn *ct)
{
	//删除了tuple也就从全局的hash表中删除了连接跟踪信息块
	hlist_del_rcu(&ct->tuplehash[IP_CT_DIR_ORIGINAL].hnode);
	hlist_del_rcu(&ct->tuplehash[IP_CT_DIR_REPLY].hnode);
	//销毁所有的期望连接
	nf_ct_remove_expectations(ct);
}

3.3 定时器的更新

当然,当收到数据包后,也应该更新该定时器,防止其超时,这是通过调用nf_ct_refresh_acct()刷新的。这个刷新动作的调用由L4协议的paket()回调负责完成,这同时也表示将超时时间的决定权给了L4协议。

/* Refresh conntrack for this many jiffies and do accounting */
static inline void nf_ct_refresh_acct(struct nf_conn *ct,
	enum ip_conntrack_info ctinfo, const struct sk_buff *skb,
	unsigned long extra_jiffies)
{
	__nf_ct_refresh_acct(ct, ctinfo, skb, extra_jiffies, 1);
}
/* Refresh conntrack for this many jiffies and do accounting if do_acct is 1 */
void __nf_ct_refresh_acct(struct nf_conn *ct, enum ip_conntrack_info ctinfo,
	const struct sk_buff *skb, unsigned long extra_jiffies, int do_acct)
{
	int event = 0;

	NF_CT_ASSERT(ct->timeout.data == (unsigned long)ct);
	NF_CT_ASSERT(skb);
	spin_lock_bh(&nf_conntrack_lock);
	//设定了该标记的连接的超时值将无法被更新
	if (test_bit(IPS_FIXED_TIMEOUT_BIT, &ct->status))
		goto acct;
	/* If not in hash table, timer will not be active yet */
	if (!nf_ct_is_confirmed(ct)) {
		//连接跟踪信息块还没有被确认时,该定时器就还没有被激活,此时
		//重新设定超时时间,认为是重新更新超时时间戳
		ct->timeout.expires = extra_jiffies;
		event = IPCT_REFRESH;
	} else {
		//传入超时值是当前时间的相对值
		unsigned long newtime = jiffies + extra_jiffies;
		//只有当新的超时值至少超过当前超时值1s时才重新更新定时器(避免频繁更新)
		if ((newtime - ct->timeout.expires >= HZ) && del_timer(&ct->timeout)) {
			ct->timeout.expires = newtime;
			add_timer(&ct->timeout);
			event = IPCT_REFRESH;
		}
	}
acct:
#ifdef CONFIG_NF_CT_ACCT
	//做数据统计
	if (do_acct) {
		ct->counters[CTINFO2DIR(ctinfo)].packets++;
		ct->counters[CTINFO2DIR(ctinfo)].bytes +=
			skb->len - skb_network_offset(skb);
		if ((ct->counters[CTINFO2DIR(ctinfo)].packets & 0x80000000)
		    || (ct->counters[CTINFO2DIR(ctinfo)].bytes & 0x80000000))
			event |= IPCT_COUNTER_FILLING;
	}
#endif
	spin_unlock_bh(&nf_conntrack_lock);
	/* must be unlocked when calling event cache */
	if (event)
		nf_conntrack_event_cache(event, skb);
}
EXPORT_SYMBOL_GPL(__nf_ct_refresh_acct);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值