lib/iana/iana.ex

defmodule Iptrie.Iana do
  @moduledoc ~S"""
  Functions to access a local snapshot of the IANA IPv4/6 Special-Purpose Address Registries.

  See also:

  - [IPv4 Special-Purpose Address Registry](https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml)
  - [IPv6 Special-Purpose Address Registry](https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml)

  At compile time, this module builds an Iptrie.t using Pfx's local copy of the
  IANA special-purpose address registries, which can then be accessed using:

  - `Iptrie.Iana.get/1` to retrieve the list of prefixes and properties for IPv4 or IPv6 prefixes
  - `Iptrie.Iana.lookup/2` to retrieve a single or all properties for a given prefix, and
  - `Iptrie.iana_special/2`, which delegates to `Iptrie.Iana.lookup/2`.

  Running `mix iana.special` will show the number of records and last update of
  these registries by Iana, as well as the numboer of records and last update
  of the local snapshot.  If the snapshot is out-of-date, it will be updated
  with the new information, in which case both Pfx and Iptrie.Iana module will
  need to be recompiled.

  ## Examples

      iex> Iptrie.Iana.lookup("10.10.10.10")
      {"10.0.0.0/8",
       %{
         allocation: "1996-02",
         destination: true,
         forward: true,
         global: false,
         name: "private-use",
         prefix: "10.0.0.0/8",
         reserved: false,
         source: true,
         spec: ["rfc1918"],
         termination: :na
       }}

      iex> Iptrie.iana_special("fc00::")
      {"fc00::/7",
        %{
          allocation: "2005-10",
          destination: true,
          forward: true,
          global: false,
          name: "unique-local",
          prefix: "fc00::/7",
          reserved: false,
          source: true,
          spec: ["rfc4193", "rfc8190"],
          termination: :na
       }}

      # retrieve a single property
      iex> Iptrie.Iana.lookup("fc00::", :global)
      false

      iex> Iptrie.Iana.lookup("fc00::", :name)
      "unique-local"

  """

  @iana Iptrie.new()
        |> Iptrie.put(Pfx.iana_special(:ip4))
        |> Iptrie.put(Pfx.iana_special(:ip6))

  @doc ~S"""
  Returns the list of IPv4 or IPv6 prefixes and their IANA special purpose address properties in a map.

  ## Examples

      iex> get(:ip4) |> length()
      25

      iex> get(:ip4) |> hd()
      {%Pfx{bits: <<0, 0, 0, 0>>, maxlen: 32},
       %{
         allocation: "1981-09",
         destination: false,
         forward: false,
         global: false,
         name: "this-host-on-this-network",
         prefix: "0.0.0.0/32",
         reserved: true,
         source: true,
         spec: ["rfc1122"],
         termination: :na
       }}

      iex> get(:ip6) |> length()
      20

      iex> get(:ip6) |> hd()
      {%Pfx{bits: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>, maxlen: 128},
       %{
         allocation: "2006-02",
         destination: false,
         forward: false,
         global: false,
         name: "unspecified-address", 
         prefix: "::/128",
         reserved: true,
         source: true,
         spec: ["rfc4291"],
         termination: :na
       }}

      # get all non-globally-routed IPv4 prefixes
      iex> get(:ip4)
      ...> |> Enum.filter(fn {_, m} -> m.global != true end)
      ...> |> Enum.map(fn {pfx, _} -> "#{pfx}" end)
      [
        "0.0.0.0", "0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8",
        "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/29", "192.0.0.0/24", "192.0.0.8",
        "192.0.0.170", "192.0.0.171", "192.0.2.0/24", "192.88.99.0/24",
        "192.168.0.0/16", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24",
        "240.0.0.0/4", "255.255.255.255"
      ]

  """
  @spec get(:ip4 | :ip6) :: [{Pfx.t(), map}]
  def get(:ip4),
    do: Iptrie.to_list(@iana, 32)

  def get(:ip6),
    do: Iptrie.to_list(@iana, 128)

  @doc """
  Performs a longest prefix match against the local IPv4 and IPv6 special
  purpose address registries and Returns either nil, a property value or
  property map for given search prefix.

  ## Examples

    iex(16)> lookup("0.0.0.0")
    {"0.0.0.0",
     %{
       allocation: "1981-09",
       destination: false,
       forward: false,
       global: false,
       name: "this-host-on-this-network",
       prefix: "0.0.0.0/32",
       reserved: true,
       source: true,
       spec: ["rfc1122"],
       termination: :na
     }}

    iex(17)> lookup("0.0.0.1")
    {"0.0.0.0/8",
     %{
       allocation: "1981-09",
       destination: false,
       forward: false,
       global: false,
       name: "this-network",
       prefix: "0.0.0.0/8",
       reserved: true,
       source: true,
       spec: ["rfc791"],
       termination: :na
     }}

      # a non-existing property `:missing`
      iex> lookup("10.10.10.10", :missing)
      nil

      iex> lookup("::ffff:0:0/96", :global)
      false

      iex> lookup("::ffff:0:0/96", :name)
      "ipv4-mapped-address"

  """
  @spec lookup(Pfx.prefix(), atom | nil) :: nil | map | any
  def lookup(prefix, property \\ nil)

  def lookup(prefix, nil),
    do: Iptrie.lookup(@iana, prefix)

  def lookup(prefix, property) do
    with {_pfx, m} <- Iptrie.lookup(@iana, prefix),
         do: Map.get(m, property)
  end
end