-
-
Notifications
You must be signed in to change notification settings - Fork 14.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
lib.network: ipv6 parser from string
Add a library function to parse and validate an IPv6 address from a string. It can parse the first two versions of an IPv6 address according to https://datatracker.ietf.org/doc/html/rfc4291#section-2.2. The third form "x:x:x:x:x:x.d.d.d.d" is not yet implemented. Optionally parser can accept prefix length (128 is default). Add shell script network.sh to test IPv6 parser functionality.
- Loading branch information
Showing
5 changed files
with
381 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
{ lib }: | ||
let | ||
inherit (import ./internal.nix { inherit lib; }) _ipv6; | ||
in | ||
{ | ||
ipv6 = { | ||
/** | ||
Creates an `IPv6Address` object from an IPv6 address as a string. If | ||
the prefix length is omitted, it defaults to 64. The parser is limited | ||
to the first two versions of IPv6 addresses addressed in RFC 4291. | ||
The form "x:x:x:x:x:x:d.d.d.d" is not yet implemented. Addresses are | ||
NOT compressed, so they are not always the same as the canonical text | ||
representation of IPv6 addresses defined in RFC 5952. | ||
# Type | ||
``` | ||
fromString :: String -> IPv6Address | ||
``` | ||
# Examples | ||
```nix | ||
fromString "2001:DB8::ffff/32" | ||
=> { | ||
address = "2001:db8:0:0:0:0:0:ffff"; | ||
prefixLength = 32; | ||
} | ||
``` | ||
# Arguments | ||
- [addr] An IPv6 address with optional prefix length. | ||
*/ | ||
fromString = | ||
addr: | ||
let | ||
splittedAddr = _ipv6.split addr; | ||
|
||
addrInternal = splittedAddr.address; | ||
prefixLength = splittedAddr.prefixLength; | ||
|
||
address = _ipv6.toStringFromExpandedIp addrInternal; | ||
in | ||
{ | ||
inherit address prefixLength; | ||
}; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
{ | ||
lib ? import ../., | ||
}: | ||
let | ||
inherit (builtins) | ||
map | ||
match | ||
genList | ||
length | ||
concatMap | ||
head | ||
toString | ||
; | ||
|
||
inherit (lib) lists strings trivial; | ||
|
||
inherit (lib.lists) last; | ||
|
||
/* | ||
IPv6 addresses are 128-bit identifiers. The preferred form is 'x:x:x:x:x:x:x:x', | ||
where the 'x's are one to four hexadecimal digits of the eight 16-bit pieces of | ||
the address. See RFC 4291. | ||
*/ | ||
ipv6Bits = 128; | ||
ipv6Pieces = 8; # 'x:x:x:x:x:x:x:x' | ||
ipv6PieceBits = 16; # One piece in range from 0 to 0xffff. | ||
ipv6PieceMaxValue = 65535; # 2^16 - 1 | ||
in | ||
let | ||
/** | ||
Expand an IPv6 address by removing the "::" compression and padding them | ||
with the necessary number of zeros. Converts an address from the string to | ||
the list of strings which then can be parsed using `_parseExpanded`. | ||
Throws an error when the address is malformed. | ||
# Type: String -> [ String ] | ||
# Example: | ||
```nix | ||
expandIpv6 "2001:DB8::ffff" | ||
=> ["2001" "DB8" "0" "0" "0" "0" "0" "ffff"] | ||
``` | ||
*/ | ||
expandIpv6 = | ||
addr: | ||
if match "^[0-9A-Fa-f:]+$" addr == null then | ||
throw "${addr} contains malformed characters for IPv6 address" | ||
else | ||
let | ||
pieces = strings.splitString ":" addr; | ||
piecesNoEmpty = lists.remove "" pieces; | ||
piecesNoEmptyLen = length piecesNoEmpty; | ||
zeros = genList (_: "0") (ipv6Pieces - piecesNoEmptyLen); | ||
hasPrefix = strings.hasPrefix "::" addr; | ||
hasSuffix = strings.hasSuffix "::" addr; | ||
hasInfix = strings.hasInfix "::" addr; | ||
in | ||
if addr == "::" then | ||
zeros | ||
else if | ||
let | ||
emptyCount = length pieces - piecesNoEmptyLen; | ||
emptyExpected = | ||
# splitString produces two empty pieces when "::" in the beginning | ||
# or in the end, and only one when in the middle of an address. | ||
if hasPrefix || hasSuffix then | ||
2 | ||
else if hasInfix then | ||
1 | ||
else | ||
0; | ||
in | ||
emptyCount != emptyExpected | ||
|| (hasInfix && piecesNoEmptyLen >= ipv6Pieces) # "::" compresses at least one group of zeros. | ||
|| (!hasInfix && piecesNoEmptyLen != ipv6Pieces) | ||
then | ||
throw "${addr} is not a valid IPv6 address" | ||
# Create a list of 8 elements, filling some of them with zeros depending | ||
# on where the "::" was found. | ||
else if hasPrefix then | ||
zeros ++ piecesNoEmpty | ||
else if hasSuffix then | ||
piecesNoEmpty ++ zeros | ||
else if hasInfix then | ||
concatMap (piece: if piece == "" then zeros else [ piece ]) pieces | ||
else | ||
pieces; | ||
|
||
/** | ||
Parses an expanded IPv6 address (see `expandIpv6`), converting each part | ||
from a string to an u16 integer. Returns an internal representation of IPv6 | ||
address (list of integers) that can be easily processed by other helper | ||
functions. | ||
Throws an error some element is not an u16 integer. | ||
# Type: [ String ] -> IPv6 | ||
# Example: | ||
```nix | ||
parseExpandedIpv6 ["2001" "DB8" "0" "0" "0" "0" "0" "ffff"] | ||
=> [8193 3512 0 0 0 0 0 65535] | ||
``` | ||
*/ | ||
parseExpandedIpv6 = | ||
addr: | ||
assert lib.assertMsg ( | ||
length addr == ipv6Pieces | ||
) "parseExpandedIpv6: expected list of integers with ${ipv6Pieces} elements"; | ||
let | ||
u16FromHexStr = | ||
hex: | ||
let | ||
parsed = trivial.fromHexString hex; | ||
in | ||
if 0 <= parsed && parsed <= ipv6PieceMaxValue then | ||
parsed | ||
else | ||
throw "0x${hex} is not a valid u16 integer"; | ||
in | ||
map (piece: u16FromHexStr piece) addr; | ||
in | ||
let | ||
/** | ||
Parses an IPv6 address from a string to the internal representation (list | ||
of integers). | ||
# Type: String -> IPv6 | ||
# Example: | ||
```nix | ||
parseIpv6FromString "2001:DB8::ffff" | ||
=> [8193 3512 0 0 0 0 0 65535] | ||
``` | ||
*/ | ||
parseIpv6FromString = addr: parseExpandedIpv6 (expandIpv6 addr); | ||
in | ||
{ | ||
/* | ||
Internally, an IPv6 address is stored as a list of 16-bit integers with 8 | ||
elements. Wherever you see `IPv6` in internal functions docs, it means that | ||
it is a list of integers produced by one of the internal parsers, such as | ||
`parseIpv6FromString` | ||
*/ | ||
_ipv6 = { | ||
/** | ||
Converts an internal representation of an IPv6 address (i.e, a list | ||
of integers) to a string. The returned string is not a canonical | ||
representation as defined in RFC 5952, i.e zeros are not compressed. | ||
# Type: IPv6 -> String | ||
# Example: | ||
```nix | ||
parseIpv6FromString [8193 3512 0 0 0 0 0 65535] | ||
=> "2001:db8:0:0:0:0:0:ffff" | ||
``` | ||
*/ | ||
toStringFromExpandedIp = | ||
pieces: strings.concatMapStringsSep ":" (piece: strings.toLower (trivial.toHexString piece)) pieces; | ||
|
||
/** | ||
Extract an address and subnet prefix length from a string. The subnet | ||
prefix length is optional and defaults to 128. The resulting address and | ||
prefix length are validated and converted to an internal representation | ||
that can be used by other functions. | ||
# Type: String -> [ {address :: IPv6, prefixLength :: Int} ] | ||
# Example: | ||
```nix | ||
split "2001:DB8::ffff/32" | ||
=> { | ||
address = [8193 3512 0 0 0 0 0 65535]; | ||
prefixLength = 32; | ||
} | ||
``` | ||
*/ | ||
split = | ||
addr: | ||
let | ||
splitted = strings.splitString "/" addr; | ||
splittedLength = length splitted; | ||
in | ||
if splittedLength == 1 then # [ ip ] | ||
{ | ||
address = parseIpv6FromString addr; | ||
prefixLength = ipv6Bits; | ||
} | ||
else if splittedLength == 2 then # [ ip subnet ] | ||
{ | ||
address = parseIpv6FromString (head splitted); | ||
prefixLength = | ||
let | ||
n = strings.toInt (last splitted); | ||
in | ||
if 1 <= n && n <= ipv6Bits then | ||
n | ||
else | ||
throw "${addr} IPv6 subnet should be in range [1;${toString ipv6Bits}], got ${toString n}"; | ||
} | ||
else | ||
throw "${addr} is not a valid IPv6 address in CIDR notation"; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
#!/usr/bin/env bash | ||
|
||
# Tests lib/network.nix | ||
# Run: | ||
# [nixpkgs]$ lib/tests/network.sh | ||
# or: | ||
# [nixpkgs]$ nix-build lib/tests/release.nix | ||
|
||
set -euo pipefail | ||
shopt -s inherit_errexit | ||
|
||
if [[ -n "${TEST_LIB:-}" ]]; then | ||
NIX_PATH=nixpkgs="$(dirname "$TEST_LIB")" | ||
else | ||
NIX_PATH=nixpkgs="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.."; pwd)" | ||
fi | ||
export NIX_PATH | ||
|
||
die() { | ||
echo >&2 "test case failed: " "$@" | ||
exit 1 | ||
} | ||
|
||
tmp="$(mktemp -d)" | ||
clean_up() { | ||
rm -rf "$tmp" | ||
} | ||
trap clean_up EXIT SIGINT SIGTERM | ||
work="$tmp/work" | ||
mkdir "$work" | ||
cd "$work" | ||
|
||
prefixExpression=' | ||
let | ||
lib = import <nixpkgs/lib>; | ||
internal = import <nixpkgs/lib/network/internal.nix> { | ||
inherit lib; | ||
}; | ||
in | ||
with lib; | ||
with lib.network; | ||
' | ||
|
||
expectSuccess() { | ||
local expr=$1 | ||
local expectedResult=$2 | ||
if ! result=$(nix-instantiate --eval --strict --json --show-trace \ | ||
--expr "$prefixExpression ($expr)"); then | ||
die "$expr failed to evaluate, but it was expected to succeed" | ||
fi | ||
if [[ ! "$result" == "$expectedResult" ]]; then | ||
die "$expr == $result, but $expectedResult was expected" | ||
fi | ||
} | ||
|
||
expectSuccessRegex() { | ||
local expr=$1 | ||
local expectedResultRegex=$2 | ||
if ! result=$(nix-instantiate --eval --strict --json --show-trace \ | ||
--expr "$prefixExpression ($expr)"); then | ||
die "$expr failed to evaluate, but it was expected to succeed" | ||
fi | ||
if [[ ! "$result" =~ $expectedResultRegex ]]; then | ||
die "$expr == $result, but $expectedResultRegex was expected" | ||
fi | ||
} | ||
|
||
expectFailure() { | ||
local expr=$1 | ||
local expectedErrorRegex=$2 | ||
if result=$(nix-instantiate --eval --strict --json --show-trace 2>"$work/stderr" \ | ||
--expr "$prefixExpression ($expr)"); then | ||
die "$expr evaluated successfully to $result, but it was expected to fail" | ||
fi | ||
if [[ ! "$(<"$work/stderr")" =~ $expectedErrorRegex ]]; then | ||
die "Error was $(<"$work/stderr"), but $expectedErrorRegex was expected" | ||
fi | ||
} | ||
|
||
# Internal functions | ||
expectSuccess '(internal._ipv6.split "0:0:0:0:0:0:0:0").address' '[0,0,0,0,0,0,0,0]' | ||
expectSuccess '(internal._ipv6.split "000a:000b:000c:000d:000e:000f:ffff:aaaa").address' '[10,11,12,13,14,15,65535,43690]' | ||
expectSuccess '(internal._ipv6.split "::").address' '[0,0,0,0,0,0,0,0]' | ||
expectSuccess '(internal._ipv6.split "::0000").address' '[0,0,0,0,0,0,0,0]' | ||
expectSuccess '(internal._ipv6.split "::1").address' '[0,0,0,0,0,0,0,1]' | ||
expectSuccess '(internal._ipv6.split "::ffff").address' '[0,0,0,0,0,0,0,65535]' | ||
expectSuccess '(internal._ipv6.split "::000f").address' '[0,0,0,0,0,0,0,15]' | ||
expectSuccess '(internal._ipv6.split "::1:1:1:1:1:1:1").address' '[0,1,1,1,1,1,1,1]' | ||
expectSuccess '(internal._ipv6.split "1::").address' '[1,0,0,0,0,0,0,0]' | ||
expectSuccess '(internal._ipv6.split "1:1:1:1:1:1:1::").address' '[1,1,1,1,1,1,1,0]' | ||
expectSuccess '(internal._ipv6.split "1:1:1:1::1:1:1").address' '[1,1,1,1,0,1,1,1]' | ||
expectSuccess '(internal._ipv6.split "1::1").address' '[1,0,0,0,0,0,0,1]' | ||
|
||
expectFailure 'internal._ipv6.split "0:0:0:0:0:0:0:-1"' "contains malformed characters for IPv6 address" | ||
expectFailure 'internal._ipv6.split "::0:"' "is not a valid IPv6 address" | ||
expectFailure 'internal._ipv6.split ":0::"' "is not a valid IPv6 address" | ||
expectFailure 'internal._ipv6.split "0::0:"' "is not a valid IPv6 address" | ||
expectFailure 'internal._ipv6.split "0:0:"' "is not a valid IPv6 address" | ||
expectFailure 'internal._ipv6.split "0:0:0:0:0:0:0:0:0"' "is not a valid IPv6 address" | ||
expectFailure 'internal._ipv6.split "0:0:0:0:0:0:0:0:"' "is not a valid IPv6 address" | ||
expectFailure 'internal._ipv6.split "::0:0:0:0:0:0:0:0"' "is not a valid IPv6 address" | ||
expectFailure 'internal._ipv6.split "0::0:0:0:0:0:0:0"' "is not a valid IPv6 address" | ||
expectFailure 'internal._ipv6.split "::10000"' "0x10000 is not a valid u16 integer" | ||
|
||
expectSuccess '(internal._ipv6.split "::").prefixLength' '128' | ||
expectSuccess '(internal._ipv6.split "::/1").prefixLength' '1' | ||
expectSuccess '(internal._ipv6.split "::/128").prefixLength' '128' | ||
|
||
expectFailure '(internal._ipv6.split "::/0").prefixLength' "IPv6 subnet should be in range \[1;128\], got 0" | ||
expectFailure '(internal._ipv6.split "::/129").prefixLength' "IPv6 subnet should be in range \[1;128\], got 129" | ||
expectFailure '(internal._ipv6.split "/::/").prefixLength' "is not a valid IPv6 address in CIDR notation" | ||
|
||
# Library API | ||
expectSuccess 'lib.network.ipv6.fromString "2001:DB8::ffff/64"' '{"address":"2001:db8:0:0:0:0:0:ffff","prefixLength":64}' | ||
expectSuccess 'lib.network.ipv6.fromString "1234:5678:90ab:cdef:fedc:ba09:8765:4321/44"' '{"address":"1234:5678:90ab:cdef:fedc:ba09:8765:4321","prefixLength":44}' | ||
|
||
echo >&2 tests ok |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters