Linux协议栈-netfilter(4)-期望连接

标签: Linux  netfilter

文章转载--------------------点击打开链接

传统的conntrack和NAT处理只对IP层和传输层头部进行转换处理,但是一些应用层协议,在协议数据报文中包含了地址信息。为了使得这些应用也能透明地完成NAT转换,NAT使用一种称作ALG的技术,它能对这些应用程序在通信时所包含的地址信息也进行相应的NAT转换。

例如:对于FTP协议的PORT/PASV命令,在数据包载荷中需要包含地址信息,并且数据包如果需要做NAT,那应用层数据部分的地址也需要做NAT转换。实现这种转换,需要在普通的conntrack条目基础上增加一个expect conntrack(期望连接)来记录这个连接上的额外信息。

本文将以tftp和ftp协议举例期望连接。

1. 以tftp举例期望连接

tftp是基于UDP的应用层协议,用于简单的文件传输。

1.1 实验拓扑

测试拓扑如下,在路由器R1上通过tftp请求PC1上的文件“ethreg.sh”,R1和PC1之间有一个路由器R2让R1位于局域网中,PC1位于公网中。在R1上输入以下命令:

tftp –gr ethreg.sh 192.168.10.100

在PC1上抓包结果如下:


第一个数据包为read request请求,数据包方向为R1->PC1(192.168.1.1:50173-> 192.168.10.100:69),其中69中tftp端口号,数据包传输过程经过了SNAT转换。

数据部分共经过两次传输完成,发送数据方向为PC1->R1(192.168.10.100:3873-> 192.168.1.1:50173),数据包在传输过程经过了DNAT转换。

可见传输数据时,第一个包是从Server端即PC1发出的,而PC1处在NAT外面,而通常情况下,这样的数据包是无法通过NAT达到局域网的,因为路由器R2上并没有记录这个连接做NAT的连接条目,NAT的作用之一就是隐藏局域网中的主机。

下面来看一下tftp协议如何依靠期望连接实现数据包的NAT转换的。

1.2 tftp协议的期望连接实现

在编译内核时打开CONFIG_NF_CONNTRACK_TFTP宏,使其以模块形式编译到内核,来支持tftp建立期望连接。该模块的init函数为nf_conntrack_tftp_init(),它初始化了一个conntrack helper方法,并将其注册到helper extension的链表中去(conntrack extensions见前面的博文)。

static struct nf_conntrack_helper tftp[MAX_PORTS][2]__read_mostly;

tftp conntrack模块的初始化函数:

  1. static int __init nf_conntrack_tftp_init(void)
  2. {
  3. int i, j, ret;
  4. char *tmpname;
  5. if (ports_c == 0)
  6. ports[ports_c++] = TFTP_PORT;
  7. for (i = 0; i < ports_c; i++) {
  8. memset(&tftp[i], 0, sizeof(tftp[i]));
  9. tftp[i][0].tuple.src.l3num = AF_INET;
  10. tftp[i][1].tuple.src.l3num = AF_INET6;
  11. for (j = 0; j < 2; j++) {
  12. tftp[i][j].tuple.dst.protonum = IPPROTO_UDP;
  13. tftp[i][j].tuple.src.u.udp.port = htons(ports[i]);
  14. tftp[i][j].expect_policy = &tftp_exp_policy;
  15. tftp[i][j].me = THIS_MODULE;
  16. tftp[i][j].help = tftp_help;
  17. tmpname = &tftp_names[i][j][0];
  18. if (ports[i] == TFTP_PORT)
  19. sprintf(tmpname, "tftp");
  20. else
  21. sprintf(tmpname, "tftp-%u", i);
  22. tftp[i][j].name = tmpname;
  23. ret = nf_conntrack_helper_register(&tftp[i][j]);
  24. if (ret) {
  25. printk("nf_ct_tftp: failed to register helper "
  26. "for pf: %u port: %u\n",
  27. tftp[i][j].tuple.src.l3num, ports[i]);
  28. nf_conntrack_tftp_fini();
  29. return ret;
  30. }
  31. }
  32. }
  33. return 0;
  34. }

tftp_exp_policy的定义如下:

  1. static const struct nf_conntrack_expect_policy tftp_exp_policy = {
  2. .max_expected = 1,
  3. .timeout = 5 * 60,
  4. };

