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, isSameSubnet } 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 if (
61 !isSameSubnet(rinfo.address, assignment.address, assignment.netmask)
62 ) {
63 return;
64 }
65
66 clearTimeout(timeout);
67 resolve(rinfo.address);
68 socket.close();
69 socket.unref();
70 }
71 );
72 socket.on('error', error => {
73 clearTimeout(timeout);
74 reject(error);
75 socket.close();
76 socket.unref();
77 });
78 socket.bind(DHCP_CLIENT_PORT, () => {
79 socket.setBroadcast(true);
80 socket.setSendBufferSize(packet.length);
81 socket.send(
82 packet,
83 0,
84 packet.length,
85 DHCP_SERVER_PORT,
86 broadcastAddress,
87 error => {
88 if (error) reject(error);
89 }
90 );
91 });
92 });
93};