From db9ab41b53bdb2231a313d7f631e68c964ef4c15 Mon Sep 17 00:00:00 2001 From: James Harton Date: Tue, 10 Oct 2017 08:46:15 +1300 Subject: [PATCH] Add Unique Local Address generation. --- lib/ip/address.ex | 186 +++++++++++++++++++++++++++++++++- lib/ip/address/ula.ex | 59 +++++++++++ lib/ip/prefix.ex | 128 ++++++++++++++++++++++- lib/ip/prefix/eui64.ex | 51 ++++++++++ lib/string/chars/ip/prefix.ex | 5 +- test/ip/address_test.exs | 2 +- test/ip/prefix/eui64_test.exs | 4 + 7 files changed, 427 insertions(+), 8 deletions(-) create mode 100644 lib/ip/address/ula.ex create mode 100644 lib/ip/prefix/eui64.ex create mode 100644 test/ip/prefix/eui64_test.exs diff --git a/lib/ip/address.ex b/lib/ip/address.ex index 3b0bff2..e52c4b9 100644 --- a/lib/ip/address.ex +++ b/lib/ip/address.ex @@ -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 + <> = 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 + """ + @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 diff --git a/lib/ip/address/ula.ex b/lib/ip/address/ula.ex new file mode 100644 index 0000000..476e698 --- /dev/null +++ b/lib/ip/address/ula.ex @@ -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 diff --git a/lib/ip/prefix.ex b/lib/ip/prefix.ex index eca4bc9..cfe4b6d 100644 --- a/lib/ip/prefix.ex +++ b/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 diff --git a/lib/ip/prefix/eui64.ex b/lib/ip/prefix/eui64.ex new file mode 100644 index 0000000..8961399 --- /dev/null +++ b/lib/ip/prefix/eui64.ex @@ -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 diff --git a/lib/string/chars/ip/prefix.ex b/lib/string/chars/ip/prefix.ex index d427a70..a12f84e 100644 --- a/lib/string/chars/ip/prefix.ex +++ b/lib/string/chars/ip/prefix.ex @@ -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 diff --git a/test/ip/address_test.exs b/test/ip/address_test.exs index f9052e4..c3dec47 100644 --- a/test/ip/address_test.exs +++ b/test/ip/address_test.exs @@ -1,4 +1,4 @@ defmodule IPAddressTest do use ExUnit.Case - doctest IP.Address + doctest IP.Address, except: [generate_ula: 3] end diff --git a/test/ip/prefix/eui64_test.exs b/test/ip/prefix/eui64_test.exs new file mode 100644 index 0000000..cb03f7c --- /dev/null +++ b/test/ip/prefix/eui64_test.exs @@ -0,0 +1,4 @@ +defmodule IPPrefixEUI64Test do + use ExUnit.Case + doctest IP.Prefix.EUI64 +end