/****************************************************************************
 * net/ipfrag/ipv6_frag.c
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.  The
 * ASF licenses this file to you 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.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>
#if defined(CONFIG_NET_IPv6) && defined (CONFIG_NET_IPFRAG)

#include <sys/ioctl.h>
#include <stdint.h>
#include <stdlib.h>
#include <debug.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <net/if.h>

#include <nuttx/kmalloc.h>
#include <nuttx/net/netconfig.h>
#include <nuttx/net/netdev.h>
#include <nuttx/net/netstats.h>
#include <nuttx/net/ip.h>
#include <nuttx/net/ipv6ext.h>

#include "netdev/netdev.h"
#include "inet/inet.h"
#include "ipfrag.h"

/****************************************************************************
 * Private Data
 ****************************************************************************/

/* The increasing number used for the IP ID field of IPv6 Fragment Header. */

static DEFINE_PER_CPU_BMP(uint32_t, g_ipv6id);
#define g_ipv6id this_cpu_var_bmp(g_ipv6id)

/****************************************************************************
 * Private Function Prototypes
 ****************************************************************************/

static int32_t ipv6_fragin_getinfo(FAR struct iob_s *iob,
                                   FAR struct ip_fraglink_s *fraglink);
static uint32_t ipv6_fragin_reassemble(FAR struct ip_fragsnode_s *node);
static inline void
ipv6_fragout_buildipv6header(FAR struct ipv6_hdr_s *ref,
                             FAR struct ipv6_hdr_s *ipv6,
                             uint16_t hdrlen, uint16_t datalen,
                             uint16_t nxthdroff, uint16_t nxtprot);
static inline void
ipv6_fragout_buildipv6fragheader(FAR struct ipv6_fragment_extension_s *frag,
                                 uint8_t nxthdr, uint16_t ipoff,
                                 uint32_t ipid);
static uint16_t ipv6_fragout_getunfraginfo(FAR struct iob_s *iob,
                                           uint16_t *hdroff,
                                           uint16_t *hdrtype);

/****************************************************************************
 * Private Functions
 ****************************************************************************/

/****************************************************************************
 * Name: ipv6_fragin_getinfo
 *
 * Description:
 *   Polulate fragment information from the input ipv6 packet data.
 *
 * Input Parameters:
 *   iob      - An IPv6 fragment
 *   fraglink - node of the lower-level linked list, it maintains information
 *              of one fragment
 *
 * Returned Value:
 *   OK    - Got fragment information.
 *   EINVAL - The input ipv6 packet is not a fragment.
 *
 ****************************************************************************/

static int32_t ipv6_fragin_getinfo(FAR struct iob_s *iob,
                                   FAR struct ip_fraglink_s *fraglink)
{
  FAR struct ipv6_hdr_s *ipv6 = (FAR struct ipv6_hdr_s *)
                                (iob->io_data + iob->io_offset);
  FAR struct ipv6_extension_s *exthdr;
  FAR uint8_t *payload;
  uint16_t paylen;
  uint8_t nxthdr;

  paylen  = ((uint16_t)ipv6->len[0] << 8) + (uint16_t)ipv6->len[1];
  payload = (FAR uint8_t *)(ipv6 + 1);
  exthdr  = (FAR struct ipv6_extension_s *)payload;
  nxthdr  = ipv6->proto;

  while (nxthdr != NEXT_FRAGMENT_EH && ipv6_exthdr(nxthdr))
    {
      uint16_t extlen;

      exthdr   = (FAR struct ipv6_extension_s *)payload;
      extlen   = EXTHDR_LEN((unsigned int)exthdr->len);
      payload += extlen;
      paylen  -= extlen;
      nxthdr   = exthdr->nxthdr;
    };

  if (nxthdr == NEXT_FRAGMENT_EH)
    {
      FAR struct ipv6_fragment_extension_s *fraghdr;

      fraghdr = (FAR struct ipv6_fragment_extension_s *)exthdr;

      /* Cut the size of fragment header, notice fragment header don't has a
       * length filed.
       */

      paylen -= EXTHDR_FRAG_LEN;

      fraglink->flink     = NULL;
      fraglink->fragsnode = NULL;

      fraglink->isipv4    = FALSE;
      fraglink->fragoff   = (fraghdr->msoffset << 8) + fraghdr->lsoffset;
      fraglink->morefrags = fraglink->fragoff & 0x1;
      fraglink->fragoff  &= 0xfff8;
      fraglink->fraglen   = paylen;
      fraglink->ipid      = NTOHL(
        ((uint32_t)(*(FAR uint16_t *)(&fraghdr->id[0])) << 16) +
         (uint32_t)(*(FAR uint16_t *)(&fraghdr->id[2])));

      fraglink->frag      = iob;

      return OK;
    }
  else
    {
      return -EINVAL;
    }
}

