(十二)洞悉linux下的Netfilter&iptables:iptables命令行工具源码解析【下】


2012-05-26 16:17:10

分类: LINUX

iptables用户空间和内核空间的交互

iptables目前已经支持IPv4IPv6两个版本了,因此它在实现上也需要同时兼容这两个版本。iptables-1.4.0在这方面做了很好的设计,主要是由libiptc库来实现。libiptciptables control library的简称,是Netfilter的一个编程接口,通常被用来显示、操作(查询、修改、添加和删除)netfilter的规则和策略等。使用libipq库和ip_queue模块,几乎可以实现任何在内核中所实现的功能。

        libiptc库位于iptables源码包里的libiptc目录下,共六个文件还是比较容易理解。我们都知道,运行在用户上下文环境中的代码是可以阻塞的,这样,便可以使用消息队列和 UNIX 域套接字来实现内核态与用户态的通信。但这些方法的数据传输效率较低,Linux 内核提供 copy_from_user()/copy_to_user() 函数来实现内核态与用户态数据的拷贝,但这两个函数会引发阻塞,所以不能用在硬、软中断中。一般将这两个特殊拷贝函数用在类似于系统调用一类的函数中,此类函数在使用中往往"穿梭"于内核态与用户态。此类方法的工作原理为:

 

        其中相关的系统调用是需要用户自行编写并载入内核。一般情况都是,内核模块注册一组设置套接字选项的函数使得用户空间进程可以调用此组函数对内核态数据进行读写。我们的libiptc库正是基于这种方式实现了用户空间和内核空间数据的交换。

    为了后面便于理解,这里我们简单了解一下在socket编程中经常要接触的两个函数:

int getsockopt(int sockfd, int proto, int cmd, void *data, int datalen)

