又弄明白一个问题

zerray | 八月 17th, 2006 - 20:13

uptime命令打出来的load average分别代表了系统在过去1分钟,5分钟和15分钟的平均负载。
这个平均负载是什么嘞?
是 指平均有多少个进程等待使用CPU,所以这个值不超过系统的CPU个数就OK了。

Running uptime on a system provides three values that relate to the system’s load average. They represent the load average during the last 1 minute, 5 minutes, and 15 mintues, respectively. Each value represents an average of the number of processes waiting to run on a CPU. The ideal load for a system is equal to the number of processors in the system.

gdb里的内存断点

zerray | 七月 20th, 2006 - 20:17

awatch — Set a watchpoint for an expression
rwatch — Set a read watchpoint for an expression
watch — Set a watchpoint for an expression

help里是这么写的,其实awatch是读写断点,内存被读或着写时都会断。而rwatch是读时断,watch 是写时断。

The art of software testing 阅读笔记(二)

zerray | 一月 23rd, 2006 - 11:04

第二章 测试的心理学与经济学

心理学

一个不好的程序测试的主要原因是,大多数程序员是以一个错误的定义开始的。他们可能 会说:

“测试是证明没有错误的过程。”
或者
“测试的目的是展示程序实现了正确的功能。”
或者
“测试是确 立程序能够正确执行的信心的过程。”

这些定义是本末倒置的。

当你测试一个程序,你是想为它加入价值,也就是提高质量和可 靠性。提高可靠性意味着找出并移除错误。所以,不要为了证明程序正确而测试,而要为了找出尽可能多的错误。

所以,对于测试一个更好的定义 应该是:

“测试是带着寻找错误的目的执行程序的过程。”

测试是一个破坏的过程,甚至是一个虐待的过程。(其实大多数人都 是倾向于建设而不是破坏的。虽然曾听一个老师说过人的本性是破坏性的,就好像幼儿园中的小孩子,当他把积木搭得很高的时候没有一下子把积木推倒时那么高 兴。看来,选择了测试这行,就要做一个”bad boy”了,呵呵)

一个好的测试用例是很可能发现未发现错误的用例。
一个成功的 测试用例是发现了一个未发现错误的的用例。

这就好像是去看医生,你并不是为了让医生告诉你你很健康,而是要让医生告诉你你哪里出了问题。

经 济学

黑盒测试,即便像判断三角形类型这样的程序也无法穷举所有可能输入,更不用说像编译器,操作系统,数据库这样的程序了。
白盒 测试,或许可以做到语句覆盖,条件覆盖,甚至是路径覆盖,但还是确定程序符合需求,无法测试出缺少的路径,也无法测试出一些与数据相关的错误。
穷 举测试是不可能的,所以需要考虑测试中的经济学,怎样用有限的测试用例找出尽可能多的错误。

测试的十条原则

1 定义期望的输出或结果是测试用例的一个必要组成部分。
2 一个程序员应避免测试自己的程序。
3 一个工作组不应该测试他们自己的程序。
4 彻底检查每个测试的结果。
5 测试用例除了要有合法的输入,还要有非法的输入。
6 检查程序是否没有完成应完成的功能只是工程的一半,另一半是看程序是否做了不应该做的事。
7 避免一次性的测试用例,除非程序真是一次性的。
8 不要在默许没有错误会被发现的假设下做测试。
9 一段程序中可能存在更多错误的可能性与这段程序中已经被发现的错误数成比例。
10 测试是一项非常挑战创造力和智力的工作。

The art of software testing 阅读笔记(一)

zerray | 一月 23rd, 2006 - 11:02

第一章 自测

测试一个判断三角形类型的小程序,程序从对话框读入三个整数,代表三边长度,输出该三角形所属类型,即不等边,等腰,等边三角形。

书中给出的应包括如下几点:

1. 有一个测试用例构成一个合法的不等边三角形。
2. 有一个测试用例构成一个合法的等边三角形。
3. 有一个测试用例构成一个合法的等腰三角形。
4. 至少有三个测试用例是一个合法的等腰三角形三边的全排列情况(如3,3,4;3,4,3;4,3,3)。
5. 有一个一边为0的测试用例。
6. 有一个一边为负值的测试用例。
7. 有一个测试用例三边为正数,其中两边之和等于第三边。
8. 至少有三个测试用例是第7条中的全排列情况。
9. 有一个测试用例三边为正数,其中两边之和小于第三边。
10. 至少有三个测试用例是第9条中的全排列情况。
11. 有一个三边全为0的测试用例。
12. 至少有一个测试用例中有非整数值。
13. 有一个测试用例给出少于或多于三个数。
14. 每一个测试用例中相对于输入都应该有一个期望的输出。

我没有考虑到的有4,8,10,14,只比职业程序员的平均值(7,8)多了2个,不过我还想到了输入不是数字的情况,看来我对做测试还是有一点点悟性的:)

VC中用 ODBC操作Access数据库

zerray | 一月 18th, 2006 - 17:22

这也是我帮同学写的那个程序中的一部分,用ODBC操作Access数据库的部分。 由于不会并且也不愿用MFC,所以就查了查资料,用API搞定了。

const char szConnect[] = "DRIVER={Microsoft Access Driver (*.mdb)};DBQ=";
const char szDBName[] = "StudData.mdb";

void ODBCConnect(HWND);
void ODBCDisconnect(HWND);
void ODBCQuery(char *);
void ODBCQueryEnd(void);
void FetchResult(void);

void ODBCConnect(HWND hDlg)
{
    try {
        SQLRETURN sr;

        sr = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &hEnv);
        if (sr != SQL_SUCCESS && sr != SQL_SUCCESS_WITH_INFO) throw sr;
        sr = SQLSetEnvAttr(hEnv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0);
        if (sr != SQL_SUCCESS && sr != SQL_SUCCESS_WITH_INFO) {
            SQLFreeHandle(SQL_HANDLE_ENV, hEnv);
            throw sr;
        }
        sr = SQLAllocHandle(SQL_HANDLE_DBC, hEnv, &hConn);
        if (sr != SQL_SUCCESS && sr != SQL_SUCCESS_WITH_INFO) {
            SQLFreeHandle(SQL_HANDLE_ENV, hEnv);
            throw sr;
        }
        strcpy(szConnectString, szConnect);
        strcat(szConnectString, szDBName);
        sr = SQLDriverConnect(hConn, hDlg, (unsigned char *)szConnectString, sizeof(szConnectString), (unsigned char *)ConnBuf, sizeof(ConnBuf), (short *)&StrLen, SQL_DRIVER_COMPLETE);
        if (sr != SQL_SUCCESS && sr != SQL_SUCCESS_WITH_INFO) {
            SQLFreeHandle(SQL_HANDLE_DBC, hConn);
            SQLFreeHandle(SQL_HANDLE_ENV, hEnv);
            throw sr;
        }
    }
    catch (SQLRETURN) {
        MessageBox(hDlg, "Database connection failed!", "Sorry", MB_OK | MB_ICONWARNING);
        ExitProcess(0);
    }
}

