如何使用WinDivert(C++)实时将域名映射到IP地址以实现流量整形?
如何使用WinDivert(C++)实时将域名映射到IP地址以实现流量整形?
嘿,我之前做过差不多的网站流量管控工具,刚好在WinDivert的域名-IP映射这块踩过不少坑,给你分享几个实际能用的方案,都是我项目里验证过的:
方案一:直接提取TLS Client Hello里的SNI(最推荐)
现在几乎所有HTTPS请求都会在TLS握手的Client Hello包里携带SNI(Server Name Indication)字段,这个字段就是明文的目标域名——完全不需要解析DNS,直接从截获的TCP payload里就能拿到,这是最准确实时的方式,毕竟不管客户端用的哪个IP,SNI里的域名不会错。
具体步骤:
- 用WinDivert设置过滤规则,只抓目标端口是443、且带有payload的TCP包:
tcp.DstPort == 443 and tcp.PayloadLength > 0 - 解析TCP payload里的TLS Client Hello结构,提取SNI字段(就是目标域名)
- 只要拿到域名,不管它对应哪个IP,后续这个连接的所有包都可以直接做延迟处理
简化的SNI解析代码片段:
我自己写了个轻量的解析函数,你可以直接改改用:
#include <string> #include <cstdint> bool ExtractSNI(const char* payload, size_t payloadLen, std::string& outSni) { // 先判断是不是TLS握手包(0x16是TLS Handshake的类型标识) if (payloadLen < 5 || payload[0] != 0x16) { return false; } size_t offset = 5; // 跳过TLS版本和长度的固定头部 // 检查是不是Client Hello(握手类型0x01) if (offset + 1 > payloadLen || payload[offset] != 0x01) { return false; } // 跳过握手消息长度、客户端版本、随机数、会话ID这些固定内容 offset += 4 + 2 + 32; if (offset + 1 > payloadLen) return false; uint8_t sessionIdLen = payload[offset]; offset += 1 + sessionIdLen; // 跳过密码套件列表 if (offset + 2 > payloadLen) return false; uint16_t cipherSuitesLen = (static_cast<uint16_t>(payload[offset]) << 8) | payload[offset+1]; offset += 2 + cipherSuitesLen; // 跳过压缩方法列表 if (offset + 1 > payloadLen) return false; uint8_t compressionLen = payload[offset]; offset += 1 + compressionLen; // 遍历TLS扩展,找SNI(扩展类型0) if (offset + 2 > payloadLen) return false; uint16_t extensionsTotalLen = (static_cast<uint16_t>(payload[offset]) << 8) | payload[offset+1]; offset += 2; while (offset + 4 <= payloadLen && extensionsTotalLen > 0) { uint16_t extType = (static_cast<uint16_t>(payload[offset]) << 8) | payload[offset+1]; uint16_t extLen = (static_cast<uint16_t>(payload[offset+2]) << 8) | payload[offset+3]; offset += 4; if (extType == 0) { // 这就是SNI扩展 if (offset + 2 > payloadLen) break; uint16_t sniListLen = (static_cast<uint16_t>(payload[offset]) << 8) | payload[offset+1]; offset += 2; while (offset + 3 <= payloadLen && sniListLen > 0) { uint8_t nameType = payload[offset]; uint16_t nameLen = (static_cast<uint16_t>(payload[offset+1]) << 8) | payload[offset+2]; offset += 3; if (nameType == 0 && offset + nameLen <= payloadLen) { // 主机名类型 outSni.assign(payload + offset, nameLen); return true; } offset += nameLen; sniListLen -= 3 + nameLen; } } else { offset += extLen; } extensionsTotalLen -= 4 + extLen; } return false; }
结合WinDivert的使用逻辑:
#include <Windows.h> #include "WinDivert.h" #include <string> #include <unordered_set> // 要限速的域名列表 std::unordered_set<std::string> g_restrictedDomains = {"example.com", "socialmedia.com"}; bool IsDomainRestricted(const std::string& domain) { return g_restrictedDomains.find(domain) != g_restrictedDomains.end(); } int main() { // 打开WinDivert,过滤443端口的TCP payload包 HANDLE divertHandle = WinDivertOpen( "tcp.DstPort == 443 and tcp.PayloadLength > 0", WINDIVERT_LAYER_NETWORK, 0, 0 ); if (divertHandle == INVALID_HANDLE_VALUE) { MessageBoxA(NULL, "打开WinDivert失败,请以管理员身份运行", "错误", MB_ICONERROR); return 1; } WINDIVERT_ADDRESS addr; char packetBuffer[WINDIVERT_MTU_MAX]; UINT packetLen; while (true) { // 接收包 if (!WinDivertRecv(divertHandle, packetBuffer, sizeof(packetBuffer), &addr, &packetLen)) { continue; } std::string sni; if (ExtractSNI(packetBuffer, packetLen, sni)) { // 检查是不是要限速的域名 if (IsDomainRestricted(sni)) { // 这里模拟2秒延迟,实际项目建议用异步队列处理,别直接Sleep阻塞线程 Sleep(2000); } } // 发送处理后的包 WinDivertSend(divertHandle, packetBuffer, packetLen, &addr, NULL); } WinDivertClose(divertHandle); return 0; }
方案二:本地DNS缓存+主动解析(兼容HTTP/旧场景)
如果还要处理HTTP(80端口)的流量,或者有些老客户端不支持SNI,可以用这个方案:
- 维护一个本地的域名-IP映射缓存,给每个映射加过期时间(比如1小时),定期刷新
- 当你要管控某个域名时,主动调用Windows的
getaddrinfo函数,把该域名对应的所有IP都加到限速列表里 - 同时,你可以用WinDivert过滤UDP 53端口的DNS响应包,解析里面的域名和IP,实时更新缓存——这样能捕获客户端自己查的DNS结果,避免漏网之鱼
解析DNS响应的思路:
DNS响应包的结构是固定的,你可以遍历回答区域(Answer Section),提取每个A/AAAA记录对应的域名和IP,然后更新到你的缓存里。
方案三:拦截本地DNS请求(补充方案)
用WinDivert过滤UDP 53端口的DNS响应包,解析出域名和对应的IP,直接把这些IP加入到限速列表。这个方案能实时捕获客户端所有的DNS查询结果,适合那些客户端已经提前解析好IP的场景。
避坑提醒(都是我踩过的)
- 别直接在包处理线程里Sleep:上面的代码用了Sleep只是演示,实际项目里一定要用异步队列,把需要延迟的包放到单独的线程队列里,定时发送——不然会阻塞整个WinDivert的包接收,导致所有网络卡成狗。
- 管理员权限:WinDivert必须以管理员身份运行,不然连系统的网络包都抓不到,记得在代码里加权限检查,或者给exe加manifest文件默认请求管理员权限。
- TCP连接跟踪:同一个域名可能对应多个IP,同一个IP也可能对应多个域名,最好维护一个TCP连接(用源IP+源端口+目标IP+目标端口标识)和域名的映射,这样能精准管控每个连接的流量。
- HTTP的Host字段解析:对于非HTTPS的80端口流量,可以解析TCP payload里的HTTP请求头,提取
Host:字段拿到域名,逻辑和SNI类似,就是找HTTP头部的Host行。
总结一下,优先用SNI方案,因为它最直接准确,不需要依赖DNS;然后结合DNS解析和拦截作为补充,就能覆盖所有场景了。有啥细节问题可以再问,我再给你唠唠具体的实现细节~




