1. 概述
ipfw是BSD系统中重要的防火墙和通信控制工具,防火墙和NAT都可以通过ipfw的相关指令来实现。
pf (包过滤Packet Filter) 是FreeBSD 系统上进行TCP/IP流量过滤和网络地址转换的软件系统。 PF 同样也能提供TCP/IP流量的整形和控制,并且提供带宽控制和数据包优先集控制。
本文不讲解pf和ipfw命令的用法,通过阅读FreeBSD内核协议栈源码,通过pf和ipfw探讨FreeBSD的防火墙实现原理
2. FreeBSD协议栈包过滤处理流程
熟悉Linux协议栈的朋友都知道,Linux的包过滤是通过Netfilter实现的,Netfilter在5个关键节点巧妙的挂在了钩子,可以改变报文行为。
FreeBSD也有Netfilter的基本机制pfil,也有比Netfilter功能更强大的NetGraph,本文主要讲解pfil,NetGraph后续会进行阐述。
FreeBSD协议栈IP报文收发包处理流程参考上图,一个数据包在几个内核变量的控制下,在协议栈的多个地方被防火墙检查。这些地方和变量如上图所示,记住包过滤的关键流程,对于理解和规划FreeBSD防火墙是很有帮助的。
在网卡注册设备时,会将ether_input和ether_input函数挂在ifnet数据结构上,网卡中断会调用ether_input函数,接下来会分别被ether_demux和ip_input处理,如果是需要转发的报文,会调用ip_tryforward、ip_output、ether_output_frame,最后调用ehter_output将报文发送至网卡。
对input方向的报文,在ether_demux和ip_input会调用pf和ipfw挂载的钩子函数,处理报文,处理的结果可以是pass、drop或改变报文内容。
3. ipfw实现概要
PFIL模块通过调用pfil_head_register注册,通常在指定协议的入口和出口进行注册。以IP的注册为例,IP使用AP_INET注册了IP协议
/* Initialize packet filter hooks. */
inet_pfil_hook.ph_type = PFIL_TYPE_AF;
inet_pfil_hook.ph_af = AF_INET;
if ((i = pfil_head_register(inet_pfil_hook)) != 0)
/usr/src/sys/netinet/ip_input.c:ip_init [7]
pfil_run_hooks遍历inet_pfil_hook的队列,运行过滤检查,IP协议的报文过滤代码如下:
if (pfil_run_hooks( inet_pfil_hook, m, m->m_pkthdr.rcvif, PFIL_IN, NULL) != 0) return;
/usr/src/sys/netinet/ip_input.c:ip_input [7]
再看看ipfw初始化流程,在/usr/src/sys/netinet/ip_fw_pfil.c文件中,调用pfil_add_hook注册了钩子函数ipfw_check_packet,用于检查IP报文。
int
ipfw_attach_hooks(int arg)
{
int error = 0;
if (arg == 0) /* detach */
ipfw_hook(0, AF_INET);
else if (V_fw_enable && ipfw_hook(1, AF_INET) != 0) {
error = ENOENT; /* see ip_fw_pfil.c::ipfw_hook() */
printf("ipfw_hook() error\n");
}
#ifdef INET6
if (arg == 0) /* detach */
ipfw_hook(0, AF_INET6);
else if (V_fw6_enable && ipfw_hook(1, AF_INET6) != 0) {
error = ENOENT;
printf("ipfw6_hook() error\n");
}
#endif
if (arg == 0) /* detach */
ipfw_hook(0, AF_LINK);
else if (V_fwlink_enable && ipfw_hook(1, AF_LINK) != 0) {
error = ENOENT;
printf("ipfw_link_hook() error\n");
}
return error;
}
static int
ipfw_hook(int onoff, int pf)
{
struct pfil_head *pfh;
void *hook_func;
pfh = pfil_head_get(PFIL_TYPE_AF, pf);
if (pfh == NULL)
return ENOENT;
hook_func = (pf == AF_LINK) ? ipfw_check_frame : ipfw_check_packet;
if (onoff)
(void) pfil_add_named_hook
(hook_func, NULL, "ipfw", PFIL_IN | PFIL_OUT | PFIL_WAITOK, pfh);
else
(void) pfil_remove_hook
(hook_func, NULL, PFIL_IN | PFIL_OUT | PFIL_WAITOK, pfh);
return 0;
}
4. 编写自己的包过滤内核模块
现在我们知道了ipw如何使用PFIL_HOOKS的原理,我们就可以自己编写一个简单的内核模块用来截获BSD IPv4协议栈的报文。
我们的模块将统计主机接收和发送的所有IPv4报文数目,因为本文将要编写一个内核模块,如果你对FreeBSD的内核模块开发没有了解过,建议参考《FreeBSD Architecture Handbook》的第九章《Writing FreeBSD Device Drivers》里的如何编写简单”Hello world”内核模块的例子。
上面的章节对FreeBSD的pfil hook机制有介绍,下面直接附上源码。
源码如下:
Makefile:
SRCS=hisar.c
KMOD=hisar
rclean:
@make clean
rm -f *~
.include <bsd.kmod.mk>
Hisar.c:
/*
* A simple network flow accountant
* examplifying PFIL_HOOKS
*
* Murat Balaban
* $Id: article.sgml,v 1.15 2005/11/25 20:07:04 murat Exp $
*
*/
#include <sys/types.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/errno.h>
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/conf.h>
#include <sys/uio.h>
#include <sys/malloc.h>
#include <sys/ioccom.h>
#include <sys/mbuf.h>
#include <sys/socket.h>
#include <sys/sysent.h>
#include <sys/sysproto.h>
#include <sys/proc.h>
#include <sys/syscall.h>
#include <machine/iodev.h>
#include <netinet/in_systm.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <net/if.h>
#include <net/pfil.h>
#define MODNAME "EnderUNIX HISAR - A simple network flow accountant"
#define MODVERSION "1.0"
#define FLWACCT_MINOR 11
static volatile int hisar_hooked = 0;
d_open_t dev_open;
d_close_t dev_close;
d_read_t dev_read;
d_write_t dev_write;
static struct cdev *sdev;
static int count = 0; /* Device Busy flag */
static int in_bytes = 0; /* Bytes IN */
static int out_bytes = 0; /* Bytes OUT */
static char *flwbuf = NULL; /* Priv. Buffer */
static struct cdevsw flwacct_cdevsw = {
.d_version = D_VERSION,
.d_open = dev_open,
.d_close = dev_close,
.d_read = dev_read,
.d_name = "flwacct", /*FreeBSD较新的版本不再用主设备号表示内核设备,此处和linux有不一样的地方*/
.d_flags = D_TTY,
};
/*ip_input的钩子函数,统计收包字节数*/
static int
hisar_chkinput(void *arg, struct mbuf **m, struct ifnet *ifp, int dir,
struct inpcb *inp)
{
in_bytes += (*m)->m_len;
return 0;
}
/*ip_output的钩子函数,统计发包字节数*/
static int
hisar_chkoutput(void *arg, struct mbuf **m, struct ifnet *ifp, int dir,
struct inpcb *inp)
{
out_bytes += (*m)->m_len;
return 0;
}
/*内核模块初始化函数*/
static int
init_module(void)
{
struct pfil_head *pfh_inet;
if (hisar_hooked)
return (0);
pfh_inet = pfil_head_get(PFIL_TYPE_AF, AF_INET);
if (pfh_inet == NULL)
return ESRCH;
/*注册钩子函数*/
pfil_add_hook(hisar_chkinput, NULL, PFIL_IN | PFIL_WAITOK, pfh_inet);
pfil_add_hook(hisar_chkoutput, NULL, PFIL_OUT | PFIL_WAITOK, pfh_inet);
hisar_hooked = 1;
flwbuf = (void *)malloc(PAGE_SIZE, M_TEMP, M_WAITOK | M_ZERO);
/*创建设备,内核模块加载后,会在dev目录生成/dev/flwacct设备,用户态程序直接打开/dev/flwacct设备即可,不再使用MAJOR、MINOR表示设备*/
sdev = make_dev(&flwacct_cdevsw,
FLWACCT_MINOR,
UID_ROOT,
GID_WHEEL,
0600,
"flwacct");
uprintf("Loaded %s %s\n", MODNAME, MODVERSION);
return 0;
}
static int
deinit_module(void)
{
struct pfil_head *pfh_inet;
if (!hisar_hooked)
return (0);
pfh_inet = pfil_head_get(PFIL_TYPE_AF, AF_INET);
if (pfh_inet == NULL)
return ESRCH;
pfil_remove_hook(hisar_chkinput, NULL, PFIL_IN | PFIL_WAITOK, pfh_inet);
pfil_remove_hook(hisar_chkoutput, NULL, PFIL_OUT | PFIL_WAITOK, pfh_inet);
hisar_hooked = 0;
free(flwbuf, M_TEMP);
destroy_dev(sdev);
uprintf("Unloaded %s %s\n", MODNAME, MODVERSION);
return 0;
}
/* Module event handler */
static int
mod_evhandler(struct module *m, int what, void *arg)
{
int err = 0;
switch(what) {
case MOD_LOAD:
err = init_module();
break;
case MOD_UNLOAD:
err = deinit_module();
break;
default:
err = EINVAL;
break;
}
return err;
}
int
dev_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
{
int err = 0;
if (count > 0)
return EBUSY;
count = 1;
return (err);
}
int
dev_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
{
int err = 0;
count = 0;
return (err);
}
/*给用户态返回内核收包字节数*/
int
dev_read(struct cdev *dev, struct uio *uio, int ioflag)
{
int rv = 0;
sprintf(flwbuf, "%016d,%016d\n", in_bytes, out_bytes);
rv = uiomove(flwbuf, MIN(uio->uio_resid, strlen(flwbuf)), uio);
return rv;
}
DEV_MODULE(hisarmodule, mod_evhandler, NULL);
MODULE_VERSION(hisarmodule, 1);
执行make命令,生成内核模块hisar.ko,执行kldload ./hisar.ko安装内核模块
用户态flwcnt.c:
/* flwcnt.c read from /dev/flwacct */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
int
main(void)
{
char buf[1024];
int fd;
if ((fd = open("/dev/flwacct", O_RDONLY)) == -1) {
perror("open");
exit(1);
}
while(read(fd, buf, sizeof(buf) - 1) > 0) {
printf("IN: %.16s bytes, OUT: %.16s bytes\n", buf, buf + 17);
sleep(1);
}
close(fd);
return 0;
}
执行命令 gcc -o flwcnt flwcnt.c,生成用户态可执行文件 flwcnt,执行./flwcnt,打印接收和发送的字节数:
# ./flwcnt
IN: 0000000015755973 bytes, OUT: 0000000000524175 bytes
IN: 0000000015756541 bytes, OUT: 0000000000524715 bytes
IN: 0000000015756877 bytes, OUT: 0000000000525067 bytes
IN: 0000000015756933 bytes, OUT: 0000000000525123 bytes
IN: 0000000015757233 bytes, OUT: 0000000000525507 bytes
IN: 0000000015757437 bytes, OUT: 0000000000525727 bytes
IN: 0000000015757493 bytes, OUT: 0000000000525783 bytes
IN: 0000000015757549 bytes, OUT: 0000000000525839 bytes
IN: 0000000015757605 bytes, OUT: 0000000000525895 bytes
IN: 0000000015757697 bytes, OUT: 0000000000525951 bytes
IN: 0000000015757753 bytes, OUT: 0000000000526007 bytes
IN: 0000000015757841 bytes, OUT: 0000000000526063 bytes
IN: 0000000015757897 bytes, OUT: 0000000000526119 bytes
本文主要参考:http://www.enderunix.org/docs/en/pfil_hook.html