Username: Password:

自己写 Netfilter 匹配器
来源:作者: 发布时间:2007-11-21 13:26:00

编译:王旭

摘要:

iptables/netfilter 框架让我们能够向其中添加功能。要添加功能,我们需要自己写一个内核模块并向这个框架注册。也就是说,依照要添加的功能的分类,写一个 iptables 模块。通过写自定义的扩展模块,您能够匹配、修改、增加保障措施、跟踪一个给定的包。事实上,您几乎能够在这个过滤的世界中做任何您想做的事情。单要小心,内核模块中的一点错误有可能导致系统立即崩溃。

简单起见,我将介绍一个我写的匹配器的骨架。通过这个骨架,我希望能够让这个和 iptables/netfilter 框架的交互更加易于理解。这里,我假设您已对 iptables 有了一定了解并且会使用 C 语言。

这个例子将告诉您怎样根据源或是目的 ip 地址来匹配一个包。

_________________ _________________ _________________

 

概述

写一个 iptables/netfilter 匹配模块的大体步骤如下:
  • 找到您要匹配的具体情况。
  • 写用于接受参数的用户空间部分程式。
  • 写用于分析包信息,得出是否匹配结论的内核空间部分程式。
 

1.0 iptables 模块

iptables 库的用途基本上讲就是和用户交互,他捕获用户要传送给核心态程式的参数。  

1.1 可用的数据结构和函数

首先是一些基本数据结构。 <iptables/include/iptables.h>
稍后文中就将能够看到这些结构的用途了。
/* Include file for additions: new matches and targets. */
struct iptables_match
{
   struct iptables_match *next;

   ipt_chainlabel name;

   const char *version;

   /* Size of match data. */
   size_t size;

   /* Size of match data relevent for userspace comparison purposes */
   size_t userspacesize;

   /* Function which prints out usage message. */
   void (*help)(void);

   /* Initialize the match. */
   void (*init)(struct ipt_entry_match *m, unsigned int *nfcache);

   /* Function which parses command options; returns true if it
           ate an option */
   int (*parse)(int c, char **argv, int invert, unsigned int *flags,
           const struct ipt_entry *entry,
           unsigned int *nfcache,
           struct ipt_entry_match **match);

   /* Final check; exit if not ok. */
   void (*final_check)(unsigned int flags);

   /* Prints out the match iff non-NULL: put space at end */
   void (*print)(const struct ipt_ip *ip,
            const struct ipt_entry_match *match, int numeric);

   /* Saves the match info in parsable form to stdout. */
   void (*save)(const struct ipt_ip *ip,
           const struct ipt_entry_match *match);

   /* Pointer to list of extra command-line options */
   const struct option *extra_opts;

   /* Ignore these men behind the curtain: */
   unsigned int option_offset;
   struct ipt_entry_match *m;
   unsigned int mflags;
#ifdef NO_SHARED_LIBS
   unsigned int loaded; /* simulate loading so options are merged properly */
#endif
};
 

1.2 深入骨架程式

 

1.2.1 初始化

我们首先初始化 ’iptables_match’ 结构中的常用字段:
static struct iptables_match ipaddr
= {
’Name’ 是您的函数库的文档名(也就是 libipt_ipaddr)。
您不能在这个位置放其他的东西,这是用来自动加载您的库的。
    .name            = "ipaddr",
下一个字段 ’version’ 是 iptables 的版本。后面的两个字段都是用于保持用户态程式和核心态共享结构的大小一致性的。
    .version         = IPTABLES_VERSION,
    .size            = IPT_ALIGN(sizeof(struct ipt_ipaddr_info)),
    .userspacesize   = IPT_ALIGN(sizeof(struct ipt_ipaddr_info)),
’Help’ 是用户输入 ’iptables -m module -h’ 的时候要调用的函数。’Parse’ 是用户输入一条新规则的时候调用的,用于验证参数的合法性。’print’ 就是使用 ’iptables -L’ 的时候显示前面添加的规则的。
    .help            = &help,
    .init            = &init,
    .parse           = &parse,
    .final_check     = &final_check,
    .print           = &print,
    .save            = &save,
    .extra_opts      = opts
};
iptables 架构能够支持多个共享库。每个共享库必须使用 <iptables/iptables.c> 中定义的 ’register_match()’ 向 iptables 注册。这个函数将在模块被 iptables 加载的时候调用。 更多信息请参考:’man dlopen’。
void _init(void)
{
   register_match(&ipaddr);
}
 

1.2.2 save 函数

假如我们有一个需要保存的规则集,能够利用 iptables 提供的工具 ’iptables-save’,他能够保存下任何的规则。显然您需要扩展这个工具来保存下来这些规则。这个扩展通过 save 函数完成。
static void save(const struct ipt_ip *ip, const struct ipt_entry_match *match)
{
   const struct ipt_ipaddr_info *info = (const struct ipt_ipaddr_info *)match->data;
假如源地址是规则的一部分的话,打印他。
   if (info->flags & IPADDR_SRC) {
      if (info->flags & IPADDR_SRC_INV)
         printf("! ");
      printf("--ipsrc ");
      print_ipaddr((u_int32_t *)&info->ipaddr.src);
   }
假如目的地址是规则的一部分的话就打印目的地址。
   if (info->flags & IPADDR_DST) {
      if (info->flags & IPADDR_DST_INV)
         printf("! ");
      printf("--ipdst ");
      print_ipaddr((u_int32_t *)&info->ipaddr.dst);
   }
}
 

1.2.3 print 函数

和上面的 save 所蕴含的哲学相同,也有一个 print 函数用于打印规则。他在 ’iptables -L’ 的时候被调用。我们将在下文里看到参数 ’ipt_entry_match *match’ 的用途,但是我们已对他有了一点概念了,是吧?
static void print(const struct ipt_ip *ip,
                  const struct ipt_entry_match *match,
                  int numeric)
{
   const struct ipt_ipaddr_info *info = (const struct ipt_ipaddr_info *)match->data;

   if (info->flags & IPADDR_SRC) {
         printf("src IP ");
      if (info->flags & IPADDR_SRC_INV)
         printf("! ");
      print_ipaddr((u_int32_t *)&info->ipaddr.src);
   }

   if (info->flags & IPADDR_DST) {
      printf("dst IP ");
      if (info->flags & IPADDR_DST_INV)
         printf("! ");
      print_ipaddr((u_int32_t *)&info->ipaddr.dst);
   }
}
 

1.2.4 final check 函数

这个函数是最后一次正确性检查的机会。他在用户输入完规则之后、参数解析刚刚完成的时候被调用。
static void final_check(unsigned int flags)
{
   if (!flags)
      exit_error(PARAMETER_PROBLEM, "ipt_ipaddr: Invalid parameters.");
}
 

1.2.5 parse 函数

