You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何使用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解析和拦截作为补充,就能覆盖所有场景了。有啥细节问题可以再问,我再给你唠唠具体的实现细节~

火山引擎 最新活动