about summary refs log tree commit diff
path: root/lib/network/internal.nix
blob: 3e05be90c5475a78d9d3678b423588eff142ae99 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
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";
  };
}