Read against hal/modules/dnsmasq-app (hal dnsmasq-app conversion, hq 08-connectivity).
On the machines it runs, the predecessor's dnsmasq answers every name: the mesh's own
itself, the rest forwarded to 1.1.1.1 and 8.8.8.8, its module's defaults; resolv.conf
names it alone at 127.0.0.1, and the container runtime's dns is the machine's tunnel
address, so the host and every container resolve the world through it. The nox module
forwarded nothing, listened on 127.0.0.55 — a convention of its own beside the one every
machine already followed — and read a machines file that the module's `facts` already
asks the mesh for, so taking it would have left an adopted machine with a resolv.conf
pointing at an address nothing answered on, and no upstream for anything else.
Now the resolver keeps `no-resolv` (the documented loop — finding its own address in
resolv.conf and becoming its own upstream — stays impossible) and forwards to the same
two explicit upstreams; listens on mesh0 and 127.0.0.1, which systemd-resolved does not
hold; requires `mesh-addressing`, since its data is the mesh's addresses; and writes the
runtime's `dns` into daemon.json beside whatever the machine had (ADR 0102), at this
machine's own address — `${machine:address}`, new in the controller. The runtime is not
restarted for it: it reads the key at start, not on reload, and a restart stops every
container; on the machine this replaces the value is already there.
resolv-conf names the resolver alone, as the predecessor's file did; its placeholder second
line was a fallback nothing ever reached. resolved-split-dns follows the address. The
mesh's suffix as a local domain comes with the machines file, so a mesh name the resolver
does not know is refused here rather than asked upstream. mDNS is not carried: no module
does it and the design says mesh names are not multicast names.
63 lines
2.5 KiB
TypeScript
63 lines
2.5 KiB
TypeScript
// dnsmasq's own code, living in the module (novox/hq ADR 0039). dnsmasq here answers the mesh's
|
|
// generated wildcard names (<service>.<node>.<suffix>) itself and forwards everything else to fixed
|
|
// upstreams — the predecessor's arrangement, so the machine and its containers ask it for the world.
|
|
// Its code reads what it was told to answer, and can resolve through itself to prove that it does.
|
|
|
|
import { readFile } from "node:fs/promises";
|
|
import { Resolver } from "node:dns/promises";
|
|
|
|
export interface AnsweredName {
|
|
/** A machine's internal name, e.g. "anchor.internal" — it and everything under it resolve here. */
|
|
name: string;
|
|
address: string;
|
|
}
|
|
|
|
export class DnsmasqClient {
|
|
constructor(
|
|
private readonly resolverPath: string,
|
|
private readonly address: string,
|
|
) {}
|
|
|
|
/**
|
|
* Build from the environment. Both values are node-local facts with mesh-chosen defaults — the
|
|
* wildcard file the module is sent, and the loopback address its config listens on (127.0.0.1,
|
|
* where the predecessor's resolver answered and every machine's resolv.conf already points) — so
|
|
* there is nothing to be unconfigured about; it never throws.
|
|
*/
|
|
static fromEnv(env: NodeJS.ProcessEnv = process.env): DnsmasqClient {
|
|
return new DnsmasqClient(
|
|
env.MESH_DNSMASQ_RESOLVER_PATH ?? "/etc/mesh-resolver/nodes.conf",
|
|
env.MESH_DNSMASQ_ADDRESS ?? "127.0.0.1",
|
|
);
|
|
}
|
|
|
|
/** The names this resolver answers, read from the mesh-generated wildcard file. */
|
|
async answeredNames(): Promise<AnsweredName[]> {
|
|
let text: string;
|
|
try {
|
|
text = await readFile(this.resolverPath, "utf8");
|
|
} catch {
|
|
return []; // not yet on the network, or the file has not been written — no names, not an error
|
|
}
|
|
const names: AnsweredName[] = [];
|
|
for (const line of text.split("\n")) {
|
|
// dnsmasq wildcard syntax the mesh writes: address=/<node>.<suffix>/<address>. The file also
|
|
// carries local=/<suffix>/, which names no machine and is not matched here.
|
|
const match = line.match(/^address=\/([^/]+)\/(.+)$/);
|
|
if (match) names.push({ name: match[1], address: match[2] });
|
|
}
|
|
return names;
|
|
}
|
|
|
|
/** Resolve a name through this node's own resolver — the check that wildcard-resolution answers. */
|
|
async resolve(name: string): Promise<string[]> {
|
|
const resolver = new Resolver();
|
|
resolver.setServers([this.address]);
|
|
try {
|
|
return await resolver.resolve4(name);
|
|
} catch {
|
|
return await resolver.resolve6(name);
|
|
}
|
|
}
|
|
}
|