/*
 *  OpenVPN -- An application to securely tunnel IP networks
 *             over a single TCP/UDP port, with support for SSL/TLS-based
 *             session authentication and key exchange,
 *             packet encryption, packet authentication, and
 *             packet compression.
 *
 *  Copyright (C) 2002-2023 OpenVPN Technologies, Inc. <sales@openvpn.net>
 *  Copyright (C) 2010      Fabian Knittel <fabian.knittel@lettink.de>
 *
 *  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; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#elif defined(_MSC_VER)
#include "config-msvc.h"
#endif

#include "syshead.h"

#include "multi.h"
#include "options.h"
#include "vlan.h"

/*
 * Retrieve the VLAN Identifier (VID) from the IEEE 802.1Q header.
 *
 * @param hdr Pointer to the Ethernet header with IEEE 802.1Q tagging.
 * @return    Returns the VID in host byte order.
 */
static uint16_t
vlanhdr_get_vid(const struct openvpn_8021qhdr *hdr)
{
    return ntohs(hdr->pcp_cfi_vid & OPENVPN_8021Q_MASK_VID);
}

/*
 * Set the VLAN Identifier (VID) in an IEEE 802.1Q header.
 *
 * @param hdr Pointer to the Ethernet header with IEEE 802.1Q tagging.
 * @param vid The VID to set (in host byte order).
 */
static void
vlanhdr_set_vid(struct openvpn_8021qhdr *hdr, const uint16_t vid)
{
    hdr->pcp_cfi_vid = (hdr->pcp_cfi_vid & ~OPENVPN_8021Q_MASK_VID)
                       | (htons(vid) & OPENVPN_8021Q_MASK_VID);
}

/*
 * vlan_decapsulate - remove 802.1q header and return VID
 *
 * For vlan_accept == VLAN_ONLY_UNTAGGED_OR_PRIORITY:
 *   Only untagged frames and frames that are priority-tagged (VID == 0) are
 *   accepted.  (This means that VLAN-tagged frames are dropped.)  For frames
 *   that aren't dropped, the global vlan_pvid is returned as VID.
 *
 * For vlan_accept == VLAN_ONLY_TAGGED:
 *   If a frame is VLAN-tagged the tagging is removed and the embedded VID is
 *   returned.  Any included priority information is lost.
 *   If a frame isn't VLAN-tagged, the frame is dropped.
 *
 * For vlan_accept == VLAN_ALL:
 *   Accepts both VLAN-tagged and untagged (or priority-tagged) frames and
 *   and handles them as described above.
 *
 * @param c   The global context.
 * @param buf The ethernet frame.
 * @return    Returns -1 if the frame is dropped or the VID if it is accepted.
 */
int16_t
vlan_decapsulate(const struct context *c, struct buffer *buf)
{
    const struct openvpn_8021qhdr *vlanhdr;
    struct openvpn_ethhdr *ethhdr;
    uint16_t vid;

    /* assume untagged frame */
    if (BLEN(buf) < sizeof(*ethhdr))
    {
        goto drop;
    }

    ethhdr = (struct openvpn_ethhdr *)BPTR(buf);
    if (ethhdr->proto != htons(OPENVPN_ETH_P_8021Q))
    {
        /* reject untagged frame */
        if (c->options.vlan_accept == VLAN_ONLY_TAGGED)
        {
            msg(D_VLAN_DEBUG,
                "dropping frame without vlan-tag (proto/len 0x%04x)",
                ntohs(ethhdr->proto));
            goto drop;
        }

        /* untagged frame is accepted and associated with the global VID */
        msg(D_VLAN_DEBUG,
            "assuming pvid for frame without vlan-tag, pvid: %u (proto/len 0x%04x)",
            c->options.vlan_pvid, ntohs(ethhdr->proto));

        return c->options.vlan_pvid;
    }

    /* tagged frame */
    if (BLEN(buf) < sizeof(*vlanhdr))
    {
        goto drop;
    }

    vlanhdr = (const struct openvpn_8021qhdr *)BPTR(buf);
    vid = vlanhdr_get_vid(vlanhdr);

    switch (c->options.vlan_accept)
    {
        case VLAN_ONLY_UNTAGGED_OR_PRIORITY:
            /* VLAN-tagged frame: drop packet */
            if (vid != 0)
            {
                msg(D_VLAN_DEBUG, "dropping frame with vlan-tag, vid: %u (proto/len 0x%04x)",
                    vid, ntohs(vlanhdr->proto));
                goto drop;
            }

        /* vid == 0 means prio-tagged packet: don't drop and fall-through */
        case VLAN_ONLY_TAGGED:
        case VLAN_ALL:
            /* tagged frame can be accepted: extract vid and strip encapsulation */

            /* in case of prio-tagged frame (vid == 0), assume the sender
             * knows what he is doing and forward the packet as it is, so to
             * keep the priority information intact.
             */
            if (vid == 0)
            {
                /* return the global VID for priority-tagged frames */
                return c->options.vlan_pvid;
            }

            /* here we have a proper VLAN tagged frame: perform decapsulation
             * and return embedded VID
             */
            msg(D_VLAN_DEBUG,
                "removing vlan-tag from frame: vid: %u, wrapped proto/len: 0x%04x",
                vid, ntohs(vlanhdr->proto));

            /* save inner protocol to be restored later after decapsulation */
            uint16_t proto = vlanhdr->proto;
            /* move the buffer head forward to adjust the headroom to a
             * non-tagged frame
             */
            buf_advance(buf, SIZE_ETH_TO_8021Q_HDR);
            /* move the content of the 802.1q header to the new head, so that
             * src/dst addresses are copied over
             */
            ethhdr = memmove(BPTR(buf), vlanhdr, sizeof(*ethhdr));
            /* restore the inner protocol value */
            ethhdr->proto = proto;

            return vid;
    }

drop:
    buf->len = 0;
    return -1;
}