void ODBCDisconnect(HWND hDlg)
{
    SQLDisconnect(hConn);
    SQLFreeHandle(SQL_HANDLE_DBC, hConn);
    SQLFreeHandle(SQL_HANDLE_ENV, hEnv);
}

void ODBCQuery(char *query)
{
    try {
        SQLRETURN sr;

        sr = SQLAllocHandle(SQL_HANDLE_STMT, hConn, &hStmt);
        if (sr != SQL_SUCCESS && sr != SQL_SUCCESS_WITH_INFO) throw sr;
        sr = SQLExecDirect(hStmt, (unsigned char *)query, strlen(query));
        if (sr != SQL_SUCCESS && sr != SQL_SUCCESS_WITH_INFO) {
            SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
            throw sr;
        }
    }
    catch (SQLRETURN) {
        char errorbuf[128];
        SQLError(hEnv, hConn, hStmt, NULL, NULL, (unsigned char *)errorbuf, sizeof(errorbuf), (short *)&StrLen);
        MessageBox(NULL, "Database query failed!", "Sorry", MB_OK | MB_ICONWARNING);
        MessageBox(NULL, errorbuf, "Sorry", MB_OK | MB_ICONWARNING);
    }
}

void ODBCQueryEnd(void)
{
    SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
}

void FetchResult(void)
{
    SQLRETURN sr;
    LV_ITEM lvi;
    int i, row;

    for (i = 0; i < 10; ++i) SQLBindCol(hStmt, i + 1, SQL_C_CHAR, buf[i], sizeof(buf[i]), (long *)&StrLen);

    for (row = 0; ; ++row) {
        sr = SQLFetch(hStmt);
        if (sr != SQL_SUCCESS && sr != SQL_SUCCESS_WITH_INFO) break;
        lvi.mask = LVIF_TEXT | LVIF_PARAM;
        lvi.iItem = row;
        lvi.iSubItem = 0;
        lvi.pszText = buf[0];
        SendMessage(hList, LVM_INSERTITEM, 0, (long)&lvi);
        lvi.mask = LVIF_TEXT;
        for (i = 1; i < 10; ++i) {
            lvi.iSubItem = i;
            lvi.pszText = buf[i];
            SendMessage(hList, LVM_SETITEM, 0, (long)&lvi);
        }
    }
}

VC中列表视图控件的使用

zerray | 一月 18th, 2006 - 17:15

好久没写学习笔记了,不是一直没有学习,而是一直懒得做笔记。 前段时间帮同学写了个小程序,写的过程中还是学到了一些东西的。

使用列表控制的步骤如下:

  1. 调用CreateWindowEx函数来创建一个列表控件,指定它的类名为SysListView32。您还可以在此处指定控件初次显示 时的方式。
  2. 创建和初始化用在列表控件中显示项目的图象列表(如果存在)。
  3. 向列表控件中插入列,如果显示的方式是报告方式这一步是必须的。
  4. 向控件中插入项目和自项目。

所用到的两种数据结构:
列:

LV_COLUMN STRUCT
imask dd ?
fmt dd ?
lx dd ?
pszText dd ?
cchTextMax dd ?
iSubItem dd ?
iImage dd ?
iOrder dd ?
LV_COLUMN ENDS

Field name Meanings
imask 一组标志位,它指示了该结构体中的那些成员变量是有效的。该结构体中的成员变量并不是同时有效的。在某些时候,可能只有某 些成员变量是有效的。结构体可以用来输入和输出。这样让WINDOWS知道那些成员变量是有效的是非常重要的。可能的标志有:

LVCF_FMT = fmt有效
LVCF_SUBITEM = iSubItem有效
LVCF_TEXT = pszText有效.
LVCF_WIDTH = lx有效

您可以一次使用几个标志。譬如,如果您向指定列的文本标签(列名),您必须在pszText成员变量中提供列名,然后指定 标志LVCF_TEXT告诉WINDOWS成员变量pszText中的值是有效的,否则WINDOWS将忽略掉pszText中的值。

fmt 指定了项目/子项目的对齐方式。可能的值有:

LVCFMT_CENTER = 文本居中
LVCFMT_LEFT = 文本左对齐
LVCFMT_RIGHT = 文本右对齐

lx lx 是列的宽度(以像素点为单位)。以后您可以发送消息LVM_SETCOLUMNWIDTH来改变列的宽度。
pszText 如果用来设定列的属性时,该成员变量为指向列名的指针。如果是查询列名,该成员变量指向一个足够大的缓冲区,用来接收返 回的列名,这是您必须在成员cchTextMax中指定缓冲区的大小。如果是设定列名时,可以忽略该变量,因为该指针指向的是一个ASCII码的字符串, 而WINDOWS可以解析出ASCII串的长度。
cchTextMax cchTextMax 以字节计的上面一个成员变量指向的缓冲区的小。该成员变量只在您查询列的属性时使用。如果是设定列的属性,那该变量将被忽略。
iSubItem 指定和该列相连的子项目的索引号。该成员变量的值用来标识和列相连系的子项目。该列的使用最好地说明了如何把列号和子项 目相连。要查询列的属性时可以发送LVM_GETCOLUMN消息,并在成员变量imask中指定LVCF_SUBITEM标志,列表控件将在 iSubItem中返回插入时设定的iSubItem值。为了使用该办法,您需要在该成员变量中放入正确的值。
iImage and iOrder 为了和IE3.0以上版本兼容。目前我没有这方面的资料。

项目和子项目:

LV_ITEM STRUCT
imask dd ?
iItem dd ?
iSubItem dd ?
state dd ?
stateMask dd ?
pszText dd ?
cchTextMax dd ?
iImage dd ?
lParam dd ?
iIndent dd ?
LV_ITEM ENDS

