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};