parse 是最重要的一个函数,因为这里要检查参数的正确性,并写入我们将共享给核心态程式的信息。他在每次参数被发现的时候被调用,也就是说,假如用户输入了两个参数,这个函数就将被以不同的参数代码 c 调用两次。
static int parse(int c, char **argv, int invert, unsigned int *flags,
                 const struct ipt_entry *entry,
                 unsigned int *nfcache,
                 struct ipt_entry_match **match)
{
我们使用特别结构来保存我们要传递给核心态程式的信息。’match’ 指针被传递给多个函数,我们能够每次使用同样的数据结构。一旦规则被加载了,这个指针就被复制到了核心态程式里。通过这个方式,内核模块能够知道用户想要分析什么(这正是问题的关键,不是么?)。
   struct ipt_ipaddr_info *info = (struct ipt_ipaddr_info *)(*match)->data;
每个参数对应着一个单独的值,于是我们能根据进入的参数决定采取何种行动。下文中我们将看到我们怎样把参数变成数值。
   switch(c) {
首先,我们检查参数是否被使用了多次。假如使用了多次的话,调用 <iptables/iptables.c> 中定义的 ’exit_error()’ 函数,这样程式会立即带着 <iptables/include/iptables_common.h> 中定义的 ’PARAMETER_PROBLEM’ 的错误状态推出。否则,我们在我们的头文档中定义的 ’IPADDR_SRC’ 中配置 ’flags’ 和 ’info->flags’。稍后我们将介绍这个头文档。

虽然这两个标志看起来差不多,但是是完全不同的。’flag’ 的作用域就是这个函数,而 ’info->flags’ 是我们用于和核心态程式共享信息的结构的一部分。
      case ’1’:
         if (*flags & IPADDR_SRC)
            exit_error(PARAMETER_PROBLEM, "ipt_ipaddr: Only use --ipsrc once!");
         *flags |= IPADDR_SRC;
         info->flags |= IPADDR_SRC;
检查假如取反标志 ’!’ 是否存在,假如有的话,在 ’info->flags’ 中写相应的值。
之后调用为这个骨架程式所写的内部函数 ’parse_ipaddr’ 来把 IP 地址从字符串转化为 32 位值。
         if (invert)
            info->flags |= IPADDR_SRC_INV;

         parse_ipaddr(argv[optind-1], &info->ipaddr.src);
         break;
同样考虑,我们检查是否存在多次配置,置恰当的标志。
      case ’2’:
         if (*flags & IPADDR_DST)
            exit_error(PARAMETER_PROBLEM, "ipt_ipaddr: Only use --ipdst once!");
         *flags |= IPADDR_DST;
         info->flags |= IPADDR_DST;
         if (invert)
            info->flags |= IPADDR_DST_INV;

         parse_ipaddr(argv[optind-1], &info->ipaddr.dst);
         break;

      default:
         return 0;
   }

   return 1;
}
 

1.2.6 options 结构

前文中,我们已谈到了要将每个参数映射到一个值。 ’struct option’ 就是个达到这个目的的好办法。要想得到关于这个结构的进一步信息,强烈建议阅读 ’man 3 getopt’。
static struct option opts[] = {
   { .name = "ipsrc",   .has_arg = 1,   .flag = 0,   .val = ’1’ },
   { .name = "ipdst",   .has_arg = 1,   .flag = 0,   .val = ’2’ },
   { .name = 0 }
};

 

1.2.7 init 函数

init 函数用于初始化一些特定的东西,比如 netfilter 的 cache 系统。现在不必过多考虑这个函数的具体用途。
static void init(struct ipt_entry_match *m, unsigned int *nfcache)
{
   /* Can’t cache this */
   *nfcache |= NFC_UNKNOWN;
}

 

1.2.7 help 函数

这个函数通过 ’iptables -m match_name -h’ 被调用,用于显示可用的参数。
static void help(void)
{
   printf (
            "IPADDR v%s options:\n"
            "[!] --ipsrc \t\t The incoming ip addr matches.\n"
            "[!] --ipdst \t\t The outgoing ip addr matches.\n"
            "\n", IPTABLES_VERSION
         );
}

 

1.2.8 头文档 ’ipt_ipaddr.h’

这个文档定义了我们需要的一些东西。
#ifndef _IPT_IPADDR_H
#define _IPT_IPADDR_H
我们已在上文中使用了这些特定的值了。
#define IPADDR_SRC   0x01     /* Match source IP addr */
#define IPADDR_DST   0x02     /* Match destination IP addr */

#define IPADDR_SRC_INV  0x10  /* Negate the condition */
#define IPADDR_DST_INV  0x20  /* Negate the condition */
结构 ’ipt_ipaddr_info’ 是将要被拷贝到核心态程式的那个数据结构。
struct ipt_ipaddr {
   u_int32_t src, dst;
};

struct ipt_ipaddr_info {

