自定义iptables/netfilter的目标模块

本文档的Copyleft归yfydz所有,使用GPL发布,可以自由拷贝,转载,转载时请保持文档的完整性,严禁用于任何商业用途。
msn: [email protected]
来源: http://yfydz.cublog.cn

1. 前言
Linux中的netfilter提供了一个防火墙框架,具有很好的扩展性,除了自带的模块之外,用户可以根据自己的需求定义新的防火墙模块加入其中,而编程过程也不是很复杂,只要依葫芦画瓢即可,可在原来的类似功能的模块基础上修改即可,甚至对各函数是如何调用,一些内部结构是如何定义的都不用详细了解,本文即介绍如何编写自定义的目标模块。
 
目标(target)是防火墙规则策略中的结果部分,定义对数据包要进行如何处理,如接受、丢弃、修改、继续等。在具体实现时分为两部分,内核部分和用户空间部分:内核中是以netfilter的扩展模块实现,定义该模块并将其挂接到netfilter的目标链表中后,在内核进行目标检测时自动根据目标名称查找该模块而调用相应的目标函数,完成真正的目标功能;在用户空间,目标模块是作为iptables的一个扩展动态库来实现,只是一个用户接口,不完成实际目标功能,完成接收用户输入并将目标数据结构传递到内核的功能,该库的名称有限制,必须为libipt_xxx.so,其中“xxx”是该目标的名字,为区别于匹配,
通常目标的名称一般都是用大写。

2. 内核模块
目标在2.4内核和在2.6内核中的函数参数略有区别,两者不兼容,但只要简单修改后即可相互移植,主体部分不需要改变,本文以2.6内核的模块为例。
 
为方便说明,还是通过举例来进行说明,要实现的目标是修改IP头中的ID字段,在实际使用中这个功能是没什么实际意义的,只是用来举例说明。
 
在内核中已经自带了net/ipv4/netfilter/ipt_ID.c模块用来修改IP头中的ID字段,我们可以以此模块为基础来进行修改。
 
首先要定义要进行目标的数据结构,用来描述目标条件,以ipt_ID.h头文件为基础修改为:

/* include/linux/netfilter_ipv4/ipt_ID.h */
#ifndef _IPT_ID_H_target
#define _IPT_ID_H_target
struct ipt_id_target_info {
 /* network order. */
 u_int16_t id;  // id是16位的数
};
#endif /*_IPT_ID_H_target*/
 
然后是定义内核模块处理,最主要的是定义一个ipt_target目标结构,该结构在include/linux/netfilter_ipv4/ip_tables.h中定义:
 
/* Registration hooks for targets. */
struct ipt_target
{
 struct list_head list;
 const char name[IPT_FUNCTION_MAXNAMELEN];
 /* Called when user tries to insert an entry of this type:
           hook_mask is a bitmask of hooks from which it can be
           called. */
 /* Should return true or false. */
 int (*checkentry)(const char *tablename,
     const struct ipt_entry *e,
     void *targinfo,
     unsigned int targinfosize,
     unsigned int hook_mask);
 /* Called when entry of this type deleted. */
 void (*destroy)(void *targinfo, unsigned int targinfosize);
 /* Returns verdict.  Argument order changed since 2.4, as this
           must now handle non-linear skbs, using skb_copy_bits and
           skb_ip_make_writable. */
 unsigned int (*target)(struct sk_buff **pskb,
          const struct net_device *in,
          const struct net_device *out,
          unsigned int hooknum,
          const void *targinfo,
          void *userdata);
 /* Set this to THIS_MODULE. */
 struct module *me;
};

该结构中有以下几个参数:
struct list_head list:用来挂接到目标链表,必须初始化为{NULL, NULL};
name: 该目标名称的名称,必须是唯一的;
checkentry函数:用于对用户层传入的数据进行合法性检查,如目标数据长度是否正确,是否是在正确的表中使用等;
destroy函数:用于释放该目标中动态分配的资源,在规则删除时会调用;
target 函数:该函数是最主要函数,完成对数据包的策略处理,包括对包中的数据进行修改,函数返回结果可能是NF_ACCEPT(接受)/NF_DROP(丢弃) /NF_STOLEN(偷窃,指该包处理由该目标接管,不再由系统网络栈处理)/IPT_CONTINUE(继续,继续按后面的规则对该包进行检查)等;
struct module *me:指向模块本身。
 
