All files / net / unstable_ip.ts

100.00% Branches 86/86
100.00% Lines 188/188
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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x6
x91
 
x91
x91
x91
x375
x375
x91
 
x91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x6
 
x29
 
x72
 
 
 
x29
 
x34
 
x38
x34
 
 
x37
x37
 
 
x29
x85
x85
x139
x139
 
x51
x51
x51
x203
x203
x29
 
x29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x6
x25
x26
x26
 
x25
x57
x63
x63
x57
 
x37
x25
 
x38
 
x38
x43
x43
 
 
x65
x38
x38
x38
x38
x38
x38
x40
x40
 
 
x63
x63
 
 
x38
x45
x45
 
 
x38
x81
x68
x73
x73
x38
 
x25
x25
x25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x6
x27
 
x27
x27
x29
x29
 
x27
x27
x27
x27
x27
x27
x28
x28
 
x27
x29
x29
 
 
x27
x28
x28
 
x42
x42
 
x27
x28
x28
 
x41
 
x41
x41
x41
x41
x41
x41
x41
x41
 
x41
x27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x6
x31
 
x31
x31
x34
x34
 
x31
x31
x31
x31
x31
x31
x32
x32
 
x31
x34
x34
 
x31
x32
x32
 
x48
x48
 
x31
x35
x35
 
x44
x44
 
x44
x44
 
x31
x117
x123
x123
x117
 
x31
x32
x32
x32
x32
x32
 
x37
x31
 
x40
x40
x45
x45
x45
x46
x46
x49
x45
x48
x48
x46
x46
x46
x46
x46
x46
 
x70
 
 
x40
x68
x68
x68
x68
 
x68
x68
x68
x68
x68
 
 
x70
x70
x70
x70
x40
 
x32
x32
x32
 
x32
x227
x227
x227
 
x32
x32




























































































































































































































































































































































// Copyright 2018-2025 the Deno authors. MIT license.

/**
 * Validates whether a given string is a valid IPv4 address.
 *
 * @experimental **UNSTABLE**: New API, yet to be vetted.
 *
 * @param addr IPv4 address in a string format (e.g., "192.168.0.1").
 * @returns A boolean indicating if the string is a valid IPv4 address.
 *
 * @example Check if the address is a IPv4
 * ```ts
 * import { isIPv4 } from "@std/net/unstable-ip"
 * import { assert, assertFalse } from "@std/assert"
 *
 * const correctIp = "192.168.0.1"
 * const incorrectIp = "192.168.0.256"
 *
 * assert(isIPv4(correctIp))
 * assertFalse(isIPv4(incorrectIp))
 * ```
 */
export function isIPv4(addr: string): boolean {
  const octets = addr.split(".");

  return (
    octets.length === 4 &&
    octets.every((octet) => {
      const n = Number(octet);
      return n >= 0 && n <= 255 && !isNaN(n);
    })
  );
}

/**
 * Validates whether a given string is a IPv6 address.
 *
 * @experimental **UNSTABLE**: New API, yet to be vetted.
 *
 * @param addr IPv6 address in a string format (e.g., "2001:db8::1").
 * @returns A boolean indicating if the string is a valid IPv6 address.
 *
 * @example Check if the address is a IPv6
 * ```ts
 * import { isIPv6 } from "@std/net/unstable-ip"
 * import { assert, assertFalse } from "@std/assert"
 *
 * const correctIp = "2001::db8:0:1"
 * const incorrectIp = "2001::db8::1"
 *
 * assert(isIPv6(correctIp))
 * assertFalse(isIPv6(incorrectIp))
 * ```
 */
export function isIPv6(addr: string): boolean {
  // more than one use of ::
  if (addr.split("::").length > 2) return false;

  const hextets = addr.split(":");

  // x:x:x:x:x:x:d.d.d.d (https://www.rfc-editor.org/rfc/rfc4291#section-2.2)
  // check if has ipv4 on
  if (addr.includes(".")) {
    // is just an ipv4
    if (hextets.length === 1) return false;

    const last = hextets.pop();
    if (!last || !isIPv4(last)) return false;

    // just to maintain the length to 8
    hextets.push("");
  }

  // expand ::
  while (hextets.length < 8) {
    const idx = hextets.indexOf("");
    if (idx === -1) break;
    hextets.splice(idx, 0, "");
  }

  return (
    hextets.length === 8 &&
    hextets.every((hextet) => {
      const n = hextet === "" ? 0 : parseInt(hextet, 16);
      return n >= 0 && n <= 65535 && !isNaN(n);
    })
  );
}

