Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lib.network: add ipv6 parser #318712

Merged
merged 3 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
.idea/
.nixos-test-history
.vscode/
.helix/
Janik-Haag marked this conversation as resolved.
Show resolved Hide resolved
outputs/
result-*
result
Expand Down
5 changes: 4 additions & 1 deletion lib/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -73,7 +76,7 @@ let
info showWarnings nixpkgsVersion version isInOldestRelease
mod compare splitByAndCompare seq deepSeq lessThan add sub
functionArgs setFunctionArgs isFunction toFunction mirrorFunctionArgs
toHexString toBaseDigits inPureEvalMode isBool isInt pathExists
fromHexString toHexString toBaseDigits inPureEvalMode isBool isInt pathExists
genericClosure readFile;
inherit (self.fixedPoints) fix fix' converge extends composeExtensions
composeManyExtensions makeExtensible makeExtensibleWithCustomName;
Expand Down
49 changes: 49 additions & 0 deletions lib/network/default.nix
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 = {
Janik-Haag marked this conversation as resolved.
Show resolved Hide resolved
/**
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
Janik-Haag marked this conversation as resolved.
Show resolved Hide resolved
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 changes: 209 additions & 0 deletions lib/network/internal.nix
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
Janik-Haag marked this conversation as resolved.
Show resolved Hide resolved
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";
};
}
16 changes: 16 additions & 0 deletions lib/tests/misc.nix
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ let
testAllTrue
toBaseDigits
toHexString
fromHexString
toInt
toIntBase10
toShellVars
Expand Down Expand Up @@ -286,6 +287,21 @@ runTests {
expected = "FA";
};

testFromHexStringFirstExample = {
expr = fromHexString "FF";
expected = 255;
};

testFromHexStringSecondExample = {
expr = fromHexString (builtins.hashString "sha256" "test");
expected = 9223372036854775807;
};

testFromHexStringWithPrefix = {
expr = fromHexString "0Xf";
expected = 15;
};

testToBaseDigits = {
expr = toBaseDigits 2 6;
expected = [ 1 1 0 ];
Expand Down
Loading