/*
 * Copyright (c) 2014, 2016 VMware, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "precomp.h"

#include "Atomic.h"
#include "Debug.h"
#include "Flow.h"
#include "Flow.h"
#include "IpHelper.h"
#include "NetProto.h"
#include "Offload.h"
#include "PacketIO.h"
#include "PacketParser.h"
#include "Switch.h"
#include "User.h"
#include "Vport.h"
#include "Vxlan.h"

#pragma warning( push )
#pragma warning( disable:4127 )


#ifdef OVS_DBG_MOD
#undef OVS_DBG_MOD
#endif
#define OVS_DBG_MOD OVS_DBG_VXLAN

/* Helper macro to check if a VXLAN ID is valid. */
#define VXLAN_ID_IS_VALID(vxlanID) (0 < (vxlanID) && (vxlanID) <= 0xffffff)
#define VXLAN_TUNNELID_TO_VNI(_tID)   (UINT32)(((UINT64)(_tID)) >> 40)
#define VXLAN_VNI_TO_TUNNELID(_vni) (((UINT64)(_vni)) << 40)
#define IP_DF_NBO 0x0040
#define VXLAN_DEFAULT_TTL 64
#define VXLAN_MULTICAST_TTL 64
#define VXLAN_DEFAULT_INSTANCE_ID 1

/* Move to a header file */
extern POVS_SWITCH_CONTEXT gOvsSwitchContext;

/*
 *----------------------------------------------------------------------------
 * This function verifies if the VXLAN tunnel already exists, in order to
 * avoid sending a duplicate request to the WFP base filtering engine.
 *----------------------------------------------------------------------------
 */
static BOOLEAN
OvsIsTunnelFilterCreated(POVS_SWITCH_CONTEXT switchContext,
                         UINT16 udpPortDest)
{
    for (UINT hash = 0; hash < OVS_MAX_VPORT_ARRAY_SIZE; hash++) {
        PLIST_ENTRY head, link, next;

        head = &(switchContext->portNoHashArray[hash & OVS_VPORT_MASK]);
        LIST_FORALL_SAFE(head, link, next) {
            POVS_VPORT_ENTRY vport = NULL;
            POVS_VXLAN_VPORT vxlanPort = NULL;
            vport = CONTAINING_RECORD(link, OVS_VPORT_ENTRY, portNoLink);
            vxlanPort = (POVS_VXLAN_VPORT)vport->priv;
            if (vxlanPort) {
                if ((udpPortDest == vxlanPort->dstPort)) {
                    /* The VXLAN tunnel was already created. */
                    return TRUE;
                }
            }
        }
    }

    return FALSE;
}

/*
 *----------------------------------------------------------------------------
 * This function allocates and initializes the OVS_VXLAN_VPORT. The function
 * also creates a WFP tunnel filter for the necessary destination port. The
 * tunnel filter create request is passed to the tunnel filter threads that
 * will complete the request at a later time when IRQL is lowered to
 * PASSIVE_LEVEL.
 *
 * udpDestPort: the vxlan is set as payload to a udp frame. If the destination
 * port of an udp frame is udpDestPort, we understand it to be vxlan.
 *----------------------------------------------------------------------------
 */
NTSTATUS
OvsInitVxlanTunnel(PIRP irp,
                   POVS_VPORT_ENTRY vport,
                   UINT16 udpDestPort,
                   PFNTunnelVportPendingOp callback,
                   PVOID tunnelContext)
{
    NTSTATUS status = STATUS_SUCCESS;
    POVS_VXLAN_VPORT vxlanPort = NULL;

    vxlanPort = OvsAllocateMemoryWithTag(sizeof (*vxlanPort),
                                         OVS_VXLAN_POOL_TAG);
    if (vxlanPort == NULL) {
        return STATUS_INSUFFICIENT_RESOURCES;
    }

    RtlZeroMemory(vxlanPort, sizeof(*vxlanPort));
    vxlanPort->dstPort = udpDestPort;
    vport->priv = (PVOID)vxlanPort;

    if (!OvsIsTunnelFilterCreated(gOvsSwitchContext, udpDestPort)) {
        status = OvsTunnelFilterCreate(irp,
                                       udpDestPort,
                                       &vxlanPort->filterID,
                                       callback,
                                       tunnelContext);
    } else {
        status = STATUS_OBJECT_NAME_EXISTS;
    }

    return status;
}