   struct ipt_ipaddr ipaddr;

   /* Flags from above */
   u_int8_t flags;

};

#endif
 

1.3 第一章小结

第一部分中,我们讨论了 iptables 库的作用。我们记述了每个函数的内容和 ’ipt_ipaddr_info’ 这个用于保存信息的将要被拷贝到核心态程式来做进一步处理的重要结构。我们也看到了 iptables 结构和怎样注册一个新的库。
应该注意,这仅仅是个用于演示框架怎样工作的骨架程式。而且,’ipt_ipaddr_info’ 和其他类似的东西并不是 iptables/netfilter 的一部分,而仅仅是这个例子的一部分。  

2.0 netfilter 模块

一个匹配模块的工作就是察看每一个收到的包并决定是否符合某个判决准则。这个模块要做如下工作:
  • 接收每个包,并察看匹配模块相关的表
  • 告知 netfilter,我们的模块是否匹配上了这个包
 

2.1 可用的函数和数据结构

首先是一些基本数据结构,这些数据结构定义在 <linux/netfilter_ipv4/ip_tables.h>。
假如您对这个结构连同前面的 iptables 部分更有兴趣的话,您能够看看 Rusty Russell 和 Harald Welte 写的 netfilter hacking howto
struct ipt_match
{
   struct list_head list;

   const char name[IPT_FUNCTION_MAXNAMELEN];

   /* Return true or false: return FALSE and set *hotdrop = 1 to
           force immediate packet drop. */
   /* Arguments changed since 2.4, as this must now handle
           non-linear skbs, using skb_copy_bits and
           skb_ip_make_writable. */
   int (*match)(const struct sk_buff *skb,
           const struct net_device *in,
           const struct net_device *out,
           const void *matchinfo,
           int offset,
           int *hotdrop);

   /* Called when user tries to insert an entry of this type. */
   /* Should return true or false. */
   int (*checkentry)(const char *tablename,
           const struct ipt_ip *ip,
           void *matchinfo,
           unsigned int matchinfosize,
           unsigned int hook_mask);

   /* Called when entry of this type deleted. */
   void (*destroy)(void *matchinfo, unsigned int matchinfosize);

   /* Set this to THIS_MODULE. */
   struct module *me;
};
 

2.2 深入骨架程式

 

2.2.1 初始化

首先,我们初始化 ’ipt_match’ 数据结构中的常用域。

static struct ipt_match ipaddr_match
= {
’name’ 是您的模块的文档名字符串(也就是说 ipt_ipaddr)。
	.name       = "ipaddr",
下面的字段是框架将要使用的回调函数.’match’是当一个包传送给您的模块的时候要调用的函数.
	.match      = match,
	.checkentry = checkentry,
	.me         = THIS_MODULE,
};
您的内核模块的 init 函数需要通过指向一个 ’struct ipt_match’ 的指针调用 ’ipt_register_match()’ 来向 netfilter 框架注册.这个函数在模块被加载的时候调用.
static int __init init(void)
{
	printk(KERN_INFO "ipt_ipaddr: init!\n");
	return ipt_register_match(&ipaddr_match);
}
当把模块从内核中移出的时候这个函数会被调用.这里我们进行的工作是注销匹配器。
static void __exit fini(void)
{
	printk(KERN_INFO "ipt_ipaddr: exit!\n");
	ipt_unregister_match(&ipaddr_match);
}
配置让这两个函数在模块装入和移出的时候被调用。
module_init(init);
module_exit(fini);

 

2.2.2 match 函数

Linux 的 TCP/IP 协议栈包括5个 netfilter 钩子。这样,一个包近来之后,协议栈把包送到相应的钩子,依次进入每个表,再依次叠带每条规则。当您的模块得到包的时候,您的模块就能够进行他的工作了。
static int match(const struct sk_buff *skb,
                 const struct net_device *in,
                 const struct net_device *out,
                 const void *matchinfo,
                 int offset,
                 const void *hdr,
                 u_int16_t datalen,
                 int *hotdrop)
{
希望您还记着我们在用户态程式里面做了些什么! :)。现在把用户态程式拷贝过来的数据结构映射到我们这里
	const struct ipt_skeleton_info *info = matchinfo;
