File size: 2,220 Bytes
4e23b01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import net from 'node:net';

export type BindClass = 'loopback' | 'lan' | 'public';

export interface ClassifyOptions {
  readonly bindClass?: 'lan' | 'public';
}

function ipv4ToInt(ip: string): number {
  const [a, b, c, d] = ip.split('.');
  return (
    (((Number(a) << 24) >>> 0) +
      ((Number(b) << 16) >>> 0) +
      ((Number(c) << 8) >>> 0) +
      (Number(d) >>> 0)) >>>
    0
  );
}

function ipv4InCidr(ip: string, base: string, prefix: number): boolean {
  const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
  return ((ipv4ToInt(ip) & mask) >>> 0) === ((ipv4ToInt(base) & mask) >>> 0);
}

function expandV6(host: string): readonly string[] | null {
  const lower = host.toLowerCase();
  if (lower.includes('::')) {
    const halves = lower.split('::');
    const leftRaw = halves[0] ?? '';
    const rightRaw = halves[1] ?? '';
    const left = leftRaw.length > 0 ? leftRaw.split(':') : [];
    const right = rightRaw.length > 0 ? rightRaw.split(':') : [];
    const missing = 8 - (left.length + right.length);
    if (missing < 0) return null;
    return [...left, ...Array<string>(missing).fill('0'), ...right];
  }
  const parts = lower.split(':');
  return parts.length === 8 ? parts : null;
}

function isLinkLocalV6(host: string): boolean {
  const groups = expandV6(host);
  if (groups === null) return false;
  const first = Number.parseInt(groups[0] ?? '', 16);
  return first >= 0xfe80 && first <= 0xfebf;
}

export function classify(host: string, opts?: ClassifyOptions): BindClass {
  if (host === '' || host === '0.0.0.0' || host === '::') {
    return opts?.bindClass ?? 'public';
  }
  if (host === 'localhost') {
    return 'loopback';
  }
  const family = net.isIP(host);
  if (family === 4) {
    if (host.startsWith('127.')) return 'loopback';
    if (ipv4InCidr(host, '10.0.0.0', 8)) return 'lan';
    if (ipv4InCidr(host, '172.16.0.0', 12)) return 'lan';
    if (ipv4InCidr(host, '192.168.0.0', 16)) return 'lan';
    if (ipv4InCidr(host, '169.254.0.0', 16)) return 'lan';
    return 'public';
  }
  if (family === 6) {
    if (host.toLowerCase() === '::1') return 'loopback';
    if (isLinkLocalV6(host)) return 'lan';
    return 'public';
  }
  return 'public';
}