/****************************************************************************
 * net/nat/ipv6_nat_entry.c
 *
 * 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 <debug.h>
#include <stdint.h>

#include <nuttx/clock.h>
#include <nuttx/hashtable.h>
#include <nuttx/kmalloc.h>
#include <nuttx/nuttx.h>

#include "inet/inet.h"
#include "nat/nat.h"
#include "netlink/netlink.h"

#ifdef CONFIG_NET_NAT66

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

static DECLARE_HASHTABLE(g_nat66_inbound, CONFIG_NET_NAT_HASH_BITS);
static DECLARE_HASHTABLE(g_nat66_outbound, CONFIG_NET_NAT_HASH_BITS);

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

/****************************************************************************
 * Name: ipv6_nat_hash_key
 *
 * Description:
 *   Create a hash key for NAT66.
 *
 ****************************************************************************/

static inline uint32_t ipv6_nat_hash_key(const net_ipv6addr_t ip,
                                         uint16_t port, uint8_t protocol)
{
  uint32_t key = (((uint32_t)ip[0] << 16) | ip[1]) ^
                 (((uint32_t)ip[2] << 16) | ip[3]) ^
                 (((uint32_t)ip[4] << 16) | ip[5]) ^
                 (((uint32_t)ip[6] << 16) | ip[7]);

  return key ^ ((uint32_t)protocol << 16) ^ port;
}

/****************************************************************************
 * Name: ipv6_nat_entry_refresh
 *
 * Description:
 *   Refresh a NAT entry, update its expiration time.
 *
 * Input Parameters:
 *   entry      - The entry to refresh.
 *
 ****************************************************************************/

static void ipv6_nat_entry_refresh(FAR ipv6_nat_entry_t *entry)
{
  entry->expire_time = nat_expire_time(entry->protocol);
}

/****************************************************************************
 * Name: ipv6_nat_entry_create
 *
 * Description:
 *   Create a NAT entry and insert into entry list.
 *
 * Input Parameters:
 *   protocol      - The L4 protocol of the packet.
 *   external_ip   - The external ip of the packet.
 *   external_port - The external port of the packet.
 *   local_ip      - The local ip of the packet.
 *   local_port    - The local port of the packet.
 *   peer_ip       - The peer ip of the packet.
 *   peer_port     - The peer port of the packet.
 *
 * Returned Value:
 *   Pointer to entry on success; null on failure
 *
 ****************************************************************************/

static FAR ipv6_nat_entry_t *
ipv6_nat_entry_create(uint8_t protocol, const net_ipv6addr_t external_ip,
                      uint16_t external_port, const net_ipv6addr_t local_ip,
                      uint16_t local_port, const net_ipv6addr_t peer_ip,
                      uint16_t peer_port)
{
  FAR ipv6_nat_entry_t *entry = kmm_malloc(sizeof(ipv6_nat_entry_t));
  if (entry == NULL)
    {
      nwarn("WARNING: Failed to allocate IPv6 NAT entry\n");
      return NULL;
    }

  entry->protocol      = protocol;
  entry->external_port = external_port;
  entry->local_port    = local_port;
#ifdef CONFIG_NET_NAT66_SYMMETRIC
  entry->peer_port     = peer_port;
#endif
  net_ipv6addr_copy(entry->external_ip, external_ip);
  net_ipv6addr_copy(entry->local_ip, local_ip);
#ifdef CONFIG_NET_NAT66_SYMMETRIC
  net_ipv6addr_copy(entry->peer_ip, peer_ip);
#endif

  ipv6_nat_entry_refresh(entry);

  hashtable_add(g_nat66_inbound, &entry->hash_inbound,
                ipv6_nat_hash_key(external_ip, external_port, protocol));
  hashtable_add(g_nat66_outbound, &entry->hash_outbound,
                ipv6_nat_hash_key(local_ip, local_port, protocol));

#ifdef CONFIG_NETLINK_NETFILTER
  netlink_conntrack_notify(IPCTNL_MSG_CT_NEW, PF_INET6, entry);
#endif

  return entry;
}

/****************************************************************************
 * Name: ipv6_nat_entry_delete
 *
 * Description:
 *   Delete a NAT entry and remove from entry list.
 *
 * Input Parameters:
 *   entry      - The entry to remove.
 *
 ****************************************************************************/

static void ipv6_nat_entry_delete(FAR ipv6_nat_entry_t *entry)
{
  ninfo("INFO: Removing NAT66 entry proto=%" PRIu8
        ", local=%04x:%04x:%04x:%04x:%04x:%04x:%04x:%04x:%" PRIu16
        ", external=:%" PRIu16 "\n",
        entry->protocol, entry->local_ip[0], entry->local_ip[1],
        entry->local_ip[2], entry->local_ip[3], entry->local_ip[4],
        entry->local_ip[5], entry->local_ip[6], entry->local_ip[7],
        entry->local_port, entry->external_port);

  hashtable_delete(g_nat66_inbound, &entry->hash_inbound,
                   ipv6_nat_hash_key(entry->external_ip,
                                     entry->external_port,
                                     entry->protocol));
  hashtable_delete(g_nat66_outbound, &entry->hash_outbound,
                   ipv6_nat_hash_key(entry->local_ip,
                                     entry->local_port,
                                     entry->protocol));

#ifdef CONFIG_NETLINK_NETFILTER
  netlink_conntrack_notify(IPCTNL_MSG_CT_DELETE, PF_INET6, entry);
#endif

  kmm_free(entry);
}

