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