/* * UDP multicast discovery — streaming callback API. * * Announcement wire format (matches discovery.c): * [u16 msg_type=0x0010][u32 payload_len] <- 6-byte frame header * [u8 protocol_version] * [u16 site_id] * [u16 tcp_port] * [u16 function_flags] * [u8 name_len] * [bytes name...] */ import dgram from 'node:dgram'; const MULTICAST_GROUP = '224.0.0.251'; const DISCOVERY_PORT = 5353; const ANNOUNCE_TYPE = 0x0010; const HEADER_SIZE = 6; const ANN_FIXED_SIZE = 8; /* * Open a discovery socket, send an immediate announce to prompt replies, * and call on_peer for each newly-seen peer (deduplicated by addr+name). * * Returns a stop() function. Call it when done (e.g. user closed the picker * or selected a node). Safe to call multiple times. */ export function start_discovery(on_peer, on_error) { const sock = dgram.createSocket({ type: 'udp4', reuseAddr: true }); const seen = new Set(); let closed = false; sock.on('error', err => { console.error('discovery socket error:', err.message); if (on_error) { on_error(err); } stop(); }); sock.on('message', (msg, rinfo) => { if (msg.length < HEADER_SIZE) { return; } const msg_type = msg.readUInt16LE(0); const payload_len = msg.readUInt32LE(2); if (msg_type !== ANNOUNCE_TYPE) { return; } if (msg.length < HEADER_SIZE + payload_len) { return; } if (payload_len < ANN_FIXED_SIZE) { return; } const p = msg.slice(HEADER_SIZE); const site_id = p.readUInt16LE(1); const tcp_port = p.readUInt16LE(3); const func_flags = p.readUInt16LE(5); const name_len = p.readUInt8(7); if (payload_len < ANN_FIXED_SIZE + name_len) { return; } const name = p.toString('utf8', 8, 8 + name_len); const key = `${rinfo.address}:${name}`; if (seen.has(key)) { return; } seen.add(key); on_peer({ addr: rinfo.address, tcp_port, site_id, function_flags: func_flags, name }); }); sock.bind(DISCOVERY_PORT, () => { sock.addMembership(MULTICAST_GROUP); send_announce(sock); }); function stop() { if (closed) { return; } closed = true; try { sock.close(); } catch {} } return stop; } function send_announce(sock) { const name = Buffer.from('web-inspector', 'utf8'); const payload_len = 8 + name.length; const buf = Buffer.allocUnsafe(6 + payload_len); buf.writeUInt16LE(ANNOUNCE_TYPE, 0); buf.writeUInt32LE(payload_len, 2); buf.writeUInt8(1, 6); /* protocol_version */ buf.writeUInt16LE(0, 7); /* site_id */ buf.writeUInt16LE(0, 9); /* tcp_port (0 = no server) */ buf.writeUInt16LE(0, 11); /* function_flags */ buf.writeUInt8(name.length, 13); name.copy(buf, 14); sock.send(buf, 0, buf.length, DISCOVERY_PORT, MULTICAST_GROUP); }