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
1 file src/background/desktop-pairing-probe.ts+68 −0
src/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+}

view file