Chinaunix首页 | 论坛 | 博客
  • 博客访问: 299562
  • 博文数量: 52
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 587
  • 用 户 组: 普通用户
  • 注册时间: 2017-03-09 09:24
个人简介

水滴

文章分类

全部博文(52)

文章存档

2021年(3)

2019年(8)

2018年(32)

2017年(9)

我的朋友

分类: LINUX

2018-05-08 11:13:45

    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);

点击(此处)折叠或打开

  1. static int __init nf_conntrack_standalone_init(void)
  2. {
  3.     return register_pernet_subsys(&nf_conntrack_net_ops);
  4. }
它作为网络空间子系统注册进了内核

点击(此处)折叠或打开

  1. static struct pernet_operations nf_conntrack_net_ops = {
  2.     .init = nf_conntrack_net_init,
  3.     .exit = nf_conntrack_net_exit,
  4. };
注册的过程中调用.init 传递给它的net参数是init_net 它是通过net_ns_init初始化到了net_namespace_list链上。

点击(此处)折叠或打开

  1. static int nf_conntrack_net_init(struct net *net)
  2. {
  3.     int ret;

  4.     ret = nf_conntrack_init(net);
  5.     if (ret < 0)
  6.         goto out_init;
  7.     ret = nf_conntrack_standalone_init_proc(net);
  8.     if (ret < 0)
  9.         goto out_proc;
  10.     net->ct.sysctl_checksum = 1;
  11.     net->ct.sysctl_log_invalid = 0;
  12.     ret = nf_conntrack_standalone_init_sysctl(net);
  13.     if (ret < 0)
  14.         goto out_sysctl;
  15.     return 0;

  16. out_sysctl:
  17.     nf_conntrack_standalone_fini_proc(net);
  18. out_proc:
  19.     nf_conntrack_cleanup(net);
  20. out_init:
  21.     return ret;
  22. }
代码不是很多,核心明显是nf_conntrack_init函数

点击(此处)折叠或打开

  1. int nf_conntrack_init(struct net *net)
  2. {
  3.     int ret;

  4.     if (net_eq(net, &init_net)) {
  5.         ret = nf_conntrack_init_init_net();
  6.         if (ret < 0)
  7.             goto out_init_net;
  8.     }
  9.     ret = nf_conntrack_proto_init(net);
  10.     if (ret < 0)
  11.         goto out_proto;
  12.     ret = nf_conntrack_init_net(net);
  13.     if (ret < 0)
  14.         goto out_net;

  15.     if (net_eq(net, &init_net)) {
  16.         /* For use by REJECT target */
  17.         RCU_INIT_POINTER(ip_ct_attach, nf_conntrack_attach);
  18.         RCU_INIT_POINTER(nf_ct_destroy, destroy_conntrack);

  19.         /* Howto get NAT offsets */
  20.         RCU_INIT_POINTER(nf_ct_nat_offset, NULL);
  21.     }
  22.     return 0;

  23. out_net:
  24.     nf_conntrack_proto_fini(net);
  25. out_proto:
  26.     if (net_eq(net, &init_net))
  27.         nf_conntrack_cleanup_init_net();
  28. out_init_net:
  29.     return ret;
  30. }
先进入nf_conntrack_init_init_net函数
nf_conntrack_htable_size 赋值和nf_conntrack_max(这个参数可以通过proc来设置.)
它和内存大小有关,大于1G的即默认为16384=16*1024=4*4k;

点击(此处)折叠或打开

  1. nf_conntrack_htable_size
  2.             = (((totalram_pages << PAGE_SHIFT) / 16384) // 16384 =16k =16*1024=4*4k
  3.              / 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呢?

点击(此处)折叠或打开

  1. /* Use a max. factor of four by default to get the same max as
  2.          * with the old struct list_heads. When a table size is given
  3.          * we use the old value of 8 to avoid reducing the max.
  4.          * entries. */
  5.         max_factor = 4;
  6.     }
  7.     nf_conntrack_max = max_factor * nf_conntrack_htable_size;
后续是设置per-cpu变量:有兴趣的可以看看.

点击(此处)折叠或打开

  1. per_cpu(nf_conntrack_untracked, cpu)

点击(此处)折叠或打开

  1. DEFINE_PER_CPU(struct nf_conn, nf_conntrack_untracked);
  2. EXPORT_PER_CPU_SYMBOL(nf_conntrack_untracked);