/*
 *----------------------------------------------------------------------------
 * This function releases the OVS_VXLAN_VPORT. The function also deletes the
 * WFP tunnel filter previously created. The tunnel filter delete request is
 * passed to the tunnel filter threads that will complete the request at a
 * later time when IRQL is lowered to PASSIVE_LEVEL.
 *----------------------------------------------------------------------------
 */
NTSTATUS
OvsCleanupVxlanTunnel(PIRP irp,
                      POVS_VPORT_ENTRY vport,
                      PFNTunnelVportPendingOp callback,
                      PVOID tunnelContext)
{
    NTSTATUS status = STATUS_SUCCESS;
    POVS_VXLAN_VPORT vxlanPort = NULL;

    if (vport->ovsType != OVS_VPORT_TYPE_VXLAN ||
        vport->priv == NULL) {
        return STATUS_SUCCESS;
    }

    vxlanPort = (POVS_VXLAN_VPORT)vport->priv;

    if (vxlanPort->filterID != 0) {
        status = OvsTunnelFilterDelete(irp,
                                       vxlanPort->filterID,
                                       callback,
                                       tunnelContext);
    } else {
        OvsFreeMemoryWithTag(vport->priv, OVS_VXLAN_POOL_TAG);
        vport->priv = NULL;
    }

    return status;
}


/*
 *----------------------------------------------------------------------------
 * OvsDoEncapVxlan
 *     Encapsulates the packet.
 *----------------------------------------------------------------------------
 */