Field name Meanings
imask 一组标志位标明该结构体中那些成员变量中的值有效。它的意义和上面我们提到的LV_COLUMN型结构体中向对应的成员 变量基本相同。更详细的信息,可以查询WIN32 API 手册。
iItem 该结构体代表的项目的索引号。索引号是从0开始编号的。该值和表单的“行”类似。
iSubItem 和上一个成员变量指定的项目相连的子项目的索引号。您可以把它当作表单的“列”。譬如您想要把一个项目插入到新创建的列 表视图控件,iItem的值应为0(因为该项目是第一个项目),iSubItem的值也应当为0(我们想把该项目插到第一列)。如果你想指定一个子项目和 该项目相连,iItem中应该是您想要相连的项目的索引号,iSubItem的值应当是大于0的值,具体的值取决于您想把该子项目插在那一列。如果你的列 表视图控件一共有4列的化,第一列包含了项目,其余3列是留给子项目的。如果您想把子项目插在第四列,应当指定该值为3。
state 该成员变量包含的标志位反应了项目的状态。状态的改变可能是由用户的操作引起的或是程序改变的。这些状态包括:是否有焦点 /高亮度显示/被选中(由于被剪切)/被选中等。另外还包括,以1为基数的索引用来代表是否处使用重叠/状态图标。
stateMask 由于上面的成员变量包含状态标志位、重叠的位图索引号、和状态位图的索引号,我们需要告诉WINDOWS我们到底需要设 定或查询那一个值。该成员变量就是用来做这项工作的。
pszText 当我们想设定项目的属性时,它包含项目名称的ASCII码的字符串的地址。当查询项目的属性时,该成员变量将用来接收查 询返回的项目的名称。
cchTextMax 仅当您用来查询项目的属性时才需要使用该值,这时它包含上一个成员变量的大小。
iImage 图标在列表视图中的图象链表中的索引号。
lParam 用户定义的值,当您给项目排序时使用。当您告诉列表视图对项目排序时,列表视图将成对地比较项目。 它将会把两个项目的lParam的值传给您,这样您就可以进行比较先列出那一个了。如果您现在还不太明白的话,没有系,我们稍后还要讲关于排序的问题。

控件通过SendMessage来发送消息来控制,常用的消息有:

LVM_INSERTCOLUMN  加入列,wParam 为整型,指定列号,lParam 为指向LV_COLUMN结构的指针
LVM_SETCOLUMN  设置列,参数同上
LVM_INSERTITEM  加入项目或子项目,wParam 为0,lParam 为指向LV_ITEM结构的指针
LVM_SETITEM  设置项目或子项目,参数同上
LVM_GETITEM  取得项目或子项目,参数同上
LVM_GETNEXTITEM  取得下一个项目或子项目,可以用来取得光标选择的项目
LVM_DELETEITEM  删除项目或子项目,wParam 为整型,指定项目索引号,lParam 为0
LVM_DELETEALLITEMS  删除所有项目,wParam 和 lParam 均为0
LVM_SETTEXTCOLOR  设置文字颜色,wParam 为0,lParam 为颜色的RGB值
LVM_SETTEXTBKCOLOR  设置文字背景色,参数同上
LVM_SETBKCOLOR  设置背景色,参数同上

下面是用SysListView32控件实现的简单的学生管理系统程序的一部分:

void InsertColumn(void)
{
    LV_COLUMN lvc;

    lvc.mask = LVCF_TEXT | LVCF_WIDTH;
    lvc.pszText = "学号";
    lvc.cx = 60;
    SendMessage(hList, LVM_INSERTCOLUMN, 0, (long)&lvc);
    lvc.pszText = "姓名";
    lvc.cx = 80;
    SendMessage(hList, LVM_INSERTCOLUMN, 1, (long)&lvc);
    lvc.pszText = "性别";
    lvc.cx = 40;
    SendMessage(hList, LVM_INSERTCOLUMN, 2, (long)&lvc);
    lvc.pszText = "年龄";
    lvc.cx = 40;
    SendMessage(hList, LVM_INSERTCOLUMN, 3, (long)&lvc);
    lvc.pszText = "出生日期";
    lvc.cx = 100;
    SendMessage(hList, LVM_INSERTCOLUMN, 4, (long)&lvc);
    lvc.pszText = "籍贯";
    lvc.cx = 150;
    SendMessage(hList, LVM_INSERTCOLUMN, 5, (long)&lvc);
    lvc.pszText = "入学时间";
    lvc.cx = 100;
    SendMessage(hList, LVM_INSERTCOLUMN, 6, (long)&lvc);
    lvc.pszText = "数学";
    lvc.cx = 40;
    SendMessage(hList, LVM_INSERTCOLUMN, 7, (long)&lvc);
    lvc.pszText = "英语";
    lvc.cx = 40;
    SendMessage(hList, LVM_INSERTCOLUMN, 8, (long)&lvc);
    lvc.pszText = "政治";
    lvc.cx = 40;
    SendMessage(hList, LVM_INSERTCOLUMN, 9, (long)&lvc);
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static HINSTANCE hInst;
    HMENU hMenu;
    LV_ITEM lvi;
    int i;

    switch (message) {
        case WM_CREATE:
            hInst = ((LPCREATESTRUCT)lParam) -> hInstance;
            hMenu = GetMenu(hwnd);
            InitCommonControls();
            hList = CreateWindowEx(NULL, TEXT("SysListView32"), NULL, LVS_REPORT | WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, hwnd, NULL, hInst, NULL);
            InsertColumn();
            SendMessage(hList, LVM_SETTEXTCOLOR, 0, RGB(255, 255, 255));
            SendMessage(hList, LVM_SETBKCOLOR, 0, RGB(100, 100, 100));
            SendMessage(hList, LVM_SETTEXTBKCOLOR, 0, RGB(0, 0, 0));
            Refresh();
            break;
        case WM_COMMAND:
            switch (LOWORD(wParam)) {
                case IDREFRESH:
                    Refresh();
                    break;
                case IDINSERT:
                    if (DialogBox(hInst, TEXT("StudInfo"), hwnd, InsertDlgProc)) InvalidateRect(hwnd, NULL, TRUE);
                    break;
                case IDEDIT:
                    lvi.iItem = SendMessage(hList, LVM_GETNEXTITEM, -1, LVNI_SELECTED);
                    if (-1 == lvi.iItem) {
                        MessageBox(hwnd, "请选择一条记录!", "Edit Info", MB_OK | MB_ICONWARNING);
                        break;
                    }
                    lvi.mask = LVIF_TEXT;
                    lvi.cchTextMax = 20;
                    for (i = 0; i < 10; ++i) {
                        lvi.iSubItem = i;
                        lvi.pszText = buf[i];
                        SendMessage(hList, LVM_GETITEM, 0, (long)&lvi);
                    }
                    if (DialogBox(hInst, TEXT("StudInfo"), hwnd, EditDlgProc)) InvalidateRect(hwnd, NULL, TRUE);
                    break;
                case IDDELETE:
                    lvi.iItem = SendMessage(hList, LVM_GETNEXTITEM, -1, LVNI_SELECTED);
                    if (-1 == lvi.iItem) {
                        MessageBox(hwnd, "请选择一条记录!", "Delete Info", MB_OK | MB_ICONWARNING);
                        break;
                    }
                    SendMessage(hList, LVM_DELETEITEM, lvi.iItem, 0);
                    lvi.mask = LVIF_TEXT;
                    lvi.iSubItem = 0;
                    lvi.pszText = buf[0];
                    lvi.cchTextMax = 20;
                    SendMessage(hList, LVM_GETITEM, 0, (long)&lvi);
                    sprintf(query, "DELETE FROM student WHERE id = %s", buf[0]);
                    ODBCConnect(hwnd);
                    ODBCQuery(query);
                    ODBCQueryEnd();
                    ODBCDisconnect(hwnd);
                    break;
                case IDEXIT:
                    SendMessage(hwnd, WM_CLOSE, 0, 0);
                    break;
                case IDABOUT:
                    MessageBox(hwnd, "Students Info Administration System\nDeveloped by anonymous", "About", MB_OK | MB_ICONINFORMATION);
            }
            break;
        case WM_SIZE:
            MoveWindow(hList, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);
            break;
        case WM_DESTROY:
            PostQuitMessage(0);
            break;
        default:
            return DefWindowProc(hwnd, message, wParam, lParam);
    }

    return 0;
}

