/** @file
  The functions and routines to handle the route caches and route table.

  Copyright (c) 2009 - 2016, Intel Corporation. All rights reserved.<BR>

  SPDX-License-Identifier: BSD-2-Clause-Patent

**/

#include "Ip6Impl.h"

/**
  This is the worker function for IP6_ROUTE_CACHE_HASH(). It calculates the value
  as the index of the route cache bucket according to the prefix of two IPv6 addresses.

  @param[in]  Ip1     The IPv6 address.
  @param[in]  Ip2     The IPv6 address.

  @return The hash value of the prefix of two IPv6 addresses.

**/
UINT32
Ip6RouteCacheHash (
  IN EFI_IPv6_ADDRESS  *Ip1,
  IN EFI_IPv6_ADDRESS  *Ip2
  )
{
  UINT32  Prefix1;
  UINT32  Prefix2;

  Prefix1 = *((UINT32 *)((UINTN *)(Ip1)));
  Prefix2 = *((UINT32 *)((UINTN *)(Ip2)));

  return ((UINT32)(Prefix1 ^ Prefix2) % IP6_ROUTE_CACHE_HASH_SIZE);
}

/**
  Allocate a route entry then initialize it with the Destination/PrefixLength
  and Gateway.

  @param[in]  Destination     The IPv6 destination address. This is an optional
                              parameter that may be NULL.
  @param[in]  PrefixLength    The destination network's prefix length.
  @param[in]  GatewayAddress  The next hop address. This is an optional parameter
                              that may be NULL.

  @return NULL if failed to allocate memory; otherwise, the newly created route entry.

**/
IP6_ROUTE_ENTRY *
Ip6CreateRouteEntry (
  IN EFI_IPv6_ADDRESS  *Destination    OPTIONAL,
  IN UINT8             PrefixLength,
  IN EFI_IPv6_ADDRESS  *GatewayAddress OPTIONAL
  )
{
  IP6_ROUTE_ENTRY  *RtEntry;

  RtEntry = AllocateZeroPool (sizeof (IP6_ROUTE_ENTRY));

  if (RtEntry == NULL) {
    return NULL;
  }

  RtEntry->RefCnt       = 1;
  RtEntry->Flag         = 0;
  RtEntry->PrefixLength = PrefixLength;

  if (Destination != NULL) {
    IP6_COPY_ADDRESS (&RtEntry->Destination, Destination);
  }

  if (GatewayAddress != NULL) {
    IP6_COPY_ADDRESS (&RtEntry->NextHop, GatewayAddress);
  }

  return RtEntry;
}

/**
  Free the route table entry. It is reference counted.

  @param[in, out]  RtEntry  The route entry to free.

**/
VOID
Ip6FreeRouteEntry (
  IN OUT IP6_ROUTE_ENTRY  *RtEntry
  )
{
  ASSERT ((RtEntry != NULL) && (RtEntry->RefCnt > 0));

  if (--RtEntry->RefCnt == 0) {
    FreePool (RtEntry);
  }
}

/**
  Search the route table for a most specific match to the Dst. It searches
  from the longest route area (prefix length == 128) to the shortest route area
  (default routes). In each route area, it will first search the instance's
  route table, then the default route table. This is required per the following
  requirements:
  1. IP search the route table for a most specific match.
  2. The local route entries have precedence over the default route entry.

  @param[in]  RtTable       The route table to search from.
  @param[in]  Destination   The destination address to search. If NULL, search
                            the route table by NextHop.
  @param[in]  NextHop       The next hop address. If NULL, search the route table
                            by Destination.

  @return NULL if no route matches the Dst. Otherwise, the point to the
  @return most specific route to the Dst.

**/
IP6_ROUTE_ENTRY *
Ip6FindRouteEntry (
  IN IP6_ROUTE_TABLE   *RtTable,
  IN EFI_IPv6_ADDRESS  *Destination OPTIONAL,
  IN EFI_IPv6_ADDRESS  *NextHop     OPTIONAL
  )
{
  LIST_ENTRY       *Entry;
  IP6_ROUTE_ENTRY  *RtEntry;
  INTN             Index;

  ASSERT (Destination != NULL || NextHop != NULL);

  RtEntry = NULL;

  for (Index = IP6_PREFIX_MAX; Index >= 0; Index--) {
    NET_LIST_FOR_EACH (Entry, &RtTable->RouteArea[Index]) {
      RtEntry = NET_LIST_USER_STRUCT (Entry, IP6_ROUTE_ENTRY, Link);

      if (Destination != NULL) {
        if (NetIp6IsNetEqual (Destination, &RtEntry->Destination, RtEntry->PrefixLength)) {
          NET_GET_REF (RtEntry);
          return RtEntry;
        }
      } else if (NextHop != NULL) {
        if (NetIp6IsNetEqual (NextHop, &RtEntry->NextHop, RtEntry->PrefixLength)) {
          NET_GET_REF (RtEntry);
          return RtEntry;
        }
      }
    }
  }

  return NULL;
}

/**
  Allocate and initialize a IP6 route cache entry.

  @param[in]  Dst           The destination address.
  @param[in]  Src           The source address.
  @param[in]  GateWay       The next hop address.
  @param[in]  Tag           The tag from the caller. This marks all the cache entries
                            spawned from one route table entry.

  @return NULL if failed to allocate memory for the cache. Otherwise, point
          to the created route cache entry.

**/
IP6_ROUTE_CACHE_ENTRY *
Ip6CreateRouteCacheEntry (
  IN EFI_IPv6_ADDRESS  *Dst,
  IN EFI_IPv6_ADDRESS  *Src,
  IN EFI_IPv6_ADDRESS  *GateWay,
  IN UINTN             Tag
  )
{
  IP6_ROUTE_CACHE_ENTRY  *RtCacheEntry;

  RtCacheEntry = AllocatePool (sizeof (IP6_ROUTE_CACHE_ENTRY));

  if (RtCacheEntry == NULL) {
    return NULL;
  }

  RtCacheEntry->RefCnt = 1;
  RtCacheEntry->Tag    = Tag;

  IP6_COPY_ADDRESS (&RtCacheEntry->Destination, Dst);
  IP6_COPY_ADDRESS (&RtCacheEntry->Source, Src);
  IP6_COPY_ADDRESS (&RtCacheEntry->NextHop, GateWay);

  return RtCacheEntry;
}

/**
  Free the route cache entry. It is reference counted.

  @param[in, out]  RtCacheEntry  The route cache entry to free.

**/
VOID
Ip6FreeRouteCacheEntry (
  IN OUT IP6_ROUTE_CACHE_ENTRY  *RtCacheEntry
  )
{
  ASSERT (RtCacheEntry->RefCnt > 0);

  if (--RtCacheEntry->RefCnt == 0) {
    FreePool (RtCacheEntry);
  }
}

/**
  Find a route cache with the destination and source address. This is
  used by the ICMPv6 redirect message process.

  @param[in]  RtTable       The route table to search the cache for.
  @param[in]  Dest          The destination address.
  @param[in]  Src           The source address.

  @return NULL if no route entry to the (Dest, Src). Otherwise, the pointer
          to the correct route cache entry.

**/
IP6_ROUTE_CACHE_ENTRY *
Ip6FindRouteCache (
  IN IP6_ROUTE_TABLE   *RtTable,
  IN EFI_IPv6_ADDRESS  *Dest,
  IN EFI_IPv6_ADDRESS  *Src
  )
{
  LIST_ENTRY             *Entry;
  IP6_ROUTE_CACHE_ENTRY  *RtCacheEntry;
  UINT32                 Index;

  Index = IP6_ROUTE_CACHE_HASH (Dest, Src);

  NET_LIST_FOR_EACH (Entry, &RtTable->Cache.CacheBucket[Index]) {
    RtCacheEntry = NET_LIST_USER_STRUCT (Entry, IP6_ROUTE_CACHE_ENTRY, Link);

    if (EFI_IP6_EQUAL (Dest, &RtCacheEntry->Destination) && EFI_IP6_EQUAL (Src, &RtCacheEntry->Source)) {
      NET_GET_REF (RtCacheEntry);
      return RtCacheEntry;
    }
  }

  return NULL;
}

/**
  Build an array of EFI_IP6_ROUTE_TABLE to be returned to the caller. The number
  of EFI_IP6_ROUTE_TABLE is also returned.

  @param[in]  RouteTable        The pointer of IP6_ROUTE_TABLE internal used.
  @param[out] EfiRouteCount     The number of returned route entries.
  @param[out] EfiRouteTable     The pointer to the array of EFI_IP6_ROUTE_TABLE.
                                If NULL, only the route entry count is returned.

  @retval EFI_SUCCESS           The EFI_IP6_ROUTE_TABLE successfully built.
  @retval EFI_OUT_OF_RESOURCES  Failed to allocate the memory for the route table.

**/
EFI_STATUS
Ip6BuildEfiRouteTable (
  IN IP6_ROUTE_TABLE       *RouteTable,
  OUT UINT32               *EfiRouteCount,
  OUT EFI_IP6_ROUTE_TABLE  **EfiRouteTable OPTIONAL
  )
{
  LIST_ENTRY           *Entry;
  IP6_ROUTE_ENTRY      *RtEntry;
  EFI_IP6_ROUTE_TABLE  *EfiTable;
  UINT32               Count;
  INT32                Index;

  ASSERT (EfiRouteCount != NULL);

  Count          = RouteTable->TotalNum;
  *EfiRouteCount = Count;

  if ((EfiRouteTable == NULL) || (Count == 0)) {
    return EFI_SUCCESS;
  }

  if (*EfiRouteTable == NULL) {
    *EfiRouteTable = AllocatePool (sizeof (EFI_IP6_ROUTE_TABLE) * Count);
    if (*EfiRouteTable == NULL) {
      return EFI_OUT_OF_RESOURCES;
    }
  }

  EfiTable = *EfiRouteTable;

  //
  // Copy the route entry to EFI route table.
  //
  Count = 0;

  for (Index = IP6_PREFIX_MAX; Index >= 0; Index--) {
    NET_LIST_FOR_EACH (Entry, &(RouteTable->RouteArea[Index])) {
      RtEntry = NET_LIST_USER_STRUCT (Entry, IP6_ROUTE_ENTRY, Link);

      Ip6CopyAddressByPrefix (
        &EfiTable[Count].Destination,
        &RtEntry->Destination,
        RtEntry->PrefixLength
        );

      IP6_COPY_ADDRESS (&EfiTable[Count].Gateway, &RtEntry->NextHop);
      EfiTable[Count].PrefixLength = RtEntry->PrefixLength;

      Count++;
    }
  }

  ASSERT (Count == RouteTable->TotalNum);

  return EFI_SUCCESS;
}

/**
  Create an empty route table. This includes its internal route cache.

  @return NULL if failed to allocate memory for the route table. Otherwise,
          the point to newly created route table.

**/
IP6_ROUTE_TABLE *
Ip6CreateRouteTable (
  VOID
  )
{
  IP6_ROUTE_TABLE  *RtTable;
  UINT32           Index;

  RtTable = AllocatePool (sizeof (IP6_ROUTE_TABLE));
  if (RtTable == NULL) {
    return NULL;
  }

  RtTable->RefCnt   = 1;
  RtTable->TotalNum = 0;

  for (Index = 0; Index <= IP6_PREFIX_MAX; Index++) {
    InitializeListHead (&RtTable->RouteArea[Index]);
  }

  for (Index = 0; Index < IP6_ROUTE_CACHE_HASH_SIZE; Index++) {
    InitializeListHead (&RtTable->Cache.CacheBucket[Index]);
    RtTable->Cache.CacheNum[Index] = 0;
  }

  return RtTable;
}

/**
  Free the route table and its associated route cache. Route
  table is reference counted.

  @param[in, out]  RtTable      The route table to free.

**/
VOID
Ip6CleanRouteTable (
  IN OUT IP6_ROUTE_TABLE  *RtTable
  )
{
  LIST_ENTRY             *Entry;
  LIST_ENTRY             *Next;
  IP6_ROUTE_ENTRY        *RtEntry;
  IP6_ROUTE_CACHE_ENTRY  *RtCacheEntry;
  UINT32                 Index;

  ASSERT (RtTable->RefCnt > 0);

  if (--RtTable->RefCnt > 0) {
    return;
  }

  //
  // Free all the route table entry and its route cache.
  //
  for (Index = 0; Index <= IP6_PREFIX_MAX; Index++) {
    NET_LIST_FOR_EACH_SAFE (Entry, Next, &RtTable->RouteArea[Index]) {
      RtEntry = NET_LIST_USER_STRUCT (Entry, IP6_ROUTE_ENTRY, Link);
      RemoveEntryList (Entry);
      Ip6FreeRouteEntry (RtEntry);
    }
  }

  for (Index = 0; Index < IP6_ROUTE_CACHE_HASH_SIZE; Index++) {
    NET_LIST_FOR_EACH_SAFE (Entry, Next, &RtTable->Cache.CacheBucket[Index]) {
      RtCacheEntry = NET_LIST_USER_STRUCT (Entry, IP6_ROUTE_CACHE_ENTRY, Link);
      RemoveEntryList (Entry);
      Ip6FreeRouteCacheEntry (RtCacheEntry);
    }
  }

  FreePool (RtTable);
}

/**
  Remove all the cache entries bearing the Tag. When a route cache
  entry is created, it is tagged with the address of route entry
  from which it is spawned. When a route entry is deleted, the cache
  entries spawned from it are also deleted.

  @param[in]  RtCache       Route cache to remove the entries from.
  @param[in]  Tag           The Tag of the entries to remove.

**/
VOID
Ip6PurgeRouteCache (
  IN IP6_ROUTE_CACHE  *RtCache,
  IN UINTN            Tag
  )
{
  LIST_ENTRY             *Entry;
  LIST_ENTRY             *Next;
  IP6_ROUTE_CACHE_ENTRY  *RtCacheEntry;
  UINT32                 Index;

  for (Index = 0; Index < IP6_ROUTE_CACHE_HASH_SIZE; Index++) {
    NET_LIST_FOR_EACH_SAFE (Entry, Next, &RtCache->CacheBucket[Index]) {
      RtCacheEntry = NET_LIST_USER_STRUCT (Entry, IP6_ROUTE_CACHE_ENTRY, Link);

      if (RtCacheEntry->Tag == Tag) {
        RemoveEntryList (Entry);
        Ip6FreeRouteCacheEntry (RtCacheEntry);
      }
    }
  }
}

/**
  Add a route entry to the route table. It is the help function for EfiIp6Routes.

  @param[in, out]  RtTable        Route table to add route to.
  @param[in]       Destination    The destination of the network.
  @param[in]       PrefixLength   The PrefixLength of the destination.
  @param[in]       GatewayAddress The next hop address.

  @retval EFI_ACCESS_DENIED     The same route already exists.
  @retval EFI_OUT_OF_RESOURCES  Failed to allocate memory for the entry.
  @retval EFI_SUCCESS           The route was added successfully.

**/
EFI_STATUS
Ip6AddRoute (
  IN OUT IP6_ROUTE_TABLE  *RtTable,
  IN EFI_IPv6_ADDRESS     *Destination,
  IN UINT8                PrefixLength,
  IN EFI_IPv6_ADDRESS     *GatewayAddress
  )
{
  LIST_ENTRY       *ListHead;
  LIST_ENTRY       *Entry;
  IP6_ROUTE_ENTRY  *Route;

  ListHead = &RtTable->RouteArea[PrefixLength];

  //
  // First check whether the route exists
  //
  NET_LIST_FOR_EACH (Entry, ListHead) {
    Route = NET_LIST_USER_STRUCT (Entry, IP6_ROUTE_ENTRY, Link);

    if (NetIp6IsNetEqual (Destination, &Route->Destination, PrefixLength) &&
        EFI_IP6_EQUAL (GatewayAddress, &Route->NextHop))
    {
      return EFI_ACCESS_DENIED;
    }
  }

  //
  // Create a route entry and insert it to the route area.
  //
  Route = Ip6CreateRouteEntry (Destination, PrefixLength, GatewayAddress);

  if (Route == NULL) {
    return EFI_OUT_OF_RESOURCES;
  }

  if (NetIp6IsUnspecifiedAddr (GatewayAddress)) {
    Route->Flag = IP6_DIRECT_ROUTE;
  }

  InsertHeadList (ListHead, &Route->Link);
  RtTable->TotalNum++;

  return EFI_SUCCESS;
}

/**
  Remove a route entry and all the route caches spawn from it.
  It is the help function for EfiIp6Routes.

  @param[in, out] RtTable           The route table to remove the route from.
  @param[in]      Destination       The destination network.
  @param[in]      PrefixLength      The PrefixLength of the Destination.
  @param[in]      GatewayAddress    The next hop address.

  @retval EFI_SUCCESS           The route entry was successfully removed.
  @retval EFI_NOT_FOUND         There is no route entry in the table with that
                                property.

**/
EFI_STATUS
Ip6DelRoute (
  IN OUT IP6_ROUTE_TABLE  *RtTable,
  IN EFI_IPv6_ADDRESS     *Destination,
  IN UINT8                PrefixLength,
  IN EFI_IPv6_ADDRESS     *GatewayAddress
  )
{
  LIST_ENTRY       *ListHead;
  LIST_ENTRY       *Entry;
  LIST_ENTRY       *Next;
  IP6_ROUTE_ENTRY  *Route;
  UINT32           TotalNum;

  ListHead = &RtTable->RouteArea[PrefixLength];
  TotalNum = RtTable->TotalNum;

  NET_LIST_FOR_EACH_SAFE (Entry, Next, ListHead) {
    Route = NET_LIST_USER_STRUCT (Entry, IP6_ROUTE_ENTRY, Link);

    if ((Destination != NULL) && !NetIp6IsNetEqual (Destination, &Route->Destination, PrefixLength)) {
      continue;
    }

    if ((GatewayAddress != NULL) && !EFI_IP6_EQUAL (GatewayAddress, &Route->NextHop)) {
      continue;
    }

    Ip6PurgeRouteCache (&RtTable->Cache, (UINTN)Route);
    RemoveEntryList (Entry);
    Ip6FreeRouteEntry (Route);

    ASSERT (RtTable->TotalNum > 0);
    RtTable->TotalNum--;
  }

  return TotalNum == RtTable->TotalNum ? EFI_NOT_FOUND : EFI_SUCCESS;
}

/**
  Search the route table to route the packet. Return/create a route
  cache if there is a route to the destination.

  @param[in]  IpSb          The IP6 service data.
  @param[in]  Dest          The destination address to search for.
  @param[in]  Src           The source address to search for.

  @return NULL if it failed to route the packet. Otherwise, a route cache
          entry that can be used to route packets.

**/
IP6_ROUTE_CACHE_ENTRY *
Ip6Route (
  IN IP6_SERVICE       *IpSb,
  IN EFI_IPv6_ADDRESS  *Dest,
  IN EFI_IPv6_ADDRESS  *Src
  )
{
  IP6_ROUTE_TABLE        *RtTable;
  LIST_ENTRY             *ListHead;
  IP6_ROUTE_CACHE_ENTRY  *RtCacheEntry;
  IP6_ROUTE_ENTRY        *RtEntry;
  EFI_IPv6_ADDRESS       NextHop;
  UINT32                 Index;

  RtTable = IpSb->RouteTable;

  ASSERT (RtTable != NULL);

  //
  // Search the destination cache in IP6_ROUTE_TABLE.
  //
  Index    = IP6_ROUTE_CACHE_HASH (Dest, Src);
  ListHead = &RtTable->Cache.CacheBucket[Index];

  RtCacheEntry = Ip6FindRouteCache (RtTable, Dest, Src);

  //
  // If found, promote the cache entry to the head of the hash bucket.
  //
  if (RtCacheEntry != NULL) {
    RemoveEntryList (&RtCacheEntry->Link);
    InsertHeadList (ListHead, &RtCacheEntry->Link);
    return RtCacheEntry;
  }

  //
  // Search the route table for the most specific route
  //
  RtEntry = Ip6FindRouteEntry (RtTable, Dest, NULL);
  if (RtEntry == NULL) {
    return NULL;
  }

  //
  // Found a route to the Dest, if it is a direct route, the packet
  // will be send directly to the destination, such as for connected
  // network. Otherwise, it is an indirect route, the packet will be
  // send the next hop router.
  //
  if ((RtEntry->Flag & IP6_DIRECT_ROUTE) == IP6_DIRECT_ROUTE) {
    IP6_COPY_ADDRESS (&NextHop, Dest);
  } else {
    IP6_COPY_ADDRESS (&NextHop, &RtEntry->NextHop);
  }

  Ip6FreeRouteEntry (RtEntry);

  //
  // Create a route cache entry, and tag it as spawned from this route entry
  //
  RtCacheEntry = Ip6CreateRouteCacheEntry (Dest, Src, &NextHop, (UINTN)RtEntry);

  if (RtCacheEntry == NULL) {
    return NULL;
  }

  InsertHeadList (ListHead, &RtCacheEntry->Link);
  NET_GET_REF (RtCacheEntry);
  RtTable->Cache.CacheNum[Index]++;

  return RtCacheEntry;
}
