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