Skip to content
Closed
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
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,11 @@ RAILWAY_API_TOKEN=
# 'queries' logs one line per query (no client addresses are stored anywhere),
# 'off' silences startup lines too.
# MOSHPIT_DNS_LOG=

# The resolvers advertised on pit.moshcode.sh/dns. Addresses, optionally named:
# MOSHPIT_DNS_RESOLVERS=dns1.pit.moshcode.sh=203.0.113.7,dns2.pit.moshcode.sh=203.0.113.8
# Unset, the page says the resolvers are not published yet and explains how to
# run one — it never invents an address for someone to paste into their network
# settings. Set on the WEB app (the resolver process does not read these).
# MOSHPIT_DNS_RESOLVERS=
# MOSHPIT_DOH_URL=https://dns.pit.moshcode.sh/dns-query
17 changes: 17 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -488,3 +488,20 @@ dialog.qhelp::backdrop { background: rgba(0,0,0,.72); backdrop-filter: blur(2px)
font-family: var(--mono); background: var(--panel);
border: 1px solid var(--line); border-radius: 6px; padding: 6px 12px;
}

/* ---- /pit/dns — resolver setup (PRD 0004 R1) ---- */
.dns-addrs { list-style: none; display: flex; flex-wrap: wrap; gap: 12px; padding: 0; margin: 0 0 18px; }
.dns-addrs li {
display: flex; flex-direction: column; gap: 4px;
background: var(--panel); border: 1px solid var(--line);
border-left: 3px solid var(--acid); border-radius: 8px; padding: 12px 16px;
}
/* The address is the thing people copy, so it gets the size and the contrast. */
.dns-ip { font-family: var(--mono); font-size: 1.25rem; color: var(--acid); user-select: all; }
.dns-host { font-family: var(--mono); font-size: .8rem; color: var(--ash); }
.dns-pre {
background: #0b0d0a; border: 1px solid var(--line); border-left: 3px solid var(--acid);
border-radius: 8px; padding: 14px 16px; overflow-x: auto; max-width: 62ch;
font-family: var(--mono); font-size: .88rem; line-height: 1.7; color: var(--bone);
}
.dns-pre code { font-family: inherit; background: none; padding: 0; }
185 changes: 185 additions & 0 deletions app/pit/dns/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import type { Metadata } from "next";
import Nav from "@/components/Nav";
import { resolverConfig } from "@/lib/moshpit-resolvers";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

export const metadata: Metadata = {
title: "Moshpit DNS — reach .moshpit without an extension",
description:
"Point your device at the Moshpit resolvers and custom TLDs resolve like any other name. The rest of the internet keeps working.",
};

