Files
nixpanel-server/server/src/core/dns.ts
T

126 lines
3.0 KiB
TypeScript
Raw Normal View History

2026-04-05 10:45:21 -05:00
import path from 'path'
import { config } from '../config.js'
import { exec, writeFile } from './exec.js'
export interface DnsZoneOptions {
domain: string
ip: string
ns1: string
ns2: string
mail?: string
serial?: number
ttl?: number
}
/**
* Generate a BIND9 zone file for a domain.
*/
export function generateZoneFile(opts: DnsZoneOptions): string {
const {
domain,
ip,
ns1,
ns2,
mail = `mail.${opts.domain}`,
serial = Math.floor(Date.now() / 1000),
ttl = 3600,
} = opts
return `; Zone file for ${domain}
; Generated by NixPanel
$TTL ${ttl}
@ IN SOA ${ns1}. hostmaster.${domain}. (
${serial} ; Serial
3600 ; Refresh
1800 ; Retry
604800 ; Expire
300 ) ; Minimum TTL
; Name servers
@ IN NS ${ns1}.
@ IN NS ${ns2}.
; A records
@ IN A ${ip}
www IN A ${ip}
mail IN A ${ip}
ftp IN A ${ip}
nixclient IN A ${ip}
2026-04-05 10:45:21 -05:00
webmail IN A ${ip}
; MX record
@ IN MX 10 ${mail}.
; SPF
@ IN TXT "v=spf1 a mx ip4:${ip} ~all"
; DMARC
_dmarc IN TXT "v=DMARC1; p=none; rua=mailto:admin@${domain}"
`
}
/**
* Write zone file and update named.conf.local, then reload BIND.
*/
export async function createZone(opts: DnsZoneOptions): Promise<string> {
const zoneFile = path.join(config.paths.bindZones, `db.${opts.domain}`)
await writeFile(zoneFile, generateZoneFile(opts))
// Add zone declaration to named.conf.local (idempotent)
const namedConf = '/etc/bind/named.conf.local'
const zoneDecl = `
zone "${opts.domain}" {
type master;
file "${zoneFile}";
allow-transfer { any; };
};
`
// Read existing, only add if not already there
const { readFile } = await import('fs/promises')
let existing = ''
try {
existing = await readFile(namedConf, 'utf8')
} catch {
// file may not exist yet in test env
}
if (!existing.includes(`zone "${opts.domain}"`)) {
const { appendFile } = await import('fs/promises')
await appendFile(namedConf, zoneDecl)
}
// Validate zone
const check = await exec('named-checkzone', [opts.domain, zoneFile])
if (check.exitCode !== 0) {
throw new Error(`Zone validation failed: ${check.stderr}`)
}
await exec('rndc', ['reload'])
return zoneFile
}
/**
* Remove a zone and reload BIND.
*/
export async function removeZone(domain: string): Promise<void> {
const zoneFile = path.join(config.paths.bindZones, `db.${domain}`)
await exec('rm', ['-f', zoneFile])
// Remove zone block from named.conf.local
const namedConf = '/etc/bind/named.conf.local'
const { readFile, writeFile: fsWrite } = await import('fs/promises')
try {
const content = await readFile(namedConf, 'utf8')
const cleaned = content.replace(
new RegExp(`zone\\s+"${domain.replace('.', '\\.')}\"[^}]+\\};\\s*`, 'g'),
''
)
await fsWrite(namedConf, cleaned, 'utf8')
} catch {
// best effort
}
await exec('rndc', ['reload'])
}