本文档为该子系统设计与实现人员提供参考。
本文简单介绍在Linux下如何构建和实现网桥式防火墙,以及如何扩展该网桥式防火墙。
构建企业网关的一个重要前提是在企业网关上实现一个防火墙。该防火墙除了对进出包进行包过滤外,还应能够选择性地将特定的包投递给特定的目的主机(保持企业的网络架构不变)。这就要求该防火墙还具备网桥功能,因此网桥式防火墙是构建企业网关的重要基础。
在Linux构建和实现网桥式防火墙首先应该了解怎么构建简单的网桥式防火墙,然后了解如何扩展基本的网桥式防火墙以满足特定的需求。
本文正是基于上述理由而撰写的。
写作指导者:范兵;
写作执笔者:彭令鹏;
本文档结构组织如下。
第一章:文档说明,简要介绍本文档;
第二章:构建简单的网桥式防火墙,介绍在Linux 下构建一个简单的网桥式防火墙的基本流程;
第三章:网桥式防火墙的内核实现,介绍Linux内核是怎么实现网桥式防火墙的,这是扩展网桥式防火墙的重要基础;
第四章:扩展网桥式防火墙,扩展简单的网桥式防火墙以满足特定的需求。
因为水平有限,并且学习的深度不够,本文档的错误一定很多,所以仅供参考。当然,本文档的更新与校正会一直进行。
网桥是一个链路层转发设备,它根据源MAC地址将数据包从特定的端口转发给目的主机。透明式网桥是本文的研究对象,它可以不影响原网络架构地插入网络中。对终端用户来说,在插入透明式网桥前后,所有网络功能都是一样的,因此透明式网桥对终端用户透明。
现在的防火墙虽然有包过滤、透明转发和记录攻击等多种功能,但包过滤仍是其最基本且最有用的功能。通过包过滤,防火墙可以部分地将攻击阻挡在防火墙之外,从而为防火墙内部用户提供一个较安全的网络环境。
网桥式防火墙简单地来说就是网桥+防火墙,我们所研究的是透明网桥式防火墙。传统的商用防火墙一般需要更改待保护的网络的架构,而透明网桥式防火墙则不需要。更为重要的是,有时候我们需要将进出包投递到特定的主机(如将数据包投递经监控服务器,以实现对网络内容的监控),这样透明网桥式则是一种不错的选择。
和Iptables
为支持网桥式防火墙,Linux2.4及以上内核提供了Netfilter和Iptables。
Netfilter是Linux内核中实现网桥式防火墙的模块,它通过在TCP/IP协议栈中插入钩子点的办法来尽量减小对TCP/IP协议栈的影响。需要网桥式防火墙功能时,应将Netfilter模块编译进内核,这个时候Netfilter在协议栈中提供钩子点,钩取数据包内容供钩子函数处理。而钩子点处的钩子函数的配置则可由Iptalbes等工具来实现。
Iptables 则利用内核中Netfilter的功能,提供了一个配置使用Netfilter的用户态接口。通过这个接口,用户可以在Netfilter各钩子点处放置钩子函数,来实现对网络数据的处理。需要注意的是Iptables也同Netfilter一样整合在内核里。
收发的数据流经钩子点时,将被钩子函数截取并处理,然后根据处理结果来决定数据的走向(是丢弃,是拒绝,还是继续往上走正常的协议栈)。
更为详细的介绍请参考3.2.2Linux防火墙的工作流程。
考虑如图 1所示的一个网络。中间的192.168.0.1为一台装有4个网卡的Linux服务器。我们需要其为另外4台机器转发数据包,并且能够选择性地拒绝某些转发请求(如拒绝ping请求)。
Linux中配置网桥的工具是bridge-utils,2.6及以上的内核已自带了这个工具。具体步骤如下。
brctl addbr br_192 /* 为网桥建立一个逻辑网段,命名为br_192 */
brctl addif br_192 eth0 /* 绑定eth0为br_192网桥的一个端口 */
brctl addif br_192 eth1 /* 绑定eth1为br_192网桥的一个端口 */
brctl addif br_192 e th2 /* 绑定eth2为br_192网桥的一个端口 */
brctl addif br_192 eth3 /* 绑定eth3为br_192网桥的一个端口 */
ifconfig eth0 0.0.0.0 /* eth0作为网桥的一个端口,已不需要IP */
ifconfig eth1 0.0.0.0 /* eth1作为网桥的一个端口,已不需要IP */
ifconfig eth2 0.0.0.0 /* eth2作为网桥的一个端口,已不需要IP */
ifconfig eth3 0.0.0.0 /* eth3作为网桥的一个端口,已不需要IP */
ifconfig eth0 promisc up /* 网桥端口需要接收转发所有数据,因此开启混杂模式*/
ifconfig eth1 promisc up /* 网桥端口需要接收转发所有数据,因此开启混杂模式*/
ifconfig eth2 promisc up /* 网桥端口需要接收转发所有数据,因此开启混杂模式*/
ifconfig eth3 promisc up /* 网桥端口需要接收转发所有数据,因此开启混杂模式*/
ifconfig br_192 192.168.1.1 /* 为网桥指定一个IP,以方便远程管理等 */
以上步骤需要重点指出如下几点。
作为网桥端口的各物理网卡的工作任务是接收转发所有链路层数据包,因此已经不需要IP,并且要工作在混杂模式。
Linux网桥可以为网桥配置一个IP和外部通信,因此网桥br_192也常被称为虚拟网络设备,这个配置的IP不是附着在某个特定的物理网卡上,而是可以附在每个网桥端口网卡上(网卡eth0接收到目的IP为192.168.1.1的数据包,会将该数据包传递给网桥本机处理,eth1、eth2和eth3也如此)。
更详细的相关内容请参考2.3.1Linux下网桥的工作原理。
1 一个简单的网桥的拓扑图
防火墙可用Iptables来配置。首先看一个简单的例子。
如果要拒绝192.168.1.2主机的ping请求或转发请求。可用如下两条语句来实现。
iptables -t filter -A FORWARD -p ICMP -s 192.168.1.2 -j REJECT /* 拒绝转发192.168.1.2的ping请求 */
iptables -t filter -A INPUT -p ICMP -s 192.168.1.2 -j REJECT /* 拒绝192.168.1.2的ping 请求(即ping网桥本机的请求)*/
实际中的防火墙常用来控制某些服务的访问,其配置可参考上述用法。例如假设192.168.1.5上开启了http服务(80端口),现要禁止192.168.1.3访问该服务,可用下面的语句来实现。
iptables -t filter -A FORWARD -p tcp -s 192.168.1.3 -d 192.168.1.5 --dport 80 -j REJECT
事实上网络上已经有丰富的Iptables配置脚本供参考,而且对于应用层检测和跟踪也有免费的模块可以利用。
很多情况下,我们需要将iptables设置的过滤规则备份,或导入iptalbes过滤规则,可以用下面的质量来实现。
iptables-save –c > /etc/iptables_rules_backup /* 保存过滤规则到/etc/iptables_rules_backup中 –c说明要保存计数器*/
iptables-restore –c –n < /etc/iptables_rules_backup /* 导入/etc/iptables_rules_backup中过滤规则,-c 用来恢复计数器, -n指不覆盖掉当前已有的过滤规则*/
关于iptables的介绍与使用,请参考相关文档。
该部分简要介绍网桥式防火墙在Linux内核中的实现。首先介绍网桥的实现,然后介绍网桥基础上的防火墙的实现。
网桥的内核实现代码在net/bridge文件夹下。下面先介绍Linux下网桥的工作原理,然后介绍Linux下网桥的工作流程,最后介绍数据结构和算法的实现。
下网桥的工作原理
前面说了,网桥是在链路层根据源MAC地址来转发数据包到特定端口的。在实现细节中,各网桥有些不同,这里的研究对象仅限于Linux下网桥(请参考2.4及其以上的内核源码)。
根据源MAC地址来转发数据包所涉及到的就是查找转发表,这个表在Linux中叫CAM表。因而建立CAM表以及如何查找CAM表是网桥的基本工作原理。
网桥采用逆向学习法(backward learning)来建CAM表。当它从端口X收到一个源MAC为Y的数据包时,它就可以认为目的MAC地址为Y的数据包应该转发到端口X,因此它就可以将YàX这个映射添加到CAM表中。
具体应用中,计算机网络架构可能会变更(例如更换网卡和搬移计算机等),所以CAM表还必须是动态的。Linux采取了动态扫描CAM表的方法,它每隔一段时间就扫描一次CAM表,如果发现某转发项对应的MAC很久没有从对应的端口发来数据包时(即超过指定时间),则删除该转发项。
当一个网络中有多个网桥工作时,为避免转发循环等问题,Linux利用STP(Spanning Tree Protoco)生成树协议来管理CAM表的更新。STP定义在IEEE 802.1D中,是一种桥到桥的链路管理协议,它在防止产生自循环的基础上提供路径冗余。该协议规定了网桥创建无环回(loop-free)逻辑拓扑结构的算法,可以用来生成遍历整个链路层网络结点(同一个广播域)的无环回树。
查找转发表的流程请参考Linux。
下网桥的工作流程
网桥的基本工作流程如下,该流程包括建立CAM表和查找CAM表的基本流程。图 2给出了Linux下网桥的基本的工作流程。
1) 在某个端口收到数据包,都要学习该MAC地址。
2) 如果数据包是多播包或广播包,则向除接收端口外的其它所有端口转发该数据包,同时如果上层协议栈(本机协议栈)对数据包感兴趣,则需要把数据包提交给上层协议栈;否则转3)。
3) 如果在CAM表中可以找到转发项,则根据转发项从转发端口转发数据包,但是如果转发端口与接收端口是同一端口,则不转发;否则转4)。
4) 向同一个网段中除接收端口外的所有端口转发数据包。
2 Linux网桥的基本工作流程
需要说明的是图 2只给出了基本的工作流程,其它工作细节,例如利用STP协议更新CAM表都未体现在图中。
Linux网桥的主要的数据结构如图 3所示。
3 Linux网桥的主要的数据结构
对图 3做些说明。
逻辑网段net_bridge在本文给出的如图 1所示的网络环境中中即网桥br_192。逻辑网段net_bridge这个结构体包括物理端口链表、虚拟网卡信息(用来实现网桥可以指定IP)、CAM转发表、STP生成树信息等。具体结构如下。
struct net_bridge
{
spinlock_t lock;
struct list_head port_list; //网桥的端口列表
struct net_device *dev; //网桥的虚拟网卡设备,能偶给网桥指定IP靠的就是它
struct net_device_stats statistics; //网桥的虚拟网卡的统计数据(同普通网卡差不多)
spinlock_t hash_lock; //CAM hash表的读写锁
struct hlist_head hash[BR_HASH_SIZE]; //CAM表
struct list_head age_list; //网桥链表
unsigned long feature_mask;
/* STP 相关*/
bridge_id designated_root;
bridge_id bridge_id;
u32 root_path_cost;
unsigned long max_age;
unsigned long hello_time;
unsigned long forward_delay;
unsigned long bridge_max_age;
unsigned long ageing_time;
unsigned long bridge_hello_time;
unsigned long bridge_forward_delay;
u8 group_addr[ETH_ALEN];
u16 root_port;
unsigned char stp_enabled;
unsigned char topology_change;
unsigned char topology_change_detected;
struct timer_list hello_timer;
struct timer_list tcn_timer;
struct timer_list topology_change_timer;
struct timer_list gc_timer;
struct kobject ifobj;
};
CAM表的每个表项用net_bridge_fdb_entry结构体来实现,该结构体如下所示。
struct net_bridge_fdb_entry
{
struct net_bridge_fdb_entry *next_hash; //用来链接表项的指针
struct net_bridge_fdb_entry **pprev_hash; //为什么是pprev不是prev呢?还没有仔细去研究
atomic_t use_count; //表项当前的引用计数器
mac_addr addr; //MAC地址
struct net_bridge_port *dst; /表此项所对应的物理转发端口
unsigned long ageing_timer; //表项的存活时间
unsigned is_local:1; //是否是本机的MAC地址
unsigned is_static:1; //是否是静态MAC地址
};
网桥的实现还有很多细节,这里就不赘述了。
Linux内核的防火墙是通过Netfilter和Iptables来实现的。该部分的代码在net/netfilter下。同介绍网桥的实现一样,下面首先讲述防火墙实现的原理,然后介绍防火墙的工作流程,最后介绍一些重要的数据结构和算法。
防火墙实现的原理
前面已提到Netfilter是防火墙实现的基础,Iptables则为用户提供一个配置防火墙的接口。Iptables根据用户需求在Netfilter提供的钩子点处,插上相应的钩子函数来实现对防火墙的配置。
为了降低对TCP/IP协议栈的影响,Netfilter在Linux内核中是一个可编译的部件。当用户将Netfilter编译进内核后,Netfilter在TCP/IP协议栈中插入5个钩子点。当数据包沿着协议栈投递途径某个钩子点时,将被这个钩子点处的钩子函数(注意,有可能有多个钩子函数构成一个钩子函数链表)处理,经所有钩子函数链表中所有钩子函数处理后,根据处理结果原有的数据包进行相应的下步操作(或被丢弃,或被拒绝,或被修改或继续沿着协议栈往上走等)。5个钩子点的位置如图 4所示。
4 钩子点的位置示意图
对图中的5个钩子点的作用时间作些说明。
NF_IP_PRE_ROUTING,在报文作路由以前执行;
NF_IP_FORWARD,在报文转向另一个NIC以前执行;
NF_IP_POST_ROUTING,在报文流出以前执行;
NF_IP_LOCAL_IN,在流入本地的报文作路由以后执行;
NF_IP_LOCAL_OUT,在本地报文做流出路由前执行。
更详细的说明请参考相关的文档。
Netfilter虽然定义了5个钩子点,但如果没有钩子函数,则5个钩子点也就是形同虚设了。Iptables是用来设置钩子函数的。
Iptables通过各个tables向Netfilter注册放置钩子函数。默认情况下,Iptables有三个table:filter、mangle和nat。在Linux2.6中每个表可能就是一个内核module,这些module在初始化时向Netfilter钩子函数。下面这条Iptables指令,在NF_IP_FORWARD这个钩子点处安了一个钩子函数。
iptables -t filter -A FORWARD -p ICMP -s 192.168.1.2 -j REJECT /* 拒绝转发192.168.1.2的ping请求 */
钩子函数就是用来过滤数据包,是实现防火墙功能的实体。
每个钩子函数有一些匹配条件(match)和一个目标(target),当数据包满足该钩子函数的所有match时,这个钩子函数的target将被执行,这个target可能是如下几种之一。
NF_ACCEPT:继续正常的报文处理;
NF_DROP:将报文丢弃;
NF_STOLEN:由钩子函数处理了该报文,不要再继续传送;
NF_QUEUE:将报文入队,通常交由用户程序处理;
NF_REPEAT:再次调用该钩子函数。
下面这条Iptables指令对应的钩子函数有2个match:源IP为192.168.1.2和协议是ICMP,有1个target:REJECT。
iptables -t filter -A FORWARD -p ICMP -s 192.168.1.2 -j REJECT /* 拒绝转发192.168.1.2的ping请求 */
最后简要总结下防火墙的实现原理:Netfilter在TCP/IP协议栈中安装钩子点(HOOK),Iptables用来在钩子点处安放钩子函数(HOOK FUNCTION),每个钩子函数用来过滤处理数据包。
防火墙的工作流程
在配置好防火墙后,网络数据包在Linux内核中的流动如图 5所示。
5 Linux防火墙的工作流程
需要注意的是图下角“继续沿协议栈向下投递”的处理过程程如再遇到钩子函数点,同样会遵循图中的钩子函数处理流程。
可以看到配置防火墙的重点操作就是:设置合适的钩子函数(第四章中将重点讲述),这可通过Iptables或手动添加钩子函数module来实现。
Netfilter是怎么在TCP/IP协议栈中安放钩子函数点的呢?Iptables又是怎样向Netfilter注册钩子函数的呢?图 6试图来说明这些问题。
从图 6看出,Netfilter通过NF_HOOK宏在协议栈中中插入钩子点,Iptables的默认表则是通过ipt_register_tabl e向Netfilter注册的,而钩子函数则是通过nf_register_hook向Netfilter注册的。然而,Iptables用户通过命令行输入的过滤规则是由Iptables通过nf_sockopt配置Netfilter生效的。
6 Iptables、Netfilter和协议栈的交互关系图
用nf_register_hook注册钩子函数,用到的一个结构体形参定义如下。
struct nf_hook_ops
{
struct list_head list; //
/* User fills in from here down. */
nf_hookfn *hook; //要注册的钩子函数的指针
struct module *owner;
int pf; //协议簇,PF_INET指的是ipv4协议簇
int hooknum; //hook类型,用来指定注册在哪个钩子点上
/* Hooks are ordered in ascending priority. */
int priority; //优先级
};
Iptables用表来组织规则,表结构如下。
struct ipt_table
{
/* 表链 */
struct list_head list;
/* 表名,如"filter"、"nat"等,为了满足自动模块加载的设计,包含该表的模块应命名为iptable_'name'.o */
char name[IPT_TABLE_MAXNAMELEN];
/* 表模子,初始为initial_table.repl */
struct ipt_replace *table;
/* 位向量,标示本表所影响的HOOK */
unsigned int valid_hooks;
/* 读写锁,初始为打开状态 */
rwlock_t lock;
/* iptable的数据区,见下 */
struct ipt_table_info *private;
/* 是否在模块中定义 */
struct module *me;
};
Ipt_table中存放的是表的一些基本的统计信息,而更为详细的表信息则定义在如下的ipt_table_info中。
struct ipt_table_info
{
/* 表大小 */
unsigned int size;
/* 表中的规则数 */
unsigned int number;
/* 初始的规则数,用于模块计数 */
unsigned int initial_entries;
/* 记录所影响的HOOK的规则入口相对于下面的entries变量的偏移量 */
unsigned int hook_entry[NF_IP_NUMHOOKS];
/* 与hook_entry相对应的规则表上限偏移量,当无规则录入时,相应的hook_entry和underflow均为0 */
unsigned int underflow[NF_IP_NUMHOOKS];
/* 规则表入口 */
char entries[0] ____cacheline_aligned;
};
表是用来存放规则的一些统计信息的,而规则的具体内容则存放在如下的ipt_entry中。
struct ipt_entry
{
/* 所要匹配的报文的IP头信息 */
struct ipt_ip ip;
/* 位向量,标示本规则关心报文的什么部分,暂未使用 */
unsigned int nfcache;
/* target区的偏移,通常target区位于match区之后,而match区则在ipt_entry的末尾;
初始化为sizeof(struct ipt_entry),即假定没有match */
u_int16_t target_offset;
/* 下一条规则相对于本规则的偏移,也即本规则所用空间的总和,
初始化为sizeof(struct ipt_entry)+sizeof(struct ipt_target),即没有match */
u_int16_t next_offset;
/* 规则返回点,标记调用本规则的HOOK号,可用于检查规则的有效性 */
unsigned int comefrom;
/* 记录该规则处理过的报文数和报文总字节数 */
struct ipt_counters counters;
/*target或者是match的起始位置 */
unsigned char elems[0];
}
前面介绍了构建简单的网桥式防火墙的一般步骤,在实际的项目中,常需要修改或扩展网桥式防火墙以满足特定的需求。
扩展网桥式防火墙通常有2种方法:添加Iptables模块和自定义钩子函数模块。
模块
Iptalbes默认只有三个表:filter、mangle和nat。而事实上,针对各种应用,已有专门的Iptalbes扩展模块供参考,这些模块是开源的,有些模块甚至做到了能分析跟踪应用层协议,Iptables的官方主页上就有一个patch-o-matic子项目的链接,这个子项目开发并收集了大量的Iptalbes模块。
Iptalbes为添加Iptables模块提供了统一的接口。可以先从网上下到与自己需求最接近的模块,然后修改这个模块以符合自己的需求。
利用Netfilter提供的注册接口,可以通过自定义钩子函数模块来满足特定的需求。下面是一个简单的自定义钩子函数模块,该模块将拒绝接收100字节长的数据包。
#include
#include
static drop_cnt = 0; /* 当前丢弃的数据包总数 */
statoc struct nf_hook_ops hook_test;
static unsigned int drop_pkt100(unsigned int hook, struct sk_buff **pskb, const struct net_device *indev, const struct net_device *outdev, int(* okfn)(struct sk_buff *))
{
if((* pskb)->len == 100)
{
drop_cnt++;
printk(“drop the %d packet!\n”, drop_cnt);
retrun NF_DROP;
}
return NF_ACCEPT;
}
static void init_hook(void)
{
/* 初始化hook函数 */
hook_test.pf = PF_INTE;
hook_test.hooknum = NF_IP_LOCAL_IN;
hook_test.hook = drop_pkt100;
/* 注册hook函数 */
nf_register_hook(&hook_test);
}
static void del_hook(void)
{
printk(“Totally have dropped %d packets!\n”, drop_cnt)
nf_unregister_hook(&hook_test); /* 注销掉hook函数 */
}
module_init(init_hook);
module_exit(del_hook);
编译后,并将其insmod后(192.168.0.111服务器上),在192.168.0.155上输入如下指令。
提示111服务器unreachable,这是因为ping包加上28字节包头后刚好是100字节,因而被丢弃了。
参考资料
[1]. ,netfilter官方站点,上面有很多有用的资料。
[2]. 杨沙洲, Linux Netfilter实现机制和扩展技术. http://www.ibm.com/developerworks/cn/linux/l-ntflt/index.html
[3]. 赵蔚,netfilter:Linux 防火墙在内核中的实现. http://www.ibm.com/developerworks/cn/linux/network/l-netip/index.html
[4]. 无名氏, Linux Netfilter源码分析. ~james/linux/netfilter-1.html
[5]. 其它网上资料.