← back to Domain Sniper
honeypot v2: DoH pre-check is ground-truth; 6-letter .com to escape saturation
c8f80e8cfd643989c672407db45b32570e8bfecd · 2026-05-12 13:39:36 -0700 · Steve Abrams
Phase 1 finds 10 DoH-confirmed-available names per bucket (regenerates until
each slot passes a Cloudflare NXDOMAIN+NXDOMAIN check). Phase 2 fires the
bait query through the suspect leak channel — its result is recorded but
not used for snipe attribution. preCheckAvailable becomes the only baseline
the check phase needs.
.com bumped from 5 to 6 letters (5-letter .com pronounceable namespace is
80%+ saturated, killing test power).
Files touched
Diff
commit c8f80e8cfd643989c672407db45b32570e8bfecd
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue May 12 13:39:36 2026 -0700
honeypot v2: DoH pre-check is ground-truth; 6-letter .com to escape saturation
Phase 1 finds 10 DoH-confirmed-available names per bucket (regenerates until
each slot passes a Cloudflare NXDOMAIN+NXDOMAIN check). Phase 2 fires the
bait query through the suspect leak channel — its result is recorded but
not used for snipe attribution. preCheckAvailable becomes the only baseline
the check phase needs.
.com bumped from 5 to 6 letters (5-letter .com pronounceable namespace is
80%+ saturated, killing test power).
---
honeypot.js | 72 ++++++++++++++++++++++++++++++++++++++++---------------------
1 file changed, 47 insertions(+), 25 deletions(-)
diff --git a/honeypot.js b/honeypot.js
index d47cb3e..ad2ffcc 100644
--- a/honeypot.js
+++ b/honeypot.js
@@ -39,12 +39,14 @@ const BANNED_STEMS = new Set([
// any Steve-brand-adjacent stems live here so we never bait something he'd want
'callr', 'butlr', 'callr', 'doctr', 'lawyr', 'rentr',
]);
-function genName(rng) {
+function genName(rng, len = 6) {
const C = 'bcdfghjklmnprstvwz';
const V = 'aeiou';
const r = (max) => Math.floor(rng() * max);
for (let tries = 0; tries < 50; tries++) {
- const n = C[r(C.length)] + V[r(V.length)] + C[r(C.length)] + V[r(V.length)] + C[r(C.length)];
+ // CVCVCV(C) alternating
+ let n = '';
+ for (let i = 0; i < len; i++) n += (i % 2 === 0 ? C : V)[r((i % 2 === 0 ? C : V).length)];
if (/(.)\1/.test(n)) continue;
if (BANNED_STEMS.has(n)) continue;
return n;
@@ -53,11 +55,12 @@ function genName(rng) {
}
const BUCKETS = [
- { id: 'A', label: 'Verisign WHOIS (.com)', tld: 'com', method: 'whois-verisign-com' },
- { id: 'B', label: 'Google WHOIS (.app)', tld: 'app', method: 'whois-google-app' },
- { id: 'C', label: 'nic.co WHOIS (.co)', tld: 'co', method: 'whois-nic-co' },
- { id: 'D', label: 'GoDaddy API (.com)', tld: 'com', method: 'godaddy-api-com' },
- { id: 'E', label: 'CONTROL no-query (.com)', tld: 'com', method: 'none' },
+ // nameLen 6 for .com to escape the saturated 5-letter namespace
+ { id: 'A', label: 'Verisign WHOIS (.com)', tld: 'com', nameLen: 6, method: 'whois-verisign-com' },
+ { id: 'B', label: 'Google WHOIS (.app)', tld: 'app', nameLen: 5, method: 'whois-google-app' },
+ { id: 'C', label: 'nic.co WHOIS (.co)', tld: 'co', nameLen: 5, method: 'whois-nic-co' },
+ { id: 'D', label: 'GoDaddy API (.com)', tld: 'com', nameLen: 6, method: 'godaddy-api-com' },
+ { id: 'E', label: 'CONTROL no-query (.com)', tld: 'com', nameLen: 6, method: 'none' },
];
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -152,6 +155,16 @@ function whoisFingerprint(domain) {
}
// ---------- LURE: generate names + fire bait queries ----------
+async function findAvailable(bucket, rng, maxTries = 30) {
+ for (let i = 0; i < maxTries; i++) {
+ const stem = genName(rng, bucket.nameLen);
+ const domain = `${stem}.${bucket.tld}`;
+ if (await dohAvailable(domain)) return { stem, domain };
+ await sleep(180);
+ }
+ return null;
+}
+
async function lure() {
const batchTs = new Date().toISOString().replace(/[:.]/g, '-');
const seed = Date.now() & 0x7fffffff;
@@ -159,35 +172,44 @@ async function lure() {
const batch = {
batchTs,
seed,
- queriedFromIp: 'local (check `curl ifconfig.me` if you need to record)',
+ note: 'DoH pre-check is ground-truth baseline. WHOIS/RDAP/API queries are pure bait — their results are recorded but not used for snipe detection.',
items: [],
};
+ fs.mkdirSync(DATA_DIR, { recursive: true });
+ const batchPath = path.join(DATA_DIR, `honeypot-${batchTs}.json`);
+
+ console.log(`\n honeypot batch ${batchTs}`);
+ console.log(` seed=${seed}\n`);
+ console.log(` phase 1: finding 10 verifiably-available names per bucket via Cloudflare DoH...\n`);
- // Generate 50 names, 10 per bucket
for (const bucket of BUCKETS) {
- for (let i = 0; i < 10; i++) {
- const stem = genName(rng);
+ let found = 0;
+ while (found < 10) {
+ const cand = await findAvailable(bucket, rng);
+ if (!cand) {
+ console.log(` [${bucket.id}] giving up at ${found}/10 — namespace too saturated`);
+ break;
+ }
batch.items.push({
bucket: bucket.id,
label: bucket.label,
method: bucket.method,
tld: bucket.tld,
- stem,
- domain: `${stem}.${bucket.tld}`,
+ stem: cand.stem,
+ domain: cand.domain,
+ preCheckAvailable: true,
+ preCheckedAt: new Date().toISOString(),
queriedAt: null,
queryResult: null,
});
+ found++;
+ process.stdout.write(` [${bucket.id}] ${cand.domain.padEnd(14)} confirmed AVAILABLE via DoH\n`);
}
+ fs.writeFileSync(batchPath, JSON.stringify(batch, null, 2));
}
- fs.mkdirSync(DATA_DIR, { recursive: true });
- const batchPath = path.join(DATA_DIR, `honeypot-${batchTs}.json`);
- fs.writeFileSync(batchPath, JSON.stringify(batch, null, 2));
-
- console.log(`\n honeypot batch ${batchTs}`);
- console.log(` seed=${seed} buckets=${BUCKETS.length} names=${batch.items.length}`);
- console.log(` → ${batchPath}\n`);
- console.log(` firing bait queries (this DELIBERATELY leaks — that's the point):\n`);
+ console.log(`\n ${batch.items.length} fresh-available bait names secured.\n`);
+ console.log(` phase 2: firing bait queries through suspect leak channels...\n`);
for (let i = 0; i < batch.items.length; i++) {
const item = batch.items[i];
@@ -217,11 +239,11 @@ async function lure() {
` [${item.bucket}] ${item.domain.padEnd(14)} ${item.method.padEnd(22)} ${item.queryResult}`,
);
fs.writeFileSync(batchPath, JSON.stringify(batch, null, 2));
- // pace it so we look like a curious human, not a scanner
- await sleep(1800 + Math.random() * 1400);
+ await sleep(1500 + Math.random() * 1200);
}
console.log(`\n bait deployed. ${batch.items.length} honeypots in the wild.`);
+ console.log(` → ${batchPath}\n`);
console.log(` wait 24-48h, then:`);
console.log(` node honeypot.js check ${path.relative(__dirname, batchPath)}\n`);
}
@@ -244,7 +266,7 @@ async function check(batchPath) {
for (const item of batch.items) {
const stillAvailable = await dohAvailable(item.domain);
- const sniped = item.queryResult === 'AVAILABLE' && !stillAvailable;
+ const sniped = item.preCheckAvailable && !stillAvailable;
results.items.push({ ...item, stillAvailable, sniped });
const flag = sniped ? '\x1b[31m🎯 SNIPED\x1b[0m' : stillAvailable ? ' open ' : ' taken ';
console.log(` [${item.bucket}] ${item.domain.padEnd(14)} ${flag} was=${item.queryResult.padEnd(10)} now=${stillAvailable ? 'avail' : 'taken'}`);
@@ -264,7 +286,7 @@ async function check(batchPath) {
};
const t = tally[item.bucket];
t.total++;
- if (item.queryResult === 'AVAILABLE') t.wasAvailable++;
+ if (item.preCheckAvailable) t.wasAvailable++;
if (item.sniped) {
t.sniped++;
t.snipedNames.push(item.domain);
← 28b7d63 step 4: honeypot.js — controlled leak-attribution hunt
·
back to Domain Sniper
·
step 5: register.js — atomic registrar API with dry-run safe 4aecb56 →