/****************************************************************************
 * net/ipforward/ipv6_forward.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>

#include <string.h>
#include <assert.h>
#include <errno.h>
#include <debug.h>

#include <nuttx/mm/iob.h>
#include <nuttx/net/ipv6ext.h>
#include <nuttx/net/net.h>
#include <nuttx/net/netdev.h>
#include <nuttx/net/netstats.h>

#include "nat/nat.h"
#include "netdev/netdev.h"
#include "sixlowpan/sixlowpan.h"
#include "devif/devif.h"
#include "icmpv6/icmpv6.h"
#include "ipfilter/ipfilter.h"
#include "ipforward/ipforward.h"

#if defined(CONFIG_NET_IPFORWARD) && defined(CONFIG_NET_IPv6)

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/

#define PACKET_FORWARDED     0
#define PACKET_NOT_FORWARDED 1

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

/****************************************************************************
 * Name: ipv6_hdrsize
 *
 * Description:
 *   Return the size of the IPv6 header and the following.
 *
 * Input Parameters:
 *   ipv6  - A pointer to the IPv6 header in within the IPv6 packet.  This
 *           is immediately followed by the L3 header which may be TCP, UDP,
 *           or ICMPv6.
 *
 * Returned Value:
 *   The size of the combined L2 + L3 headers is returned on success.  An
 *   error is returned only if the prototype is not supported.
 *
 ****************************************************************************/

#ifdef CONFIG_DEBUG_NET_WARN
static int ipv6_hdrsize(FAR struct ipv6_hdr_s *ipv6)
{
  /* Size is determined by the following protocol header, */

  switch (ipv6->proto)
    {
#ifdef CONFIG_NET_TCP
    case IP_PROTO_TCP:
      {
        FAR struct tcp_hdr_s *tcp =
          (FAR struct tcp_hdr_s *)((FAR uint8_t *)ipv6 + IPv6_HDRLEN);
        unsigned int tcpsize;

        /* The TCP header length is encoded in the top 4 bits of the
         * tcpoffset field (in units of 32-bit words).
         */

        tcpsize = ((uint16_t)tcp->tcpoffset >> 4) << 2;
        return IPv6_HDRLEN + tcpsize;
      }
      break;
#endif

#ifdef CONFIG_NET_UDP
    case IP_PROTO_UDP:
      return IPv6_HDRLEN + UDP_HDRLEN;
      break;
#endif

#ifdef CONFIG_NET_ICMPv6
    case IP_PROTO_ICMP6:
      return IPv6_HDRLEN + ICMPv6_HDRLEN;
      break;
#endif

#ifdef CONFIG_NET_IPFRAG
    case NEXT_FRAGMENT_EH:
      return IPv6_HDRLEN + EXTHDR_FRAG_LEN;
      break;
#endif

    default:
      nwarn("WARNING: Unrecognized proto: %u\n", ipv6->proto);
      return -EPROTONOSUPPORT;
    }
}
#endif

/****************************************************************************
 * Name: ipv6_decr_ttl
 *
 * Description:
 *   Decrement the IPv6 TTL (time to live value).  TTL field is set by the
 *   sender of the packet and reduced by every router on the route to its
 *   destination. If the TTL field reaches zero before the datagram arrives
 *   at its destination, then the datagram is discarded and an ICMP error
 *   packet (11 - Time Exceeded) is sent back to the sender.
 *
 *   The purpose of the TTL field is to avoid a situation in which an
 *   undeliverable datagram keeps circulating on an Internet system, and
 *   such a system eventually becoming swamped by such "immortals".
 *
 * Input Parameters:
 *   ipv6  - A pointer to the IPv6 header in within the IPv6 packet to be
 *           forwarded.
 *
 * Returned Value:
 *   The new TTL value is returned.  A value <= 0 means the hop limit has
 *   expired.
 *
 ****************************************************************************/

