defmodule IP.Subnet do @moduledoc """ Convenience type which encapsulates the idea of an IP subnet. See: https://en.wikipedia.org/wiki/Subnetwork ### NB The distinction between an `IP.Range` and an `IP.Subnet` is that a Subnet must have its bounds at certain powers-of-two and multiple thereof that are governed by the subnet bit-length. A range is not constrained and is a simple "dumb list of ip addresses". Typically ranges will be proper subsets of Subnets. ### Enumerable Implements the Enumerable protocol, so the following sorts of things are possible: ```elixir iex> import IP iex> Enum.map(~i"10.0.0.4/30", &IP.to_string/1) ["10.0.0.4", "10.0.0.5", "10.0.0.6", "10.0.0.7"] ``` ### Membership In the `IP.Subnet` implementation of Enumerable, the `member?/2` callback is implemented to provide a fastlane membership function. You can thus check IP address membership without having to enumerate all members of the list first. ```elixir iex> import IP iex> ~i"10.0.0.1" in ~i"10.0.0.0/24" true iex> ~i"10.0.0.1..10.0.0.33" in ~i"10.0.0.0/24" true iex> ~i"10.0.0.0/26" in ~i"10.0.0.0/24" true iex> ~i"10.0.0.1..10.0.1.1" in ~i"10.0.0.0/24" false iex> ~i"10.0.0.0/22" in ~i"10.0.0.0/24" false ``` """ @enforce_keys [:routing_prefix, :bit_length] defstruct @enforce_keys ++ [:__enum__] require IP @typedoc "ip subnet typed to ipv4 or ipv6" @type t(ip_type) :: %__MODULE__{ routing_prefix: ip_type, bit_length: 0..128 } @typedoc "generic ip subnet" @type t :: t(IP.v4) | t(IP.v6) @spec is_subnet(any) :: Macro.t @doc """ true if the term is a subnet struct, and it's valid. usable in guards. ```elixir iex> import IP iex> IP.Subnet.is_subnet(~i"10.0.0.0/32") true iex> IP.Subnet.is_subnet(:foo) false iex> IP.Subnet.is_subnet(%IP.Subnet{routing_prefix: {10, 0, 0, 0}, bit_length: 33}) false ``` """ defguard is_subnet(subnet) when is_struct(subnet) and :erlang.map_get(:__struct__, subnet) == __MODULE__ and ((IP.is_ipv4(:erlang.map_get(:routing_prefix, subnet)) and :erlang.map_get(:bit_length, subnet) <= 32 and :erlang.map_get(:bit_length, subnet) >= 0) or (IP.is_ipv6(:erlang.map_get(:routing_prefix, subnet)) and :erlang.map_get(:bit_length, subnet) <= 128 and :erlang.map_get(:bit_length, subnet) >= 0)) import Bitwise, only: [<<<: 2] @doc """ true if the `ip` parameter is inside the subnet. `ip` must be a single ip address; if you need a membership function that accepts ranges or subnets, use `Kernel.in/2`. Currently only works for ipv4 addresses. usable in guards. ```elixir iex> import IP iex> IP.Subnet.is_in(~i"10.0.0.0/24", ~i"10.0.0.2") true iex> IP.Subnet.is_in(~i"10.0.0.0/24", ~i"10.0.1.5") false ``` """ defguard is_in(subnet, ip) when IP.is_ipv4(ip) and ip >= :erlang.map_get(:routing_prefix, subnet) and ((:erlang.map_get(:bit_length, subnet) == 32 and ip == :erlang.map_get(:routing_prefix, subnet)) or (:erlang.map_get(:bit_length, subnet) < 32 and :erlang.map_get(:bit_length, subnet) >= 24 and IP.octet_13(ip) == IP.octet_13(:erlang.map_get(:routing_prefix, subnet)) and IP.octet_4(ip) - IP.octet_4(:erlang.map_get(:routing_prefix, subnet)) < (1 <<< (32 - :erlang.map_get(:bit_length, subnet)))) or (:erlang.map_get(:bit_length, subnet) < 24 and :erlang.map_get(:bit_length, subnet) >= 16 and IP.octet_12(ip) == IP.octet_12(:erlang.map_get(:routing_prefix, subnet)) and IP.octet_34(ip) - IP.octet_34(:erlang.map_get(:routing_prefix, subnet)) < (1 <<< 32 - :erlang.map_get(:bit_length, subnet))) or (:erlang.map_get(:bit_length, subnet) < 16 and :erlang.map_get(:bit_length, subnet) >= 8 and IP.octet_1(ip) == IP.octet_1(:erlang.map_get(:routing_prefix, subnet)) and IP.octet_24(ip) - IP.octet_24(:erlang.map_get(:routing_prefix, subnet)) < (1 <<< 32 - :erlang.map_get(:bit_length, subnet))) or (:erlang.map_get(:bit_length, subnet) < 8 and :erlang.map_get(:bit_length, subnet) >= 0 and IP.octet_14(ip) - IP.octet_14(:erlang.map_get(:routing_prefix, subnet)) < (1 <<< 32 - :erlang.map_get(:bit_length, subnet)))) @spec new(IP.v4, 0..32) :: t(IP.v4) @spec new(IP.v6, 0..128) :: t(IP.v6) @doc """ creates a new IP Subnet struct from a routing prefix and bit length. The routing prefix must be an actual routing prefix for the bit length, otherwise it will raise `ArgumentError`. If you are attempting to find the subnet for a given ip address, use `of/2` """ def new(routing_prefix, bit_length) when IP.is_ipv4(routing_prefix) and 0 <= bit_length and bit_length <= 32 do unless routing_prefix == IP.prefix(routing_prefix, bit_length) do raise ArgumentError, "the routing prefix is not a proper ip subnet prefix. Use IP.Subnet.of/2 instead." end %__MODULE__{ routing_prefix: routing_prefix, bit_length: bit_length } end def new(routing_prefix, bit_length) when IP.is_ipv6(routing_prefix) and 0 <= bit_length and bit_length <= 128 do unless routing_prefix == IP.prefix(routing_prefix, bit_length) do raise ArgumentError, "the routing prefix is not a proper ip subnet prefix. Use IP.Subnet.of/2 instead." end %__MODULE__{ routing_prefix: routing_prefix, bit_length: bit_length } end @spec of(IP.v4, 0..32) :: t(IP.v4) @spec of(IP.v6, 0..128) :: t(IP.v6) @doc """ creates a corresponding IP subnet associated with a given IP address and bit length. """ def of(ip_addr, bit_length) when IP.is_ipv4(ip_addr) and 0 <= bit_length and bit_length <= 32 do %__MODULE__{ routing_prefix: IP.prefix(ip_addr, bit_length), bit_length: bit_length } end def of(ip_addr, bit_length) when IP.is_ipv6(ip_addr) and 0 <= bit_length and bit_length <= 128 do %__MODULE__{ routing_prefix: IP.prefix(ip_addr, bit_length), bit_length: bit_length } end @spec to_string(t) :: String.t @doc """ converts an ip subnet to standard CIDR-form, with a slash delimiter. ```elixir iex> IP.Subnet.to_string(%IP.Subnet{routing_prefix: {10, 0, 0, 0}, bit_length: 24}) "10.0.0.0/24" ``` """ def to_string(subnet) when is_subnet(subnet) do "#{IP.to_string(subnet.routing_prefix)}/#{subnet.bit_length}" end @spec from_string!(String.t) :: t | no_return @doc """ converts a string to an ip subnet. checks if the values are sensible. ```elixir iex> import IP iex> IP.Subnet.from_string!("10.0.0.0/24") %IP.Subnet{ routing_prefix: {10, 0, 0, 0}, bit_length: 24 } ``` """ def from_string!(subnet_str) do case from_string(subnet_str) do {:ok, subnet} -> subnet {:error, :einval} -> raise ArgumentError, "malformed subnet string #{subnet_str}" {:error, :invalid_subnet} -> raise ArgumentError, "invalid subnet value in #{subnet_str}" {:error, :not_a_binary} -> raise ArgumentError, "invalid input #{inspect subnet_str}" end end @doc """ Finds an ip subnet in a string, returning an ok or error tuple on failure. """ def from_string(subnet_str) when is_binary(subnet_str) do with [routing_prefix_str, bit_length_str] <- String.split(subnet_str, "/"), {:ok, routing_prefix} <- IP.from_string(routing_prefix_str), {bit_length, ""} <- Integer.parse(bit_length_str), true <- valid_subnet(routing_prefix, bit_length) do {:ok, of(routing_prefix, bit_length)} else list when is_list(list) -> {:error, :einval} :error -> {:error, :invalid_subnet} false -> {:error, :invalid_subnet} {int, _} when is_integer(int) -> {:error, :einval} error -> error end end def from_string(_), do: {:error, :not_a_binary} @doc """ finds an ip address and subnet together from a `config representation` (this is an ip/cidr string where the ip is not necessarily the routing prefix for the cidr block). returns `{:ok, ip, subnet}` if the config string is valid; `{:error, reason}` otherwise. """ def config_from_string(config_str) when is_binary(config_str) do with [ip_str, bit_length_str] <- String.split(config_str, "/"), {:ok, ip} <- IP.from_string(ip_str), {bit_length, ""} <- Integer.parse(bit_length_str), true <- valid_subnet(ip, bit_length) do {:ok, ip, of(ip, bit_length)} else list when is_list(list) -> {:error, :einval} :error -> {:error, :invalid_subnet} false -> {:error, :invalid_subnet} {int, _} when is_integer(int) -> {:error, :einval} error -> error end end def config_from_string(_), do: {:error, :not_a_binary} @doc """ finds an ip address and subnet together from a `config representation` (this is an ip/cidr string where the ip is not necessarily the routing prefix for the cidr block). This function is useful if you have configuration files that specify IP address/subnet identities in this fashion (for example `ifupdown` or `netplan` configuration files) returns `{ip, subnet}` if the config string is valid; raises otherwise. ```elixir iex> IP.Subnet.config_from_string!("10.0.0.4/24") {{10, 0, 0, 4}, %IP.Subnet{routing_prefix: {10, 0, 0, 0}, bit_length: 24}} ``` """ def config_from_string!(config_str) do case config_from_string(config_str) do {:ok, ip, subnet} -> {ip, subnet} {:error, :einval} -> raise ArgumentError, "malformed subnet string #{config_str}" {:error, :invalid_subnet} -> raise ArgumentError, "invalid subnet value in #{config_str}" {:error, :not_a_binary} -> raise ArgumentError, "invalid input #{inspect config_str}" end end require IP defp valid_subnet(ip, length) when IP.is_ipv4(ip), do: length in 0..32 defp valid_subnet(ip, length) when IP.is_ipv6(ip), do: length in 0..128 @spec broadcast(t(IP.v4)) :: IP.v4 @doc """ finds the broadcast address for a subnet ```elixir iex> import IP iex> IP.Subnet.broadcast(~i"10.0.0.0/23") {10, 0, 1, 255} """ def broadcast(subnet = %{routing_prefix: rp, bit_length: bl}) when is_subnet(subnet) do mask = bl |> IP.mask(:v4) |> IP.to_integer inv_mask = Bitwise.bxor(mask, 0xFFFF_FFFF) rp |> IP.to_integer |> Bitwise.&&&(mask) |> Bitwise.|||(inv_mask) |> IP.from_integer(:v4) end @spec prefix(t) :: IP.addr @doc """ retrieves the routing prefix from a subnet. ```elixir iex> import IP iex> IP.Subnet.prefix(~i"10.0.0.0/24") {10, 0, 0, 0} ``` """ def prefix(%{routing_prefix: rp}), do: rp @spec bitlength(t(IP.v4)) :: 0..32 @spec bitlength(t(IP.v6)) :: 0..128 @doc """ retrieves the bitlength from a subnet. ```elixir iex> import IP iex> IP.Subnet.bitlength(~i"10.0.0.0/24") 24 ``` """ def bitlength(%{bit_length: bl}), do: bl @spec netmask(t) :: IP.addr @doc """ computes the netmask for a subnet. ```elixir iex> import IP iex> IP.Subnet.netmask(~i"10.0.0.0/24") {255, 255, 255, 0} ``` """ def netmask(%{routing_prefix: rp, bit_length: bl}) when IP.is_ipv4(rp), do: IP.mask(bl, :v4) def netmask(%{routing_prefix: rp, bit_length: _bl}) when IP.is_ipv6(rp), do: raise "not implemented yet" ################################################################### ## PRIVATE API @spec type(t(IP.v4)) :: :v4 @spec type(t(IP.v6)) :: :v6 @doc false def type(subnet) when is_subnet(subnet), do: IP.type(subnet.routing_prefix) end defimpl Inspect, for: IP.Subnet do import Inspect.Algebra def inspect(subnet, _opts) do concat(["~i\"", IP.Subnet.to_string(subnet) , "\""]) end end defimpl Enumerable, for: IP.Subnet do alias IP.Subnet alias IP.Range @spec count(Subnet.t) :: {:ok, non_neg_integer} def count(subnet) do import Bitwise {:ok, 2 <<< (31 - subnet.bit_length)} end @spec member?(Subnet.t, IP.addr | Range.t | Subnet.t) :: {:ok, boolean} def member?(subnet, other = %Range{}) do {:ok, subnet.routing_prefix <= other.first and other.last <= Subnet.broadcast(subnet)} end def member?(subnet, other = %Subnet{}) do {:ok, subnet.routing_prefix <= other.routing_prefix and Subnet.broadcast(other) <= Subnet.broadcast(subnet)} end def member?(subnet, this_ip) do {:ok, subnet.routing_prefix <= this_ip and this_ip <= Subnet.broadcast(subnet)} end @spec reduce(Subnet.t, Enumerable.acc, fun) :: Enumerable.result def reduce(_subnet, {:halt, acc}, _), do: {:halted, acc} def reduce(subnet, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(subnet, &1, fun)} def reduce(subnet = %{__enum__: nil}, {:cont, acc}, fun) do placeholder = {IP.next(subnet.routing_prefix), Subnet.broadcast(subnet)} reduce(%{subnet | __enum__: placeholder}, fun.(subnet.routing_prefix, acc), fun) end def reduce(subnet = %{__enum__: {this, last}}, {:cont, acc}, fun) when this <= last do placeholder = {IP.next(this), last} reduce(%{subnet | __enum__: placeholder}, fun.(this, acc), fun) end def reduce(_, {:cont, acc}, _fun), do: {:done, acc} @spec slice(Subnet.t) :: {:ok, non_neg_integer, Enumerable.slicing_fun} def slice(subnet) do type = Subnet.type(subnet) {:ok, count} = count(subnet) {:ok, count, fn start, length -> first_int = IP.to_integer(subnet.routing_prefix) + start last_int = first_int + length - 1 Enum.map(first_int..last_int, &IP.from_integer(&1, type)) end} end end