nf_conntrack_proto_init

点击(此处)折叠或打开

  1. struct nf_conntrack_l4proto nf_conntrack_l4proto_generic __read_mostly =
  2. {
  3.     .l3proto        = PF_UNSPEC,
  4.     .l4proto        = 255,
  5.     .name            = "unknown",
  6.     .pkt_to_tuple        = generic_pkt_to_tuple,
  7.     .invert_tuple        = generic_invert_tuple,
  8.     .print_tuple        = generic_print_tuple,
  9.     .packet            = generic_packet,
  10.     .get_timeouts        = generic_get_timeouts,
  11.     .new            = generic_new,
  12. #if IS_ENABLED(CONFIG_NF_CT_NETLINK_TIMEOUT)
  13.     .ctnl_timeout        = {
  14.         .nlattr_to_obj    = generic_timeout_nlattr_to_obj,
  15.         .obj_to_nlattr    = generic_timeout_obj_to_nlattr,
  16.         .nlattr_max    = CTA_TIMEOUT_GENERIC_MAX,
  17.         .obj_size    = sizeof(unsigned int),
  18.         .nla_policy    = generic_timeout_nla_policy,
  19.     },
  20. #endif /* CONFIG_NF_CT_NETLINK_TIMEOUT */
  21.     .init_net        = generic_init_net,
  22.     .get_net_proto        = generic_get_net_proto,
  23. };
初始化通用协议以及建立sysctl,并初始化nf_ct_l3protos为nf_conntrack_l3proto_generic.
nf_conntrack_init_net初始化hash链表和建立cache相关后续讨论.
先了解下基本的初始化工作后,我们从hook点说起ct是如何建立起来的
nf_conntrack_l3proto_ipv4.c

点击(此处)折叠或打开

  1. /* Connection tracking may drop packets, but never alters them, so
  2.    make it the first hook. */
  3. static struct nf_hook_ops ipv4_conntrack_ops[] __read_mostly = {
  4.     {
  5.         .hook        = ipv4_conntrack_in,
  6.         .owner        = THIS_MODULE,
  7.         .pf        = NFPROTO_IPV4,
  8.         .hooknum    = NF_INET_PRE_ROUTING,
  9.         .priority    = NF_IP_PRI_CONNTRACK,
  10.     },
  11.     {
  12.         .hook        = ipv4_conntrack_local,
  13.         .owner        = THIS_MODULE,
  14.         .pf        = NFPROTO_IPV4,
  15.         .hooknum    = NF_INET_LOCAL_OUT,
  16.         .priority    = NF_IP_PRI_CONNTRACK,
  17.     },
  18.     {
  19.         .hook        = ipv4_helper,
  20.         .owner        = THIS_MODULE,
  21.         .pf        = NFPROTO_IPV4,
  22.         .hooknum    = NF_INET_POST_ROUTING,
  23.         .priority    = NF_IP_PRI_CONNTRACK_HELPER,
  24.     },
  25.     {
  26.         .hook        = ipv4_confirm,
  27.         .owner        = THIS_MODULE,
  28.         .pf        = NFPROTO_IPV4,
  29.         .hooknum    = NF_INET_POST_ROUTING,
  30.         .priority    = NF_IP_PRI_CONNTRACK_CONFIRM,
  31.     },
  32.     {
  33.         .hook        = ipv4_helper,
  34.         .owner        = THIS_MODULE,
  35.         .pf        = NFPROTO_IPV4,
  36.         .hooknum    = NF_INET_LOCAL_IN,
  37.         .priority    = NF_IP_PRI_CONNTRACK_HELPER,
  38.     },
  39.     {
  40.         .hook        = ipv4_confirm,
  41.         .owner        = THIS_MODULE,
  42.         .pf        = NFPROTO_IPV4,
  43.         .hooknum    = NF_INET_LOCAL_IN,
  44.         .priority    = NF_IP_PRI_CONNTRACK_CONFIRM,
  45.     },
  46. }
我们会发现它的优先级比较高.除了上面的钩子还有其他的:
还有nf_defrag_ipv4.c

