您的位置:首页 > 运维架构 > Linux

Linux内核防火墙Netfilter实现机制

2010-07-16 10:19 483 查看

Linux内核防火墙Netfilter实现机制

版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
http://damocles.blogbus.com/logs/12355731.html

从图1和图2中我们可以看清楚linux下防火墙netfilter的运作方式,设有5个钩子函数

HookCalled...
NF_IP_PRE_ROUTINGAfter sanity checks, before routing decisions.
NF_IP_LOCAL_INAfter routing decisions if packet is for this host.
NF_IP_FORWARDIf the packet is destined for another interface.
NF_IP_LOCAL_OUTFor packets coming from local processes on their way out.
NF_IP_POST_ROUTINGJust before outbound packets "hit the wire".
图1 钩子函数

不同的模块将会注册不同的钩子函数,每当数据包处理到这个阶段时,就调用模块中定义的处理函数,而netfilter提供了一个整体框架,避免了原来代码的杂乱无章。如图,当数据包从外部进来,首先处理的是PRE_ROUTING,然后转到路由,路由判断究竟是forward这个数据包还是读入本机处理,如果是转发的,会进入到FORWARD钩子,如果是读入处理的,会到LOCAL_IN钩子。当数据包是从本机发出的数据包时,经过LOCAL_OUT钩子,然后路由判断。最后FORWARD和LOCAL_OUT都会经过POST_ROUTING钩子。这样整个防火墙的结构就很清晰了,主要由INPUT OUTPUT FORWARD三个链条组成。



图2 netfilter处理流程
我们通过源代码来看一下它的实现。我们首先看一下发送ip数据包到上层的函数ip_local_deliver(),它所在的文件是net/ipv4/ip_input.c
261 /*
262  *      Deliver IP Packets to the higher protocol layers.
263  */
264 int ip_local_deliver(struct sk_buff *skb)
265 {
266         /*
267          *      Reassemble IP fragments.
268          */
269
270         if (skb->nh.iph->frag_off & htons(IP_MF|IP_OFFSET)) {
271                 skb = ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER);
272                 if (!skb)
273                         return 0;
274         }
275
276         return NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL,
277                        ip_local_deliver_finish);
278 }

我们关心其中的那个returen语句,调用了函数NF_HOOK,这个自然就是netfilter的hook调用。我们发现这其实是一个宏,那么继续深入下去看看:
246 #define NF_HOOK(pf, hook, skb, indev, outdev, okfn) /
247         NF_HOOK_THRESH(pf, hook, skb, indev, outdev, okfn, INT_MIN)

可见它的形参分别是协议类型,钩子类型,skb,进去的device,出去的device以及回调函数指针,它首先会探寻说我们的规则表中有没有对这类情况注册钩子函数进行匹配处理,如果有的话,会调用钩子函数,如果没有的话,则继续执行形参中的回调函数,完成整个过程。可见netfilter是一个很轻量级的,和内核网络代码能轻易剥离的防火墙。我们继续往下看:
182 /**
183  *      nf_hook_thresh - call a netfilter hook
184  *
185  *      Returns 1 if the hook has allowed the packet to pass.  The function
186  *      okfn must be invoked by the caller in this case.  Any other return
187  *      value indicates the packet has been consumed by the hook.
188  */
189 static inline int nf_hook_thresh(int pf, unsigned int hook,
190                                  struct sk_buff **pskb,
191                                  struct net_device *indev,
192                                  struct net_device *outdev,
193                                  int (*okfn)(struct sk_buff *), int thresh,
194                                  int cond)
195 {
196         if (!cond)
197                 return 1;
198 #ifndef CONFIG_NETFILTER_DEBUG
199         if (list_empty(&nf_hooks[pf][hook]))
200                 return 1;
201 #endif
202         return nf_hook_slow(pf, hook, pskb, indev, outdev, okfn, thresh);
203 }

这里出现了一个非常重要的数据结构nf_hooks,我们去看一下到底是怎么样子的
58 struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS];
一个很典型的二维数组,第一维是协议类型,第二维是一个协议最多的钩子函数的数量。这个数组的每一项就是一个list头,指向一串有钩子函数的链表,当这个数组的这一项为空时,即没有钩子函数挂接时,函数nf_hook_thresh返回1,也就是直接执行okfn函数,否则的话继续调用nf_hook_slow()。
我们来看一个nf_hooks初始化的例子,在net/ipv4/netfilter/iptable_filter.c中的初始化函数