初识加瓦

zerray | 八月 24th, 2005 - 20:12

Hello World的程序就不记了,太EASY了。

试着写了个A + B problem的程序,就遇到了个问题。怎么读整数呢?System.in.read不行,难道要一个一个读字符再自己做转换?不会吧,那也太麻烦了。再 想想别的办法,翻了翻文档,挑了一个java.io.DataInputStream,用方法readInt试了一下,还是不行:

readInt

 public final int readInt() throws IOException
从当前数据输入流中读取一个有符号的 32 位整数。 此方法从基本输入流中读入四个字节。 如果读入的字节,顺序为 b1b2,b3b4, 满足 0 <= b1, b2,b3,b4 <= 255, 那么结果等于:

    (b1 << 24) | (b2 << 16) + (b3 << 8) +b4

该方法将一直阻塞,直到此四个字节数据被读入, 或检测到了数据流尾或抛出异常。

返回值:
当前输入流的下 四个字节,解释为一个 int
抛出: EOFException
如 果在读入四个字节前到达了文件尾。
抛出: IOException
如果发生某个 I/O 错误。

大 名鼎鼎的加瓦不会连读个数都这么不方便吧?继续找文档,在util里找到了一个叫Scanner的类,用它的nextInt方法,hoho,这会就对了 嘛。

nextInt

public int nextInt()
Scans the next token of the input as an int.An invocation of this method of the form nextInt() behaves in exactly the same way as the invocation nextInt(radix), where radix is the default radix of this scanner.

Returns:
the int scanned from the input
Throws:
InputMismatchException – if the next token does not match the Integer regular expression, or is out of range
NoSuchElementException – if input is exhausted
IllegalStateException – if this scanner is closed

下面是完整的程序:

import  java.io.*;
import java.util.*;

public class Main {
public  static void main(String args[]) {
int a, b;
try {
Scanner  in = new Scanner(System.in);
a = in.nextInt();
b =  in.nextInt();
System.out.println(a + b);
}
catch  (Exception e) {}
}
}

嘿嘿,我的第一个加瓦程序,志之^_^

至于加瓦其它的那么多特性, 以后慢慢体验吧!

一个想法

zerray | 六月 23rd, 2005 - 22:29

网络防火墙是监控主机接收和发送的数据包,设定各种各样的rule,进而保证系统的安全。

病毒防火墙(实时监控)是根据病毒特征码来检查是否有病毒正要运行。

那么可不可以把网络防火墙的思想用在防病毒上呢?

例如实现一个 API 防火墙,对于每个正在运行的程序,监控它要调用的 API ,对于一些有可能造成破坏的 API 调用弹出提示,由用户决定是否允许调用。这样,一些像 RegCreateKey,WriteProcessMemory,CreateRemoteThread 等病毒常用的函数需要经过用户允许才能调用,对于病毒的发现和预防都很有用。实现的话用 Hook API 就可以吧?不知道对系统资源消耗有多大。

这只是我自己的空想,可能没什么价值,也可能已经有这样的软件了。欢迎感兴趣的朋友们跟我讨论:)

如何实现防火墙钩子驱动

zerray | 六月 20th, 2005 - 21:33

介绍

也许,防火墙钩子驱动是在 Windows 系统下开发数据包过滤程序的方法中最没有文档可查的一个了。微软没有给出关于它的任何文档,你唯一能够找到点儿什么的地方是 DDK 头文件(ipFirewall.h)。事实上,当我安装了 Windows 2000 DDK,我对于发现这个 .h 文件(和它的内容)非常惊奇,因为没有文档提到防火墙钩子的存在。在下一个版本的 DDK 中,微软加了一些关于它的文档:“这个方法存在但不建议使用”。

然而,因为它是个实现防火墙的简单方法,我认为了解防火墙钩子驱动如何工作将是很有趣的。

防火墙钩子驱动

我不明白为什么微软不建议使用防火墙钩子驱动的开发。对于开发一个完整的防火墙方案我的确不建议用它,但对于小程序,它可是个好的选择。基本上,防 火墙钩子驱动能够做过滤钩子驱动(见我的文章《在 Windows 2000/XP下开发防火墙》)所做的相同工作,但限制更少。

可能你还记得,过滤钩子函数仅允许在系统中安装一个过滤函数。如果有程序已经使用了这个功能,你的程序就没用了。使用防火墙钩子驱动,你不会遇到这 个问题。你可以安装你需要的所有过滤函数。每个过滤函数被赋予一个优先级,于是系统会一个接一个的(以优先级顺序)调用过滤函数知道一个函数返回 “DROP PACKET(丢弃包)”。如果所有的函数都返回“ALLOW PACKET(允许包)”,这个包就被允许通过了。你可以把它想象称一个过滤函数链。当其中一个返回了“DROP PACKET”链就被切断了。链中每个函数的顺序是由它的优先级值数给出的。

在上图中,我表示了以下过程:

  1. 你的主机收到了一个包。IP 驱动拥有以优先级排列的过滤函数列表(具有更高优先级的函数是 Filter Function 1)。
  2. 首先,IP 驱动将包传递给优先级最高的过滤函数并等待返回值。
  3. Filter function 1 返回“ALLOW PACKET”。
  4. 因为 Filter function 1 允许了包,IP 驱动将包传递给了下一个过滤函数:Filter function 2。
  5. 在图示情况下,Filter function 2 返回了“DROP PACKET”。所以,IP 驱动丢弃了包而不在继续调用下一个过滤函数。

你会发现的过滤钩子驱动的另一个问题是对于包的发送,你不能访问包中的数据内容。但是,用防火墙钩子驱动你可以访问所有的数据。防火墙钩子过滤函数 接收到的数据结构比过滤钩子驱动的更复杂。它更接近于 NDIS 驱动中包的结构,整个的包是由一个缓冲链组成的。但是请耐心点,你将在后面看到更多。

像过滤钩子驱动一样,防火墙钩子驱动只是个用来安装回调函数的内核模式驱动(但防火墙钩子驱动 在 IP 驱动中安装回调函数)。实际上,安装防火墙钩子驱动的过程与安装过滤钩子驱动类似。在 ipFirewall.h 文件中,你会看到下面的内容:

