/** * Detect the local LAN IP (192.168.x.x, 10.x.x.x, 172.16-31.x.x) * using WebRTC ICE candidate gathering. * * Returns the first private IPv4 found, or undefined if none detected * (e.g. browser blocks mDNS candidates). */ export async function detectLocalIP(timeoutMs = 3000): Promise { return new Promise((resolve) => { let resolved = false; const done = (ip?: string) => { if (resolved) return; resolved = true; pc.close(); clearTimeout(timer); resolve(ip); }; const timer = setTimeout(() => done(undefined), timeoutMs); const pc = new RTCPeerConnection({ iceServers: [{ urls: "stun:stun.l.google.com:19302" }], }); pc.createDataChannel(""); pc.onicecandidate = (event) => { if (!event.candidate?.candidate) return; const match = event.candidate.candidate.match( /(?:srflx|host)\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s/, ); if (match) { const ip = match[1]; if (isPrivateIPv4(ip)) { done(ip); } } }; pc.createOffer() .then((offer) => pc.setLocalDescription(offer)) .catch(() => done(undefined)); }); } function isPrivateIPv4(ip: string): boolean { return ( ip.startsWith("192.168.") || ip.startsWith("10.") || /^172\.(1[6-9]|2\d|3[01])\./.test(ip) ); }