Mirror: Best-effort discovery of the machine's local network using just Node.js dgram sockets

feat: Add CLI for testing (#3)

* Remove default assignment

* Add test CLI

* Add changeset

+5
.changeset/plenty-ears-laugh.md
···
+
---
+
'lan-network': patch
+
---
+
+
Add CLI for testing
+15
cli/package.json
···
+
{
+
"name": "lan-network-cli",
+
"private": true,
+
"version": "0.0.0",
+
"main": "../dist/lan-network-cli.js",
+
"types": "../dist/lan-network-cli.d.ts",
+
"source": "../src/cli.ts",
+
"exports": {
+
".": {
+
"types": "../dist/lan-network-cli.d.ts",
+
"require": "../dist/lan-network-cli.js",
+
"source": "../src/cli.ts"
+
}
+
}
+
}
+8
package.json
···
"changeset:version": "changeset version && pnpm install --lockfile-only",
"changeset:publish": "changeset publish"
},
+
"bin": {
+
"lan-network": "./dist/lan-network-cli.js"
+
},
"exports": {
".": {
"types": "./dist/lan-network.d.ts",
···
"types": "./dist/lan-network-subprocess.d.ts",
"require": "./dist/lan-network-subprocess.js",
"source": "./src/subprocess.ts"
+
},
+
"./cli": {
+
"types": "./dist/lan-network-cli.d.ts",
+
"require": "./dist/lan-network-cli.js",
+
"source": "./src/cli.ts"
},
"./package.json": "./package.json"
},
+149
src/cli.ts
···
+
#!/usr/bin/env node
+
+
import type { GatewayAssignment } from './types';
+
import {
+
DEFAULT_ASSIGNMENT,
+
interfaceAssignments,
+
matchAssignment,
+
} from './network';
+
import { probeDefaultRoute } from './route';
+
import { dhcpDiscover } from './dhcp';
+
import { lanNetwork } from './index';
+
+
type Mode = 'help' | 'dhcp' | 'probe' | 'fallback' | 'default';
+
+
function help() {
+
const output = [
+
"Discover the machine's default gateway and local network IP (test utility)",
+
'',
+
'Usage',
+
' $ lan-network',
+
' $ lan-network --default',
+
'',
+
'Modes',
+
' --probe Discover gateway via UDP4 socket to publicly routed address',
+
' --dhcp Discover gateway via DHCPv4 discover broadcast',
+
' --fallback Return highest-priority IPv4 network interface assignment',
+
' --default Try the three above modes in order',
+
' --help Print help output',
+
].join('\n');
+
console.log(output);
+
}
+
+
async function dhcp() {
+
const assignments = interfaceAssignments();
+
if (!assignments.length) {
+
console.error('No available network interface assignments');
+
process.exit(1);
+
}
+
const discoveries = await Promise.allSettled(
+
assignments.map(assignment => {
+
// For each assignment, we send a DHCPDISCOVER packet to its network mask
+
return dhcpDiscover(assignment);
+
})
+
);
+
console.log(discoveries);
+
let assignment: GatewayAssignment | null = null;
+
for (const discovery of discoveries) {
+
// The first discovered gateway is returned, if it matches an assignment
+
if (discovery.status === 'fulfilled' && discovery.value) {
+
const dhcpRoute = discovery.value;
+
if ((assignment = matchAssignment(assignments, dhcpRoute))) {
+
break;
+
}
+
}
+
}
+
if (assignment && assignment !== DEFAULT_ASSIGNMENT) {
+
console.log(JSON.stringify(assignment, null, 2));
+
process.exit(0);
+
} else {
+
console.error('No DHCP router was discoverable');
+
process.exit(1);
+
}
+
}
+
+
async function probe() {
+
const assignments = interfaceAssignments();
+
if (!assignments.length) {
+
console.error('No available network interface assignments');
+
process.exit(1);
+
}
+
try {
+
const defaultRoute = await probeDefaultRoute();
+
const assignment = matchAssignment(assignments, defaultRoute);
+
if (assignment && assignment !== DEFAULT_ASSIGNMENT) {
+
console.log(JSON.stringify(assignment, null, 2));
+
process.exit(0);
+
} else {
+
console.error('No default gateway or route');
+
process.exit(1);
+
}
+
} catch (error) {
+
console.error('No default gateway or route');
+
console.error(error);
+
process.exit(1);
+
}
+
}
+
+
async function fallback() {
+
const assignments = interfaceAssignments();
+
if (!assignments.length) {
+
console.error('No available network interface assignments');
+
process.exit(1);
+
}
+
const assignment = { ...assignments[0], gateway: null };
+
console.log(JSON.stringify(assignment, null, 2));
+
process.exit(0);
+
}
+
+
async function main() {
+
const assignment = await lanNetwork();
+
if (assignment !== DEFAULT_ASSIGNMENT) {
+
console.log(JSON.stringify(assignment, null, 2));
+
process.exit(0);
+
} else {
+
console.error('No default gateway, route, or DHCP router');
+
process.exit(1);
+
}
+
}
+
+
function cli() {
+
let mode: Mode = 'default';
+
parseArgs: for (let i = 1; i < process.argv.length; i++) {
+
const arg = process.argv[i].trim().toLowerCase();
+
switch (arg) {
+
case '-h':
+
case '--help':
+
mode = 'help';
+
break parseArgs;
+
case '-d':
+
case '--dhcp':
+
mode = 'dhcp';
+
break;
+
case '-p':
+
case '--probe':
+
mode = 'probe';
+
break;
+
case '-f':
+
case '--fallback':
+
mode = 'fallback';
+
break;
+
default:
+
if (arg.startsWith('-')) throw new TypeError(`Invalid flag: ${arg}`);
+
}
+
}
+
switch (mode) {
+
case 'help':
+
return help();
+
case 'dhcp':
+
return dhcp();
+
case 'probe':
+
return probe();
+
case 'fallback':
+
return fallback();
+
case 'default':
+
return main();
+
}
+
}
+
+
cli();
+1 -1
src/dhcp.ts
···
socket.close();
socket.unref();
});
-
socket.bind(DHCP_CLIENT_PORT, assignment.address, () => {
+
socket.bind(DHCP_CLIENT_PORT, () => {
socket.setBroadcast(true);
socket.setSendBufferSize(packet.length);
socket.send(
+5 -12
src/index.ts
···
import { spawnSync } from 'child_process';
import { dhcpDiscover } from './dhcp';
import { probeDefaultRoute } from './route';
-
import { interfaceAssignments, matchAssignment } from './network';
+
import {
+
DEFAULT_ASSIGNMENT,
+
interfaceAssignments,
+
matchAssignment,
+
} from './network';
import type { GatewayAssignment } from './types';
-
-
const DEFAULT_ASSIGNMENT: GatewayAssignment = {
-
iname: 'lo0',
-
address: '127.0.0.1',
-
netmask: '255.0.0.0',
-
family: 'IPv4',
-
mac: '00:00:00:00:00:00',
-
internal: true,
-
cidr: '127.0.0.1/8',
-
gateway: null,
-
};
export async function lanNetwork(): Promise<GatewayAssignment> {
// Get IPv4 network assignments, sorted by:
+11
src/network.ts
···
import os from 'node:os';
import type { GatewayAssignment, NetworkAssignment } from './types';
+
export const DEFAULT_ASSIGNMENT: GatewayAssignment = {
+
iname: 'lo0',
+
address: '127.0.0.1',
+
netmask: '255.0.0.0',
+
family: 'IPv4',
+
mac: '00:00:00:00:00:00',
+
internal: true,
+
cidr: '127.0.0.1/8',
+
gateway: null,
+
};
+
export const parseMacStr = (macStr: string): number[] =>
macStr
.split(':')