点击(此处)折叠或打开

  1. static struct nf_hook_ops ipv4_defrag_ops[] = {
  2.     {
  3.         .hook        = ipv4_conntrack_defrag,
  4.         .owner        = THIS_MODULE,
  5.         .pf        = NFPROTO_IPV4,
  6.         .hooknum    = NF_INET_PRE_ROUTING,
  7.         .priority    = NF_IP_PRI_CONNTRACK_DEFRAG,
  8.     },
  9.     {
  10.         .hook = ipv4_conntrack_defrag,
  11.         .owner = THIS_MODULE,
  12.         .pf = NFPROTO_IPV4,
  13.         .hooknum = NF_INET_LOCAL_OUT,
  14.         .priority = NF_IP_PRI_CONNTRACK_DEFRAG,
  15.     },
  16. };
除了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里传递给它的

点击(此处)折叠或打开

  1. static unsigned int ipv4_conntrack_defrag(unsigned int hooknum,
  2.                      struct sk_buff *skb,
  3.                      const struct net_device *in,
  4.                      const struct net_device *out,
  5.                      int (*okfn)(struct sk_buff *))
  6. {
  7.     struct sock *sk = skb->sk;
  8.     struct inet_sock *inet = inet_sk(skb->sk);

  9.     if (sk && (sk->sk_family == PF_INET) &&
  10.      inet->nodefrag)
  11.         return NF_ACCEPT;

  12. #if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
  13. #if !defined(CONFIG_NF_NAT) && !defined(CONFIG_NF_NAT_MODULE)
  14.     /* Previously seen (loopback)? Ignore. Do this before
  15.      fragment check. */
  16.     if (skb->nfct && !nf_ct_is_template((struct nf_conn *)skb->nfct))
  17.         return NF_ACCEPT;
  18. #endif
  19. #endif
  20.     /* Gather fragments. */
  21.     if (ip_is_fragment(ip_hdr(skb))) {
  22.         enum ip_defrag_users user = nf_ct_defrag_user(hooknum, skb); // IP_DEFRAG_CONNTRACK_IN
  23.         if (nf_ct_ipv4_gather_frags(skb, user))
  24.             return NF_STOLEN;
  25.     }
  26.     return NF_ACCEPT;
  27. }
用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来管理

点击(此处)折叠或打开

  1. /* Describe an entry in the "incomplete datagrams" queue. */
  2. struct ipq {
  3.     struct inet_frag_queue q;

  4.     u32        user;
  5.     __be32        saddr;
  6.     __be32        daddr;
  7.     __be16        id;
  8.     u8        protocol;
  9.     u8        ecn; /* RFC3168 support */
  10.     int iif;
  11.     unsigned int rid;
  12.     struct inet_peer *peer;
  13. };
查找是否已经有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中 

点击(此处)折叠或打开

  1. static unsigned int ipv4_conntrack_in(unsigned int hooknum,
  2.                  struct sk_buff *skb,
  3.                  const struct net_device *in,
  4.                  const struct net_device *out,
  5.                  int (*okfn)(struct sk_buff *))
  6. {
  7.     return nf_conntrack_in(dev_net(in), PF_INET, hooknum, skb);
  8. }
首先根据协议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层相关的.

点击(此处)折叠或打开

  1. struct nf_conntrack_l3proto nf_conntrack_l3proto_ipv4 __read_mostly = {
  2.     .l3proto     = PF_INET,
  3.     .name         = "ipv4",
  4.     .pkt_to_tuple     = ipv4_pkt_to_tuple,
  5.     .invert_tuple     = ipv4_invert_tuple,
  6.     .print_tuple     = ipv4_print_tuple,
  7.     .get_l4proto     = ipv4_get_l4proto,
  8. #if defined(CONFIG_NF_CT_NETLINK) || defined(CONFIG_NF_CT_NETLINK_MODULE)
  9.     .tuple_to_nlattr = ipv4_tuple_to_nlattr,
  10.     .nlattr_tuple_size = ipv4_nlattr_tuple_size,
  11.     .nlattr_to_tuple = ipv4_nlattr_to_tuple,
  12.     .nla_policy     = ipv4_nla_policy,
  13. #endif
  14. #if defined(CONFIG_SYSCTL) && defined(CONFIG_NF_CONNTRACK_PROC_COMPAT)
  15.     .ctl_table_path = "net/ipv4/netfilter",
  16. #endif
  17.     .init_net     = ipv4_init_net,
  18.     .me         = THIS_MODULE,
  19. }
这个很重要的结构体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,不过它返回的是类型:

点击(此处)折叠或打开

  1. /* Connections have two entries in the hash table: one for each way */
  2. struct nf_conntrack_tuple_hash {
  3.     struct hlist_nulls_node hnnode;
  4.     struct nf_conntrack_tuple tuple;
  5. };
对于第一个包肯定为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:

点击(此处)折叠或打开

  1. __nf_ct_helper_find(&ct->tuplehash[IP_CT_DIR_REPLY].tuple)
这个参数我们知道就是当前tuple的反转五元组. 而查找的时候计算hash值只用到了五元组的协议号、端口 (还有一个是ipv4 or ipv6)
(跟我们之前查找ct的时候计算的hash需要的参数少了很多.) 很明显helper注册的时候也用了这样的hash算法.
回头看看tftp_helper注册的时候:

点击(此处)折叠或打开

  1. tftp[i][j].tuple.dst.protonum = IPPROTO_UDP;
  2.             tftp[i][j].tuple.src.u.udp.port = htons(ports[i]);
这两个值是事先给定好的. 其实发现没有,虽然很容易关联,但是也面临着冲突的问题.所以需要补全ip和端口信息
既然找到了那么如何处理呢?

点击(此处)折叠或打开

  1. if (help == NULL) {
  2.         help = nf_ct_helper_ext_add(ct, helper, flags);
  3.         if (help == NULL) {
  4.             ret = -ENOMEM;
  5.             goto out;
  6.         }
  7.     }
help是什么呢?struct nf_conn_help *help;

点击(此处)折叠或打开

  1. /* nf_conn feature for connections that have a helper */
  2. struct nf_conn_help {
  3.     /* Helper. if any */
  4.     struct nf_conntrack_helper __rcu *helper;

  5.     struct hlist_head expectations;

  6.     /* Current number of expected connections */
  7.     u8 expecting[NF_CT_MAX_EXPECT_CLASSES];

  8.     /* private helper information. */
  9.     char data[];
  10. };
nf_ct_helper_ext_add扩展ct的ext空间. 然后把找到的helper指针赋给help->helper:

点击(此处)折叠或打开

  1. rcu_assign_pointer(help->helper, helper);
那么以后我们就可以通过help = nfct_help(ct);这样的接口找到我们关联的helper了.
关于查找exp补充说明一下:
expected函数有什么作用?
当一个新的包到达init_conntrack时,就会根据包中的源地址、目的地址等信息填充一个struct nf_conn实例,通常定义为ct的变量。接下来检查当前的连接是否是另外一条已经存在连接的期望连接:

点击(此处)折叠或打开

  1. spin_lock_bh(&nf_conntrack_lock);
  2.     exp = nf_ct_find_expectation(net, zone, tuple);
  3.     if (exp) {
  4.         pr_debug("conntrack: expectation arrives ct=%p exp=%p\n",
  5.              ct, exp);
  6.         /* Welcome, Mr. Bond. We've been expecting you... */
  7.         __set_bit(IPS_EXPECTED_BIT, &ct->status);
  8.         ct->master = exp->master;
  9.         if (exp->helper) {
  10.             help = nf_ct_helper_ext_add(ct, exp->helper,
  11.                          GFP_ATOMIC);
  12.             if (help)
  13.                 rcu_assign_pointer(help->helper, exp->helper);
  14.         }

  15. #ifdef CONFIG_NF_CONNTRACK_MARK
  16.         ct->mark = exp->master->mark;
  17. #endif
  18. #ifdef CONFIG_NF_CONNTRACK_SECMARK
  19.         ct->secmark = exp->master->secmark;
  20. #endif
  21.         nf_conntrack_get(&ct->master->ct_general);
  22.         NF_CT_STAT_INC(net, expect_new);
  23.     }
如果exp不为空,就表示当前的连接是另外一条已经存在连接的期望连接.接下来,就是expectfn的工作了:根据master的连接跟踪信息更新新建立的ct连接跟踪信息,并放到连接跟踪表中,详见nf_nat_follow_master函数(因为expectfn通常指向的nf_nat_follow_master).
回到主线函数:
最后把ct加入一个未认证的hlist:

