WindivertDotnet快速发Ping

1 前言

WindivertDotnet是面向对象的WinDivert的dotnet异步封装,其提供如下的发送数据方法:

ValueTask<int> SendAsync(
    WinDivertPacket packet, 
    WinDivertAddress addr,
    CancellationToken cancellationToken)

在修改包的场景,我们通过RecvAsync()方法获取具有内容的WinDivertPacketWinDivertAddress对象实例,简单修改这两个对象的一些值之后,就可以发送出去。

但在注入的场景,我们需要无中生成WinDivertPacketWinDivertAddress两个对象,前者是IP包的完整数据,后者主要指示数据要经过的网络适配器的索引、数据是入口还是出口方向、是否为loopback等信息,下面我将使用WindivertDotnet来开发一个批量Ping功能的示例来教大家怎么注入数据包。

2 发出Ping包

2.1 路由计算

在发Ping的场景中,我们只知道目的地IP地址,WinDivertRouter对象可以帮们提前算出路由信息,得到以下表格的内容:

属性 说明
IPAddress DstAddress 目的地IP地址
IPAddress SrcAddress 源IP地址
int InterfaceIndex 经过的网络适配器的索引
bool IsOutbound 是否为出口方向
// 使用dstAddr创建router
var router = new WinDivertRouter(dstAddr); 

2.2 创建WinDivertAddress

WinDivertAddress的如下属性必须要设置正确,它是IP数据包构建链路数据包必须的项:

属性 说明
WinDivertAddress.NetWork->IfIdx 发包的网络适配器的索引
WinDivertAddress.Flags.OutboundFlag 是否为出口方向
WinDivertAddress.Flags.LoopbackFlag 是否为回环
// 使用router创建WinDivertAddress 
using WinDivertAddress addr = router.CreateAddress();

2.3 创建WinDivertPacket

因为从router里知道了源IP和目标IP,所以创建ICMP ping功能的WinDivertPacket就比较容易。

/// <summary>
/// 创建icmp的echo包
/// </summary>
/// <param name="srcAddr"></param>
/// <param name="dstAddr"></param>
/// <returns></returns>
private unsafe WinDivertPacket CreateIPV4EchoPacket(IPAddress srcAddr, IPAddress dstAddr)
{
    // ipv4头
    var ipHeader = new IPV4Header
    {
        TTL = 128,
        Version = 4,
        DstAddr = dstAddr,
        SrcAddr = srcAddr,
        Protocol = ProtocolType.Icmp,
        HdrLength = (byte)(sizeof(IPV4Header) / 4),
        Id = ++this.id,
        Length = (ushort)(sizeof(IPV4Header) + sizeof(IcmpV4Header))
    };

    // icmp头
    var icmpHeader = new IcmpV4Header
    {
        Type = IcmpV4MessageType.EchoRequest,
        Code = default,
        Identifier = ipHeader.Id,
        SequenceNumber = ++this.sequenceNumber,
    };

    // 将数据写到packet缓冲区
    var packet = new WinDivertPacket(ipHeader.Length);

    var writer = packet.GetWriter();
    writer.Write(ipHeader);
    writer.Write(icmpHeader);

    return packet;
}

2.4 发出数据包

现在我们可使用Windivert对象,将为每个目的地IP创建的WinDivertPacketWinDivertAddress两个对象发送出去:

/// <summary>
/// 发送icmp的echo请求包
/// </summary>
/// <param name="dstAddrs"></param>
/// <returns></returns>
private async Task SendEchoRequestAsync(IEnumerable<IPAddress> dstAddrs)
{
    foreach (var address in dstAddrs)
    {
        // 使用router计算将进行通讯的本机地址
        var router = new WinDivertRouter(address);
        using var addr = router.CreateAddress();
        using var packet = this.CreateIPV4EchoPacket(router.SrcAddress, router.DstAddress);

        packet.CalcChecksums(addr);     // 计算checksums,因为创建包时没有计算

        await this.divert.SendAsync(packet, addr);
    }
}

3 接收回复包

3.1 Filter

我们可以使用过滤器,将接收的内容过滤为icmp,并且数据是入口方向,必要不必要的数据到达我们的应用层而增加了处理负担:

// 只接受进入系统的icmp
var filter = Filter.True.And(f => f.IsIcmp && f.Network.Inbound);
this.divert = new WinDivert(filter, WinDivertLayer.Network);

3.2 接收数据

接收数据这个就简单了,这是WindivertDotnet最擅长的技能:

/// <summary>
/// 监听ping的回复
/// </summary>
/// <param name="cancellationToken">取消令牌</param>
/// <returns></returns>
private async Task<HashSet<IPAddress>> RecvEchoReplyAsync(CancellationToken cancellationToken)
{
    var results = new HashSet<IPAddress>();
    using var packet = new WinDivertPacket();
    using var addr = new WinDivertAddress();

    while (cancellationToken.IsCancellationRequested == false)
    {
        try
        {
            await this.divert.RecvAsync(packet, addr, cancellationToken);
            if (TryGetEchoReplyAddr(packet, out var value))
            {
                results.Add(value);
            }
            // 把packet发出,避免系统其它软件此刻也有ping而收不到回复
            await this.divert.SendAsync(packet, addr, cancellationToken);
        }
        catch (OperationCanceledException)
        {
            break;
        }
    }
    return results;
}

3.3 解析回复的IP

/// <summary>
/// 解析出icmp回复信息
/// </summary>
/// <param name="packet">数据包</param>
/// <param name="value">回复的IP</param>
/// <returns></returns>
private unsafe static bool TryGetEchoReplyAddr(WinDivertPacket packet, [MaybeNullWhen(false)] out IPAddress value)
{
   var result = packet.GetParseResult();
   if (result.IcmpV4Header != null &&
       result.IcmpV4Header->Type == IcmpV4MessageType.EchoReply)
   {
       value = result.IPV4Header->SrcAddr;
       return true;
   }
   else if (result.IcmpV6Header != null &&
       result.IcmpV6Header->Type == IcmpV6MessageType.EchoReply)
   {
       value = result.IPV6Header->SrcAddr;
       return true;
   }

   value = null;
   return false;
}

4 整合数据

我们需要一个线程来开启接收ping回复,同时另一个线程把所有ping发出去,最后拿ping的所有IP和ping回复的所有IP求交集,就是我们需要的结果。


/// <summary>
/// Ping所有地址
/// 占用两个线程
/// </summary>
/// <param name="dstAddrs">目标地址</param>
/// <param name="delay">最后一个IP发出ping之后的等待回复时长</param>
/// <returns></returns>
public async Task<IPAddress[]> PingAllAsync(IEnumerable<IPAddress> dstAddrs, TimeSpan delay)
{
    // 开始监听ping的回复
    using var cts = new CancellationTokenSource();
    var recvTask = this.RecvEchoReplyAsync(cts.Token);

    // 对所有ip发ping
    await this.SendEchoRequestAsync(dstAddrs);

    // 延时取消监听
    cts.CancelAfter(delay);
    var results = await recvTask;

    // 清洗数据
    return results.Intersect(dstAddrs).ToArray();
}

后记

通过WindivertDotnet的路由,无中生有IP数据包,并可以将其正确的发送的指定的目的地IP地址。像本示例的这个Ping方式,10秒ping完1万个IP并拿到其回复的IP是非常轻松的。

posted @ 2022-10-19 20:46  jiulang  阅读(527)  评论(2编辑  收藏  举报