目录
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() 新建一个连接跟踪该数据包。具体流程如下:
- 调用函数__nf_conntrack_alloc创建一个数据连接跟踪项
- 根据刚创建的连接跟踪项的原始方向的nf_conntrack_tuple变量,查找期望连接链表,看能否找到符合条件的期望连接跟踪。若查找到:则更新新创建连接跟踪项的master指针,以及调用exp->expectfn;若没有查找到:则遍历helpers链表,找到符合条件的nf_conntrack_helper变量后,则更新连接跟踪项的helper指针。
- 将新创建的连接跟踪项插入到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有如下不同:
- 在src中有L3协议编号,dst中存储的却是L4协议编号;
- 在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
};