在init函数中对nf_conntrack_helper结构体的tuple进行了部分初始化,由于tftp数据包的协议和port是固定的,所以可以预先赋值。同时注册了help函数为tftp_help(),函数定义如下,注意,参数skb是当前处理的数据包,ct是相应的conntrack,处理tftp read/write request的时候会进入这个函数。

  1. static int tftp_help(struct sk_buff *skb,
  2. unsigned int protoff,
  3. struct nf_conn *ct,
  4. enum ip_conntrack_info ctinfo)
  5. {
  6. const struct tftphdr *tfh;
  7. struct tftphdr _tftph;
  8. struct nf_conntrack_expect *exp;
  9. struct nf_conntrack_tuple *tuple;
  10. unsigned int ret = NF_ACCEPT;
  11. typeof(nf_nat_tftp_hook) nf_nat_tftp;
  12. /* 获得tftp首部 */
  13. tfh = skb_header_pointer(skb, protoff + sizeof(struct udphdr),
  14. sizeof(_tftph), &_tftph);
  15. if (tfh == NULL)
  16. return NF_ACCEPT;
  17. /* 从tftp首部中获得操作码 */
  18. switch (ntohs(tfh->opcode)) {
  19. case TFTP_OPCODE_READ: /* read请求 */
  20. case TFTP_OPCODE_WRITE: /* write请求 */
  21. /* RRQ and WRQ works the same way */
  22. /*
  23. 在nf_ct_expect_cachep上分配一个expect连接,同时赋两个值:
  24. exp->master = ct,
  25. exp->use = 1。
  26. */
  27. exp = nf_ct_expect_alloc(ct);
  28. if (exp == NULL)
  29. return NF_DROP;
  30. /* 根据ct初始化expect */
  31. tuple = &ct->tuplehash[IP_CT_DIR_REPLY].tuple;
  32. nf_ct_expect_init(exp, NF_CT_EXPECT_CLASS_DEFAULT,
  33. nf_ct_l3num(ct),
  34. &tuple->src.u3, &tuple->dst.u3,
  35. IPPROTO_UDP, NULL, &tuple->dst.u.udp.port);
  36. /* 指向help() -- nf_nat_tftp.c */
  37. nf_nat_tftp = rcu_dereference(nf_nat_tftp_hook);
  38. /* 数据包需要走NAT时,if成立,局域网传输则else成立。 */
  39. if (nf_nat_tftp && ct->status & IPS_NAT_MASK)
  40. ret = nf_nat_tftp(skb, ctinfo, exp);
  41. else if (nf_ct_expect_related(exp) != 0)
  42. ret = NF_DROP;
  43. nf_ct_expect_put(exp);
  44. break;
  45. case TFTP_OPCODE_DATA: /* 数据 */
  46. case TFTP_OPCODE_ACK: /* 数据的ACK */
  47. pr_debug("Data/ACK opcode\n");
  48. break;
  49. case TFTP_OPCODE_ERROR:
  50. pr_debug("Error opcode\n");
  51. break;
  52. default:
  53. pr_debug("Unknown opcode\n");
  54. }
  55. return ret;
  56. }

tftp_help()的工作有两部分:

1.   根据数据包的ct初始化一个expect连接,由于help函数是在ipv4_confirm()时调用的,所以ct是存在的。另外需要说明一点,tftp请求(读或写)只能从client到server,所以,如果要走NAT,tftp请求包的方向一定是从内网到外网的。

按照上面的例子,当前ct为:

ORGINAL tuple: 17 192.168.10.1:58747 -> 192.168.10.100:69

REPLY tuple: 17 192.168.10.100:69 -> 192.168.10.1:58747

生成的expect为:

ORGINAL tuple: 17 192.168.10.100:0 -> 192.168.10.1:58747

同时,exp->master = ct。注意,expect只有一个tuple,即只有一个方向,这里只看到ORIGNAL方向的tuple,只是因为tuple的dir没赋值,默认为0。

2.   如果ct做了NAT,就调用nf_nat_tftp指向的函数,这里它指向nf_nat_tftp.c中的help()函数。

  1. static unsigned int help(struct sk_buff *skb,
  2. enum ip_conntrack_info ctinfo,
  3. struct nf_conntrack_expect *exp)
  4. {
  5. const struct nf_conn *ct = exp->master;
  6. exp->saved_proto.udp.port
  7. = ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple.src.u.udp.port;
  8. exp->dir = IP_CT_DIR_REPLY;
  9. exp->expectfn = nf_nat_follow_master;
  10. if (nf_ct_expect_related(exp) != 0) /* 插入链表 */
  11. return NF_DROP;
  12. return NF_ACCEPT;
  13. }

