basil00 / WinDivert

WinDivert: Windows Packet Divert
https://reqrypt.org/windivert.html
Other
2.57k stars 513 forks source link

WinDivert dropping IPv6 packets. #28

Closed rafael-santiago closed 10 years ago

rafael-santiago commented 10 years ago

Hi!

I was testing the webfilter sample changing the filter from "outbound && ip && tcp.DstPort == 80 && tcp.PayloadLength > 0" to "outbound && ipv6 && tcp.DstPort == 80 && tcp.PayloadLength > 0" and IPv6 packets were dropped in some specific cases.

Steps to reproduce this behaviour:

In additional, I noticed that in some cases the recalculation of checksum is required even with non-changed packets otherwise the packets will be dropped... I don't know the reason.

My impression is that there is some problem inside the driver code. Can you try to reproduce it inside your environment and give me some feedback?

Thanks in advance!

basil00 commented 10 years ago

In additional, I noticed that in some cases the recalculation of checksum is required even with non-changed packets otherwise the packets will be dropped...

In theory WinDivertRecv should (by default) return a packet with correct checksums. However, looking at the driver code, it appears the driver will only calculate checksums for IPv4 packets (windivert_update_checksums() immediately returns if not IPv4). I'm not sure why, as this code was written a long time ago. This may explain why unchanged IPv6 packets are dropped.

I will add a fix in the next release.

rafael-santiago commented 10 years ago

Ok, thank you for the clarifications.

rafael-santiago commented 10 years ago

Hi,

I think that I found a way to "solve" this issue but it isn't the best solution yet:

At first I implemented the IPv6 pseudo-header composition besides the usage of it at windivert_update_checksums() function.

After, I changed windivert_queue_packet() a little because I noticed that "packet->udp_checksum" is always "TRUE" to TCP packets and "packet->tcp_checksum" is always "FALSE"... I don't know the reason... I suspect that it is due to "NDIS_TCP_IP_CHECKSUM_NET_BUFFER_LIST_INFO" be composed by unions (and unions are....)... then now I'm setting it by myself.

Something like:

  // proto = ipv4 or ipv6 protocol field
  switch (proto) {
         case IPPROTO_TCP:
            packet->tcp_checksum = TRUE;
            break;
         case IPPROTO_UDP:
             packet->udp_checksum = TRUE;
            break;
         default:
             packet->tcp_checksum = FALSE;
             packet->udp_checksum = FALSE;
             break;
   }

Maybe it can cause overhead but using it, at least, I can access the local address (eg.: IPv4 loopback, IPv6 loopback, real IPv4 and real IPv6).

The windivert_update_checksums() function which I used in my tests:

static void windivert_update_checksums(void *header, size_t len, BOOL update_ip, BOOL     update_tcp, BOOL update_udp)
{
     struct
    {
        UINT32 SrcAddr;
        UINT32 DstAddr;
        UINT8  Zero;
        UINT8  Protocol;
        UINT16 TransLength;
    } pseudo_header;
    struct
    {
        UINT32 SrcAddr[4];
        UINT32 DstAddr[4];        
        UINT8 Protocol;
        UINT8 Zero;
        UINT16 TransLength;
    } pseudo_header6;
    struct iphdr *ip_header = NULL;
    struct ipv6hdr *ip6_header = NULL;
    unsigned char version = (*((unsigned char *)header)) >> 4;
    unsigned char proto = 0;
    size_t ip_header_len, trans_len, pseudo_header_len;
    void *trans_header;
    void *pseudo_header_p;
    struct tcphdr *tcp_header;
    struct udphdr *udp_header;
    UINT16 *trans_check_ptr;
    UINT sum;

  if (!update_ip && !update_tcp && !update_udp)
  {
      return;
  }

  if (len < sizeof(struct iphdr))
  {
      return;
  }

  switch (version)
  {
    case 4:
      ip_header = (struct iphdr *)header;
      break;

    case 6:
      ip6_header = (struct ipv6hdr *)header;
      break;

    default:
      return;
      break;
  }

  if (version == 4) {
    ip_header_len = ip_header->HdrLength*sizeof(UINT32);
    if (len < ip_header_len)
    {
        return;
    }

    if (update_ip)
    {
        ip_header->Checksum = 0;
        ip_header->Checksum = windivert_checksum(NULL, 0, ip_header,
            ip_header_len);
    }
    trans_len = RtlUshortByteSwap(ip_header->Length) - ip_header_len;
    trans_header = (UINT8 *)ip_header + ip_header_len;
    proto = ip_header->Protocol;
  } else {
    trans_len = RtlUshortByteSwap(ip6_header->Length);
    trans_header = (UINT8 *)ip6_header + 40;
    proto = ip6_header->NextHdr;
  }    

  switch (proto)
  {
      case IPPROTO_TCP:
          if (!update_tcp)
          {
              return;
          }
          tcp_header = (struct tcphdr *)trans_header;
          if (trans_len < sizeof(struct tcphdr))
          {
              return;
          }
          trans_check_ptr = &tcp_header->Checksum;
          break;
      case IPPROTO_UDP:
          if (!update_udp)
          {
              return;
          }
          udp_header = (struct udphdr *)trans_header;
          if (trans_len < sizeof(struct udphdr))
          {
              return;
          }
          trans_check_ptr = &udp_header->Checksum;
          break;
      default:
          return;
  }

  if (version == 4) {
    pseudo_header.SrcAddr     = ip_header->SrcAddr;
    pseudo_header.DstAddr     = ip_header->DstAddr;
    pseudo_header.Zero        = 0x0;
    pseudo_header.Protocol    = proto;
    pseudo_header.TransLength = RtlUshortByteSwap((UINT16)trans_len);
    pseudo_header_p = (UINT8 *)&pseudo_header;
    pseudo_header_len = sizeof(pseudo_header);
  } else {
    memcpy(pseudo_header6.SrcAddr, ip6_header->SrcAddr, sizeof(ip6_header->SrcAddr));
    memcpy(pseudo_header6.DstAddr, ip6_header->DstAddr, sizeof(ip6_header->DstAddr));
    pseudo_header6.Zero        = 0x0;
    pseudo_header6.Protocol    = proto;
    pseudo_header6.TransLength = RtlUshortByteSwap((UINT16)trans_len);
    pseudo_header_p = (UINT8 *)&pseudo_header6;
    pseudo_header_len = sizeof(pseudo_header6);
  }

  *trans_check_ptr = 0x0;
  sum = windivert_checksum(pseudo_header_p, pseudo_header_len,
      trans_header, trans_len);
  if (sum == 0 && proto == IPPROTO_UDP)
  {
      *trans_check_ptr = 0xFFFF;
  }
  else
  {
      *trans_check_ptr = (UINT16)sum;
  }
}
basil00 commented 10 years ago

So the problem is confirmed. The driver does not calculate checksums for ipv6 packets. If the packet is re-injected unchanged, then it is rejected. I am working on a patch.

Longer term: the checksum calculation should probably be moved out of the driver and into the user-mode DLL. Also, I think checksum calculation should be disabled by default, since most users modify packets anyway. This is for WinDivert1.2.

rafael-santiago commented 10 years ago

Yes, I agree about the execution context of checksum calculation. Anyway, I think is rather weird the ".Transmit.TcpChecksum" field be always 0 and ".Transmit.UdpChecksum" be 1 even when the transport protocol is in fact 6. At least in my test environment I noticed it.

basil00 commented 10 years ago

I can confirm for IPv6 loopback packets. I think there is mix up between Transmit & Receive somewhere, so Transmit.UdpChecksum = Receive.TcpChecksumSucceeded.

For non-loopback both flags are zero. This is what I except since my test set-up does not seem to support IPv6 offloading.

basil00 commented 10 years ago

The driver now calculates checksums for IPv6 packets. But still need to figure out what is going on with the checksum flags for loopback packets.

basil00 commented 10 years ago

Changed the driver so that it ignores the checksum info for loopback packets. This should fix the issue for all packets.