Files
headplane/app/server/headscale/config-dns.ts
T
2026-06-22 16:40:29 -04:00

120 lines
3.1 KiB
TypeScript

import { access, constants, readFile, writeFile } from "node:fs/promises";
import { setTimeout } from "node:timers/promises";
import log from "~/utils/log";
export interface DNSRecord {
type: "A" | "AAAA" | (string & {});
name: string;
value: string;
}
// This class is solely for DNS records that are out of tree in the main
// Headscale config file. If you are using dns.extra_records_path, it will
// be managed here and not in the main config file.
//
// All DNS insertions and deletions are handled by the main config manager,
// but are passed through to here if the extra file is being used.
export class HeadscaleDNSConfig {
private records: DNSRecord[];
private access: "rw" | "ro" | "no";
private path?: string;
private writeLock = false;
constructor(access: "rw" | "ro" | "no", records?: DNSRecord[], path?: string) {
this.access = access;
this.records = records ?? [];
this.path = path;
}
readable() {
return this.access !== "no";
}
writable() {
return this.access === "rw";
}
get r() {
return this.records;
}
async patch(records: DNSRecord[]) {
if (!this.path || !this.readable() || !this.writable()) {
return;
}
this.records = records;
log.debug("config", "Patching DNS records (%d -> %d)", this.records.length, records.length);
return this.write();
}
private async write() {
if (!this.path || !this.writable()) {
return;
}
while (this.writeLock) {
await setTimeout(100);
}
this.writeLock = true;
log.debug("config", "Writing updated DNS configuration to %s", this.path);
const data = JSON.stringify(this.records, null, 4);
await writeFile(this.path, data);
this.writeLock = false;
}
}
export async function loadHeadscaleDNS(path?: string) {
if (!path) {
return;
}
log.debug("config", "Loading Headscale DNS configuration file: %s", path);
const { w, r } = await validateConfigPath(path);
if (!r) {
return new HeadscaleDNSConfig("no");
}
const records = await loadConfigFile(path);
if (!records) {
return new HeadscaleDNSConfig("no");
}
return new HeadscaleDNSConfig(w ? "rw" : "ro", records, path);
}
async function validateConfigPath(path: string) {
try {
await access(path, constants.F_OK | constants.R_OK);
log.info("config", "Found a valid Headscale DNS file at %s", path);
} catch (error) {
log.error("config", "Unable to read a Headscale DNS file at %s", path);
log.error("config", "%s", error);
return { w: false, r: false };
}
try {
await access(path, constants.F_OK | constants.W_OK);
return { w: true, r: true };
} catch {
log.warn("config", "Headscale DNS file at %s is not writable", path);
return { w: false, r: true };
}
}
async function loadConfigFile(path: string) {
log.debug("config", "Reading Headscale DNS file at %s", path);
try {
const data = await readFile(path, "utf8");
const records = JSON.parse(data) as DNSRecord[];
return records;
} catch (e) {
log.error("config", "Error reading Headscale DNS file at %s", path);
log.error("config", "%s", e);
return false;
}
}