网络过滤:TDI过滤、WFP框架、网络数据包拦截、防火墙驱动基础

说到Windows下的网络过滤,很多做应用层开发的朋友第一反应就是WinPcap、Npcap或者Raw Socket。但到了内核层面,玩法完全不一样。我最早接触这个领域是在做一款企业级终端防火墙的时候,那时候Windows还是XP/Vista时代,TDI过滤还是主流方案。后来到了Vista之后,微软推出了WFP框架,整个网络过滤的架构发生了翻天覆地的变化。

今天我们就来聊聊这两个时代的网络过滤技术。说白了,就是搞清楚两个问题:怎么拦截网络数据包?怎么在内核里做防火墙?

TDI过滤:老司机才懂的方案

TDI,全称Transport Driver Interface,是Windows网络栈中传输层的一个接口层。它位于TDI Client(比如AFD.sys)和TDI Transport(比如TCP/IP协议驱动)之间。TDI过滤驱动,就是在这个接口上做手脚。

我个人习惯把TDI过滤理解成「中间人攻击」——你假装自己是传输层驱动,让上层把数据交给你,你再决定是放行、修改还是丢弃。

TDI过滤的核心机制

TDI过滤驱动需要做两件事:

  • 绑定到TDI设备对象:通过IoGetDeviceObjectPointer获取TDI传输驱动的设备对象,然后用自己的设备对象挂载上去。
  • 拦截IRP:TDI通信走的是IRP(I/O Request Packet),你需要在Dispatch函数里处理TDI_REQUEST、TDI_SEND、TDI_RECEIVE等IRP。

嗯,这里要注意:TDI过滤驱动必须处理连接建立数据收发两个阶段。连接阶段你可以决定是否允许某个IP/端口建立连接;数据收发阶段你可以检查每个数据包的内容。

核心要点:TDI过滤工作在传输层,你能看到的是TCP/UDP的伪头部(IP地址、端口、协议类型),但看不到完整的IP包。这意味着你没法做IP分片重组,也没法处理ICMP。

一个简单的TDI过滤示例

下面这段代码展示如何在TDI过滤驱动中拦截TCP连接请求:

NTSTATUS TdiFilterDispatch(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
    PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation(Irp);
    NTSTATUS status = STATUS_SUCCESS;

    // 检查是否是TDI请求
    if (irpSp->MajorFunction == IRP_MJ_INTERNAL_DEVICE_CONTROL)
    {
        // 获取TDI请求信息
        PTDI_REQUEST_KERNEL tdiRequest = 
            (PTDI_REQUEST_KERNEL)irpSp->Parameters.Others.Argument1;

        if (tdiRequest->RequestNotifyObject == TdiEventConnect)
        {
            // 这里可以检查远程地址
            PTRANSPORT_ADDRESS remoteAddr = 
                (PTRANSPORT_ADDRESS)tdiRequest->RequestConnectionInformation;

            // 检查IP和端口,决定是否放行
            if (IsBlockedAddress(remoteAddr))
            {
                // 拒绝连接
                Irp->IoStatus.Status = STATUS_CONNECTION_REFUSED;
                IoCompleteRequest(Irp, IO_NO_INCREMENT);
                return STATUS_CONNECTION_REFUSED;
            }
        }
    }

    // 放行,交给下层驱动
    return TdiPassThrough(DeviceObject, Irp);
}

这段代码看起来简单,但实际项目中坑很多。我曾经在调试一个TDI过滤驱动时,发现某些应用程序连接不上网络,查了两天才发现是TDI事件通知的上下文结构体没对齐——Windows对结构体对齐要求很严格,稍微偏一点就蓝屏。

避坑指南:TDI过滤驱动在Windows 8之后已经被微软标记为「过时技术」。如果你还在做新项目,建议直接上WFP。我当年维护的一个老防火墙项目,从TDI迁移到WFP花了整整三个月,因为两者的设计哲学完全不同。

WFP框架:新时代的网络过滤标准

WFP(Windows Filtering Platform)是微软从Vista开始推出的网络过滤框架。它把网络栈分成了多个分层(Layers)标注点(Aleter Points),你可以在不同层级注册回调函数,拦截或修改网络数据。

说白了,WFP就是给你一张「网络栈地图」,告诉你每个数据包会经过哪些检查点。你只需要在感兴趣的点上「贴个标签」,写个回调函数就行。

WFP的核心概念

概念 说明
分层(Layer) 网络栈的不同层级,如ALE层(应用层强制)、传输层、网络层
标注点(Aleter Point) 分层中的具体检查点,比如FWPM_LAYER_INBOUND_TRANSPORT_V4
标注(Aleter) 你注册的回调函数,决定数据包是放行、阻止还是修改
标注上下文(Aleter Context) 传递给回调函数的自定义数据
过滤条件(Filter Condition) 匹配规则,比如源IP、目标端口、协议类型

WFP最让我喜欢的一点是:你不用自己处理IRP了。你只需要注册回调,框架会在合适的时机调用你。这大大降低了驱动开发的复杂度。

WFP过滤驱动的典型结构

一个WFP过滤驱动通常包含以下几个步骤:

  1. 注册标注提供者:调用FwpsAleterRegister0注册你的标注驱动。
  2. 添加标注:在特定分层上添加标注,指定回调函数。
  3. 实现回调函数:在回调中检查数据包,返回FWP_ALE_TERM_BLOCK或FWP_ALE_TERM_PERMIT。
  4. 清理资源:在驱动卸载时移除标注并注销提供者。

