about summary refs log tree commit diff
path: root/lib/network/internal.nix
diff options
context:
space:
mode:
Diffstat (limited to 'lib/network/internal.nix')
-rw-r--r--lib/network/internal.nix209
1 files changed, 209 insertions, 0 deletions
diff --git a/lib/network/internal.nix b/lib/network/internal.nix
new file mode 100644
index 0000000000000..3e05be90c5475
--- /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";
+  };
+}