Add Unique Local Address generation.
This commit is contained in:
parent
78885b2b2d
commit
db9ab41b53
7 changed files with 427 additions and 8 deletions
|
@ -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
59
lib/ip/address/ula.ex
Normal 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
|
128
lib/ip/prefix.ex
128
lib/ip/prefix.ex
|
@ -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
51
lib/ip/prefix/eui64.ex
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
defmodule IPAddressTest do
|
||||
use ExUnit.Case
|
||||
doctest IP.Address
|
||||
doctest IP.Address, except: [generate_ula: 3]
|
||||
end
|
||||
|
|
4
test/ip/prefix/eui64_test.exs
Normal file
4
test/ip/prefix/eui64_test.exs
Normal file
|
@ -0,0 +1,4 @@
|
|||
defmodule IPPrefixEUI64Test do
|
||||
use ExUnit.Case
|
||||
doctest IP.Prefix.EUI64
|
||||
end
|
Loading…
Reference in a new issue