feat(dns): add built-in dcrouter DNS provider support and rename manual domains to dcrouter-hosted/local
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '13.8.0',
|
||||
version: '13.9.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -1792,7 +1792,8 @@ export class DcRouter {
|
||||
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.
|
||||
// Hand the DnsServer to DnsManager so DB-backed local records on
|
||||
// dcrouter-hosted domains get registered too.
|
||||
if (this.dnsManager && this.dnsServer) {
|
||||
await this.dnsManager.attachDnsServer(this.dnsServer);
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@ import type {
|
||||
* Responsibilities:
|
||||
* - Load Domain/DnsRecord docs from the DB on start
|
||||
* - First-boot seeding from legacy constructor config (dnsScopes/dnsRecords/dnsNsDomains)
|
||||
* - Register manual-domain records with smartdns.DnsServer at startup
|
||||
* - Provide CRUD methods used by OpsServer handlers (manual domains hit smartdns,
|
||||
* provider domains hit the provider API)
|
||||
* - Register dcrouter-hosted domain records with smartdns.DnsServer at startup
|
||||
* - Provide CRUD methods used by OpsServer handlers (dcrouter-hosted domains hit
|
||||
* smartdns, provider domains hit the provider API)
|
||||
* - Expose a provider lookup used by the ACME DNS-01 wiring in setupSmartProxy()
|
||||
*
|
||||
* Provider-managed domains are NEVER served from the embedded DnsServer — the
|
||||
@@ -69,12 +69,12 @@ export class DnsManager {
|
||||
|
||||
/**
|
||||
* Wire the embedded DnsServer instance after it has been created by
|
||||
* DcRouter.setupDnsWithSocketHandler(). After this, manual records loaded
|
||||
* from the DB are registered with the server.
|
||||
* DcRouter.setupDnsWithSocketHandler(). After this, local records on
|
||||
* dcrouter-hosted domains loaded from the DB are registered with the server.
|
||||
*/
|
||||
public async attachDnsServer(dnsServer: plugins.smartdns.dnsServerMod.DnsServer): Promise<void> {
|
||||
this.dnsServer = dnsServer;
|
||||
await this.applyManualDomainsToDnsServer();
|
||||
await this.applyDcrouterDomainsToDnsServer();
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
@@ -83,7 +83,8 @@ export class DnsManager {
|
||||
|
||||
/**
|
||||
* If no DomainDocs exist yet but the constructor has legacy DNS fields,
|
||||
* seed them as `source: 'manual'` records. On subsequent boots (DB has
|
||||
* seed them as dcrouter-hosted (`domain.source: 'dcrouter'`) zones with
|
||||
* local (`record.source: 'local'`) records. On subsequent boots (DB has
|
||||
* entries), constructor config is ignored with a warning.
|
||||
*/
|
||||
private async seedFromConstructorConfigIfEmpty(): Promise<void> {
|
||||
@@ -117,7 +118,7 @@ export class DnsManager {
|
||||
const domain = new DomainDoc();
|
||||
domain.id = plugins.uuid.v4();
|
||||
domain.name = scope.toLowerCase();
|
||||
domain.source = 'manual';
|
||||
domain.source = 'dcrouter';
|
||||
domain.authoritative = true;
|
||||
domain.createdAt = now;
|
||||
domain.updatedAt = now;
|
||||
@@ -144,7 +145,7 @@ export class DnsManager {
|
||||
record.type = rec.type as TDnsRecordType;
|
||||
record.value = rec.value;
|
||||
record.ttl = rec.ttl ?? 300;
|
||||
record.source = 'manual';
|
||||
record.source = 'local';
|
||||
record.createdAt = now;
|
||||
record.updatedAt = now;
|
||||
record.createdBy = 'seed';
|
||||
@@ -174,28 +175,31 @@ export class DnsManager {
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Manual-domain DnsServer wiring
|
||||
// DcRouter-hosted domain DnsServer wiring
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Register all manual-domain records from the DB with the embedded DnsServer.
|
||||
* Called once after attachDnsServer().
|
||||
* Register all records from dcrouter-hosted domains in the DB with the
|
||||
* embedded DnsServer. Called once after attachDnsServer().
|
||||
*/
|
||||
private async applyManualDomainsToDnsServer(): Promise<void> {
|
||||
private async applyDcrouterDomainsToDnsServer(): Promise<void> {
|
||||
if (!this.dnsServer) {
|
||||
return;
|
||||
}
|
||||
const allDomains = await DomainDoc.findAll();
|
||||
const manualDomains = allDomains.filter((d) => d.source === 'manual');
|
||||
const dcrouterDomains = allDomains.filter((d) => d.source === 'dcrouter');
|
||||
let registered = 0;
|
||||
for (const domain of manualDomains) {
|
||||
for (const domain of dcrouterDomains) {
|
||||
const records = await DnsRecordDoc.findByDomainId(domain.id);
|
||||
for (const rec of records) {
|
||||
this.registerRecordWithDnsServer(rec);
|
||||
registered++;
|
||||
}
|
||||
}
|
||||
logger.log('info', `DnsManager: registered ${registered} manual DNS record(s) from DB`);
|
||||
logger.log(
|
||||
'info',
|
||||
`DnsManager: registered ${registered} dcrouter-hosted DNS record(s) from DB`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -381,6 +385,12 @@ export class DnsManager {
|
||||
credentials: TDnsProviderCredentials;
|
||||
createdBy: string;
|
||||
}): Promise<string> {
|
||||
if (args.type === 'dcrouter') {
|
||||
throw new Error(
|
||||
'createProvider: cannot create a DnsProviderDoc with type "dcrouter" — ' +
|
||||
'that type is reserved for the built-in pseudo-provider surfaced at read time.',
|
||||
);
|
||||
}
|
||||
const now = Date.now();
|
||||
const doc = new DnsProviderDoc();
|
||||
doc.id = plugins.uuid.v4();
|
||||
@@ -473,10 +483,10 @@ export class DnsManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a manual (authoritative) domain. dcrouter will serve DNS records
|
||||
* for this domain via the embedded smartdns.DnsServer.
|
||||
* Create a dcrouter-hosted (authoritative) domain. dcrouter will serve
|
||||
* DNS records for this domain via the embedded smartdns.DnsServer.
|
||||
*/
|
||||
public async createManualDomain(args: {
|
||||
public async createDcrouterDomain(args: {
|
||||
name: string;
|
||||
description?: string;
|
||||
createdBy: string;
|
||||
@@ -485,7 +495,7 @@ export class DnsManager {
|
||||
const doc = new DomainDoc();
|
||||
doc.id = plugins.uuid.v4();
|
||||
doc.name = args.name.toLowerCase();
|
||||
doc.source = 'manual';
|
||||
doc.source = 'dcrouter';
|
||||
doc.authoritative = true;
|
||||
doc.description = args.description;
|
||||
doc.createdAt = now;
|
||||
@@ -571,10 +581,11 @@ export class DnsManager {
|
||||
/**
|
||||
* Delete a domain and all of its DNS records. For provider domains, only
|
||||
* removes the local mirror — does NOT touch the provider.
|
||||
* For manual domains, also unregisters records from the embedded DnsServer.
|
||||
* For dcrouter-hosted domains, also unregisters records from the embedded
|
||||
* DnsServer.
|
||||
*
|
||||
* Note: smartdns has no public unregister-by-name API in the version pinned
|
||||
* here, so manual record deletes only take effect after a restart. The DB
|
||||
* here, so local record deletes only take effect after a restart. The DB
|
||||
* is the source of truth and the next start will not register the deleted
|
||||
* record.
|
||||
*/
|
||||
@@ -652,7 +663,7 @@ export class DnsManager {
|
||||
doc.value = args.value;
|
||||
doc.ttl = args.ttl ?? 300;
|
||||
if (args.proxied !== undefined) doc.proxied = args.proxied;
|
||||
doc.source = 'manual';
|
||||
doc.source = 'local';
|
||||
doc.createdAt = now;
|
||||
doc.updatedAt = now;
|
||||
doc.createdBy = args.createdBy;
|
||||
@@ -678,7 +689,7 @@ export class DnsManager {
|
||||
return { success: false, message: `Provider rejected record: ${(err as Error).message}` };
|
||||
}
|
||||
} else {
|
||||
// Manual / authoritative — register with embedded DnsServer immediately
|
||||
// dcrouter-hosted / authoritative — register with embedded DnsServer immediately
|
||||
this.registerRecordWithDnsServer(doc);
|
||||
}
|
||||
|
||||
@@ -722,7 +733,7 @@ export class DnsManager {
|
||||
return { success: false, message: `Provider rejected update: ${(err as Error).message}` };
|
||||
}
|
||||
} else {
|
||||
// Re-register the manual record so the new closure picks up the updated fields
|
||||
// Re-register the local record so the new closure picks up the updated fields
|
||||
this.registerRecordWithDnsServer(doc);
|
||||
}
|
||||
|
||||
@@ -748,7 +759,7 @@ export class DnsManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
// For manual records: smartdns has no unregister API in the pinned version,
|
||||
// For local records: smartdns has no unregister API in the pinned version,
|
||||
// so the record stays served until the next restart. The DB delete still
|
||||
// takes effect — on restart, the record will not be re-registered.
|
||||
|
||||
@@ -807,7 +818,7 @@ export class DnsManager {
|
||||
public toPublicDomain(doc: DomainDoc): {
|
||||
id: string;
|
||||
name: string;
|
||||
source: 'manual' | 'provider';
|
||||
source: 'dcrouter' | 'provider';
|
||||
providerId?: string;
|
||||
authoritative: boolean;
|
||||
nameservers?: string[];
|
||||
|
||||
@@ -38,6 +38,17 @@ export function createDnsProvider(
|
||||
}
|
||||
return new CloudflareDnsProvider(credentials.apiToken);
|
||||
}
|
||||
case 'dcrouter': {
|
||||
// The built-in DcRouter pseudo-provider has no runtime client — dcrouter
|
||||
// itself serves the records via the embedded smartdns.DnsServer. This
|
||||
// case exists only to satisfy the exhaustive switch; it should never
|
||||
// actually run because the handler layer rejects any CRUD that would
|
||||
// result in a DnsProviderDoc with type: 'dcrouter'.
|
||||
throw new Error(
|
||||
`createDnsProvider: 'dcrouter' is a built-in pseudo-provider — no runtime client exists. ` +
|
||||
`This call indicates a DnsProviderDoc with type: 'dcrouter' was persisted, which should never happen.`,
|
||||
);
|
||||
}
|
||||
default: {
|
||||
// If you see a TypeScript error here after extending TDnsProviderType,
|
||||
// add a `case` for the new type above. The `never` enforces exhaustiveness.
|
||||
|
||||
@@ -46,15 +46,28 @@ export class DnsProviderHandler {
|
||||
}
|
||||
|
||||
private registerHandlers(): void {
|
||||
// Get all providers
|
||||
// Get all providers — prepends the built-in DcRouter pseudo-provider
|
||||
// so operators see a uniform "who serves this?" list that includes the
|
||||
// authoritative dcrouter alongside external accounts.
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetDnsProviders>(
|
||||
'getDnsProviders',
|
||||
async (dataArg) => {
|
||||
await this.requireAuth(dataArg, 'dns-providers:read');
|
||||
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
||||
if (!dnsManager) return { providers: [] };
|
||||
return { providers: await dnsManager.listProviders() };
|
||||
const synthetic: interfaces.data.IDnsProviderPublic = {
|
||||
id: interfaces.data.DCROUTER_BUILTIN_PROVIDER_ID,
|
||||
name: 'DcRouter',
|
||||
type: 'dcrouter',
|
||||
status: 'ok',
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
createdBy: 'system',
|
||||
hasCredentials: false,
|
||||
builtIn: true,
|
||||
};
|
||||
const real = dnsManager ? await dnsManager.listProviders() : [];
|
||||
return { providers: [synthetic, ...real] };
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -78,6 +91,12 @@ export class DnsProviderHandler {
|
||||
'createDnsProvider',
|
||||
async (dataArg) => {
|
||||
const userId = await this.requireAuth(dataArg, 'dns-providers:write');
|
||||
if (dataArg.type === 'dcrouter') {
|
||||
return {
|
||||
success: false,
|
||||
message: 'cannot create built-in provider',
|
||||
};
|
||||
}
|
||||
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
||||
if (!dnsManager) {
|
||||
return { success: false, message: 'DnsManager not initialized (DB disabled?)' };
|
||||
@@ -99,6 +118,9 @@ export class DnsProviderHandler {
|
||||
'updateDnsProvider',
|
||||
async (dataArg) => {
|
||||
await this.requireAuth(dataArg, 'dns-providers:write');
|
||||
if (dataArg.id === interfaces.data.DCROUTER_BUILTIN_PROVIDER_ID) {
|
||||
return { success: false, message: 'cannot edit built-in provider' };
|
||||
}
|
||||
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
||||
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
||||
const ok = await dnsManager.updateProvider(dataArg.id, {
|
||||
@@ -116,6 +138,9 @@ export class DnsProviderHandler {
|
||||
'deleteDnsProvider',
|
||||
async (dataArg) => {
|
||||
await this.requireAuth(dataArg, 'dns-providers:write');
|
||||
if (dataArg.id === interfaces.data.DCROUTER_BUILTIN_PROVIDER_ID) {
|
||||
return { success: false, message: 'cannot delete built-in provider' };
|
||||
}
|
||||
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
||||
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
||||
return await dnsManager.deleteProvider(dataArg.id, dataArg.force ?? false);
|
||||
@@ -129,6 +154,13 @@ export class DnsProviderHandler {
|
||||
'testDnsProvider',
|
||||
async (dataArg) => {
|
||||
await this.requireAuth(dataArg, 'dns-providers:read');
|
||||
if (dataArg.id === interfaces.data.DCROUTER_BUILTIN_PROVIDER_ID) {
|
||||
return {
|
||||
ok: false,
|
||||
error: 'built-in provider has no external connection to test',
|
||||
testedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
||||
if (!dnsManager) {
|
||||
return { ok: false, error: 'DnsManager not initialized', testedAt: Date.now() };
|
||||
@@ -144,6 +176,12 @@ export class DnsProviderHandler {
|
||||
'listProviderDomains',
|
||||
async (dataArg) => {
|
||||
await this.requireAuth(dataArg, 'dns-providers:read');
|
||||
if (dataArg.providerId === interfaces.data.DCROUTER_BUILTIN_PROVIDER_ID) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'built-in provider has no external domain listing — use "Add DcRouter Domain" instead',
|
||||
};
|
||||
}
|
||||
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
||||
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
||||
try {
|
||||
|
||||
@@ -71,7 +71,7 @@ export class DomainHandler {
|
||||
),
|
||||
);
|
||||
|
||||
// Create manual domain
|
||||
// Create dcrouter-hosted domain
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateDomain>(
|
||||
'createDomain',
|
||||
@@ -80,7 +80,7 @@ export class DomainHandler {
|
||||
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
||||
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
||||
try {
|
||||
const id = await dnsManager.createManualDomain({
|
||||
const id = await dnsManager.createDcrouterDomain({
|
||||
name: dataArg.name,
|
||||
description: dataArg.description,
|
||||
createdBy: userId,
|
||||
|
||||
Reference in New Issue
Block a user