static int ipv6_decr_ttl(FAR struct ipv6_hdr_s *ipv6)
{
  int ttl = (int)ipv6->ttl - 1;

  if (ttl <= 0)
    {
      /* Return zero which must cause the packet to be dropped */

      return 0;
    }

  /* Save the updated TTL value */

  ipv6->ttl = ttl;

  /* NOTE: We do not have to recalculate the IPv6 checksum because (1) the
   * IPv6 header does not include a checksum itself and (2) the TTL is not
   * included in the sum for the TCP and UDP headers.
   */

  return ttl;
}

/****************************************************************************
 * Name: ipv6_packet_conversion
 *
 * Description:
 *   Generic output conversion hook.  Only needed for IEEE802.15.4 for now
 *   but this is a point where support for other conversions may be
 *   provided.
 *
 * Returned Value:
 *   PACKET_FORWARDED     - Packet was forwarded
 *   PACKET_NOT_FORWARDED - Packet was not forwarded
 *   < 0                  - And error occurred (and packet not forwarded).
 *
 ****************************************************************************/

#ifdef CONFIG_NET_6LOWPAN
static int ipv6_packet_conversion(FAR struct net_driver_s *dev,
                                  FAR struct net_driver_s *fwddev,
                                  FAR struct ipv6_hdr_s *ipv6)
{
  int ret = PACKET_NOT_FORWARDED;

  if (dev->d_len > 0)
    {
      /* Check if this is a device served by 6LoWPAN */

      if (fwddev->d_lltype != NET_LL_IEEE802154 &&
          fwddev->d_lltype != NET_LL_PKTRADIO)
        {
          nwarn("WARNING:  Unsupported link layer... Not forwarded\n");
        }
      else
#ifdef CONFIG_NET_TCP
      if (ipv6->proto == IP_PROTO_TCP)
        {
          /* Decrement the TTL in the IPv6 header.  If it decrements to
           * zero, then drop the packet.
           */

          ret = ipv6_decr_ttl(ipv6);
          if (ret < 1)
            {
              nwarn("WARNING: Hop limit exceeded... Dropping!\n");
              ret = -EMULTIHOP;
            }
          else
            {
              /* Let 6LoWPAN convert IPv6 TCP output into IEEE802.15.4
               * frames.
               */

              sixlowpan_tcp_send(dev, fwddev, ipv6);

              /* The packet was forwarded */

              dev->d_len = 0;
              return PACKET_FORWARDED;
            }
        }
      else
#endif
#ifdef CONFIG_NET_UDP
      if (ipv6->proto == IP_PROTO_UDP)
        {
          /* Decrement the TTL in the IPv6 header.  If it decrements to
           * zero, then drop the packet.
           */

          ret = ipv6_decr_ttl(ipv6);
          if (ret < 1)
            {
              nwarn("WARNING: Hop limit exceeded... Dropping!\n");
              ret = -EMULTIHOP;
            }
          else
            {
              /* Let 6LoWPAN convert IPv6 UDP output into IEEE802.15.4
               * frames.
               */

              sixlowpan_udp_send(dev, fwddev, ipv6);

              /* The packet was forwarded */

              dev->d_len = 0;
              return PACKET_FORWARDED;
            }
        }
      else
#endif

#ifdef CONFIG_NET_ICMPv6
      if (ipv6->proto == IP_PROTO_ICMP6)
        {
          /* Decrement the TTL in the IPv6 header.  If it decrements to
           * zero, then drop the packet.
           */

          ret = ipv6_decr_ttl(ipv6);
          if (ret < 1)
            {
              nwarn("WARNING: Hop limit exceeded... Dropping!\n");
              ret = -EMULTIHOP;
            }
          else
            {
              /* Let 6LoWPAN convert IPv6 ICMPv6 output into IEEE802.15.4
               * frames.
               */

              sixlowpan_icmpv6_send(dev, fwddev, ipv6);

              /* The packet was forwarded */

              dev->d_len = 0;
              return PACKET_FORWARDED;
            }
        }
      else
#endif
        {
          /* Otherwise, we cannot forward the packet */

          nwarn("WARNING: Dropping. Unsupported 6LoWPAN protocol: %d\n",
                ipv6->proto);
        }
    }

  /* The packet was not forwarded (or the HOP limit was exceeded) */

  ipv6_dropstats(ipv6);
  return ret;
}
#else
#  define ipv6_packet_conversion(dev, fwddev, ipv6) (PACKET_NOT_FORWARDED)
#endif /* CONFIG_NET_6LOWPAN */

