anydrop/web/src/lib/localIP.ts
ordinarthur 99a182a831
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 41s
feat: LAN detection fallback via WebRTC local IP
iCloud Private Relay hides iPhones' real public IP, breaking
IP-based LAN grouping. Devices now detect their local IP
(192.168.x.x) via WebRTC ICE candidates and send it to the
server. The server groups devices by local subnet as a fallback
when public IPs differ.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 12:27:36 +02:00

53 lines
1.3 KiB
TypeScript

/**
* 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<string | undefined> {
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)
);
}