这个函数进一步初始化了expect,同时将这个expect插入struct nf_conn_help结构的链表以及全局的期望连接链表expect_hash中:

  1. static void nf_ct_expect_insert(struct nf_conntrack_expect *exp)
  2. {
  3. /* 获得exp->master的help */
  4. struct nf_conn_help *master_help = nfct_help(exp->master);
  5. struct net *net = nf_ct_exp_net(exp);
  6. const struct nf_conntrack_expect_policy *p;
  7. unsigned int h = nf_ct_expect_dst_hash(&exp->tuple);
  8. atomic_inc(&exp->use);
  9. /* 插入到help->expectations链表 */
  10. hlist_add_head(&exp->lnode, &master_help->expectations);
  11. master_help->expecting[exp->class]++;
  12. /* 插入到全局的expect_hash表 */
  13. hlist_add_head_rcu(&exp->hnode, &net->ct.expect_hash[h]);
  14. net->ct.expect_count++;
  15. /* 设置并启动定时器 */
  16. setup_timer(&exp->timeout, nf_ct_expectation_timed_out,
  17. (unsigned long)exp);
  18. p = &master_help->helper->expect_policy[exp->class];
  19. exp->timeout.expires = jiffies + p->timeout * HZ;
  20. add_timer(&exp->timeout);
  21. atomic_inc(&exp->use);
  22. NF_CT_STAT_INC(net, expect_create);
  23. }

我们还注意到在help()函数中将exp-> expectfn赋值为nf_nat_follow_master(),这个函数的作用在后面会提到。

上面的内容是在客户端发送tftp请求后触发的动作,主要的效果就是生成了一个期望连接并可以被使用了。下面以请求读数据来看一下传输数据时的数据包变化。

当tftp请求包进入nf_conntrack_in的时候,由于没有ct条目,所以调用init_conntrack()尝试新建一个条目,在这个函数中,根据skb新建两个方向的tuple,之后有这样的代码:
  1. spin_lock_bh(&nf_conntrack_lock);
  2. exp = nf_ct_find_expectation(net, tuple);
  3. /* 如果在期望连接链表中 */
  4. if (exp) {
  5. __set_bit(IPS_EXPECTED_BIT, &ct->status);
  6. ct->master = exp->master;
  7. if (exp->helper) {
  8. help = nf_ct_helper_ext_add(ct, GFP_ATOMIC);
  9. if (help)
  10. rcu_assign_pointer(help->helper, exp->helper);
  11. }
  12. nf_conntrack_get(&ct->master->ct_general);
  13. NF_CT_STAT_INC(net, expect_new);
  14. } else {
  15. __nf_ct_try_assign_helper(ct, GFP_ATOMIC);
  16. NF_CT_STAT_INC(net, new);
  17. }
  18. hlist_nulls_add_head_rcu(&ct->tuplehash[IP_CT_DIR_ORIGINAL].hnnode,
  19. &net->ct.unconfirmed);
  20. spin_unlock_bh(&nf_conntrack_lock);
  21. if (exp) {
  22. if (exp->expectfn)
  23. exp->expectfn(ct, exp);
  24. nf_ct_expect_put(exp);
  25. }
  26. return &ct->tuplehash[IP_CT_DIR_ORIGINAL];

这里会在全局的期望连接链表expect_hash中查找是否有匹配新建tuple的期望连接。第一次过来的数据包肯定是没有的,于是走else分支,__nf_ct_try_assign_helper()函数去nf_ct_helper_hash哈希表中匹配当前tuple,由于我们在本节开头提到nf_conntrack_tftp_init()已经把tftp的helper extension添加进去了,所以可以匹配成功,于是把找到的helper赋值给nfct_help(ct)->helper,而这个helper的help方法就是tftp_help()。

当tftp请求包走到ipv4_confirm的时候,会去执行这个help方法,即tftp_help(),也就是建立一个期望连接。

当后续tftp传输数据时,在nf_conntrack_in里面,新建tuple后,在expect_hash表中查可以匹配到新建tuple的期望连接(因为只根据源端口来匹配),因此上面代码的if成立,所以ct->master被赋值为exp->master,并且,还会执行exp->expectfn()函数,这个函数上面提到是指向nf_nat_follow_master()的,该函数根据ct的master来给ct做NAT,ct在经过这个函数处理前后的tuple分别为:

before expectfn:

ORGINAL : tuple 83b38338: 17 192.168.10.100:3873 ->192.168.10.1:50173

REPLY : tuple 83b38368: 17 192.168.10.1:50173 ->192.168.10.100:3873

after expectfn:

ORGINAL : tuple 83b38338: 17 192.168.10.100:3873 ->192.168.10.1:50173

REPLY : tuple 83b38368: 17192.168.5.1:50173 -> 192.168.10.100:3873

在ipv4_confirm中,由于ct没有期望连接,所以跳过helper有关的代码直接调用nf_conntrack_confirm()函数。

其他相关问题:

1.   数据传输时的服务器端的端口时谁指定的:

服务器端指定的。在从server->client传数据时,server收到tftp read request之后,就直接发送数据,数据从block1开始计数。而从client->server传数据时,server收到tftp write request之后,先回复一个tftp ack(标记为block0),包中携带server打开的端口号,client便知道使用的目的端口,然后client开始发送数据,数据从block1开始计数。

