feat(dns): add db-backed DNS provider, domain, and record management with ops UI support
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user