- dnsmasq holds mesh-dns-resolver: provides wildcard-resolution mesh-wide, forwards every declared zone (zones fact), listens on the private address and loopback only, reads no hosts file and no operator's files, and no longer writes the container runtime's dns. - resolv-conf names the mesh's resolver by address, then 1.1.1.1, timeout 1, one attempt; it now holds the runtime's live-restore, which dnsmasq held and every node needs. - resolved-split-dns routes the suffix to the mesh's resolver by address, not 127.0.0.1. - hosts: new module holding node-hosts-file — the machine's own lines in its block of /etc/hosts, the operator's lines kept, changed by entries/add/remove through sudo -n.
70 lines
3.3 KiB
TypeScript
70 lines
3.3 KiB
TypeScript
// The hosts file's verbs over files shaped like the workstation's on 2026-10-03 (novox/hq ADR 0199):
|
|
// distribution lines, an operator's development names, the mesh's block and another tool's.
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { HOSTS_FILE, escalated, parse, withAdded, withRemoved } from "../client.ts";
|
|
|
|
const FILE =
|
|
"# Static table lookup for hostnames.\n" +
|
|
"127.0.0.1\tlocaldev.example.com\n" +
|
|
"127.0.0.1 a.example.com b.example.com\n" +
|
|
"# BEGIN mesh hosts.own\n" +
|
|
"127.0.0.1\tlocalhost\n" +
|
|
"::1\tlocalhost\n" +
|
|
"# END mesh hosts.own\n" +
|
|
"# BEGIN other-tool\n" +
|
|
"192.0.2.7\tproject.test\n" +
|
|
"# END other-tool\n";
|
|
|
|
const blocks = (text: string) => parse(text).filter((l) => l.owner !== "operator").map((l) => l.text);
|
|
|
|
test("every line says whose it is", () => {
|
|
const lines = parse(FILE);
|
|
assert.equal(lines.length, 10);
|
|
assert.deepEqual(lines[1], { text: "127.0.0.1\tlocaldev.example.com", owner: "operator", address: "127.0.0.1", names: ["localdev.example.com"] });
|
|
assert.equal(lines[4].owner, "mesh hosts.own");
|
|
assert.equal(lines[8].owner, "other-tool");
|
|
assert.deepEqual(lines[8].names, ["project.test"]);
|
|
});
|
|
|
|
test("add appends an operator line, and is a no-op when the names are there", () => {
|
|
const after = withAdded(FILE, "192.0.2.9", ["lab.test", "www.lab.test"]);
|
|
assert.ok(after.endsWith("192.0.2.9\tlab.test www.lab.test\n"));
|
|
assert.deepEqual(blocks(after), blocks(FILE));
|
|
assert.equal(withAdded(FILE, "127.0.0.1", ["a.example.com"]), FILE);
|
|
assert.ok(withAdded(FILE, "127.0.0.1", ["a.example.com", "c.example.com"]).endsWith("127.0.0.1\tc.example.com\n"));
|
|
});
|
|
|
|
test("add refuses what is not an address or a host name", () => {
|
|
assert.throws(() => withAdded(FILE, "not-an-ip", ["x.test"]), /not an IPv4 or IPv6 address/);
|
|
assert.throws(() => withAdded(FILE, "192.0.2.9", ["bad name\n10.0.0.1 evil"]), /not a host name/);
|
|
assert.throws(() => withAdded(FILE, "192.0.2.9", []), /at least one name/);
|
|
});
|
|
|
|
test("remove takes one name or one address from the operator's lines, and blocks stay byte for byte", () => {
|
|
const one = withRemoved(FILE, "a.example.com");
|
|
assert.equal(one.removed, 1);
|
|
assert.ok(one.text.includes("127.0.0.1\tb.example.com\n"));
|
|
assert.ok(!one.text.includes("a.example.com"));
|
|
assert.deepEqual(blocks(one.text), blocks(FILE));
|
|
const all = withRemoved(FILE, "127.0.0.1");
|
|
assert.equal(all.removed, 2);
|
|
assert.ok(all.text.includes("# BEGIN mesh hosts.own\n127.0.0.1\tlocalhost\n"), "the mesh's own localhost is not the operator's to remove");
|
|
});
|
|
|
|
test("remove refuses a name only a block writes, naming whose", () => {
|
|
assert.throws(() => withRemoved(FILE, "project.test"), /written by other-tool/);
|
|
assert.equal(withRemoved(FILE, "nowhere.test").removed, 0);
|
|
});
|
|
|
|
test("the file is written as root through sudo where the account is not root", () => {
|
|
assert.deepEqual(escalated("install", ["x"], 1000), ["sudo", ["-n", "install", "x"]]);
|
|
assert.deepEqual(escalated("install", ["x"], 0), ["install", ["x"]]);
|
|
});
|
|
|
|
test("the path the code writes is the path the manifest's resource declares", () => {
|
|
const manifest = JSON.parse(readFileSync(new URL("../module.json", import.meta.url), "utf8"));
|
|
assert.equal(manifest.resources.find((r: { id: string }) => r.id === "own").path, HOSTS_FILE);
|
|
});
|