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.

woojiq d559eed9 52cc703b

+3
lib/default.nix
···
# linux kernel configuration
kernel = callLibs ./kernel.nix;
# TODO: For consistency, all builtins should also be available from a sub-library;
# these are the only ones that are currently not
inherit (builtins) addErrorContext isPath trace;
···
# linux kernel configuration
kernel = callLibs ./kernel.nix;
+
# network
+
network = callLibs ./network;
+
# TODO: For consistency, all builtins should also be available from a sub-library;
# these are the only ones that are currently not
inherit (builtins) addErrorContext isPath trace;
+49
lib/network/default.nix
···
···
+
{ 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;
+
};
+
};
+
}
+209
lib/network/internal.nix
···
···
+
{
+
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";
+
};
+
}
+117
lib/tests/network.sh
···
···
+
#!/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
+3
lib/tests/test-with-nix.nix
···
echo "Running lib/tests/sources.sh"
TEST_LIB=$PWD/lib bash lib/tests/sources.sh
echo "Running lib/fileset/tests.sh"
TEST_LIB=$PWD/lib bash lib/fileset/tests.sh
···
echo "Running lib/tests/sources.sh"
TEST_LIB=$PWD/lib bash lib/tests/sources.sh
+
echo "Running lib/tests/network.sh"
+
TEST_LIB=$PWD/lib bash lib/tests/network.sh
+
echo "Running lib/fileset/tests.sh"
TEST_LIB=$PWD/lib bash lib/fileset/tests.sh