feat(dns): add db-backed DNS provider, domain, and record management with ops UI support

This commit is contained in:
2026-04-08 11:08:18 +00:00
parent e77fe9451e
commit 21c80e173d
57 changed files with 3753 additions and 65 deletions

View File

@@ -27,6 +27,7 @@ import { VpnManager, type IVpnManagerConfig } from './vpn/index.js';
import { RouteConfigManager, ApiTokenManager, ReferenceResolver, DbSeeder, TargetProfileManager } from './config/index.js';
import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/index.js';
import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js';
import { DnsManager } from './dns/manager.dns.js';
export interface IDcRouterOptions {
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
@@ -116,13 +117,6 @@ export interface IDcRouterOptions {
useIngressProxy?: boolean; // Whether to replace server IP with proxy IP (default: true)
}>;
/** DNS challenge configuration for ACME (optional) */
dnsChallenge?: {
/** Cloudflare API key for DNS challenges */
cloudflareApiKey?: string;
/** Other DNS providers can be added here */
};
/**
* Unified database configuration.
* All persistent data (config, certs, VPN, cache, etc.) is stored via smartdata.
@@ -279,6 +273,9 @@ export class DcRouter {
public referenceResolver?: ReferenceResolver;
public targetProfileManager?: TargetProfileManager;
// Domain / DNS management (DB-backed providers, domains, records)
public dnsManager?: DnsManager;
// Auto-discovered public IP (populated by generateAuthoritativeRecords)
public detectedPublicIp: string | null = null;
@@ -393,10 +390,33 @@ export class DcRouter {
.withRetry({ maxRetries: 1, baseDelayMs: 1000 }),
);
// SmartProxy: critical, depends on DcRouterDb (if enabled)
// DnsManager: optional, depends on DcRouterDb — owns DB-backed DNS state
// (providers, domains, records). Must run before SmartProxy so ACME DNS-01
// wiring can look up providers.
if (this.options.dbConfig?.enabled !== false) {
this.serviceManager.addService(
new plugins.taskbuffer.Service('DnsManager')
.optional()
.dependsOn('DcRouterDb')
.withStart(async () => {
this.dnsManager = new DnsManager(this.options);
await this.dnsManager.start();
})
.withStop(async () => {
if (this.dnsManager) {
await this.dnsManager.stop();
this.dnsManager = undefined;
}
})
.withRetry({ maxRetries: 1, baseDelayMs: 500 }),
);
}
// SmartProxy: critical, depends on DcRouterDb + DnsManager (if enabled)
const smartProxyDeps: string[] = [];
if (this.options.dbConfig?.enabled !== false) {
smartProxyDeps.push('DcRouterDb');
smartProxyDeps.push('DnsManager');
}
this.serviceManager.addService(
new plugins.taskbuffer.Service('SmartProxy')
@@ -415,9 +435,11 @@ export class DcRouter {
.withRetry({ maxRetries: 0 }),
);
// SmartAcme: optional, depends on SmartProxy — aggressive retry for rate limits
// Only registered if DNS challenge is configured
if (this.options.dnsChallenge?.cloudflareApiKey) {
// SmartAcme: optional, depends on SmartProxy — aggressive retry for rate limits.
// Always registered when the DB is enabled; setupSmartProxy() decides whether
// to actually instantiate SmartAcme based on whether any DnsProviderDoc exists.
// If `this.smartAcme` is unset by the time this service starts, withStart is a no-op.
if (this.options.dbConfig?.enabled !== false) {
this.serviceManager.addService(
new plugins.taskbuffer.Service('SmartAcme')
.optional()
@@ -849,12 +871,14 @@ export class DcRouter {
};
}
// Configure DNS challenge if available
// Configure DNS-01 challenge if any DnsProviderDoc exists in the DB.
// The DnsManager dispatches each challenge to the right provider client
// based on the FQDN being certificated.
let challengeHandlers: any[] = [];
if (this.options.dnsChallenge?.cloudflareApiKey) {
logger.log('info', 'Configuring Cloudflare DNS challenge for ACME');
const cloudflareAccount = new plugins.cloudflare.CloudflareAccount(this.options.dnsChallenge.cloudflareApiKey);
const dns01Handler = new plugins.smartacme.handlers.Dns01Handler(cloudflareAccount);
if (this.dnsManager && (await this.dnsManager.hasAcmeCapableProvider())) {
logger.log('info', 'Configuring DNS-01 challenge for ACME via DnsManager (DB providers)');
const convenientDnsProvider = this.dnsManager.buildAcmeConvenientDnsProvider();
const dns01Handler = new plugins.smartacme.handlers.Dns01Handler(convenientDnsProvider);
challengeHandlers.push(dns01Handler);
}
@@ -1720,8 +1744,13 @@ export class DcRouter {
this.registerDnsRecords(allRecords);
logger.log('info', `Registered ${allRecords.length} DNS records (${authoritativeRecords.length} authoritative, ${emailDnsRecords.length} email, ${dkimRecords.length} DKIM, ${this.options.dnsRecords?.length || 0} user-defined)`);
}
// Hand the DnsServer to DnsManager so DB-backed manual records get registered too.
if (this.dnsManager && this.dnsServer) {
await this.dnsManager.attachDnsServer(this.dnsServer);
}
}
/**
* Create DNS socket handler for DoH
*/