/****************************************************************************
 * Name: ipv6_nat_reclaim_entry
 *
 * Description:
 *   Try reclaim all expired NAT entries.
 *   Only works after every CONFIG_NET_NAT_ENTRY_RECLAIM_SEC (low frequency).
 *
 *   Although expired entries will be automatically reclaimed when matching
 *   inbound/outbound entries, there might be some situations that entries
 *   will be kept in memory, e.g. big hashtable with only a few connections.
 *
 * Assumptions:
 *   NAT is initialized.
 *
 ****************************************************************************/

#if CONFIG_NET_NAT_ENTRY_RECLAIM_SEC > 0
static void ipv6_nat_reclaim_entry_cb(FAR ipv6_nat_entry_t *entry,
                                      FAR void *arg)
{
  int32_t current_time = *(FAR int32_t *)arg;

  if (entry->expire_time - current_time <= 0)
    {
      ipv6_nat_entry_delete(entry);
    }
}

static void ipv6_nat_reclaim_entry(int32_t current_time)
{
  static int32_t next_reclaim_time = CONFIG_NET_NAT_ENTRY_RECLAIM_SEC;

  if (next_reclaim_time - current_time <= 0)
    {
      ninfo("INFO: Reclaiming all expired NAT66 entries.\n");
      ipv6_nat_entry_foreach(ipv6_nat_reclaim_entry_cb, &current_time);
      next_reclaim_time = current_time + CONFIG_NET_NAT_ENTRY_RECLAIM_SEC;
    }
}
#else
#  define ipv6_nat_reclaim_entry(t)
#endif

/****************************************************************************
 * Name: ipv6_nat_entry_clear_cb
 *
 * Description:
 *   Clear an entry related to dev. Called when NAT will be disabled on
 *   any device.
 *
 ****************************************************************************/

static void ipv6_nat_entry_clear_cb(FAR ipv6_nat_entry_t *entry,
                                    FAR void *arg)
{
  FAR struct net_driver_s *dev = arg;

  if (NETDEV_IS_MY_V6ADDR(dev, entry->external_ip))
    {
      ipv6_nat_entry_delete(entry);
    }
}

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

/****************************************************************************
 * Name: ipv6_nat_entry_clear
 *
 * Description:
 *   Clear all entries related to dev. Called when NAT will be disabled on
 *   any device.
 *
 * Input Parameters:
 *   dev - The device on which NAT entries will be cleared.
 *
 * Assumptions:
 *   NAT is initialized.
 *
 ****************************************************************************/

void ipv6_nat_entry_clear(FAR struct net_driver_s *dev)
{
  ninfo("INFO: Clearing all NAT66 entries for %s\n", dev->d_ifname);
  ipv6_nat_entry_foreach(ipv6_nat_entry_clear_cb, dev);
}

/****************************************************************************
 * Name: ipv6_nat_entry_foreach
 *
 * Description:
 *   Call the callback function for each NAT entry.
 *
 * Input Parameters:
 *   cb  - The callback function.
 *   arg - The argument to pass to the callback function.
 *
 ****************************************************************************/

void ipv6_nat_entry_foreach(ipv6_nat_entry_cb_t cb, FAR void *arg)
{
  FAR hash_node_t *p;
  FAR hash_node_t *tmp;
  int i;

  hashtable_for_every_safe(g_nat66_inbound, p, tmp, i)
    {
      FAR ipv6_nat_entry_t *entry =
        container_of(p, ipv6_nat_entry_t, hash_inbound);

      cb(entry, arg);
    }
}

/****************************************************************************
 * Name: ipv6_nat_inbound_entry_find
 *
 * Description:
 *   Find the inbound entry in NAT entry list.
 *
 * Input Parameters:
 *   protocol      - The L4 protocol of the packet.
 *   external_ip   - The external ip of the packet, supports INADDR_ANY.
 *   external_port - The external port of the packet.
 *   peer_ip       - The peer ip of the packet.
 *   peer_port     - The peer port of the packet.
 *   refresh       - Whether to refresh the selected entry.
 *
 * Returned Value:
 *   Pointer to entry on success; null on failure
 *
 ****************************************************************************/