2.   期望连接的匹配原则:

添加到net->ct.expect_hash[]中的hash规则为nf_ct_expect_dst_hash(&exp->tuple),在查找的时候使用同样的hash规则。

__nf_ct_helper_find(tuple)函数用于在nf_ct_helper_hash[]表中查找匹配的structnf_conntrack_helper结构。过程中不匹配tuple->src.u3,即不匹配源IP地址,需要匹配的是源端口、L3协议号和L4协议号,dir和目的port也需要不匹配。当然,在对tuple进行hash的时候,也只用到了源端口、L3协议号和L4协议号。所以,对于下面的tuple,只比较红色部分:

REPLY : tuple 83a5d5b0: 17192.168.10.100:69 -> 192.168.5.1:40240

nf_ct_find_expectation(net, tuple)函数用于在net->ct.expect_hash[]查找是否有和tuple匹配的连接跟踪,匹配函数为nf_ct_tuple_mask_cmp(tuple,&exp->tuple, &exp->mask),要求tuple中所有条件都符合,而由于expect连接的源端口是0。不过没关系,源端口的mask被设置为0,所以相当于不匹配该项(可参考创建expecttuple的函数nf_ct_expect_init是如何根据源端口设置mask的)。

待匹配tuple:ORGINAL : tuple 801efc1c: 17 192.168.10.100:4593-> 192.168.10.1:40240

expect tuple:ORGINAL : tuple 839dd908: 17 192.168.10.100:0 ->192.168.10.1:40240

我们现在可以来整理一下连接上的tuple和expect tuple之间的关系,

上图浅绿色内容是模块初始化时注册的,黄色内容是处理tftp请求包时初始化的,蓝色的数组是全局hash表。tftp数据包就根据上面的内容进行NAT转换。

注意,在nf_ct_find_expectation()查找期望连接时,如果找到了,就把该期望连接从struct nf_conn_help结构的链表以及全局的期望连接链表expect_hash中删除。在expect_hash中的连接通过设置的超时时间来超时。

2. 以ftp协议举例期望连接

有些应用层协议字段需要携带IP和PORT信息,例如ftp协议,它是基于TCP的文件传输协议,FTP 使用TCP 生成一个虚拟连接用于控制信息,然后再生成一个单独的TCP 连接用于数据传输。而简单的NAT转换并不关注TCP的数据部分,所以需要额外的helper和expect连接来让外网的服务器看到的源IP确实是WAN口的IP并且使用于传输数据的TCP连接上的数据包可以进行NAT转换。

测试拓扑如下图:


在PC1上抓包,在Router上查看连接信息。

在ftp客户端获取服务器上的do_exit-tty.log.rar文件:

  1. [[email protected] build]# ftp 172.31.1.23
  2. ftp> passive
  3. ftp> get do_exit-tty.log.rar
  4. ftp> disconnect
  5. ftp> quit

下图是ftp在PORT(主动)模式下get服务器上的文件的抓包,其中黄色框部分是用于交互控制信息的TCP连接建立连接和断开连接的过程,绿色框部分是用于传输数据而建立的TCP连接。从红色框的数据包可以看到在ftp头部中包含了请求方的IP地址,而这个地址已经被转换成PC1的WAN口地址。


从上图可以看到,传输数据的TCP连接是从服务器端发起的,所以如果没有期望连接,路由器就不知道如何进行DNAT转换。

用于ftp控制信息的TCP连接的conntrack信息为:

ORGINAL : tuple 83b13580: 6 192.168.5.103:1757 ->172.31.1.23:21

REPLY : tuple 83b135b0: 6 172.31.1.23:21 ->172.31.70.42:1757

数据传输的TCP连接便是使用控制信息的TCP连接来生成期望连接的,并且将其作为期望连接的master。生成的期望连接为:

ORGINAL : tuple 83b1c9b8: 6 172.31.1.23:0 ->172.31.70.42:1762

实际机型NAT转换的函数依然是exp->expectfn(),并且仍然指向nf_nat_follow_master()函数,该函数根据ct的master来给ct做NAT,ct在经过这个函数处理前后的tuple分别为:

before expectfn:

ORGINAL : tuple 83b22ee8: 6 172.31.1.23:20 ->172.31.70.42:1762

REPLY : tuple 83b22f18: 6 172.31.70.42:1762 ->172.31.1.23:20

after expectfn:

ORGINAL : tuple 83b22ee8: 6 172.31.1.23:20 ->172.31.70.42:1762

REPLY : tuple 83b22f18: 6 192.168.5.103:1762 ->172.31.1.23:20

系统中的期望连接条目记录在proc文件/proc/net/nf_conntrack_expect中,显示文件内容的函数为exp_seq_show()。
原文链接:加载失败,请重新获取