Add IP address scopes.

This commit is contained in:
James Harton 2017-10-10 22:56:30 +13:00
parent e60eced8b0
commit 26abbcd64f
7 changed files with 188 additions and 18 deletions

View file

@ -0,0 +1,75 @@
defimpl Enumerable, for: IP.Prefix do
alias IP.{Prefix, Address}
use Bitwise
@moduledoc """
Implements `Enumerable` for `IP.Prefix`, allowing consumers to iterate
through all addresses in a prefix.
"""
@doc """
Returns the number of addresses within the `prefix`.
## Examples
iex> "192.0.2.128/25"
...> |> IP.Prefix.from_string!()
...> |> Enum.count()
128
iex> "2001:db8::/121"
...> |> IP.Prefix.from_string!()
...> |> Enum.count()
128
"""
@spec count(Prefix.t) :: {:ok, non_neg_integer} | {:error, module}
def count(prefix), do: {:ok, Prefix.space(prefix)}
@doc """
Returns whether an `address` is contained by the `prefix`.
## Examples
iex> IP.Prefix.from_string!("192.0.2.128/25")
...> |> Enum.member?(IP.Address.from_string!("192.0.2.250"))
true
"""
@spec member?(Prefix.t, Address.t) :: {:ok, boolean} | {:error, module}
def member?(prefix, %Address{} = address), do: {:ok, Prefix.contains?(prefix, address)}
@doc """
Allows the reduction of `prefix` into a colection of addresses.
## Examples
iex> IP.Prefix.from_string!("192.0.2.128/29")
...> |> Stream.filter(fn a -> rem(IP.Address.to_integer(a), 2) == 0 end)
...> |> Enum.map(fn a -> IP.Address.to_string(a) end)
["192.0.2.130", "192.0.2.132", "192.0.2.134"]
"""
@spec reduce(Prefix.t, Enumerable.acc, Enumerable.reducer) :: Enumerable.result
def reduce(_, {:halt, acc}, _fun), do: {:halted, acc}
def reduce({prefix, pos, last}, {:suspend, acc}, fun), do: {:suspended, acc, &reduce({prefix, pos, last}, &1, fun)}
def reduce(%Prefix{} = prefix, {:cont, acc}, fun) do
first = prefix
|> Prefix.first()
|> Address.to_integer()
last = prefix
|> Prefix.last()
|> Address.to_integer()
reduce({prefix, first, last}, {:cont, acc}, fun)
end
def reduce({%Prefix{address: %Address{version: version}} = prefix, pos, last}, {:cont, acc}, fun) do
case pos do
^last -> {:done, acc}
pos ->
pos = pos + 1
next = Address.from_integer!(pos, version)
reduce({prefix, pos, last}, fun.(next, acc), fun)
end
end
end

View file

@ -28,12 +28,12 @@ defmodule IP.Prefix do
#IP.Prefix<2001:db8::/64>
"""
@spec new(Address.t, ipv4_prefix_length | ipv6_prefix_length) :: t
def new(%Address{address: address, version: 4}, length) when length > 0 and length <= 32 do
def new(%Address{address: address, version: 4}, length) when length >= 0 and length <= 32 do
mask = calculate_mask_from_length(length, 32)
%Prefix{address: Address.from_integer!(address, 4), mask: mask}
end
def new(%Address{address: address, version: 6}, length) when length > 0 and length <= 128 do
def new(%Address{address: address, version: 6}, length) when length >= 0 and length <= 128 do
mask = calculate_mask_from_length(length, 128)
%Prefix{address: Address.from_integer!(address, 6), mask: mask}
end
@ -188,13 +188,12 @@ defmodule IP.Prefix do
iex> IP.Prefix.from_string!("192.0.2.0/24")
...> |> IP.Prefix.subnet_mask()
"255.255.255.0"
#IP.Address<255.255.255.0>
"""
@spec subnet_mask(t) :: binary
def subnet_mask(%Prefix{mask: mask, address: %Address{version: 4}}) do
mask
|> Address.from_integer!(4)
|> Address.to_string()
end
@doc """
@ -204,7 +203,7 @@ defmodule IP.Prefix do
iex> IP.Prefix.from_string!("192.0.2.0/24")
...> |> IP.Prefix.wildcard_mask()
"0.0.0.255"
#IP.Address<0.0.0.255>
"""
@spec wildcard_mask(t) :: binary
def wildcard_mask(%Prefix{mask: mask, address: %Address{version: 4}}) do
@ -212,7 +211,6 @@ defmodule IP.Prefix do
|> bnot()
|> band(@ipv4_mask)
|> Address.from_integer!(4)
|> Address.to_string()
end
@doc """
@ -358,13 +356,15 @@ defmodule IP.Prefix do
18446744073709551616
"""
@spec space(t) :: non_neg_integer
def space(%Prefix{} = prefix) do
first = prefix
|> Prefix.first()
|> Address.to_integer()
last = prefix
|> Prefix.last()
|> Address.to_integer()
def space(%Prefix{address: %Address{address: address, version: 4}, mask: mask}) do
first = address &&& mask
last = first + (~~~mask &&& @ipv4_mask)
last - first + 1
end
def space(%Prefix{address: %Address{address: address, version: 6}, mask: mask}) do
first = address &&& mask
last = first + (~~~mask &&& @ipv6_mask)
last - first + 1
end