/****************************************************************************
 * Name: ipv6_dev_forward
 *
 * Description:
 *   This function is called from ipv6_forward when it is necessary to
 *   forward a packet from the current device to different device.  In this
 *   case, the forwarding operation must be performed asynchronously when
 *   the TX poll is received from the forwarding device.
 *
 * Input Parameters:
 *   dev      - The device on which the packet was received and which
 *              contains the IPv6 packet.
 *   fwdddev  - The device on which the packet must be forwarded.
 *   ipv6     - A pointer to the IPv6 header in within the IPv6 packet
 *
 * Returned Value:
 *   Zero is returned if the packet was successfully forwarded;  A negated
 *   errno value is returned if the packet is not forwardable.  In that
 *   latter case, the caller (ipv6_input()) should drop the packet.
 *
 ****************************************************************************/

static int ipv6_dev_forward(FAR struct net_driver_s *dev,
                            FAR struct net_driver_s *fwddev,
                            FAR struct ipv6_hdr_s *ipv6)
{
  FAR struct forward_s *fwd = NULL;
#ifdef CONFIG_DEBUG_NET_WARN
  int hdrsize;
#endif
  int ret;

#ifdef CONFIG_NET_IPFILTER
  /* Do filter before forwarding, to make sure we drop silently before
   * replying any other errors.
   */

  ret = ipv6_filter_fwd(dev, fwddev, ipv6);
  if (ret < 0)
    {
      ninfo("Drop/Reject FORWARD packet due to filter %d\n", ret);

      /* Let ipv6_forward reply the reject. */

      if (ret == IPFILTER_TARGET_REJECT)
        {
          ret = -ENETUNREACH;
        }

      goto errout;
    }
#endif

  /* If the interface isn't "up", we can't forward. */

  if ((fwddev->d_flags & IFF_UP) == 0)
    {
      nwarn("WARNING: device is DOWN\n");
      ret = -EHOSTUNREACH;
      goto errout;
    }

  /* Perform any necessary packet conversions. */

  ret = ipv6_packet_conversion(dev, fwddev, ipv6);
  if (ret < 0)
    {
      nwarn("WARNING: ipv6_packet_conversion failed: %d\n", ret);
      goto errout;
    }
  else if (ret == PACKET_NOT_FORWARDED)
    {
      /* Verify that the full packet will fit within the forwarding devices
       * MTU.  We provide no support for fragmenting forwarded packets.
       */

      if (NET_LL_HDRLEN(fwddev) + dev->d_len > NETDEV_PKTSIZE(fwddev))
        {
          nwarn("WARNING: Packet > MTU... Dropping\n");
          ret = -EFBIG;
          goto errout;
        }

      /* Get a pre-allocated forwarding structure,  This structure will be
       * completely zeroed when we receive it.
       */

      fwd = ipfwd_alloc();
      if (fwd == NULL)
        {
          nwarn("WARNING: Failed to allocate forwarding structure\n");
          ret = -ENOMEM;
          goto errout;
        }

      /* Initialize the easy stuff in the forwarding structure */

      fwd->f_dev    = fwddev;   /* Forwarding device */
#ifdef CONFIG_NET_IPv4
      fwd->f_domain = PF_INET6; /* IPv6 address domain */
#endif

#ifdef CONFIG_DEBUG_NET_WARN
      /* Get the size of the IPv6 + L3 header. */

      hdrsize = ipv6_hdrsize(ipv6);
      if (hdrsize < IPv6_HDRLEN)
        {
          nwarn("WARNING: Could not determine L2+L3 header size\n");
          ret = -EPROTONOSUPPORT;
          goto errout_with_fwd;
        }

      /* The L2/L3 headers must fit within one, contiguous IOB. */

      if (hdrsize > CONFIG_IOB_BUFSIZE)
        {
          nwarn("WARNING: Header is too big for pre-allocated structure\n");
          ret = -E2BIG;
          goto errout_with_fwd;
        }
#endif

      /* Relay the device buffer */

      fwd->f_iob = dev->d_iob;

      /* Decrement the TTL in the copy of the IPv6 header (retaining the
       * original TTL in the sourcee to handle the broadcast case).  If the
       * TTL decrements to zero, then do not forward the packet.
       */

      ret = ipv6_decr_ttl(ipv6);
      if (ret < 1)
        {
          nwarn("WARNING: Hop limit exceeded... Dropping!\n");
          ret = -EMULTIHOP;
          goto errout_with_fwd;
        }

#ifdef CONFIG_NET_NAT66
      /* Try NAT outbound, rule matching will be performed in NAT module. */

      ret = ipv6_nat_outbound(fwd->f_dev, ipv6, NAT_MANIP_SRC);
      if (ret < 0)
        {
          nwarn("WARNING: Performing NAT66 outbound failed, dropping!\n");
          goto errout_with_fwd;
        }
#endif

      /* Then set up to forward the packet according to the protocol. */

      ret = ipfwd_forward(fwd);
      if (ret >= 0)
        {
          netdev_iob_clear(dev);
          return OK;
        }
    }

errout_with_fwd:
  if (fwd != NULL)
    {
      ipfwd_free(fwd);
    }

errout:
  return ret;
}

