Skip to content

Commit

Permalink
Merge pull request #318712 from woojiq/lib-network-ipv6-parser
Browse files Browse the repository at this point in the history
lib.network: add ipv6 parser
  • Loading branch information
Janik-Haag authored Jul 11, 2024
2 parents 4b522ae + d559eed commit c20399e
Show file tree
Hide file tree
Showing 8 changed files with 425 additions and 1 deletion.
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/
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 typeOf unsafeGetAttrPos;
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 = {
/**
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 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
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

0 comments on commit c20399e

Please sign in to comment.