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
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ moshpit-dns service uninstall stop doing that
moshpit-dns tlds [--json] list the endings claimed in the Pit
moshpit-dns records <name> [CNAME|MX|TXT] [--json]
inspect records published for a name
moshpit-dns resolve <name> [--json]
what a name resolves to, and why
moshpit-dns resolve <name...> [--json]
what names resolve to, and why
moshpit-dns start [--ttl N] run the bridge in the foreground
moshpit-dns install print the resolver config without applying it
```
Expand All @@ -76,9 +76,14 @@ records, the final DNS address for a resolution, and structured status warnings
without mixing human-readable lines into stdout.
Failures such as an unreachable registry still produce valid JSON and a
non-zero exit status where the command normally fails.
For compatibility, resolving one name returns the established JSON object.
Resolving multiple names returns an ordered array and exits non-zero when any
name is invalid or the registry cannot answer it. Repeated names share one
registry lookup while still retaining their original positions in the output.

```sh
moshpit-dns resolve california.oranges --json | jq .address
moshpit-dns resolve california.oranges blue.eggs --json | jq '.[].address'
moshpit-dns records california.oranges MX --json | jq '.records[]'
moshpit-dns status --json | jq '.warnings[]?.code'
```
Expand Down
49 changes: 33 additions & 16 deletions bin/moshpit-dns.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ const USAGE = `moshpit-dns — resolve Moshpit names on this machine
moshpit-dns tlds [--json] list the endings claimed in the Pit
moshpit-dns records <name> [CNAME|MX|TXT] [--json]
inspect records published for a name
moshpit-dns resolve <name> [--json]
show what a name resolves to, and why
moshpit-dns resolve <name...> [--json]
show what names resolve to, and why
moshpit-dns start run the bridge in the foreground
moshpit-dns install print the resolver config without applying it

Expand Down Expand Up @@ -366,27 +366,44 @@ export async function run(argv = process.argv.slice(2)) {
}

