[LinuxFocus-icon]
<--  | 站点地图  | 索引  | 搜索

新闻 | 过往期刊 | 链接 | 关于LF
This document is available in: English  ChineseGB  Deutsch  Francais  

Photo of Nicolas Bouliane
by Nicolas Bouliane
<nib(at)cookinglinux!org>

关于作者:
Nicolas 是自由软件界的一个年轻斗士。自从 1998 年在自己的计算机上安装了 Linux 之后,他就沉溺于此了。他长时间地研究 Linux 网络协议栈,写自由软件,并参加 OLS 等 Linux 相关的会议。当 Nicolas 不在计算机前的时候,他喜欢看科幻电影。下象棋和听 Richard Stallman 的访谈.
目录:

 

自己写 Netfilter 匹配器

[Illustration]

摘要:

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 模块

一个匹配模块的工作就是察看每一个收到的包并决定是否符合某个判决准则。这个模块要做如下工作:  

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.  

对这篇文章发表评论

每篇文章都有各自的反馈页面。在这个页面里,您可以提交评论,也可以查看其他读者的评论:




主页由LinuxFocus编辑组维护
© Nicolas Bouliane
"some rights reserved" see linuxfocus.org/license/
http://www.LinuxFocus.org
翻译信息:
en --> -- : Nicolas Bouliane <nib(at)cookinglinux!org>
en --> zh: 王旭 <wangxu(at)linuxfocus.org>

2005-04-28, generated by lfparser version 2.52