feat: DNS provider abstraction — Technitium, Cloudflare, RFC 2136, Manual
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled

- dns-providers/: adapter base class + registry with auto-discovery
- technitium.js: wraps existing Technitium API calls into adapter interface
- cloudflare.js: Cloudflare API v4 adapter (zones, records, credentials)
- rfc2136.js: RFC 2136 dynamic DNS via nsupdate (BIND, PowerDNS, etc.)
- manual.js: no-op adapter for external DNS management with instructions
- provider-dns.js: provider-aware DNS context, resolves active adapter from config
- Universal helper methods: universalCreateRecord/Delete/ResolveRecord
- All 7 route files updated to use universal methods instead of raw dns.call()
- Setup wizard: provider dropdown (Technitium, Cloudflare, RFC 2136, Manual)
- DNS template selector: added Cloudflare and External/Manual options
- Config schema: validates dns.provider field
- Capability gating on Technitium-specific endpoints (logs, restart, update)
- Backward compatible: no provider set = auto-detect (technitium if dns.ip exists)
This commit is contained in:
Hermes
2026-06-10 15:06:41 -07:00
parent 0aa1c3d077
commit 2de72ed506
21 changed files with 1965 additions and 33 deletions
+1 -1
View File
@@ -316,7 +316,7 @@ module.exports = function({ docker, caddy, credentialManager, servicesStateManag
let dnsWarning = null;
if (config.createDns && !isSubdirectoryMode) {
try {
await ctx.dns.createRecord(config.subdomain, config.ip);
await ctx.dns.universalCreateRecord(config.subdomain, config.ip);
log.info('deploy', 'DNS record created', { domain: ctx.buildDomain(config.subdomain), ip: config.ip });
} catch (dnsError) {
await logError('app-deploy-dns', dnsError, { appId, subdomain: config.subdomain, ip: config.ip });
+5 -10
View File
@@ -71,18 +71,13 @@ module.exports = function({
if (shouldDeleteContainer && subdomain && ctx.dns.getToken()) {
try {
const domain = ctx.buildDomain(subdomain);
const getResult = await ctx.dns.call(ctx.siteConfig.dnsServerIp, '/api/zones/records/get', {
token: ctx.dns.getToken(), domain, zone: ctx.siteConfig.tld.replace(/^\./, ''), listZone: 'true'
});
const resolveResult = await ctx.dns.universalResolveRecord(domain, 'A');
let recordIp = ip || 'localhost';
if (getResult.status === 'ok' && getResult.response?.records) {
const aRecord = getResult.response.records.find(r => r.type === 'A');
if (aRecord && aRecord.rData?.ipAddress) recordIp = aRecord.rData.ipAddress;
if (resolveResult) {
recordIp = resolveResult;
}
const dnsResult = await ctx.dns.call(ctx.siteConfig.dnsServerIp, '/api/zones/records/delete', {
token: ctx.dns.getToken(), domain, type: 'A', ipAddress: recordIp
});
results.dns = dnsResult.status === 'ok' ? 'deleted' : (dnsResult.errorMessage || 'failed');
await ctx.dns.universalDeleteRecord(domain, recordIp);
results.dns = 'deleted';
log.info('dns', 'DNS record removal', { result: results.dns });
} catch (error) {
results.dns = error.message;
+1 -1
View File
@@ -458,7 +458,7 @@ module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, e
// DNS record
if (manifest.config.createDns && manifest.caddy.routingMode !== 'subdirectory') {
try {
await ctx.dns.createRecord(manifest.config.subdomain, manifest.config.ip);
await ctx.dns.universalCreateRecord(manifest.config.subdomain, manifest.config.ip);
log.info('restore', 'DNS record recreated', { subdomain: manifest.config.subdomain });
} catch (e) {
log.warn('restore', `DNS recreation failed: ${e.message}`);
+3 -5
View File
@@ -107,10 +107,8 @@ module.exports = function({
if (oldSubdomain && ctx.dns.getToken()) {
try {
const oldDomain = oldSubdomain.includes('.') ? oldSubdomain : ctx.buildDomain(oldSubdomain);
const result = await ctx.dns.call(ctx.siteConfig.dnsServerIp, '/api/zones/records/delete', {
token: ctx.dns.getToken(), domain: oldDomain, type: 'A', ipAddress: ip || 'localhost'
});
results.oldDns = result.status === 'ok' ? 'deleted' : result.errorMessage;
await ctx.dns.universalDeleteRecord(oldDomain, ip || 'localhost');
results.oldDns = 'deleted';
log.info('dns', 'Old DNS record deleted', { domain: oldDomain });
} catch (error) {
results.oldDns = `failed: ${error.message}`;
@@ -120,7 +118,7 @@ module.exports = function({
if (newSubdomain && ctx.dns.getToken()) {
try {
await ctx.dns.createRecord(newSubdomain, ip || 'localhost');
await ctx.dns.universalCreateRecord(newSubdomain, ip || 'localhost');
results.newDns = 'created';
log.info('dns', 'New DNS record created', { domain: ctx.buildDomain(newSubdomain) });
} catch (error) {