5. 两个主要的连接跟踪函数: ip_conntrack_in()和ip_confirm()
5.1.1 ip_conntrack_in()函数ip_conntrack_core.c
接收倒的数据包进入Netfilter后,首先进行分片处理,然后就会调用ip_conntrack_in函数, ip_conntrack_in 主要完成的工作就是判断数据包是否已在连接跟踪表中,如果不在,则为数据包分配ip_conntrack,并初始化它,然后,为这个数据包设置连接状态。
unsigned int ip_conntrack_in(unsigned int hooknum,
struct sk_buff **pskb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
struct ip_conntrack *ct;
enum ip_conntrack_info ctinfo;
struct ip_conntrack_protocol *proto;
int set_reply;
int ret;
/* 分片包会在前一个Hook中被处理,事实上,并不会触发该条件 */
if ((*pskb)->nh.iph->frag_off & htons(IP_OFFSET)) {
return NF_ACCEPT;
if (net_ratelimit()) {
printk(KERN_ERR "ip_conntrack_in: Frag of proto %u (hook=%u)\n",
(*pskb)->nh.iph->protocol, hooknum);
}
return NF_DROP;
}
/* 将当前数据包设置为未修改 */
(*pskb)->nfcache |= NFC_UNKNOWN;
/* 判断当前数据包是否已被检查过了*/
if ((*pskb)->nfct)
return NF_ACCEPT;
/* 根据当前数据包的协议,查找与之相应的struct ip_conntrack_protocol结构 */
proto = ip_ct_find_proto((*pskb)->nh.iph->protocol);
/* 如果是icmp错误报文 */
if ((*pskb)->nh.iph->protocol == IPPROTO_ICMP
&& icmp_error_track(*pskb, &ctinfo, hooknum))
return NF_ACCEPT;
/* 在全局的连接表中,查找与当前包相匹配的连接结构,返回的是struct ip_conntrack *类型指针,它用于描述一个数据包的连接状态 */
if (!(ct = resolve_normal_ct(*pskb, proto,&set_reply,hooknum,&ctinfo)))
return NF_ACCEPT;
if (IS_ERR(ct))
return NF_DROP;
IP_NF_ASSERT((*pskb)->nfct);
/* 如果注册了相应的协议的ip_conntrack_protocol结构,则在这里调用其中的packet函数做一些检查 */
ret = proto->packet(ct, *pskb, ctinfo);
if (ret == -1) {
/* Invalid */
nf_conntrack_put((*pskb)->nfct);
(*pskb)->nfct = NULL;
return NF_ACCEPT;
}
/* 如果注册了相应协议的ip_conntrack_helper结构,则在这里调用其help函数 */
if (ret != NF_DROP && ct->helper) {
ret = ct->helper->help(*pskb, ct, ctinfo);
if (ret == -1) {
/* Invalid */
nf_conntrack_put((*pskb)->nfct);
(*pskb)->nfct = NULL;
return NF_ACCEPT;
}
}
if (set_reply)
set_bit(IPS_SEEN_REPLY_BIT, &ct->status);
return ret;
}
连接跟踪模块将所有支持的协议,都使用struct ip_conntrack_protocol 结构封装,注册至全局链表中,这里首先调用函数ip_ct_find_proto根据当前数据包的协议值,找到协议注册对应的模块。然后调用resolve_normal_ct 函数进一步处理。
5.1.2 resolve_normal_ct()函数 ip_conntrack_core.c
函数判断数据包在连接跟踪表是否存在,如果不存在,则为数据包分配相应的连接跟踪节点空间并初始化,然后设置连接状态。
static inline struct ip_conntrack *
resolve_normal_ct(struct sk_buff *skb,
struct ip_conntrack_protocol *proto,
int *set_reply,
unsigned int hooknum,
enum ip_conntrack_info *ctinfo)
{
struct ip_conntrack_tuple tuple;
struct ip_conntrack_tuple_hash *h;
IP_NF_ASSERT((skb->nh.iph->frag_off & htons(IP_OFFSET)) == 0);
/* 将数据包转换成tuple */
if (!get_tuple(skb->nh.iph, skb, skb->nh.iph->ihl*4, &tuple, proto))
return NULL;
/* 查找对应的tuple在连接跟踪表中是否存在 */
h = ip_conntrack_find_get(&tuple, NULL);
/* 如果不存在,初始化该连接 */
if (!h) {
h = init_conntrack(&tuple, proto, skb);
if (!h)
return NULL;
if (IS_ERR(h))
return (void *)h;
}
/* 判断连接方向 */
if (DIRECTION(h) == IP_CT_DIR_REPLY) {
*ctinfo = IP_CT_ESTABLISHED + IP_CT_IS_REPLY;
/* Please set reply bit if this packet OK */
*set_reply = 1;
} else {
/* Once we’ve had two way comms, always ESTABLISHED. */
if (test_bit(IPS_SEEN_REPLY_BIT, &h->ctrack->status)) {
DEBUGP("ip_conntrack_in: normal packet for %p\n",
h->ctrack);
*ctinfo = IP_CT_ESTABLISHED;
} else if (test_bit(IPS_EXPECTED_BIT, &h->ctrack->status)) {
DEBUGP("ip_conntrack_in: related packet for %p\n",
h->ctrack);
*ctinfo = IP_CT_RELATED;
} else {
DEBUGP("ip_conntrack_in: new packet for %p\n",
h->ctrack);
*ctinfo = IP_CT_NEW;
}
*set_reply = 0;
}
/* 设置skb的对应成员,如使用计数器、数据包状态标记 */
skb->nfct = &h->ctrack->infos[*ctinfo];
return h->ctrack;
}
5.1.3获取tuple结构 ip_conntrack_core.c
get_tuple()函数将数据包转换成tuple结构
int get_tuple(const struct iphdr *iph,
const struct sk_buff *skb,
unsigned int dataoff,
struct ip_conntrack_tuple *tuple,
const struct ip_conntrack_protocol *protocol)
{
/* Never happen */
if (iph->frag_off & htons(IP_OFFSET)) {
printk("ip_conntrack_core: Frag of proto %u.\n",
iph->protocol);
return 0;
}
/* 设置来源、目的地址和协议号 */
tuple->src.ip = iph->saddr;
tuple->dst.ip = iph->daddr;
tuple->dst.protonum = iph->protocol;
tuple->src.u.all = tuple->dst.u.all = 0;
/* 这里根据协议的不同调用各自的函数 */
return protocol->pkt_to_tuple(skb, dataoff, tuple);
}
以tcp协议为例,ip_conntrack_proto_tcp.c :
static int tcp_pkt_to_tuple(const struct sk_buff *skb,
unsigned int dataoff,
struct ip_conntrack_tuple *tuple)
{
struct tcphdr hdr;
/* Actually only need first 8 bytes. */
if (skb_copy_bits(skb, dataoff, &hdr, 8) != 0)
return 0;
/* 根据报头的端口信息,设置tuple对应成员 */
tuple->src.u.tcp.port = hdr.source;
tuple->dst.u.tcp.port = hdr.dest;
return 1;
}
5.1.4搜索hash表
要对Hash表进行遍历,首要需要找到hash表的入口,然后来遍历该入口指向的链表。每个链表的节点是struct ip_conntrack_tuple_hash,它封装了tuple,所谓封装,就是把待查找的tuple与节点中已存的tuple相比较。
struct ip_conntrack_tuple_hash *
ip_conntrack_find_get(const struct ip_conntrack_tuple *tuple,
const struct ip_conntrack *ignored_conntrack)
{
struct ip_conntrack_tuple_hash *h;
READ_LOCK(&ip_conntrack_lock);
/* 查找整个hash表,返回一个节点 */
h = __ip_conntrack_find(tuple, ignored_conntrack);
/* 找到则累加计数器 */
if (h)
atomic_inc(&h->ctrack->ct_general.use);
READ_UNLOCK(&ip_conntrack_lock);
return h;
}
计算hash值,是调用hash_conntrack函数,根据数据包对应的tuple实现的:
static struct ip_conntrack_tuple_hash *
__ip_conntrack_find(const struct ip_conntrack_tuple *tuple,
const struct ip_conntrack *ignored_conntrack)
{
struct ip_conntrack_tuple_hash *h;
/* 计算tuple的hash值,这样,tuple对应的hash表入口即为ip_conntrack_hash[hash],也就是链表的首节点*/
unsigned int hash = hash_conntrack(tuple);
MUST_BE_READ_LOCKED(&ip_conntrack_lock);
/* 找到链表入口后遍历链表,返回一个节点。比较函数是conntrack_tuple_cmp() */
h = LIST_FIND(&ip_conntrack_hash[hash],
conntrack_tuple_cmp,
struct ip_conntrack_tuple_hash *,
tuple, ignored_conntrack);
return h;
}
5.1.5 init_conntrack()函数 ip_conntrack_core.c
static struct ip_conntrack_tuple_hash *
init_conntrack(const struct ip_conntrack_tuple *tuple,
struct ip_conntrack_protocol *protocol,
struct sk_buff *skb)
{
struct ip_conntrack *conntrack;
struct ip_conntrack_tuple repl_tuple;
size_t hash;
struct ip_conntrack_expect *expected;
int i;
static unsigned int drop_next;
/* 如果计算hash值的随机数种子没有被初始化,则初始化之 */
if (!ip_conntrack_hash_rnd_initted) {
get_random_bytes(&ip_conntrack_hash_rnd, 4);
ip_conntrack_hash_rnd_initted = 1;
}
/* 计算hash值 */
hash = hash_conntrack(tuple);
/* 判断连接跟踪表是否已满 */
if (ip_conntrack_max &&
atomic_read(&ip_conntrack_count) >= ip_conntrack_max) {
/* Try dropping from random chain, or else from the
chain about to put into (in case they’re trying to
bomb one hash chain). */
unsigned int next = (drop_next++)%ip_conntrack_htable_size;
if (!early_drop(&ip_conntrack_hash[next])
&& !early_drop(&ip_conntrack_hash[hash])) {
#if defined(CONFIG_MIPS_BRCM)
/* Sorry, we have to kick one out regardless. */
while (!regardless_drop(&ip_conntrack_hash[next]))
next = (drop_next++)%ip_conntrack_htable_size;
#else
if (net_ratelimit())
printk(KERN_WARNING
"ip_conntrack: table full, dropping"
" packet.\n");
return ERR_PTR(-ENOMEM);
#endif
}
}
/* 根据当前的tuple取反,计算该数据包的“应答”的tuple */
if (!invert_tuple(&repl_tuple, tuple, protocol)) {
DEBUGP("Can’t invert tuple.\n");
return NULL;
}
/* 为数据包对应的连接分配空间 */
conntrack = kmem_cache_alloc(ip_conntrack_cachep, GFP_ATOMIC);
if (!conntrack) {
DEBUGP("Can’t allocate conntrack.\n");
return ERR_PTR(-ENOMEM);
}
/* 初始化该结构,使用计数器累加,设置destroy函数指针,设置两个方向的tuple */
memset(conntrack, 0, sizeof(*conntrack));
atomic_set(&conntrack->ct_general.use, 1);
conntrack->ct_general.destroy = destroy_conntrack;
conntrack->tuplehash[IP_CT_DIR_ORIGINAL].tuple = *tuple;
conntrack->tuplehash[IP_CT_DIR_ORIGINAL].ctrack = conntrack;
conntrack->tuplehash[IP_CT_DIR_REPLY].tuple = repl_tuple;
conntrack->tuplehash[IP_CT_DIR_REPLY].ctrack = conntrack;
for (i=0; i < IP_CT_NUMBER; i++)
conntrack->infos.master = &conntrack->ct_general;
/* 创建一个该协议对应的ip_conntrack_protocol结构 */
if (!protocol->new(conntrack, skb)) {
kmem_cache_free(ip_conntrack_cachep, conntrack);
return NULL;
}
/*初始化时间计数器,并设置超时初始函数 */
init_timer(&conntrack->timeout);
conntrack->timeout.data = (unsigned long)conntrack;
conntrack->timeout.function = death_by_timeout;
/* 初始化预期连接链表 */
INIT_LIST_HEAD(&conntrack->sibling_list);
WRITE_LOCK(&ip_conntrack_lock);
/* Need finding and deleting of expected ONLY if we win race */
READ_LOCK(&ip_conntrack_expect_tuple_lock);
/* 在预期连接表中查找,有没有匹配的预期连接 */
expected = LIST_FIND(&ip_conntrack_expect_list, expect_cmp,
struct ip_conntrack_expect *, tuple);
READ_UNLOCK(&ip_conntrack_expect_tuple_lock);
/* If master is not in hash table yet (ie. packet hasn’t left
this machine yet), how can other end know about expected?
Hence these are not the droids you are looking for (if
master ct never got confirmed, we’d hold a reference to it
and weird things would happen to future packets). */
if (expected && !is_confirmed(expected->expectant))
expected = NULL;
/* 如果没有找到期望的连接,则搜索相关的helper结构 */
if (!expected)
conntrack->helper = ip_ct_find_helper(&repl_tuple);
/* If the expectation is dying, then this is a loser. */
if (expected
&& expected->expectant->helper->timeout
&& ! del_timer(&expected->timeout))
expected = NULL;
if (expected) {
DEBUGP("conntrack: expectation arrives ct=%p exp=%p\n",
conntrack, expected);
/* Welcome, Mr. Bond. We’ve been expecting you… */
__set_bit(IPS_EXPECTED_BIT, &conntrack->status);
conntrack->master = expected;
expected->sibling = conntrack;
#if CONFIG_IP_NF_CONNTRACK_MARK
conntrack->mark = expected->expectant->mark;
#endif
LIST_DELETE(&ip_conntrack_expect_list, expected);
expected->expectant->expecting–;
nf_conntrack_get(&master_ct(conntrack)->infos[0]);
}
atomic_inc(&ip_conntrack_count);
WRITE_UNLOCK(&ip_conntrack_lock);
if (expected && expected->expectfn)
expected->expectfn(conntrack);
/* 返回初始方向的hash节点 */
return &conntrack->tuplehash[IP_CT_DIR_ORIGINAL];
}
5.2.1 ip_ confirm函数 ip_conntrack_standalone.c
ip_confirm直接返回ip_conntrack_confirm(),而ip_conntrack_confirm()里又调用了__ip_conntrack_confirm()函数
static unsigned int ip_confirm(unsigned int hooknum,
struct sk_buff **pskb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
/* We’ve seen it coming out the other side: confirm it */
return ip_conntrack_confirm(*pskb);
}
static inline int ip_conntrack_confirm(struct sk_buff *skb)
{
if (skb->nfct
&& !is_confirmed((struct ip_conntrack *)skb->nfct->master))
return __ip_conntrack_confirm(skb->nfct);
return NF_ACCEPT;
}
在数据包穿过filter到达post_routing或local_in节点时,__ip_conntrack_confirm函数将该连接正式加入连接状态表中
int
__ip_conntrack_confirm(struct nf_ct_info *nfct)
{
unsigned int hash, repl_hash;
struct ip_conntrack *ct;
enum ip_conntrack_info ctinfo;
ct = __ip_conntrack_get(nfct, &ctinfo);
/* ipt_REJECT uses ip_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. */
if (CTINFO2DIR(ctinfo) != IP_CT_DIR_ORIGINAL)
return NF_ACCEPT;
hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
repl_hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_REPLY].tuple);
/* We’re not in hash table, and we refuse to set up related
connections for unconfirmed conns. But packet copies and
REJECT will give spurious warnings here. */
/* IP_NF_ASSERT(atomic_read(&ct->ct_general.use) == 1); */
/* No external references means noone else could have
confirmed us. */
IP_NF_ASSERT(!is_confirmed(ct));
DEBUGP("Confirming conntrack %p\n", ct);
WRITE_LOCK(&ip_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. */
if (!LIST_FIND(&ip_conntrack_hash[hash],
conntrack_tuple_cmp,
struct ip_conntrack_tuple_hash *,
&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple, NULL)
&& !LIST_FIND(&ip_conntrack_hash[repl_hash],
conntrack_tuple_cmp,
struct ip_conntrack_tuple_hash *,
&ct->tuplehash[IP_CT_DIR_REPLY].tuple, NULL)) {
list_prepend(&ip_conntrack_hash[hash],
&ct->tuplehash[IP_CT_DIR_ORIGINAL]);
list_prepend(&ip_conntrack_hash[repl_hash],
&ct->tuplehash[IP_CT_DIR_REPLY]);
/* 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);
WRITE_UNLOCK(&ip_conntrack_lock);
return NF_ACCEPT;
}
WRITE_UNLOCK(&ip_conntrack_lock);
return NF_DROP;
}
6. 具体协议的连接跟踪源码分析
前面是conntrack的总体框架,下面以tftp为例分析其具体实现
相关的文件: ip_conntrack_tftp.c ip_conntrack_tftp.h
6.1 helper和expect结构
6.1.1 ip_conntrack_helper结构
每个使用动态地址和端口的协议一般都定义了数据结构ip_conntrack_helper,并把它放到一个全局的链表中。这类协议一般都会有控制连接和数据连接两个不同的连接,如FTP协议等。
struct ip_conntrack_helper
{
struct list_head list; /* 链表头 */
const char *name; /* 模块的名称 */
unsigned char flags;
struct module *me; /* 指向模块本身 */
unsigned int max_expected; /* 期望的连接数的最大值 */
unsigned int timeout; /* 超时位 */
struct ip_conntrack_tuple tuple;
struct ip_conntrack_tuple mask;
int (*help)(struct sk_buff *skb,
struct ip_conntrack *ct,
enum ip_conntrack_info conntrackinfo);
};
6.1.2 tftp的init()初始化
将协议对应的ip_conntrack_helper结构数组初始化,并链接到全局链表里
static int __init init(void)
{
int i, ret;
char *tmpname;
if (!ports[0])
ports[0]=TFTP_PORT;
for (i = 0 ; (i < MAX_PORTS) && ports ; i++) {
/* Create helper structure */
memset(&tftp, 0, sizeof(struct ip_conntrack_helper));
tftp.tuple.dst.protonum = IPPROTO_UDP; /* 协议号17 */
tftp.tuple.src.u.udp.port = htons(ports); /* 源端口500 */
tftp.mask.dst.protonum = 0xFFFF;
tftp.mask.src.u.udp.port = 0xFFFF;
tftp.max_expected = 1;
tftp.timeout = 0;
tftp.flags = IP_CT_HELPER_F_REUSE_EXPECT;
tftp.me = THIS_MODULE;
tftp.help = tftp_help; /* tftp_help函数,最重要的部分 */
tmpname = &tftp_names[0];
if (ports == TFTP_PORT)
sprintf(tmpname, "tftp");
else
sprintf(tmpname, "tftp-%d", i);
tftp.name = tmpname;
DEBUGP("port #%d: %d\n", i, ports);
/* 将tftp的helper结构注册到一个全局链表&helpter里 */
ret=ip_conntrack_helper_register(&tftp);
if (ret) {
printk("ERROR registering helper for port %d\n",
ports);
fini();
return(ret);
}
ports_c++;
}
return(0);
}
6.1.3 ip_conntrack_expect结构
结构定义在ip_conntrack.h,ip_conntrack里的master变量就是这个结构,它的作用后面再说
struct ip_conntrack_expect
{
/* 链表头,在被某连接引用之前,所有expect结构都由此链表维护 */
struct list_head list;
/* 引用计数 */
atomic_t use;
/* 主连接的预期的子连接的链表 */
struct list_head expected_list;
/* 期待者,即预期连接对应的主连接,换句话说就是将此连接当作是其预期连接的连接… */
struct ip_conntrack *expectant;
/* 预期连接对应的真实的子连接 */
struct ip_conntrack *sibling;
/* 连接的tuple值 */
struct ip_conntrack_tuple ct_tuple;
/* 定时器 */
struct timer_list timeout;
/* 预期连接的tuple和mask,搜索预期连接时要用到的 */
struct ip_conntrack_tuple tuple, mask;
/* 预期连接函数,一般是NULL,有特殊需要时才定义 */
int (*expectfn)(struct ip_conntrack *new);
/* TCP协议时,主连接中描述子连接的数据起始处对应的序列号值 */
u_int32_t seq;
/* 跟踪各个多连接IP层协议相关的数据 */
union ip_conntrack_expect_proto proto;
/* 跟踪各个多连接应用层协议相关的数据 */
union ip_conntrack_expect_help help;
};
6.2数据包在连接跟踪模块中的行进过程
6.2.1 ip_conntrack_in阶段
数据包进入netfilter后,首先会调用ip_conntrack_in()函数,确认数据包未被检查过后,首先确定数据包使用的协议类型,如果是ICMP错误报文的话要做特别处理:
proto = ip_ct_find_proto((*pskb)->nh.iph->protocol);
proto是一个ip_conntrack_protocol指针,该函数根据sk_buff的protocol字段在全局的protocol_list变量里搜索匹配的ip_conntrack_protocol结构,bcm源码里注册了esp,gre,icmp,tcp,udp等协议
这里返回的是udp协议的ip_conntrack_protocol结构ip_conntrack_protocol_udp
接下来就要在连接跟踪表内查找属于该数据包的连接状态,如果能找到,说明该数据包是之前某连接的后续报文,那么只需要做一些标记处理就可以放行了,如果找不到,说明是一个新的连接,那么就创建一个新的连接状态,然后仍然放行(连接跟踪只是跟踪记录连接状态,不修改也不过滤)。将找到的,或者新建的连接状态返回给参数ct,ct就是一个ip_conntrack指针。
ct = resolve_normal_ct(*pskb, proto,&set_reply,hooknum,&ctinfo)
前面说了连接跟踪表是一个采用hash算法的链表数组,搜索hash表药先获取该连接的tuple结构,一般包括协议、来源/目的地址、来源/目的端口等,因不同协议而异(参考前面对tuple结构的描述)。resolve_normal_ct调用get_tuple函数获取报文的tuple结构,返回到第四个参数&tuple里:
get_tuple(skb->nh.iph, skb, skb->nh.iph->ihl*4, &tuple, proto)
tftp使用udp协议,固定端口是69。
然后就调用函数:
h = ip_conntrack_find_get(&tuple, NULL)
在连接跟踪表里查找连接,h是一个ip_conntrack_tuple_hash指针,是链表的一个节点
查找的时候先计算hash值
hash = hash_conntrack(tuple)
根据hash值找到链表的入口,然后遍历链表比较tuple
LIST_FIND(&ip_conntrack_hash[hash],
conntrack_tuple_cmp,
struct ip_conntrack_tuple_hash *,
tuple, ignored_conntrack);
查不到就新建一个连接,查到了就跳过这一步,这里假设是第一个数据包,那么肯定查不到
h = init_conntrack(&tuple, proto, skb)
新建连接时首先也要计算hash值(又算一遍),为了将来决定将它放入哪条链用
hash = hash_conntrack(tuple)
接下来计算相反方向的tuple,即repl_tuple
invert_tuple(&repl_tuple, tuple, protocol)
只是简单的将来源/目的颠倒而已
一个完整的连接状态是包括两个方向的tuple的,可以看到在ip_conntrack结构中
struct ip_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX]
定义了一个conntrack_tuple_hash数组,它包括tuplehash[IP_CT_DIR_ORIGINAL]和tuplehash[IP_CT_DIR_REPLY]两个元素
然后就要为新的连接创建一个ip_conntrack结构了
atomic_set(&conntrack->ct_general.use, 1);
conntrack->ct_general.destroy = destroy_conntrack;
conntrack->tuplehash[IP_CT_DIR_ORIGINAL].tuple = *tuple;
conntrack->tuplehash[IP_CT_DIR_ORIGINAL].ctrack = conntrack;
conntrack->tuplehash[IP_CT_DIR_REPLY].tuple = repl_tuple;
conntrack->tuplehash[IP_CT_DIR_REPLY].ctrack = conntrack;
引用计数加1,初始化了一个destory函数,用来销毁该结构的。然后是两个方向的tuple
ctrack是tuple里的一个指针,用来指向它所匿属于的ip_conntrack(两个互相指,可能是为了传递参数的时候比较方便,不过这个ctrack在新的内核中被去掉了)
将所有状态的infos变量都初始化,infos是一个nf_ct_info结构的数组,实际上是一个nf_conntrack结构数组,包含一个引用计数和一个销毁函数,见前面关于infos的定义
for (i=0; i < IP_CT_NUMBER; i++)
conntrack->infos.master = &conntrack->ct_general;
ct_general是ip_conntrack的第一个变量,结合下面的语句:
skb->nfct = &h->ctrack->infos[*ctinfo]
我们就可以通过:
struct ip_conntrack *ct = (struct ip_conntrack *)nfct->master
来从sk_buff里的nfct变量获取其所属连接的ip_conntrack结构
因为是一个新的连接,所以要调用对应的协议的ip_conntrack_protocol结构里的new函数,这个函数的作用因协议不同而异,而udp协议的new函数实际上不做任何事情,直接返回1
protocol->new(conntrack, skb)
初始化一些超时相关的变量
init_timer(&conntrack->timeout);
conntrack->timeout.data = (unsigned long)conntrack;
conntrack->timeout.function = death_by_timeout;
根据tuple在预期连接全局链表里搜索预期的连接,比较函数是expect_cmp
expected = LIST_FIND(&ip_conntrack_expect_list, expect_cmp,
struct ip_conntrack_expect *, tuple)
到底什么是预期的连接,现在还不是很清楚,但ip_conntrack_expect_list全局变量还没出现过,应该是空的,所以什么也查不到
查不到预期的连接,就查找有没有对应的helper结构,跟据tuple对应的repl_tuple,在全局链表helpers里搜索,此时协议是udp,目的端口是69
conntrack->helper = ip_ct_find_helper(&repl_tuple)
可想而知,在conntrack_tftp模块初始化时注册的tftp的helper结构被找到了,还是来看一下最终的比较函数
第一个参数是当前数据包的repl_tuple,后两个参数是tftp初始化时ip_conntrack_helper里的tuple和mask
static inline int ip_ct_tuple_mask_cmp(const struct ip_conntrack_tuple *t,
const struct ip_conntrack_tuple *tuple,
const struct ip_conntrack_tuple *mask)
{
return !(((t->src.ip ^ tuple->src.ip) & mask->src.ip)
|| ((t->dst.ip ^ tuple->dst.ip) & mask->dst.ip)
|| ((t->src.u.all ^ tuple->src.u.all) & mask->src.u.all)
|| ((t->dst.u.all ^ tuple->dst.u.all) & mask->dst.u.all)
|| ((t->dst.protonum ^ tuple->dst.protonum)& mask->dst.protonum));
}
mask用来区分哪些内容需要比较,哪些不需要,在ip_conntrack_tftp.c的初始化函数里,我们看到
tftp.mask.dst.protonum = 0xFFFF;
tftp.mask.src.u.udp.port = 0xFFFF;
所以比较函数里起作用的只有划线的两行,即协议和端口号,这里是UDP 69
最终将tftp的helper结构返回给了conntrack->helper
从init_conntrack函数返回,接下来确定数据包属于连接跟踪的哪个的状态,即ctinfo的值(是个枚举类型,这里应该是IP_CT_NEW,因为是第一个数据包),然后将对应的infos变量附给sk_buff *skb中的nfct,这样从skb里也可以得到本数据包的连接状态信息了
在前面见到过“ if ((*pskb)->nfct) return NF_ACCEPT;”之类的语句,意思就是每个包与一个状态结构关联,如果关联已存在,就不需要重复检查
skb->nfct = &h->ctrack->infos[*ctinfo]
从resolve_normal_ct函数里返回后调用ip_conntrack_protocol里的packet函数,对udp协议来说只是刷新一下超时位
ip_ct_refresh(conntrack, ip_ct_udp_timeout);
接下来要调用help函数了,之前把tftp注册的helper结构附给了conntrack-〉helper
ret = ct->helper->help(*pskb, ct, ctinfo)
最关键的地方到了,看看tftp_help函数做了些什么
static int tftp_help(struct sk_buff *skb,
struct ip_conntrack *ct,
enum ip_conntrack_info ctinfo)
{
struct tftphdr tftph;
struct ip_conntrack_expect *exp;
if (skb_copy_bits(skb, skb->nh.iph->ihl * 4 + sizeof(struct udphdr),
&tftph, sizeof(tftph)) != 0)
return NF_ACCEPT;
switch (ntohs(tftph.opcode)) {
/* RRQ and WRQ works the same way */
case TFTP_OPCODE_READ:
case TFTP_OPCODE_WRITE:
DEBUGP("");
DUMP_TUPLE(&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
DUMP_TUPLE(&ct->tuplehash[IP_CT_DIR_REPLY].tuple);
/* 初始化了一个ip_conntrack_expect结构 */
exp = ip_conntrack_expect_alloc();
if (exp == NULL)
return NF_ACCEPT;
/* 将expect的tuple设置为连接的应答方向的tuple,然后将源/目的地址,目的端口以及协议的mask位都置了1,只有源端口置0,也就是tftp里的69端口对应的mask */
exp->tuple = ct->tuplehash[IP_CT_DIR_REPLY].tuple;
exp->mask.src.ip = 0xffffffff;
exp->mask.dst.ip = 0xffffffff;
exp->mask.dst.u.udp.port = 0xffff;
exp->mask.dst.protonum = 0xffff;
exp->expectfn = NULL;
DEBUGP("expect: ");
DUMP_TUPLE(&exp->tuple);
DUMP_TUPLE(&exp->mask);
/* 将创建的expect结构与当前的连接关联起来 */
ip_conntrack_expect_related(exp, ct);
break;
case TFTP_OPCODE_DATA:
case TFTP_OPCODE_ACK:
DEBUGP("Data/ACK opcode\n");
break;
case TFTP_OPCODE_ERROR:
DEBUGP("Error opcode\n");
break;
default:
DEBUGP("Unknown opcode\n");
}
return NF_ACCEPT;
}
函数主要做了两件事情,首先初始化了一个ip_conntrack_expect结构,并将conntrack中应答方向的tuple结构附值给exp-〉tuple,然后调用:ip_conntrack_expect_related(exp, ct)
将这个expect结构与当前的连接状态关联起来,并把它注册到一个专门组织expect结构的全局链表ip_conntrack_expect_list里。
expect结构有什么用呢?当有返回的数据包时,首先仍然是搜索hash表,如果找不到可以匹配的连接,还会在全局链表里搜索匹配的expect结构,然后找到相关的连接状态。
为什么要这样做呢?假设有主机A向主机B发送消息,如下
A(10.10.10.1:1001)——> B(10.10.10.2:69)
从A的1001端口发往B的69端口,连接跟踪模块跟踪并记录了次条连接,保存在一个ip_conntrack结构里(用tuple来识别),但是我们知道,一个连接有两个方向,我们怎么确定两个方向相反的数据包是属于同一个连接的呢?最简单的判断方法就是将地址和端口号倒过来,就是说如果有下面的数据包:
B(10.10.10.2:69)——> A(10.10.10.1:1001)
虽然源/目的端口/地址全都不一样,不能匹配初始数据包的tuple,但是与对应的repl_tuple完全匹配,所以显然应该是同一个连接,所以见到这样的数据包时就可以直接确定其所属的连接了,当然不需要什么expect
然而不是所有协议都这么简单,对于tftp协议,相应的数据包可能是
B(10.10.10.2:1002)——> A(10.10.10.1:1001)
并不完全颠倒,就是说不能直接匹配初始数据包的tuple的反防向的repl_tuple,在hash表里找不到对应的节点,但我们仍然认为它和前面第一条消息有密切的联系,甚至我们可以明确,将所有下面形式的数据包都归属于这一连接的相关连接
B(10.10.10.2:XXX)——> A(10.10.10.1:1001)
怎么实现这一想法呢,只好再多创建一个expect了,它的tuple结构和repl_tuple完全相同,只是在mask中将源端口位置0,就是说不比较这一项,只要其他项匹配就OK。
(注意一下,ip_conntrack_expect结构里有个mask,ip_conntrack_helper里也有个mask,即使是同一个连接,它们的值也是不一样的。)
以上就是helper和expect的作用了,但是具体的实现方法还跟协议有关,像ftp的连接跟踪就相当复杂。
从help函数返回后,连接跟踪的第一阶段就结束了。
6.2.2 ip_confirm阶段
在3.1 init_or_cleanup()里面可以看到,数据包再次进入ip_conntrack模块是在NF_IP_LOCAL_IN点上,由ip_confirm函数来处理。
ip_confirm直接返回ip_conntrack_confirm(*pskb)
ip_conntrack_confirm检查一下数据包是否已经被检查过了,然后调用
__ip_conntrack_confirm(skb->nfct)
int __ip_conntrack_confirm(struct nf_ct_info *nfct)
{
unsigned int hash, repl_hash;
struct ip_conntrack *ct;
enum ip_conntrack_info ctinfo;
/* 获取该数据包的连接状态,就是通过前面提过的
struct ip_conntrack *ct = (struct ip_conntrack *)nfct->master
来实现的 */
ct = __ip_conntrack_get(nfct, &ctinfo);
/* 如果当前包不是一个初始方向的数据包,则直接返回,因为该连接状态在之前已经被添加到连接状态表里了,所以不需要再做什么。 */
if (CTINFO2DIR(ctinfo) != IP_CT_DIR_ORIGINAL)
return NF_ACCEPT;
/* 计算两个方向的hash值 */
hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
repl_hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_REPLY].tuple);
IP_NF_ASSERT(!is_confirmed(ct));
DEBUGP("Confirming conntrack %p\n", ct);
WRITE_LOCK(&ip_conntrack_lock);
/* 根据计算出来的hash值,在hash表中找到合适的节点,将当前连接插入,并设置相应的超时和状态位。 */
if (!LIST_FIND(&ip_conntrack_hash[hash],
conntrack_tuple_cmp,
struct ip_conntrack_tuple_hash *,
&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple, NULL)
&& !LIST_FIND(&ip_conntrack_hash[repl_hash],
conntrack_tuple_cmp,
struct ip_conntrack_tuple_hash *,
&ct->tuplehash[IP_CT_DIR_REPLY].tuple, NULL)) {
list_prepend(&ip_conntrack_hash[hash],
&ct->tuplehash[IP_CT_DIR_ORIGINAL]);
list_prepend(&ip_conntrack_hash[repl_hash],
&ct->tuplehash[IP_CT_DIR_REPLY]);
ct->timeout.expires += jiffies;
add_timer(&ct->timeout);
atomic_inc(&ct->ct_general.use);
set_bit(IPS_CONFIRMED_BIT, &ct->status);
WRITE_UNLOCK(&ip_conntrack_lock);
return NF_ACCEPT;
}
WRITE_UNLOCK(&ip_conntrack_lock);
return NF_DROP;
}
/* 到此,一个连就正式的被添加到系统的连接状态表里面了。 */
6.2.3连接跟踪对后续报文的处理
首先仍然是获取协议信息
proto = ip_ct_find_proto((*pskb)->nh.iph->protocol);
然后调用resolve_normal_ct()
ct = resolve_normal_ct(*pskb, proto,&set_reply,hooknum,&ctinfo)
在resolve_normal_ct函数内部,
get_tuple(skb->nh.iph, skb, skb->nh.iph->ihl*4, &tuple, proto)
然后查找hash表
h = ip_conntrack_find_get(&tuple, NULL)
这时候,还是举刚才的例子,如果之前曾有这样的数据包通过:
A(10.10.10.1:1001)——> B(10.10.10.2:69)
那么相应的连接状态应该已经建立了,这时候如果有完全相反的数据包
B(10.10.10.2:69)——> A(10.10.10.1:1001)
那么在搜索hash表的时候就能顺利找到对应的连接,找到以后就简单了
skb->nfct = &h->ctrack->infos[*ctinfo]
将连接状态附值给nfct位,接下来就没什么特别的事情要做了,设置几个标志位,检查一下数据包等等
我们主要关心的还是这样的情况,如果后续的数据包是这样的:
B(10.10.10.2:1002)——> A(10.10.10.1:1001)
那么程序执行到ip_conntrack_find_get处时,会发现在hash表里找不到与当前数据包相匹配的连接,于是还是调用init_conntrack()创建连接
接下来的部分都和初始连接一样,计算hash值,初始化一个ip_conntrack等等,直到:
expected = LIST_FIND(&ip_conntrack_expect_list, expect_cmp,
struct ip_conntrack_expect *, tuple)
之前注册的expect被找到了,找到以后,进行下面的操作
if (expected) {
DEBUGP("conntrack: expectation arrives ct=%p exp=%p\n",
conntrack, expected);
/* 设置状态位为IPS_EXPECTED_BIT,当前的连接是一个预期的连接 */
__set_bit(IPS_EXPECTED_BIT, &conntrack->status);
/* conntrack的master位指向搜索到的expected,而expected的sibling位指向conntrack……..解释一下,这时候有两个conntrack,一个是一开始的初始连接(比如69端口的那个)也就是主连接conntrack1,一个是现在正在处理的连接(1002)子连接conntrack2,两者和expect的关系是:
1. expect的sibling指向conntrack2,而expectant指向conntrack1,
2.一个主连接conntrack1可以有若干个expect(int expecting表示当前数量),这些
expect也用一个链表组织,conntrack1中的struct list_head sibling_list就是该
链表的头。
3.一个子连接只有一个主连接,conntrack2的struct ip_conntrack_expect *master
指向expect
通过一个中间结构expect将主连接和子连接关联起来 */
conntrack->master = expected;
expected->sibling = conntrack;
/* 将此连接从预期连接全局链表里删除(只是从全局链表里删除,但连接本身还在),因为此连接即将被正式添加到全局的连接表里,所以下次如果再有B(10.10.10.2:1002)——> A(10.10.10.1:1001)这样的数据包,就能直接从hash表里找到了,不必再借用expect。因此主连接中的预期连接数expecting也自动减1 */
LIST_DELETE(&ip_conntrack_expect_list, expected);
expected->expectant->expecting–;
nf_conntrack_get(&master_ct(conntrack)->infos[0]);
}
接下来,如果expect里的expectfn函数有定义的话就执行它(一般是没有的)
if (expected && expected->expectfn)
expected->expectfn(conntrack);
后面的过程略去了,和之前差不多