/****************************************************************************
 * Name: ipv6_forward_callback
 *
 * Description:
 *   This function is a callback from netdev_foreach.  It implements the
 *   the broadcast forwarding action for each network device (other than, of
 *   course, the device that received the packet).
 *
 * Input Parameters:
 *   dev   - The device on which the packet was received and which contains
 *           the IPv6 packet.
 *   ipv6  - A convenience pointer to the IPv6 header in within the IPv6
 *           packet
 *
 * Returned Value:
 *   Typically returns zero (meaning to continue the enumeration), but will
 *   return a non-zero to stop the enumeration if an error occurs.
 *
 ****************************************************************************/

#ifdef CONFIG_NET_IPFORWARD_BROADCAST
static int ipv6_forward_callback(FAR struct net_driver_s *fwddev,
                                 FAR void *arg)
{
  FAR struct net_driver_s *dev = (FAR struct net_driver_s *)arg;
  FAR struct ipv6_hdr_s *ipv6;
  FAR struct iob_s *iob;
  int ret;

  DEBUGASSERT(fwddev != NULL);

  /* Only IFF_UP device and non-loopback device need forward packet */

  if (!IFF_IS_UP(fwddev->d_flags) || fwddev->d_lltype == NET_LL_LOOPBACK)
    {
      return OK;
    }

  DEBUGASSERT(dev != NULL && dev->d_buf != NULL);

  /* Check if we are forwarding on the same device that we received the
   * packet from.
   */

  if (fwddev != dev)
    {
      /* Backup the forward IP packet */

      iob = netdev_iob_clone(dev, true);
      if (iob == NULL)
        {
          nerr("ERROR: IOB clone failed when forwarding broadcast.\n");
          return -ENOMEM;
        }

      /* Recover the pointer to the IPv6 header in the receiving device's
       * d_buf.
       */

      ipv6 = IPv6BUF;

      /* Send the packet asynchrously on the forwarding device. */

      ret = ipv6_dev_forward(dev, fwddev, ipv6);
      if (ret < 0)
        {
          iob_free_chain(iob);
          nwarn("WARNING: ipv6_dev_forward failed: %d\n", ret);
          return ret;
        }

      /* Restore device iob with backup iob */

      netdev_iob_replace(dev, iob);
    }

  return OK;
}
#endif

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