/*
 * vlan_encapsulate - add 802.1q header and set the context related VID
 *
 * Assumes vlan_accept == VLAN_ONLY_TAGGED
 *
 * @param c   The current context.
 * @param buf The ethernet frame to encapsulate.
 */
void
vlan_encapsulate(const struct context *c, struct buffer *buf)
{
    const struct openvpn_ethhdr *ethhdr;
    struct openvpn_8021qhdr *vlanhdr;

    if (BLEN(buf) < sizeof(*ethhdr))
    {
        goto drop;
    }

    ethhdr = (const struct openvpn_ethhdr *)BPTR(buf);
    if (ethhdr->proto == htons(OPENVPN_ETH_P_8021Q))
    {
        /* Priority-tagged frame. (VLAN-tagged frames have been dropped before
         * getting to this point)
         */

        /* Frame too small for header type? */
        if (BLEN(buf) < sizeof(*vlanhdr))
        {
            goto drop;
        }

        vlanhdr = (struct openvpn_8021qhdr *)BPTR(buf);

        /* sanity check: ensure this packet is really just prio-tagged */
        uint16_t vid = vlanhdr_get_vid(vlanhdr);
        if (vid != 0)
        {
            goto drop;
        }
    }
    else
    {
        /* Untagged frame. */

        /* Not enough head room for VLAN tag? */
        if (buf_reverse_capacity(buf) < SIZE_ETH_TO_8021Q_HDR)
        {
            goto drop;
        }

        vlanhdr = (struct openvpn_8021qhdr *)buf_prepend(buf,
                                                         SIZE_ETH_TO_8021Q_HDR);

        /* Initialise VLAN/802.1q header.
         * Move the Eth header so to keep dst/src addresses the same and then
         * assign the other fields.
         *
         * Also, save the inner protocol first, so that it can be restored later
         * after the memmove()
         */
        uint16_t proto = ethhdr->proto;
        memmove(vlanhdr, ethhdr, sizeof(*ethhdr));
        vlanhdr->tpid = htons(OPENVPN_ETH_P_8021Q);
        vlanhdr->pcp_cfi_vid = 0;
        vlanhdr->proto = proto;
    }

    /* set the VID corresponding to the current context (client) */
    vlanhdr_set_vid(vlanhdr, c->options.vlan_pvid);

    msg(D_VLAN_DEBUG, "tagging frame: vid %u (wrapping proto/len: %04x)",
        c->options.vlan_pvid, vlanhdr->proto);
    return;

drop:
    /* Drop the frame. */
    buf->len = 0;
}

/*
 * vlan_is_tagged - check if a packet is VLAN-tagged
 *
 * Checks whether ethernet frame is VLAN-tagged.
 *
 * @param buf The ethernet frame.
 * @return    Returns true if the frame is VLAN-tagged, false otherwise.
 */
bool
vlan_is_tagged(const struct buffer *buf)
{
    const struct openvpn_8021qhdr *vlanhdr;
    uint16_t vid;

    if (BLEN(buf) < sizeof(struct openvpn_8021qhdr))
    {
        /* frame too small to be VLAN-tagged */
        return false;
    }

    vlanhdr = (const struct openvpn_8021qhdr *)BPTR(buf);

    if (ntohs(vlanhdr->tpid) != OPENVPN_ETH_P_8021Q)
    {
        /* non tagged frame */
        return false;
    }

    vid = vlanhdr_get_vid(vlanhdr);
    if (vid == 0)
    {
        /* no vid: piority tagged only */
        return false;
    }

    return true;
}

void
vlan_process_outgoing_tun(struct multi_context *m, struct multi_instance *mi)
{
    if (!m->top.options.vlan_tagging)
    {
        return;
    }

    if (m->top.options.vlan_accept == VLAN_ONLY_UNTAGGED_OR_PRIORITY)
    {
        /* Packets forwarded to the TAP devices aren't VLAN-tagged. Only packets
         * matching the PVID configured globally are allowed to be received
         */
        if (m->top.options.vlan_pvid != mi->context.options.vlan_pvid)
        {
            /* Packet is coming from the wrong VID, drop it.  */
            mi->context.c2.to_tun.len = 0;
        }
    }
    else if (m->top.options.vlan_accept == VLAN_ALL)
    {
        /* Packets either need to be VLAN-tagged or not, depending on the
         * packet's originating VID and the port's native VID (PVID).  */

        if (m->top.options.vlan_pvid != mi->context.options.vlan_pvid)
        {
            /* Packets need to be VLAN-tagged, because the packet's VID does not
             * match the port's PVID.  */
            vlan_encapsulate(&mi->context, &mi->context.c2.to_tun);
        }
    }
    else if (m->top.options.vlan_accept == VLAN_ONLY_TAGGED)
    {
        /* All packets on the port (the tap device) need to be VLAN-tagged.  */
        vlan_encapsulate(&mi->context, &mi->context.c2.to_tun);
    }
}
