From f063d153fb40bcb6aec52e79faafaf1a2045c4b7 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 30 Aug 2026 14:13:19 +0000 Subject: [PATCH] fix(moshpit): never sell or answer for a real top-level domain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `.sh` was claimed through the ordinary registration flow, for $2, by someone who is not us. It is Saint Helena's ccTLD. It is also where this project's own registry lives: pit.moshcode.sh. Every bridge that fetched the ending list therefore learned to treat the registry as a Moshpit name. It stopped forwarding it to the clearnet and tried to resolve it through the registry it had just made unreachable, which is a resolver that cannot resolve the thing it needs in order to resolve anything. The observed state on an affected desktop was a bridge that was running, listening, correctly routed to, and answering NXDOMAIN to the entire internet: pit.moshcode.sh → NXDOMAIN alt.2600 → NXDOMAIN mail-ads.google.com → NXDOMAIN Moshpit names failed for the same reason clearnet ones did. Resolving a name needs a live registry query, and the registry was the thing being swallowed. It took the rest of the ccTLD with it. On a machine running the bridge as its resolver, real `.sh` sites answered a Moshpit parking IP. The static RESERVED_TLDS list was meant to cover this. It named com, net, org, edu, mil, int and arpa. There are 1438 delegated TLDs. So both halves are closed: - the registry refuses to sell an ending IANA already delegates, on both the single and bulk registration paths, and refuses it even for callers passing `allowReserved` — reserving `.bank` is policy and policy has exceptions, answering for `.sh` is a machine that has lost part of the internet - the bridge refuses to answer for one whatever the registry says it sold Neither is sufficient alone, and the second is the one that helps anyone today: 622 colliding endings are already registered across 14 owners, and a person running a bridge should not have to wait for that to be cleaned up before their resolver works. `isOurs` is now the last word rather than the ending list. The list is vendored, not fetched at run time. 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 depend on the very thing it exists to avoid needing. `scripts/generate-iana-tlds.mjs` regenerates it; the two copies exist because `src/` ships to npm and `apps/pwa/` deploys as the Pit and is not a workspace, and a test fails if they drift. Verified against a live bridge holding all 18224 endings including `sh`: pit.moshcode.sh now answers 69.46.46.24 where it used to NXDOMAIN, moshcode.sh answers its real 69.46.46.31, blue.eggs and alt.2600 are unchanged, and moshcoding.sh correctly returns nothing because it genuinely does not exist — rather than a parking IP claiming it does. One existing test asserted `tldRejection("ai")` was null. `.ai` is Anguilla's ccTLD; the expectation encoded the bug. Rewritten to test the length rule with an ending that is only about length. Not included: deleting the 622 endings already sold. The guards above make them harmless without it, and removing paid claims across 14 owners is a decision to take deliberately rather than as a side effect of a bug fix. Suite: 2726 tests, 0 failures. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ThnQwoieWt8VR6N7gtgnhp --- apps/pwa/src/lib/iana-tlds.mjs | 1461 +++++++++++++++++++++++++++ apps/pwa/src/lib/moshpit-name.mjs | 12 + apps/pwa/src/moshpit.mjs | 8 + apps/pwa/test/moshpit-name.test.mjs | 7 +- scripts/generate-iana-tlds.mjs | 68 ++ src/dns.mjs | 19 +- src/iana-tlds.mjs | 1461 +++++++++++++++++++++++++++ test/iana-tlds.test.mjs | 109 ++ 8 files changed, 3143 insertions(+), 2 deletions(-) create mode 100644 apps/pwa/src/lib/iana-tlds.mjs create mode 100644 scripts/generate-iana-tlds.mjs create mode 100644 src/iana-tlds.mjs create mode 100644 test/iana-tlds.test.mjs diff --git a/apps/pwa/src/lib/iana-tlds.mjs b/apps/pwa/src/lib/iana-tlds.mjs new file mode 100644 index 00000000..c219c36d --- /dev/null +++ b/apps/pwa/src/lib/iana-tlds.mjs @@ -0,0 +1,1461 @@ +// Generated by scripts/generate-iana-tlds.mjs — do not edit by hand. +// +// Every top-level domain IANA delegates, as of: +// Version 2026083000, Last Updated Sun Aug 30 07:07:01 2026 UTC +// +// 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 = "Version 2026083000, Last Updated Sun Aug 30 07:07:01 2026 UTC"; + +export const IANA_TLDS = new Set([ + "aaa", + "aarp", + "abb", + "abbott", + "abbvie", + "abc", + "able", + "abogado", + "abudhabi", + "ac", + "academy", + "accenture", + "accountant", + "accountants", + "aco", + "actor", + "ad", + "ads", + "adult", + "ae", + "aeg", + "aero", + "aetna", + "af", + "afl", + "africa", + "ag", + "agakhan", + "agency", + "ai", + "aig", + "airbus", + "airforce", + "airtel", + "akdn", + "al", + "alibaba", + "alipay", + "allfinanz", + "allstate", + "ally", + "alsace", + "alstom", + "am", + "amazon", + "americanexpress", + "americanfamily", + "amex", + "amfam", + "amica", + "amsterdam", + "analytics", + "android", + "anquan", + "anz", + "ao", + "aol", + "apartments", + "app", + "apple", + "aq", + "aquarelle", + "ar", + "arab", + "aramco", + "archi", + "army", + "arpa", + "art", + "arte", + "as", + "asda", + "asia", + "associates", + "at", + "athleta", + "attorney", + "au", + "auction", + "audi", + "audible", + "audio", + "auspost", + "author", + "auto", + "autos", + "aw", + "aws", + "ax", + "axa", + "az", + "azure", + "ba", + "baby", + "baidu", + "banamex", + "band", + "bank", + "bar", + "barcelona", + "barclaycard", + "barclays", + "barefoot", + "bargains", + "baseball", + "basketball", + "bauhaus", + "bayern", + "bb", + "bbc", + "bbt", + "bbva", + "bcg", + "bcn", + "bd", + "be", + "beats", + "beauty", + "beer", + "berlin", + "best", + "bestbuy", + "bet", + "bf", + "bg", + "bh", + "bharti", + "bi", + "bible", + "bid", + "bike", + "bing", + "bingo", + "bio", + "biz", + "bj", + "black", + "blackfriday", + "blockbuster", + "blog", + "bloomberg", + "blue", + "bm", + "bms", + "bmw", + "bn", + "bnpparibas", + "bo", + "boats", + "boehringer", + "bofa", + "bom", + "bond", + "boo", + "book", + "booking", + "bosch", + "bostik", + "boston", + "bot", + "boutique", + "box", + "br", + "bradesco", + "bridgestone", + "broadway", + "broker", + "brother", + "brussels", + "bs", + "bt", + "build", + "builders", + "business", + "buy", + "buzz", + "bv", + "bw", + "by", + "bz", + "bzh", + "ca", + "cab", + "cafe", + "cal", + "call", + "calvinklein", + "cam", + "camera", + "camp", + "canon", + "capetown", + "capital", + "capitalone", + "car", + "caravan", + "cards", + "care", + "career", + "careers", + "cars", + "casa", + "case", + "cash", + "casino", + "cat", + "catering", + "catholic", + "cba", + "cbn", + "cbre", + "cc", + "cd", + "center", + "ceo", + "cern", + "cf", + "cfa", + "cfd", + "cg", + "ch", + "chanel", + "channel", + "charity", + "chase", + "chat", + "cheap", + "chintai", + "christmas", + "chrome", + "church", + "ci", + "cipriani", + "circle", + "cisco", + "citadel", + "citi", + "citic", + "city", + "ck", + "cl", + "claims", + "cleaning", + "click", + "clinic", + "clinique", + "clothing", + "cloud", + "club", + "clubmed", + "cm", + "cn", + "co", + "coach", + "codes", + "coffee", + "college", + "cologne", + "com", + "commbank", + "community", + "company", + "compare", + "computer", + "comsec", + "condos", + "construction", + "consulting", + "contact", + "contractors", + "cooking", + "cool", + "coop", + "corsica", + "country", + "coupon", + "coupons", + "courses", + "cpa", + "cr", + "credit", + "creditcard", + "creditunion", + "cricket", + "crown", + "crs", + "cruise", + "cruises", + "cu", + "cuisinella", + "cv", + "cw", + "cx", + "cy", + "cymru", + "cyou", + "cz", + "dad", + "dance", + "data", + "date", + "dating", + "datsun", + "day", + "dclk", + "dds", + "de", + "deal", + "dealer", + "deals", + "degree", + "delivery", + "dell", + "deloitte", + "delta", + "democrat", + "dental", + "dentist", + "desi", + "design", + "dev", + "dhl", + "diamonds", + "diet", + "digital", + "direct", + "directory", + "discount", + "discover", + "dish", + "diy", + "dj", + "dk", + "dm", + "dnp", + "do", + "docs", + "doctor", + "dog", + "domains", + "dot", + "download", + "drive", + "dtv", + "dubai", + "dupont", + "durban", + "dvag", + "dvr", + "dz", + "earth", + "eat", + "ec", + "eco", + "edeka", + "edu", + "education", + "ee", + "eg", + "email", + "emerck", + "energy", + "engineer", + "engineering", + "enterprises", + "epson", + "equipment", + "er", + "ericsson", + "erni", + "es", + "esq", + "estate", + "et", + "eu", + "eurovision", + "eus", + "events", + "exchange", + "expert", + "exposed", + "express", + "extraspace", + "fage", + "fail", + "fairwinds", + "faith", + "family", + "fan", + "fans", + "farm", + "farmers", + "fashion", + "fast", + "fedex", + "feedback", + "ferrari", + "ferrero", + "fi", + "fidelity", + "fido", + "film", + "final", + "finance", + "financial", + "fire", + "firestone", + "firmdale", + "fish", + "fishing", + "fit", + "fitness", + "fj", + "fk", + "flickr", + "flights", + "flir", + "florist", + "flowers", + "fly", + "fm", + "fo", + "foo", + "food", + "football", + "ford", + "forex", + "forsale", + "forum", + "foundation", + "fox", + "fr", + "free", + "fresenius", + "frl", + "frogans", + "frontier", + "ftr", + "fujitsu", + "fun", + "fund", + "furniture", + "futbol", + "fyi", + "ga", + "gal", + "gallery", + "gallo", + "gallup", + "game", + "games", + "gap", + "garden", + "gay", + "gb", + "gbiz", + "gd", + "gdn", + "ge", + "gea", + "gent", + "genting", + "george", + "gf", + "gg", + "ggee", + "gh", + "gi", + "gift", + "gifts", + "gives", + "giving", + "gl", + "glass", + "gle", + "global", + "globo", + "gm", + "gmail", + "gmbh", + "gmo", + "gmx", + "gn", + "godaddy", + "gold", + "goldpoint", + "golf", + "goodyear", + "goog", + "google", + "gop", + "got", + "gov", + "gp", + "gq", + "gr", + "grainger", + "graphics", + "gratis", + "green", + "gripe", + "grocery", + "group", + "gs", + "gt", + "gu", + "gucci", + "guge", + "guide", + "guitars", + "guru", + "gw", + "gy", + "hair", + "hamburg", + "hangout", + "haus", + "hbo", + "hdfc", + "hdfcbank", + "health", + "healthcare", + "help", + "helsinki", + "here", + "hermes", + "hiphop", + "hisamitsu", + "hitachi", + "hiv", + "hk", + "hkt", + "hm", + "hn", + "hockey", + "holdings", + "holiday", + "homedepot", + "homegoods", + "homes", + "homesense", + "honda", + "horse", + "hospital", + "host", + "hosting", + "hot", + "hotels", + "hotmail", + "house", + "how", + "hr", + "hsbc", + "ht", + "hu", + "hughes", + "hyatt", + "hyundai", + "ibm", + "icbc", + "ice", + "icu", + "id", + "ie", + "ieee", + "ifm", + "ikano", + "il", + "im", + "imamat", + "imdb", + "immo", + "immobilien", + "in", + "inc", + "industries", + "infiniti", + "info", + "ing", + "ink", + "institute", + "insurance", + "insure", + "int", + "international", + "intuit", + "investments", + "io", + "ipiranga", + "iq", + "ir", + "irish", + "is", + "ismaili", + "ist", + "istanbul", + "it", + "itau", + "itv", + "jaguar", + "java", + "jcb", + "je", + "jeep", + "jetzt", + "jewelry", + "jio", + "jll", + "jm", + "jmp", + "jnj", + "jo", + "jobs", + "joburg", + "jot", + "joy", + "jp", + "jpmorgan", + "jprs", + "juegos", + "juniper", + "kaufen", + "kddi", + "ke", + "kerryhotels", + "kerryproperties", + "kfh", + "kg", + "kh", + "ki", + "kia", + "kids", + "kim", + "kindle", + "kitchen", + "kiwi", + "km", + "kn", + "koeln", + "komatsu", + "kosher", + "kp", + "kpmg", + "kpn", + "kr", + "krd", + "kred", + "kuokgroup", + "kw", + "ky", + "kyoto", + "kz", + "la", + "lacaixa", + "lamborghini", + "lamer", + "land", + "landrover", + "lanxess", + "lasalle", + "lat", + "latino", + "latrobe", + "law", + "lawyer", + "lb", + "lc", + "lds", + "lease", + "leclerc", + "lefrak", + "legal", + "lego", + "lexus", + "lgbt", + "li", + "lidl", + "life", + "lifeinsurance", + "lifestyle", + "lighting", + "like", + "lilly", + "limited", + "limo", + "lincoln", + "link", + "live", + "living", + "lk", + "llc", + "llp", + "loan", + "loans", + "locker", + "locus", + "lol", + "london", + "lotte", + "lotto", + "love", + "lpl", + "lplfinancial", + "lr", + "ls", + "lt", + "ltd", + "ltda", + "lu", + "lundbeck", + "luxe", + "luxury", + "lv", + "ly", + "ma", + "madrid", + "maif", + "maison", + "makeup", + "man", + "management", + "mango", + "map", + "market", + "marketing", + "markets", + "marriott", + "marshalls", + "mattel", + "mba", + "mc", + "mckinsey", + "md", + "me", + "med", + "media", + "meet", + "melbourne", + "meme", + "memorial", + "men", + "menu", + "merck", + "merckmsd", + "mg", + "mh", + "miami", + "microsoft", + "mil", + "mini", + "mint", + "mit", + "mitsubishi", + "mk", + "ml", + "mlb", + "mls", + "mm", + "mma", + "mn", + "mo", + "mobi", + "mobile", + "moda", + "moe", + "moi", + "mom", + "monash", + "money", + "monster", + "mormon", + "mortgage", + "moscow", + "moto", + "motorcycles", + "mov", + "movie", + "mp", + "mq", + "mr", + "ms", + "msd", + "mt", + "mtn", + "mtr", + "mu", + "museum", + "music", + "mv", + "mw", + "mx", + "my", + "mz", + "na", + "nab", + "nagoya", + "name", + "navy", + "nba", + "nc", + "ne", + "nec", + "net", + "netbank", + "netflix", + "network", + "neustar", + "new", + "news", + "next", + "nextdirect", + "nexus", + "nf", + "nfl", + "ng", + "ngo", + "nhk", + "ni", + "nico", + "nike", + "nikon", + "ninja", + "nissan", + "nissay", + "nl", + "no", + "nokia", + "norton", + "now", + "nowruz", + "nowtv", + "np", + "nr", + "nra", + "nrw", + "ntt", + "nu", + "nyc", + "nz", + "obi", + "observer", + "office", + "okinawa", + "olayan", + "olayangroup", + "ollo", + "om", + "omega", + "one", + "ong", + "onl", + "online", + "ooo", + "open", + "oracle", + "orange", + "org", + "organic", + "origins", + "osaka", + "otsuka", + "ott", + "ovh", + "pa", + "page", + "panasonic", + "paris", + "pars", + "partners", + "parts", + "party", + "pay", + "pccw", + "pe", + "pet", + "pf", + "pfizer", + "pg", + "ph", + "pharmacy", + "phd", + "philips", + "phone", + "photo", + "photography", + "photos", + "physio", + "pics", + "pictet", + "pictures", + "pid", + "pin", + "ping", + "pink", + "pioneer", + "pizza", + "pk", + "pl", + "place", + "play", + "playstation", + "plumbing", + "plus", + "pm", + "pn", + "pnc", + "pohl", + "poker", + "politie", + "porn", + "post", + "pr", + "praxi", + "press", + "prime", + "pro", + "prod", + "productions", + "prof", + "progressive", + "promo", + "properties", + "property", + "protection", + "pru", + "prudential", + "ps", + "pt", + "pub", + "pw", + "pwc", + "py", + "qa", + "qpon", + "quebec", + "quest", + "racing", + "radio", + "re", + "read", + "realestate", + "realtor", + "realty", + "recipes", + "red", + "redumbrella", + "rehab", + "reise", + "reisen", + "reit", + "reliance", + "ren", + "rent", + "rentals", + "repair", + "report", + "republican", + "rest", + "restaurant", + "review", + "reviews", + "rexroth", + "rich", + "richardli", + "ricoh", + "ril", + "rio", + "rip", + "ro", + "rocks", + "rodeo", + "rogers", + "room", + "rs", + "rsvp", + "ru", + "rugby", + "ruhr", + "run", + "rw", + "rwe", + "ryukyu", + "sa", + "saarland", + "safe", + "safety", + "sakura", + "sale", + "salon", + "samsclub", + "samsung", + "sandvik", + "sandvikcoromant", + "sanofi", + "sap", + "sarl", + "sas", + "save", + "saxo", + "sb", + "sbi", + "sbs", + "sc", + "scb", + "schaeffler", + "schmidt", + "scholarships", + "school", + "schule", + "schwarz", + "science", + "scot", + "sd", + "se", + "search", + "seat", + "secure", + "security", + "seek", + "select", + "sener", + "services", + "seven", + "sew", + "sex", + "sexy", + "sfr", + "sg", + "sh", + "shangrila", + "sharp", + "shell", + "shia", + "shiksha", + "shoes", + "shop", + "shopping", + "shouji", + "show", + "si", + "silk", + "sina", + "singles", + "site", + "sj", + "sk", + "ski", + "skin", + "sky", + "skype", + "sl", + "sling", + "sm", + "smart", + "smile", + "sn", + "sncf", + "so", + "soccer", + "social", + "softbank", + "software", + "sohu", + "solar", + "solutions", + "song", + "sony", + "soy", + "spa", + "space", + "sport", + "spot", + "sr", + "srl", + "ss", + "st", + "stada", + "staples", + "star", + "statebank", + "statefarm", + "stc", + "stcgroup", + "stockholm", + "storage", + "store", + "stream", + "studio", + "study", + "style", + "su", + "sucks", + "supplies", + "supply", + "support", + "surf", + "surgery", + "suzuki", + "sv", + "swatch", + "swiss", + "sx", + "sy", + "sydney", + "systems", + "sz", + "tab", + "taipei", + "talk", + "taobao", + "target", + "tatamotors", + "tatar", + "tattoo", + "tax", + "taxi", + "tc", + "tci", + "td", + "tdk", + "team", + "tech", + "technology", + "tel", + "temasek", + "tennis", + "teva", + "tf", + "tg", + "th", + "thd", + "theater", + "theatre", + "tiaa", + "tickets", + "tienda", + "tips", + "tires", + "tirol", + "tj", + "tjmaxx", + "tjx", + "tk", + "tkmaxx", + "tl", + "tm", + "tmall", + "tn", + "to", + "today", + "tokyo", + "tools", + "top", + "toray", + "toshiba", + "total", + "tours", + "town", + "toyota", + "toys", + "tr", + "trade", + "trading", + "training", + "travel", + "travelers", + "travelersinsurance", + "trust", + "trv", + "tt", + "tube", + "tui", + "tunes", + "tushu", + "tv", + "tvs", + "tw", + "tz", + "ua", + "ubank", + "ubs", + "ug", + "uk", + "unicom", + "university", + "uno", + "uol", + "ups", + "us", + "uy", + "uz", + "va", + "vacations", + "vana", + "vanguard", + "vc", + "ve", + "vegas", + "ventures", + "verisign", + "versicherung", + "vet", + "vg", + "vi", + "viajes", + "video", + "vig", + "viking", + "villas", + "vin", + "vip", + "virgin", + "visa", + "vision", + "viva", + "vivo", + "vlaanderen", + "vn", + "vodka", + "volvo", + "vote", + "voting", + "voto", + "voyage", + "vu", + "wales", + "walmart", + "walter", + "wang", + "wanggou", + "watch", + "watches", + "weather", + "weatherchannel", + "web", + "webcam", + "weber", + "website", + "wed", + "wedding", + "weibo", + "weir", + "wf", + "whoswho", + "wien", + "wiki", + "williamhill", + "win", + "windows", + "wine", + "winners", + "wme", + "woodside", + "work", + "works", + "world", + "wow", + "ws", + "wtc", + "wtf", + "xbox", + "xerox", + "xihuan", + "xin", + "xn--11b4c3d", + "xn--1ck2e1b", + "xn--1qqw23a", + "xn--2scrj9c", + "xn--30rr7y", + "xn--3bst00m", + "xn--3ds443g", + "xn--3e0b707e", + "xn--3hcrj9c", + "xn--3pxu8k", + "xn--42c2d9a", + "xn--45br5cyl", + "xn--45brj9c", + "xn--45q11c", + "xn--4dbrk0ce", + "xn--4gbrim", + "xn--54b7fta0cc", + "xn--55qw42g", + "xn--55qx5d", + "xn--5su34j936bgsg", + "xn--5tzm5g", + "xn--6frz82g", + "xn--6qq986b3xl", + "xn--80adxhks", + "xn--80ao21a", + "xn--80aqecdr1a", + "xn--80asehdb", + "xn--80aswg", + "xn--8y0a063a", + "xn--90a3ac", + "xn--90ae", + "xn--90ais", + "xn--9dbq2a", + "xn--9et52u", + "xn--9krt00a", + "xn--b4w605ferd", + "xn--bck1b9a5dre4c", + "xn--c1avg", + "xn--c2br7g", + "xn--cck2b3b", + "xn--cckwcxetd", + "xn--cg4bki", + "xn--clchc0ea0b2g2a9gcd", + "xn--czr694b", + "xn--czrs0t", + "xn--czru2d", + "xn--d1acj3b", + "xn--d1alf", + "xn--e1a4c", + "xn--eckvdtc9d", + "xn--efvy88h", + "xn--fct429k", + "xn--fhbei", + "xn--fiq228c5hs", + "xn--fiq64b", + "xn--fiqs8s", + "xn--fiqz9s", + "xn--fjq720a", + "xn--flw351e", + "xn--fpcrj9c3d", + "xn--fzc2c9e2c", + "xn--fzys8d69uvgm", + "xn--g2xx48c", + "xn--gckr3f0f", + "xn--gecrj9c", + "xn--gk3at1e", + "xn--h2breg3eve", + "xn--h2brj9c", + "xn--h2brj9c8c", + "xn--hxt814e", + "xn--i1b6b1a6a2e", + "xn--imr513n", + "xn--io0a7i", + "xn--j1aef", + "xn--j1amh", + "xn--j6w193g", + "xn--jlq480n2rg", + "xn--jvr189m", + "xn--kcrx77d1x4a", + "xn--kprw13d", + "xn--kpry57d", + "xn--kput3i", + "xn--l1acc", + "xn--lgbbat1ad8j", + "xn--mgb9awbf", + "xn--mgba3a3ejt", + "xn--mgba3a4f16a", + "xn--mgba7c0bbn0a", + "xn--mgbaam7a8h", + "xn--mgbab2bd", + "xn--mgbah1a3hjkrd", + "xn--mgbai9azgqp6j", + "xn--mgbayh7gpa", + "xn--mgbbh1a", + "xn--mgbbh1a71e", + "xn--mgbc0a9azcg", + "xn--mgbca7dzdo", + "xn--mgbcpq6gpa1a", + "xn--mgberp4a5d4ar", + "xn--mgbgu82a", + "xn--mgbi4ecexp", + "xn--mgbpl2fh", + "xn--mgbt3dhd", + "xn--mgbtx2b", + "xn--mgbx4cd0ab", + "xn--mix891f", + "xn--mk1bu44c", + "xn--mxtq1m", + "xn--ngbc5azd", + "xn--ngbe9e0a", + "xn--ngbrx", + "xn--node", + "xn--nqv7f", + "xn--nqv7fs00ema", + "xn--nyqy26a", + "xn--o3cw4h", + "xn--ogbpf8fl", + "xn--otu796d", + "xn--p1acf", + "xn--p1ai", + "xn--pgbs0dh", + "xn--pssy2u", + "xn--q7ce6a", + "xn--q9jyb4c", + "xn--qcka1pmc", + "xn--qxa6a", + "xn--qxam", + "xn--rhqv96g", + "xn--rovu88b", + "xn--rvc1e0am3e", + "xn--s9brj9c", + "xn--ses554g", + "xn--t60b56a", + "xn--tckwe", + "xn--tiq49xqyj", + "xn--unup4y", + "xn--vermgensberater-ctb", + "xn--vermgensberatung-pwb", + "xn--vhquv", + "xn--vuq861b", + "xn--w4r85el8fhu5dnra", + "xn--w4rs40l", + "xn--wgbh1c", + "xn--wgbl6a", + "xn--xhq521b", + "xn--xkc2al3hye2a", + "xn--xkc2dl3a5ee0h", + "xn--y9a3aq", + "xn--yfro4i67o", + "xn--ygbi2ammx", + "xn--zfr164b", + "xxx", + "xyz", + "yachts", + "yahoo", + "yamaxun", + "yandex", + "ye", + "yodobashi", + "yoga", + "yokohama", + "you", + "youtube", + "yt", + "yun", + "za", + "zappos", + "zara", + "zero", + "zip", + "zm", + "zone", + "zuerich", + "zw", +]); + +/** Does this ending collide with a real top-level domain? */ +export function isRealTld(tld) { + return IANA_TLDS.has(String(tld ?? "").trim().toLowerCase()); +} diff --git a/apps/pwa/src/lib/moshpit-name.mjs b/apps/pwa/src/lib/moshpit-name.mjs index 19d19523..084ed5e2 100644 --- a/apps/pwa/src/lib/moshpit-name.mjs +++ b/apps/pwa/src/lib/moshpit-name.mjs @@ -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", @@ -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; } diff --git a/apps/pwa/src/moshpit.mjs b/apps/pwa/src/moshpit.mjs index 32f933c4..3cf0d329 100644 --- a/apps/pwa/src/moshpit.mjs +++ b/apps/pwa/src/moshpit.mjs @@ -49,6 +49,7 @@ import { parseTldList, tldRejection, } from "./lib/moshpit-name.mjs"; +import { isRealTld } from "./lib/iana-tlds.mjs"; import { agreedTerms, effectiveStatus, @@ -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 }; diff --git a/apps/pwa/test/moshpit-name.test.mjs b/apps/pwa/test/moshpit-name.test.mjs index 7357bf68..feeff451 100644 --- a/apps/pwa/test/moshpit-name.test.mjs +++ b/apps/pwa/test/moshpit-name.test.mjs @@ -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", () => { diff --git a/scripts/generate-iana-tlds.mjs b/scripts/generate-iana-tlds.mjs new file mode 100644 index 00000000..7ba08b31 --- /dev/null +++ b/scripts/generate-iana-tlds.mjs @@ -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})`); +} diff --git a/src/dns.mjs b/src/dns.mjs index e08828d1..50f95b96 100644 --- a/src/dns.mjs +++ b/src/dns.mjs @@ -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 = {}) { @@ -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, diff --git a/src/iana-tlds.mjs b/src/iana-tlds.mjs new file mode 100644 index 00000000..c219c36d --- /dev/null +++ b/src/iana-tlds.mjs @@ -0,0 +1,1461 @@ +// Generated by scripts/generate-iana-tlds.mjs — do not edit by hand. +// +// Every top-level domain IANA delegates, as of: +// Version 2026083000, Last Updated Sun Aug 30 07:07:01 2026 UTC +// +// 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 = "Version 2026083000, Last Updated Sun Aug 30 07:07:01 2026 UTC"; + +export const IANA_TLDS = new Set([ + "aaa", + "aarp", + "abb", + "abbott", + "abbvie", + "abc", + "able", + "abogado", + "abudhabi", + "ac", + "academy", + "accenture", + "accountant", + "accountants", + "aco", + "actor", + "ad", + "ads", + "adult", + "ae", + "aeg", + "aero", + "aetna", + "af", + "afl", + "africa", + "ag", + "agakhan", + "agency", + "ai", + "aig", + "airbus", + "airforce", + "airtel", + "akdn", + "al", + "alibaba", + "alipay", + "allfinanz", + "allstate", + "ally", + "alsace", + "alstom", + "am", + "amazon", + "americanexpress", + "americanfamily", + "amex", + "amfam", + "amica", + "amsterdam", + "analytics", + "android", + "anquan", + "anz", + "ao", + "aol", + "apartments", + "app", + "apple", + "aq", + "aquarelle", + "ar", + "arab", + "aramco", + "archi", + "army", + "arpa", + "art", + "arte", + "as", + "asda", + "asia", + "associates", + "at", + "athleta", + "attorney", + "au", + "auction", + "audi", + "audible", + "audio", + "auspost", + "author", + "auto", + "autos", + "aw", + "aws", + "ax", + "axa", + "az", + "azure", + "ba", + "baby", + "baidu", + "banamex", + "band", + "bank", + "bar", + "barcelona", + "barclaycard", + "barclays", + "barefoot", + "bargains", + "baseball", + "basketball", + "bauhaus", + "bayern", + "bb", + "bbc", + "bbt", + "bbva", + "bcg", + "bcn", + "bd", + "be", + "beats", + "beauty", + "beer", + "berlin", + "best", + "bestbuy", + "bet", + "bf", + "bg", + "bh", + "bharti", + "bi", + "bible", + "bid", + "bike", + "bing", + "bingo", + "bio", + "biz", + "bj", + "black", + "blackfriday", + "blockbuster", + "blog", + "bloomberg", + "blue", + "bm", + "bms", + "bmw", + "bn", + "bnpparibas", + "bo", + "boats", + "boehringer", + "bofa", + "bom", + "bond", + "boo", + "book", + "booking", + "bosch", + "bostik", + "boston", + "bot", + "boutique", + "box", + "br", + "bradesco", + "bridgestone", + "broadway", + "broker", + "brother", + "brussels", + "bs", + "bt", + "build", + "builders", + "business", + "buy", + "buzz", + "bv", + "bw", + "by", + "bz", + "bzh", + "ca", + "cab", + "cafe", + "cal", + "call", + "calvinklein", + "cam", + "camera", + "camp", + "canon", + "capetown", + "capital", + "capitalone", + "car", + "caravan", + "cards", + "care", + "career", + "careers", + "cars", + "casa", + "case", + "cash", + "casino", + "cat", + "catering", + "catholic", + "cba", + "cbn", + "cbre", + "cc", + "cd", + "center", + "ceo", + "cern", + "cf", + "cfa", + "cfd", + "cg", + "ch", + "chanel", + "channel", + "charity", + "chase", + "chat", + "cheap", + "chintai", + "christmas", + "chrome", + "church", + "ci", + "cipriani", + "circle", + "cisco", + "citadel", + "citi", + "citic", + "city", + "ck", + "cl", + "claims", + "cleaning", + "click", + "clinic", + "clinique", + "clothing", + "cloud", + "club", + "clubmed", + "cm", + "cn", + "co", + "coach", + "codes", + "coffee", + "college", + "cologne", + "com", + "commbank", + "community", + "company", + "compare", + "computer", + "comsec", + "condos", + "construction", + "consulting", + "contact", + "contractors", + "cooking", + "cool", + "coop", + "corsica", + "country", + "coupon", + "coupons", + "courses", + "cpa", + "cr", + "credit", + "creditcard", + "creditunion", + "cricket", + "crown", + "crs", + "cruise", + "cruises", + "cu", + "cuisinella", + "cv", + "cw", + "cx", + "cy", + "cymru", + "cyou", + "cz", + "dad", + "dance", + "data", + "date", + "dating", + "datsun", + "day", + "dclk", + "dds", + "de", + "deal", + "dealer", + "deals", + "degree", + "delivery", + "dell", + "deloitte", + "delta", + "democrat", + "dental", + "dentist", + "desi", + "design", + "dev", + "dhl", + "diamonds", + "diet", + "digital", + "direct", + "directory", + "discount", + "discover", + "dish", + "diy", + "dj", + "dk", + "dm", + "dnp", + "do", + "docs", + "doctor", + "dog", + "domains", + "dot", + "download", + "drive", + "dtv", + "dubai", + "dupont", + "durban", + "dvag", + "dvr", + "dz", + "earth", + "eat", + "ec", + "eco", + "edeka", + "edu", + "education", + "ee", + "eg", + "email", + "emerck", + "energy", + "engineer", + "engineering", + "enterprises", + "epson", + "equipment", + "er", + "ericsson", + "erni", + "es", + "esq", + "estate", + "et", + "eu", + "eurovision", + "eus", + "events", + "exchange", + "expert", + "exposed", + "express", + "extraspace", + "fage", + "fail", + "fairwinds", + "faith", + "family", + "fan", + "fans", + "farm", + "farmers", + "fashion", + "fast", + "fedex", + "feedback", + "ferrari", + "ferrero", + "fi", + "fidelity", + "fido", + "film", + "final", + "finance", + "financial", + "fire", + "firestone", + "firmdale", + "fish", + "fishing", + "fit", + "fitness", + "fj", + "fk", + "flickr", + "flights", + "flir", + "florist", + "flowers", + "fly", + "fm", + "fo", + "foo", + "food", + "football", + "ford", + "forex", + "forsale", + "forum", + "foundation", + "fox", + "fr", + "free", + "fresenius", + "frl", + "frogans", + "frontier", + "ftr", + "fujitsu", + "fun", + "fund", + "furniture", + "futbol", + "fyi", + "ga", + "gal", + "gallery", + "gallo", + "gallup", + "game", + "games", + "gap", + "garden", + "gay", + "gb", + "gbiz", + "gd", + "gdn", + "ge", + "gea", + "gent", + "genting", + "george", + "gf", + "gg", + "ggee", + "gh", + "gi", + "gift", + "gifts", + "gives", + "giving", + "gl", + "glass", + "gle", + "global", + "globo", + "gm", + "gmail", + "gmbh", + "gmo", + "gmx", + "gn", + "godaddy", + "gold", + "goldpoint", + "golf", + "goodyear", + "goog", + "google", + "gop", + "got", + "gov", + "gp", + "gq", + "gr", + "grainger", + "graphics", + "gratis", + "green", + "gripe", + "grocery", + "group", + "gs", + "gt", + "gu", + "gucci", + "guge", + "guide", + "guitars", + "guru", + "gw", + "gy", + "hair", + "hamburg", + "hangout", + "haus", + "hbo", + "hdfc", + "hdfcbank", + "health", + "healthcare", + "help", + "helsinki", + "here", + "hermes", + "hiphop", + "hisamitsu", + "hitachi", + "hiv", + "hk", + "hkt", + "hm", + "hn", + "hockey", + "holdings", + "holiday", + "homedepot", + "homegoods", + "homes", + "homesense", + "honda", + "horse", + "hospital", + "host", + "hosting", + "hot", + "hotels", + "hotmail", + "house", + "how", + "hr", + "hsbc", + "ht", + "hu", + "hughes", + "hyatt", + "hyundai", + "ibm", + "icbc", + "ice", + "icu", + "id", + "ie", + "ieee", + "ifm", + "ikano", + "il", + "im", + "imamat", + "imdb", + "immo", + "immobilien", + "in", + "inc", + "industries", + "infiniti", + "info", + "ing", + "ink", + "institute", + "insurance", + "insure", + "int", + "international", + "intuit", + "investments", + "io", + "ipiranga", + "iq", + "ir", + "irish", + "is", + "ismaili", + "ist", + "istanbul", + "it", + "itau", + "itv", + "jaguar", + "java", + "jcb", + "je", + "jeep", + "jetzt", + "jewelry", + "jio", + "jll", + "jm", + "jmp", + "jnj", + "jo", + "jobs", + "joburg", + "jot", + "joy", + "jp", + "jpmorgan", + "jprs", + "juegos", + "juniper", + "kaufen", + "kddi", + "ke", + "kerryhotels", + "kerryproperties", + "kfh", + "kg", + "kh", + "ki", + "kia", + "kids", + "kim", + "kindle", + "kitchen", + "kiwi", + "km", + "kn", + "koeln", + "komatsu", + "kosher", + "kp", + "kpmg", + "kpn", + "kr", + "krd", + "kred", + "kuokgroup", + "kw", + "ky", + "kyoto", + "kz", + "la", + "lacaixa", + "lamborghini", + "lamer", + "land", + "landrover", + "lanxess", + "lasalle", + "lat", + "latino", + "latrobe", + "law", + "lawyer", + "lb", + "lc", + "lds", + "lease", + "leclerc", + "lefrak", + "legal", + "lego", + "lexus", + "lgbt", + "li", + "lidl", + "life", + "lifeinsurance", + "lifestyle", + "lighting", + "like", + "lilly", + "limited", + "limo", + "lincoln", + "link", + "live", + "living", + "lk", + "llc", + "llp", + "loan", + "loans", + "locker", + "locus", + "lol", + "london", + "lotte", + "lotto", + "love", + "lpl", + "lplfinancial", + "lr", + "ls", + "lt", + "ltd", + "ltda", + "lu", + "lundbeck", + "luxe", + "luxury", + "lv", + "ly", + "ma", + "madrid", + "maif", + "maison", + "makeup", + "man", + "management", + "mango", + "map", + "market", + "marketing", + "markets", + "marriott", + "marshalls", + "mattel", + "mba", + "mc", + "mckinsey", + "md", + "me", + "med", + "media", + "meet", + "melbourne", + "meme", + "memorial", + "men", + "menu", + "merck", + "merckmsd", + "mg", + "mh", + "miami", + "microsoft", + "mil", + "mini", + "mint", + "mit", + "mitsubishi", + "mk", + "ml", + "mlb", + "mls", + "mm", + "mma", + "mn", + "mo", + "mobi", + "mobile", + "moda", + "moe", + "moi", + "mom", + "monash", + "money", + "monster", + "mormon", + "mortgage", + "moscow", + "moto", + "motorcycles", + "mov", + "movie", + "mp", + "mq", + "mr", + "ms", + "msd", + "mt", + "mtn", + "mtr", + "mu", + "museum", + "music", + "mv", + "mw", + "mx", + "my", + "mz", + "na", + "nab", + "nagoya", + "name", + "navy", + "nba", + "nc", + "ne", + "nec", + "net", + "netbank", + "netflix", + "network", + "neustar", + "new", + "news", + "next", + "nextdirect", + "nexus", + "nf", + "nfl", + "ng", + "ngo", + "nhk", + "ni", + "nico", + "nike", + "nikon", + "ninja", + "nissan", + "nissay", + "nl", + "no", + "nokia", + "norton", + "now", + "nowruz", + "nowtv", + "np", + "nr", + "nra", + "nrw", + "ntt", + "nu", + "nyc", + "nz", + "obi", + "observer", + "office", + "okinawa", + "olayan", + "olayangroup", + "ollo", + "om", + "omega", + "one", + "ong", + "onl", + "online", + "ooo", + "open", + "oracle", + "orange", + "org", + "organic", + "origins", + "osaka", + "otsuka", + "ott", + "ovh", + "pa", + "page", + "panasonic", + "paris", + "pars", + "partners", + "parts", + "party", + "pay", + "pccw", + "pe", + "pet", + "pf", + "pfizer", + "pg", + "ph", + "pharmacy", + "phd", + "philips", + "phone", + "photo", + "photography", + "photos", + "physio", + "pics", + "pictet", + "pictures", + "pid", + "pin", + "ping", + "pink", + "pioneer", + "pizza", + "pk", + "pl", + "place", + "play", + "playstation", + "plumbing", + "plus", + "pm", + "pn", + "pnc", + "pohl", + "poker", + "politie", + "porn", + "post", + "pr", + "praxi", + "press", + "prime", + "pro", + "prod", + "productions", + "prof", + "progressive", + "promo", + "properties", + "property", + "protection", + "pru", + "prudential", + "ps", + "pt", + "pub", + "pw", + "pwc", + "py", + "qa", + "qpon", + "quebec", + "quest", + "racing", + "radio", + "re", + "read", + "realestate", + "realtor", + "realty", + "recipes", + "red", + "redumbrella", + "rehab", + "reise", + "reisen", + "reit", + "reliance", + "ren", + "rent", + "rentals", + "repair", + "report", + "republican", + "rest", + "restaurant", + "review", + "reviews", + "rexroth", + "rich", + "richardli", + "ricoh", + "ril", + "rio", + "rip", + "ro", + "rocks", + "rodeo", + "rogers", + "room", + "rs", + "rsvp", + "ru", + "rugby", + "ruhr", + "run", + "rw", + "rwe", + "ryukyu", + "sa", + "saarland", + "safe", + "safety", + "sakura", + "sale", + "salon", + "samsclub", + "samsung", + "sandvik", + "sandvikcoromant", + "sanofi", + "sap", + "sarl", + "sas", + "save", + "saxo", + "sb", + "sbi", + "sbs", + "sc", + "scb", + "schaeffler", + "schmidt", + "scholarships", + "school", + "schule", + "schwarz", + "science", + "scot", + "sd", + "se", + "search", + "seat", + "secure", + "security", + "seek", + "select", + "sener", + "services", + "seven", + "sew", + "sex", + "sexy", + "sfr", + "sg", + "sh", + "shangrila", + "sharp", + "shell", + "shia", + "shiksha", + "shoes", + "shop", + "shopping", + "shouji", + "show", + "si", + "silk", + "sina", + "singles", + "site", + "sj", + "sk", + "ski", + "skin", + "sky", + "skype", + "sl", + "sling", + "sm", + "smart", + "smile", + "sn", + "sncf", + "so", + "soccer", + "social", + "softbank", + "software", + "sohu", + "solar", + "solutions", + "song", + "sony", + "soy", + "spa", + "space", + "sport", + "spot", + "sr", + "srl", + "ss", + "st", + "stada", + "staples", + "star", + "statebank", + "statefarm", + "stc", + "stcgroup", + "stockholm", + "storage", + "store", + "stream", + "studio", + "study", + "style", + "su", + "sucks", + "supplies", + "supply", + "support", + "surf", + "surgery", + "suzuki", + "sv", + "swatch", + "swiss", + "sx", + "sy", + "sydney", + "systems", + "sz", + "tab", + "taipei", + "talk", + "taobao", + "target", + "tatamotors", + "tatar", + "tattoo", + "tax", + "taxi", + "tc", + "tci", + "td", + "tdk", + "team", + "tech", + "technology", + "tel", + "temasek", + "tennis", + "teva", + "tf", + "tg", + "th", + "thd", + "theater", + "theatre", + "tiaa", + "tickets", + "tienda", + "tips", + "tires", + "tirol", + "tj", + "tjmaxx", + "tjx", + "tk", + "tkmaxx", + "tl", + "tm", + "tmall", + "tn", + "to", + "today", + "tokyo", + "tools", + "top", + "toray", + "toshiba", + "total", + "tours", + "town", + "toyota", + "toys", + "tr", + "trade", + "trading", + "training", + "travel", + "travelers", + "travelersinsurance", + "trust", + "trv", + "tt", + "tube", + "tui", + "tunes", + "tushu", + "tv", + "tvs", + "tw", + "tz", + "ua", + "ubank", + "ubs", + "ug", + "uk", + "unicom", + "university", + "uno", + "uol", + "ups", + "us", + "uy", + "uz", + "va", + "vacations", + "vana", + "vanguard", + "vc", + "ve", + "vegas", + "ventures", + "verisign", + "versicherung", + "vet", + "vg", + "vi", + "viajes", + "video", + "vig", + "viking", + "villas", + "vin", + "vip", + "virgin", + "visa", + "vision", + "viva", + "vivo", + "vlaanderen", + "vn", + "vodka", + "volvo", + "vote", + "voting", + "voto", + "voyage", + "vu", + "wales", + "walmart", + "walter", + "wang", + "wanggou", + "watch", + "watches", + "weather", + "weatherchannel", + "web", + "webcam", + "weber", + "website", + "wed", + "wedding", + "weibo", + "weir", + "wf", + "whoswho", + "wien", + "wiki", + "williamhill", + "win", + "windows", + "wine", + "winners", + "wme", + "woodside", + "work", + "works", + "world", + "wow", + "ws", + "wtc", + "wtf", + "xbox", + "xerox", + "xihuan", + "xin", + "xn--11b4c3d", + "xn--1ck2e1b", + "xn--1qqw23a", + "xn--2scrj9c", + "xn--30rr7y", + "xn--3bst00m", + "xn--3ds443g", + "xn--3e0b707e", + "xn--3hcrj9c", + "xn--3pxu8k", + "xn--42c2d9a", + "xn--45br5cyl", + "xn--45brj9c", + "xn--45q11c", + "xn--4dbrk0ce", + "xn--4gbrim", + "xn--54b7fta0cc", + "xn--55qw42g", + "xn--55qx5d", + "xn--5su34j936bgsg", + "xn--5tzm5g", + "xn--6frz82g", + "xn--6qq986b3xl", + "xn--80adxhks", + "xn--80ao21a", + "xn--80aqecdr1a", + "xn--80asehdb", + "xn--80aswg", + "xn--8y0a063a", + "xn--90a3ac", + "xn--90ae", + "xn--90ais", + "xn--9dbq2a", + "xn--9et52u", + "xn--9krt00a", + "xn--b4w605ferd", + "xn--bck1b9a5dre4c", + "xn--c1avg", + "xn--c2br7g", + "xn--cck2b3b", + "xn--cckwcxetd", + "xn--cg4bki", + "xn--clchc0ea0b2g2a9gcd", + "xn--czr694b", + "xn--czrs0t", + "xn--czru2d", + "xn--d1acj3b", + "xn--d1alf", + "xn--e1a4c", + "xn--eckvdtc9d", + "xn--efvy88h", + "xn--fct429k", + "xn--fhbei", + "xn--fiq228c5hs", + "xn--fiq64b", + "xn--fiqs8s", + "xn--fiqz9s", + "xn--fjq720a", + "xn--flw351e", + "xn--fpcrj9c3d", + "xn--fzc2c9e2c", + "xn--fzys8d69uvgm", + "xn--g2xx48c", + "xn--gckr3f0f", + "xn--gecrj9c", + "xn--gk3at1e", + "xn--h2breg3eve", + "xn--h2brj9c", + "xn--h2brj9c8c", + "xn--hxt814e", + "xn--i1b6b1a6a2e", + "xn--imr513n", + "xn--io0a7i", + "xn--j1aef", + "xn--j1amh", + "xn--j6w193g", + "xn--jlq480n2rg", + "xn--jvr189m", + "xn--kcrx77d1x4a", + "xn--kprw13d", + "xn--kpry57d", + "xn--kput3i", + "xn--l1acc", + "xn--lgbbat1ad8j", + "xn--mgb9awbf", + "xn--mgba3a3ejt", + "xn--mgba3a4f16a", + "xn--mgba7c0bbn0a", + "xn--mgbaam7a8h", + "xn--mgbab2bd", + "xn--mgbah1a3hjkrd", + "xn--mgbai9azgqp6j", + "xn--mgbayh7gpa", + "xn--mgbbh1a", + "xn--mgbbh1a71e", + "xn--mgbc0a9azcg", + "xn--mgbca7dzdo", + "xn--mgbcpq6gpa1a", + "xn--mgberp4a5d4ar", + "xn--mgbgu82a", + "xn--mgbi4ecexp", + "xn--mgbpl2fh", + "xn--mgbt3dhd", + "xn--mgbtx2b", + "xn--mgbx4cd0ab", + "xn--mix891f", + "xn--mk1bu44c", + "xn--mxtq1m", + "xn--ngbc5azd", + "xn--ngbe9e0a", + "xn--ngbrx", + "xn--node", + "xn--nqv7f", + "xn--nqv7fs00ema", + "xn--nyqy26a", + "xn--o3cw4h", + "xn--ogbpf8fl", + "xn--otu796d", + "xn--p1acf", + "xn--p1ai", + "xn--pgbs0dh", + "xn--pssy2u", + "xn--q7ce6a", + "xn--q9jyb4c", + "xn--qcka1pmc", + "xn--qxa6a", + "xn--qxam", + "xn--rhqv96g", + "xn--rovu88b", + "xn--rvc1e0am3e", + "xn--s9brj9c", + "xn--ses554g", + "xn--t60b56a", + "xn--tckwe", + "xn--tiq49xqyj", + "xn--unup4y", + "xn--vermgensberater-ctb", + "xn--vermgensberatung-pwb", + "xn--vhquv", + "xn--vuq861b", + "xn--w4r85el8fhu5dnra", + "xn--w4rs40l", + "xn--wgbh1c", + "xn--wgbl6a", + "xn--xhq521b", + "xn--xkc2al3hye2a", + "xn--xkc2dl3a5ee0h", + "xn--y9a3aq", + "xn--yfro4i67o", + "xn--ygbi2ammx", + "xn--zfr164b", + "xxx", + "xyz", + "yachts", + "yahoo", + "yamaxun", + "yandex", + "ye", + "yodobashi", + "yoga", + "yokohama", + "you", + "youtube", + "yt", + "yun", + "za", + "zappos", + "zara", + "zero", + "zip", + "zm", + "zone", + "zuerich", + "zw", +]); + +/** Does this ending collide with a real top-level domain? */ +export function isRealTld(tld) { + return IANA_TLDS.has(String(tld ?? "").trim().toLowerCase()); +} diff --git a/test/iana-tlds.test.mjs b/test/iana-tlds.test.mjs new file mode 100644 index 00000000..cbe18a85 --- /dev/null +++ b/test/iana-tlds.test.mjs @@ -0,0 +1,109 @@ +// Moshpit must not sell, or answer for, an ending that already exists. +// +// `.sh` was claimed through the ordinary registration flow for $2. It is Saint +// Helena's ccTLD, and it is also where this project's own registry lives: +// `pit.moshcode.sh`. Every bridge that fetched the ending list therefore +// learned to treat its own registry as a Moshpit name — refused to forward it, +// tried to resolve it through the registry it had just made unreachable, and +// returned NXDOMAIN. A resolver that cannot resolve the thing it needs in order +// to resolve. +// +// It took the rest of the ccTLD with it. On a machine running the bridge as its +// resolver, every real `.sh` site answered a parking IP instead. +// +// So both halves are tested here. The registry must refuse to sell one, which +// stops it happening again; the bridge must refuse to answer for one, which is +// what protects a person whose registry already sold 622 of them. Neither is +// sufficient alone — the second is the one that works today. +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; + +import { IANA_TLDS, IANA_VERSION, isRealTld } from "../src/iana-tlds.mjs"; +import { isOurs } from "../src/dns.mjs"; +import { tldRejection } from "../apps/pwa/src/lib/moshpit-name.mjs"; + +/* ------------------------------------------------------------- the list */ + +test("the list is the real one, not a stub someone shortened", () => { + assert.ok(IANA_TLDS.size > 1000, `only ${IANA_TLDS.size} TLDs — a truncated list silently re-opens the hole`); + assert.match(IANA_VERSION, /Version \d{10}/); + for (const real of ["com", "net", "org", "sh", "ai", "dev", "app", "io", "uk", "xn--p1ai"]) { + assert.equal(isRealTld(real), true, `${real} is a real TLD`); + } +}); + +test("endings that are not real TLDs stay claimable", () => { + // The namespace has to keep working. These are live Moshpit endings. + for (const made of ["eggs", "hacker", "2600", "moshpit", "agentic"]) { + assert.equal(isRealTld(made), false, `${made} is not delegated by IANA and must stay available`); + } +}); + +test("case and dots do not smuggle one past", () => { + assert.equal(isRealTld("SH"), true); + assert.equal(isRealTld(" sh "), true); + assert.equal(isRealTld(""), false); + assert.equal(isRealTld(null), false); + assert.equal(isRealTld(undefined), false); +}); + +/* -------------------------------------------------------- the registry */ + +test("the registry refuses to sell a real top-level domain", () => { + for (const real of ["sh", "ai", "dev", "app", "store", "blog", "aws"]) { + assert.match(tldRejection(real) || "", /real top-level domain/, `.${real} must be refused`); + } +}); + +test("the registry still sells endings that are not real TLDs", () => { + for (const made of ["eggs", "hacker", "2600", "agentic"]) { + assert.equal(tldRejection(made), null, `.${made} must stay registerable`); + } +}); + +test("the reserved list still does its own job", () => { + // `openai` and `moshpit` are not IANA TLDs, so they are refused for the older + // reason — and that reason must not have been replaced by this one. (`bank` + // makes a poor example here: it really is a delegated gTLD.) + for (const name of ["openai", "anthropic", "moshpit", "police"]) { + assert.equal(isRealTld(name), false, `${name} is not IANA-delegated`); + assert.match(tldRejection(name) || "", /reserved/, `.${name} must still be refused as reserved`); + } +}); + +/* ---------------------------------------------------------- the bridge */ + +test("the bridge never answers for a real TLD, even when the registry sold it", () => { + // Exactly the state of the live registry: `sh` present in the ending set. + const sold = new Set(["sh", "ai", "dev", "eggs", "2600"]); + assert.equal(isOurs("pit.moshcode.sh", sold), false, "its own registry must be forwarded, not swallowed"); + assert.equal(isOurs("moshcoding.sh", sold), false); + assert.equal(isOurs("anything.ai", sold), false); + assert.equal(isOurs("foo.dev", sold), false); +}); + +test("the bridge still answers for the endings Moshpit actually owns", () => { + const sold = new Set(["sh", "ai", "dev", "eggs", "2600"]); + assert.equal(isOurs("blue.eggs", sold), true); + assert.equal(isOurs("alt.2600", sold), true); +}); + +test("an ending the registry never sold is still not ours", () => { + assert.equal(isOurs("blue.eggs", new Set(["hacker"])), false); + assert.equal(isOurs("blue.eggs", new Set()), false); +}); + +/* ------------------------------------------------------------- vendoring */ + +test("the CLI's copy and the Pit's copy are identical", async () => { + // They live in two packages that never import each other — `src/` ships to + // npm, `apps/pwa/` deploys as the Pit and is not a workspace. A list that + // drifts is a registry selling what the bridge refuses to answer for, or + // worse, the reverse. + const [cli, pit] = await Promise.all([ + readFile(new URL("../src/iana-tlds.mjs", import.meta.url), "utf8"), + readFile(new URL("../apps/pwa/src/lib/iana-tlds.mjs", import.meta.url), "utf8"), + ]); + assert.equal(cli, pit, "regenerate both with: node scripts/generate-iana-tlds.mjs"); +});