/**
 * Checks if an IP address matches a subnet or specific IP address.
 *
 * @experimental **UNSTABLE**: New API, yet to be vetted.
 *
 * @param addr The IP address to check (IPv4 or IPv6)
 * @param subnetOrIps The subnet in CIDR notation (e.g., "192.168.1.0/24") or a specific IP address
 * @returns true if the IP address matches the subnet or IP, false otherwise
 * @example Check if the address is a IPv6
 *
 * ```ts
 * import { matchSubnets } from "@std/net/unstable-ip"
 * import { assert, assertFalse } from "@std/assert"
 *
 * assert(matchSubnets("192.168.1.10", ["192.168.1.0/24"]));
 * assertFalse(matchSubnets("192.168.2.10", ["192.168.1.0/24"]));
 *
 * assert(matchSubnets("2001:db8::ffff", ["2001:db8::/64"]));
 * assertFalse(matchSubnets("2001:db9::1", ["2001:db8::/64"]));
 * ```
 */
export function matchSubnets(addr: string, subnetOrIps: string[]): boolean {
  if (!isValidIP(addr)) {
    return false;
  }

  for (const subnetOrIp of subnetOrIps) {
    if (matchSubnet(addr, subnetOrIp)) {
      return true;
    }
  }

  return false;
}

function matchSubnet(addr: string, subnet: string): boolean {
  // If the subnet doesn't contain "/", treat it as a specific IP address
  if (!subnet.includes("/")) {
    return addr === subnet;
  }

  // Parse subnet into IP address and prefix length
  const [subnetIP, prefixLengthStr] = subnet.split("/");
  if (
    !subnetIP ||
    subnetIP === "" ||
    !prefixLengthStr ||
    prefixLengthStr === ""
  ) {
    return false;
  }

  // Check if both IP and subnet are the same type (IPv4 or IPv6)
  const ipIsV4 = isIPv4(addr);
  const subnetIsV4 = isIPv4(subnetIP);

  // IP and subnet must be the same version (both IPv4 or both IPv6)
  if (ipIsV4 !== subnetIsV4) {
    return false;
  }

  // Delegate to the appropriate subnet matching function
  if (ipIsV4) {
    return matchIPv4Subnet(addr, subnet);
  } else {
    return matchIPv6Subnet(addr, subnet);
  }
}

function isValidIP(ip: string): boolean {
  return isIPv4(ip) || isIPv6(ip);
}

/**
 * Checks if an IPv4 address matches a subnet or specific IPv4 address.
 *
 * @experimental **UNSTABLE**: New API, yet to be vetted.
 *
 * @param addr The IP address to check (IPv4)
 * @param subnet The subnet in CIDR notation (e.g., "192.168.1.0/24") or a specific IP address
 * @returns true if the IP address matches the subnet or IP, false otherwise
 * @example Check if the address is a IPv6
 *
 * ```ts
 * import { matchIPv4Subnet } from "@std/net/unstable-ip"
 * import { assert, assertFalse } from "@std/assert"
 *
 * assert(matchIPv4Subnet("192.168.1.10", "192.168.1.0/24"));
 * assertFalse(matchIPv4Subnet("192.168.2.10", "192.168.1.0/24"));
 * ```
 */
export function matchIPv4Subnet(addr: string, subnet: string): boolean {
  const [subnetIP, prefixLengthStr] = subnet.split("/");

  const prefix = parseInt(prefixLengthStr!, 10);
  if (isNaN(prefix)) {
    return false;
  }

  if (
    !subnetIP ||
    subnetIP === "" ||
    !prefixLengthStr ||
    prefixLengthStr === ""
  ) {
    return false;
  }

  if (prefix < 0 || prefix > 32) {
    return false;
  }

  // Special case: /0 matches all IPv4 addresses
  if (prefix === 0) {
    return true;
  }

  const ipBytes = addr.split(".").map(Number);
  const subnetBytes = subnetIP.split(".").map(Number);

  if (ipBytes.length !== 4 || subnetBytes.length !== 4) {
    return false;
  }

  const mask = (0xffffffff << (32 - prefix)) >>> 0;

  const ipInt = (ipBytes[0]! << 24) |
    (ipBytes[1]! << 16) |
    (ipBytes[2]! << 8) |
    ipBytes[3]!;
  const subnetInt = (subnetBytes[0]! << 24) |
    (subnetBytes[1]! << 16) |
    (subnetBytes[2]! << 8) |
    subnetBytes[3]!;

  return ((ipInt >>> 0) & mask) === ((subnetInt >>> 0) & mask);
}