static __inline NDIS_STATUS
OvsDoEncapVxlan(POVS_VPORT_ENTRY vport,
                PNET_BUFFER_LIST curNbl,
                OvsIPv4TunnelKey *tunKey,
                POVS_FWD_INFO fwdInfo,
                POVS_PACKET_HDR_INFO layers,
                POVS_SWITCH_CONTEXT switchContext,
                PNET_BUFFER_LIST *newNbl)
{
    NDIS_STATUS status;
    PNET_BUFFER curNb;
    PMDL curMdl;
    PUINT8 bufferStart;
    EthHdr *ethHdr;
    IPHdr *ipHdr;
    UDPHdr *udpHdr;
    VXLANHdr *vxlanHdr;
    POVS_VXLAN_VPORT vportVxlan;
    UINT32 headRoom = OvsGetVxlanTunHdrSize();
    UINT32 packetLength;
    ULONG mss = 0;
    NDIS_TCP_IP_CHECKSUM_NET_BUFFER_LIST_INFO csumInfo;

    curNb = NET_BUFFER_LIST_FIRST_NB(curNbl);
    packetLength = NET_BUFFER_DATA_LENGTH(curNb);

    if (layers->isTcp) {
        mss = OVSGetTcpMSS(curNbl);

        OVS_LOG_TRACE("MSS %u packet len %u", mss,
                      packetLength);
        if (mss) {
            OVS_LOG_TRACE("l4Offset %d", layers->l4Offset);
            *newNbl = OvsTcpSegmentNBL(switchContext, curNbl, layers,
                                       mss, headRoom);
            if (*newNbl == NULL) {
                OVS_LOG_ERROR("Unable to segment NBL");
                return NDIS_STATUS_FAILURE;
            }
            /* Clear out LSO flags after this point */
            NET_BUFFER_LIST_INFO(*newNbl, TcpLargeSendNetBufferListInfo) = 0;
        }
    }

    vportVxlan = (POVS_VXLAN_VPORT) GetOvsVportPriv(vport);
    ASSERT(vportVxlan);

    /* If we didn't split the packet above, make a copy now */
    if (*newNbl == NULL) {
        *newNbl = OvsPartialCopyNBL(switchContext, curNbl, 0, headRoom,
                                    FALSE /*NBL info*/);
        if (*newNbl == NULL) {
            OVS_LOG_ERROR("Unable to copy NBL");
            return NDIS_STATUS_FAILURE;
        }
        csumInfo.Value = NET_BUFFER_LIST_INFO(curNbl,
                                              TcpIpChecksumNetBufferListInfo);
        status = OvsApplySWChecksumOnNB(layers, *newNbl, &csumInfo);

        if (status != NDIS_STATUS_SUCCESS) {
            goto ret_error;
        }
    }

    curNbl = *newNbl;
    for (curNb = NET_BUFFER_LIST_FIRST_NB(curNbl); curNb != NULL;
            curNb = curNb->Next) {
        status = NdisRetreatNetBufferDataStart(curNb, headRoom, 0, NULL);
        if (status != NDIS_STATUS_SUCCESS) {
            goto ret_error;
        }

        curMdl = NET_BUFFER_CURRENT_MDL(curNb);
        bufferStart = (PUINT8)MmGetSystemAddressForMdlSafe(curMdl,
                                                           LowPagePriority);
        if (!bufferStart) {
            status = NDIS_STATUS_RESOURCES;
            goto ret_error;
        }

        bufferStart += NET_BUFFER_CURRENT_MDL_OFFSET(curNb);
        if (NET_BUFFER_NEXT_NB(curNb)) {
            OVS_LOG_TRACE("nb length %u next %u",
                          NET_BUFFER_DATA_LENGTH(curNb),
                          NET_BUFFER_DATA_LENGTH(curNb->Next));
        }

        /* L2 header */
        ethHdr = (EthHdr *)bufferStart;
        ASSERT(((PCHAR)&fwdInfo->dstMacAddr + sizeof fwdInfo->dstMacAddr) ==
               (PCHAR)&fwdInfo->srcMacAddr);
        NdisMoveMemory(ethHdr->Destination, fwdInfo->dstMacAddr,
                       sizeof ethHdr->Destination + sizeof ethHdr->Source);
        ethHdr->Type = htons(ETH_TYPE_IPV4);

        /* IP header */
        ipHdr = (IPHdr *)((PCHAR)ethHdr + sizeof *ethHdr);

        ipHdr->ihl = sizeof *ipHdr / 4;
        ipHdr->version = IPPROTO_IPV4;
        ipHdr->tos = tunKey->tos;
        ipHdr->tot_len = htons(NET_BUFFER_DATA_LENGTH(curNb) - sizeof *ethHdr);
        ipHdr->id = (uint16)atomic_add64(&vportVxlan->ipId,
                                         NET_BUFFER_DATA_LENGTH(curNb));
        ipHdr->frag_off = (tunKey->flags & OVS_TNL_F_DONT_FRAGMENT) ?
                          IP_DF_NBO : 0;
        ipHdr->ttl = tunKey->ttl ? tunKey->ttl : VXLAN_DEFAULT_TTL;
        ipHdr->protocol = IPPROTO_UDP;
        ASSERT(tunKey->dst == fwdInfo->dstIpAddr);
        ASSERT(tunKey->src == fwdInfo->srcIpAddr || tunKey->src == 0);
        ipHdr->saddr = fwdInfo->srcIpAddr;
        ipHdr->daddr = fwdInfo->dstIpAddr;

        ipHdr->check = 0;

        /* UDP header */
        udpHdr = (UDPHdr *)((PCHAR)ipHdr + sizeof *ipHdr);
        udpHdr->source = htons(tunKey->flow_hash | MAXINT16);
        udpHdr->dest = htons(vportVxlan->dstPort);
        udpHdr->len = htons(NET_BUFFER_DATA_LENGTH(curNb) - headRoom +
                            sizeof *udpHdr + sizeof *vxlanHdr);

        if (tunKey->flags & OVS_TNL_F_CSUM) {
            udpHdr->check = IPPseudoChecksum(&ipHdr->saddr, &ipHdr->daddr,
                                             IPPROTO_UDP, ntohs(udpHdr->len));
        } else {
            udpHdr->check = 0;
        }

        /* VXLAN header */
        vxlanHdr = (VXLANHdr *)((PCHAR)udpHdr + sizeof *udpHdr);
        vxlanHdr->flags1 = 0;
        vxlanHdr->locallyReplicate = 0;
        vxlanHdr->flags2 = 0;
        vxlanHdr->reserved1 = 0;
        vxlanHdr->vxlanID = VXLAN_TUNNELID_TO_VNI(tunKey->tunnelId);
        vxlanHdr->instanceID = 1;
        vxlanHdr->reserved2 = 0;
    }

    csumInfo.Value = 0;
    csumInfo.Transmit.IpHeaderChecksum = 1;
    csumInfo.Transmit.IsIPv4 = 1;
    if (tunKey->flags & OVS_TNL_F_CSUM) {
        csumInfo.Transmit.UdpChecksum = 1;
    }
    NET_BUFFER_LIST_INFO(curNbl,
                         TcpIpChecksumNetBufferListInfo) = csumInfo.Value;


    return STATUS_SUCCESS;

ret_error:
    OvsCompleteNBL(switchContext, *newNbl, TRUE);
    *newNbl = NULL;
    return status;
}


