lib/vintage_net/ip.ex

defmodule VintageNet.IP do
  @moduledoc """
  This module contains utilities for handling IP addresses.

  By far the most important part of handling IP addresses is to
  pay attention to whether your addresses are names, IP addresses
  as strings or IP addresses at tuples. This module doesn't resolve
  names. While IP addresses in string form are convenient to type,
  nearly all Erlang and Elixir code uses IP addresses in tuple
  form.
  """

  defguardp is_ipv4_octet(v) when v >= 0 and v <= 255
  defguardp is_ipv6_hextet(v) when v >= 0 and v <= 65535

  @doc """
  Convert an IP address to a string

  Examples:

      iex> VintageNet.IP.ip_to_string({192, 168, 0, 1})
      "192.168.0.1"

      iex> VintageNet.IP.ip_to_string("192.168.9.1")
      "192.168.9.1"

      iex> VintageNet.IP.ip_to_string({65152, 0, 0, 0, 0, 0, 0, 1})
      "fe80::1"
  """
  @spec ip_to_string(VintageNet.any_ip_address()) :: String.t()
  def ip_to_string(ipa) when is_tuple(ipa) do
    :inet.ntoa(ipa) |> List.to_string()
  end

  def ip_to_string(ipa) when is_binary(ipa), do: ipa

  @doc """
  Convert an IP address w/ prefix to a CIDR-formatted string

  Examples:

      iex> VintageNet.IP.cidr_to_string({192, 168, 0, 1}, 24)
      "192.168.0.1/24"
  """
  @spec cidr_to_string(:inet.ip_address(), VintageNet.prefix_length()) :: String.t()
  def cidr_to_string(ipa, bits) do
    ip_to_string(ipa) <> "/" <> Integer.to_string(bits)
  end

  @doc """
  Convert an IP address to tuple form

  Examples:

      iex> VintageNet.IP.ip_to_tuple("192.168.0.1")
      {:ok, {192, 168, 0, 1}}

      iex> VintageNet.IP.ip_to_tuple({192, 168, 1, 1})
      {:ok, {192, 168, 1, 1}}

      iex> VintageNet.IP.ip_to_tuple("fe80::1")
      {:ok, {65152, 0, 0, 0, 0, 0, 0, 1}}

      iex> VintageNet.IP.ip_to_tuple({65152, 0, 0, 0, 0, 0, 0, 1})
      {:ok, {65152, 0, 0, 0, 0, 0, 0, 1}}

      iex> VintageNet.IP.ip_to_tuple("bologna")
      {:error, "Invalid IP address: bologna"}
  """
  @spec ip_to_tuple(VintageNet.any_ip_address()) ::
          {:ok, :inet.ip_address()} | {:error, String.t()}
  def ip_to_tuple({a, b, c, d} = ipa)
      when is_ipv4_octet(a) and is_ipv4_octet(b) and is_ipv4_octet(c) and is_ipv4_octet(d),
      do: {:ok, ipa}

  def ip_to_tuple({a, b, c, d, e, f, g, h} = ipa)
      when is_ipv6_hextet(a) and
             is_ipv6_hextet(b) and
             is_ipv6_hextet(c) and
             is_ipv6_hextet(d) and
             is_ipv6_hextet(e) and
             is_ipv6_hextet(f) and
             is_ipv6_hextet(g) and
             is_ipv6_hextet(h),
      do: {:ok, ipa}

  def ip_to_tuple(ipa) when is_binary(ipa) do
    case :inet.parse_address(to_charlist(ipa)) do
      {:ok, addr} -> {:ok, addr}
      {:error, :einval} -> {:error, "Invalid IP address: #{ipa}"}
    end
  end

  def ip_to_tuple(ipa), do: {:error, "Invalid IP address: #{inspect(ipa)}"}

  @doc """
  Raising version of ip_to_tuple/1
  """
  @spec ip_to_tuple!(VintageNet.any_ip_address()) :: :inet.ip_address()
  def ip_to_tuple!(ipa) do
    case ip_to_tuple(ipa) do
      {:ok, addr} ->
        addr

      {:error, error} ->
        raise ArgumentError, error
    end
  end

  @doc """
  Convert an IPv4 subnet mask to a prefix length.

  Examples:

      iex> VintageNet.IP.subnet_mask_to_prefix_length({255, 255, 255, 0})
      {:ok, 24}

      iex> VintageNet.IP.subnet_mask_to_prefix_length({192, 168, 1, 1})
      {:error, "{192, 168, 1, 1} is not a valid IPv4 subnet mask"}
  """
  @spec subnet_mask_to_prefix_length(:inet.ip_address()) ::
          {:ok, VintageNet.prefix_length()} | {:error, String.t()}
  def subnet_mask_to_prefix_length(subnet_mask) when tuple_size(subnet_mask) == 4 do
    # Not exactly efficient...
    lookup = for bits <- 0..32, into: %{}, do: {prefix_length_to_subnet_mask(:inet, bits), bits}

    case Map.get(lookup, subnet_mask) do
      nil -> {:error, "#{inspect(subnet_mask)} is not a valid IPv4 subnet mask"}
      bits -> {:ok, bits}
    end
  end

  def subnet_mask_to_prefix_length(subnet_mask) when tuple_size(subnet_mask) == 8 do
    # Not exactly efficient...
    lookup = for bits <- 0..128, into: %{}, do: {prefix_length_to_subnet_mask(:inet6, bits), bits}

    case Map.get(lookup, subnet_mask) do
      nil -> {:error, "#{inspect(subnet_mask)} is not a valid IPv6 subnet mask"}
      bits -> {:ok, bits}
    end
  end

  @doc """
  Convert an IPv4 or IPv6 prefix length to a subnet mask.

  Examples:

      iex> VintageNet.IP.prefix_length_to_subnet_mask(:inet, 24)
      {255, 255, 255, 0}

      iex> VintageNet.IP.prefix_length_to_subnet_mask(:inet, 28)
      {255, 255, 255, 240}

      iex> VintageNet.IP.prefix_length_to_subnet_mask(:inet6, 64)
      {65535, 65535, 65535, 65535, 0, 0, 0, 0}
  """
  @spec prefix_length_to_subnet_mask(:inet | :inet6, VintageNet.prefix_length()) ::
          :inet.ip_address()
  def prefix_length_to_subnet_mask(:inet, len) when len >= 0 and len <= 32 do
    rest = 32 - len
    <<a, b, c, d>> = <<-1::size(len), 0::size(rest)>>
    {a, b, c, d}
  end

  def prefix_length_to_subnet_mask(:inet6, len) when len >= 0 and len <= 128 do
    rest = 128 - len

    <<a::size(16), b::size(16), c::size(16), d::size(16), e::size(16), f::size(16), g::size(16),
      h::size(16)>> = <<-1::size(len), 0::size(rest)>>

    {a, b, c, d, e, f, g, h}
  end

  @doc """
  Utility function to trim an IP address to its subnet

  Examples:

      iex> VintageNet.IP.to_subnet({192, 168, 1, 50}, 24)
      {192, 168, 1, 0}

      iex> VintageNet.IP.to_subnet({192, 168, 255, 50}, 22)
      {192, 168, 252, 0}

      iex> VintageNet.IP.to_subnet({64768, 43690, 0, 0, 4144, 58623, 65276, 33158}, 64)
      {64768, 43690, 0, 0, 0, 0, 0, 0}
  """
  @spec to_subnet(:inet.ip_address(), VintageNet.prefix_length()) :: :inet.ip_address()
  def to_subnet({a, b, c, d}, subnet_bits) when subnet_bits >= 0 and subnet_bits <= 32 do
    not_subnet_bits = 32 - subnet_bits
    <<subnet::size(subnet_bits), _::size(not_subnet_bits)>> = <<a, b, c, d>>
    <<new_a, new_b, new_c, new_d>> = <<subnet::size(subnet_bits), 0::size(not_subnet_bits)>>
    {new_a, new_b, new_c, new_d}
  end

  def to_subnet({a, b, c, d, e, f, g, h}, subnet_bits)
      when subnet_bits >= 0 and subnet_bits <= 128 do
    not_subnet_bits = 128 - subnet_bits

    <<subnet::size(subnet_bits), _::size(not_subnet_bits)>> =
      <<a::16, b::16, c::16, d::16, e::16, f::16, g::16, h::16>>

    <<new_a::16, new_b::16, new_c::16, new_d::16, new_e::16, new_f::16, new_g::16, new_h::16>> =
      <<subnet::size(subnet_bits), 0::size(not_subnet_bits)>>

    {new_a, new_b, new_c, new_d, new_e, new_f, new_g, new_h}
  end
end