FAR ipv6_nat_entry_t *
ipv6_nat_inbound_entry_find(uint8_t protocol,
                            const net_ipv6addr_t external_ip,
                            uint16_t external_port,
                            const net_ipv6addr_t peer_ip,
                            uint16_t peer_port, bool refresh)
{
  FAR hash_node_t *p;
  FAR hash_node_t *tmp;
  bool skip_ip = net_ipv6addr_cmp(external_ip, g_ipv6_unspecaddr);
#ifdef CONFIG_NET_NAT66_SYMMETRIC
  bool skip_peer = net_ipv6addr_cmp(peer_ip, g_ipv6_unspecaddr);
#endif
  int32_t current_time = TICK2SEC(clock_systime_ticks());

  ipv6_nat_reclaim_entry(current_time);

  hashtable_for_every_possible_safe(g_nat66_inbound, p, tmp,
                    ipv6_nat_hash_key(external_ip, external_port, protocol))
    {
      FAR ipv6_nat_entry_t *entry =
        container_of(p, ipv6_nat_entry_t, hash_inbound);

      /* Remove expired entries. */

      if (entry->expire_time - current_time <= 0)
        {
          ipv6_nat_entry_delete(entry);
          continue;
        }

      if (entry->protocol == protocol &&
          (skip_ip || net_ipv6addr_cmp(entry->external_ip, external_ip)) &&
          entry->external_port == external_port
#ifdef CONFIG_NET_NAT66_SYMMETRIC
          && (skip_peer || (net_ipv6addr_cmp(entry->peer_ip, peer_ip) &&
                            entry->peer_port == peer_port))
#endif
          )
        {
          if (refresh)
            {
              ipv6_nat_entry_refresh(entry);
            }

          return entry;
        }
    }

  if (refresh) /* false = a test of whether entry exists, no need to warn */
    {
      nwarn("WARNING: Failed to find IPv6 inbound NAT entry for proto="
            "%" PRIu8 ",external=[%04x:%04x:%04x:%04x:%04x:%04x:%04x:%04x]:"
            "%" PRIu16 "\n",
            protocol, external_ip[0], external_ip[1], external_ip[2],
            external_ip[3], external_ip[4], external_ip[5], external_ip[6],
            external_ip[7], external_port);
    }

  return NULL;
}

/****************************************************************************
 * Name: ipv6_nat_outbound_entry_find
 *
 * Description:
 *   Find the outbound entry in NAT entry list. Create one if corresponding
 *   entry does not exist.
 *
 * Input Parameters:
 *   dev        - The device on which the packet will be sent.
 *   protocol   - The L4 protocol of the packet.
 *   local_ip   - The local ip of the packet.
 *   local_port - The local port of the packet.
 *   peer_ip    - The peer ip of the packet.
 *   peer_port  - The peer port of the packet.
 *   try_create - Try create the entry if no entry found.
 *
 * Returned Value:
 *   Pointer to entry on success; null on failure
 *
 ****************************************************************************/

FAR ipv6_nat_entry_t *
ipv6_nat_outbound_entry_find(FAR struct net_driver_s *dev, uint8_t protocol,
                             const net_ipv6addr_t local_ip,
                             uint16_t local_port,
                             const net_ipv6addr_t peer_ip,
                             uint16_t peer_port, bool try_create)
{
  FAR hash_node_t *p;
  FAR hash_node_t *tmp;
  FAR union ip_addr_u *external_ip;
  uint16_t external_port;
  int32_t current_time = TICK2SEC(clock_systime_ticks());

  ipv6_nat_reclaim_entry(current_time);

  hashtable_for_every_possible_safe(g_nat66_outbound, p, tmp,
                          ipv6_nat_hash_key(local_ip, local_port, protocol))
    {
      FAR ipv6_nat_entry_t *entry =
        container_of(p, ipv6_nat_entry_t, hash_outbound);

      /* Remove expired entries. */

      if (entry->expire_time - current_time <= 0)
        {
          ipv6_nat_entry_delete(entry);
          continue;
        }

      if (entry->protocol == protocol &&
          NETDEV_IS_MY_V6ADDR(dev, entry->external_ip) &&
          net_ipv6addr_cmp(entry->local_ip, local_ip) &&
          entry->local_port == local_port
#ifdef CONFIG_NET_NAT66_SYMMETRIC
          && net_ipv6addr_cmp(entry->peer_ip, peer_ip) &&
          entry->peer_port == peer_port
#endif
          )
        {
          ipv6_nat_entry_refresh(entry);
          return entry;
        }
    }

  if (!try_create)
    {
      return NULL;
    }

  /* Failed to find the entry, create one. */

  ninfo("INFO: Failed to find IPv6 outbound NAT entry for proto=%" PRIu8
        ", local=[%04x:%04x:%04x:%04x:%04x:%04x:%04x:%04x]:%" PRIu16
        ", try create one.\n",
        protocol, local_ip[0], local_ip[1], local_ip[2], local_ip[3],
        local_ip[4], local_ip[5], local_ip[6], local_ip[7], local_port);

  external_ip = (FAR union ip_addr_u *)netdev_ipv6_srcaddr(dev, peer_ip);
  external_port = nat_port_select(dev, PF_INET6, protocol,
                                  external_ip, local_port);

  if (!external_port)
    {
      nwarn("WARNING: Failed to find an available port!\n");
      return NULL;
    }

  return ipv6_nat_entry_create(protocol, external_ip->ipv6, external_port,
                               local_ip, local_port, peer_ip, peer_port);
}

#endif /* CONFIG_NET_NAT66 */
