Add IP address scopes.
This commit is contained in:
parent
e60eced8b0
commit
26abbcd64f
7 changed files with 188 additions and 18 deletions
75
lib/enumerable/ip/prefix.ex
Normal file
75
lib/enumerable/ip/prefix.ex
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -85,8 +85,8 @@ defmodule IP.Prefix.Parser do
|
|||
case Address.from_string(mask, 4) do
|
||||
{:ok, address} ->
|
||||
mask = address
|
||||
|> Address.to_integer()
|
||||
|> calculate_length_from_mask()
|
||||
|> Address.to_integer()
|
||||
|> calculate_length_from_mask()
|
||||
{:ok, mask}
|
||||
_ ->
|
||||
{:ok, String.to_integer(mask)}
|
||||
|
|
81
lib/ip/scope.ex
Normal file
81
lib/ip/scope.ex
Normal 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
|
4
test/enumerable/ip/prefix_test.exs
Normal file
4
test/enumerable/ip/prefix_test.exs
Normal file
|
@ -0,0 +1,4 @@
|
|||
defmodule EnumerableIPPrefixTest do
|
||||
use ExUnit.Case
|
||||
doctest Enumerable.IP.Prefix
|
||||
end
|
4
test/ip/scope_test.exs
Normal file
4
test/ip/scope_test.exs
Normal file
|
@ -0,0 +1,4 @@
|
|||
defmodule IPScopeTest do
|
||||
use ExUnit.Case
|
||||
doctest IP.Scope
|
||||
end
|
Loading…
Reference in a new issue