/****************************************************************************
 * Name: ipv6_forward
 *
 * Description:
 *   This function is called from ipv6_input when a packet is received that
 *   is not destined for us.  In this case, the packet may need to be
 *   forwarded to another device (or sent back out the same device)
 *   depending configuration, routing table information, and the IPv6
 *   networks served by various network devices.
 *
 * Input Parameters:
 *   dev   - The device on which the packet was received and which contains
 *           the IPv6 packet.
 *   ipv6  - A convenience pointer to the IPv6 header in within the IPv6
 *           packet
 *
 *   On input:
 *   - dev->d_buf holds the received packet.
 *   - dev->d_len holds the length of the received packet MINUS the
 *     size of the L1 header.  That was subtracted out by ipv6_input.
 *   - ipv6 points to the IPv6 header with dev->d_buf.
 *
 * Returned Value:
 *   Zero is returned if the packet was successfully forward;  A negated
 *   errno value is returned if the packet is not forwardable.  In that
 *   latter case, the caller (ipv6_input()) should drop the packet.
 *
 ****************************************************************************/

int ipv6_forward(FAR struct net_driver_s *dev, FAR struct ipv6_hdr_s *ipv6)
{
  FAR struct net_driver_s *fwddev;
  int ret;
#ifdef CONFIG_NET_ICMPv6
  int icmpv6_reply_type;
  int icmpv6_reply_code;
  int icmpv6_reply_data;
#endif /* CONFIG_NET_ICMP */

  /* Search for a device that can forward this packet. */

  fwddev = netdev_findby_ripv6addr(ipv6->srcipaddr, ipv6->destipaddr);
  if (fwddev == NULL)
    {
      nwarn("WARNING: Not routable\n");
      ret = -ENETUNREACH;
      goto drop;
    }

  /* Check if we are forwarding on the same device that we received the
   * packet from.
   */

  if (fwddev != dev)
    {
      /* Send the packet asynchrously on the forwarding device. */

      ret = ipv6_dev_forward(dev, fwddev, ipv6);
      if (ret < 0)
        {
          nwarn("WARNING: ipv6_dev_forward failed: %d\n", ret);
          goto drop;
        }
    }
  else
#if defined(CONFIG_NET_6LOWPAN) /* REVISIT:  Currently only support for 6LoWPAN */
    {
      /* Single network device.  The use case here is where an endpoint acts
       * as a hub in a star configuration.  This is typical for a wireless
       * star configuration where not all endpoints are accessible from all
       * other endpoints, but seems less useful for a wired network.
       */

      /* Perform any necessary packet conversions.  If the packet was handled
       * via a backdoor path (or dropped), then dev->d_len will be zero.  If
       * the packet needs to be forwarded in the normal manner then
       * dev->d_len will be unchanged.
       */

      ret = ipv6_packet_conversion(dev, dev, ipv6);
      if (ret < 0)
        {
          nwarn("WARNING: ipv6_packet_conversion failed: %d\n", ret);
          goto drop;
        }
      else if (ret == PACKET_NOT_FORWARDED)
        {
#ifdef CONFIG_NET_ETHERNET
          /* REVISIT:
           *  For Ethernet we may have to fix up the Ethernet header:
           * - source MAC, the MAC of the current device.
           * - dest MAC, the MAC associated with the destination IPv6
           *   address.
           *   This  will involve ICMPv6 and Neighbor Discovery.
           */

          /* Correct dev->d_buf by adding back the L1 header length */

#endif

          /* Nothing other 6LoWPAN forwarding is currently handled and that
           * case was dealt with in ipv6_packet_conversion().
           *
           * REVISIT: Is this an issue?  Do other use cases make sense?
           */

          nwarn("WARNING: Packet forwarding supported only for 6LoWPAN\n");
          ret = -ENOSYS;
          goto drop;
        }
    }

#else /* CONFIG_NET_6LOWPAN */
    {
      nwarn(
         "WARNING: Packet forwarding not supported in this configuration\n");
      ret = -ENOSYS;
      goto drop;
    }
#endif /* CONFIG_NET_6LOWPAN */

  /* Return success.  ipv6_input will return to the network driver with
   * dev->d_len set to the packet size and the network driver will perform
   * the transfer.
   */

  return OK;

drop:
  ipv6_dropstats(ipv6);

#ifdef CONFIG_NET_ICMPv6
  /* Try reply ICMPv6 to the sender. */

  switch (ret)
    {
      case -ENETUNREACH:
        icmpv6_reply_type = ICMPv6_DEST_UNREACHABLE;
        icmpv6_reply_code = ICMPv6_ADDR_UNREACH;
        icmpv6_reply_data = 0;
        goto reply;

      case -EFBIG:
        icmpv6_reply_type = ICMPv6_PACKET_TOO_BIG;
        icmpv6_reply_code = 0;
        icmpv6_reply_data = NETDEV_PKTSIZE(fwddev) - NET_LL_HDRLEN(fwddev);
        goto reply;

      case -EMULTIHOP:
        icmpv6_reply_type = ICMPv6_PACKET_TIME_EXCEEDED;
        icmpv6_reply_code = ICMPV6_EXC_HOPLIMIT;
        icmpv6_reply_data = 0;
        goto reply;

      default:
        break; /* We don't know how to reply, just go on (to drop). */
    }
#endif

  dev->d_len = 0;
  return ret;

#ifdef CONFIG_NET_ICMPv6
reply:
#  ifdef CONFIG_NET_NAT66
  /* Before we reply ICMPv6, call NAT outbound to try to translate
   * destination address & port back to original status.
   */

  ipv6_nat_outbound(dev, ipv6, NAT_MANIP_DST);
#  endif /* CONFIG_NET_NAT66 */

  icmpv6_reply(dev, icmpv6_reply_type, icmpv6_reply_code, icmpv6_reply_data);
  return OK;
#endif /* CONFIG_NET_ICMP */
}

