绿盟科技--www.nsfocus.com--绿盟月刊

来源:百度文库 编辑:神马文学网 时间:2024/10/02 16:45:14



绿盟安全月刊->第42期->技术专题
期刊号: --全部--第57期第56期第55期第54期第53期第52期第51期第50期第49期第48期第47期第46期第45期第44期第43期第42期第41期第40期第39期第38期第37期第36期第35期第34期第33期第32期第31期第30期第29期第28期第27期第26期第25期第24期第23期第22期第21期第20期第19期第18期第17期第16期第15期第14期第13期第12期第11期第10期第9期第8期第7期第6期第5期第4期第3期第2期第1期 类型: --全部--业界动态安全新闻最新漏洞技术专题工具介绍安全文摘专题报道安全资源 关键词:
LINUX2.4.x 连接跟踪和地址转换
作者:林风
出处:http://www.nsfocus.com
日期:2003-07-02
本文是上一篇文章《LINUX2.4.x网络安全框架》的后续文章,主要分析连接跟踪和地址转换在LINUX2.4.x中的实现,本文引用代码是LINUX2.4.20。
1.      概述
在上一篇文章中我们提到LINUX2.4.x的网络安全实现较之LINUX2.2.x有了很大的改进,其中的两个重要改进一是增加了连接跟踪的功能,二是增强了地址转换功能的实现。由于地址转换功能是在连接跟踪的基础上实现的,所以把这两个模块放在一起来分析。
连接跟踪(CONNTRACK),顾名思义,就是跟踪并且记录连接状态。这里的连接并不仅仅指TCP连接,它也包括UDP、ICMP等协议的虚拟连接。连接跟踪是实现地址转换的基础,在使用地址转换功能时必须加载这个模块。
增强的地址转换实现了一个全功能的地址转换模块。在LINUX2.2.x中只有地址伪装和透明代理,目的转换还是在地址伪装基础上实现的。在LINUX2.4.x中,源转换(SNAT)包括了地址伪装和源地址转换,目的转换(DNAT)包括了透明代理和目的地址转换,并且这些功能是在同一个框架下实现的,代码结构比以前清晰,可扩展性更强。
2.      检查点以及检查点上的函数
先来看看连接跟踪和地址转换在检查点上注册了相应的结构,如图:

[图1.1  IPV4的功能点在各检查点上注册的结构]
在图中,我们可以看到不同的数据包所要经过的三条路径:
·发往本机上层的包:经过的检查点是NF_IP_PRE_ROUTING,NF_IP_LOCAL_IN。
·由本机转发的包:经过的检查点是NF_IP_PRE_ROUTING,NF_IP_FORWARD,NF_IP_POST_ROUTING。
·从本机发出的包:NF_IP_LOCAL_OUT,NF_IP_POST_ROUTING。
在每一条路径上,连接跟踪和地址转换都注册了相应的函数,并创建与之相关的数据结构,完成其功能。每条路径上功能点的顺序如图所示:

[图1.2 路径上功能点的顺序]
在这个图中,CONNTRACK代表连接跟踪,DNAT代表目的转换,SNAT代表源转换,FILTER代表包过虑。可以看到,CONNTRACK在路径上出现了两次,其作用是:第一个点上它创建连接跟踪的结构,这个结构会在后面的地址转换和包过滤中被使用,在第二个点上它将连接跟踪的结构加到系统的连接表中。
检查点上注册的函数如下图所示:

[图1.3 连接跟踪和地址转换在检查点上调用的函数]
图中用粗体字标识连接跟踪的函数,用斜体字标识地址转换的函数,括号里面的函数名是实际调用的函数的名称。这些函数在检查点上被调用。下面将分析这些函数的实现以及其中所涉及的重要数据结构和过程。
3.      连接跟踪的重要数据结构和过程
连接跟踪记录连接的状态,并且实现状态间的转换。连接并不只是指TCP协议连接,它也包括UDP协议和ICMP协议。当然这只是内核的标准实现中包含的协议,其他协议的连接跟踪可以自己添加到内核中。连接的数据结构如下:
struct ip_conntrack
{
struct nf_conntrack ct_general;  /* 结构的引用计数 */
struct ip_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX]; /* 不同方向上的计算哈希值的参数*/
volatile unsigned long status;                  /* 结构的状态 */
struct timer_list timeout;
struct list_head sibling_list;                    /* 与此结构关联的expect链表*/
unsigned int expecting;              /* 与此结构关联的expect的数量*/
struct ip_conntrack_expect *master;     /* 指向创建此结构的expect */
struct ip_conntrack_helper *helper;       /* 与此结构关联的helper */
struct nf_ct_info infos[IP_CT_NUMBER];       /* 不同状态下由skb引用 */
union ip_conntrack_proto proto;
union ip_conntrack_help help;
struct {
struct ip_nat_info info;
union ip_conntrack_nat_help help;
int masq_index;
} nat;                                                    /* 与地址转换相关的结构 */
};
在这个结构中tuplehash[]记录了此连接的正反两个方向包的参数,这些参数包括协议、源地址/源端口、目的地址/目的端口等,如果没有端口,比如ICMP协议,就用它的id,type,code等代替。每个连接都根据这些参数计算哈希值并连接到一个全局的哈希表中。连接是双向的,所以每个连接在哈希表中出现两次,匹配连接和应答两个方向上的包。
前面说到,对不同协议,连接跟踪记录的参数不同,所以不同的协议定义了不同的 ip_conntrack_protocol结构来处理与协议相关的内容。这些结构被注册到一个全局的链表中,在使用时根据协议去查找,并调用相应的处理函数来完成相应的动作。下面将要讲到的ip_nat_protocol的作用与此类似,它处理的是与协议相关的地址转换方面的内容。
不同协议连接跟踪的过程不同。
TCP的连接跟踪可以用下面的表来说明:(假设是A到B的TCP连接)
发包的方向    标志位   连接状态           说明
A--> B        SYN      IP_CT_NEW          A发起连接
B-->A         SYN/ACK  IP_CT_ESTABLISHED  B响应A的连接请求
+ IP_CT_IS_REPLY
A-->B         ACK      IP_CT_ESTABLISHED  A确认B的响应
[表2.1 TCP建立连接的状态变迁]
这个表说明的是创建TCP连接所经历的状态变迁。表中没有显示删除TCP连接所经历的状态,这是因为删除TCP连接是通过超时执行的,任何状态下都可以删除连接,所以并不需要额外的状态变迁。
对TCP连接,还有一个TCP的状态变迁。它和连接的状态不同,它记录了TCP协议的状态转换,其中涉及更多与TCP连接相关的状态。并且超时也是在这个状态基础上赋值的。具体的实现可以参考源代码。
除了上面列出的状态外,还有一个重要的状态IP_CT_RELATED。这个状态用于创建动态连接时使用。动态连接有两类,一是处理使用了动态地址或端口的应用协议;二是针对TCP、UDP、ICMP等协议的ICMP差错报文。动态连接与原连接相关,所以用了一个特殊的状态来标识它们。
UDP的连接跟踪可以用下面的表来说明:
发包的方向  连接状态            说明
A-->B       IP_CT_NEW           A到B的UPD包
B-->A       IP_CT_ESTABLISHED   B到A的UDP包(端口与A到B的包相反)
+ IP_CT_IS_REPLY
[表2.2 UDP的状态变迁]
UDP本来是一个无状态的协议,连接跟踪记录的的是两个方向上的UDP包,并且不同状态下,连接的超时值不同。
ICMP的连接跟踪可以用下面的表来说明:
发包的方向    连接状态           说明
A-->B         IP_CT_NEW          A到B的ICMP查询包
B-->A         IP_CT_ESTABLISHED  B到A的对ICMP查询包的应答包
+ IP_CT_IS_REPLY
[表2.3 ICMP的状态变迁]
ICMP协议也是一个无状态的协议,连接跟踪记录的是ICMP的查询包和查询应答包,只有在记录了查询包的情况下才允许相应的应答包通过。
为了处理应用协议里面的动态地址和端口,每个使用动态地址和端口的协议一般都定义了数据结构ip_conntrack_helper,并把它放到一个全局的链表中。这类协议一般都会有控制连接和数据连接两个不同的连接,如FTP协议。而在这个结构中定义了匹配其协议控制连接的参数,当创建或更新结构ip_conntrack时都会查找链表中有没有相应的ip_connctrack_helper结构,如果存在,就把它的指针赋给ip_conntrack里面的helper。ip_conntrack_helper里面的函数会根据协议里的地址和端口创建与控制连接相关的结构ip_conntrack_expect,并且在有应答时根据ip_conntrack_expect得到相应数据连接的参数。ip_conntrack与ip_conntrack_expect的关系可以用下图来表示:

[2.1 ip_conntrack和ip_conntrack_expect的关系]
在helper里并没有直接创建ip_conntrack结构,而是创建了一个过渡的结构ip_conntrack_expect,并把它放到了一个全局的链表中。接着,当建立数据连接时,再找到这个ip_conntrack_expect结构,并把它与新建的ip_conntrack结构连接起来。
连接跟踪主要在ip_conntrack_in和ip_confirm这两个函数里实现。在ip_conntrack_in里面创建ip_conntrack结构。接下来,地址转换改动数据包里的地址,包过滤会禁止包通过。如果这个包能够到达ip_confirm,由它把相应的ip_conntrack结构加到系统的哈希表中。ip_conntrack_in函数在文件ip_conntrack_core.c中,下面是它的一个修改后的版本,每行前的行号与原文件里的行号相同。
unsigned int ip_conntrack_in
{
804      (*pskb)->nfcache |= NFC_UNKNOWN;
823      if ((*pskb)->nfct)
824                  return NF_ACCEPT;
指针pskb指向传入函数的数据包,第804行设置这个数据包的标记为NFC_UNKNOWN,说明它没有被修改过。同类的标记还有NFC_ALTERED,用于说明这个数据包已被修改过。在连接跟踪里不会修改数据包的任何参数,所以把它的标记置为NFC_UNKNOWN。第823行检查这个数据包是否已被检查过了,因为已检查过的数据包它的nfct成员指向它所属的ip_conntrack结构里的infos[]中的某一成员,以说明这个数据包是此连接的哪个状态下收到的包。
827      if ((*pskb)->nh.iph->frag_off & htons(IP_MF|IP_OFFSET)) {
828                  *pskb = ip_ct_gather_frags(*pskb);
829                  if (!*pskb)
830                              return NF_STOLEN;
831      }
第827-831行进行分片重组。在LINUX2.4.x网络安全的实现中,分片重组是自动进行的,并不需要打开像LINUX2.2.x中ip_always_defrag那样的开关。
833      proto = ip_ct_find_proto((*pskb)->nh.iph->protocol);
836      if ((*pskb)->nh.iph->protocol == IPPROTO_ICMP
837          && icmp_error_track(*pskb, &ctinfo, hooknum))
838                  return NF_ACCEPT;
第833-838行找到与数据包协议相同的ip_conntrack_protocol结构,并且如果协议是ICMP则调用icmp_error_track做特殊处理。
840      if (!(ct = resolve_normal_ct(*pskb, proto,&set_reply,hooknum,&ctinfo)))
841                  return NF_ACCEPT;
850      ret = proto->packet(ct, (*pskb)->nh.iph, (*pskb)->len, ctinfo);
851      if (ret == -1) {
853                  nf_conntrack_put((*pskb)->nfct);
854                  (*pskb)->nfct = NULL;
855                  return NF_ACCEPT;
856      }
第840-856行根据数据包中的参数在全局的连接表中查找与此包匹配的连接结构。前面说过,连接结构在连接表中出现两次,代表连接的两个方向。如果没有找到相应的连接,则创建新的连接。新的连接创建之后,需要查找ip_conntrack_expect_list看看是否这个连接是与控制连接相关的数据连接。同时还要查找ip_conntrack_helper的链表,看看此连接是否有与之关联的ip_conntrack_helper结构。
858      if (ret != NF_DROP && ct->helper) {
859                  ret = ct->helper->help((*pskb)->nh.iph, (*pskb)->len, ct, ctinfo);
861                  if (ret == -1) {
863                              nf_conntrack_put((*pskb)->nfct);
864                              (*pskb)->nfct = NULL;
865                              return NF_ACCEPT;
866                  }
867      }
第858-867行调用与连接关联的ip_conntrack_helper中的函数处理协议中的动态地址和端口。
ip_confirm函数实际调用的是函数__ip_conntrack_confirm,在调用之前它会先确认相关连接是否已经在全局的哈希表中,如果已经在了,就返回。__ip_conntrack_confirm函数在文件ip_conntrack_core.c中,下面是它的一个修改过的版本,每行前的行号是原文件中的行号。
int__ip_conntrack_confirm(struct nf_ct_info *nfct)
{
426      ct = __ip_conntrack_get(nfct, &ctinfo);
432      if (CTINFO2DIR(ctinfo) != IP_CT_DIR_ORIGINAL)
433                  return NF_ACCEPT;
第426行从数据包的nfct变量中得到它所属的ip_conntrack结构,第432行检查此时的连接状态是否是IP_CT_DIR_ORIGINAL方向,也就是创建此连接包的方向上的状态,如果不是,返回。
435      hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
436      repl_hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_REPLY].tuple);
第435-436行计算两个方向上的哈希值。
448      WRITE_LOCK(&ip_conntrack_lock);
452      if (!LIST_FIND(&ip_conntrack_hash[hash],
conntrack_tuple_cmp,
struct ip_conntrack_tuple_hash *,
&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple, NULL)
456          && !LIST_FIND(&ip_conntrack_hash[repl_hash],
conntrack_tuple_cmp,
struct ip_conntrack_tuple_hash *,
&ct->tuplehash[IP_CT_DIR_REPLY].tuple, NULL)) {
460                  list_prepend(&ip_conntrack_hash[hash],
&ct->tuplehash[IP_CT_DIR_ORIGINAL]);
462                  list_prepend(&ip_conntrack_hash[repl_hash],
&ct->tuplehash[IP_CT_DIR_REPLY]);
470                  WRITE_UNLOCK(&ip_conntrack_lock);
471                  return NF_ACCEPT;
472      }
474      WRITE_UNLOCK(&ip_conntrack_lock);
475      return NF_DROP;
}
第448-475行将连接加到全局的哈希表中。在加入之前,先要确认它不在哈希表中,而且要把表锁住,禁止其他内核路径读或写哈希表。如果加入成功,则返回NF_ACCEPT,如果不成功,则返回NF_DROP。
4.      地址转换的重要数据结构和过程
地址转换与连接跟踪紧密相关。事实上,与地址转换相关的数据也就放在ip_conntrack结构里面,它的内容如下:
struct ip_nat_info
{
int initialized;
unsigned int num_manips;                      /* 需要改动的tuple的个数 */
struct ip_nat_info_manip manips[IP_NAT_MAX_MANIPS];   /* 在不同检查点上修改后的地址和端口*/
const struct ip_nat_mapping_type *mtype;        /* 地址转换的类型 */
struct ip_nat_hash bysource, byipsproto;           /* 链接到哈希表 */
struct ip_nat_helper *helper;                 /* 与此相关的helper */
struct ip_nat_seq seq[IP_CT_DIR_MAX];
};
结构中manips[]保存的是修改后的地址和端口(这与协议有关,比如说ICMP协议里面修改可能是id,type,code等。前面提到的ip_nat_protocol结构就是处理与协议相关的参数的),这些地址用来替代原包中的地址或端口。
bysource,byipsproto两个成员把它所在ip_conntrack链接到两个哈希表中。这两个哈希表,一个用于源地址转换,一个用于目的地址转换。
如前所述,ip_nat_helper处理应用协议里的动态地址和端口,这个helper与前面提到的ip_conntrack_helper是配合起来使用的。ip_conntrack_helper创建与ip_conntrack相关的ip_conntrack_expect结构,然后再由ip_conntrack_helper修改其中的地址和端口。
完成地址转换的主要函数是ip_nat_fn。在这个函数里,根据manips[]的信息修改包的地址或端口。manips[]里面的参数的填充主要来自两个方面:一是匹配相应的地址转换规则(由ip_nat_rule_find查找),匹配到相应的规则后,规则里的TARGET会将相应的信息填充到manips[]中;还有一个就是由ip_conntrack_helper创建ip_conntrack结构,它的完整信息由ip_nat_helper里的expect函数填充。填充ip_nat_info里参数的函数是ip_nat_setup_info,在这个函数里为ip_nat_info选择唯一的地址和端口,如果有多个可选地址,还可以做简单的负载均衡。
ip_nat_fn函数在文件ip_nat_standalone.c中,下面是它的一个修改过的版本。
ip_nat_fn
{
79        (*pskb)->nfcache |= NFC_UNKNOWN;
85        ct = ip_conntrack_get(*pskb, &ctinfo);
第79行设置skb的标志,这个标志是暂时的,因为地址转换要修改数据包里的参数,所以在改动之后,这个标志会设置为NFC_ALTERED。第85行是得到与此包关联的ip_contrack结构。
103      switch (ctinfo) {
111      case IP_CT_NEW:
118                  info = &ct->nat.info;
120                  WRITE_LOCK(&ip_nat_lock);
123                  if (!(info->initialized & (1 << maniptype))) {
127                              if (ct->master
&& master_ct(ct)->nat.info.helper
&& master_ct(ct)->nat.info.helper->expect) {
130                                          ret = call_expect(master_ct(ct), pskb,
hooknum, ct, info);
132                              } else {
133                                          ret = ip_nat_rule_find(pskb, hooknum, in, out,
ct, info);
135                              }
第103-135行填充ip_nat_info结构,这里有两种情况。对创建ip_conntrack结构的包,因为是它的ip_nat_info结构还没有初始化,所以调用ip_nat_rule_find去查找相应的地址转换规则,这个函数又会调用ipt_do_table函数。如果这个ip_conntrack结构是由ip_conntrack_helper创建的,则调用它的控制连接的ip_nat_helper中的expect函数填充数据连接中ip_nat_info中的参数。如果相应的ip_nat_info结构已经填充过了,则跳过这部分处理。
142                              if (in_hashes) {
144                                          replace_in_hashes(ct, info);
145                              } else {
146                                          place_in_hashes(ct, info);
147                              }
148                  }
152                  WRITE_UNLOCK(&ip_nat_lock);
第142-152行根据新的地址和端口将ip_conntrack放到全局的哈希表中,或者从哈希表中将使用旧参数的ip_conntrack取下来,并使用新的参数把它重新放到哈希表中。
163      return do_bindings(ct, ctinfo, info, hooknum, pskb);
第163行的do_bindings函数完成数据包的修改,并且如果ip_conntrack上有与之相关的ip_nat_helper,则调用的help函数处理应用协议里的数据。
5.      连接跟踪、地址转换与包过滤的关系
LINUX2.4.x中的连接跟踪和包过滤是两个独立的功能。连接跟踪创建的连接结构并不作为允许或禁止包通过的依据,只是在包过滤中可以匹配连接跟踪里面的状态,进而决定允许或禁止包通过。这一点与一般意义上的状态检测不同。在状态检测里面,如果有相应的连接存在,就不检查过滤规则,只进行相应的状态变迁。在LINUX2.4.x中,不管连接跟踪的状态如何,都要去检查过滤规则才能决定是否允许包通过。其检查效率要差一些。
从前面的检查点的顺序图可以看出,包过滤是在目的转换之后,源转换之前进行的,所以在添加过滤规则时,规则中的目的地址要用转换后的地址,也就是数据包真正的目的地,而不是包中原来地目的地址。
6.      小结
以上是对LINUX2.4.x中连接跟踪和地址转换实现的简单分析。在分析中我们可以看到,在这个实现里面,有许多可以扩展的东西:如ip_conntrack_protocol,ip_nat_protocol,ip_conntrack_helper,ip_nat_helper等。加上上一篇介绍的ipt_table,ipt_match,ipt_target,nf_hook_ops等。LINUX2.4.x网络安全的实现就是建立在这些可扩展结构的基础之上,所以它有很好的可扩展性。并且,针对不同的协议定义的不同的结构处理与协议相关的信息,使得信息隐蔽性好,模块化增强。这正是一个出色的框架所具备的特点。不过它并没有实现真正意义上的状态检测,而且它的包过滤的效率也有待改进。
参考资料:
1:LINUX2.4.20内核源代码
2:Netfilter Hacking HOWTO
2: iptables tutorial by Oskar Andreasson
3:Iptables connection tracking by James C. Stephens.
作者简介:
林风,独立撰稿人。熟悉LINUX网络安全技术。比较感兴趣的方向是网络协议栈的实现。写文章,是为整理思路,发现问题,与更多人分享经验,知识,或者教训。邮件地址是droplet@163.net,欢迎批评,鼓励或指正。
版权所有,未经许可,不得转载
中联绿盟信息技术(北京)有限公司版权所有 联系:webadmin@nsfocus.com©1999-2005 NSFOCUS Corporation. All rights Reserved.
京ICP备05004765号
_xyz