/****************************************************************************
 * Name: ipv6_fragin_reassemble
 *
 * Description:
 *   Reassemble all ipv6 fragments to build an IP frame.
 *
 * Input Parameters:
 *   node - node of the upper-level linked list, it maintains
 *          information about all fragments belonging to an IP datagram
 *
 * Returned Value:
 *   The length of the reassembled IP frame
 *
 ****************************************************************************/

static uint32_t ipv6_fragin_reassemble(FAR struct ip_fragsnode_s *node)
{
  FAR struct iob_s *head = NULL;
  FAR struct ipv6_hdr_s *ipv6;
  FAR struct ip_fraglink_s *fraglink;

  /* Loop to walk through the fragment list and reassemble those fragments,
   * the fraglink list was ordered by fragment offset value
   */

  fraglink = node->frags;
  node->frags = NULL;

  while (fraglink != NULL)
    {
      FAR uint8_t *payload;
      uint8_t nxthdr;
      FAR struct iob_s *iob;
      FAR struct ip_fraglink_s *linknext;
      FAR struct ipv6_extension_s *exthdr;
      FAR struct ipv6_fragment_extension_s *fraghdr;

      iob     = fraglink->frag;
      ipv6    = (FAR struct ipv6_hdr_s *)(iob->io_data + iob->io_offset);
      payload = (FAR uint8_t *)(ipv6 + 1);
      exthdr  = (FAR struct ipv6_extension_s *)payload;
      nxthdr  = ipv6->proto;

      /* Find fragment header and the front header which is close to the
       * fragment header
       */

      while (nxthdr != NEXT_FRAGMENT_EH && ipv6_exthdr(nxthdr))
        {
          uint16_t extlen;

          exthdr   = (FAR struct ipv6_extension_s *)payload;
          extlen   = EXTHDR_LEN((unsigned int)exthdr->len);
          payload += extlen;
          nxthdr   = exthdr->nxthdr;
        };

      fraghdr  = (FAR struct ipv6_fragment_extension_s *)payload;

      /* Skip fragment header, notice fragment header don't has a length
       * filed
       */

      payload += EXTHDR_FRAG_LEN;

      if (fraglink->fragoff == 0)
        {
          /* This is the zero fragment, Set the front header's next header
           * filed to the next header value of the fragment header
           */

          if (ipv6->proto == NEXT_FRAGMENT_EH)
            {
              ipv6->proto = fraghdr->nxthdr;
            }
          else
            {
              exthdr->nxthdr = fraghdr->nxthdr;
            }

          /* Remove fragment header and fix up the data length */

          memmove(fraghdr, payload,
                  iob->io_len - (payload - (iob->io_data + iob->io_offset)));
          iob->io_len    -= EXTHDR_FRAG_LEN;
          iob->io_pktlen -= EXTHDR_FRAG_LEN;

          /* Remember the head iob */

          head = iob;
        }
      else
        {
          uint16_t new_off;

          /* Fix up the value of offset and length for this none zero
           * fragment
           */

          new_off         = payload - iob->io_data;
          iob->io_len    -= new_off - iob->io_offset;
          iob->io_pktlen -= new_off - iob->io_offset;
          iob->io_offset  = new_off;

          /* Concatenate this iob to the reassembly chain */

          iob_concat(head, iob);
        }

      linknext = fraglink->flink;
      kmm_free(fraglink);
      fraglink = linknext;
    }

  /* Remember the reassembled outgoing IP frame */

  node->outgoframe = head;

  /* Adjust the length value in the IP Header */

  ipv6 = (FAR struct ipv6_hdr_s *)(head->io_data + head->io_offset);
  ipv6->len[0] = (head->io_pktlen - IPv6_HDRLEN) >> 8;
  ipv6->len[1] = (head->io_pktlen - IPv6_HDRLEN) & 0xff;

  return head->io_pktlen;
}

