summaryrefslogtreecommitdiff
path: root/lib/network
diff options
context:
space:
mode:
authorwoojiq <yurii.shymon@gmail.com>2024-06-23 19:33:30 +0300
committerwoojiq <yurii.shymon@gmail.com>2024-06-29 10:02:00 +0300
commitd559eed93af199646f495f8ef4f91971743e480c (patch)
tree4f1fcff07fd299294f207c9654ba0789c291a2f9 /lib/network
parentlib: add fromHexString (diff)
downloadnixpkgs-d559eed93af199646f495f8ef4f91971743e480c.tar.gz
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.
Diffstat (limited to 'lib/network')
-rw-r--r--lib/network/default.nix49
-rw-r--r--lib/network/internal.nix209
2 files changed, 258 insertions, 0 deletions
diff --git a/lib/network/default.nix b/lib/network/default.nix
new file mode 100644
index 000000000000..e0c583ee7506
--- /dev/null
+++ b/lib/network/default.nix
@@ -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;
+ };
+ };
+}
diff --git a/lib/network/internal.nix b/lib/network/internal.nix
new file mode 100644
index 000000000000..3e05be90c547
--- /dev/null
+++ b/lib/network/internal.nix
@@ -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";
+ };
+}