1{
2 lib ? import ../.,
3}:
4let
5 inherit (builtins)
6 map
7 match
8 genList
9 length
10 concatMap
11 head
12 toString
13 ;
14
15 inherit (lib) lists strings trivial;
16
17 inherit (lib.lists) last;
18
19 /**
20 IPv6 addresses are 128-bit identifiers. The preferred form is 'x:x:x:x:x:x:x:x',
21 where the 'x's are one to four hexadecimal digits of the eight 16-bit pieces of
22 the address. See RFC 4291.
23 */
24 ipv6Bits = 128;
25 ipv6Pieces = 8; # 'x:x:x:x:x:x:x:x'
26 ipv6PieceBits = 16; # One piece in range from 0 to 0xffff.
27 ipv6PieceMaxValue = 65535; # 2^16 - 1
28in
29let
30 /**
31 Expand an IPv6 address by removing the "::" compression and padding them
32 with the necessary number of zeros. Converts an address from the string to
33 the list of strings which then can be parsed using `_parseExpanded`.
34 Throws an error when the address is malformed.
35
36 # Type: String -> [ String ]
37
38 # Example:
39
40 ```nix
41 expandIpv6 "2001:DB8::ffff"
42 => ["2001" "DB8" "0" "0" "0" "0" "0" "ffff"]
43 ```
44 */
45 expandIpv6 =
46 addr:
47 if match "^[0-9A-Fa-f:]+$" addr == null then
48 throw "${addr} contains malformed characters for IPv6 address"
49 else
50 let
51 pieces = strings.splitString ":" addr;
52 piecesNoEmpty = lists.remove "" pieces;
53 piecesNoEmptyLen = length piecesNoEmpty;
54 zeros = genList (_: "0") (ipv6Pieces - piecesNoEmptyLen);
55 hasPrefix = strings.hasPrefix "::" addr;
56 hasSuffix = strings.hasSuffix "::" addr;
57 hasInfix = strings.hasInfix "::" addr;
58 in
59 if addr == "::" then
60 zeros
61 else if
62 let
63 emptyCount = length pieces - piecesNoEmptyLen;
64 emptyExpected =
65 # splitString produces two empty pieces when "::" in the beginning
66 # or in the end, and only one when in the middle of an address.
67 if hasPrefix || hasSuffix then
68 2
69 else if hasInfix then
70 1
71 else
72 0;
73 in
74 emptyCount != emptyExpected
75 || (hasInfix && piecesNoEmptyLen >= ipv6Pieces) # "::" compresses at least one group of zeros.
76 || (!hasInfix && piecesNoEmptyLen != ipv6Pieces)
77 then
78 throw "${addr} is not a valid IPv6 address"
79 # Create a list of 8 elements, filling some of them with zeros depending
80 # on where the "::" was found.
81 else if hasPrefix then
82 zeros ++ piecesNoEmpty
83 else if hasSuffix then
84 piecesNoEmpty ++ zeros
85 else if hasInfix then
86 concatMap (piece: if piece == "" then zeros else [ piece ]) pieces
87 else
88 pieces;
89
90 /**
91 Parses an expanded IPv6 address (see `expandIpv6`), converting each part
92 from a string to an u16 integer. Returns an internal representation of IPv6
93 address (list of integers) that can be easily processed by other helper
94 functions.
95 Throws an error some element is not an u16 integer.
96
97 # Type: [ String ] -> IPv6
98
99 # Example:
100
101 ```nix
102 parseExpandedIpv6 ["2001" "DB8" "0" "0" "0" "0" "0" "ffff"]
103 => [8193 3512 0 0 0 0 0 65535]
104 ```
105 */
106 parseExpandedIpv6 =
107 addr:
108 assert lib.assertMsg (
109 length addr == ipv6Pieces
110 ) "parseExpandedIpv6: expected list of integers with ${ipv6Pieces} elements";
111 let
112 u16FromHexStr =
113 hex:
114 let
115 parsed = trivial.fromHexString hex;
116 in
117 if 0 <= parsed && parsed <= ipv6PieceMaxValue then
118 parsed
119 else
120 throw "0x${hex} is not a valid u16 integer";
121 in
122 map (piece: u16FromHexStr piece) addr;
123in
124let
125 /**
126 Parses an IPv6 address from a string to the internal representation (list
127 of integers).
128
129 # Type: String -> IPv6
130
131 # Example:
132
133 ```nix
134 parseIpv6FromString "2001:DB8::ffff"
135 => [8193 3512 0 0 0 0 0 65535]
136 ```
137 */
138 parseIpv6FromString = addr: parseExpandedIpv6 (expandIpv6 addr);
139in
140{
141 /**
142 Internally, an IPv6 address is stored as a list of 16-bit integers with 8
143 elements. Wherever you see `IPv6` in internal functions docs, it means that
144 it is a list of integers produced by one of the internal parsers, such as
145 `parseIpv6FromString`
146 */
147 _ipv6 = {
148 /**
149 Converts an internal representation of an IPv6 address (i.e, a list
150 of integers) to a string. The returned string is not a canonical
151 representation as defined in RFC 5952, i.e zeros are not compressed.
152
153 # Type: IPv6 -> String
154
155 # Example:
156
157 ```nix
158 parseIpv6FromString [8193 3512 0 0 0 0 0 65535]
159 => "2001:db8:0:0:0:0:0:ffff"
160 ```
161 */
162 toStringFromExpandedIp =
163 pieces: strings.concatMapStringsSep ":" (piece: strings.toLower (trivial.toHexString piece)) pieces;
164
165 /**
166 Extract an address and subnet prefix length from a string. The subnet
167 prefix length is optional and defaults to 128. The resulting address and
168 prefix length are validated and converted to an internal representation
169 that can be used by other functions.
170
171 # Type: String -> [ {address :: IPv6, prefixLength :: Int} ]
172
173 # Example:
174
175 ```nix
176 split "2001:DB8::ffff/32"
177 => {
178 address = [8193 3512 0 0 0 0 0 65535];
179 prefixLength = 32;
180 }
181 ```
182 */
183 split =
184 addr:
185 let
186 splitted = strings.splitString "/" addr;
187 splittedLength = length splitted;
188 in
189 if splittedLength == 1 then # [ ip ]
190 {
191 address = parseIpv6FromString addr;
192 prefixLength = ipv6Bits;
193 }
194 else if splittedLength == 2 then # [ ip subnet ]
195 {
196 address = parseIpv6FromString (head splitted);
197 prefixLength =
198 let
199 n = strings.toInt (last splitted);
200 in
201 if 1 <= n && n <= ipv6Bits then
202 n
203 else
204 throw "${addr} IPv6 subnet should be in range [1;${toString ipv6Bits}], got ${toString n}";
205 }
206 else
207 throw "${addr} is not a valid IPv6 address in CIDR notation";
208 };
209}