网络过滤: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过滤驱动通常包含以下几个步骤:
- 注册标注提供者:调用FwpsAleterRegister0注册你的标注驱动。
- 添加标注:在特定分层上添加标注,指定回调函数。
- 实现回调函数:在回调中检查数据包,返回FWP_ALE_TERM_BLOCK或FWP_ALE_TERM_PERMIT。
- 清理资源:在驱动卸载时移除标注并注销提供者。
个人经验: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与用户态管理程序交互,动态更新规则。
我个人习惯把防火墙驱动的架构分成三层:
- 过滤层:WFP标注回调,负责数据包拦截和放行。
- 规则管理层:维护规则表,支持增删改查。
- 通信层:通过设备对象与用户态程序通信。
关键设计决策:规则表放在分页内存还是非分页内存?我的建议是:规则表本身放在分页内存(因为可能很大),但每个规则的关键字段(IP、端口)要缓存一份在非分页内存中,供DISPATCH_LEVEL下的回调函数使用。
SVG:WFP网络过滤架构图
下面这张图展示了WFP框架下网络数据包的过滤流程:
这张图展示了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