/**
 * Checks if an IPv6 address matches a subnet or specific IPv6 address.
 *
 * @experimental **UNSTABLE**: New API, yet to be vetted.
 *
 * @param addr The IP address to check (IPv6)
 * @param subnet The subnet in CIDR notation (e.g., "2001:db8::/64") or a specific IP address
 * @returns true if the IP address matches the subnet or IP, false otherwise
 * @example Check if the address is a IPv6
 *
 * ```ts
 * import { matchIPv6Subnet } from "@std/net/unstable-ip"
 * import { assert, assertFalse } from "@std/assert"
 *
 * assert(matchIPv6Subnet("2001:db8::ffff", "2001:db8::/64"));
 * assertFalse(matchIPv6Subnet("2001:db9::1", "2001:db8::/64"));
 * ```
 */
export function matchIPv6Subnet(addr: string, subnet: string): boolean {
  const [subnetIP, prefixLengthStr] = subnet.split("/");

  const prefix = parseInt(prefixLengthStr!, 10);
  if (isNaN(prefix)) {
    return false;
  }

  if (
    !subnetIP ||
    subnetIP === "" ||
    !prefixLengthStr ||
    prefixLengthStr === ""
  ) {
    return false;
  }

  if (prefix < 0 || prefix > 128) {
    return false;
  }

  if (prefix === 0) {
    return true;
  }

  const ipExpanded = expandIPv6(addr);
  const subnetExpanded = expandIPv6(subnetIP);

  if (!ipExpanded || !subnetExpanded) {
    return false;
  }

  const ipBytes = ipv6ToBytes(ipExpanded);
  const subnetBytes = ipv6ToBytes(subnetExpanded);

  const fullBytes = Math.floor(prefix / 8);
  const remainingBits = prefix % 8;

  for (let i = 0; i < fullBytes; i++) {
    if (ipBytes[i] !== subnetBytes[i]) {
      return false;
    }
  }

  if (remainingBits > 0) {
    const mask = 0xff << (8 - remainingBits);
    const ipByte = ipBytes[fullBytes]!;
    const subnetByte = subnetBytes[fullBytes]!;
    return (ipByte & mask) === (subnetByte & mask);
  }

  return true;
}

function expandIPv6(addr: string): string | null {
  if (addr.includes(".")) {
    const parts = addr.split(":");
    const ipv4Part = parts.pop();
    if (!ipv4Part) {
      return null;
    }
    const ipv4Bytes = ipv4Part!.split(".").map(Number);
    if (ipv4Bytes.length !== 4) {
      return null;
    }
    const ipv4Hex =
      ((ipv4Bytes[0]! << 8) | ipv4Bytes[1]!).toString(16).padStart(4, "0") +
      ":" +
      ((ipv4Bytes[2]! << 8) | ipv4Bytes[3]!).toString(16).padStart(4, "0");
    addr = parts.join(":") + ":" + ipv4Hex;
  }

  let expanded = addr;

  // Handle ::
  if (expanded.includes("::")) {
    const parts = expanded.split("::");
    const leftParts = parts[0] ? parts[0].split(":") : [];
    const rightParts = parts[1] ? parts[1].split(":") : [];
    const missingParts = 8 - leftParts.length - rightParts.length;

    expanded = leftParts
      .concat(new Array(missingParts).fill("0"))
      .concat(rightParts)
      .join(":");
  }

  // Pad each hextet to 4 digits
  return expanded
    .split(":")
    .map((hextet) => hextet.padStart(4, "0"))
    .join(":");
}

function ipv6ToBytes(expandedIPv6: string): number[] {
  const hextets = expandedIPv6.split(":");
  const bytes: number[] = [];

  for (const hextet of hextets) {
    const value = parseInt(hextet, 16);
    bytes.push((value >> 8) & 0xff, value & 0xff);
  }

  return bytes;
}