/****************************************************************************
 * Name: ipv6_fragout_buildipv6header
 *
 * Description:
 *   Build IPv6 header for an IPv6 fragment.
 *
 * Input Parameters:
 *   ref    - The reference IPv6 Header
 *   ipv6   - The pointer of the newly generated IPv6 Header
 *   hdrlen - Including the length of IPv6 basic header and all
 *            extention headers
 *   datalen   - The data length follows the IPv6 basic header
 *   nxthdroff - The offset of 'next header' to be updated
 *   nxtprot   - The value of 'next header' to be updated
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

static inline void
ipv6_fragout_buildipv6header(FAR struct ipv6_hdr_s *ref,
                             FAR struct ipv6_hdr_s *ipv6,
                             uint16_t hdrlen, uint16_t datalen,
                             uint16_t nxthdroff, uint16_t nxtprot)
{
  if (ref != ipv6)
    {
      /* Copy unfragmentable header data from reference header */

      memcpy(ipv6, ref, hdrlen);
    }

  /* Update length filed */

  ipv6->len[0]      = datalen >> 8;
  ipv6->len[1]      = datalen & 0xff;

  /* If extension headers exist, update the Next Header field in the
   * last extension header of the unfragmentable part; Otherwise update
   * the Next Header field of the basic IPv6 header.
   */

  *((uint8_t *)ipv6 + nxthdroff) = nxtprot;
}

/****************************************************************************
 * Name: ipv6_fragout_buildipv6fragheader
 *
 * Description:
 *   Build IPv6 fragment extension header for an IPv6 fragment.
 *
 * Input Parameters:
 *   frag    - The pointer of the newly generated IPv6 fragment Header
 *   nxthdr  - The first header type in the fragmentable part
 *   ipoff   - Fragment offset
 *   ipid    - The value of IPv6 IP ID
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

static inline void
ipv6_fragout_buildipv6fragheader(FAR struct ipv6_fragment_extension_s *frag,
                                 uint8_t nxthdr, uint16_t ipoff,
                                 uint32_t ipid)
{
  frag->nxthdr = nxthdr;
  frag->reserved = 0;
  frag->msoffset = ipoff >> 8;
  frag->lsoffset = ipoff & 0xff;
  *(FAR uint16_t *)&frag->id[0] = HTONL(ipid) & 0xffff;
  *(FAR uint16_t *)&frag->id[2] = HTONL(ipid) >> 16;
}

/****************************************************************************
 * Name: ipv6_fragout_getunfraginfo
 *
 * Description:
 *   Get the length of Unfragmentable Part of the original ipv6 packet,
 *   remember the offset and value of nextheader in the last extension
 *   header of the unfragmentable part.
 *   Refer to rfc2460, section-4.1, section-4.5
 *
 * Input Parameters:
 *   iob     - Outgoing data waiting for fragment
 *   hdroff  - The offset of the last next header position in the
 *             unfragmentable part
 *   hdrtype - The first header type in the fragmentable part
 *
 * Returned Value:
 *   Unfragmentable Part length
 *
 ****************************************************************************/

