# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
defmodule CIDR do
use Bitwise
@moduledoc """
Classless Inter-Domain Routing (CIDR)
"""
defstruct first: nil, last: nil, mask: nil, hosts: nil
defimpl String.Chars, for: CIDR do
@doc """
Prints cidr objectes in human readable format
IPv4: 1.1.1.0/24
IPv6: 2001::/64
"""
def to_string(cidr), do: "#{:inet.ntoa(cidr.first)}/#{cidr.mask}"
end
@doc """
Check whether the argument is a CIDR value.
## Examples
iex> CIDR.is_cidr?("192.168.1.254/32")
true
"""
def is_cidr?(cidr) when is_map(cidr) do
cidr.__struct__ == CIDR
end
def is_cidr?(string) when is_bitstring(string) do
string
|> parse
|> is_cidr?
end
def is_cidr?(_), do: false
@doc """
Checks if an IP address is in the provided CIDR.
Returns `{:ok, true}` if the address is in the CIDR range, `{:ok, false}` if
it's not, and `{:error, reason}` if the second argument isn't a valid IP
address.
"""
def match(cidr, address) when is_binary(address) do
case parse_address(address) do
{:ok, ip} -> match(cidr, ip)
{:error, reason} -> {:error, reason}
end
end
def match(%CIDR{first: {a, b, c, d}, last: {e, f, g, h}}, {i, j, k, l} = address) do
if is_ipv4(address) do
result =
i in a..e and
j in b..f and
k in c..g and
l in d..h
{:ok, result}
else
{:error, "Tuple is not a valid IP address"}
end
end
def match(
%CIDR{first: {a, b, c, d, e, f, g, h}, last: {i, j, k, l, m, n, o, p}},
{q, r, s, t, u, v, w, x} = address
) do
if is_ipv6(address) do
result =
q in a..i and
r in b..j and
s in c..k and
t in d..l and
u in e..m and
v in f..n and
w in g..o and
x in h..p
{:ok, result}
else
{:error, "Tuple is not a valid IP address"}
end
end
def match(_cidr, _address),
do: {:error, "Argument must be a binary or IP tuple of the same protocol"}
@doc """
Throwing version of match/2, raises `ArgumentError` on error.
"""
def match!(cidr, address) do
case match(cidr, address) do
{:ok, result} -> result
{:error, reason} -> raise ArgumentError, message: reason
end
end
@doc """
Returns a stream of all hosts in the range
## Examples
iex> CIDR.parse("192.168.0.0/31") |> CIDR.hosts |> Enum.map(fn(x) -> x end)
[{192, 168, 0, 0}, {192, 168, 0, 1}]
"""
def hosts(%CIDR{first: {_, _, _, _}} = cidr) do
t = tuple2number(cidr.first, 32 - cidr.mask)
Stream.map(0..(cidr.hosts - 1), fn x -> number2tuple(t + x, :ipv4) end)
end
def hosts(%CIDR{first: {_, _, _, _, _, _, _, _}} = cidr) do
t = tuple2number(cidr.first, 128 - cidr.mask)
Stream.map(0..(cidr.hosts - 1), fn x -> number2tuple(t + x, :ipv6) end)
end
@doc """
Checks if two cidr objects are equal
### Examples
iex> d = CIDR.parse("10.0.0.0/24")
%CIDR{first: {10, 0, 0, 0}, hosts: 256, last: {10, 0, 0, 255}, mask: 24}
iex> c = CIDR.parse("10.0.0.0/24")
%CIDR{first: {10, 0, 0, 0}, hosts: 256, last: {10, 0, 0, 255}, mask: 24}
iex(21)> CIDR.equal?(d, c)
true
"""
def equal?(a, b) do
a.first == b.first and
a.last == b.last
end
@doc """
Checks if a is a subnet of b
"""
def subnet?(%CIDR{mask: mask_a}, %CIDR{mask: mask_b}) when mask_a < mask_b do
false
end
def subnet?(a, b) do
tuple2number(a.first, 0) >= tuple2number(b.first, 0) and
tuple2number(a.last, 0) <= tuple2number(b.last, 0)
end
@doc """
Checks if a is a supernet of b
"""
def supernet?(%CIDR{mask: mask_a}, %CIDR{mask: mask_b}) when mask_a > mask_b do
false
end
def supernet?(a, b) do
tuple2number(a.first, 0) <= tuple2number(b.first, 0) and
tuple2number(a.last, 0) >= tuple2number(b.last, 0)
end
@doc """
Splits an existing cidr into smaller blocks
### Examples
iex> CIDR.parse("192.168.0.0/24") |> CIDR.split(25) |> Enum.map(&(&1))
[%CIDR{first: {192, 168, 0, 0}, hosts: 128, last: {192, 168, 0, 127}, mask: 25},
%CIDR{first: {192, 168, 0, 128}, hosts: 128, last: {192, 168, 0, 255}, mask: 25}]
"""
def split(%CIDR{mask: mask}, new_mask) when mask > new_mask do
{:error, "New mask must be larger than existing cidr"}
end
def split(%CIDR{first: {_, _, _, _}} = cidr, new_mask) do
x = tuple2number(cidr.first, 32 - cidr.mask)
split(x, new_mask, cidr.mask, :ipv4)
end
def split(%CIDR{first: {_, _, _, _, _, _, _, _}} = cidr, new_mask) do
x = tuple2number(cidr.first, 128 - cidr.mask)
split(x, new_mask, cidr.mask, :ipv6)
end
defp split(start, new_mask, old_mask, afi) do
n = (:math.pow(2, new_mask - old_mask) - 1) |> round
step = num_hosts(afi, new_mask)
Stream.map(0..n, fn x ->
offset = start + step * x
first = number2tuple(offset, afi)
last = number2tuple(offset + (step - 1), afi)
%CIDR{first: first, last: last, mask: new_mask, hosts: step}
end)
end
@doc """
Parses a bitstring into a CIDR struct
"""
def parse(string) when string |> is_bitstring do
[address | mask] = string |> String.split("/")
case parse_address(address) do
{:ok, address} -> parse(address, mask)
{:error, reason} -> {:error, reason}
end
end
# Only bitstrings can be parsed
def parse(_other) do
{:error, "Not a bitstring"}
end
# We got a simple IP address without mask
defp parse(address, []) when tuple_size(address) == 4 do
create(address, address, 32, num_hosts(:ipv4, 32))
end
defp parse(address, []) when tuple_size(address) == 8 do
create(address, address, 128, num_hosts(:ipv6, 128))
end
# We got a mask and need to convert it to integer
defp parse(address, [mask]) do
parse(address, mask |> int)
end
# Validate that mask is valid
defp parse(address, mask) when tuple_size(address) == 4 and mask not in 0..32 do
{:error, "Invalid mask #{mask}"}
end
defp parse(address, mask) when tuple_size(address) == 8 and mask not in 0..128 do
{:error, "Invalid mask #{mask}"}
end
# Everything is fine
defp parse(address, mask) when tuple_size(address) == 4 do
parse(address, mask, :ipv4)
end
defp parse(address, mask) when tuple_size(address) == 8 do
parse(address, mask, :ipv6)
end
defp parse(address, mask, version) do
first = range_address(version, address, mask, false)
last = range_address(version, address, mask, true)
create(first, last, mask, num_hosts(version, mask))
end
defp parse_address(address) do
address |> String.to_charlist() |> :inet.parse_address()
end
defp create(first, last, mask, hosts) do
%CIDR{first: first, last: last, mask: mask, hosts: hosts}
end
defp num_hosts(:ipv4, mask), do: 1 <<< (32 - mask)
defp num_hosts(:ipv6, mask), do: 1 <<< (128 - mask)
defp range_address(:ipv4, tuple, mask, is_last) do
s = 32 - mask
x = tuple2number(tuple, s)
x = if is_last, do: x ||| (1 <<< s) - 1, else: x
x |> number2tuple(:ipv4)
end
defp range_address(:ipv6, tuple, mask, is_last) do
s = 128 - mask
x = tuple2number(tuple, s)
x = if is_last, do: x ||| (1 <<< s) - 1, else: x
x |> number2tuple(:ipv6)
end
def number2tuple(n, afi) do
case afi do
:ipv6 -> n |> number2list(0, 16, 8, 0xFFFF) |> List.to_tuple()
:ipv4 -> n |> number2list(0, 8, 4, 0xFF) |> List.to_tuple()
end
end
def number2list(_, _, _, 0, _), do: []
def number2list(x, s, d, i, m) do
number2list(x, s + d, d, i - 1, m) ++ [x >>> s &&& m]
end
def tuple2number({a, b, c, d}, s) do
(a <<< 24 ||| b <<< 16 ||| c <<< 8 ||| d) >>> s <<< s
end
def tuple2number({a, b, c, d, e, f, g, h}, s) do
(a <<< 112 ||| b <<< 96 ||| c <<< 80 ||| d <<< 64 |||
e <<< 48 ||| f <<< 32 ||| g <<< 16 ||| h) >>> s <<< s
end
defp is_ipv4({_, _, _, _} = tuple), do: is_ipvx(tuple, 0..255)
defp is_ipv4(_), do: false
defp is_ipv6({_, _, _, _, _, _, _, _} = tuple), do: is_ipvx(tuple, 0..65_535)
defp is_ipv6(_), do: false
defp is_ipvx(tuple, range) do
tuple
|> Tuple.to_list()
|> Enum.all?(&(&1 in range))
end
defp int(x) do
case x |> Integer.parse() do
:error -> -1
{a, _} -> a
end
end
@doc """
Returns a list of deaggregated networks from a IP range in the format of "192.168.1.0-192.168.2.0".
## Examples
iex> CIDR.parse_range("192.168.1.0-192.168.2.0")
[
%CIDR{first: {192, 168, 1, 0}, hosts: 256, last: {192, 168, 1, 255}, mask: 24},
%CIDR{first: {192, 168, 2, 0}, hosts: 1, last: {192, 168, 2, 0}, mask: 32}
]
"""
def parse_range(string) when is_bitstring(string) do
ips = String.split(string, "-")
case length(ips) do
1 -> parse(string) # single ip found, use normal `parse\1` function
2 -> parse_range_impl(ips) # found two values, continue
_ -> {:error, :einval}
end
end
defp parse_range_impl([a, b]) when is_bitstring(a) and is_bitstring(b) do
case {parse(a), parse(b)} do
# only do it for ipv4.
# mask has to be 32 bit.
{%CIDR{mask: 32, first: first_a}, %CIDR{mask: 32, first: first_b}}
when tuple_size(first_a) == 4 and tuple_size(first_b) == 4 and first_a <= first_b ->
lower = tuple2number(first_a, 0)
higher = tuple2number(first_b, 0)
range_reduce_outer(lower, higher)
|> Enum.map(fn {ip, net} -> parse(number2tuple(ip, :ipv4), net, :ipv4) end)
_ ->
{:error, :einval}
end
end
defp range_reduce_outer(lower, higher, acc \\ []) do
# check if `higher` isn't smaller then `lower`
case lower <= higher do
true ->
{new_acc, new_lower} = range_reduce_inner(lower, higher, 0, acc)
range_reduce_outer(new_lower, higher, new_acc)
false ->
acc
end
end
defp range_reduce_inner(lower, higher, step, acc) do
outer_test = (lower ||| 1 <<< step) != lower
inner_test = (lower ||| 0xFFFFFFFF >>> (32 - 1 - step)) > higher
case outer_test && !inner_test do
true -> range_reduce_inner(lower, higher, step + 1, acc)
false -> {acc ++ [{lower, 32 - step}], lower + (1 <<< step)}
end
end
end