mirror of
https://github.com/NixOS/nixpkgs.git
synced 2024-11-25 16:33:15 +00:00
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
|
# linux kernel configuration
|
||||||
kernel = callLibs ./kernel.nix;
|
kernel = callLibs ./kernel.nix;
|
||||||
|
|
||||||
|
# network
|
||||||
|
network = callLibs ./network;
|
||||||
|
|
||||||
# TODO: For consistency, all builtins should also be available from a sub-library;
|
# TODO: For consistency, all builtins should also be available from a sub-library;
|
||||||
# these are the only ones that are currently not
|
# these are the only ones that are currently not
|
||||||
inherit (builtins) addErrorContext isPath trace;
|
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"
|
echo "Running lib/tests/sources.sh"
|
||||||
TEST_LIB=$PWD/lib bash 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"
|
echo "Running lib/fileset/tests.sh"
|
||||||
TEST_LIB=$PWD/lib bash lib/fileset/tests.sh
|
TEST_LIB=$PWD/lib bash lib/fileset/tests.sh
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user