static uint16_t ipv6_fragout_getunfraginfo(FAR struct iob_s *iob,
                                           uint16_t *hdroff,
                                           uint16_t *hdrtype)
{
  uint32_t     iter = 0;
  bool         destopt = false;
  uint16_t     delta = sizeof(struct ipv6_hdr_s);
  uint16_t     unfraglen = delta;
  uint8_t      nxthdr;
  FAR struct   ipv6_hdr_s *ipv6;
  FAR struct   ipv6_extension_s *exthdr;
  FAR uint8_t *payload;

  ipv6     = (FAR struct ipv6_hdr_s *)(iob->io_data + iob->io_offset);
  payload  = (FAR uint8_t *)(ipv6 + 1);
  exthdr   = (FAR struct ipv6_extension_s *)payload;
  nxthdr   = ipv6->proto;

  *hdroff  = offsetof(struct ipv6_hdr_s, proto);
  *hdrtype = ipv6->proto;

  /* Traverse up to three extension headers, if the Destination Options
   * Header appears repeatedly, ingore the secondary one and end the search.
   * refer to rfc2460, section-4.1
   */

  while (ipv6_exthdr(nxthdr) && iter++ < 3)
    {
      uint16_t extlen;

      exthdr   = (FAR struct ipv6_extension_s *)payload;
      extlen   = EXTHDR_LEN((unsigned int)exthdr->len);

      switch (nxthdr)
        {
          case NEXT_DESTOPT_EH:
            if (!destopt)
              {
                destopt = true;
              }
            else
              {
                /* This is the secondary Destination Options Header,
                 * end the search
                 */

                goto done;
              }

          case NEXT_ROUTING_EH:
          case NEXT_HOPBYBOT_EH:
              unfraglen = delta + extlen;
              *hdroff = delta;
              *hdrtype = exthdr->nxthdr;
        }

      payload += extlen;
      delta   += extlen;
      nxthdr   = exthdr->nxthdr;
    }

done:
  return unfraglen;
}

/****************************************************************************
 * Public Functions
 ****************************************************************************/

/****************************************************************************
 * Name: ipv6_fragin
 *
 * Description:
 *   Handling incoming IPv6 fragment input, the input data
 *   (dev->d_iob) can be an I/O buffer chain
 *
 * Input Parameters:
 *   dev    - The NIC device that the fragmented data comes from
 *
 * Returned Value:
 *   ENOMEM - No memory
 *   OK     - The input fragment is processed as expected
 *
 ****************************************************************************/

int32_t ipv6_fragin(FAR struct net_driver_s *dev)
{
  FAR struct ip_fragsnode_s *node = NULL;
  FAR struct ip_fraglink_s *fraginfo = NULL;
  bool restartwdog;

  if (dev->d_len != dev->d_iob->io_pktlen)
    {
      nerr("ERROR: Parameters error.\n");
      return -EINVAL;
    }

  fraginfo = kmm_malloc(sizeof(struct ip_fraglink_s));
  if (fraginfo == NULL)
    {
      nerr("ERROR: Failed to allocate buffer.\n");
      return -ENOMEM;
    }

  /* Polulate fragment information from input packet data */

  ipv6_fragin_getinfo(dev->d_iob, fraginfo);

  nxmutex_lock(&g_ipfrag_lock);

  /* Need to restart reassembly worker if the original linked list is empty */

  restartwdog = ip_fragin_enqueue(dev, fraginfo);

  node = fraginfo->fragsnode;
  if (node->verifyflag & IP_FRAGVERIFY_RECVDALLFRAGS)
    {
      /* Well, all fragments of an IP frame have been received, remove
       * node from link list first, then reassemble and dispatch to the
       * stack.
       */

      ip_frag_remnode(node);

      /* All fragments belonging to one IP frame have been separated
       * from the fragment processing module, unlocks mutex as soon
       * as possible
       */

      nxmutex_unlock(&g_ipfrag_lock);

      /* Reassemble fragments to one IP frame and set the resulting
       * IP frame to dev->d_iob
       */

      ipv6_fragin_reassemble(node);
      netdev_iob_replace_l2(dev, node->outgoframe);

      /* Free the memory of node */

      kmm_free(node);

      return ipv6_input(dev);
    }

  nxmutex_unlock(&g_ipfrag_lock);

  if (restartwdog)
    {
      /* Restart the work queue for fragment processing */

      ip_frag_startwdog();
    }

  return OK;
}

/****************************************************************************
 * Name: ipv6_fragout
 *
 * Description:
 *   Execute the ipv6 fragment function. After this work is done, all
 *   fragments are maintained by dev->d_fragout. In order to reduce the
 *   cyclomatic complexity and facilitate maintenance, fragmentation is
 *   performed in two steps:
 *   1. Reconstruct I/O Buffer according to MTU, which will reserve
 *      the space for the L3 header;
 *   2. Fill the L3 header into the reserved space.
 *
 * Input Parameters:
 *   dev    - The NIC device
 *   mtu    - The MTU of current NIC
 *
 * Returned Value:
 *   0 if success or a negative value if fail.
 *
 * Assumptions:
 *   Data length(dev->d_iob->io_pktlen) is grater than the MTU of
 *   current NIC
 *
 ****************************************************************************/