View file

@ -5,9 +5,9 @@ defmodule IP.Prefix.Helpers do
@doc false
def calculate_mask_from_length(length, mask_length) do
pad = mask_length - length
0..(length - 1)
|> Enum.reduce(0, fn (i, mask) -> mask + (1 <<< i + pad) end)
pad = mask_length - length - 1
mask = n_times_reduce(length, 0, fn (i, mask) -> mask + (1 <<< i) end)
mask <<< pad
end
@doc false
@ -20,4 +20,10 @@ defmodule IP.Prefix.Helpers do
end)
|> Enum.count()
end
defp n_times_reduce(0, acc, _fun), do: acc
defp n_times_reduce(n, acc, fun) do
acc = fun.(n, acc)
n_times_reduce(n - 1, acc, fun)
end
end

81
lib/ip/scope.ex Normal file
View file

@ -0,0 +1,81 @@
defmodule IP.Scope do
alias IP.{Prefix, Address}
use Bitwise
require IP.Prefix
@moduledoc """
Implements scope lookup for all (currently) known scopes.
Please open a pull-request if this needs changing.
"""
@v4_scopes [
{"0.0.0.0/8", "CURRENT NETWORK"},
{"10.0.0.0/8", "RFC1918 PRIVATE"},
{"127.0.0.0/8", "LOOPBACK"},
{"168.254.0.0/16", "AUTOCONF PRIVATE"},
{"172.16.0.0/12", "RFC1918 PRIVATE"},
{"192.0.0.0/24", "RESERVED (IANA)"},
{"192.0.2.0/24", "DOCUMENTATION"},
{"192.88.99.0/24", "6to4 ANYCAST"},
{"192.168.0.0/16", "RFC1918 PRIVATE"},
{"198.18.0.0/15", "NETWORK BENCHMARK TESTS"},
{"198.51.100.0/24", "DOCUMENTATION"},
{"203.0.113.0/24", "DOCUMENTATION"},
{"239.0.0.0/8", "LOCAL MULTICAST"},
{"224.0.0.0/4", "GLOBAL MULTICAST"},
{"240.0.0.0/4", "RESERVED"},
{"255.255.255.255/32", "GLOBAL BROADCAST"},
{"0.0.0.0/0", "GLOBAL UNICAST"}
]
@v6_scopes [
{"2001:10::/28", "ORCHID"},
{"2001:db8::/32", "DOCUMENTATION"},
{"2000::/3", "GLOBAL UNICAST"},
{"::/128", "UNSPECIFIED ADDRESS"},
{"::1/128", "LINK LOCAL LOOPBACK"},
{"::ffff:0:0/96", "IPv4 MAPPED"},
{"::/96", "IPv4 TRANSITION (deprecated)"},
{"fc00::/7", "UNIQUE LOCAL UNICAST"},
{"fec0::/10", "SITE LOCAL (deprecated)"},
{"fe80::/10", "LINK LOCAL UNICAST"},
{"ff00::/8", "MULTICAST"},
{"::/0", "RESERVED"}
]
@doc """
Return the scope of `address``
## Examples
iex> IP.Address.from_string!("192.0.2.0")
...> |> IP.Scope.address_scope()
"DOCUMENTATION"
iex> IP.Address.from_string!("2001:db8::")
...> |> IP.Scope.address_scope()
"DOCUMENTATION"
"""
@spec address_scope(Address.t) :: binary
Enum.each(@v4_scopes, fn {prefix, description} ->
%Prefix{address: %Address{address: addr0}, mask: mask} = Prefix.from_string!(prefix)
def address_scope(%Address{address: addr1})
when (unquote(addr0) &&& unquote(mask)) <= addr1
and (unquote(addr0) &&& unquote(mask)) + (~~~unquote(mask) &&& 0xffffffff) >= addr1
do
unquote(description)
end
end)
Enum.each(@v6_scopes, fn {prefix, description} ->
%Prefix{address: %Address{address: addr0}, mask: mask} = Prefix.from_string!(prefix)
def address_scope(%Address{address: addr1})
when (unquote(addr0) &&& unquote(mask)) <= addr1
and (unquote(addr0) &&& unquote(mask)) + (~~~unquote(mask) &&& 0xffffffffffffffffffffffffffffffff) >= addr1
do
unquote(description)
end
end)
end

View file

@ -0,0 +1,4 @@
defmodule EnumerableIPPrefixTest do
use ExUnit.Case
doctest Enumerable.IP.Prefix
end

4
test/ip/scope_test.exs Normal file
View file

@ -0,0 +1,4 @@
defmodule IPScopeTest do
use ExUnit.Case
doctest IP.Scope
end