/*
 *  TAP-Windows -- A kernel driver to provide virtual tap
 *                 device functionality on Windows.
 *
 *  This code was inspired by the CIPE-Win32 driver by Damion K. Wilson.
 *
 *  This source code is Copyright (C) 2002-2014 OpenVPN Technologies, Inc.,
 *  and is released under the GPL version 2 (see below).
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2
 *  as published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program (see the file COPYING included with this
 *  distribution); if not, write to the Free Software Foundation, Inc.,
 *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

//
// Include files.
//

#include "tap.h"

//======================================================================
// TAP Receive Path Support
//======================================================================

#ifdef ALLOC_PRAGMA
#pragma alloc_text(PAGE, TapDeviceWrite)
#endif  // ALLOC_PRAGMA

//===============================================================
// Used in cases where internally generated packets such as
// ARP or DHCP replies must be returned to the kernel, to be
// seen as an incoming packet "arriving" on the interface.
//===============================================================

VOID IndicateReceivePacket(__in PTAP_ADAPTER_CONTEXT Adapter, __in PUCHAR packetData,
                           __in const unsigned int packetLength) {
  PUCHAR injectBuffer;

  //
  // Handle miniport Pause
  // ---------------------
  // NDIS 6 miniports implement a temporary "Pause" state normally followed
  // by the Restart. While in the Pause state it is forbidden for the miniport
  // to indicate receive NBLs.
  //
  // That is: The device interface may be "up", but the NDIS miniport send/receive
  // interface may be temporarily "down".
  //
  // BUGBUG!!! In the initial implementation of the NDIS 6 TapOas inject path
  // the code below will simply ignore inject packets passed to the driver while
  // the miniport is in the Paused state.
  //
  // The correct implementation is to go ahead and build the NBLs corresponding
  // to the inject packet - but queue them. When Restart is entered the
  // queued NBLs would be dequeued and indicated to the host.
  //
  if (tapAdapterSendAndReceiveReady(Adapter) != NDIS_STATUS_SUCCESS) {
    DEBUGP(("[%s] Lying send in IndicateReceivePacket while adapter paused\n",
            MINIPORT_INSTANCE_ID(Adapter)));

    return;
  }

  // Allocate flat buffer for packet data.
  injectBuffer = (PUCHAR)NdisAllocateMemoryWithTagPriority(
      Adapter->MiniportAdapterHandle, packetLength, TAP_RX_INJECT_BUFFER_TAG, NormalPoolPriority);

  if (injectBuffer) {
    PMDL mdl;

    // Copy packet data to flat buffer.
    NdisMoveMemory(injectBuffer, packetData, packetLength);

    // Allocate MDL for flat buffer.
    mdl = NdisAllocateMdl(Adapter->MiniportAdapterHandle, injectBuffer, packetLength);

    if (mdl) {
      PNET_BUFFER_LIST netBufferList;

      mdl->Next = NULL;  // No next MDL

      // Allocate the NBL and NB. Link MDL chain to NB.
      netBufferList = NdisAllocateNetBufferAndNetBufferList(Adapter->ReceiveNblPool,
                                                            0,    // ContextSize
                                                            0,    // ContextBackFill
                                                            mdl,  // MDL chain
                                                            0, packetLength);

      if (netBufferList != NULL) {
        ULONG receiveFlags = 0;
        LONG nblCount;

        NET_BUFFER_LIST_NEXT_NBL(netBufferList) = NULL;  // Only one NBL

        if (KeGetCurrentIrql() == DISPATCH_LEVEL) {
          receiveFlags |= NDIS_RECEIVE_FLAGS_DISPATCH_LEVEL;
        }

        // Set flag indicating that this is an injected packet
        TAP_RX_NBL_FLAGS_CLEAR_ALL(netBufferList);
        TAP_RX_NBL_FLAG_SET(netBufferList, TAP_RX_NBL_FLAGS_IS_INJECTED);

        netBufferList->MiniportReserved[0] = NULL;
        netBufferList->MiniportReserved[1] = NULL;

        // Increment in-flight receive NBL count.
        nblCount = NdisInterlockedIncrement(&Adapter->ReceiveNblInFlightCount);
        ASSERT(nblCount > 0);

        netBufferList->SourceHandle = Adapter->MiniportAdapterHandle;

        //
        // Indicate the packet
        // -------------------
        // Irp->AssociatedIrp.SystemBuffer with length irpSp->Parameters.Write.Length
        // contains the complete packet including Ethernet header and payload.
        //
        NdisMIndicateReceiveNetBufferLists(Adapter->MiniportAdapterHandle, netBufferList,
                                           NDIS_DEFAULT_PORT_NUMBER,
                                           1,  // NumberOfNetBufferLists
                                           receiveFlags);

        return;
      } else {
        DEBUGP(("[%s] NdisAllocateNetBufferAndNetBufferList failed in IndicateReceivePacket\n",
                MINIPORT_INSTANCE_ID(Adapter)));
        NOTE_ERROR();

        NdisFreeMdl(mdl);
        NdisFreeMemory(injectBuffer, 0, 0);
      }
    } else {
      DEBUGP(("[%s] NdisAllocateMdl failed in IndicateReceivePacket\n",
              MINIPORT_INSTANCE_ID(Adapter)));
      NOTE_ERROR();

      NdisFreeMemory(injectBuffer, 0, 0);
    }
  } else {
    DEBUGP(("[%s] NdisAllocateMemoryWithTagPriority failed in IndicateReceivePacket\n",
            MINIPORT_INSTANCE_ID(Adapter)));
    NOTE_ERROR();
  }
}

VOID tapCompleteIrpAndFreeReceiveNetBufferList(__in PTAP_ADAPTER_CONTEXT Adapter,
                                               __in PNET_BUFFER_LIST
                                                   NetBufferList,  // Only one NB here...
                                               __in NTSTATUS IoCompletionStatus) {
  PIRP irp;
  ULONG frameType, netBufferCount, byteCount;
  LONG nblCount;

  // Fetch NB frame type.
  frameType = tapGetNetBufferFrameType(NET_BUFFER_LIST_FIRST_NB(NetBufferList));

  // Fetch statistics for all NBs linked to the NB.
  netBufferCount = tapGetNetBufferCountsFromNetBufferList(NetBufferList, &byteCount);

  // Update statistics by frame type
  if (IoCompletionStatus == STATUS_SUCCESS) {
    switch (frameType) {
      case NDIS_PACKET_TYPE_DIRECTED:
        Adapter->FramesRxDirected += netBufferCount;
        Adapter->BytesRxDirected += byteCount;
        break;

      case NDIS_PACKET_TYPE_BROADCAST:
        Adapter->FramesRxBroadcast += netBufferCount;
        Adapter->BytesRxBroadcast += byteCount;
        break;

      case NDIS_PACKET_TYPE_MULTICAST:
        Adapter->FramesRxMulticast += netBufferCount;
        Adapter->BytesRxMulticast += byteCount;
        break;

      default:
        ASSERT(FALSE);
        break;
    }
  }

  //
  // Handle P2P Packet
  // -----------------
  // Free MDL allocated for P2P Ethernet header.
  //
  if (TAP_RX_NBL_FLAG_TEST(NetBufferList, TAP_RX_NBL_FLAGS_IS_P2P)) {
    PNET_BUFFER netBuffer;
    PMDL mdl;

    netBuffer = NET_BUFFER_LIST_FIRST_NB(NetBufferList);
    mdl = NET_BUFFER_FIRST_MDL(netBuffer);
    mdl->Next = NULL;

    NdisFreeMdl(mdl);
  }

  //
  // Handle Injected Packet
  // -----------------------
  // Free MDL and data buffer allocated for injected packet.
  //
  if (TAP_RX_NBL_FLAG_TEST(NetBufferList, TAP_RX_NBL_FLAGS_IS_INJECTED)) {
    PNET_BUFFER netBuffer;
    PMDL mdl;
    PUCHAR injectBuffer;

    netBuffer = NET_BUFFER_LIST_FIRST_NB(NetBufferList);
    mdl = NET_BUFFER_FIRST_MDL(netBuffer);

    injectBuffer = (PUCHAR)MmGetSystemAddressForMdlSafe(mdl, NormalPagePriority);

    if (injectBuffer) {
      NdisFreeMemory(injectBuffer, 0, 0);
    }

    NdisFreeMdl(mdl);
  }

  //
  // Complete the IRP
  //
  irp = (PIRP)NetBufferList->MiniportReserved[0];

  if (irp) {
    irp->IoStatus.Status = IoCompletionStatus;
    IoCompleteRequest(irp, IO_NO_INCREMENT);
  }

  // Decrement in-flight receive NBL count.
  nblCount = NdisInterlockedDecrement(&Adapter->ReceiveNblInFlightCount);
  ASSERT(nblCount >= 0);
  if (0 == nblCount) {
    NdisSetEvent(&Adapter->ReceiveNblInFlightCountZeroEvent);
  }

  // Free the NBL
  NdisFreeNetBufferList(NetBufferList);
}

VOID AdapterReturnNetBufferLists(__in NDIS_HANDLE MiniportAdapterContext,
                                 __in PNET_BUFFER_LIST NetBufferLists, __in ULONG ReturnFlags) {
  PTAP_ADAPTER_CONTEXT adapter = (PTAP_ADAPTER_CONTEXT)MiniportAdapterContext;
  PNET_BUFFER_LIST currentNbl, nextNbl;

  UNREFERENCED_PARAMETER(ReturnFlags);

  //
  // Process each NBL individually
  //
  currentNbl = NetBufferLists;
  while (currentNbl) {
    PNET_BUFFER_LIST nextNbl;

    nextNbl = NET_BUFFER_LIST_NEXT_NBL(currentNbl);
    NET_BUFFER_LIST_NEXT_NBL(currentNbl) = NULL;

    // Complete write IRP and free NBL and associated resources.
    tapCompleteIrpAndFreeReceiveNetBufferList(adapter, currentNbl, STATUS_SUCCESS);

    // Move to next NBL
    currentNbl = nextNbl;
  }
}

// IRP_MJ_WRITE callback.
NTSTATUS
TapDeviceWrite(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
  NTSTATUS ntStatus = STATUS_SUCCESS;  // Assume success
  PIO_STACK_LOCATION irpSp;            // Pointer to current stack location
  PTAP_ADAPTER_CONTEXT adapter = NULL;
  ULONG dataLength;

  PAGED_CODE();

  irpSp = IoGetCurrentIrpStackLocation(Irp);

  //
  // Fetch adapter context for this device.
  // --------------------------------------
  // Adapter pointer was stashed in FsContext when handle was opened.
  //
  adapter = (PTAP_ADAPTER_CONTEXT)(irpSp->FileObject)->FsContext;

  ASSERT(adapter);

  //
  // Sanity checks on state variables
  //
  if (!tapAdapterReadAndWriteReady(adapter)) {
    // DEBUGP (("[%s] Interface is down in IRP_MJ_WRITE\n",
    //    MINIPORT_INSTANCE_ID (adapter)));
    // NOTE_ERROR();

    Irp->IoStatus.Status = ntStatus = STATUS_CANCELLED;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return ntStatus;
  }

  // Save IRP-accessible copy of buffer length
  Irp->IoStatus.Information = irpSp->Parameters.Write.Length;

  if (Irp->MdlAddress == NULL) {
    DEBUGP(("[%s] MdlAddress is NULL for IRP_MJ_WRITE\n", MINIPORT_INSTANCE_ID(adapter)));

    NOTE_ERROR();
    Irp->IoStatus.Status = ntStatus = STATUS_INVALID_PARAMETER;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return ntStatus;
  }

  //
  // Try to get a virtual address for the MDL.
  //
  NdisQueryMdl(Irp->MdlAddress, &Irp->AssociatedIrp.SystemBuffer, &dataLength, NormalPagePriority);

  if (Irp->AssociatedIrp.SystemBuffer == NULL) {
    DEBUGP(("[%s] Could not map address in IRP_MJ_WRITE\n", MINIPORT_INSTANCE_ID(adapter)));

    NOTE_ERROR();
    Irp->IoStatus.Status = ntStatus = STATUS_INSUFFICIENT_RESOURCES;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return ntStatus;
  }

  ASSERT(dataLength == irpSp->Parameters.Write.Length);

  Irp->IoStatus.Information = irpSp->Parameters.Write.Length;

  //
  // Handle miniport Pause
  // ---------------------
  // NDIS 6 miniports implement a temporary "Pause" state normally followed
  // by the Restart. While in the Pause state it is forbidden for the miniport
  // to indicate receive NBLs.
  //
  // That is: The device interface may be "up", but the NDIS miniport send/receive
  // interface may be temporarily "down".
  //
  // BUGBUG!!! In the initial implementation of the NDIS 6 TapOas receive path
  // the code below will perform a "lying send" for write IRPs passed to the
  // driver while the miniport is in the Paused state.
  //
  // The correct implementation is to go ahead and build the NBLs corresponding
  // to the user-mode write - but queue them. When Restart is entered the
  // queued NBLs would be dequeued and indicated to the host.
  //
  if (tapAdapterSendAndReceiveReady(adapter) == NDIS_STATUS_SUCCESS) {
    if (!adapter->m_tun && ((irpSp->Parameters.Write.Length) >= ETHERNET_HEADER_SIZE)) {
      PNET_BUFFER_LIST netBufferList;

      DUMP_PACKET("IRP_MJ_WRITE ETH", (unsigned char *)Irp->AssociatedIrp.SystemBuffer,
                  irpSp->Parameters.Write.Length);

      //=====================================================
      // If IPv4 packet, check whether or not packet
      // was truncated.
      //=====================================================
#if PACKET_TRUNCATION_CHECK
      IPv4PacketSizeVerify((unsigned char *)Irp->AssociatedIrp.SystemBuffer,
                           irpSp->Parameters.Write.Length, FALSE, "RX", &adapter->m_RxTrunc);
#endif
      (Irp->MdlAddress)->Next = NULL;  // No next MDL

      // Allocate the NBL and NB. Link MDL chain to NB.
      netBufferList = NdisAllocateNetBufferAndNetBufferList(adapter->ReceiveNblPool,
                                                            0,                // ContextSize
                                                            0,                // ContextBackFill
                                                            Irp->MdlAddress,  // MDL chain
                                                            0, dataLength);

      if (netBufferList != NULL) {
        LONG nblCount;

        NET_BUFFER_LIST_NEXT_NBL(netBufferList) = NULL;  // Only one NBL

        // Stash IRP pointer in NBL MiniportReserved[0] field.
        netBufferList->MiniportReserved[0] = Irp;
        netBufferList->MiniportReserved[1] = NULL;

        // This IRP is pended.
        IoMarkIrpPending(Irp);

        // This IRP cannot be cancelled while in-flight.
        IoSetCancelRoutine(Irp, NULL);

        TAP_RX_NBL_FLAGS_CLEAR_ALL(netBufferList);

        // Increment in-flight receive NBL count.
        nblCount = NdisInterlockedIncrement(&adapter->ReceiveNblInFlightCount);
        ASSERT(nblCount > 0);

        //
        // Indicate the packet
        // -------------------
        // Irp->AssociatedIrp.SystemBuffer with length irpSp->Parameters.Write.Length
        // contains the complete packet including Ethernet header and payload.
        //
        NdisMIndicateReceiveNetBufferLists(adapter->MiniportAdapterHandle, netBufferList,
                                           NDIS_DEFAULT_PORT_NUMBER,
                                           1,  // NumberOfNetBufferLists
                                           0   // ReceiveFlags
        );

        ntStatus = STATUS_PENDING;
      } else {
        DEBUGP(("[%s] NdisMIndicateReceiveNetBufferLists failed in IRP_MJ_WRITE\n",
                MINIPORT_INSTANCE_ID(adapter)));
        NOTE_ERROR();

        // Fail the IRP
        Irp->IoStatus.Information = 0;
        ntStatus = STATUS_INSUFFICIENT_RESOURCES;
      }
    } else if (adapter->m_tun && ((irpSp->Parameters.Write.Length) >= IP_HEADER_SIZE)) {
      PETH_HEADER p_UserToTap = &adapter->m_UserToTap;
      PMDL mdl;  // Head of MDL chain.

      // For IPv6, need to use Ethernet header with IPv6 proto
      if (IPH_GET_VER(((IPHDR *)Irp->AssociatedIrp.SystemBuffer)->version_len) == 6) {
        p_UserToTap = &adapter->m_UserToTap_IPv6;
      }

      DUMP_PACKET2("IRP_MJ_WRITE P2P", p_UserToTap,
                   (unsigned char *)Irp->AssociatedIrp.SystemBuffer,
                   irpSp->Parameters.Write.Length);

      //=====================================================
      // If IPv4 packet, check whether or not packet
      // was truncated.
      //=====================================================
#if PACKET_TRUNCATION_CHECK
      IPv4PacketSizeVerify((unsigned char *)Irp->AssociatedIrp.SystemBuffer,
                           irpSp->Parameters.Write.Length, TRUE, "RX", &adapter->m_RxTrunc);
#endif

      //
      // Allocate MDL for Ethernet header
      // --------------------------------
      // Irp->AssociatedIrp.SystemBuffer with length irpSp->Parameters.Write.Length
      // contains the only the Ethernet payload. Prepend the user-mode provided
      // payload with the Ethernet header pointed to by p_UserToTap.
      //
      mdl = NdisAllocateMdl(adapter->MiniportAdapterHandle, p_UserToTap, sizeof(ETH_HEADER));

      if (mdl != NULL) {
        PNET_BUFFER_LIST netBufferList;

        // Chain user's Ethernet payload behind Ethernet header.
        mdl->Next = Irp->MdlAddress;
        (Irp->MdlAddress)->Next = NULL;  // No next MDL

        // Allocate the NBL and NB. Link MDL chain to NB.
        netBufferList = NdisAllocateNetBufferAndNetBufferList(adapter->ReceiveNblPool,
                                                              0,    // ContextSize
                                                              0,    // ContextBackFill
                                                              mdl,  // MDL chain
                                                              0, sizeof(ETH_HEADER) + dataLength);

        if (netBufferList != NULL) {
          LONG nblCount;

          NET_BUFFER_LIST_NEXT_NBL(netBufferList) = NULL;  // Only one NBL

          // This IRP is pended.
          IoMarkIrpPending(Irp);

          // This IRP cannot be cancelled while in-flight.
          IoSetCancelRoutine(Irp, NULL);

          // Stash IRP pointer in NBL MiniportReserved[0] field.
          netBufferList->MiniportReserved[0] = Irp;
          netBufferList->MiniportReserved[1] = NULL;

          // Set flag indicating that this is P2P packet
          TAP_RX_NBL_FLAGS_CLEAR_ALL(netBufferList);
          TAP_RX_NBL_FLAG_SET(netBufferList, TAP_RX_NBL_FLAGS_IS_P2P);

          // Increment in-flight receive NBL count.
          nblCount = NdisInterlockedIncrement(&adapter->ReceiveNblInFlightCount);
          ASSERT(nblCount > 0);

          //
          // Indicate the packet
          //
          NdisMIndicateReceiveNetBufferLists(adapter->MiniportAdapterHandle, netBufferList,
                                             NDIS_DEFAULT_PORT_NUMBER,
                                             1,  // NumberOfNetBufferLists
                                             0   // ReceiveFlags
          );

          ntStatus = STATUS_PENDING;
        } else {
          mdl->Next = NULL;
          NdisFreeMdl(mdl);

          DEBUGP(("[%s] NdisMIndicateReceiveNetBufferLists failed in IRP_MJ_WRITE\n",
                  MINIPORT_INSTANCE_ID(adapter)));
          NOTE_ERROR();

          // Fail the IRP
          Irp->IoStatus.Information = 0;
          ntStatus = STATUS_INSUFFICIENT_RESOURCES;
        }
      } else {
        DEBUGP(("[%s] NdisAllocateMdl failed in IRP_MJ_WRITE\n", MINIPORT_INSTANCE_ID(adapter)));
        NOTE_ERROR();

        // Fail the IRP
        Irp->IoStatus.Information = 0;
        ntStatus = STATUS_INSUFFICIENT_RESOURCES;
      }
    } else {
      DEBUGP(("[%s] Bad buffer size in IRP_MJ_WRITE, len=%d\n", MINIPORT_INSTANCE_ID(adapter),
              irpSp->Parameters.Write.Length));
      NOTE_ERROR();

      Irp->IoStatus.Information = 0;  // ETHERNET_HEADER_SIZE;
      Irp->IoStatus.Status = ntStatus = STATUS_BUFFER_TOO_SMALL;
    }
  } else {
    DEBUGP(
        ("[%s] Lying send in IRP_MJ_WRITE while adapter paused\n", MINIPORT_INSTANCE_ID(adapter)));

    ntStatus = STATUS_SUCCESS;
  }

  if (ntStatus != STATUS_PENDING) {
    Irp->IoStatus.Status = ntStatus;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
  }

  return ntStatus;
}