/**
* pit.moshcode.sh/dns — the setup instructions for the public resolvers.
*
* The addresses come from the environment (see lib/moshpit-resolvers.ts): a
* page that hardcoded them would keep telling people to use a box that moved.
* When none are configured the page says so plainly and explains how to run
* one, because inventing an address for someone to paste into their network
* settings is worse than admitting the resolvers are not up yet.
*/
export default function MoshpitDnsPage() {
const { resolvers, doh, published } = resolverConfig();

return (
<div id="site">
<Nav />

<section className="hero">
<div className="hero-copy">
<p className="eyebrow">
<span className="dot" /> moshpit dns
</p>
<h1 className="title">
ONE SETTING.
<br />
<span className="acid">.ANYTHING</span> RESOLVES.
</h1>
<p className="pit-lede">
Moshpit names live outside the traditional DNS root, so a normal browser has
nowhere to look them up. These resolvers know where. Point a laptop, a phone or a
whole router at one and <code>.moshpit</code>, <code>.eggs</code>,{" "}
<code>.yeah</code> resolve like any other name — while <code>.com</code>,{" "}
<code>.org</code> and the rest of the internet keep working exactly as before,
forwarded on to 8.8.8.8 and 1.1.1.1.
</p>
<p className="pit-lede pit-dim">
No extension, no app, no account. Every operating system already has this setting.
</p>
</div>
</section>

<section className="pit-how">
<p className="tag">// the addresses</p>
{published ? (
<>
<ul className="dns-addrs">
{resolvers.map((r) => (
<li key={r.address}>
<code className="dns-ip">{r.address}</code>
{r.name ? <span className="dns-host">{r.name}</span> : null}
</li>
))}
</ul>
<p className="pit-lede pit-dim">
Use both, in that order. The second is there so the first can be rebooted
without the namespace going with it. You type the addresses, not the names —
a resolver&apos;s own name cannot be looked up until you already have a
working resolver.
</p>
</>
) : (
<>
<p className="pit-lede">
<strong>Not published yet.</strong> The resolver is built and tested, but no
public instance is announced here — and this page will not invent an address
for you to paste into your network settings.
</p>
<p className="pit-lede pit-dim">
You can run your own today (see below), and it works for every name in the
namespace, not just yours.
</p>
</>
)}
</section>

<section className="pit-how">
<p className="tag">// set it up</p>
<ol className="pit-steps">
<li>
<strong>macOS.</strong> System Settings → Network → your connection → Details →
DNS. Add the address with <code>+</code>, drag it to the top, Save.
</li>
<li>
<strong>Windows.</strong> Settings → Network &amp; Internet → your adapter → DNS
server assignment → Edit → Manual, switch IPv4 on and set the preferred server.
</li>
<li>
<strong>Linux.</strong> <code>resolvectl dns &lt;interface&gt; &lt;address&gt;</code>{" "}
under systemd-resolved, or a <code>nameserver</code> line in{" "}
<code>/etc/resolv.conf</code>.
</li>
<li>
<strong>Router.</strong> Set it as the DNS your router hands out over DHCP and
every device on the network gets the namespace. This is the setup it is really
for.
</li>
<li>
<strong>A locked-down machine</strong> where DNS is not yours to change: use DNS
over HTTPS in the browser instead — Firefox under Privacy &amp; Security → DNS
over HTTPS → custom provider, Chrome under Security → Use secure DNS → custom.
{doh ? (
<>
{" "}
The endpoint is <code>{doh}</code>.
</>
) : (
<> An endpoint is published here once a resolver is up.</>
)}
</li>
</ol>
</section>

<section className="pit-how">
<p className="tag">// check it worked</p>
<pre className="dns-pre">
<code>{`dig +short anything.moshpit # an address, not an error
dig +short example.com # the ordinary internet, still fine

nslookup anything.moshpit # the Windows spelling`}</code>
</pre>
<p className="pit-lede pit-dim">
A <code>TXT</code> lookup on any Moshpit name reports which registry and gateway
answered, which is the fastest way to tell a resolver problem from a site problem.
</p>
</section>

<section className="pit-how">
<p className="tag">// what still breaks</p>
<p className="pit-lede">
<code>https://</code> on a Moshpit name will warn. No public certificate authority
will issue a certificate for <code>scrambled.eggs</code>, because none of them
recognise a namespace that does not descend from the ICANN root. Plain{" "}
<code>http://</code> works, and so does the clearnet page for the name here on{" "}
<code>pit.moshcode.sh</code>. A certificate authority you opt into is the real
answer, and it is not built yet.
</p>
<p className="pit-lede pit-dim">
Clearnet lookups are forwarded to Google and Cloudflare, which is what a forwarder
does — run your own resolver if that trade is wrong for you, and point it wherever
you like.
</p>
</section>

<section className="pit-how">
<p className="tag">// run your own</p>
<p className="pit-lede">
The resolver is in the open, has no dependencies and no database, and reads the
registry over ordinary HTTPS. Nothing about it privileges ours — a private pit
points at a different registry, a household one runs on whatever is already on the
shelf.
</p>
<pre className="dns-pre">
<code>{`git clone https://github.com/moshcoder/moshcoding
cd moshcoding && bun run dns # port 5354, no privileges needed

dig @127.0.0.1 -p 5354 +short anything.moshpit`}</code>
</pre>
<p className="pit-lede pit-dim">
Setup, deployment and the operating notes:{" "}
<a
className="acid"
href="https://github.com/moshcoder/moshcoding/blob/master/docs/moshpit-dns.md"
target="_blank"
rel="noopener noreferrer"
>
docs/moshpit-dns.md
</a>
. Claim a name first over at <a className="acid" href="/pit">the pit</a>.
</p>
</section>
</div>
);
}
11 changes: 8 additions & 3 deletions app/pit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,14 @@ export default async function PitPage() {
<p className="tag">// reaching a .moshpit address</p>
<p className="pit-lede">
These names live outside the traditional DNS root, so a normal browser doesn&apos;t
know where to look. This page is the way in until the resolver ships: names are
registered and looked up here over ordinary HTTPS, and{" "}
<code>pit.moshcode.sh</code> stays a working entry point for anyone without it.
know where to look. Two ways in.{" "}
<a className="acid" href="/pit/dns">
Point your device at the Moshpit resolvers
</a>{" "}
and <code>.anything</code> resolves everywhere on that device — one setting, no
install, and the rest of the internet keeps working. Or skip it: names are
registered and looked up here over ordinary HTTPS, and <code>pit.moshcode.sh</code>{" "}
stays a working entry point for anyone who has changed nothing.
</p>
<p className="pit-lede pit-dim">
The browser extension that resolves <code>.anything</code> natively is not out yet.
Expand Down
22 changes: 19 additions & 3 deletions docs/moshpit-dns.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,20 @@ dns2.pit A <resolver 2 address>
dns A <load balancer or resolver 1> ; DoH: https://dns.pit.moshcode.sh/dns-query
```

These have to be **explicit** records. The wildcard that serves the HTTP
gateway (`*.pit.moshcode.sh`) would otherwise answer for `dns1.pit` and point
people's resolvers at a web server; an exact name always beats a wildcard.
These have to be **explicit** records, and they have to stay explicit. There is
no `*.pit.moshcode.sh` wildcard today — only `pit.moshcode.sh` itself resolves,
to Railway — but PRD `0004` R1 calls for one, and the day it is added it would
otherwise answer for `dns1.pit` and point people's resolvers at a web server.
An exact name always beats a wildcard, so publishing these first is what makes
that safe.

Then tell the site about them, so `pit.moshcode.sh/dns` shows real addresses
instead of "not published yet":

```
MOSHPIT_DNS_RESOLVERS=dns1.pit.moshcode.sh=<address>,dns2.pit.moshcode.sh=<address>
MOSHPIT_DOH_URL=https://dns.pit.moshcode.sh/dns-query
```

Then set `MOSHPIT_GATEWAY_HOST=pit.moshcode.sh` (the default) and the resolvers
follow the gateway wherever it moves, because they look its address up through
Expand All @@ -112,6 +123,11 @@ a resolver on a Raspberry Pi for one household. Nothing here privileges

## Using one

The user-facing version of this section is a page on the site itself —
`pit.moshcode.sh/dns` (`app/pit/dns/page.tsx`) — which reads the addresses from
`MOSHPIT_DNS_RESOLVERS` and walks through the same steps without asking anyone
to read a repo.

**macOS** — System Settings → Network → your connection → Details → DNS, and
add `dns1.pit.moshcode.sh`'s address as the first server.

Expand Down
65 changes: 65 additions & 0 deletions lib/moshpit-resolvers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// The public resolvers, as advertised on the site.
//
// Read from the environment rather than hardcoded, because the addresses are
// operational facts that change when a box moves, and a setup page that tells
// people to type a stale address is worse than one that tells them nothing.
//
// Addresses are validated here for the same reason: this list goes onto a page
// where strangers copy it into their network settings. A typo in an env var
// should show up as a missing entry, not as an instruction to point their DNS
// at something that is not an address at all.

export type PublicResolver = {
/** A human name for the row, when the operator gave one. */
name: string | null;
address: string;
};

const IPV4 = /^\d{1,3}(\.\d{1,3}){3}$/;

export function isIpAddress(value: string): boolean {
const raw = String(value ?? "").trim();
if (IPV4.test(raw)) return raw.split(".").every((octet) => Number(octet) <= 255);
// Loose on IPv6 by design: the exact grammar lives in the resolver's codec,
// and this only has to reject things that are obviously not addresses.
return /^[0-9a-f:]+$/i.test(raw) && raw.includes(":") && !raw.includes(":::");
}

/**
* Parse `dns1.pit.moshcode.sh=203.0.113.7, dns2.pit.moshcode.sh=203.0.113.8`.
*
* The name is optional — `203.0.113.7` on its own is a complete instruction,
* since what a person types into their DNS settings is an address. The name is
* there so the page can say which box they are pointing at.
*/
export function parseResolvers(spec: string | undefined | null): PublicResolver[] {
return String(spec ?? "")
.split(/[,\s]+/)
.map((entry) => entry.trim())
.filter(Boolean)
.map((entry) => {
const at = entry.lastIndexOf("=");
const name = at > 0 ? entry.slice(0, at).trim() : null;
const address = (at > 0 ? entry.slice(at + 1) : entry).trim();
return { name: name || null, address };
})
.filter((resolver) => isIpAddress(resolver.address));
}

export type ResolverConfig = {
resolvers: PublicResolver[];
/** The DoH endpoint, when one is published. */
doh: string | null;
/** Whether there is anything to tell people to use yet. */
published: boolean;
};

export function resolverConfig(env: Record<string, string | undefined> = process.env): ResolverConfig {
const resolvers = parseResolvers(env.MOSHPIT_DNS_RESOLVERS);
const doh = (env.MOSHPIT_DOH_URL || "").trim();
return {
resolvers,
doh: /^https:\/\/\S+$/.test(doh) ? doh : null,
published: resolvers.length > 0,
};
}
49 changes: 49 additions & 0 deletions tests/moshpit-resolvers.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// The resolver list goes onto a public page for strangers to copy into their
// network settings, so a malformed entry has to disappear rather than be
// rendered as an instruction.
import assert from "node:assert/strict";
import test from "node:test";

import { isIpAddress, parseResolvers, resolverConfig } from "../lib/moshpit-resolvers.ts";

test("resolvers parse with or without a name", () => {
assert.deepEqual(parseResolvers("dns1.pit.moshcode.sh=203.0.113.7, 203.0.113.8"), [
{ name: "dns1.pit.moshcode.sh", address: "203.0.113.7" },
{ name: null, address: "203.0.113.8" },
]);
});

test("anything that is not an address is dropped, not printed", () => {
assert.deepEqual(parseResolvers("dns1=not-an-address, 203.0.113.999, =, 203.0.113.7"), [
{ name: null, address: "203.0.113.7" },
]);
assert.deepEqual(parseResolvers(""), []);
assert.deepEqual(parseResolvers(undefined), []);
});

test("IPv6 resolvers are accepted", () => {
assert.deepEqual(parseResolvers("dns1=2606:4700::1111"), [{ name: "dns1", address: "2606:4700::1111" }]);
assert.equal(isIpAddress("::1"), true);
assert.equal(isIpAddress("2606:4700:::1111"), false);
assert.equal(isIpAddress("hello"), false);
});

test("an unconfigured deployment says nothing is published rather than guessing", () => {
const config = resolverConfig({});
assert.equal(config.published, false);
assert.deepEqual(config.resolvers, []);
assert.equal(config.doh, null);
});

test("the DoH endpoint has to be an https URL to be advertised", () => {
assert.equal(resolverConfig({ MOSHPIT_DOH_URL: "https://dns.pit.moshcode.sh/dns-query" }).doh, "https://dns.pit.moshcode.sh/dns-query");
// Plain HTTP would be advertised as "secure DNS" in a browser settings pane.
assert.equal(resolverConfig({ MOSHPIT_DOH_URL: "http://dns.pit.moshcode.sh/dns-query" }).doh, null);
assert.equal(resolverConfig({ MOSHPIT_DOH_URL: "dns.pit.moshcode.sh" }).doh, null);
});

test("a configured deployment is published", () => {
const config = resolverConfig({ MOSHPIT_DNS_RESOLVERS: "dns1.pit.moshcode.sh=203.0.113.7" });
assert.equal(config.published, true);
assert.equal(config.resolvers[0].address, "203.0.113.7");
});
Loading