int32_t ipv6_fragout(FAR struct net_driver_s *dev, uint16_t mtu)
{
  uint16_t unfraglen;
  uint16_t offset = 0;
  uint32_t ipid;
  uint32_t iter;
  uint32_t nfrags;
  uint16_t hdroff;
  uint16_t hdrtype;
  FAR struct iob_s *frag = NULL;
  FAR struct ipv6_hdr_s *ref = NULL;
  FAR struct ipv6_fragment_extension_s *fraghdr;
  struct iob_queue_s fragq =
    {
      NULL, NULL
    };

  /* Get the length of Unfragmentable Part of the original ipv6 packet,
   * Get the offset and value of nextheader filed in the last extension
   * header of the unfragmentable part.
   */

  unfraglen = ipv6_fragout_getunfraginfo(dev->d_iob, &hdroff, &hdrtype);

  /* Reconstruct I/O Buffer according to MTU, which will reserve
   * the space for the L3 header
   */

  nfrags = ip_fragout_slice(dev->d_iob, PF_INET6, mtu, unfraglen, &fragq);
  netdev_iob_clear(dev);

  /* No I/O Buffer is the only cause of failure */

  if (nfrags == 0)
    {
      goto fail;
    }

  ipid = ++g_ipv6id;

  /* Fill the L3 header into the reserved space */

  for (iter = 0; iter < nfrags; iter++)
    {
      frag = iob_remove_queue(&fragq);

      if (iter == 0)
        {
          ref = (FAR struct ipv6_hdr_s *)(frag->io_data + frag->io_offset);

          /* Update the IPv6 header for the zero fragment */

          ipv6_fragout_buildipv6header(ref, ref, unfraglen,
                frag->io_pktlen - IPv6_HDRLEN, hdroff, NEXT_FRAGMENT_EH);

          /* Build the fragment header for the zero fragment */

          fraghdr = (FAR struct ipv6_fragment_extension_s *)
                    (frag->io_data + frag->io_offset + unfraglen);
          ipv6_fragout_buildipv6fragheader(fraghdr, hdrtype,
                    FRAGHDR_FRAG_MOREFRAGS, ipid);
        }
      else
        {
          uint16_t ipoff = offset - iter * (unfraglen + EXTHDR_FRAG_LEN);

          if (iter < nfrags - 1)
            {
              ipoff |= FRAGHDR_FRAG_MOREFRAGS;
            }

          /* Refer to the zero fragment ipv6 header to construct the ipv6
           * header of non-zero fragment
           */

          ipv6_fragout_buildipv6header(ref,
                  (FAR struct ipv6_hdr_s *)(frag->io_data + frag->io_offset),
                  unfraglen, frag->io_pktlen - IPv6_HDRLEN, hdroff,
                  NEXT_FRAGMENT_EH);

          /* Build extension fragment header for non-zero fragment */

          fraghdr = (FAR struct ipv6_fragment_extension_s *)
                    (frag->io_data + frag->io_offset + unfraglen);
          ipv6_fragout_buildipv6fragheader(fraghdr, hdrtype, ipoff, ipid);
        }

      /* Enqueue this fragment to dev->d_fragout */

      if (iob_tryadd_queue(frag, &dev->d_fragout) < 0)
        {
          goto fail;
        }

      offset += frag->io_pktlen;
    }

#ifdef CONFIG_NET_STATISTICS
  g_netstats.ipv6.sent += nfrags - 1;
#endif

  netdev_txnotify_dev(dev, IPFRAG_POLL);

  return OK;

fail:
  netdev_iob_release(dev);
  iob_free_chain(frag);
  iob_free_queue(&fragq);
  iob_free_queue(&dev->d_fragout);
  --g_ipv6id;
  return -ENOMEM;
}

#endif /* CONFIG_NET_IPv6 && CONFIG_NET_IPFRAG */