Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,461 changes: 1,461 additions & 0 deletions apps/pwa/src/lib/iana-tlds.mjs

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions apps/pwa/src/lib/moshpit-name.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
* trademark problem, and neither is cheap to unwind after the fact. A static
* list is a blunt instrument, but it is the one that works on day one.
*/
import { isRealTld } from "./iana-tlds.mjs";

export const RESERVED_TLDS = new Set([
// trades on trust in money
"bank", "banking", "paypal", "visa", "mastercard", "amex", "stripe", "coinbase",
Expand Down Expand Up @@ -65,6 +67,16 @@ export function normalizeTld(input) {
/** Why a TLD cannot be registered, or null when it is fine. */
export function tldRejection(tld) {
if (RESERVED_TLDS.has(tld)) return "that name is reserved";
// Selling an ending that IANA already delegates does not create a name, it
// takes one away: every resolver running a Moshpit bridge stops forwarding
// that whole TLD and starts answering for it out of a namespace that has
// never heard of the real thing. `.sh` went for $2 and took `pit.moshcode.sh`
// — this registry's own hostname — off the air for every bridge, along with
// the rest of Saint Helena's ccTLD.
//
// The static RESERVED_TLDS list above tried to cover this with `com`, `net`
// and `org`. There are 1438 of them.
if (isRealTld(tld)) return `.${tld} is a real top-level domain — claiming it would stop Moshpit resolvers from reaching the rest of it`;
if (tld.length < 2) return "a TLD needs at least 2 characters";
return null;
}
Expand Down
8 changes: 8 additions & 0 deletions apps/pwa/src/moshpit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
parseTldList,
tldRejection,
} from "./lib/moshpit-name.mjs";
import { isRealTld } from "./lib/iana-tlds.mjs";
import {
agreedTerms,
effectiveStatus,
Expand Down Expand Up @@ -200,6 +201,13 @@ export async function registerTld({ tld: input, userId, ownerEmail = null, owner
const tld = normalizeTld(input);
if (!tld) return { ok: false, error: "not a valid TLD — letters, digits and dashes only, no dots" };

// Refused even for callers allowed to take reserved names. Reserving `.bank`
// is policy and policy has exceptions; answering for `.sh` is a machine that
// has lost a chunk of the internet, and there is no caller for whom that is
// the intended outcome.
if (isRealTld(tld)) {
return { ok: false, error: `.${tld} is a real top-level domain — claiming it would stop Moshpit resolvers from reaching the rest of it` };
}
const rejected = tldRejection(tld);
if (rejected && !allowReserved) return { ok: false, error: rejected };

Expand Down
7 changes: 6 additions & 1 deletion apps/pwa/test/moshpit-name.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@ test("reserved names cannot be claimed", () => {

test("a TLD needs at least two characters", () => {
assert.equal(tldRejection("a"), "a TLD needs at least 2 characters");
assert.equal(tldRejection("ai"), null);
// Was `ai`, which reads as a two-character ending and is also Anguilla's
// ccTLD. Selling it stops every Moshpit resolver forwarding the real one, so
// it is refused now and the length rule needs an example that is only about
// length.
assert.equal(tldRejection("42"), null);
assert.match(tldRejection("ai") || "", /real top-level domain/);
});

test("parseMoshpitName splits exactly one dot", () => {
Expand Down
68 changes: 68 additions & 0 deletions scripts/generate-iana-tlds.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env node
// Regenerate the IANA TLD list that Moshpit refuses to sell or resolve.
//
// node scripts/generate-iana-tlds.mjs
//
// Writes two identical modules, because the two halves that need this list live
// in two packages that never import each other: `src/` ships to npm as the CLI
// and its bridge, `apps/pwa/` is deployed as the Pit and is not a workspace.
// `test/iana-tlds-drift.test.mjs` fails if they diverge.
//
// The list is vendored rather than fetched at runtime on purpose. A registry
// that phoned IANA on every claim would sell `.sh` the moment IANA had a bad
// minute, and a resolver that did it per query would be a resolver with a
// dependency on the thing it exists to avoid needing.
import { writeFile } from "node:fs/promises";

const SOURCE = "https://data.iana.org/TLD/tlds-alpha-by-domain.txt";

const TARGETS = [
"src/iana-tlds.mjs",
"apps/pwa/src/lib/iana-tlds.mjs",
];

const res = await fetch(SOURCE);
if (!res.ok) throw new Error(`IANA returned ${res.status}`);
const text = await res.text();

const version = text.split("\n")[0].replace(/^#\s*/, "").trim();
const tlds = text
.split("\n")
.map((line) => line.trim().toLowerCase())
.filter((line) => line && !line.startsWith("#"))
// Punycode IDNs come through as `xn--…`, which is exactly the form a
// hostname carries on the wire, so they are kept as-is.
.sort();

if (tlds.length < 1000) throw new Error(`only ${tlds.length} TLDs parsed — refusing to write a short list`);

const body = `// Generated by scripts/generate-iana-tlds.mjs — do not edit by hand.
//
// Every top-level domain IANA delegates, as of:
// ${version}
//
// Moshpit will not sell an ending that collides with one of these, and the
// bridge will not answer for one. Both halves matter and neither is sufficient:
// the registry stops new collisions, and the bridge makes the ones already sold
// harmless without waiting for them to be cleaned up.
//
// The cost of getting this wrong is not a broken Moshpit name, it is a broken
// internet. An ending claimed here is one this machine's resolver stops
// forwarding — \`.sh\` was claimed for $2 and took \`pit.moshcode.sh\`, the
// registry every bridge depends on, off the air for anyone running one.
export const IANA_VERSION = ${JSON.stringify(version)};

export const IANA_TLDS = new Set([
${tlds.map((t) => ` ${JSON.stringify(t)},`).join("\n")}
]);

/** Does this ending collide with a real top-level domain? */
export function isRealTld(tld) {
return IANA_TLDS.has(String(tld ?? "").trim().toLowerCase());
}
`;

for (const target of TARGETS) {
await writeFile(target, body);
console.log(`wrote ${target} (${tlds.length} TLDs, ${version})`);
}
19 changes: 18 additions & 1 deletion src/dns.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1163,7 +1163,23 @@ export function forwardQuery(msg, upstream, { timeoutMs = 3000 } = {}) {
export function isOurs(name, tldSet) {
if (!(tldSet instanceof Set) || tldSet.size === 0) return false;
const parsed = parseRegistryName(name);
return Boolean(parsed) && tldSet.has(parsed.tld);
if (!parsed) return false;
// A real top-level domain is never ours, whatever the registry says it sold.
//
// Answering for one does not add a name, it removes the internet: every
// lookup under that TLD stops being forwarded and starts being answered from
// a namespace that has never heard of it. `.sh` was claimed for $2 and took
// `pit.moshcode.sh` — the registry every bridge fetches its endings from —
// off the air for anyone running a bridge, which is a resolver that cannot
// resolve the thing it needs in order to resolve. It also blackholed the real
// Saint Helena ccTLD on those machines, quietly, with a parking IP.
//
// Checked here rather than trusted from the registry because this is the half
// that protects a person who is already running a bridge, today, against
// endings that were sold before anyone noticed. The registry refusing to sell
// new ones is the other half and it fixes nothing already on disk.
if (isRealTld(parsed.tld)) return false;
return tldSet.has(parsed.tld);
}

export function createServer(options = {}) {
Expand Down Expand Up @@ -2320,6 +2336,7 @@ import { applyTrust, applyUntrust, createAutoTrust, trustName, verifyStockTls }
import { readFile, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { isRealTld } from "./iana-tlds.mjs";
import { installService, removeService, serviceUnit, servicePaths, UNIT_NAME } from "./dns-service.mjs";
import {
applyPlan, daemonStatus, describePlan, detectPlatform, disablePlan, enablePlan,
Expand Down
Loading
Loading