ip/lib/ip/address.ex

594 lines
16 KiB
Elixir

defmodule IP.Address do
alias IP.Address
alias IP.Address.{Helpers, InvalidAddress, ULA}
alias IP.Prefix
defstruct ~w(address version)a
import Helpers
use Bitwise
@moduledoc """
Simple representations of IP Addresses.
"""
@typedoc "Valid IPv4 address - integer between zero and 32 ones."
@type ipv4 :: 0..0xFFFFFFFF
@typedoc "Valid IPv6 address - integer between zero and 128 ones."
@type ipv6 :: 0..0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
@typedoc "Valid IP address"
@type ip :: ipv4 | ipv6
@typedoc "Valid IP version (currently only 4 and 6 are deployed in the wild)."
@type version :: 4 | 6
@typedoc "IP address struct type, contains a valid address and version."
@type t :: %Address{address: ip, version: version}
@doc """
Convert from (packed) binary representations (either 32 or 128 bits long) into an address.
## Examples
iex> <<192, 0, 2, 1>>
...> |> IP.Address.from_binary()
{:ok, %IP.Address{address: 3221225985, version: 4}}
iex> <<32, 1, 13, 184, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>
...> |> IP.Address.from_binary()
{:ok, %IP.Address{address: 42540766411282592856903984951653826560, version: 6}}
iex> "192.0.2.1"
...> |> IP.Address.from_binary()
{:error, "Unable to convert binary to address"}
"""
@spec from_binary(binary) :: {:ok, t} | {:error, term}
def from_binary(<<address::unsigned-integer-size(32)>>),
do: {:ok, %Address{address: address, version: 4}}
def from_binary(<<address::unsigned-integer-size(128)>>),
do: {:ok, %Address{address: address, version: 6}}
def from_binary(_address), do: {:error, "Unable to convert binary to address"}
@doc """
Convert from a packed binary presentation to an address or raise an
`IP.Address.InvalidAddress` exception.
## Examples
iex> <<192, 0, 2, 1>>
...> |> IP.Address.from_binary!()
%IP.Address{address: 3221225985, version: 4}
iex> <<32, 1, 13, 184, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1>>
...> |> IP.Address.from_binary!()
%IP.Address{address: 42540766411282592856903984951653826561, version: 6}
"""
@spec from_binary!(binary) :: t
def from_binary!(address) do
case from_binary(address) do
{:ok, address} -> address
{:error, msg} -> raise(InvalidAddress, message: msg)
end
end
@doc """
Convert an integer into an IP address of specified version.
## Examples
iex> 3221225985
...> |> IP.Address.from_integer(4)
{:ok, %IP.Address{address: 3221225985, version: 4}}
iex> 42540766411282592856903984951653826561
...> |> IP.Address.from_integer(6)
{:ok, %IP.Address{address: 42540766411282592856903984951653826561, version: 6}}
"""
@spec from_integer(ip, version) :: {:ok, t} | {:error, term}
def from_integer(address, 4) when valid_ipv4_integer?(address) do
{:ok, %Address{address: address, version: 4}}
end
def from_integer(address, 6) when valid_ipv6_integer?(address) do
{:ok, %Address{address: address, version: 6}}
end
def from_integer(_address, 4), do: {:error, "Supplied address not within IPv4 address space"}
def from_integer(_address, 6), do: {:error, "Supplied address not within IPv6 address space"}
def from_integer(_address, version), do: {:error, "No such IP version #{inspect(version)}"}
@doc """
Convert an integer into an IP address of specified version or raise an
`IP.Address.InvalidAddress` exception.
## Examples
iex> 3221225985
...> |> IP.Address.from_integer!(4)
%IP.Address{address: 3221225985, version: 4}
iex> 42540766411282592856903984951653826561
...> |> IP.Address.from_integer!(6)
%IP.Address{address: 42540766411282592856903984951653826561, version: 6}
"""
@spec from_integer!(ip, version) :: t
def from_integer!(address, version) do
case from_integer(address, version) do
{:ok, address} -> address
{:error, msg} -> raise(InvalidAddress, message: msg)
end
end
@doc """
Convert a string representation into an IP address of unknown version.
Tries to parse the string as IPv6, then IPv4 before failing. Obviously if
you know the version then using `from_string/2` is faster.
## Examples
iex> "192.0.2.1"
...> |> IP.Address.from_string()
{:ok, %IP.Address{address: 3221225985, version: 4}}
iex> "2001:db8::1"
...> |> IP.Address.from_string()
{:ok, %IP.Address{address: 42540766411282592856903984951653826561, version: 6}}
"""
@spec from_string(binary) :: {:ok, t} | {:error, term}
def from_string(address) when is_binary(address) do
case from_string(address, 6) do
{:ok, address} ->
{:ok, address}
{:error, _} ->
case from_string(address, 4) do
{:ok, address} -> {:ok, address}
{:error, _} -> {:error, "Unable to parse IP address"}
end
end
end
@doc """
Convert a string representation into an IP address or raise an
`IP.Address.InvalidAddress` exception.
## Examples
iex> "192.0.2.1"
...> |> IP.Address.from_string!()
%IP.Address{address: 3221225985, version: 4}
iex> "2001:db8::1"
...> |> IP.Address.from_string!()
%IP.Address{address: 42540766411282592856903984951653826561, version: 6}
"""
@spec from_string!(binary) :: t
def from_string!(address) when is_binary(address) do
case from_string(address) do
{:ok, addr} -> addr
{:error, msg} -> raise(InvalidAddress, message: msg)
end
end
@doc """
Convert a string representation into an IP address of specified version.
## Examples
iex> "192.0.2.1"
...> |> IP.Address.from_string(4)
{:ok, %IP.Address{address: 3221225985, version: 4}}
iex> "2001:db8::1"
...> |> IP.Address.from_string(6)
{:ok, %IP.Address{address: 42540766411282592856903984951653826561, version: 6}}
"""
@spec from_string(binary, version) :: {:ok, t} | {:error, term}
def from_string(address, 4) when is_binary(address) do
case :inet.parse_ipv4strict_address(String.to_charlist(address)) do
{:ok, addr} ->
addr =
addr
|> Tuple.to_list()
|> from_bytes()
{:ok, %Address{version: 4, address: addr}}
{:error, _} ->
{:error, "Cannot parse IPv4 address"}
end
end
def from_string(address, 6) when is_binary(address) do
case :inet.parse_ipv6strict_address(String.to_charlist(address)) do
{:ok, addr} ->
addr =
addr
|> Tuple.to_list()
|> from_bytes()
{:ok, %Address{version: 6, address: addr}}
{:error, _} ->
{:error, "Cannot parse IPv6 address"}
end
end
def from_string(_address, 4), do: {:error, "Cannot parse IPv4 address"}
def from_string(_address, 6), do: {:error, "Cannot parse IPv6 address"}
def from_string(_address, version), do: {:error, "No such IP version #{inspect(version)}"}
@doc """
Convert a string representation into an IP address of specified version or raise an
`IP.Address.InvalidAddress` exception.
## Examples
iex> "192.0.2.1"
...> |> IP.Address.from_string!(4)
%IP.Address{address: 3221225985, version: 4}
iex> "2001:db8::1"
...> |> IP.Address.from_string!(6)
%IP.Address{address: 42540766411282592856903984951653826561, version: 6}
"""
@spec from_string!(binary, version) :: t
def from_string!(address, version) do
case from_string(address, version) do
{:ok, address} -> address
{:error, msg} -> raise(InvalidAddress, msg)
end
end
@doc """
Convert an Erlang-style tuple of bytes to an address.
## Examples
iex> {192, 0, 2, 1}
...> |> IP.Address.from_tuple()
{:ok, %IP.Address{address: 3221225985, version: 4}}
iex> {8193, 3512, 0, 0, 0, 0, 0, 1}
...> |> IP.Address.from_tuple()
{:ok, %IP.Address{address: 42540766411282592856903984951653826561, version: 6}}
"""
@spec from_tuple(:socket.in_addr() | :socket.in6_addr()) :: {:ok, t} | {:error, term}
def from_tuple({a, b, c, d})
when valid_byte?(a) and valid_byte?(b) and valid_byte?(c) and valid_byte?(d),
do: {:ok, %Address{version: 4, address: from_bytes([a, b, c, d])}}
def from_tuple({a, b, c, d, e, f, g, h})
when valid_quartet?(a) and valid_quartet?(b) and valid_quartet?(c) and valid_quartet?(d) and
valid_quartet?(e) and valid_quartet?(f) and valid_quartet?(g) and valid_quartet?(h),
do: {:ok, %Address{version: 6, address: from_bytes([a, b, c, d, e, f, g, h])}}
def from_tuple(_), do: {:error, "Invalid address"}
@doc """
Convert an Erlang-style tuple of bytes to an address.
## Examples
iex> {192, 0, 2, 1}
...> |> IP.Address.from_tuple!()
%IP.Address{address: 3221225985, version: 4}
iex> {8193, 3512, 0, 0, 0, 0, 0, 1}
...> |> IP.Address.from_tuple!()
%IP.Address{address: 42540766411282592856903984951653826561, version: 6}
"""
@spec from_tuple!(:socket.in_addr() | :socket.in6_addr()) :: t | no_return
def from_tuple!(tuple) do
case from_tuple(tuple) do
{:ok, address} -> address
{:error, msg} -> raise(InvalidAddress, msg)
end
end
@doc """
Convert an `address` into a string.
## Examples
iex> ~i(192.0.2.1)
...> |> IP.Address.to_string()
"192.0.2.1"
iex> ~i(2001:db8::1)
...> |> IP.Address.to_string()
"2001:db8::1"
"""
@spec to_string(t) :: binary
def to_string(%Address{version: 4, address: addr}) do
a = addr >>> 0x18 &&& 0xFF
b = addr >>> 0x10 &&& 0xFF
c = addr >>> 0x08 &&& 0xFF
d = addr &&& 0xFF
{a, b, c, d}
|> :inet.ntoa()
|> List.to_string()
end
def to_string(%Address{version: 6, address: addr}) do
a = addr >>> 0x70 &&& 0xFFFF
b = addr >>> 0x60 &&& 0xFFFF
c = addr >>> 0x50 &&& 0xFFFF
d = addr >>> 0x40 &&& 0xFFFF
e = addr >>> 0x30 &&& 0xFFFF
f = addr >>> 0x20 &&& 0xFFFF
g = addr >>> 0x10 &&& 0xFFFF
h = addr &&& 0xFFFF
{a, b, c, d, e, f, g, h}
|> :inet.ntoa()
|> List.to_string()
end
@doc """
Convert an `address` to an `IP.Prefix`.
## Examples
iex> ~i(192.0.2.1)
...> |> IP.Address.to_prefix(32)
#IP.Prefix<192.0.2.1/32 DOCUMENTATION>
"""
@spec to_prefix(t, Prefix.prefix_length()) :: Prefix.t()
def to_prefix(%Address{} = address, length), do: Prefix.new(address, length)
@doc """
Returns the IP version of the address.
## Examples
iex> ~i(192.0.2.1)
...> |> IP.Address.version()
4
iex> ~i(2001:db8::1)
...> |> IP.Address.version()
6
"""
@spec version(t) :: version
def version(%Address{version: version}), do: version
@doc """
Returns the IP Address as an integer
## Examples
iex> ~i(192.0.2.1)
...> |> IP.Address.to_integer()
3221225985
iex> ~i(2001:db8::1)
...> |> IP.Address.to_integer()
42540766411282592856903984951653826561
"""
@spec to_integer(t) :: ip
def to_integer(%Address{address: address}), do: address
@doc """
Returns true if `address` is version 6.
## Examples
iex> ~i(192.0.2.1)
...> |> IP.Address.v6?
false
iex> ~i(2001:db8::)
...> |> IP.Address.v6?
true
"""
@spec v6?(t) :: boolean
def v6?(%Address{version: 6} = _address), do: true
def v6?(_address), do: false
@doc """
Returns true if `address` is version 4.
## Examples
iex> ~i(192.0.2.1)
...> |> IP.Address.v4?
true
iex> ~i(2001:db8::)
...> |> IP.Address.v4?
false
"""
@spec v4?(t) :: boolean
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> ~i(2001:db8::62f8:1dff:fead:d890)
...> |> IP.Address.eui_64?()
true
"""
@spec eui_64?(t) :: boolean
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> ~i(2001:db8::62f8:1dff:fead:d890)
...> |> IP.Address.eui_64_mac()
{:ok, "60f8.1dad.d890"}
"""
@spec eui_64_mac(t) :: {:ok, binary} | {:error, term}
def eui_64_mac(%Address{address: address, version: 6})
when (address &&& 0x20000FFFE000000) == 0x20000FFFE000000 do
mac = address &&& 0xFFFFFFFFFFFFFFFF
head = mac >>> 40
tail = mac &&& 0xFFFFFF
mac = Bitwise.bxor((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> ~i(192.0.2.1)
...> |> IP.Address.to_6to4()
#IP.Address<2002:c000:201:: GLOBAL UNICAST (6to4)>
"""
@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> ~i(2002:c000:201::)
...> |> IP.Address.is_6to4?()
true
iex> ~i(2001:db8::)
...> |> IP.Address.is_6to4?()
false
"""
@spec is_6to4?(t) :: boolean
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> ~i(2002:c000:201::)
...> |> IP.Address.from_6to4()
...> |> inspect()
"{:ok, #IP.Address<192.0.2.1 DOCUMENTATION>}"
iex> ~i(2001:db8::)
...> |> 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> ~i(2001::)
...> |> IP.Address.is_teredo?()
true
"""
@spec is_teredo?(t) :: boolean
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> ~i(2001:0:4136:e378:8000:63bf:3fff:fdd2)
...> |> IP.Address.teredo()
...> |> Map.get(:server)
#IP.Address<65.54.227.120 GLOBAL UNICAST>
iex> ~i(2001:0:4136:e378:8000:63bf:3fff:fdd2)
...> |> IP.Address.teredo()
...> |> Map.get(:client)
#IP.Address<63.255.253.210 GLOBAL UNICAST>
iex> ~i(2001:0:4136:e378:8000:63bf:3fff:fdd2)
...> |> 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, boolean) :: {:ok, t} | {:error, term}
def generate_ula(mac, subnet_id \\ 0, locally_assigned \\ true) do
case ULA.generate(mac, subnet_id, locally_assigned) do
{:ok, address} -> from_integer(address, 6)
{:error, reason} -> {:error, reason}
end
end
defp from_bytes([a, b, c, d]) do
(a <<< 24) + (b <<< 16) + (c <<< 8) + d
end
defp from_bytes([a, b, c, d, e, f, g, h]) do
(a <<< 0x70) + (b <<< 0x60) + (c <<< 0x50) + (d <<< 0x40) + (e <<< 0x30) + (f <<< 0x20) +
(g <<< 0x10) + h
end
end