个人经验:WFP的回调函数运行在DISPATCH_LEVEL,这意味着你不能做任何分页内存访问、不能等待、不能调用大部分同步函数。我刚开始写WFP驱动时,在回调里调用了ExAllocatePoolWithTag,结果系统直接蓝屏——因为那个函数在DISPATCH_LEVEL下不能调用分页内存。

WFP数据包拦截示例

下面是一个在传输层拦截出站TCP连接的简化示例:

// 标注回调函数
NTSTATUS AleterClassifyFn(
    _In_ const FWPS_INCOMING_VALUES* inFixedValues,
    _In_ const FWPS_INCOMING_METADATA_VALUES* inMetaValues,
    _Inout_opt_ void* layerData,
    _In_opt_ const void* classifyContext,
    _In_ const FWPS_FILTER* filter,
    _In_ UINT64 flowContext,
    _Inout_ FWPS_CLASSIFY_OUT* classifyOut)
{
    // 获取远程IP和端口
    UINT32 remoteAddr = inFixedValues->incomingValue[
        FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_REMOTE_ADDRESS].value.uint32;
    UINT16 remotePort = inFixedValues->incomingValue[
        FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_REMOTE_PORT].value.uint16;

    // 检查是否在黑名单中
    if (IsAddressBlocked(remoteAddr, remotePort))
    {
        // 阻止连接
        classifyOut->actionType = FWP_ALE_TERM_BLOCK;
        return STATUS_SUCCESS;
    }

    // 放行连接
    classifyOut->actionType = FWP_ALE_TERM_PERMIT;
    return STATUS_SUCCESS;
}

// 注册标注
NTSTATUS RegisterAleter()
{
    FWPS_ALE_TERM_CALLOUT callout = {0};
    callout.flags = 0;
    callout.classifyFn = AleterClassifyFn;
    callout.notifyFn = AleterNotifyFn;
    callout.flowDeleteFn = NULL;

    return FwpsAleterRegister0(
        &calloutGuid,          // 标注GUID
        &callout,              // 标注函数表
        &calloutHandle);       // 返回的标注句柄
}

你想想看,这段代码比TDI过滤简洁多了吧?WFP帮你处理了IRP分发、同步、上下文管理等脏活累活,你只需要关注业务逻辑。

防火墙驱动基础:从拦截到管理

一个完整的防火墙驱动,不仅仅是拦截数据包那么简单。我参与过的几个防火墙项目,核心功能通常包括:

  • 规则引擎:支持IP、端口、协议、应用程序路径等多维度的过滤规则。
  • 状态检测:跟踪TCP连接状态,只允许合法连接的数据包通过。
  • 日志记录:记录被拦截的连接信息,包括时间、源/目标地址、端口等。
  • 用户态通信:通过IOCTL与用户态管理程序交互,动态更新规则。

我个人习惯把防火墙驱动的架构分成三层:

  1. 过滤层:WFP标注回调,负责数据包拦截和放行。
  2. 规则管理层:维护规则表,支持增删改查。
  3. 通信层:通过设备对象与用户态程序通信。

关键设计决策:规则表放在分页内存还是非分页内存?我的建议是:规则表本身放在分页内存(因为可能很大),但每个规则的关键字段(IP、端口)要缓存一份在非分页内存中,供DISPATCH_LEVEL下的回调函数使用。

SVG:WFP网络过滤架构图

下面这张图展示了WFP框架下网络数据包的过滤流程:

WFP网络过滤架构图 用户态 防火墙管理程序(通过IOCTL与内核通信) 内核态 WFP标注层 ALE_AUTH_CONNECT_V4 INBOUND_TRANSPORT_V4 OUTBOUND_TRANSPORT_V4 规则引擎 IP/端口/协议匹配 应用程序路径匹配 状态检测 通信层(设备对象 + IOCTL) 规则更新 / 日志上报 / 状态查询 数据包流入 → ← 放行/阻止

这张图展示了WFP过滤驱动的核心工作流程:数据包到达WFP标注层,标注回调函数查询规则引擎,根据匹配结果决定放行或阻止,同时通过通信层与用户态管理程序交互。

实战中的几个坑

最后分享几个我在实际项目中踩过的坑:

  • 性能问题:WFP回调函数里不要做耗时操作。我曾经在回调里查数据库(通过用户态通信),结果网络延迟飙升到几百毫秒。正确的做法是把规则缓存到内核内存中。
  • 蓝屏排查:WFP驱动蓝屏后,用WinDbg分析dump时,重点关注FwpsAleterClassify0的调用栈。我遇到过几次是因为标注上下文指针被意外释放导致的。
  • 兼容性:不同Windows版本的WFP API有差异。比如Windows 7和Windows 10的ALE分层GUID就不同。写代码时一定要用条件编译处理版本差异。
  • 测试环境:千万别在生产环境上调试WFP驱动。我建议用Hyper-V虚拟机搭建测试环境,快照功能可以让你快速恢复系统。

我的建议:如果你刚开始学习WFP,先从简单的ALE标注开始,只做IP和端口的黑白名单过滤。等熟悉了回调机制和同步问题后,再尝试更复杂的数据包修改和流跟踪功能。

网络过滤这块内容确实不少,但掌握了TDI和WFP的核心思想后,你会发现Windows网络栈其实没那么神秘。说白了,就是找到合适的拦截点,写好回调函数,处理好同步和性能问题。剩下的,就是业务逻辑了。


公众号:蓝海资料掘金营,微信deep3321