[object Object]

← back to Domain Sniper

step 2: watch-outbound.js — HTTPS forward proxy to identify the leaking tool

f13a4f9bfb64d59c913f02840030540481f53308 · 2026-05-12 13:16:54 -0700 · Steve Abrams

Local proxy on 127.0.0.1:8088. Set HTTPS_PROXY env, re-run the suspected
tool, and every domain-intel hostname it contacts gets surfaced in red.
SNI-only logging — no TLS interception or CA cert install needed. Catches
the host (api.godaddy.com / whoisxmlapi.com / etc.) which is enough to ID
which tool is leaking. Companion tcpdump one-liner in the file header
covers the raw port-43 whois case.

Files touched

Diff

commit f13a4f9bfb64d59c913f02840030540481f53308
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue May 12 13:16:54 2026 -0700

    step 2: watch-outbound.js — HTTPS forward proxy to identify the leaking tool
    
    Local proxy on 127.0.0.1:8088. Set HTTPS_PROXY env, re-run the suspected
    tool, and every domain-intel hostname it contacts gets surfaced in red.
    SNI-only logging — no TLS interception or CA cert install needed. Catches
    the host (api.godaddy.com / whoisxmlapi.com / etc.) which is enough to ID
    which tool is leaking. Companion tcpdump one-liner in the file header
    covers the raw port-43 whois case.
---
 watch-outbound.js | 178 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 178 insertions(+)