/*
 *----------------------------------------------------------------------------
 * OvsEncapVxlan --
 *     Encapsulates the packet if L2/L3 for destination resolves. Otherwise,
 *     enqueues a callback that does encapsulatation after resolution.
 *----------------------------------------------------------------------------
 */
NDIS_STATUS
OvsEncapVxlan(POVS_VPORT_ENTRY vport,
              PNET_BUFFER_LIST curNbl,
              OvsIPv4TunnelKey *tunKey,
              POVS_SWITCH_CONTEXT switchContext,
              POVS_PACKET_HDR_INFO layers,
              PNET_BUFFER_LIST *newNbl)
{
    NTSTATUS status;
    OVS_FWD_INFO fwdInfo;

    status = OvsLookupIPFwdInfo(tunKey->dst, &fwdInfo);
    if (status != STATUS_SUCCESS) {
        OvsFwdIPHelperRequest(NULL, 0, tunKey, NULL, NULL, NULL);
        // return NDIS_STATUS_PENDING;
        /*
         * XXX: Don't know if the completionList will make any sense when
         * accessed in the callback. Make sure the caveats are known.
         *
         * XXX: This code will work once we are able to grab locks in the
         * callback.
         */
        return NDIS_STATUS_FAILURE;
    }

    return OvsDoEncapVxlan(vport, curNbl, tunKey, &fwdInfo, layers,
                           switchContext, newNbl);
}


/*
 *----------------------------------------------------------------------------
 * OvsDecapVxlan
 *     Decapsulates to tunnel header in 'curNbl' and puts into 'tunKey'.
 *----------------------------------------------------------------------------
 */
