Current section

Files

Jump to
cidr lib cidr.ex
Raw

lib/cidr.ex

# 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
@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}}, address = {i, j, k, l}) 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}},
address = {q, r, s, t, u, v, w, x}) 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 """
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 not mask in 0..32 do
{:error, "Invalid mask #{mask}"}
end
defp parse(address, mask) when tuple_size(address) == 8 and not mask 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_char_list |> :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)
if is_last, do: x = x ||| ((1 <<< s) - 1)
a = ((x >>> 24) &&& 0xFF)
b = ((x >>> 16) &&& 0xFF)
c = ((x >>> 8) &&& 0xFF)
d = ((x >>> 0) &&& 0xFF)
{a, b, c, d}
end
defp range_address(:ipv6, tuple, mask, is_last) do
s = (128 - mask)
x = tuple2number(tuple, s)
if is_last, do: x = x ||| ((1 <<< s) - 1)
a = ((x >>> 112) &&& 0xFFFF)
b = ((x >>> 96) &&& 0xFFFF)
c = ((x >>> 80) &&& 0xFFFF)
d = ((x >>> 64) &&& 0xFFFF)
e = ((x >>> 48) &&& 0xFFFF)
f = ((x >>> 32) &&& 0xFFFF)
g = ((x >>> 16) &&& 0xFFFF)
h = ((x >>> 0) &&& 0xFFFF)
{a, b, c, d, e, f, g, h}
end
defp tuple2number({a, b, c, d}, s) do
(((a <<< 24) ||| (b <<< 16) ||| (c <<< 8) ||| d) >>> s) <<< s
end
defp 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..65535)
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
end