142 static int __init iptable_filter_init(void)
143 {
144         int ret;
145
146         if (forward < 0 || forward > NF_MAX_VERDICT) {
147                 printk("iptables forward must be 0 or 1/n");
148                 return -EINVAL;
149         }
150
151         /* Entry 1 is the FORWARD hook */
152         initial_table.entries[1].target.verdict = -forward - 1;
153
154         /* Register table */
155         ret = ipt_register_table(&packet_filter, &initial_table.repl);
156         if (ret < 0)
157                 return ret;
158
159         /* Register hooks */
160         ret = nf_register_hooks(ipt_ops, ARRAY_SIZE(ipt_ops));
161         if (ret < 0)
162                 goto cleanup_table;
163
164         return ret;
165
166  cleanup_table:
167         ipt_unregister_table(&packet_filter);
168         return ret;
169 }

其中的注册table和注册钩子函数就很清晰了,这些都是在初始化时候完成的。我们继续看nf_register_hooks函数,它调用了nf_register_hook函数。

62 int nf_register_hook(struct nf_hook_ops *reg)
63 {
64         struct list_head *i;
65
66         spin_lock_bh(&nf_hook_lock);
67         list_for_each(i, &nf_hooks[reg->pf][reg->hooknum]) {
68                 if (reg->priority < ((struct nf_hook_ops *)i)->priority)
69                         break;
70         }
71         list_add_rcu(®->list, i->prev);
72         spin_unlock_bh(&nf_hook_lock);
73
74         synchronize_net();
75         return 0;
76 }

这个函数很清楚了,它注册一个数据结构到nf_hook_ops的数据结构到表nf_hooks中的相应位置中去,在list中的位置根据reg的priority的值,应该是数值越小,优先级越高,就越先处理。而nf_hook_ops的内容猜都能猜出来吧,肯定是钩子函数咯。
我们回到前面的调用函数nf_hook_slow那个地方,看看这个函数究竟做什么的。

161 int nf_hook_slow(int pf, unsigned int hook, struct sk_buff **pskb,
162                  struct net_device *indev,
163                  struct net_device *outdev,
164                  int (*okfn)(struct sk_buff *),
165                  int hook_thresh)
166 {
167         struct list_head *elem;
168         unsigned int verdict;
169         int ret = 0;
170
171         /* We may already have this, but read-locks nest anyway */
172         rcu_read_lock();
173
174         elem = &nf_hooks[pf][hook];
175 next_hook:
176         verdict = nf_iterate(&nf_hooks[pf][hook], pskb, hook, indev,
177                              outdev, &elem, okfn, hook_thresh);
178         if (verdict == NF_ACCEPT || verdict == NF_STOP) {
179                 ret = 1;
180                 goto unlock;
181         } else if (verdict == NF_DROP) {
182                 kfree_skb(*pskb);
183                 ret = -EPERM;
184         } else if ((verdict & NF_VERDICT_MASK)  == NF_QUEUE) {
185                 NFDEBUG("nf_hook: Verdict = QUEUE./n");
186                 if (!nf_queue(pskb, elem, pf, hook, indev, outdev, okfn,
187                               verdict >> NF_VERDICT_BITS))
188                         goto next_hook;
189         }
190 unlock:
191         rcu_read_unlock();
192         return ret;
193 }

我们发现一个变量verdict,这个就是钩子函数对数据包的处理结果,它有以下几种类型,NF_STOP我也不知道是干嘛的!!

