ef38059b9120
feat: probeSubnetForPort — handshake-free subnet probe for desktop pairing (v4.0.2)
- time
- 2026-06-17 12:19 UTC · 4 months ago
- machine
- koh2
- agent
- claude-opus-4-8
- session
- desktop helper pairing integration
- parent
- 13c7d70db28a
↓ peach-browser-ef38059b.tar.gzatlas at this save
files changed1 file
src/background/desktop-pairing-probe.ts+68 −0src/background/desktop-pairing-probe.ts+68 −0
@@ -0,0 +1,68 @@1+// Handshake-free /24 port probe for desktop pairing (Change 5).2+//3+// Unlike `probeSubnetForPeer` (chirp-sync-transport.ts), this does NOT run4+// Noise — during code pairing we have no long-term pubkey, and the pairing5+// server on port 49142 speaks the plaintext PEACH_HELLO protocol, not Noise6+// XX. This probe answers only "is a WebSocket server reachable here?"; the7+// 6-digit code + user-confirmed SAS are the actual trust boundary.8+9+const DEFAULT_PROBE_TIMEOUT_MS = 500;10+const DEFAULT_PROBE_BATCH = 16;11+12+export interface PortProbeOptions {13+ batch?: number;14+ timeoutMs?: number;15+ /** Injection seam for tests — defaults to the real WebSocket open check. */16+ probeOne?: (ip: string, port: number, timeoutMs: number) => Promise<boolean>;17+}18+19+/**20+ * Scan `subnet` (e.g. "192.168.1") for the first host accepting a WebSocket21+ * connection on `port`. Returns the IP string, or null if none answered.22+ */23+export async function probeSubnetForPort(24+ subnet: string,25+ port: number,26+ options: PortProbeOptions = {},27+): Promise<string | null> {28+ const batchSize = options.batch ?? DEFAULT_PROBE_BATCH;29+ const timeoutMs = options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;30+ const probe = options.probeOne ?? wsPortOpen;31+32+ for (let start = 1; start <= 254; start += batchSize) {33+ const end = Math.min(start + batchSize - 1, 254);34+ const ips: string[] = [];35+ for (let i = start; i <= end; i++) ips.push(`${subnet}.${i}`);36+37+ const results = await Promise.all(38+ ips.map(async (ip) => ((await probe(ip, port, timeoutMs)) ? ip : null)),39+ );40+ const hit = results.find((r): r is string => r !== null);41+ if (hit) return hit;42+ }43+ return null;44+}45+46+/** True if a WebSocket to ws://ip:port/ reaches onopen within timeoutMs. */47+function wsPortOpen(ip: string, port: number, timeoutMs: number): Promise<boolean> {48+ return new Promise((resolve) => {49+ let ws: WebSocket;50+ let settled = false;51+ const done = (ok: boolean) => {52+ if (settled) return;53+ settled = true;54+ try { ws.close(); } catch { /* ignore */ }55+ resolve(ok);56+ };57+ try {58+ ws = new WebSocket(`ws://${ip}:${port}/`);59+ } catch {60+ resolve(false);61+ return;62+ }63+ const t = setTimeout(() => done(false), timeoutMs);64+ ws.onopen = () => { clearTimeout(t); done(true); };65+ ws.onerror = () => { clearTimeout(t); done(false); };66+ ws.onclose = () => { clearTimeout(t); done(false); };67+ });68+}