The mesh's resolver is more than config after all. Tools: dnsmasq_names (what this node answers, from the generated wildcard file) and dnsmasq_resolve (resolve a name through this node's own resolver — the check that wildcard-resolution actually answers). Event: it watches the wildcard file and emits module.dnsmasq.name.added/.removed as machines' names become resolvable here — DNS having propagated to this node, distinct from the mesh's node.* events. Typechecks; manifest parses.
60 lines
2.2 KiB
TypeScript
60 lines
2.2 KiB
TypeScript
// dnsmasq's own code, living in the module (novox/hq ADR 0044). dnsmasq here is a pure resolver: it
|
|
// answers the mesh's generated wildcard names (<service>.<node>.<suffix>) and forwards nothing. So
|
|
// 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 — 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.55",
|
|
);
|
|
}
|
|
|
|
/** 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>
|
|
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);
|
|
}
|
|
}
|
|
}
|