Return CodeMeaning
NF_DROP Discard the packet.
NF_ACCEPT Keep the packet.
NF_STOLEN Forget about the packet.
NF_QUEUE Queue packet for userspace.
NF_REPEAT Call this hook function again.
处理的方式从代码中可见
1.是当接受和停止时,返回1
2.丢弃时释放内存空间
3.进队列则为加入队列然后继续下一个hook
我们可以看到其中的中心函数必定是nf_iterate,它将返回verdict给我们。
117 unsigned int nf_iterate(struct list_head *head,
118                         struct sk_buff **skb,
119                         int hook,
120                         const struct net_device *indev,
121                         const struct net_device *outdev,
122                         struct list_head **i,
123                         int (*okfn)(struct sk_buff *),
124                         int hook_thresh)
125 {
126         unsigned int verdict;
127
128         /*
129          * The caller must not block between calls to this
130          * function because of risk of continuing from deleted element.
131          */
132         list_for_each_continue_rcu(*i, head) {
133                 struct nf_hook_ops *elem = (struct nf_hook_ops *)*i;
134
135                 if (hook_thresh > elem->priority)
136                         continue;
137
138                 /* Optimization: we don't need to hold module
139                    reference here, since function can't sleep. --RR */
140                 verdict = elem->hook(hook, skb, indev, outdev, okfn);
141                 if (verdict != NF_ACCEPT) {
142 #ifdef CONFIG_NETFILTER_DEBUG
143                         if (unlikely((verdict & NF_VERDICT_MASK)
144                                                         > NF_MAX_VERDICT)) {
145                                 NFDEBUG("Evil return from %p(%u)./n",
146                                         elem->hook, hook);
147                                 continue;
148                         }
149 #endif
150                         if (verdict != NF_REPEAT)
151                                 return verdict;
152                         *i = (*i)->prev;
153                 }
154         }
155         return NF_ACCEPT;
156 }

这下我们应该清楚了,这个迭代就是挨个运行nf_hooks[pf][hook]所指向链表中的钩子函数elem->hook。

如果其中有一个钩子函数没有ACCEPT且不是repeat,就直接跳出循环了,然后返回verdict,如果ACCEPT了,则继续处理下一个钩子函数,直到处理完。

60 struct nf_hook_ops
61 {
62         struct list_head list;
63
64         /* User fills in from here down. */
65         nf_hookfn *hook;
66         struct module *owner;
67         int pf;
68         int hooknum;
69         /* Hooks are ordered in ascending priority. */
70         int priority;
71 };

我们看看这个数据结构,再想想前面注册hook时候的情景,应该明白了。它其中定义了pf和hooknum,指定了在nf_hooks表中的元素位置,nf_hookfn则是现实的钩子函数,而priority则指定了它在这个链表中的位置,按照升序排列。那么nf_hookfn是什么时候指定的呢?这个自然是和各个协议相关的。在net/ipv4/netfilter/iptable_filter.c中,我们看到这么一个赋值语句。

114 static struct nf_hook_ops ipt_ops[] = {
115         {
116                 .hook           = ipt_hook,
117                 .owner          = THIS_MODULE,
118                 .pf             = PF_INET,
119                 .hooknum        = NF_IP_LOCAL_IN,
120                 .priority       = NF_IP_PRI_FILTER,
121         },
122         {
123                 .hook           = ipt_hook,
124                 .owner          = THIS_MODULE,
125                 .pf             = PF_INET,
126                 .hooknum        = NF_IP_FORWARD,
127                 .priority       = NF_IP_PRI_FILTER,
128         },
129         {
130                 .hook           = ipt_local_out_hook,
131                 .owner          = THIS_MODULE,
132                 .pf             = PF_INET,
133                 .hooknum        = NF_IP_LOCAL_OUT,
134                 .priority       = NF_IP_PRI_FILTER,
135         },
136 };