NDIS_STATUS
OvsDecapVxlan(POVS_SWITCH_CONTEXT switchContext,
              PNET_BUFFER_LIST curNbl,
              OvsIPv4TunnelKey *tunKey,
              PNET_BUFFER_LIST *newNbl)
{
    PNET_BUFFER curNb;
    PMDL curMdl;
    EthHdr *ethHdr;
    IPHdr *ipHdr;
    UDPHdr *udpHdr;
    VXLANHdr *vxlanHdr;
    UINT32 tunnelSize, packetLength;
    PUINT8 bufferStart;
    NDIS_STATUS status;

    /* Check the length of the UDP payload */
    curNb = NET_BUFFER_LIST_FIRST_NB(curNbl);
    packetLength = NET_BUFFER_DATA_LENGTH(curNb);
    tunnelSize = OvsGetVxlanTunHdrSize();
    if (packetLength < tunnelSize) {
        return NDIS_STATUS_INVALID_LENGTH;
    }

    /*
     * Create a copy of the NBL so that we have all the headers in one MDL.
     */
    *newNbl = OvsPartialCopyNBL(switchContext, curNbl,
                                tunnelSize, 0,
                                TRUE /*copy NBL info */);

    if (*newNbl == NULL) {
        return NDIS_STATUS_RESOURCES;
    }

    /* XXX: Handle VLAN header. */
    curNbl = *newNbl;
    curNb = NET_BUFFER_LIST_FIRST_NB(curNbl);
    curMdl = NET_BUFFER_CURRENT_MDL(curNb);
    bufferStart = (PUINT8)MmGetSystemAddressForMdlSafe(curMdl, LowPagePriority) +
                  NET_BUFFER_CURRENT_MDL_OFFSET(curNb);
    if (!bufferStart) {
        status = NDIS_STATUS_RESOURCES;
        goto dropNbl;
    }

    ethHdr = (EthHdr *)bufferStart;
    /* XXX: Handle IP options. */
    ipHdr = (IPHdr *)((PCHAR)ethHdr + sizeof *ethHdr);
    tunKey->src = ipHdr->saddr;
    tunKey->dst = ipHdr->daddr;
    tunKey->tos = ipHdr->tos;
    tunKey->ttl = ipHdr->ttl;
    tunKey->pad = 0;
    udpHdr = (UDPHdr *)((PCHAR)ipHdr + sizeof *ipHdr);

    /* Validate if NIC has indicated checksum failure. */
    status = OvsValidateUDPChecksum(curNbl, udpHdr->check == 0);
    if (status != NDIS_STATUS_SUCCESS) {
        goto dropNbl;
    }

    /* Calculate and verify UDP checksum if NIC didn't do it. */
    if (udpHdr->check != 0) {
        tunKey->flags |= OVS_TNL_F_CSUM;
        status = OvsCalculateUDPChecksum(curNbl, curNb, ipHdr, udpHdr,
                                         packetLength);
        if (status != NDIS_STATUS_SUCCESS) {
            goto dropNbl;
        }
    }

    vxlanHdr = (VXLANHdr *)((PCHAR)udpHdr + sizeof *udpHdr);
    if (vxlanHdr->instanceID) {
        tunKey->flags |= OVS_TNL_F_KEY;
        tunKey->tunnelId = VXLAN_VNI_TO_TUNNELID(vxlanHdr->vxlanID);
    } else {
        tunKey->flags &= ~OVS_TNL_F_KEY;
        tunKey->tunnelId = 0;
    }

    /* Clear out the receive flag for the inner packet. */
    NET_BUFFER_LIST_INFO(curNbl, TcpIpChecksumNetBufferListInfo) = 0;
    NdisAdvanceNetBufferDataStart(curNb, tunnelSize, FALSE, NULL);
    return NDIS_STATUS_SUCCESS;

dropNbl:
    OvsCompleteNBL(switchContext, *newNbl, TRUE);
    *newNbl = NULL;
    return status;
}


NDIS_STATUS
OvsSlowPathDecapVxlan(const PNET_BUFFER_LIST packet,
                   OvsIPv4TunnelKey *tunnelKey)
{
    NDIS_STATUS status = NDIS_STATUS_FAILURE;
    UDPHdr udpStorage;
    const UDPHdr *udp;
    VXLANHdr *VxlanHeader;
    VXLANHdr  VxlanHeaderBuffer;
    struct IPHdr ip_storage;
    const struct IPHdr *nh;
    OVS_PACKET_HDR_INFO layers;

    layers.value = 0;

    do {
        nh = OvsGetIp(packet, layers.l3Offset, &ip_storage);
        if (nh) {
            layers.l4Offset = layers.l3Offset + nh->ihl * 4;
        } else {
            break;
        }

        /* make sure it's a VXLAN packet */
        udp = OvsGetUdp(packet, layers.l4Offset, &udpStorage);
        if (udp) {
            layers.l7Offset = layers.l4Offset + sizeof *udp;
            if (udp->check != 0) {
                tunnelKey->flags |= OVS_TNL_F_CSUM;
            }
        } else {
            break;
        }

        VxlanHeader = (VXLANHdr *)OvsGetPacketBytes(packet,
                                                    sizeof(*VxlanHeader),
                                                    layers.l7Offset,
                                                    &VxlanHeaderBuffer);

        if (VxlanHeader) {
            tunnelKey->src = nh->saddr;
            tunnelKey->dst = nh->daddr;
            tunnelKey->ttl = nh->ttl;
            tunnelKey->tos = nh->tos;
            if (VxlanHeader->instanceID) {
                tunnelKey->flags |= OVS_TNL_F_KEY;
                tunnelKey->tunnelId = VXLAN_VNI_TO_TUNNELID(VxlanHeader->vxlanID);
            } else {
                tunnelKey->flags &= ~OVS_TNL_F_KEY;
                tunnelKey->tunnelId = 0;
            }
        } else {
            break;
        }
        status = NDIS_STATUS_SUCCESS;

    } while(FALSE);

    return status;
}

#pragma warning( pop )