’skb’ 包含了我们想要处理的包。想要得到关于这个在 linux 的 TCP/IP 协议栈中到处都是功能强大的数据结构的信息,能够看看 Harald Welte 写的一出色的文章 article (ftp://ftp.gnumonks.org/pub/doc/skb-doc.html)
   struct iphdr *iph = skb->nh.iph;
这里,我们就是打印一些有趣的东西来看看他们长成什么样子。宏 ’NIPQUAD’ 用于以可读的方式显示一个 IP 地址,他是在 <linux/include/linux/kernel.h> 中定义的。
   printk(KERN_INFO "ipt_ipaddr: IN=%s OUT=%s TOS=0x%02X "
                    "TTL=%x SRC=%u.%u.%u.%u DST=%u.%u.%u.%u "
                    "ID=%u IPSRC=%u.%u.%u.%u IPDST=%u.%u.%u.%u\n",

                    in ? (char *)in : "", out ? (char *)out : "", iph->tos,
                    iph->ttl, NIPQUAD(iph->saddr), NIPQUAD(iph->daddr),
                    ntohs(iph->id), NIPQUAD(info->ipaddr.src), NIPQUAD(info->ipaddr.dst)
         );
假如输入了 ’--ipsrc’ 参数,我们察看源地址是否和规则指定的地址相匹配。别忘了考虑反标志 ’!’。假如没有匹配,我们返回 0.
   if (info->flags & IPADDR_SRC) {
      if ( (ntohl(iph->saddr) != ntohl(info->ipaddr.src)) ^ !!(info->flags & IPADDR_SRC_INV) ) {

         printk(KERN_NOTICE "src IP %u.%u.%u.%u is not matching %s.\n",
                            NIPQUAD(info->ipaddr.src),
                            info->flags & IPADDR_SRC_INV ? " (INV)" : "");
         return 0;
      }
   }
这里,我们进行完全相同的工作,只是察看 ’--ipdst’ 参数。
   if (info->flags & IPADDR_DST) {
      if ( (ntohl(iph->daddr) != ntohl(info->ipaddr.dst)) ^ !!(info->flags & IPADDR_DST_INV) )  {

         printk(KERN_NOTICE "dst IP %u.%u.%u.%u is not matching%s.\n",
                            NIPQUAD(info->ipaddr.dst),
                            info->flags & IPADDR_DST_INV ? " (INV)" : "");
         return 0;
      }
   }
假如都不成功,返回 1,表明我们匹配了这个包。
   return 1;
}
 

2.2.3 checkentry 函数

