ct是netfilter非常重要的基础和架构核心.它为状态防火墙,nat等打下基础. 一直觉的它很神秘,所以就下定决心分析一下.
这里依然不从框架开始说,而是从实际代码着手.
参考内核 kernel3.8.13
先看看它的初始化:
Net/netfilter/nf_conntrack_core.c
int nf_conntrack_init(struct net *net);
入口在nf_conntrack_standalone.c
module_init(nf_conntrack_standalone_init);
-
static int __init nf_conntrack_standalone_init(void)
-
{
-
return register_pernet_subsys(&nf_conntrack_net_ops);
-
}
它作为网络空间子系统注册进了内核
-
static struct pernet_operations nf_conntrack_net_ops = {
-
.init = nf_conntrack_net_init,
-
.exit = nf_conntrack_net_exit,
-
};
注册的过程中调用.init 传递给它的net参数是init_net 它是通过net_ns_init初始化到了net_namespace_list链上。
-
static int nf_conntrack_net_init(struct net *net)
-
{
-
int ret;
-
-
ret = nf_conntrack_init(net);
-
if (ret < 0)
-
goto out_init;
-
ret = nf_conntrack_standalone_init_proc(net);
-
if (ret < 0)
-
goto out_proc;
-
net->ct.sysctl_checksum = 1;
-
net->ct.sysctl_log_invalid = 0;
-
ret = nf_conntrack_standalone_init_sysctl(net);
-
if (ret < 0)
-
goto out_sysctl;
-
return 0;
-
-
out_sysctl:
-
nf_conntrack_standalone_fini_proc(net);
-
out_proc:
-
nf_conntrack_cleanup(net);
-
out_init:
-
return ret;
-
}
代码不是很多,核心明显是nf_conntrack_init函数
-
int nf_conntrack_init(struct net *net)
-
{
-
int ret;
-
-
if (net_eq(net, &init_net)) {
-
ret = nf_conntrack_init_init_net();
-
if (ret < 0)
-
goto out_init_net;
-
}
-
ret = nf_conntrack_proto_init(net);
-
if (ret < 0)
-
goto out_proto;
-
ret = nf_conntrack_init_net(net);
-
if (ret < 0)
-
goto out_net;
-
-
if (net_eq(net, &init_net)) {
-
/* For use by REJECT target */
-
RCU_INIT_POINTER(ip_ct_attach, nf_conntrack_attach);
-
RCU_INIT_POINTER(nf_ct_destroy, destroy_conntrack);
-
-
/* Howto get NAT offsets */
-
RCU_INIT_POINTER(nf_ct_nat_offset, NULL);
-
}
-
return 0;
-
-
out_net:
-
nf_conntrack_proto_fini(net);
-
out_proto:
-
if (net_eq(net, &init_net))
-
nf_conntrack_cleanup_init_net();
-
out_init_net:
-
return ret;
-
}
先进入nf_conntrack_init_init_net函数
nf_conntrack_htable_size 赋值和nf_conntrack_max(这个参数可以通过proc来设置.)
它和内存大小有关,大于1G的即默认为16384=16*1024=4*4k;
-
nf_conntrack_htable_size
-
= (((totalram_pages << PAGE_SHIFT) / 16384) // 16384 =16k =16*1024=4*4k
-
/ sizeof(struct hlist_head));
比如对于4G的内存,那么它的计算:
size=((1024*1024*4k )/(4*4k))/4= 1024*256/4=1024*64=1024*16*4=4*(4*4k)=4*16384
nf_conntrack_max呢?
-
/* Use a max. factor of four by default to get the same max as
-
* with the old struct list_heads. When a table size is given
-
* we use the old value of 8 to avoid reducing the max.
-
* entries. */
-
max_factor = 4;
-
}
-
nf_conntrack_max = max_factor * nf_conntrack_htable_size;
后续是设置per-cpu变量:有兴趣的可以看看.
-
per_cpu(nf_conntrack_untracked, cpu)
-
DEFINE_PER_CPU(struct nf_conn, nf_conntrack_untracked);
-
EXPORT_PER_CPU_SYMBOL(nf_conntrack_untracked);
nf_conntrack_proto_init
-
struct nf_conntrack_l4proto nf_conntrack_l4proto_generic __read_mostly =
-
{
-
.l3proto = PF_UNSPEC,
-
.l4proto = 255,
-
.name = "unknown",
-
.pkt_to_tuple = generic_pkt_to_tuple,
-
.invert_tuple = generic_invert_tuple,
-
.print_tuple = generic_print_tuple,
-
.packet = generic_packet,
-
.get_timeouts = generic_get_timeouts,
-
.new = generic_new,
-
#if IS_ENABLED(CONFIG_NF_CT_NETLINK_TIMEOUT)
-
.ctnl_timeout = {
-
.nlattr_to_obj = generic_timeout_nlattr_to_obj,
-
.obj_to_nlattr = generic_timeout_obj_to_nlattr,
-
.nlattr_max = CTA_TIMEOUT_GENERIC_MAX,
-
.obj_size = sizeof(unsigned int),
-
.nla_policy = generic_timeout_nla_policy,
-
},
-
#endif /* CONFIG_NF_CT_NETLINK_TIMEOUT */
-
.init_net = generic_init_net,
-
.get_net_proto = generic_get_net_proto,
-
};
初始化通用协议以及建立sysctl,并初始化nf_ct_l3protos为nf_conntrack_l3proto_generic.
nf_conntrack_init_net初始化hash链表和建立cache相关后续讨论.
先了解下基本的初始化工作后,我们从hook点说起ct是如何建立起来的
nf_conntrack_l3proto_ipv4.c
-
/* Connection tracking may drop packets, but never alters them, so
-
make it the first hook. */
-
static struct nf_hook_ops ipv4_conntrack_ops[] __read_mostly = {
-
{
-
.hook = ipv4_conntrack_in,
-
.owner = THIS_MODULE,
-
.pf = NFPROTO_IPV4,
-
.hooknum = NF_INET_PRE_ROUTING,
-
.priority = NF_IP_PRI_CONNTRACK,
-
},
-
{
-
.hook = ipv4_conntrack_local,
-
.owner = THIS_MODULE,
-
.pf = NFPROTO_IPV4,
-
.hooknum = NF_INET_LOCAL_OUT,
-
.priority = NF_IP_PRI_CONNTRACK,
-
},
-
{
-
.hook = ipv4_helper,
-
.owner = THIS_MODULE,
-
.pf = NFPROTO_IPV4,
-
.hooknum = NF_INET_POST_ROUTING,
-
.priority = NF_IP_PRI_CONNTRACK_HELPER,
-
},
-
{
-
.hook = ipv4_confirm,
-
.owner = THIS_MODULE,
-
.pf = NFPROTO_IPV4,
-
.hooknum = NF_INET_POST_ROUTING,
-
.priority = NF_IP_PRI_CONNTRACK_CONFIRM,
-
},
-
{
-
.hook = ipv4_helper,
-
.owner = THIS_MODULE,
-
.pf = NFPROTO_IPV4,
-
.hooknum = NF_INET_LOCAL_IN,
-
.priority = NF_IP_PRI_CONNTRACK_HELPER,
-
},
-
{
-
.hook = ipv4_confirm,
-
.owner = THIS_MODULE,
-
.pf = NFPROTO_IPV4,
-
.hooknum = NF_INET_LOCAL_IN,
-
.priority = NF_IP_PRI_CONNTRACK_CONFIRM,
-
},
-
}
我们会发现它的优先级比较高.除了上面的钩子还有其他的:
还有nf_defrag_ipv4.c
-
static struct nf_hook_ops ipv4_defrag_ops[] = {
-
{
-
.hook = ipv4_conntrack_defrag,
-
.owner = THIS_MODULE,
-
.pf = NFPROTO_IPV4,
-
.hooknum = NF_INET_PRE_ROUTING,
-
.priority = NF_IP_PRI_CONNTRACK_DEFRAG,
-
},
-
{
-
.hook = ipv4_conntrack_defrag,
-
.owner = THIS_MODULE,
-
.pf = NFPROTO_IPV4,
-
.hooknum = NF_INET_LOCAL_OUT,
-
.priority = NF_IP_PRI_CONNTRACK_DEFRAG,
-
},
-
};
除了hook点,我们需要记住的就是:
连接追踪入口 和
连接追踪出口
记录如何生成呢?我们看报文的流程:
1.发送给本机的数据包
流程:PRE_ROUTING----LOCAL_IN---本地进程
2.需要本机转发的数据包
流程:PRE_ROUTING---FORWARD---POST_ROUTING---外出
3.从本机发出的数据包
流程:LOCAL_OUT----POST_ROUTING---外出
那么就选择从流程1分析看看ct是如何一步一步建立起来的.
先从入口说起,接收的报文首先经过钩子点NF_INET_PRE_ROUTING
从优先级上先经过ipv4_conntrack_defrag 再经过ipv4_conntrack_in
对于帧接收,查询并交给处理协议我们已经很熟悉不过了,对于ip,当然先进入ip_rcv
Ip_input.c
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
ip_rcv_finish);
ip的处理工作主要在ip_rcv_finish里完成,ip_rcv主要做了些安全检查。
ipv4_conntrack_defrag看看这个函数,参数就是NF_HOOK里传递给它的
-
static unsigned int ipv4_conntrack_defrag(unsigned int hooknum,
-
struct sk_buff *skb,
-
const struct net_device *in,
-
const struct net_device *out,
-
int (*okfn)(struct sk_buff *))
-
{
-
struct sock *sk = skb->sk;
-
struct inet_sock *inet = inet_sk(skb->sk);
-
-
if (sk && (sk->sk_family == PF_INET) &&
-
inet->nodefrag)
-
return NF_ACCEPT;
-
-
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
-
#if !defined(CONFIG_NF_NAT) && !defined(CONFIG_NF_NAT_MODULE)
-
/* Previously seen (loopback)? Ignore. Do this before
-
fragment check. */
-
if (skb->nfct && !nf_ct_is_template((struct nf_conn *)skb->nfct))
-
return NF_ACCEPT;
-
#endif
-
#endif
-
/* Gather fragments. */
-
if (ip_is_fragment(ip_hdr(skb))) {
-
enum ip_defrag_users user = nf_ct_defrag_user(hooknum, skb); // IP_DEFRAG_CONNTRACK_IN
-
if (nf_ct_ipv4_gather_frags(skb, user))
-
return NF_STOLEN;
-
}
-
return NF_ACCEPT;
-
}
用ip_is_fragment判断是否是分片报文,如果有分片则调用nf_ct_ipv4_gather_frags--->ip_defrag
对于ip_defrag的调用的地方很少. 当需要传递给本地更高协议层的时候通过ip_local_deliver来组包.
补充:
NF_STOLEN
模块接管该数据报,告诉Netfilter“忘掉”该数据报。该回调函数将从此开始对数据包的处理,并且Netfilter应当放弃对该数据包做任何的处理。但是,这并不意味着该数据包的资源已经被释放。这个数据包以及它独自的sk_buff数据结构仍然有效,只是回调函数从Netfilter
获取了该数据包的所有权.
首先把skb独立出来,除去owner,然后调用ip_defrag组包,这也是netfilter效率低的原因之一.(重新组报文很耗费内存和时间)
每个分片报文都会创建一个struct ipq *qp来管理
-
/* Describe an entry in the "incomplete datagrams" queue. */
-
struct ipq {
-
struct inet_frag_queue q;
-
-
u32 user;
-
__be32 saddr;
-
__be32 daddr;
-
__be16 id;
-
u8 protocol;
-
u8 ecn; /* RFC3168 support */
-
int iif;
-
unsigned int rid;
-
struct inet_peer *peer;
-
};
查找是否已经有ipq, 根据ip的id ,saddr,daddr、protocol计算hash值,由于如果属于同一ip报文的分片则这些相同.
从ip4_frags全局的hash链表里查询,如果没有就创建
hlist_add_head(&qp->list, &f->hash[hash]); 这个qp是结构体struct inet_frag_queue
得到ipq后,通过ip_frag_queue把skb加入到队列里.
ip分片会插入到qp->q.fragments里
最后当满足一定条件时,进行IP重组。当收到了第一个和最后一个IP分片,且收到的IP分片的最大长度等于收到的IP分片的总长度时,表明所有的IP分片已收集齐,调用ip_frag_reasm重组包,成功返回0. 关于ip分片与重组参考的资料有很多.
下面看ipv4_conntrack_in
在nf_conntrack_l3proto_ipv4.c中
-
static unsigned int ipv4_conntrack_in(unsigned int hooknum,
-
struct sk_buff *skb,
-
const struct net_device *in,
-
const struct net_device *out,
-
int (*okfn)(struct sk_buff *))
-
{
-
return nf_conntrack_in(dev_net(in), PF_INET, hooknum, skb);
-
}
首先根据协议PF_INET找到链(ipv4)协议号超出范围则使用默认值nf_conntrack_l3proto_generic。
struct nf_conntrack_l3proto __rcu *nf_ct_l3protos[AF_MAX] __read_mostly;
通过接口nf_conntrack_l3proto_register注册了ipv4和ipv6到nf_ct_l3protos
正常的ipv4是:它负责对ip层报文的解析函数API,后续还有l4层相关的.
-
struct nf_conntrack_l3proto nf_conntrack_l3proto_ipv4 __read_mostly = {
-
.l3proto = PF_INET,
-
.name = "ipv4",
-
.pkt_to_tuple = ipv4_pkt_to_tuple,
-
.invert_tuple = ipv4_invert_tuple,
-
.print_tuple = ipv4_print_tuple,
-
.get_l4proto = ipv4_get_l4proto,
-
#if defined(CONFIG_NF_CT_NETLINK) || defined(CONFIG_NF_CT_NETLINK_MODULE)
-
.tuple_to_nlattr = ipv4_tuple_to_nlattr,
-
.nlattr_tuple_size = ipv4_nlattr_tuple_size,
-
.nlattr_to_tuple = ipv4_nlattr_to_tuple,
-
.nla_policy = ipv4_nla_policy,
-
#endif
-
#if defined(CONFIG_SYSCTL) && defined(CONFIG_NF_CONNTRACK_PROC_COMPAT)
-
.ctl_table_path = "net/ipv4/netfilter",
-
#endif
-
.init_net = ipv4_init_net,
-
.me = THIS_MODULE,
-
}
这个很重要的结构体struct nf_conntrack_l3proto
找个这个结构体后,调用它节点函数获取l4 协议号和dataoff
然后去找到struct nf_conntrack_l4proto *l4proto这个东西,如果找到即nf_ct_protos[l3proto][l4proto]
异常则为nf_conntrack_l4proto_generic
通过nf_conntrack_l4proto_register注册了tcp、udp、icmp;其他模块还有dccp、gre、sctp、udplite(轻量级用户数据包协议)
根据四层协议error函数check包的正确性。
然后调用
resolve_normal_ct .之前我们看到skb->nfct ,一开始肯定为null,它在这个函数里被赋值
首先nf_ct_get_tuple获取struct nf_conntrack_tuple tuple;由它可以判断一个连接即五元组;一个连接由一“去”一“回”两个五元组来唯一确定.
ipv4_pkt_to_tuple 获取srcip、dstip
tuple->src.l3num = l3num;
tuple->src.u3.ip = ap[0];
tuple->dst.u3.ip = ap[1];
tuple->dst.protonum = protonum;
tuple->dst.dir = IP_CT_DIR_ORIGINAL;
然后在解析l4信息:
例如tcp则解析端口:
tuple->src.u.tcp.port = hp->source;
tuple->dst.u.tcp.port = hp->dest;
现在我们有了 srcip、dstip、sportt 、dport,协议号,以及方向信息
然后查询追踪全局表是否已经有了这个流,hash_conntrack_raw计算hash值
__nf_conntrack_find_get:
hlist_nulls_for_each_entry_rcu(h, n, &net->ct.hash[bucket],
如果找到则返回,否则返回null,不过它返回的是类型:
-
/* Connections have two entries in the hash table: one for each way */
-
struct nf_conntrack_tuple_hash {
-
struct hlist_nulls_node hnnode;
-
struct nf_conntrack_tuple tuple;
-
};
对于第一个包肯定为null, 然后init_conntrack创建它.
先反转tuple得到repl_tuple
__nf_conntrack_alloc 申请struct nf_conn *ct;
从cache里申请
ct = kmem_cache_alloc(net->ct.nf_conntrack_cachep, gfp);
然后初始化
ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple = *orig;
ct->tuplehash[IP_CT_DIR_ORIGINAL].hnnode.pprev = NULL;/* save hash for reusing when confirming */
*(unsigned long *)(&
ct->tuplehash[IP_CT_DIR_REPLY].hnnode.pprev) = hash;
ct->tuplehash[IP_CT_DIR_REPLY].tuple = *repl;
并设置ct定时器 death_by_timeout
l4proto->new(ct, skb, dataoff, timeouts) 设置l4 ct参数。
关于nf_ct_acct_ext_add这里先不讨论.
然后找到协议注册的expect :struct nf_conntrack_expect
exp = nf_ct_find_expectation(net, zone, tuple);
它查找的是net->ct.expect_hash[h] ,即当前ct所期望关联的tuple
我们看看内核注册了哪些expect
通过nf_ct_expect_alloc申请,这个貌似和上层应用关联用的。
对于应用的关联,不是很清楚,简单看看tftp的nf_conntrack_tftp_init
它有两个关键的地方:
1.tftp[i][j].help = tftp_help;
2.nf_conntrack_helper_register(&tftp[i][j]);
这个tftp是struct nf_conntrack_helper结构体。关于helper这里说明一下:
Netfilter的连接跟踪为我们提供了一个非常有用的功能模块:helper。该模块可以使我们以很小的代价来完成对连接跟踪功能的扩展。这种应用场景需求一般是,当一个数据包即将离开Netfilter框架之前,我们可以对数据包再做一些最后的处理.
同时还有个补充tftp[i][j].expect_policy = &tftp_exp_policy; 它也是相关的
它把helper加入hlist:全局nf_ct_helper_hash[h]
我们__nf_ct_try_assign_helper这个被init_conntrack调用,也就是新建ct的时候
一开始net->ct.expect_hash应该为null
但是expect_hash和nf_ct_helper_hash 又是如何关联起来的呢?
nf_ct_expect_insert会操作expect_hash并插入,它最后封装在nf_ct_expect_related
刚才说到tftp expect对吧,tftp_help里刚好调用了它
在tftp_help里它申请一个exp = nf_ct_expect_alloc(ct); 然后初始化nf_ct_expect_init。
最后调用nf_ct_expect_related把这个exp和具体的ct关联到expect_hash里。
它属于被动的,还得从tftp说起,虽然它依helper方式把注册进了help_hash。
但是它又是如何运作起来的呢?毕竟这个时候只是静态的注册而已,即需要触发tftp_help函数.
要触发它,就需要找到注册的helper,就需要计算hash。刚好在__nf_ct_try_assign_helper中有
__nf_conntrack_helper_find查找注册的helper和当前ct的关联.
我们看看查找的时候用的tuple:
-
__nf_ct_helper_find(&ct->tuplehash[IP_CT_DIR_REPLY].tuple)
这个参数我们知道就是当前tuple的反转五元组. 而查找的时候计算hash值只用到了五元组的协议号、端口 (还有一个是ipv4 or ipv6)
(跟我们之前查找ct的时候计算的hash需要的参数少了很多.) 很明显helper注册的时候也用了这样的hash算法.
回头看看tftp_helper注册的时候:
-
tftp[i][j].tuple.dst.protonum = IPPROTO_UDP;
-
tftp[i][j].tuple.src.u.udp.port = htons(ports[i]);
这两个值是事先给定好的. 其实发现没有,虽然很容易关联,但是也面临着冲突的问题.所以需要补全ip和端口信息
既然找到了那么如何处理呢?
-
if (help == NULL) {
-
help = nf_ct_helper_ext_add(ct, helper, flags);
-
if (help == NULL) {
-
ret = -ENOMEM;
-
goto out;
-
}
-
}
help是什么呢?struct nf_conn_help *help;
-
/* nf_conn feature for connections that have a helper */
-
struct nf_conn_help {
-
/* Helper. if any */
-
struct nf_conntrack_helper __rcu *helper;
-
-
struct hlist_head expectations;
-
-
/* Current number of expected connections */
-
u8 expecting[NF_CT_MAX_EXPECT_CLASSES];
-
-
/* private helper information. */
-
char data[];
-
};
nf_ct_helper_ext_add扩展ct的ext空间. 然后把找到的helper指针赋给help->helper:
-
rcu_assign_pointer(help->helper, helper);
那么以后我们就可以通过help = nfct_help(ct);这样的接口找到我们关联的helper了.
关于查找exp补充说明一下:
expected函数有什么作用?
当一个新的包到达init_conntrack时,就会根据包中的源地址、目的地址等信息填充一个struct nf_conn实例,通常定义为ct的变量。接下来检查当前的连接是否是另外一条已经存在连接的期望连接:
-
spin_lock_bh(&nf_conntrack_lock);
-
exp = nf_ct_find_expectation(net, zone, tuple);
-
if (exp) {
-
pr_debug("conntrack: expectation arrives ct=%p exp=%p\n",
-
ct, exp);
-
/* Welcome, Mr. Bond. We've been expecting you... */
-
__set_bit(IPS_EXPECTED_BIT, &ct->status);
-
ct->master = exp->master;
-
if (exp->helper) {
-
help = nf_ct_helper_ext_add(ct, exp->helper,
-
GFP_ATOMIC);
-
if (help)
-
rcu_assign_pointer(help->helper, exp->helper);
-
}
-
-
#ifdef CONFIG_NF_CONNTRACK_MARK
-
ct->mark = exp->master->mark;
-
#endif
-
#ifdef CONFIG_NF_CONNTRACK_SECMARK
-
ct->secmark = exp->master->secmark;
-
#endif
-
nf_conntrack_get(&ct->master->ct_general);
-
NF_CT_STAT_INC(net, expect_new);
-
}
如果exp不为空,就表示当前的连接是另外一条已经存在连接的期望连接.接下来,就是expectfn的工作了:根据master的连接跟踪信息更新新建立的ct连接跟踪信息,并放到连接跟踪表中,详见nf_nat_follow_master函数(因为expectfn通常指向的nf_nat_follow_master).
回到主线函数:
最后把ct加入一个未认证的hlist:
-
/* Overload tuple linked list to put us in unconfirmed list. */
-
hlist_nulls_add_head_rcu(&ct->tuplehash[IP_CT_DIR_ORIGINAL].hnnode,
-
&net->ct.unconfirmed);
并返回return &ct->tuplehash[IP_CT_DIR_ORIGINAL].
我们看如果直接找到了ct那么下面的工作很简单:设置一些状态值然后赋值skb
skb->nfct = &ct->ct_general;
skb->nfctinfo = *ctinfo; // 对于第一次报文 值为:IP_CT_NEW
return ct;
skb建立ct关联后,然后更新ct的状态,调用l4协议的packet函数:
-
ret = l4proto->packet(ct, skb, dataoff, ctinfo, pf, hooknum, timeouts);
以上只是简单流程的分析
通过上面的分析我们知道当我们用到ct的时候,把skb->nfct强制类型转换就可以了
虽然nfct是struct nf_conntrack *nfct;
-
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
-
struct nf_conntrack {
-
atomic_t use;
-
};
-
#endif
但是我们看到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;
-
...
-
}
我们是不是明白了为什么skb->nfct那么使用。新的内核也提供了接口:
-
/* Return conntrack_info and tuple hash for given skb. */
-
static inline struct nf_conn *
-
nf_ct_get(const struct sk_buff *skb, enum ip_conntrack_info *ctinfo)
-
{
-
*ctinfo = skb->nfctinfo;
-
return (struct nf_conn *)skb->nfct;
-
}
连接追踪用结构体 struct nf_conn表示 ,而状态信息用enum ip_conntrack_info 表示
1. IP_CT_ESTABLISHED
Packet是一个已建连接的一部分,在其初始方向。
2. IP_CT_RELATED
Packet属于一个已建连接的相关连接,在其初始方向。
3. IP_CT_NEW
Packet试图建立新的连接
4. IP_CT_ESTABLISHED+IP_CT_IS_REPLY
Packet是一个已建连接的一部分,在其响应方向。
5. IP_CT_RELATED+IP_CT_IS_REPLY
Packet属于一个已建连接的相关连接,在其响应方向
刚才我们分析了第一个过来的包,属于新建连接,即IP_CT_NEW。
对于每个进来的包都先获取struct nf_conntrack_tuple信息 和查询或者创建struct nf_conntrack_tuple_hash
接着我们需要看的是ip_conntrack_help()和ip_confirm();优先级上先是helper 然后是confirm.对于新版内核接口名字有所改变:ipv4_help/ipv4_confirm
-
static unsigned int ipv4_helper(unsigned int hooknum,
-
struct sk_buff *skb,
-
const struct net_device *in,
-
const struct net_device *out,
-
int (*okfn)(struct sk_buff *))
-
{
-
struct nf_conn *ct;
-
enum ip_conntrack_info ctinfo;
-
const struct nf_conn_help *help;
-
const struct nf_conntrack_helper *helper;
-
unsigned int ret;
-
-
/* This is where we call the helper: as the packet goes out. */
-
ct = nf_ct_get(skb, &ctinfo);
-
if (!ct || ctinfo == IP_CT_RELATED_REPLY)
-
return NF_ACCEPT;
-
-
help = nfct_help(ct);
-
if (!help)
-
return NF_ACCEPT;
-
-
/* rcu_read_lock()ed by nf_hook_slow */
-
helper = rcu_dereference(help->helper);
-
if (!helper)
-
return NF_ACCEPT;
-
-
ret = helper->help(skb, skb_network_offset(skb) + ip_hdrlen(skb),
-
ct, ctinfo);
-
if (ret != NF_ACCEPT && (ret & NF_VERDICT_MASK) != NF_QUEUE) {
-
nf_log_packet(NFPROTO_IPV4, hooknum, skb, in, out, NULL,
-
"nf_ct_%s: dropping packet", helper->name);
-
}
-
return ret;
-
}
这个函数很简单,直接找到之前关联的helper然后调用help函数.对于tftp这个helper,它的help即:tftp_help
我们看看help做了什么工作.
首先获取协议头,然后根据协议的特性来填充expt的信息.完善起来.首先是expt->tuple的填充,它除了srcport,其他就是当前ct的tuple的反转tuple。
还有把当前ct赋expt->master=ct.当然关于这个expt->tuple的dport即源端口肯能会根据具体协议重新获取,比如
ftp协议被动模式下PASV命令 响应码是227 它里面包含了ip和端口信息。然后把expt插入到之前我们提到过的expect_hash里.
我们回头看看,假如我们查找到了exp那么意味着什么呢?首先它是新建连接,但是它的目的ip和端口,也就是expt的目的ip和端口即所期望的.而建立起这个expt的ct的源ip和源端口和expt的目的ip和目的端口一样.那么意味着建立expt的ct能更快的和当前报文建立联系.也就是经常说的ct过程中一“去”一“回”快速联系起来,当然关于helper针对不同的协议还需要我们
自行写解析函数去获取想要的信息.
或许是时候该看看最后一层的处理函数了.ipv4_confirm
直接看nf_conntrack_confirm
-
/* Confirm a connection given skb; places it in hash table */
-
int
-
__nf_conntrack_confirm(struct sk_buff *skb)
函数并不复杂,利用源方向的hash和反方向的hash,查找ct全局表,为什么呢
,因为在这个报处理的过程中,可能会收到反方向的报文而建立ct.所以如果两个hash任意一个找到表里已有,则返回NF_DROP.
紧接着从unconfirm的hlist删除.设置ct->status |= IPS_CONFIRMED;
添加ct定时器.最后把来和回的tuple_hash都添加到ct全局表中.
-
static void __nf_conntrack_hash_insert(struct nf_conn *ct,
-
unsigned int hash,
-
unsigned int repl_hash)
-
{
-
struct net *net = nf_ct_net(ct);
-
-
hlist_nulls_add_head_rcu(&ct->tuplehash[IP_CT_DIR_ORIGINAL].hnnode,
-
&net->ct.hash[hash]);
-
hlist_nulls_add_head_rcu(&ct->tuplehash[IP_CT_DIR_REPLY].hnnode,
-
&net->ct.hash[repl_hash]);
-
}
到这里,整个流程已经结束了,看起来有点枯燥,后续会补上框架图.仅仅是从代码层去一窥其神秘,在代码里我们见到不少nat相关的东西,一开始我们就说了ct是nat的基础.