Add Unique Local Address generation.

This commit is contained in:
James Harton 2017-10-10 08:46:15 +13:00
parent 78885b2b2d
commit db9ab41b53
7 changed files with 427 additions and 8 deletions

View file

@ -1,6 +1,6 @@
defmodule IP.Address do
alias __MODULE__
alias IP.Address.{InvalidAddress, Helpers, Prefix}
alias IP.{Address, Prefix}
alias IP.Address.{InvalidAddress, Helpers, ULA}
defstruct ~w(address version)a
import Helpers
use Bitwise
@ -343,6 +343,188 @@ defmodule IP.Address do
def v4?(%Address{version: 4} = _address), do: true
def v4?(_address), do: false
@doc """
Returns true if the address is an EUI-64 address.
## Examples
iex> "2001:db8::62f8:1dff:fead:d890"
...> |> IP.Address.from_string!()
...> |> IP.Address.eui_64?()
true
"""
@spec eui_64?(t) :: true | false
def eui_64?(%Address{address: address, version: 6})
when (address &&& 0x20000fffe000000) == 0x20000fffe000000,
do: true
def eui_64?(_address), do: false
@doc """
Return a MAC address coded in an EUI-64 address.
## Examples
iex> "2001:db8::62f8:1dff:fead:d890"
...> |> IP.Address.from_string!()
...> |> IP.Address.eui_64_mac()
{:ok, "60f8.1dad.d890"}
"""
@spec eui_64_mac(t) :: binary
def eui_64_mac(%Address{address: address, version: 6})
when (address &&& 0x20000fffe000000) == 0x20000fffe000000
do
mac = address &&& 0xffffffffffffffff
head = mac >>> 40
tail = mac &&& 0xffffff
mac = ((head <<< 24) + tail) ^^^ 0x20000000000
<<a::binary-size(4), b::binary-size(4), c::binary-size(4)>> = mac
|> Integer.to_string(16)
|> String.downcase()
|> String.pad_leading(12, "0")
{:ok, "#{a}.#{b}.#{c}"}
end
def eui_64_mac(_address), do: {:error, "Not an EUI-64 address"}
@doc """
Convert an IPv4 address into a 6to4 address.
## Examples
iex> "192.0.2.1"
...> |> IP.Address.from_string!()
...> |> IP.Address.to_6to4()
#IP.Address<2002:c000:201::>
"""
@spec to_6to4(t) :: {:ok, t} | {:error, term}
def to_6to4(%Address{address: address, version: 4}) do
address = (0x2002 <<< 112) + (address <<< 80)
%Address{address: address, version: 6}
end
def to_6to4(_address), do: {:error, "Not an IPv4 address"}
@doc """
Determine if the IP address is a 6to4 address.
## Examples
iex> "2002:c000:201::"
...> |> IP.Address.from_string!()
...> |> IP.Address.is_6to4?()
true
iex> "2001:db8::"
...> |> IP.Address.from_string!()
...> |> IP.Address.is_6to4?()
false
"""
@spec is_6to4?(t) :: true | false
def is_6to4?(%Address{address: address, version: 6})
when (address >>> 112) == 0x2002, do: true
def is_6to4?(_address), do: false
@doc """
Convert a 6to4 IPv6 address to it's correlated IPv6 address.
## Examples
iex> "2002:c000:201::"
...> |> IP.Address.from_string!()
...> |> IP.Address.from_6to4()
...> |> inspect()
"{:ok, #IP.Address<192.0.2.1>}"
iex> "2001:db8::"
...> |> IP.Address.from_string!()
...> |> IP.Address.from_6to4()
{:error, "Not a 6to4 address"}
"""
@spec from_6to4(t) :: {:ok, t} | {:error, term}
def from_6to4(%Address{address: address, version: 6})
when (address >>> 112) == 0x2002
do
address = (address >>> 80) &&& 0xffffffff
Address.from_integer(address, 4)
end
def from_6to4(_address), do: {:error, "Not a 6to4 address"}
@doc """
Determine if an IP address is a teredo connection.
## Examples
iex> "2001::"
...> |> IP.Address.from_string!()
...> |> IP.Address.is_teredo?()
true
"""
@spec is_teredo?(t) :: true | false
def is_teredo?(%Address{address: address, version: 6})
when (address >>> 96) == 0x20010000, do: true
def is_teredo?(_address), do: false
@doc """
Return information about a teredo connection.
## Examples
iex> "2001:0:4136:e378:8000:63bf:3fff:fdd2"
...> |> IP.Address.from_string!()
...> |> IP.Address.teredo()
...> |> Map.get(:server)
#IP.Address<65.54.227.120>
iex> "2001:0:4136:e378:8000:63bf:3fff:fdd2"
...> |> IP.Address.from_string!()
...> |> IP.Address.teredo()
...> |> Map.get(:client)
#IP.Address<63.255.253.210>
iex> "2001:0:4136:e378:8000:63bf:3fff:fdd2"
...> |> IP.Address.from_string!()
...> |> IP.Address.teredo()
...> |> Map.get(:port)
25535
"""
@spec teredo(t) :: {:ok, map} | {:error, term}
def teredo(%Address{address: address, version: 6})
when (address >>> 96) == 0x20010000 do
server = address >>> 64 &&& ((1 <<< 32) - 1)
client = address &&& ((1 <<< 32) - 1) &&& ((1 <<< 32) - 1)
port = (address >>> 32) &&& ((1 <<< 16) - 1)
%{server: Address.from_integer!(server, 4),
client: Address.from_integer!(client, 4),
port: port}
end
def teredo(_address), do: {:error, "Not a teredo address"}
@doc """
Generate an IPv6 Unique Local Address
Note that the MAC address is just used as a source of randomness, so where you
get it from is not important and doesn't restrict this ULA to just that system.
See RFC4193
## Examples
iex> IP.Address.generate_ula("60:f8:1d:ad:d8:90")
#IP.Address<fd29:f1ef:86a1::>
"""
@spec generate_ula(binary, non_neg_integer, true | false) :: {:ok, t} | {:error, term}
def generate_ula(mac, subnet_id \\ 0, locally_assigned \\ true) do
with {:ok, address} <- ULA.generate(mac, subnet_id, locally_assigned),
{:ok, address} <- from_integer(address, 6)
do
{:ok, address}
end
end
defp from_bytes([a, b, c, d]) do
(a <<< 24) + (b <<< 16) + (c <<< 8) + d
end

59
lib/ip/address/ula.ex Normal file
View file

@ -0,0 +1,59 @@
defmodule IP.Address.ULA do
alias IP.Address
alias IP.Prefix.EUI64
use Bitwise
@moduledoc """
Used to generate Unique Local Addresses
"""
@doc """
Generates an IPv6 Unique Local Address
"""
@spec generate(binary, non_neg_integer, true | false) :: {:ok, Address.ipv4} | {:error, term}
def generate(mac, subnet_id, locally_assigned)
when is_binary(mac)
and is_integer(subnet_id) and subnet_id >= 0 and subnet_id <= 0xffff
and is_boolean(locally_assigned)
do
with %DateTime{} = now <- DateTime.utc_now(),
{:ok, ntp_time} <- ntp_time(now),
{:ok, eui} <- EUI64.eui_portion(mac),
{:ok, digest} <- generate_digest(ntp_time, eui),
{:ok, global_id} <- last_40_bits_of_digest(digest),
{:ok, prefix} <- generate_address(locally_assigned, subnet_id, global_id)
do
{:ok, prefix}
end
end
defp ntp_time(%DateTime{} = time) do
seconds = DateTime.to_unix(time)
{msec, _} = time.microsecond
{:ok, ((seconds + 0x83AA7E80) <<< 32) + msec}
end
defp generate_digest(ntp_time, eui) do
with key <- << ntp_time::unsigned-integer-size(64), eui::unsigned-integer-size(64) >>,
digest <- :crypto.hash(:sha, key),
digest <- :binary.decode_unsigned(digest)
do
{:ok, digest}
end
end
defp last_40_bits_of_digest(digest) do
{:ok, digest &&& 0xffffffffff}
end
defp generate_address(locally_assigned, subnet_id, global_id) do
address = (0xfc <<< 120) +
(local_assignment_bit(locally_assigned) <<< 120) +
(global_id <<< 80) +
((subnet_id &&& 0xffff) <<< 64)
{:ok, address}
end
defp local_assignment_bit(true), do: 1
defp local_assignment_bit(false), do: 0
end

View file