checkentry 通常是最后一次合法性检查的机会。关于他何时被调用有些难以理解。看看 post (http://www.mail-archive.com/netfilter-devel@lists.samba.org/msg00625.html) 作为一个解释吧。这篇文章也是一篇 netfilter hacking howto。
static int checkentry(const char *tablename,
                             const struct ipt_ip *ip,
                             void *matchinfo,
                             unsigned int matchsize,
                             unsigned int hook_mask)
{
   const struct ipt_skeleton_info *info = matchinfo;

   if (matchsize != IPT_ALIGN(sizeof(struct ipt_skeleton_info))) {
      printk(KERN_ERR "ipt_skeleton: matchsize differ, you may have forgotten to recompile me.\n");
      return 0;
   }

   printk(KERN_INFO "ipt_skeleton: Registered in the %s table, hook=%x, proto=%u\n",
                    tablename, hook_mask, ip->proto);

   return 1;
}

 

2.3 第二章小结

在第二部分,我们讲了 netfilter 模块连同怎样使用特定结构注册他。另外我们还讨论了怎样根据用户空间部分给出的判据匹配特定的情况。  

3.0 运行 iptables/netfilter

我们已看到了怎样写一个新的 iptables/netfilter 匹配模块。现在我们将把他添加到内核中来运行他。这里,我假设您知道怎样编译内核。首先把骨架匹配文档从本文下载页面下载下来。  

3.1 iptables

现在,假如您还没有 iptables 的源代码的话,能够从 ftp://ftp.netfilter.org/pub/iptables/ 下载。然后拷贝 ’libipt_ipaddr.c’ 到 <iptables/extensions/>。

这是 <iptables/extensions/Makefile> 中的一行,您应该加上 ’ipaddr’。
PF_EXT_SLIB:=ah addrtype comment connlimit connmark conntrack dscp ecn
esp hashlimit helper icmp iprange length limit ipaddr mac mark
multiport owner physdev pkttype realm rpc sctp standard state tcp tcpmss
tos ttl udp unclean CLASSIFY CONNMARK DNAT DSCP ECN LOG MARK MASQUERADE
MIRROR NETMAP NOTRACK REDIRECT REJECT SAME SNAT TARPIT TCPMSS TOS TRACE
TTL ULOG
 

3.2 内核

首先,您应该拷贝 ’ipt_ipaddr.c’ 到 <linux/net/ipv4/netfilter/>,拷贝 ’ ipt_ipaddr.h’ 到 <linux/net/ipv4/netfilter/>。有些读者可能还在使用 2.4 内核,所以我同时提供了 2.4和 2.6 的文档。

对于 2.4 内核,编辑 <linux/net/ipv4/netfilter/Config.in> ,加入下面加重的行。
# The simple matches.
  dep_tristate ’  limit match support’ CONFIG_IP_NF_MATCH_LIMIT $CONFIG_IP_NF_IPTABLES
  dep_tristate ’  ipaddr match support’ CONFIG_IP_NF_MATCH_IPADDR $CONFIG_IP_NF_IPTABLES
然后,编辑 <linux/Documentation/Configure.help> 加入加重的行。我复制了一些文本来帮助您找到要加入内容的地方。
limit match support
CONFIG_IP_NF_MATCH_LIMIT
  limit matching allows you to control the rate at which a rule can be
  ...
ipaddr match support
CONFIG_IP_NF_MATCH_IPADDR
  ipaddr matching. etc etc.
最后,您必须把加重的行加入到 <linux/net/ipv4/netfilter/Makefile> 之中。
# matches
obj-$(CONFIG_IP_NF_MATCH_HELPER) += ipt_helper.o
obj-$(CONFIG_IP_NF_MATCH_LIMIT) += ipt_limit.o
obj-$(CONFIG_IP_NF_MATCH_IPADDR) += ipt_ipaddr.o
Now for 2.6, files to edit are <linux/net/ipv4/netfilter/Kconfig> and <linux/net/ipv4/netfilter/Makefile>. 对 2.6 内核,编辑的文档应该是 <linux/net/ipv4/netfilter/Kconfig>和 <linux/net/ipv4/netfilter/Makefile>。  

总结

下面剩下的的就是从新编译连同我忘了说的了。
Happy hacking!!
感谢 Samuel Jean.  

喜欢本文,那就收藏到:

    Del.icio.us Google书签 Digg Live Bookmark Technorati Furl Yahoo书签 Facebook 百度搜藏 新浪ViVi 365Key网摘 天极网摘 和讯网摘 博拉网 POCO网摘 添加到饭否 QQ书签 Digbuzz我挖网
相关评论  我也要评论
还没有关于此文章的相关评论!
  • 昵称: (为空则显示guest)
  • 评论分数: ★ ★ ★★★ ★★★★ ★★★★★
  • 评论内容:(不能超过250字,需审核后才会公布,请自觉遵守互联网相关政策法规。
  • 导航
    赞助商
    文章类别
    订阅