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.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.
This commit is contained in:
parent
52cc703bba
commit
d559eed93a
@ -64,6 +64,9 @@ let
|
||||
# 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
Normal file
49
lib/network/default.nix
Normal file
@ -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;
|
||||
};
|
||||
};
|
||||
}
|
209
lib/network/internal.nix
Normal file
209
lib/network/internal.nix
Normal file
@ -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";
|
||||
};
|
||||
}
|
117
lib/tests/network.sh
Executable file
117
lib/tests/network.sh
Executable file
@ -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
|
@ -65,6 +65,9 @@ pkgs.runCommand "nixpkgs-lib-tests-nix-${nix.version}" {
|
||||
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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user