desktop-pairing-probe.ts

2.4 KB · raw

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.89const DEFAULT_PROBE_TIMEOUT_MS = 500;10const DEFAULT_PROBE_BATCH = 16;1112export 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}1819/**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 */23export 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;3132  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}`);3637    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}4546/** True if a WebSocket to ws://ip:port/ reaches onopen within timeoutMs. */47function 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}