Mirror: Best-effort discovery of the machine's local network using just Node.js dgram sockets
1import { randomBytes } from 'node:crypto';
2import { createSocket } from 'node:dgram';
3import { parseIpStr, toIpStr, parseMacStr } from './network';
4import type { NetworkAssignment } from './types';
5
6class DHCPTimeoutError extends TypeError {
7 code = 'ETIMEDOUT';
8}
9
10const computeBroadcastAddress = (assignment: NetworkAssignment) => {
11 const address = parseIpStr(assignment.address);
12 const netmask = parseIpStr(assignment.netmask);
13 return toIpStr(address | ~netmask);
14};
15
16const dhcpDiscoverPacket = (macStr: string) => {
17 const MAC_ADDRESS = new Uint8Array(16);
18 MAC_ADDRESS.set(parseMacStr(macStr));
19 const packet = new Uint8Array(244);
20 const XID = randomBytes(4);
21 packet[0] = 1; // op = request
22 packet[1] = 1; // hw_type = ethernet
23 packet[2] = 6; // hw_len = ethernet
24 packet[3] = 0; // hops = 0
25 packet.set(XID, 4);
26 // elapsed = 0 seconds [2 bytes]
27 packet[10] = 0x80; // flags = broadcast discovery [2 bytes]
28 // client IP = null [4 bytes]
29 // own IP = null [4 bytes]
30 // server IP = null [4 bytes]
31 // gateway IP = null [4 bytes]
32 packet.set(MAC_ADDRESS, 28);
33 // sname = null [64 bytes]
34 // boot file = null [128 bytes]
35 packet.set([0x63, 0x82, 0x53, 0x63], 236); // Magic cookie
36 packet.set([0x35, 0x01, 0x01, 0xff], 240); // Trailer
37 return packet;
38};
39
40const DHCP_TIMEOUT = 250;
41const DHCP_CLIENT_PORT = 68;
42const DHCP_SERVER_PORT = 67;
43
44export const dhcpDiscover = (
45 assignment: NetworkAssignment
46): Promise<string> => {
47 return new Promise((resolve, reject) => {
48 const broadcastAddress = computeBroadcastAddress(assignment);
49 const packet = dhcpDiscoverPacket(assignment.mac);
50 const timeout = setTimeout(() => {
51 reject(
52 new DHCPTimeoutError(
53 `Received no reply to DHCPDISCOVER in ${DHCP_TIMEOUT}ms`
54 )
55 );
56 }, DHCP_TIMEOUT);
57 const socket = createSocket(
58 { type: 'udp4', reuseAddr: true },
59 (_msg, rinfo) => {
60 clearTimeout(timeout);
61 resolve(rinfo.address);
62 socket.close();
63 socket.unref();
64 }
65 );
66 socket.on('error', error => {
67 clearTimeout(timeout);
68 reject(error);
69 socket.close();
70 socket.unref();
71 });
72 socket.bind(DHCP_CLIENT_PORT, assignment.address, () => {
73 socket.setBroadcast(true);
74 socket.setSendBufferSize(packet.length);
75 socket.send(
76 packet,
77 0,
78 packet.length,
79 DHCP_SERVER_PORT,
80 broadcastAddress,
81 error => {
82 if (error) reject(error);
83 }
84 );
85 });
86 });
87};