我是靠谱客的博主 故意红牛,最近开发中收集的这篇文章主要介绍连接跟踪子系统之核心实现1. 连接跟踪入口: nf_conntrack_in()2. 连接跟踪子系统出口:nf_conntrack_confirm()3. 连接超时机制,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

如上一篇笔记连接跟踪子系统之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 occuredn");
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 modulen");
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 tuplen");
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 %pn", ct);
*ctinfo = IP_CT_ESTABLISHED;
} else if (test_bit(IPS_EXPECTED_BIT, &ct->status)) {
pr_debug("nf_conntrack_in: related packet for %pn", ct);
*ctinfo = IP_CT_RELATED;
} else {
//初始方向的第一个数据包
pr_debug("nf_conntrack_in: new packet for %pn", 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);

最后

以上就是故意红牛为你收集整理的连接跟踪子系统之核心实现1. 连接跟踪入口: nf_conntrack_in()2. 连接跟踪子系统出口:nf_conntrack_confirm()3. 连接超时机制的全部内容,希望文章能够帮你解决连接跟踪子系统之核心实现1. 连接跟踪入口: nf_conntrack_in()2. 连接跟踪子系统出口:nf_conntrack_confirm()3. 连接超时机制所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(63)

评论列表共有 0 条评论

立即
投稿
返回
顶部