这是在ipv4的filter中预先注册的,我们知道还有预先注册的像nat和mangle,当然我们也可以自己写模块,实现这个hook函数。像这个例子中,第一个的hook函数就是ipt_hook,属于的协议是ipv4,属于的钩子类型是LOCAL_IN。
说实话ipt_hook以及之后调用的ipt_do_table我没有看懂,汗,谁看懂了交流一下吧,呵呵! 这周继续阅读netfilter部分的代码和文档,看了一下netfilter hacking howto和水木上的m文,对前一周一些没有搞懂的问题弄明白了。
主要的一个问题是netfilter是在内核中,那么它和iptables这个基于user space的工具怎么交互呢?其实我们应该这么想,netfilter和table是分开实现的,我们通过协议类型和挂载点来找到的在二维数组nf_hooks的位置上,是一个链表的头指针,它所指向的这个链表中有对于这个特定协议和特定挂载点的对于各个表中规则的处理,这些表包括像filter,nat和mangle之类的表。我们在用户空间中调用iptables时,我们的参数为所针对的table,针对的处理链chain,match的规则,和最终的处理方式target,而它们中每个都能在内核空间找到对应关系,像chain针对的就是挂载点。
我们重新回到nf_hooks数组,我们得到一个处理链表,自然进行遍历,在每个节点上都调用hook函数,hook函数指定为ipt_hook之类的函数,具体需要参照加载的函数,我们只是找出一个进行举例。ipt_hook调用ipt_do_table,这个函数是对table进行操作的,函数很长,我们来看一下。
215 /* Returns one of the generic firewall policies, like NF_ACCEPT. */
216 unsigned int
217 ipt_do_table(struct sk_buff **pskb,
218              unsigned int hook,
219              const struct net_device *in,
220              const struct net_device *out,
221              struct ipt_table *table,//这个是我们要操作的table,如filter
222              void *userdata)
223 {
224         static const char nulldevname[IFNAMSIZ] __attribute__((aligned(sizeof(long))));
225         u_int16_t offset;
226         struct iphdr *ip;
227         u_int16_t datalen;
228         int hotdrop = 0;
//hotdrop为1时就直接返回NF_DROP了,这是快速扔包的方法
229         /* Initializing verdict to NF_DROP keeps gcc happy. */
230         unsigned int verdict = NF_DROP;
231         const char *indev, *outdev;
232         void *table_base;
233         struct ipt_entry *e, *back;
234         struct xt_table_info *private = table->private;
//xt_table_info中蕴含了整套规则,以及这些规则的偏移量,使寻找变得容易
235
236         /* Initialization */
237         ip = (*pskb)->nh.iph;
238         datalen = (*pskb)->len - ip->ihl * 4;
239         indev = in ? in->name : nulldevname;
240         outdev = out ? out->name : nulldevname;
241         /* We handle fragments by dealing with the first fragment as
242          * if it was a normal packet.  All other fragments are treated
243          * normally, except that they will NEVER match rules that ask
244          * things we don't know, ie. tcp syn flag or ports).  If the
245          * rule is also a fragment-specific rule, non-fragments won't
246          * match it. */
247         offset = ntohs(ip->frag_off) & IP_OFFSET;
248
249         read_lock_bh(&table->lock);
250         IP_NF_ASSERT(table->valid_hooks & (1 << hook));
251         table_base = (void *)private->entries[smp_processor_id()];
252         e = get_entry(table_base, private->hook_entry[hook]);
//得到这个hook点起始规则的偏移
253
254         /* For return from builtin chain */
255         back = get_entry(table_base, private->underflow[hook]);
//得到这个hook点规则末尾的偏移
256
257         do {
258                 IP_NF_ASSERT(e);
259                 IP_NF_ASSERT(back);
260                 if (ip_packet_match(ip, indev, outdev, &e->ip, offset)) {
261                         struct ipt_entry_target *t;
262
263                         if (IPT_MATCH_ITERATE(e, do_match,
264                                               *pskb, in, out,
265                                               offset, &hotdrop) != 0)
//这个宏用来遍历所有的match,会调用do_match函数
266                                 goto no_match;
//如果没有匹配的规则,则跳转到no_match
267
268                         ADD_COUNTER(e->counters, ntohs(ip->tot_len), 1);
269
270                         t = ipt_get_target(e);//获得target
271                         IP_NF_ASSERT(t->u.kernel.target);
272                         /* Standard target? */
273                         if (!t->u.kernel.target->target) {
//当为NULL时,就是standard target,它是没有模块定义target函数的
274                                 int v;
275
276                                 v = ((struct ipt_standard_target *)t)->verdict;
277                                 if (v < 0) {
278                                         /* Pop from stack? */
279                                         if (v != IPT_RETURN) {
280                                                 verdict = (unsigned)(-v) - 1;
281                                                 break;
282                                         }
283                                         e = back;
284                                         back = get_entry(table_base,
285                                                          back->comefrom);
286                                         continue;
287                                 }
288                                 if (table_base + v != (void *)e + e->next_offset
289                                     && !(e->ip.flags & IPT_F_GOTO)) {
290                                         /* Save old back ptr in next entry */
291                                         struct ipt_entry *next
292                                                 = (void *)e + e->next_offset;
293                                         next->comefrom
294                                                 = (void *)back - table_base;
295                                         /* set back pointer to next entry */
296                                         back = next;
297                                 }
298
299                                 e = get_entry(table_base, v);
300                         } else {
301                                 /* Targets which reenter must return
302                                    abs. verdicts */
303 #ifdef CONFIG_NETFILTER_DEBUG
304                                 ((struct ipt_entry *)table_base)->comefrom
305                                         = 0xeeeeeeec;
306 #endif
307                                 verdict = t->u.kernel.target->target(pskb,
308                                                                      in, out,
309                                                                      hook,
310                                                                      t->u.kernel.target,
311                                                                      t->data,
312                                                                      userdata);
//调用模块中定义的target函数,返回一个verdict
313
314 #ifdef CONFIG_NETFILTER_DEBUG
315                                 if (((struct ipt_entry *)table_base)->comefrom
316                                     != 0xeeeeeeec
317                                     && verdict == IPT_CONTINUE) {
318                                         printk("Target %s reentered!/n",
319                                                t->u.kernel.target->name);
320                                         verdict = NF_DROP;
321                                 }
322                                 ((struct ipt_entry *)table_base)->comefrom
323                                         = 0x57acc001;
324 #endif
325                                 /* Target might have changed stuff. */
326                                 ip = (*pskb)->nh.iph;
327                                 datalen = (*pskb)->len - ip->ihl * 4;
328
329                                 if (verdict == IPT_CONTINUE)
330                                         e = (void *)e + e->next_offset;
331                                 else
332                                         /* Verdict */
333                                         break;
334                         }
335                 } else {
336
337                 no_match:
338                         e = (void *)e + e->next_offset;
//如果没有匹配,则找到下一个ipt_entry
339                 }
340         } while (!hotdrop);
341
342         read_unlock_bh(&table->lock);
343
344 #ifdef DEBUG_ALLOW_ALL
345         return NF_ACCEPT;
346 #else
347         if (hotdrop)
348                 return NF_DROP;
349         else return verdict;
350 #endif
351 }

