128 lines
3.0 KiB
TypeScript
128 lines
3.0 KiB
TypeScript
import { constants, access, 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 (error) {
|
|
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;
|
|
}
|
|
}
|