diff --git a/watch-outbound.js b/watch-outbound.js
new file mode 100644
index 0000000..7e4a01f
--- /dev/null
+++ b/watch-outbound.js
@@ -0,0 +1,178 @@
+#!/usr/bin/env node
+// watch-outbound.js — local HTTPS forward proxy that surfaces every domain-intel
+// request leaving THIS machine. Same MITM technology aggregators use to monitor
+// incoming WHOIS queries — but inverted so YOU see what YOUR tools send.
+//
+// Run it:
+//   node watch-outbound.js
+//
+// Then point your tools at it:
+//   export HTTPS_PROXY=http://127.0.0.1:8088
+//   export HTTP_PROXY=http://127.0.0.1:8088
+//   export NO_PROXY=localhost,127.0.0.1
+//
+// ...and re-run whatever you suspect is leaking (Claude with the domain-suite MCP,
+// a node script, a `curl` against an availability API, etc.). Hits print to stdout
+// and append to data/outbound.jsonl with full timestamps for forensics.
+//
+// What this catches:
+//   - Every HTTPS request from any tool that respects HTTPS_PROXY (most Node/Python clients do)
+//   - SNI hostname is visible even without TLS decryption — enough to ID the leaking tool
+//
+// What this does NOT catch:
+//   - Raw `whois` command (port 43). For that, run in another terminal:
+//       sudo tcpdump -i any -nn -A 'port 43' -l
+//   - Tools that ignore HTTPS_PROXY (some Go binaries, browsers w/ own proxy settings)
+//   - DNS queries to OS resolver (use `sudo tcpdump -i any -nn 'udp port 53' -l`)
+
+const http = require('http');
+const net = require('net');
+const fs = require('fs');
+const path = require('path');
+const { URL } = require('url');
+
+const PORT = parseInt(process.env.WATCH_PORT || '8088', 10);
+const DATA_DIR = path.join(__dirname, 'data');
+const LOG_FILE = path.join(DATA_DIR, 'outbound.jsonl');
+
+// Hostname substrings that scream "domain-intel API call". Anything matching → log loud (red).
+// Anything else still logs but in dim grey so you can spot unexpected callers.
+const DOMAIN_INTEL_PATTERNS = [
+  'whois',
+  'rdap',
+  'godaddy',
+  'namecheap',
+  'porkbun',
+  'cloudflare.com/client/v4/registrar',
+  'whoisxmlapi',
+  'whoisxml',
+  'domaintools',
+  'nic.google',
+  'verisign',
+  'iana.org',
+  'icann.org',
+  'pir.org',
+  'afilias',
+  'identitydigital',
+  'donuts.co',
+  'gandi.net',
+  'opensrs',
+  'enom',
+  'tucows',
+  'name.com',
+  'dynadot',
+  'hover.com',
+  'domains.google',
+  'registry.google',
+  'registrar-servers.com',
+  'epik.com',
+  'dropcatch',
+  'snapnames',
+  'pool.com',
+  'expireddomains',
+  'newdomains',
+];
+
+function classify(host) {
+  const h = (host || '').toLowerCase();
+  const matched = DOMAIN_INTEL_PATTERNS.find((p) => h.includes(p));
+  return matched ? { level: 'INTEL', match: matched } : { level: 'OTHER', match: null };
+}
+
+function ts() {
+  return new Date().toISOString();
+}
+
+function logHit(rec) {
+  fs.mkdirSync(DATA_DIR, { recursive: true });
+  fs.appendFileSync(LOG_FILE, JSON.stringify(rec) + '\n');
+}
+
+function colorPrint(level, host, extra = '') {
+  const t = new Date().toISOString().replace('T', ' ').replace(/\..*/, '');
+  if (level === 'INTEL') {
+    process.stdout.write(`\x1b[31m[INTEL]\x1b[0m ${t}  \x1b[1m${host}\x1b[0m  ${extra}\n`);
+  } else {
+    process.stdout.write(`\x1b[2m[other] ${t}  ${host}  ${extra}\x1b[0m\n`);
+  }
+}
+
+// ---------- HTTPS CONNECT tunnel (the common case for API calls) ----------
+function handleConnect(req, clientSocket, head) {
+  const [hostRaw, portRaw] = req.url.split(':');
+  const host = hostRaw;
+  const port = parseInt(portRaw || '443', 10);
+  const c = classify(host);
+  const rec = { ts: ts(), type: 'CONNECT', host, port, via: 'HTTPS_PROXY', ...c };
+  logHit(rec);
+  colorPrint(c.level, `${host}:${port}`, 'CONNECT');
+
+  const serverSocket = net.connect(port, host, () => {
+    clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
+    serverSocket.write(head);
+    serverSocket.pipe(clientSocket);
+    clientSocket.pipe(serverSocket);
+  });
+  serverSocket.on('error', (err) => {
+    clientSocket.end();
+    if (c.level === 'INTEL') console.error(`[INTEL] upstream error ${host}:${port}  ${err.message}`);
+  });
+  clientSocket.on('error', () => serverSocket.end());
+}
+
+// ---------- Plain HTTP forward (rare for APIs but happens) ----------
+function handleRequest(req, res) {
+  const target = req.headers.host;
+  if (!target) {
+    res.writeHead(400);
+    return res.end('proxy: missing Host');
+  }
+  const c = classify(target);
+  const rec = { ts: ts(), type: 'HTTP', host: target, method: req.method, url: req.url, ...c };
+  logHit(rec);
+  colorPrint(c.level, target, `${req.method} ${req.url}`);
+
+  let u;
+  try {
+    u = new URL(req.url.startsWith('http') ? req.url : `http://${target}${req.url}`);
+  } catch (e) {
+    res.writeHead(400);
+    return res.end('proxy: bad URL');
+  }
+  const fwd = http.request(
+    {
+      host: u.hostname,
+      port: u.port || 80,
+      method: req.method,
+      path: u.pathname + u.search,
+      headers: req.headers,
+    },
+    (fwdRes) => {
+      res.writeHead(fwdRes.statusCode, fwdRes.headers);
+      fwdRes.pipe(res);
+    },
+  );
+  fwd.on('error', (err) => {
+    res.writeHead(502);
+    res.end(`proxy upstream error: ${err.message}`);
+  });
+  req.pipe(fwd);
+}
+
+const server = http.createServer(handleRequest);
+server.on('connect', handleConnect);
+
+server.listen(PORT, '127.0.0.1', () => {
+  console.log(`\n  domain-sniper :: outbound watcher`);
+  console.log(`  listening on  http://127.0.0.1:${PORT}`);
+  console.log(`  logging to    ${LOG_FILE}\n`);
+  console.log(`  In another shell, point your tools at this proxy:`);
+  console.log(`    export HTTPS_PROXY=http://127.0.0.1:${PORT}`);
+  console.log(`    export HTTP_PROXY=http://127.0.0.1:${PORT}`);
+  console.log(`    export NO_PROXY=localhost,127.0.0.1\n`);
+  console.log(`  Then re-run the tool you suspect is leaking. Watch [INTEL] lines below.\n`);
+  console.log(`  For raw 'whois' (port 43) and DNS leaks, run in a separate terminal:`);
+  console.log(`    sudo tcpdump -i any -nn 'port 43 or (udp port 53)' -l\n`);
+});
+
+process.on('SIGINT', () => { console.log('\n  bye.'); process.exit(0); });

← b738669 initial scaffold — leak-minimal DoH checker + CertStream wat  ·  back to Domain Sniper  ·  step 3: live web dashboard at http://127.0.0.1:9895 4296639 →