if (sub === "resolve") {
const name = positionals()[0];
if (!name) {
const names = positionals();
if (!names[0]) {
if (json) outJson({ name: null, error: "missing name" });
else out("usage: moshpit-dns resolve <name>");
return 1;
}
const result = await resolveName(name, { registryBase, timeoutMs });
const park = result.status === "parked" ? await parkingAddress() : null;
const address = result.status === "live" ? result.target : park;
const report = buildResolutionReport(name, result, address, registryBase);

const lookups = new Map();
let parkingLookup;
const reports = [];
for (const name of names) {
let lookup = lookups.get(name);
if (!lookup) {
lookup = resolveName(name, { registryBase, timeoutMs });
lookups.set(name, lookup);
}
const result = await lookup;
if (result.status === "parked" && !parkingLookup) parkingLookup = parkingAddress();
const park = result.status === "parked" ? await parkingLookup : null;
const address = result.status === "live" ? result.target : park;
const report = buildResolutionReport(name, result, address, registryBase);
reports.push({ name, result, park, report });
}
if (json) {
outJson(report);
outJson(reports.length === 1 ? reports[0].report : reports.map(({ report }) => report));
} else {
out({
live: () => `${name} → ${result.target}`,
parked: () => `${name} → ${park || "(parking host unresolvable)"} [${report.reason}]`,
unreachable: () => `${name} → NXDOMAIN [${report.reason}]`,
"not-a-name": () => `${name} → NXDOMAIN [${report.reason}]`,
}[result.status]());
for (const { name, result, park, report } of reports) {
out({
live: () => `${name} → ${result.target}`,
parked: () => `${name} → ${park || "(parking host unresolvable)"} [${report.reason}]`,
unreachable: () => `${name} → NXDOMAIN [${report.reason}]`,
"not-a-name": () => `${name} → NXDOMAIN [${report.reason}]`,
}[result.status]());
}
}
return result.status === "live" || result.status === "parked" ? 0 : 1;
return reports.every(({ result }) => result.status === "live" || result.status === "parked")
? 0
: 1;
}

if (sub === "start") {
Expand Down
114 changes: 113 additions & 1 deletion test/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ async function startRegistry(t, onRequest = () => {}) {
response.end(JSON.stringify({ name_registered: false, target: null, records: [] }));
return;
}
if (request.url === "/api/moshpit/resolve?name=empty.eggs&records=1") {
if (request.url === "/api/moshpit/resolve?name=empty.eggs"
|| request.url === "/api/moshpit/resolve?name=empty.eggs&records=1") {
response.end(JSON.stringify({ name_registered: true, target: null, records: [] }));
return;
}
Expand Down Expand Up @@ -140,6 +141,113 @@ test("resolve --json reports the address and decision reason", async (t) => {
});
});

test("resolve reports multiple names in input order", async (t) => {
let requests = 0;
const registry = await startRegistry(t, (request) => {
if (request.url.startsWith("/api/moshpit/resolve")) requests += 1;
});
const result = await run([
"resolve",
"--timeout",
"1500",
"blue.eggs",
"--registry",
registry,
"localhost",
"blue.eggs",
"--json",
]);

assert.equal(result.status, 1);
assert.equal(requests, 1);
assert.deepEqual(jsonOutput(result), [
{
registry,
name: "blue.eggs",
status: "live",
address: "203.0.113.9",
target: "203.0.113.9",
registered: null,
reason: "registry target",
},
{
registry,
name: "localhost",
status: "not-a-name",
address: null,
target: null,
registered: null,
reason: "not a Moshpit name: one label and one ending",
},
{
registry,
name: "blue.eggs",
status: "live",
address: "203.0.113.9",
target: "203.0.113.9",
registered: null,
reason: "registry target",
},
]);

const human = await run([
"resolve",
"blue.eggs",
"localhost",
"--registry",
registry,
]);
assert.equal(human.status, 1);
assert.equal(human.stderr, "");
assert.equal(
human.stdout,
"blue.eggs → 203.0.113.9\n"
+ "localhost → NXDOMAIN [not a Moshpit name: one label and one ending]\n",
);
});

test("resolve returns success when every batch entry resolves", async (t) => {
const registry = await startRegistry(t);
const result = await run([
"resolve",
"blue.eggs",
"blue.eggs",
"--registry",
registry,
"--json",
]);

assert.equal(result.status, 0);
const reports = jsonOutput(result);
assert.equal(reports.length, 2);
assert.deepEqual(reports[0], reports[1]);
});

test("resolve reuses lookups for repeated parked names", async (t) => {
let requests = 0;
const registry = await startRegistry(t, (request) => {
if (request.url === "/api/moshpit/resolve?name=empty.eggs") requests += 1;
});
const result = await run([
"resolve",
"empty.eggs",
"empty.eggs",
"--registry",
registry,
"--json",
]);

assert.equal(result.status, 0);
assert.equal(requests, 1);
const reports = jsonOutput(result);
assert.equal(reports.length, 2);
assert.equal(reports[0].status, "parked");
assert.equal(reports[0].target, null);
assert.equal(reports[0].registered, true);
assert.equal(reports[0].reason, "claimed, not pointed at an IP");
assert.deepEqual(reports[0], reports[1]);
});

test("records inspects and filters the registry record set", async (t) => {
const registry = await startRegistry(t);
const human = await run(["records", "blue.eggs", "mx", "--registry", registry]);
Expand Down Expand Up @@ -309,6 +417,10 @@ test("resolve --json keeps malformed and missing names machine-readable", async
const missing = await run(["resolve", "--json"]);
assert.equal(missing.status, 1);
assert.deepEqual(jsonOutput(missing), { name: null, error: "missing name" });

const empty = await run(["resolve", "", "--json"]);
assert.equal(empty.status, 1);
assert.deepEqual(jsonOutput(empty), { name: null, error: "missing name" });
});

test("tlds --json returns valid JSON when the registry is unreachable", async () => {
Expand Down
Loading