typedef struct _IP_SET_FIREWALL_HOOK_INFO
{
    // Packet filter callout. 
    IPPacketFirewallPtr FirewallPtr;

    // Priority of the hook
    UINT Priority;

       // if TRUE then ADD else DELETE 
       BOOLEAN Add;
} IP_SET_FIREWALL_HOOK_INFO, *PIP_SET_FIREWALL_HOOK_INFO;   

#define DD_IP_DEVICE_NAME L\\Device\\Ip
#define _IP_CTL_CODE(function, method, access) \
        CTL_CODE(FSCTL_IP_BASE, function, method, access)
#define IOCTL_IP_SET_FIREWALL_HOOK \
        _IP_CTL_CODE(12, METHOD_BUFFERED, FILE_WRITE_ACCESS)

这几行代码告诉你了安装回调函数的大致方法。你只需用你的回调函数的数据填写 IP_SET_FIREWALL_HOOK_INFO 结构并安装它发送 IOCTL IOCTL_IP_SET_FIREWALL_HOOK 到 IP 设备。简单,如果你与驱动程序打过交道或是曾做过那个有文档的过滤钩子函数。这个结构的一个重要参数是优先级域。每个优先级域包含了该过滤函数的优先级, 其值越来越大。

PDEVICE_OBJECT ipDeviceObject=NULL;
IP_SET_FIREWALL_HOOK_INFO filterData; 

//..... 

// Init structure filterData.
FirewallPtr = filterFunction;
filterData.Priority = 1;
filterData.Add = TRUE; 

//.... 

// Send the commando to ip driver
IoCallDriver(ipDeviceObject, irp);
如果你要卸载过滤函数,可以用同样的代码,只要把 filterData.Add 的值改成 FALSE 就行了。

过滤函数

防火墙钩子驱动的过滤函数比过滤钩子驱动的更复杂。因为没有关于该函数及其参数的文档,所以复 杂度增加了。该函数具有以下特征:

FORWARD_ACTION cbFilterFunction(VOID **pData,
                                     UINT RecvInterfaceIndex,
                                     UINT *pSendInterfaceIndex,
                                     UCHAR *pDestinationType,
                                     VOID *pContext, 
                                     UINT ContextLength,
                                     struct IPRcvBuf **pRcvBuf);

用耐心和调试方法(还有参数名字的解释:)),我得出了以下关于参数的信息:

pData *pData 指向一个带有包缓冲的 (struct IPRcvBuf *) 结构。
RecvInterfaceIndex 数据接收的接口。
pSendInterfaceIndex 指向无符号整型的指针,包含数据发送的索引值。虽然它是个指针,改变它的值并不能让包改变路径:(。
pDestinationType 指向无符号整型的指针,指出目标类型:本地网络,远程,广播,多播,等。
pContext 指向 FIREWALL_CONTEXT_T 结构的指针,使你可以像流入和流出的包一样取得包的信息。
ContextLength 指向的缓冲区的大小。其值总是 sizeof(FIREWALL_CONTEXT_T)。
pRcvBuf *pRcvBuf 总是指向 NULL。

这些信息可能会在将来的 Windows 版本中改变,因为没有官方的文档可用。我只能保证这是我在 Windows 2000 和 Windows XP 下测试得到的这些域的意义。

对于每个包,我们的函数会被调用,并且取决于它的返回值,包会被丢弃或是放行。在过滤函数中你可以返回的值有:

FORWARD 包被允许。
DROP 包被丢弃。
ICMP_ON_DROP 包被丢弃并有一个 ICMP 包被发送到远程主机。

释放缓冲

在防火墙钩子过滤函数中,你并不是像在过滤钩子驱动中那样直接接收带有包头部和内容的缓冲区。 在一些测试后,我弄清了缓冲的内部结构。如我前面所说,发送/接收 包是被传递给 pData 参数的。*pData 指向一个 IPRcvBuf 结构:

struct IPRcvBuf
{
    // Point to the next buffer in the chain
    struct IPRcvBuf *ipr_next;

    // Always 0 
    UINT ipr_owner;

    // Buffer data
    UCHAR *ipr_buffer;

    // Buffer data size
    UINT ipr_size;

    // In my tests always a pointer to NULL.
    // Maybe the system could use MDLs instead of IPRcvBuf structures (but
    // i never have seen it).
    PMDL ipr_pMdl;

    // Always a pointer to NULL.
    UINT *ipr_pClientCnt;

    // Always a pointer to NULL.
    UCHAR *ipr_RcvContext;

    // Always 0. I suppose this field is a offset into buffer data
    // but because I haven't a value different from 0, I can affirm it.
    UINT ipr_RcvOffset;

    // In Windows 2003 DDK the name of this field have changed to flags.
    // In my tests I always get 0 value for local traffic and 2 for remote.
    // It's the only thing I can tell you about this field.
       ULONG ipr_promiscuous;
};

按照我们的意图,只需要知道域 ipr_next, ipr_buffer 和 ipr_size。ipr_buffer 域包含包的 ipr_size 字节。然而,整个包不必在一个缓冲里,系统能够链接多个缓冲。正因如此,ipr_next 域被使用了。这个域指向了下一个带有包数据的结构。当数据结构中的 ipr_next 指向 NULL 时我们就得到了整个的数据包。所以,我们在防火墙钩子驱动中发现了链接缓冲结构正如在 NDIS 驱动中所见。在我的测试中,对于所有接收的包,函数只接收到了一个结构,在其缓冲中带有全部的数据。对发送的包,我发现若干个链接的缓冲,每个包含一种协 议的信息。我的意思是,例如我发送一个 ICMP 包,就会有三个链接的缓冲:一个带有 IP 头部,一个带有 ICMP 头部,而另一个带有数据。然而,就像我们对 NDIS 驱动做的一样,我们不能依赖于系统如何填写这些缓冲。

在下面的图中,你可以看到在防火墙钩子驱动中如何构建数据包的例子:

简单的,你可以由下面的代码看到如何从链接缓冲中得到一个带有数据包内容的正统缓冲:

char *pPacket = NULL;
int iBufferSize;
struct IPRcvBuf *pBuffer = (struct IPRcvBuf *) *pData;

// First, I calculate the total size of the packet
iBufferSize = buffer->ipr_size;
while(pBuffer->ipr_next != NULL)
{
    pBuffer = pBuffer->ipr_next;
    iBufferSize += pBuffer->ipr_size;
}

// Reserve memory to the lineal buffer.
pPacket = (char *) ExAllocatePool(NonPagedPool, iBufferSize);
if(pPacket != NULL)
{
    unsigned int iOffset = 0;
    pBuffer = (struct IPRcvBuf *) *pData;

    // we are going to copy each buffer of the chain in the lineal buffer.
    memcpy(pPacket, pBuffer->ipr_buffer, pBuffer->ipr_size);
    while(pBuffer->ipr_next != NULL)
    {
        iOffset += pBuffer->ipr_size;
        pBuffer = pBbuffer->ipr_next;
        memcpy(pPacket + iOffset, pBuffer->ipr_buffer,
                                  pBbuffer->ipr_size);
    }
}

还有,对于所有好奇的人(在你问到我之前:P),你可以修改包的数据,风险自己承担。没有任何工具来做这类的软件,要做类似的东西,我建议你实现一 个 NDIS IM 驱动或者 TDI 过滤驱动。我没有做太多测试但我不很信赖防火墙钩子驱动改变数据包内容的稳定性。为什么?因为我们不知道 IP 驱动怎样管理这些缓冲和修改它们要冒多大的风险。一句话,我建议:只许看,不许摸。

结合的时候到了!

好了,现在我们知道了过滤函数的语法和传给它的包的格式。现在,我们要知道的就剩如何把这两样 东西结合成一个数据包过滤程序了。我用的方法是试着定义一个类似于过滤钩子驱动中用到的过滤函数,因为这样更容易理解。由于传给过滤函数的参数在这两个驱 动中是不一样的,对于防火墙钩子,我实现了一个中间函数(实际的防火墙钩子过滤函数)来包裹过滤函数。我的意思是,在防火墙钩子过滤函数中,我用了一个函 数来处理包,将它拷贝到一个正统的缓冲中,然后把它传递给过滤函数。有下面的代码,我想你能更好的理解它:

FORWARD_ACTION cbFilterFunction(VOID **pData,
                                     UINT RecvInterfaceIndex,
                                     UINT *pSendInterfaceIndex,
                                     UCHAR *pDestinationType,
                                     VOID *pContext,
                                     UINT ContextLength,
                                     struct IPRcvBuf **pRcvBuf)
{
    FORWARD_ACTION result = FORWARD;
    char *pPacket = NULL;
    int iBufferSize;
    struct IPRcvBuf *pBbuffer =(struct IPRcvBuf *) *pData;
    PFIREWALL_CONTEXT_T fwContext = (PFIREWALL_CONTEXT_T)pContext;

    IPHeader *pIpHeader;

    // Convert chained buffer to lineal buffer as we see before.
    // This won't be the fastest code but
    // will help us to understand better the method.

    // ...........

    pIpHeader = (IPHeader *)pPacket;

    // Call the real filter function and return result     
    result = FilterPacket(pPacket,
                          // length in bytes = ipp->headerLength * (32 bits/8)
                          pPacket + (pIpHeader ->headerLength * 4),
                          iBufferSize - (pIpHeader ->headerLength * 4),
                          (fwContext != NULL) ? fwContext->Direction: 0,
                          RecvInterfaceIndex,
                          (pSendInterfaceIndex != NULL) ? *pSendInterfaceIndex : 0);

    return result;
}

代码

你能很快认出这篇文章的程序。是的,图形界面和我在过滤钩子驱动中用的完全一样。为什么?因为我写了个简单的数据包过滤程序用来测试我写的所有防火 墙方法。这样,我有了一个通用的图形界面给它们,来提供相同的功能,但在底层,它们工作的不一样。我有这个程序的不同版本(只做最小的改动)来测试我的过 滤钩子驱动,防火墙钩子驱动,LSP DLL,TDI 过滤驱动,NDIS 驱动……所以,我想这个方法是好理解的。几乎不更改图形界面,这样你只想了解使用的新方法。

像我其它的文章一样,这个程序只实现了包的过滤。很多人问我关于加入一些更多的功能,像包记录,安装为服务……但我想要按照提供方法而不是答案的思 想。如果你想要加入一些这样的功能,我很高兴:)。你可以联系我,问所有你想要的问题。

结论

好了,一旦我写完了这篇文章,它就是你的了。我希望你能从中学到与我一样多的东西。Enjoy it!!



在Windows 2000/XP下开发防火墙

zerray | 六月 20th, 2005 - 21:24

介绍

如果你决定在 Linux 下开发一个防火墙,你可以找到很多资料和源代码,全部免费。但对 Windows 平台下的防火墙感兴趣的人们就有些困难了,不仅是找资料难,找免费的源代码更是个不可能的任务!

所以,我决定写这片文章来简述一下在 Windows 2000/XP 下开发防火墙的简单方法,借以帮助那些对此感兴趣的人们。

背景

在 Windows 2000 DDK 中,微软包含了一种新的网络驱动叫做过滤钩子驱动( Filter-Hook Driver )。使用它,你可以建立一个函数来过滤所有到达或离开接口的数据包。

因为关于这个主题的文档很少,并且没有例子,所以我写了这篇文章来介绍成功使用它所需的步骤。我希望这篇文章能帮你理解这个简单的方法。

过滤钩子驱动

正如我前面所说,过滤钩子驱动是微软在 Windows 2000 DDK 中引入的。实际上,它不是一个新的网络驱动类,它只是扩展 IP 过滤驱动(包含在 Windows 2000 及以后的版本中)功能的一种方法。

事实上,过滤钩子驱动不是网络驱动,而是内核模式驱动。基本的,我们在过滤钩子驱动中实现一个回调函数,然后把这个回调函数与 IP 过滤驱动注册到一起。这样做了之后,IP 过滤驱动会在一个数据包被发送或接受时调用我们的回调函数。那么……这么做主要的步骤有哪些呢?

我总结了以下五个步骤:

  1. 建立一个过滤器钩子驱动。这一步,你必须建立内核模式驱动,选择一个名字,DOS 名和其它驱动字符,没什么特别的要求但我建议你用描述性的名字。
  2. 如果我们要安装过滤函数,首先必须取得一个指向 IP 过滤驱动的指针。所以,这是第二个步骤。
  3. 我们已经有了指针,现在可以安装过滤函数了。我们可以通过发送指定的 IRP 来做这件事。在传递的“消息”数据中包含指向过滤函数的指针。
  4. 过滤数据包!!!
  5. 当我们决定停止过滤时,必须注销过滤函数。我们可以通过注册过滤函数到空指针来实现注销。

哦,只有五步,而且看起来很简单,但是……要怎样建立内核模式驱动呢?怎么取得指向 IP 过滤驱动的指针?怎么……是的,请稍等,我现在就来解释这些步骤:P,展示源码例子。

建立内核模式驱动

过滤钩子驱动是内核模式驱动,所以如果我们想做,就得做个内核模式驱动。这篇文章并不是“5分钟学会怎样开发内核模式驱动”,所以我假设读者们已经 有了这方面的知识。

过滤钩子驱动的结构是典型的内核模式驱动结构:

  1. 建立设备的驱动入口(注:建立设备是驱动入口的定语),设置标准例程来处理 IRP(分派,加载,卸载,创建……)和建立与其它应用程序通讯的符号连接。
  2. 管理 IRP 的标准例程。在你开始编写代码之前,我建议,考虑你要从设备驱动引出到应用程序哪些 IOCTL。在我的例子中,我实现了四个 IOCTL 代码:START_IP_HOOK(注册过滤函数)、STOP_IP_HOOK(注销过滤函数)、ADD_FILTER(安装新规则)和 CLEAR_FILTER(清除所有的规则)。
  3. 还必须为我们的驱动实现另一个函数:过滤函数。

我建议你使用程序生成内核模式驱动的结构,然后你只要把添加代码到生成的函数里就行了。例如,我在这个项目中使用的 QuickSYS

你可以看我自己实现的驱动结构,代码如下:

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,
                IN PUNICODE_STRING RegistryPath)
{

    //....

    dprintf("DrvFltIp.SYS: entering DriverEntry\n");

    //we have to create the device
    RtlInitUnicodeString(&deviceNameUnicodeString, NT_DEVICE_NAME);

    ntStatus = IoCreateDevice(DriverObject,
                0,
                &deviceNameUnicodeString,
                FILE_DEVICE_DRVFLTIP,
                0,
                FALSE,
                &deviceObject);

    if ( NT_SUCCESS(ntStatus) )
    {
        // Create a symbolic link that Win32 apps can specify to gain access
        // to this driver/device
        RtlInitUnicodeString(&deviceLinkUnicodeString, DOS_DEVICE_NAME);

        ntStatus = IoCreateSymbolicLink(&deviceLinkUnicodeString,
                                        &deviceNameUnicodeString);

        //....

        // Create dispatch points for device control, create, close.

        DriverObject->MajorFunction[IRP_MJ_CREATE]         =
        DriverObject->MajorFunction[IRP_MJ_CLOSE]          =
        DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DrvDispatch;
        DriverObject->DriverUnload                         = DrvUnload;
    }

    if ( !NT_SUCCESS(ntStatus) )
    {
        dprintf("Error in initialization. Unloading...");

        DrvUnload(DriverObject);
    }

    return ntStatus;
}

NTSTATUS DrvDispatch(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{

    // ....

    switch (irpStack->MajorFunction)
    {
    case IRP_MJ_CREATE:

        dprintf("DrvFltIp.SYS: IRP_MJ_CREATE\n");

        break;

    case IRP_MJ_CLOSE:

        dprintf("DrvFltIp.SYS: IRP_MJ_CLOSE\n");

        break;

    case IRP_MJ_DEVICE_CONTROL:

        dprintf("DrvFltIp.SYS: IRP_MJ_DEVICE_CONTROL\n");

        ioControlCode = irpStack->Parameters.DeviceIoControl.IoControlCode;

        switch (ioControlCode)
        {
        // ioctl code to start filtering
        case START_IP_HOOK:
        {
            SetFilterFunction(cbFilterFunction);

            break;
        }

        // ioctl to stop filtering
        case STOP_IP_HOOK:
        {
            SetFilterFunction(NULL);

            break;
        }

        // ioctl to add a filter rule
        case ADD_FILTER:
        {
            if(inputBufferLength == sizeof(IPFilter))
            {
                IPFilter *nf;

                nf = (IPFilter *)ioBuffer;

                AddFilterToList(nf);
            }

            break;
        }

        // ioctl to free filter rule list
        case CLEAR_FILTER:
        {
            ClearFilterList();

            break;
        }

        default:
            Irp->IoStatus.Status = STATUS_INVALID_PARAMETER;

            dprintf("DrvFltIp.SYS: unknown IRP_MJ_DEVICE_CONTROL\n");

            break;
    }

        break;
    }

    ntStatus = Irp->IoStatus.Status;

    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    // We never have pending operation so always return the status code.
    return ntStatus;
}

VOID DrvUnload(IN PDRIVER_OBJECT DriverObject)
{
    UNICODE_STRING deviceLinkUnicodeString;

    dprintf("DrvFltIp.SYS: Unloading\n");

    SetFilterFunction(NULL);

    // Free any resources
    ClearFilterList();

    // Delete the symbolic link
    RtlInitUnicodeString(&deviceLinkUnicodeString, DOS_DEVICE_NAME);
    IoDeleteSymbolicLink(&deviceLinkUnicodeString);

    // Delete the device object
    IoDeleteDevice(DriverObject->DeviceObject);
}
我们已经做好了驱动的主要代码,接下来是过滤钩子驱动的代码。

注册过滤函数

在上面的代码中,你已经看到了一个叫做 SetFilterFunction(..) 的函数。实现这个函数是用来注册函数到 IP 过滤驱动的。有下面的几步:

  1. 首先,我们必须取得一个指向 IP 过滤驱动的指针。这要求 IP 过滤驱动已经安装并运行了。我的用户应用程序,在加载这个驱动之前就加载并启动了 IP 过滤驱动,来保证这一点。
  2. 第二,我们必须建立一个 IRP 指定 IOCTL_PF_SET_EXTENSION_POINTER 作为 IO 控制代码。我们必须以参数的形式传递一个 PF_SET_EXTENSION_HOOK_INFO 结构,其中包含了指向过滤函数的指针。如果你要卸载这个函数,你必须按照同样的步骤并用 NULL 代替指向过滤函数的指针。
  3. 发送建立的 IRP 到设备驱动。

这里,对于这个驱动有一个更大的问题。只有一个过滤函数可以被安装,所以如果其它的应用程序安装了一个,那你就不能安装你的函数了。

下面是这个函数的代码:

NTSTATUS SetFilterFunction
            (PacketFilterExtensionPtr filterFunction)
{
    NTSTATUS status = STATUS_SUCCESS, waitStatus=STATUS_SUCCESS;
    UNICODE_STRING filterName;
    PDEVICE_OBJECT ipDeviceObject=NULL;
    PFILE_OBJECT ipFileObject=NULL;

    PF_SET_EXTENSION_HOOK_INFO filterData;

    KEVENT event;
    IO_STATUS_BLOCK ioStatus;
    PIRP irp;

    dprintf("Getting pointer to IpFilterDriver\n");

    //first of all, we have to get a pointer to IpFilterDriver Device
    RtlInitUnicodeString(&filterName, DD_IPFLTRDRVR_DEVICE_NAME);
    status = IoGetDeviceObjectPointer(&filterName,STANDARD_RIGHTS_ALL,
                                    &ipFileObject, &ipDeviceObject);

    if(NT_SUCCESS(status))
    {
        //initialize the struct with functions parameters
        filterData.ExtensionPointer = filterFunction;

        //we need initialize the event used later by
        //the IpFilterDriver to signal us
        //when it finished its work
        KeInitializeEvent(&event, NotificationEvent, FALSE);

        //we build the irp needed to establish fitler function
        irp = IoBuildDeviceIoControlRequest(IOCTL_PF_SET_EXTENSION_POINTER,
                                                            ipDeviceObject,
        if(irp != NULL)
        {
            // we send the IRP
            status = IoCallDriver(ipDeviceObject, irp);

            //and finally, we wait for
            //"acknowledge" of IpFilter Driver
            if (status == STATUS_PENDING)
            {
                waitStatus = KeWaitForSingleObject(&event,
                                Executive, KernelMode, FALSE, NULL);

                if (waitStatus != STATUS_SUCCESS )
                    dprintf("Error waiting for IpFilterDriver response.");
            }

            status = ioStatus.Status;

            if(!NT_SUCCESS(status))
                dprintf("Error, IO error with ipFilterDriver\n");
        }

        else
        {
            //if we cant allocate the space,
            //we return the corresponding code error
            status = STATUS_INSUFFICIENT_RESOURCES;

            dprintf("Error building IpFilterDriver IRP\n");
        }

        if(ipFileObject != NULL)
            ObDereferenceObject(ipFileObject);

        ipFileObject = NULL;
        ipDeviceObject = NULL;
    }

    else
        dprintf("Error while getting the pointer\n");

    return status;
}

你会发现当我们完成了建立过滤函数的处理,我们必须废除( de-reference )在我们取得指向设备驱动的指针时获得的文件对象。我用了一个将在 IP 过滤驱动完成对 IRP 的处理时提示的事件。

过滤函数

我们已经看到了如何开发驱动和如何安装过滤函数,但我们还不知道过滤函数是什么样的。

我已经说过了这个函数总是在主机接受或发送一个数据包时被调用。根据这个函数的返回值,系统来决定对这个包作些什么。

这个函数的原型必须是这样的:

typedef  PF_FORWARD_ACTION
(*PacketFilterExtensionPtr)(
  // Ip Packet Header
  IN unsigned char *PacketHeader,
  // Packet. Don't include Header
  IN unsigned char *Packet,
  // Packet length. Don't Include length of ip header
  IN unsigned int PacketLength,
  // Index number for the interface adapter
  //over which the packet arrived
  IN unsigned int RecvInterfaceIndex,
  // Index number for the interface adapter
  //over which the packet will be transmitted
  IN unsigned int SendInterfaceIndex,
  //IP address for the interface
  //adapter that received the packet
  IN IPAddr RecvLinkNextHop,
  //IP address for the interface adapter
  //that will transmit the packet
  IN IPAddr SendLinkNextHop
  );

PF_FORWARD_ACTION 是一个枚举类型,可以取值为:

  • PF_FORWARD指定 IP 过滤驱动立即向 IP 栈返回 forward 响应。对于本地包,IP 将他们直接入栈。如果包的目的地是另一台计算机并且允许路由,IP 将它们相应的路由。
  • PF_DROP指定 IP 过滤驱动立即向 IP 栈返回 drop 响应。IP 应该丢弃数据包。
  • PF_PASS指定 IP 过滤驱动过滤包并向 IP 栈返回结果响应。IP 过滤驱动如何处理过滤数据包取决于数据包过滤 API 的设置。如果过滤钩子决定不处理这个包而交给 IP 过滤驱动来过滤这个包,返回这个 pass 响应。

虽然 DDK 文档只包含了这三个值,但你查看 pfhook.h(过滤钩子驱动需要包含)就会看到另一个值。这个值是 PF_ICMP_ON_DROP 。我估计这个值对应的是丢弃包并以一个 ICMP 包反馈错误信息。

正如你在过滤函数的定义中看到的,数据包及它的报头是以指针传递的。所以,你可以修改报头或负载然后传递该包。这是很有用的,例如做网络地址转换( NAT )。如果你改变了目标地址,IP 会路由该包。

在我的实现中,过滤函数将每个包和一个由用户应用程序引入的规则列表比较。这个列表是用链表实现的,由每个 START_IP_HOOK IOCTL 在运行时建立。你可以在我的代码中看到这些。

代码

在这篇文章的第一个版本中包含了一个简单的例子,但由于有人想让我帮他开发实际的程序,我把它更新成了一个更复杂的例子。新的例子时一个小的数据包 过滤程序。用这个新程序你可以实现你自己的过滤规则,就像你在一些商业防火墙软件中做的那样。

在第一个版本中,程序包含两个组件:

  • 用户应用程序:一个 MFC 应用程序用来管理过滤规则。这个应用程序发送规则到驱动程序并决定驱动什么时候开始过滤。过滤数据分三步:
    • 定义你需要的规则。用添加和删除命令你可以添加或删除过滤规则。
    • 安装规则。当你定义好了规则,点击安装按钮,发送它们到驱动程序。
    • 开始过滤。你只需要点击开始按钮来开始过滤。
  • 过滤钩子驱动:根据由用户应用程序接收到的过滤规则来过滤 IP 数据包的驱动。

过滤钩子驱动必须和用户应用程序的可执行文件处在同一个目录中。

为什么用这种方法开发防火墙?

这不是在 Windows 开发防火墙的唯一方法,还有其它的像 NDIS 防火墙,TDI 防火墙,Winsock 层的防火墙,数据包过滤 API,……所以我总结了一些过滤钩子驱动的优点和缺点,以供你在将来需要用到时参考。

  • 用这种方法有很大的灵活性。你可以过滤所有的 IP 数据包(以及上层的数据包)。但你不能过滤更底层的报头,例如,你不能过滤以太网帧。你需要用 NDIS 过滤来实现,更复杂也更灵活。
  • 这是一个简单的方法。安装防火墙和实现过滤函数用这种方法都是很简单的。但数据包过滤 API 更为简单,虽然没有它灵活。你不能访问数据包的内容,也不能用数据包过滤 API 修改它。

结果:过滤钩子驱动并不是最好的,但它也没有什么坏特性。可是为什么这个方法没有用在商业产品中呢?

答案很简单。虽然这个驱动没有不好的特性,但它有一个很大的缺点。正如我之前提到的,每次只能安装一个过滤函数。我们可以开发一个强大的防火墙,它 可能被上千的用户下载并安装,但如果其它的应用程序使用了过滤(并在之前安装了过滤函数),我们的程序就什么都做不了了。

这个方法还有另一个缺点没有被微软的文档提到。虽然 DDK 文档说你可以在过滤函数中访问数据包内容,但那不是真的。你可以访问收到的包的内容,但对于发送的包,你只能读 IP 和 TCP,UDP 或 ICMP 报头。我不明白为什么……

微软在 Windows XP 中引入了另一种没有这个限制的驱动:防火墙钩子驱动。它的安装很类似,但微软并不建议使用它,以为“它消耗太多的网络栈”。也许这个驱动会在以后的 Windows 版本中消失。

结论

好了,到结尾了。我只到这并不是开发防火墙的最好方法(我在前面提到了它的缺点)。但我认为这对正在查找资料和对此感兴趣的人们是个好的开始。

我希望你能得到些帮助,并开始想要开发个强大的防火墙了。