@ -1,6 +1,6 @@
defmodule IP.Prefix do
alias IP.{Prefix, Address}
alias IP.Prefix.{Parser, InvalidPrefix, Helpers}
alias IP.Prefix.{Parser, InvalidPrefix, Helpers, EUI64}
defstruct ~w(address mask)a
use Bitwise
import Helpers
@ -30,12 +30,12 @@ defmodule IP.Prefix do
@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
mask = calculate_mask_from_length(length, 32)
%Prefix{address: Address.from_integer!(address &&& mask, 4), mask: mask}
%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
mask = calculate_mask_from_length(length, 128)
%Prefix{address: Address.from_integer!(address &&& mask, 6), mask: mask}
%Prefix{address: Address.from_integer!(address, 6), mask: mask}
end
@doc """
@ -146,6 +146,29 @@ defmodule IP.Prefix do
@spec length(t) :: ipv4_prefix_length | ipv6_prefix_length
def length(%Prefix{mask: mask}), do: calculate_length_from_mask(mask)
@doc """
Alter the bit-`length` of the `prefix`.
## Example
iex> "192.0.2.0/24"
...> |> IP.Prefix.from_string!()
...> |> IP.Prefix.length(25)
#IP.Prefix<192.0.2.0/25>
"""
@spec length(t, ipv4_prefix_length | ipv6_prefix_length) :: t
def length(%Prefix{address: %Address{version: 4}} = prefix, length)
when is_number(length) and length >= 0 and length <= 32
do
%{prefix | mask: calculate_mask_from_length(length, 32)}
end
def length(%Prefix{address: %Address{version: 6}} = prefix, length)
when is_number(length) and length >= 0 and length <= 128
do
%{prefix | mask: calculate_mask_from_length(length, 128)}
end
@doc """
Returns the calculated mask of the prefix.
@ -272,4 +295,103 @@ defmodule IP.Prefix do
def contains?(_prefix, _address), do: false
@doc """
Generate an EUI-64 host address within the specifed IPv6 `prefix`.
EUI-64 addresses can only be generated for 64 bit long IPv6 prefixes.
## Examples
iex> "2001:db8::/64"
...> |> IP.Prefix.from_string!
...> |> IP.Prefix.eui_64("60:f8:1d:ad:d8:90")
...> |> inspect()
"{:ok, #IP.Address<2001:db8::62f8:1dff:fead:d890>}"
"""
@spec eui_64(t, binary) :: Address.t
def eui_64(%Prefix{address: %Address{version: 6},
mask: 0xffffffffffffffff0000000000000000} = prefix, mac)
do
with {:ok, eui_portion} <- EUI64.eui_portion(mac),
address <- Prefix.first(prefix),
address <- Address.to_integer(address),
address <- address + eui_portion,
{:ok, address} <- Address.from_integer(address, 6)
do
{:ok, address}
end
end
@doc """
Generate an EUI-64 host address within the specifed IPv6 `prefix`.
EUI-64 addresses can only be generated for 64 bit long IPv6 prefixes.
## Examples
iex> "2001:db8::/64"
...> |> IP.Prefix.from_string!
...> |> IP.Prefix.eui_64!("60:f8:1d:ad:d8:90")
#IP.Address<2001:db8::62f8:1dff:fead:d890>
"""
@spec eui_64!(t, binary) :: Address.t
def eui_64!(prefix, mac) do
case eui_64(prefix, mac) do
{:ok, address} -> address
{:error, msg} -> raise(InvalidPrefix, msg)
end
end
@doc """
Return the address space within this address.
## Examples
iex> "192.0.2.0/24"
...> |> IP.Prefix.from_string!()
...> |> IP.Prefix.space()
256
iex> "2001:db8::/64"
...> |> IP.Prefix.from_string!()
...> |> IP.Prefix.space()
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()
last - first + 1
end
@doc """
Return the usable IP address space within this address.
## Examples
iex> "192.0.2.0/24"
...> |> IP.Prefix.from_string!()
...> |> IP.Prefix.usable()
254
iex> "2001:db8::/64"
...> |> IP.Prefix.from_string!()
...> |> IP.Prefix.usable()
18446744073709551616
"""
@spec usable(t) :: non_neg_integer
def usable(%Prefix{address: %Address{version: 4}} = prefix) do
space = prefix
|> IP.Prefix.space()
space - 2
end
def usable(%Prefix{address: %Address{version: 6}} = prefix) do
IP.Prefix.space(prefix)
end
end

51
lib/ip/prefix/eui64.ex Normal file
View file

@ -0,0 +1,51 @@
defmodule IP.Prefix.EUI64 do
use Bitwise
@moduledoc """
Handles functions related to EUI64 addresses.
"""
@doc """
Parse a mac address into an integer.
## Examples
iex> "60:f8:1d:ad:d8:90"
...> |> IP.Prefix.EUI64.eui_portion()
{:ok, 7131482995267852432}
"""
@spec eui_portion(binary) :: {:ok, non_neg_integer} | {:error, term}
def eui_portion(mac) do
with {:ok, mac} <- remove_non_digits(mac),
{:ok, mac} <- hex_to_int(mac),
{:ok, head, tail} <- split_mac(mac),
{:ok, eui} <- generate_eui(head, tail)
do
{:ok, eui}
else
{:error, _} = e -> e
end
end
defp remove_non_digits(mac) do
{:ok, Regex.replace(~r/[^0-9a-f]/i, mac, "")}
end
defp split_mac(mac) when is_integer(mac) and mac >= 0 and mac <= 0xffffffffffff do
head = mac >>> 24
tail = mac &&& 0xffffff
{:ok, head, tail}
end
def generate_eui(head, tail) do
address = (head <<< 40) + (0xfffe <<< 24) + tail
address = address ^^^ 0x0200000000000000
{:ok, address}
end
def hex_to_int(mac) do
{:ok, String.to_integer(mac, 16)}
rescue
ArgumentError -> {:error, "Unable to parse MAC address"}
end
end

View file

@ -14,8 +14,9 @@ defimpl String.Chars, for: IP.Prefix do
...> "#{prefix}"
"192.0.2.1/32"
"""
def to_string(%Prefix{address: address} = prefix) do
length = Prefix.length(prefix)
def to_string(%Prefix{} = prefix) do
length = Prefix.length(prefix)
address = Prefix.first(prefix)
"#{address}/#{length}"
end
end

View file

@ -1,4 +1,4 @@
defmodule IPAddressTest do
use ExUnit.Case
doctest IP.Address
doctest IP.Address, except: [generate_ula: 3]
end

View file

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