Mirror: Best-effort discovery of the machine's local network using just Node.js dgram sockets
1import { spawnSync } from 'child_process';
2import { dhcpDiscover } from './dhcp';
3import { probeDefaultRoute } from './route';
4import { interfaceAssignments, matchAssignment } from './network';
5import type { GatewayAssignment } from './types';
6
7const DEFAULT_ASSIGNMENT: GatewayAssignment = {
8 iname: 'lo0',
9 address: '127.0.0.1',
10 netmask: '255.0.0.0',
11 family: 'IPv4',
12 mac: '00:00:00:00:00:00',
13 internal: true,
14 cidr: '127.0.0.1/8',
15 gateway: null,
16};
17
18export async function lanNetwork(): Promise<GatewayAssignment> {
19 // Get IPv4 network assignments, sorted by:
20 // - external first
21 // - LAN-reserved IP range priority
22 // - address value
23 const assignments = interfaceAssignments();
24 if (!assignments.length) {
25 // If we have no assignments (which shouldn't ever happen, we make up a loopback interface)
26 return DEFAULT_ASSIGNMENT;
27 }
28
29 let assignment: GatewayAssignment | null;
30
31 // First, we attempt to probe the default route to a publicly routed IP
32 // This will generally fail if there's no route, e.g. if the network is offline
33 try {
34 const defaultRoute = await probeDefaultRoute();
35 // If this route matches a known assignment, return it without a gateway
36 if ((assignment = matchAssignment(assignments, defaultRoute))) {
37 return assignment;
38 }
39 } catch {
40 // Ignore errors, since we have a fallback method
41 }
42
43 // Second, attempt to discover a gateway's DHCP network
44 // Because without a gateway we won't get a reply, we do this in parallel
45 const discoveries = await Promise.allSettled(
46 assignments.map(assignment => {
47 // For each assignment, we send a DHCPDISCOVER packet to its network mask
48 return dhcpDiscover(assignment);
49 })
50 );
51 for (const discovery of discoveries) {
52 // The first discovered gateway is returned, if it matches an assignment
53 if (discovery.status === 'fulfilled' && discovery.value) {
54 const dhcpRoute = discovery.value;
55 if ((assignment = matchAssignment(assignments, dhcpRoute))) {
56 return assignment;
57 }
58 }
59 }
60
61 // As a fallback, we choose the first assignment, since they're ordered by likely candidates
62 // This may return 127.0.0.1, typically as a last resort
63 return { ...assignments[0], gateway: null };
64}
65
66export function lanNetworkSync(): GatewayAssignment {
67 const subprocessPath = require.resolve('lan-network/subprocess');
68 const { error, status, stdout } = spawnSync(
69 process.execPath,
70 [subprocessPath],
71 {
72 shell: false,
73 timeout: 500,
74 encoding: 'utf8',
75 windowsVerbatimArguments: false,
76 windowsHide: true,
77 }
78 );
79 if (status || error) {
80 return DEFAULT_ASSIGNMENT;
81 } else if (!status && typeof stdout === 'string') {
82 const json = JSON.parse(stdout.trim()) as GatewayAssignment;
83 return typeof json === 'object' && json && 'address' in json
84 ? json
85 : DEFAULT_ASSIGNMENT;
86 } else {
87 return DEFAULT_ASSIGNMENT;
88 }
89}