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