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是非常轻松的。

发表评论

评论已关闭。

相关文章