这个两个函数用来控制相关(获取(获取(禁止sockfd:为protoIP RAW的就用TCP/UDP socket的可用socket是都可以使用低层 的;

data:数据缓冲区起始位置指针,get的时候是将内核中的数据读入该缓冲区;

data中的数据长度。

(即前面的)来实现特殊应用程序的内核与用户空间的数据交换,内核实现新的netfilter为例Netfilter新定义的命令字如下:

#define IPT_SO_SET_REPLACE //设置规则

getsockopt新增命令字#define IPT_SO_GET_INFO               //获取#define IPT_SO_GET_ENTRIES         //获取规则

match

target

       一个标准的optname是否为netfilter所设置的特殊处理函数,于是加入sockopt特殊处理后,新的流程如下:

 

        netfitler对于会实例化一些struct nf_sockopt_ops{}对象,然后通过nf_register_sockopt()将其注册到全局链表nf_sockopts里。

{

         int pf;

         /* Non-inclusive ranges: use 0/0/NULL to never get called. */

         int set_optmax;

         int (*compat_set)(struct sock *sk, int optval,void __user *user, unsigned int len);

         int get_optmin;

         int (*get)(struct sock *sk, int optval, void __user *user, int *len);

 

         unsigned int use;

};

 

         继续回到libiptc中。libiptc库中的所有函数均以“iptc_”开头,主要有下面一些接口(节选自libiptc.h)

 

int iptc_is_chain(const char *chain, const iptc_handle_t handle);

/* Take a snapshot of the rules.  Returns NULL on error. */

 

void iptc_free(iptc_handle_t *h);

/* Iterator functions to run through the chains.  Returns NULL at end. */

const char *iptc_next_chain(iptc_handle_t *handle);

/* Get first rule in the given chain: NULL for empty chain. */

/* Returns NULL when rules run out. */

 

const char *iptc_get_target(const struct ipt_entry *e,iptc_handle_t *handle);

/* Is this a built-in chain? */

 

                         const struct ipt_entry *e,

 

int iptc_zero_entries(const ipt_chainlabel chain,iptc_handle_t *handle);

/* Creates a new chain. */

 

int iptc_commit(iptc_handle_t *handle);

STRUCT_TC_HANDLE这个比较拉风的结构体,它用于存储我们和内核所需要交换的数据。说的通俗一些,就是从内核中取出的表的信息会存储到该结构体类型的变量中;当我们向内核提交iptables变更时,也需要一个该结构体类型的变量用于存储我们所要提交的数据。(定义在ip_tables.h头文件中)

适用于当IPT_SO_GET_INFO,用于从内核读取表信息

{

         char name[IPT_TABLE_MAXNAMELEN];

         unsigned int valid_hooks; /* Which hook entry points are valid: bitmask */

         unsigned int underflow[NF_IP_NUMHOOKS]; /* Underflow points. */

         unsigned int size; /* Size of entries. */

/* The argument to IPT_SO_GET_ENTRIES. */

{

         char name[IPT_TABLE_MAXNAMELEN];

 

};

        

()、从内核获取数据:iptc_init()

都说磨刀不误砍柴工,接下来我们继续上一篇中do_command()函数里剩下的部分。*handle = iptc_init(*table); 这里即根据表名table去从内核中获取该表的自身信息和表中的所有规则。关于表自身的一些信息存储在handle->info成员里;表中所有规则的信息保存在handle->entries成员里。

      如果handle获取失败,则尝试加载完内核中相应的ko模块后再次执行iptc_init()函数。

      然后,针对“ADRI”操作需要做一些合法性检查,诸如-o选项不能用在PREROUTINGINPUT链中、-i选项不能用在POSTROUTINGOUTPUT链中。

                   fprintf(stderr,"Warning: using chain %s, not extension\n",jumpto);

                            free(target->t);

                   printf("Target is a chain,but we have gotten a target,then free it!\n");

                   target = NULL;

if (!target  #如果没有指定target同样,我们的规则也不会执行到这里

&& (strlen(jumpto) == 0|| iptc_is_chain(jumpto, *handle)) #或者{

    … …        

size = sizeof(struct ipt_entry);

         size += matchp->match->m->u.match_size;

e = fw_malloc(size + target->u.target_size);

e->target_offset = size;

 

for (matchp = matches; matchp; matchp = matchp->next) {

         size += matchp->match->m->u.match_size;

memcpy(e->elems + size, target, target->u.target_size);

        最后所生成的规则e,其内存结构如下图所示:

 

        这里再联系我们对内核中netfilter的分析就很容易理解了,一旦我们获取一条规则ipt_entry的首地址,那么我们能通过target_offset很快获得这条规则的target地址,同时也可以通过next_offset获得下一条ipt_entry规则的起始地址,很方便我们到时候做数据包匹配的操作。

 紧接着就是对解析出来的command命令进行具体操作,这里我们是-A命令,因此最后command命令就是CMD_APPEND,这里则执行append_entry()函数。

ret = iptc_append_entry(chain, fw, handle),其实就是由宏即struct chain_head

         struct list_head list;

         unsigned int hooknum;             /* hook number+1 if builtin */

         int verdict;                          /* verdict if builtin */

         struct counter_map counter_map;

         struct list_head rules;             /* 本链中所有规则的入口点 */

         unsigned int index;           /* index (needed for jump resolval) */

         unsigned int foot_index; /* index (needed for counter_map) */

};

struct rule_head

         struct list_head list;

         struct counter_map counter_map;

         unsigned int offset;          /* offset in rule blob */

         enum iptcc_rule_type type;

 

         STRUCT_ENTRY entry[0];   #真正的规则入口点 sizeof计算时不会包含这个字段

int

                   const STRUCT_ENTRY *e,

{

         struct rule_head *r;

         iptc_fn = TC_APPEND_ENTRY;

                   DEBUGP("unable to find chain `%s'\n", chain);

                   return 0;

 

#ipt_entry                   DEBUGP("unable to allocate rule for chain `%s'\n", chain);

                   return 0;

 

         r->counter_map.maptype = COUNTER_MAP_SET;

         if (!iptcc_map_target(*handle, r)) {   #主要是设置规则rtarget,后面分析。

<span lang="EN-US" style="font-family:; font-size: 12pt;" 宋体;mso-font-kerning:0pt"="">                   DEBUGP("unable to map target of rule for chain `%s'\n", chain);

                   return 0;

 

         c->num_rules++;              #同时将链中的规则计数增加

         set_changed(*handle);    #因为INPUT链中的规则已经被改变,则handle->changed=1;

}

         接下来分析一下设置target时其函数内部流程:

iptcc_map_target(const TC_HANDLE_T handle,

{

         STRUCT_ENTRY_TARGET *t = GET_TARGET(e);    #取规则的 

         if (strcmp(t->u.user.name, "") == 0) { #如果没有指定target,则将规则类型设为全放行

                   return 1;

 

         else if (strcmp(t->u.user.name, LABEL_ACCEPT) == 0)

         else if (strcmp(t->u.user.name, LABEL_DROP) == 0)

         else if (strcmp(t->u.user.name, LABEL_QUEUE) == 0)

         else if (strcmp(t->u.user.name, LABEL_RETURN) == 0)

         else if (TC_BUILTIN(t->u.user.name, handle)) {

                   errno = EINVAL;

         } else {

                   struct chain_head *c;

<span lang="EN-US" style="font-family:; font-size: 12pt;" 宋体;mso-font-kerning:0pt"="">                   DEBUGP("trying to find chain `%s': ", t->u.user.name);

                   if (c) {

                            r->type = IPTCC_R_JUMP;  #rule_head结构的type字段置为跳转

                            c->references++;         #跳转到的目的链因此而被引用了一次,则计数器++

                   }

         }

         /* 如果不是用户自定义链,它一定一个用户自定义开发的target模块,比如SNATLOG等。If not, kernel will reject... */

         memset(t->u.user.name + strlen(t->u.user.name),

                FUNCTION_MAXNAMELEN - 1 - strlen(t->u.user.name));

         set_changed(handle);

}

 在append_entry()函数最后,将执行的执行结果返回给ret1表示成功;0表示失败。然后在做一下善后清理工作,如果命令行中有-v则将内核中表的快照dump一份详细信息出来显示给用户看:

if (verbose > 1)

     dump_entries(*handle);

clear_rule_matches(&matches); //释放matches所占的存储空间

 由struct ipt_entry e;所存储的规则信息已经被提交给了handle对象对应的成员,因此将e所占的存储空间也释放:

if (e != NULL) {

              free(e);

              e = NULL;

}

 将全局变量opts复位,初始化时opts=ret = do_command(argc, argv, &table, &handle);

         ret = iptc_commit(&handle);

  当do_command()执行成功后才会去执行iptc_commit()函数,将handle里的数据提交给Netfilter内核。

        iptc_commit()的实现函数为int TC_COMMIT(TC_HANDLE_T *handle),我们只分析IPv4的情形,因此专注于libiptc.c文件中该函数的实现。

        在TC_COMMIT()函数中,又出现了我们在分析Netfilterfilter表时所见到的一些重要结构体STRUCT_COUNTERS_INFO *newcounters;还有前面出现的iptcc_compile_table_prep(*handle,& new_size);

iptcc_compile_table_prep()该函数主要做的工作包含几个方面:

a.初始化handle里每个struct chain_head{}结构体成员中的head_offsetfoot_indexfoot_offset

b.对每个链(struct chain_head{})中的每条规则,再分别计算它们的offsetindex

c.计算handle所指示的表中所有规则所占的存储空间的大小new_size,以及规则的总条数new_number

 接下来,为指针repl;申请存储空间,所申请的大小为sizeof(struct ipt_replace)+new_size。因为struct ipt_replace{}结构的末尾有一个柔性数组struct ipt_entry entries[0]; 它是不计入sizeof的计算结果的。因此,iptables的所有规则实际上是存储在struct ipt_entry entries[0]柔性数组中的,这里所有规则所占大小已经得到:new_size

  因为,每条规则entry都一个计数器,用来记录该规则处理了多少数据包,注意结构体STRUCT_COUNTERS_INFO{}的末尾也有一个柔性数组strcpy(repl->name, (*handle)->info.name);

repl->size = new_size;

repl->num_counters = (*handle)->info.num_entries;

SO_SET_REPLACE, static struct nf_sockopt_ops ipt_sockopts = {

         .set_optmin     = IPT_BASE_CTL,    

  .set_optmax    = IPT_SO_SET_MAX+1,

         .get_optmin     = IPT_BASE_CTL,

         .get           = do_ipt_get_ctl,

static int do_replace(void __user *user, unsigned int len)

         int ret;

         struct xt_table_info *newinfo;

 

                   return -EFAULT;

         /* Hack: Causes ipchains to give correct error msg --RR */

                   return -ENOPROTOOPT;

… …

tmp中去。然后设置规则计数器newcounters,通过setsockopt系统调用将newcounters设置到内核:

setsockopt(sockfd, TC_IPPROTO, newcounters, counterlen);

 此时,在do_ipt_set_ctl()中执行的是do_add_counters()函数。至此,iptables用户空间的所有代码流程就算分析完了。命令:

<span lang="EN-US" 宋体;color:#339966;mso-font-kerning:0pt"="">iptables –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8 –d 10.1.28.184 –j ACCEPT

即被设置到内核的Netfilter规则中去了。

未完,待续

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值