做一些基本的注释,其实我不喜欢一篇技术博客长篇累牍贴代码,尤其是Linux内核源代码,更多的是希望能够进行分析。但往往越是讨厌的事情,自己往往又这么做了。好吧,还是来解释一下这个函数吧。
它是对table进行操作,至于对于哪个table,是由调用函数决定的。调用之后,他的主要工作如下:
1.找到table的地址,其实重要的是找到ipt_entry的地址,因为里面存着match和target。
2.对match进行匹配,如果匹配成功,则获取target。如果失败,跳转到no_match,找到下一个ipt_entry,进行下一轮匹配。
3.成功的话会有两种情况,一是得到初始化时候的standard target,这时候target函数是初始化为NULL的。二是得到一个由用户新加的target,跟据verdict和hotdrop,来决定是怎么返回。
这边有一点需要指出的是,match的匹配原则是如果这个数据包都匹配了,则不再进行比较,直接可以返回了,只有在no match的情况下才继续匹配下一条match。这个和在上文提到的nf_iterate中的处理方式不同,这个要小心区分的。在同一张表中,是只要匹配就返回verdict,而在nf_iterate 调用钩子函数处理中,如果是ACCEPT的情况,则需要继续处理。天哪,其实这个貌似没有什么特别的联系,只是我自己混淆了一开始,我想大家应该不会像我 这样犯这么低级的错误,写出来算是警戒一下自己。

刚才做了个实验,验证了一下。
实验一:
sudo iptables -t filter -A INPUT -j ACCEPT -p tcp
sudo iptables -t filter -A INPUT -j DROP -p tcp -s hk-in-f104.google.com
这时候是能访问google的,因为数据包进入钩子函数之后只匹配了第一条就返回了,第二条规则是没有意义的。

实验二:
sudo iptables -t filter -A INPUT -j DROP -p tcp -s hk-in-f104.google.com
sudo iptables -t filter -A INPUT -j DROP -p tcp
这时候不能访问任何网站,因为google以外任意一个网站第一条不匹配,则进入下一条,然后drop掉,google则第一条就不匹配,直接drop。

这个在ip_do_table的源代码里面就很清楚了。至于表示table的那些源代码,真的是看了厥倒,这种匪夷所思的数据结构,这些hacker们还胆子真够大的,真不怕内存读写错误啊,要是我我可不敢这么写,他们果然是艺高人胆大,这个下次再分析吧,今天还算是差不多搞清楚了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: