lib/ip_reserved.ex

defmodule IpReserved do
  @moduledoc """
  Documentation for `IpReserved`, a tool that checks if the given IP is private / reserved or "normal".
  Works with IPv4 and IPv6 addresses.
  """

  @ip_v4_reserved_blocks [
    # Current network
    "0.0.0.0/8",
    # rfc1918, Private network
    "10.0.0.0/8",
    # NAT
    "100.64.0.0/10",
    # Loopback addresses
    "127.0.0.0/8",
    # Link-local addresses
    "169.254.0.0/16",
    # rfc1918, Local communications for private network
    "172.16.0.0/12",
    "192.0.0.0/29",
    "192.0.0.170/31",
    # Documentation and examples
    "192.0.2.0/24",
    # rfc1918, Local communications for private network
    "192.168.0.0/16",
    # benchmark testing
    "198.18.0.0/15",
    # Documentation and examples
    "198.51.100.0/24",
    # Documentation and examples
    "203.0.113.0/24",
    # Future use
    "240.0.0.0/4",
    # limited broadcast
    "255.255.255.255/32",

    # other things
    "240.0.0.0/4"
  ]

  @ip_v6_reserved_blocks [
    # loopback address to local host
    "::1/128",
    # unspecified
    "::/128",
    "::ffff:0:0/96",
    # Discard prefix
    "100::/64",
    "2001::/23",
    "2001:2::/48",
    # Documentation
    "2001:db8::/32",
    "2001:10::/28",
    # Unique local address
    "fc00::/7",
    # Link-local address
    "fe80::/10",

    # Other things
    "::/8",
    "100::/8",
    "200::/7",
    "400::/6",
    "800::/5",
    "1000::/4",
    "4000::/3",
    "6000::/3",
    "8000::/3",
    "A000::/3",
    "C000::/3",
    "E000::/4",
    "F000::/5",
    "F800::/6",
    "FE00::/9"
  ]

  @private_blocks @ip_v4_reserved_blocks ++ @ip_v6_reserved_blocks

  @type ipv4_address :: {0..255, 0..255, 0..255, 0..255}
  @type ipv6_address() ::
          {0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535}

  @type ip_address :: ipv4_address | ipv6_address

  @doc """
  Returns whether an IP address is reserved or not.

  ## Examples

      iex> IpReserved.is_reserved?("192.168.0.1")
      true

      iex> IpReserved.is_reserved?({8, 8, 1, 1})
      false

      iex> IpReserved.is_reserved?("2001:db8:0:85a3::ac1f:8001")
      true

  """
  @spec is_reserved?(String.t()) :: boolean
  def is_reserved?(ip) when is_bitstring(ip) do
    ip
    |> InetCidr.parse_address!()
    |> is_reserved?()
  end

  @spec is_reserved?(ip_address()) :: boolean
  def is_reserved?(ip) when is_tuple(ip) do
    test_ip_against_block(ip, @private_blocks)
  end

  defp test_ip_against_block(ip, block) when is_tuple(ip) and is_list(block) do
    Enum.reduce_while(block, false, fn block, acc ->
      cidr = InetCidr.parse(block)

      if InetCidr.contains?(cidr, ip) do
        {:halt, true}
      else
        {:cont, acc}
      end
    end)
  end
end