Skip to content

Commit

Permalink
added IPv4 deaggregation feature
Browse files Browse the repository at this point in the history
  • Loading branch information
ifoo committed Jan 12, 2020
1 parent 88f7c41 commit 8376e57
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 0 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ iex(10)> cidr |> CIDR.match("1.2.3.1000")
{:error, "Tuple is not a valid IP address."}
```

IPv4 deaggregation works as well. It returns a list of the largest possible networks for an IPv4 range in the format of `$first_ip-$last_ip`:

```elixir
iex(1)> 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}
]
```

## Contribution

See [Collective Code Construction Contract (C4)](http://rfc.zeromq.org/spec:42/C4/)
Expand Down
59 changes: 59 additions & 0 deletions lib/cidr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -323,4 +323,63 @@ defmodule CIDR do
{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
20 changes: 20 additions & 0 deletions test/cidr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -194,5 +194,25 @@ defmodule CIDRTest do
assert CIDR.subnet?(CIDR.parse("10.0.0.128/25"), CIDR.parse("10.0.0.0/24"))
end

test "IPv4 Range" do
assert CIDR.parse_range("192.168.1.0") == %CIDR{first: {192, 168, 1, 0}, hosts: 1, last: {192, 168, 1, 0}, mask: 32}
assert CIDR.parse_range("192.168.1.0-") == {:error, :einval}
assert CIDR.parse_range("-192.168.1.0") == {:error, :einval}
assert 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}
]
assert CIDR.parse_range("2001::/126") == %CIDR{
first: {8193, 0, 0, 0, 0, 0, 0, 0},
hosts: 4,
last: {8193, 0, 0, 0, 0, 0, 0, 3},
mask: 126
}
assert CIDR.parse_range("192.168.1.0/32-192.168.2.0/32") == [
%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}
]
end

end

0 comments on commit 8376e57

Please sign in to comment.