/****************************************************************************
 * Name: ipv6_forward_broadcast
 *
 * Description:
 *   This function is called from ipv6_input when a broadcast or multicast
 *   packet is received.  If CONFIG_NET_IPFORWARD_BROADCAST is enabled, this
 *   function will forward the broadcast packet to other networks through
 *   other network devices.
 *
 * Input Parameters:
 *   dev   - The device on which the packet was received and which contains
 *           the IPv6 packet.
 *   ipv6  - A convenience pointer to the IPv6 header in within the IPv6
 *           packet
 *
 *   On input:
 *   - dev->d_buf holds the received packet.
 *   - dev->d_len holds the length of the received packet MINUS the
 *     size of the L1 header.  That was subtracted out by ipv6_input.
 *   - ipv6 points to the IPv6 header with dev->d_buf.
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

#ifdef CONFIG_NET_IPFORWARD_BROADCAST
void ipv6_forward_broadcast(FAR struct net_driver_s *dev,
                            FAR struct ipv6_hdr_s *ipv6)
{
  /* Don't bother if the TTL would expire */

  if (ipv6->ttl > 1)
    {
      /* Forward the the broadcast/multicast packet to all devices except,
       * of course, the device that received the packet.
       */

      netdev_foreach(ipv6_forward_callback, dev);
    }
}
#endif

#endif /* CONFIG_NET_IPFORWARD && CONFIG_NET_IPv6 */