本例中的目标结构定义如下:
static struct ipt_target ipt_id_reg = {
 .name  = "ID",
 .target  = target,
 .checkentry = checkentry,
 .me  = THIS_MODULE,
};
 
target函数是目标的核心函数,返回1表示数据符合目标条件,0表示不目标,函数定义为:
 unsigned int (*target)(struct sk_buff **pskb,
          const struct net_device *in,
          const struct net_device *out,
          unsigned int hooknum,
          const void *targinfo,
          void *userdata);
在target函数参数为:
skb:数据包
in:数据进入的网卡
out:数据发出的网卡
hooknum:hook号,取值为:NF_IP_PRE_ROUTING/NF_IP_LOCAL_IN/NF_IP_FORWARD/
NF_IP_LOCAL_OUT/NF_IP_POST_ROUTING之一
targetinfo:目标条件信息的指针
offset:碎片数据偏移量
userdata:特殊专用数据,目前内核中基本没有用到,通常都是NULL;
 
 
check函数对数据进行检查,返回1表示数据合法,0表示非法,函数定义为:
 int (*checkentry)(const char *tablename,
     const struct ipt_entry *e,
     void *targinfo,
     unsigned int targinfosize,
     unsigned int hook_mask);
tablename:表名,如“filter”,“nat”,“mangle”等,可用来限制目标只能在指定的表中处理;
struct ipt_entry *e:指向规则的指针;
targetinfo:用户空间传入的目标条件信息的指针;
targetinfosize:用户空间传入目标条件信息的长度;
hook_mask:表示挂接点(PREROUTING/INPUT/FORWARD/OUTPUT/POSTROUTING)的掩码,可用来限制目标只能在指定的挂接点中处理;
 
宏IPT_ALIGN用来得到实际目标结构的实际大小。
 
本例中需要检查目标数据长度是否正确,表名是否是“mangle”,因为需要对数据进行修改,所以一般要在mangle表中进行处理,其实也可以不限制;
 
destroy函数释放该目标中动态分配的资源,无返回值,函数定义为:
 void (*destroy)(void *targetinfo, unsigned int targetinfosize);
函数参数同check()函数说明,在本例中,并没有分配相关资源,所以没定义此函数。
 
最后在模块初始化函数中要调用ipt_register_target()函数将一个ipt_target目标结构挂接到系统目标链表中,在模块的结束函数中调用ipt_unregister_target()函数将目标结构从目标链表中去除。
 
修改net/ipv4/netfilter/Makefile和Kconfig文件,加入关于ipt_ID相关内容即可在编译内核中自动编译,或者单独直接将其编译为模块插入内核。
 
ipt_ID.c代码如下:
/* This is a module which is used for setting the ID field of a packet.
 * based on ipt_ID.c
 * in fact, it's useless.
 */
#include <linux/module.h>
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <net/checksum.h>
#include <linux/netfilter_ipv4/ip_tables.h>
#include <linux/netfilter_ipv4/ipt_ID.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("yfydz<[email protected]>");
MODULE_DESCRIPTION("iptables ID mangling module");
 
static unsigned int
target(struct sk_buff **pskb,
       const struct net_device *in,
       const struct net_device *out,
       unsigned int hooknum,
       const void *targinfo,
       void *userinfo)
{
 const struct ipt_id_target_info *idinfo = targinfo;
// 如果当前包的ID值和指定值不同,将指定值赋值给IP头中的ID字段
 if ((*pskb)->nh.iph->id != idinfo->id) {
  u_int16_t diffs[2];
  if (!skb_ip_make_writable(pskb, sizeof(struct iphdr)))
   return NF_DROP;
  diffs[0] = htons((*pskb)->nh.iph->id) ^ 0xFFFF;
  (*pskb)->nh.iph->id
   = idinfo->id;
  diffs[1] = htons((*pskb)->nh.iph->id);
// 由于IP包数据进行了修改,需要重新计算IP头中的校验和
  (*pskb)->nh.iph->check
   = csum_fold(csum_partial((char *)diffs,
       sizeof(diffs),
       (*pskb)->nh.iph->check
       ^0xFFFF));
  (*pskb)->nfcache |= NFC_ALTERED;
 }
// 返回IPT_CONTINUE,表示继续按下一条规则处理数据包
 return IPT_CONTINUE;
}
 
static int
checkentry(const char *tablename,
    const struct ipt_entry *e,
           void *targinfo,
           unsigned int targinfosize,
           unsigned int hook_mask)
{
 const u_int8_t id = ((struct ipt_id_target_info *)targinfo)->id;
// 检查用户传入数据的长度是否正确
 if (targinfosize != IPT_ALIGN(sizeof(struct ipt_id_target_info))) {
  printk(KERN_WARNING "ID: targinfosize %u != %Zu\n",
         targinfosize,
         IPT_ALIGN(sizeof(struct ipt_id_target_info)));
  return 0;
 }
// 判断是否在mangle表中
 if (strcmp(tablename, "mangle") != 0) {
  printk(KERN_WARNING "ID: can only be called from \"mangle\" table, not
\"%s\"\n", tablename);
  return 0;
 }
 return 1;
}
 
static struct ipt_target ipt_id_reg = {
 .name  = "ID",
 .target  = target,
 .checkentry = checkentry,
 .me  = THIS_MODULE,
};
 
static int __init init(void)
{
 return ipt_register_target(&ipt_id_reg);
}
 
static void __exit fini(void)
{
 ipt_unregister_target(&ipt_id_reg);
}
 
module_init(init);
module_exit(fini);
 
如果对包中的数据进行了修改,需要修改数据头相应的校验和,如IP头校验和和TCP/UDP头校验和,所以可能会需要调用如 csum_partial(),tcp_v4_check(),csum_tcpudp_magic(),ip_send_check(iph)等函数计算校验和,这些函数的使用方法可参考内核中的代码实例。
 
有些目标对包的操作比较复杂,如REJECT,MIRROR等,需要构造回应包发出,所以一般目标要比匹配要复杂一些。

3. iptables用户层目标模块
iptables中的扩展目标模块是以动态库方式处理,在命令行中用“-j xxx”来使iptables调用相应的libipt_xxx.so动态库,扩展的目标代码通常在 iptables-<version>/extension目录下,编译好的动态库缺省放在/usr/local/lib/iptables 目录下。
 
目标动态库的作用用于解析用户输入的目标信息,显示目标信息等功能。
 
写好libipt_xxx.c程序后放到iptables-<version>/extension目录下,修改该目录下的Makefile文件,将xxx添加到扩展表中,make就能自动将其编译为动态库。
 
对于目标,最重要的数据结构就是struct iptables_target结构,扩展的目标程序就是要定义一个这个结构并将其挂接到iptables目标链表中,该结构定义如下:
 
struct iptables_target
{
 struct iptables_target *next;
 ipt_chainlabel name;
 const char *version;
 /* Size of target data. */
 size_t size;
 /* Size of target data relevent for userspace comparison purposes */
 size_t userspacesize;
 /* Function which prints out usage message. */
 void (*help)(void);
 /* Initialize the target. */
 void (*init)(struct ipt_entry_target *t, 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,
       struct ipt_entry_target **target);
 /* Final check; exit if not ok. */
 void (*final_check)(unsigned int flags);
 /* Prints out the target iff non-NULL: put space at end */
 void (*print)(const struct ipt_ip *ip,
        const struct ipt_entry_target *target, int numeric);
 /* Saves the targinfo in parsable form to stdout. */
 void (*save)(const struct ipt_ip *ip,
       const struct ipt_entry_target *target);
 /* Pointer to list of extra command-line options */
 struct option *extra_opts;
 /* Ignore these men behind the curtain: */
 unsigned int option_offset;
 struct ipt_entry_target *t;
 unsigned int tflags;
 unsigned int used;
#ifdef NO_SHARED_LIBS
 unsigned int loaded; /* simulate loading so options are merged properly */
#endif
};
 
struct iptables_target结构参数说明如下:
next: 目标链表的下一个,目标链表是一个单向链表;
name:目标的名称,必须是唯一的;
version:iptables的版本;
size:目标结构的数据长度;
userspacesize:用于目标部分的数据长度,通常此值等于size,但某些情况可能会小于size;
help函数:打印帮助信息,当 "-j xxx -h"时调用;
init函数:初始化函数,可对目标结构赋初值;
parse函数:解析用户输入参数,这是最主要的处理函数;
final_check函数:对用户数据进行最后的检查;
print函数:打印目标信息,iptables -L时调用
save函数:保存当前iptables规则时打印目标格式,被iptables-save程序调用;
extra_opts:选项信息,选项格式是标准的UNIX选项格式,通过getopt函数识别;
option_offset:选项偏移;
t:指向iptables规则;
tflags:规则相关标志
used: 模块使用计数;
 
本例中,用户输入的参数是新的ID值,格式为:
“-j ID --set-id id_value”
id_value为id的实际数值。
 
libipt_id.c函数就是填写struct iptables_target结构,然后定义动态库初始化函数_init()将该结构挂接到iptables的选项链表中。程序在libipt_id.c的基础上修改,程序比较简单,直接将代码列出,相关说明在注释中:
 
/* libipt_id.c */
/* Shared library add-on to iptables to add ID target support. */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <getopt.h>
#include <iptables.h>
#include <linux/netfilter_ipv4/ip_tables.h>
#include <linux/netfilter_ipv4/ipt_ID.h>
struct idinfo {
 struct ipt_entry_target t;
 struct ipt_id_target_info id;
};

/* Function which prints out usage message. */
static void
help(void)
{
 unsigned int i;
 printf(
"ID target v%s options:\n"
"  --set-id value                 Set ID of IP headere\n",
IPTABLES_VERSION);
}
 
// opts结构第一个参数为选项名称,
// 第二个参数为1表示选项名称后还带参数,为0表示选项名称后不带参数
// 第3个参数是标志,表示返回数据的格式,一般都设为0
// 第4个参数表示该选项的索引值
static struct option opts[] = {
 { "set-id", 1, 0, '1' },
 { 0 }
};
/* Initialize the target. */
static void
init(struct ipt_entry_target *t, unsigned int *nfcache)
{
// 空函数,不需要预处理
}
 
static void
parse_id(const unsigned char *s, struct ipt_id_target_info *info)
{
 unsigned int i, id;
 if (string_to_number(s, 0, 255, &id) != -1) {
// 注意要将主机序数据转换为网络序
      info->id = htons((u_int16_t )id);
      return;
 }
 exit_error(PARAMETER_PROBLEM, "Bad ID value `%s'", s);
}
 
/* Function which parses command options; returns true if it
   ate an option */
static int
parse(int c, char **argv, int invert, unsigned int *flags,
      const struct ipt_entry *entry,
      struct ipt_entry_target **target)
{
 struct ipt_id_target_info *idinfo
  = (struct ipt_id_target_info *)(*target)->data;
 switch (c) {
 case '1':
  if (*flags)
   exit_error(PARAMETER_PROBLEM,
              "ID target: Cant specify --set-id twice");
  parse_id(optarg, idinfo);
  *flags = 1;
  break;
 default:
  return 0;
 }
 return 1;
}
 
static void
final_check(unsigned int flags)
{
 if (!flags)
  exit_error(PARAMETER_PROBLEM,
             "ID target: Parameter --set-id is required");
}
 
static void
print_id(u_int8_t id, int numeric)
{
 unsigned int i;
 printf("0x%x ", ntohs(id));
}
 
/* Prints out the targinfo. */
static void
print(const struct ipt_ip *ip,
      const struct ipt_entry_target *target,
      int numeric)
{
 const struct ipt_id_target_info *idinfo =
  (const struct ipt_id_target_info *)target->data;
 printf("ID set ");
 print_id(idinfo->id, numeric);
}
 
/* Saves the union ipt_targinfo in parsable form to stdout. */
static void
save(const struct ipt_ip *ip, const struct ipt_entry_target *target)
{
 const struct ipt_id_target_info *idinfo =
  (const struct ipt_id_target_info *)target->data;
 printf("--set-id 0x%x ", ntohs(idinfo->id));
}
 
static
struct iptables_target id
= { NULL,
    "ID",
    IPTABLES_VERSION,
    IPT_ALIGN(sizeof(struct ipt_id_target_info)),
    IPT_ALIGN(sizeof(struct ipt_id_target_info)),
    &help,
    &init,
    &parse,
    &final_check,
    &print,
    &save,
    opts
};
 
void _init(void)
{
 register_target(&id);
}
 
4. 结论
netfilter/iptables可以很方便地扩展新的目标模块,只需要按指定的方式编写代码就可以,让开发者将注意力集中在功能的具体实现上,而不用再考虑其他因素,在具体实现时可以以现成的目标模块为基础进行修改即可,甚至不需要更仔细了解内部结构的定义就可以完成编码,是一个程序模块化的很优秀的实现例子。在netfilter官方网站(www.netfilter.org)上提供patch-o-matic程序包,其中包含了许多爱好者编写的未并入Linux官方内核中的匹配和目标模块。

猜你喜欢

转载自cxw06023273.iteye.com/blog/866852