点击(此处)折叠或打开

  1. /* Overload tuple linked list to put us in unconfirmed list. */
  2.     hlist_nulls_add_head_rcu(&ct->tuplehash[IP_CT_DIR_ORIGINAL].hnnode,
  3.          &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函数:

点击(此处)折叠或打开

  1. ret = l4proto->packet(ct, skb, dataoff, ctinfo, pf, hooknum, timeouts);
以上只是简单流程的分析
通过上面的分析我们知道当我们用到ct的时候,把skb->nfct强制类型转换就可以了
虽然nfct是struct nf_conntrack *nfct;

点击(此处)折叠或打开

  1. #if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
  2. struct nf_conntrack {
  3.     atomic_t use;
  4. };
  5. #endif
但是我们看到struct nf_conn的结构体

点击(此处)折叠或打开

  1. struct nf_conn {
  2.     /* Usage count in here is 1 for hash table/destruct timer, 1 per skb,
  3.            plus 1 for any connection(s) we are `master' for */
  4.     struct nf_conntrack ct_general;
  5. ...
  6. }
我们是不是明白了为什么skb->nfct那么使用。新的内核也提供了接口:

点击(此处)折叠或打开

  1. /* Return conntrack_info and tuple hash for given skb. */
  2. static inline struct nf_conn *
  3. nf_ct_get(const struct sk_buff *skb, enum ip_conntrack_info *ctinfo)
  4. {
  5.     *ctinfo = skb->nfctinfo;
  6.     return (struct nf_conn *)skb->nfct;
  7. }
连接追踪用结构体 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

点击(此处)折叠或打开

  1. static unsigned int ipv4_helper(unsigned int hooknum,
  2.                 struct sk_buff *skb,
  3.                 const struct net_device *in,
  4.                 const struct net_device *out,
  5.                 int (*okfn)(struct sk_buff *))
  6. {
  7.     struct nf_conn *ct;
  8.     enum ip_conntrack_info ctinfo;
  9.     const struct nf_conn_help *help;
  10.     const struct nf_conntrack_helper *helper;
  11.     unsigned int ret;

  12.     /* This is where we call the helper: as the packet goes out. */
  13.     ct = nf_ct_get(skb, &ctinfo);
  14.     if (!ct || ctinfo == IP_CT_RELATED_REPLY)
  15.         return NF_ACCEPT;

  16.     help = nfct_help(ct);
  17.     if (!help)
  18.         return NF_ACCEPT;

  19.     /* rcu_read_lock()ed by nf_hook_slow */
  20.     helper = rcu_dereference(help->helper);
  21.     if (!helper)
  22.         return NF_ACCEPT;

  23.     ret = helper->help(skb, skb_network_offset(skb) + ip_hdrlen(skb),
  24.              ct, ctinfo);
  25.     if (ret != NF_ACCEPT && (ret & NF_VERDICT_MASK) != NF_QUEUE) {
  26.         nf_log_packet(NFPROTO_IPV4, hooknum, skb, in, out, NULL,
  27.              "nf_ct_%s: dropping packet", helper->name);
  28.     }
  29.     return ret;
  30. }
这个函数很简单,直接找到之前关联的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

点击(此处)折叠或打开

  1. /* Confirm a connection given skb; places it in hash table */
  2. int
  3. __nf_conntrack_confirm(struct sk_buff *skb)
函数并不复杂,利用源方向的hash和反方向的hash,查找ct全局表,为什么呢 ,因为在这个报处理的过程中,可能会收到反方向的报文而建立ct.所以如果两个hash任意一个找到表里已有,则返回NF_DROP.  紧接着从unconfirm的hlist删除.设置ct->status |= IPS_CONFIRMED; 添加ct定时器.最后把来和回的tuple_hash都添加到ct全局表中.

点击(此处)折叠或打开

  1. static void __nf_conntrack_hash_insert(struct nf_conn *ct,
  2.                  unsigned int hash,
  3.                  unsigned int repl_hash)
  4. {
  5.     struct net *net = nf_ct_net(ct);

  6.     hlist_nulls_add_head_rcu(&ct->tuplehash[IP_CT_DIR_ORIGINAL].hnnode,
  7.              &net->ct.hash[hash]);
  8.     hlist_nulls_add_head_rcu(&ct->tuplehash[IP_CT_DIR_REPLY].hnnode,
  9.              &net->ct.hash[repl_hash]);
  10. }
到这里,整个流程已经结束了,看起来有点枯燥,后续会补上框架图.仅仅是从代码层去一窥其神秘,在代码里我们见到不少nat相关的东西,一开始我们就说了ct是nat的基础.
阅读(2275) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~