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);
它作为网络空间子系统注册进了内核
注册的过程中调用.init 传递给它的net参数是init_net 它是通过net_ns_init初始化到了net_namespace_list链上。
代码不是很多,核心明显是nf_conntrack_init函数
先进入nf_conntrack_init_init_net函数
nf_conntrack_htable_size 赋值和nf_conntrack_max(这个参数可以通过proc来设置.)
它和内存大小有关,大于1G的即默认为16384=16*1024=4*4k;
比如对于4G的内存,那么它的计算:
size=((1024*1024*4k )/(4*4k))/4= 1024*256/4=1024*64=1024*16*4=4*(4*4k)=4*16384
nf_conntrack_max呢?
后续是设置per-cpu变量:有兴趣的可以看看.
nf_conntrack_proto_init
初始化通用协议以及建立sysctl,并初始化nf_ct_l3protos为nf_conntrack_l3proto_generic.
nf_conntrack_init_net初始化hash链表和建立cache相关后续讨论.
先了解下基本的初始化工作后,我们从hook点说起ct是如何建立起来的
nf_conntrack_l3proto_ipv4.c
我们会发现它的优先级比较高.除了上面的钩子还有其他的:
还有nf_defrag_ipv4.c
除了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里传递给它的
用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来管理
查找是否已经有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中
首先根据协议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
找个这个结构体后,调用它节点函数获取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,不过它返回的是类型:
对于第一个包肯定为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:
这个参数我们知道就是当前tuple的反转五元组. 而查找的时候计算hash值只用到了五元组的协议号、端口 (还有一个是ipv4 or ipv6)
(跟我们之前查找ct的时候计算的hash需要的参数少了很多.) 很明显helper注册的时候也用了这样的hash算法.
回头看看tftp_helper注册的时候:
这两个值是事先给定好的. 其实发现没有,虽然很容易关联,但是也面临着冲突的问题.所以需要补全ip和端口信息
既然找到了那么如何处理呢?
help是什么呢?struct nf_conn_help *help;
nf_ct_helper_ext_add扩展ct的ext空间. 然后把找到的helper指针赋给help->helper:
那么以后我们就可以通过help = nfct_help(ct);这样的接口找到我们关联的helper了.
关于查找exp补充说明一下:
expected函数有什么作用?
当一个新的包到达init_conntrack时,就会根据包中的源地址、目的地址等信息填充一个struct nf_conn实例,通常定义为ct的变量。接下来检查当前的连接是否是另外一条已经存在连接的期望连接:
如果exp不为空,就表示当前的连接是另外一条已经存在连接的期望连接.接下来,就是expectfn的工作了:根据master的连接跟踪信息更新新建立的ct连接跟踪信息,并放到连接跟踪表中,详见nf_nat_follow_master函数(因为expectfn通常指向的nf_nat_follow_master).
skb->nfct = &ct->ct_general;
skb->nfctinfo = *ctinfo; // 对于第一次报文 值为:IP_CT_NEW
return ct;
这里依然不从框架开始说,而是从实际代码着手.
参考内核 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,
- };
点击(此处)折叠或打开
- 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;
- }
点击(此处)折叠或打开
- 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_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));
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(nf_conntrack_untracked, cpu)
点击(此处)折叠或打开
- DEFINE_PER_CPU(struct nf_conn, nf_conntrack_untracked);
- EXPORT_PER_CPU_SYMBOL(nf_conntrack_untracked);
点击(此处)折叠或打开
- 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,
- };
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,
- },
- };
记录如何生成呢?我们看报文的流程:
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_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;
- };
从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);
- }
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,
- }
找个这个结构体后,调用它节点函数获取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;
- };
先反转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)
(跟我们之前查找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]);
既然找到了那么如何处理呢?
点击(此处)折叠或打开
- if (help == NULL) {
- help = nf_ct_helper_ext_add(ct, helper, flags);
- if (help == NULL) {
- ret = -ENOMEM;
- goto out;
- }
- }
点击(此处)折叠或打开
- /* 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[];
- };
点击(此处)折叠或打开
- rcu_assign_pointer(help->helper, 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);
- }
回到主线函数:
最后把ct加入一个未认证的hlist:
并返回return &ct->tuplehash[IP_CT_DIR_ORIGINAL].
我们看如果直接找到了ct那么下面的工作很简单:设置一些状态值然后赋值skb
最后把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);
skb->nfct = &ct->ct_general;
skb->nfctinfo = *ctinfo; // 对于第一次报文 值为:IP_CT_NEW
return ct;
skb建立ct关联后,然后更新ct的状态,调用l4协议的packet函数:
以上只是简单流程的分析
通过上面的分析我们知道当我们用到ct的时候,把skb->nfct强制类型转换就可以了
虽然nfct是struct nf_conntrack *nfct;
但是我们看到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
点击(此处)折叠或打开
- 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 {
- /* 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;
- ...
- }
点击(此处)折叠或打开
- /* 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;
- }
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
这个函数很简单,直接找到之前关联的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
函数并不复杂,利用源方向的hash和反方向的hash,查找ct全局表,为什么呢 ,因为在这个报处理的过程中,可能会收到反方向的报文而建立ct.所以如果两个hash任意一个找到表里已有,则返回NF_DROP. 紧接着从unconfirm的hlist删除.设置ct->status |= IPS_CONFIRMED; 添加ct定时器.最后把来和回的tuple_hash都添加到ct全局表中.
到这里,整个流程已经结束了,看起来有点枯燥,后续会补上框架图.仅仅是从代码层去一窥其神秘,在代码里我们见到不少nat相关的东西,一开始我们就说了ct是nat的基础.
点击(此处)折叠或打开
- 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;
- }
我们看看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)
点击(此处)折叠或打开
- 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]);
- }