@serve.zone/dcrouter 17.10.2 → 18.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +1 -1
- package/dist_serve/bundle.js +360 -360
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/acme/acme-failure-classification.d.ts +64 -0
- package/dist_ts/acme/acme-failure-classification.js +114 -0
- package/dist_ts/acme/classes.smartacme-lifecycle.d.ts +42 -0
- package/dist_ts/acme/classes.smartacme-lifecycle.js +75 -4
- package/dist_ts/acme/index.d.ts +1 -0
- package/dist_ts/acme/index.js +2 -1
- package/dist_ts/classes.dcrouter.d.ts +33 -9
- package/dist_ts/classes.dcrouter.js +145 -11
- package/dist_ts/config/classes.route-config-manager.d.ts +56 -0
- package/dist_ts/config/classes.route-config-manager.js +161 -1
- package/dist_ts/db/documents/classes.dns-authority.doc.d.ts +30 -0
- package/dist_ts/db/documents/classes.dns-authority.doc.js +108 -0
- package/dist_ts/db/documents/index.d.ts +1 -0
- package/dist_ts/db/documents/index.js +2 -1
- package/dist_ts/dns/classes.dns-server-runtime.d.ts +109 -1
- package/dist_ts/dns/classes.dns-server-runtime.js +212 -34
- package/dist_ts/dns/domain-ownership.d.ts +111 -0
- package/dist_ts/dns/domain-ownership.js +152 -0
- package/dist_ts/dns/index.d.ts +2 -0
- package/dist_ts/dns/index.js +3 -1
- package/dist_ts/dns/manager.dns-authority.d.ts +143 -0
- package/dist_ts/dns/manager.dns-authority.js +481 -0
- package/dist_ts/dns/manager.dns.d.ts +121 -12
- package/dist_ts/dns/manager.dns.js +298 -28
- package/dist_ts/errors/error.codes.d.ts +4 -0
- package/dist_ts/errors/error.codes.js +5 -1
- package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
- package/dist_ts/opsserver/classes.opsserver.js +3 -1
- package/dist_ts/opsserver/handlers/acme-config.handler.js +6 -1
- package/dist_ts/opsserver/handlers/certificate.handler.d.ts +16 -0
- package/dist_ts/opsserver/handlers/certificate.handler.js +96 -10
- package/dist_ts/opsserver/handlers/config.handler.js +4 -2
- package/dist_ts/opsserver/handlers/dns-authority.handler.d.ts +20 -0
- package/dist_ts/opsserver/handlers/dns-authority.handler.js +108 -0
- package/dist_ts/opsserver/handlers/dns-provider.handler.js +5 -1
- package/dist_ts/opsserver/handlers/domain.handler.js +9 -1
- package/dist_ts/opsserver/handlers/gatewayclient.handler.js +2 -2
- package/dist_ts/opsserver/handlers/index.d.ts +1 -0
- package/dist_ts/opsserver/handlers/index.js +2 -1
- package/dist_ts_interfaces/data/dns-authority.d.ts +98 -0
- package/dist_ts_interfaces/data/dns-authority.js +26 -0
- package/dist_ts_interfaces/data/index.d.ts +1 -0
- package/dist_ts_interfaces/data/index.js +2 -1
- package/dist_ts_interfaces/data/route-management.d.ts +9 -2
- package/dist_ts_interfaces/data/route-management.js +3 -1
- package/dist_ts_interfaces/requests/certificate.d.ts +23 -0
- package/dist_ts_interfaces/requests/certificate.js +1 -1
- package/dist_ts_interfaces/requests/dns-authority.d.ts +80 -0
- package/dist_ts_interfaces/requests/dns-authority.js +3 -0
- package/dist_ts_interfaces/requests/index.d.ts +1 -0
- package/dist_ts_interfaces/requests/index.js +2 -1
- package/dist_ts_oci_container/index.js +9 -4
- package/dist_ts_web/00_commitinfo_data.js +2 -2
- package/package.json +1 -1
- package/readme.hints.md +412 -0
- package/readme.md +48 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/acme/acme-failure-classification.ts +201 -0
- package/ts/acme/classes.smartacme-lifecycle.ts +104 -3
- package/ts/acme/index.ts +1 -0
- package/ts/classes.dcrouter.ts +193 -23
- package/ts/config/classes.route-config-manager.ts +197 -0
- package/ts/db/documents/classes.dns-authority.doc.ts +49 -0
- package/ts/db/documents/index.ts +1 -0
- package/ts/dns/classes.dns-server-runtime.ts +257 -38
- package/ts/dns/domain-ownership.ts +272 -0
- package/ts/dns/index.ts +2 -0
- package/ts/dns/manager.dns-authority.ts +558 -0
- package/ts/dns/manager.dns.ts +373 -27
- package/ts/errors/error.codes.ts +4 -0
- package/ts/opsserver/classes.opsserver.ts +2 -0
- package/ts/opsserver/handlers/acme-config.handler.ts +7 -0
- package/ts/opsserver/handlers/certificate.handler.ts +103 -8
- package/ts/opsserver/handlers/config.handler.ts +3 -1
- package/ts/opsserver/handlers/dns-authority.handler.ts +142 -0
- package/ts/opsserver/handlers/dns-provider.handler.ts +6 -0
- package/ts/opsserver/handlers/domain.handler.ts +12 -0
- package/ts/opsserver/handlers/gatewayclient.handler.ts +1 -1
- package/ts/opsserver/handlers/index.ts +1 -0
- package/ts/readme.md +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
package/ts/dns/manager.dns.ts
CHANGED
|
@@ -18,6 +18,28 @@ import type {
|
|
|
18
18
|
IDnsProviderPublic,
|
|
19
19
|
IProviderDomainListing,
|
|
20
20
|
} from '../../ts_interfaces/data/dns-provider.js';
|
|
21
|
+
import {
|
|
22
|
+
resolveDomainOwnership,
|
|
23
|
+
type IDomainOwnershipZone,
|
|
24
|
+
type TDomainOwnership,
|
|
25
|
+
} from './domain-ownership.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Where a runtime DnsServer handler came from.
|
|
29
|
+
*
|
|
30
|
+
* - 'persisted' → rebuilt from a DnsRecordDoc row.
|
|
31
|
+
* - 'generated-default' → synthesised by dcrouter (apex NS today; SOA and other
|
|
32
|
+
* zone defaults would join this origin), so it has no DB row to enumerate and
|
|
33
|
+
* must be torn down from the registry.
|
|
34
|
+
*/
|
|
35
|
+
export type TDnsRuntimeRegistrationOrigin = 'persisted' | 'generated-default';
|
|
36
|
+
|
|
37
|
+
/** One runtime DnsServer handler key owned by a DomainDoc. */
|
|
38
|
+
export interface IDnsRuntimeRegistration {
|
|
39
|
+
name: string;
|
|
40
|
+
type: TDnsRecordType;
|
|
41
|
+
origin: TDnsRuntimeRegistrationOrigin;
|
|
42
|
+
}
|
|
21
43
|
|
|
22
44
|
/**
|
|
23
45
|
* DnsManager — owns runtime DNS state on top of the embedded DnsServer.
|
|
@@ -36,7 +58,7 @@ import type {
|
|
|
36
58
|
export class DnsManager {
|
|
37
59
|
/**
|
|
38
60
|
* Reference to the active smartdns DnsServer (set by DcRouter once it exists).
|
|
39
|
-
* May be undefined if
|
|
61
|
+
* May be undefined if dnsNsDomains isn't configured.
|
|
40
62
|
*/
|
|
41
63
|
public dnsServer?: plugins.smartdns.dnsServerMod.DnsServer;
|
|
42
64
|
|
|
@@ -46,8 +68,94 @@ export class DnsManager {
|
|
|
46
68
|
*/
|
|
47
69
|
private providerClients = new Map<string, IDnsProviderClient>();
|
|
48
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Per-domain registry of the runtime DnsServer handlers this manager owns,
|
|
73
|
+
* keyed by DomainDoc.id then by `name|type`.
|
|
74
|
+
*
|
|
75
|
+
* Teardown must remove exactly what registration added — no more, no less.
|
|
76
|
+
* smartdns' `unregisterHandler(pattern, types)` removes *every* handler for a
|
|
77
|
+
* pattern/type pair, including the nameserver glue handlers owned by
|
|
78
|
+
* DnsServerRuntime and the global wildcard A handler. Reconstructing "would I
|
|
79
|
+
* have registered this?" at delete time is therefore unsafe: any drift in the
|
|
80
|
+
* inputs (authority changed, ownership changed, a record row vanished) makes the
|
|
81
|
+
* reconstruction tear down another owner's handlers or miss its own. Recording
|
|
82
|
+
* the actual registrations, with their origin, removes that class of bug.
|
|
83
|
+
*/
|
|
84
|
+
private runtimeRegistrations = new Map<string, Map<string, IDnsRuntimeRegistration>>();
|
|
85
|
+
|
|
86
|
+
/** Supplies the effective authority set (bootstrap ∪ delegation-verified). */
|
|
87
|
+
private authorityZonesResolver?: () => string[];
|
|
88
|
+
|
|
49
89
|
constructor(private options: IDcRouterOptions) {}
|
|
50
90
|
|
|
91
|
+
// ==========================================================================
|
|
92
|
+
// Runtime registration registry
|
|
93
|
+
// ==========================================================================
|
|
94
|
+
|
|
95
|
+
private trackRuntimeRegistration(
|
|
96
|
+
domainId: string,
|
|
97
|
+
name: string,
|
|
98
|
+
type: TDnsRecordType,
|
|
99
|
+
origin: TDnsRuntimeRegistrationOrigin,
|
|
100
|
+
): void {
|
|
101
|
+
let owned = this.runtimeRegistrations.get(domainId);
|
|
102
|
+
if (!owned) {
|
|
103
|
+
owned = new Map<string, IDnsRuntimeRegistration>();
|
|
104
|
+
this.runtimeRegistrations.set(domainId, owned);
|
|
105
|
+
}
|
|
106
|
+
owned.set(this.rrsetKey(name, type), { name: name.toLowerCase(), type, origin });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private untrackRuntimeRegistration(
|
|
110
|
+
domainId: string,
|
|
111
|
+
name: string,
|
|
112
|
+
type: TDnsRecordType,
|
|
113
|
+
): void {
|
|
114
|
+
const owned = this.runtimeRegistrations.get(domainId);
|
|
115
|
+
if (!owned) return;
|
|
116
|
+
owned.delete(this.rrsetKey(name, type));
|
|
117
|
+
if (owned.size === 0) {
|
|
118
|
+
this.runtimeRegistrations.delete(domainId);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Registrations currently owned by a domain. Exposed for teardown assertions. */
|
|
123
|
+
public listRuntimeRegistrations(domainId: string): IDnsRuntimeRegistration[] {
|
|
124
|
+
return [...(this.runtimeRegistrations.get(domainId)?.values() || [])];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Unregister every runtime handler owned by a domain and forget them.
|
|
129
|
+
*
|
|
130
|
+
* Called from every path that ends dcrouter's authority over a zone. Without
|
|
131
|
+
* it, deleting a DomainDoc removed the DB source of truth (so record queries
|
|
132
|
+
* started answering REFUSED) while the generated apex NS handler kept answering
|
|
133
|
+
* `aa` with our nameservers until the process was restarted — stale authority
|
|
134
|
+
* live in memory, with nothing telling the next operator to restart. A restart
|
|
135
|
+
* is not an acceptable delete procedure: it costs 30–60 s of total public
|
|
136
|
+
* outage.
|
|
137
|
+
*/
|
|
138
|
+
private tearDownDomainRuntimeRegistrations(domainId: string): void {
|
|
139
|
+
const owned = this.runtimeRegistrations.get(domainId);
|
|
140
|
+
this.runtimeRegistrations.delete(domainId);
|
|
141
|
+
if (!owned || !this.dnsServer) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const generatedZones: string[] = [];
|
|
145
|
+
for (const registration of owned.values()) {
|
|
146
|
+
this.dnsServer.unregisterHandler(registration.name, [registration.type]);
|
|
147
|
+
if (registration.origin === 'generated-default') {
|
|
148
|
+
generatedZones.push(`${registration.name} ${registration.type}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
logger.log(
|
|
152
|
+
'info',
|
|
153
|
+
`DnsManager: unregistered ${owned.size} runtime DNS handler key(s) for domain ${domainId}`
|
|
154
|
+
+ (generatedZones.length ? `, including generated default(s) ${generatedZones.join(', ')}` : ''),
|
|
155
|
+
{ zone: 'dns', domainId },
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
51
159
|
// ==========================================================================
|
|
52
160
|
// Lifecycle
|
|
53
161
|
// ==========================================================================
|
|
@@ -58,6 +166,7 @@ export class DnsManager {
|
|
|
58
166
|
|
|
59
167
|
public async stop(): Promise<void> {
|
|
60
168
|
this.providerClients.clear();
|
|
169
|
+
this.runtimeRegistrations.clear();
|
|
61
170
|
this.dnsServer = undefined;
|
|
62
171
|
}
|
|
63
172
|
|
|
@@ -84,6 +193,12 @@ export class DnsManager {
|
|
|
84
193
|
return;
|
|
85
194
|
}
|
|
86
195
|
const allDomains = await DomainDoc.findAll();
|
|
196
|
+
// One ownership snapshot for the whole startup pass, not one scan per zone.
|
|
197
|
+
const ownershipZones: IDomainOwnershipZone[] = allDomains.map((domainArg) => ({
|
|
198
|
+
name: domainArg.name,
|
|
199
|
+
source: domainArg.source,
|
|
200
|
+
providerId: domainArg.providerId,
|
|
201
|
+
}));
|
|
87
202
|
const dcrouterDomains = allDomains.filter((d) => d.source === 'dcrouter');
|
|
88
203
|
let registered = 0;
|
|
89
204
|
for (const domain of dcrouterDomains) {
|
|
@@ -94,7 +209,7 @@ export class DnsManager {
|
|
|
94
209
|
await this.refreshLocalRrset(domain.id, name, type);
|
|
95
210
|
}
|
|
96
211
|
registered += records.length;
|
|
97
|
-
await this.registerAuthoritativeZoneDefaults(domain);
|
|
212
|
+
await this.registerAuthoritativeZoneDefaults(domain, ownershipZones);
|
|
98
213
|
}
|
|
99
214
|
logger.log(
|
|
100
215
|
'info',
|
|
@@ -104,12 +219,22 @@ export class DnsManager {
|
|
|
104
219
|
|
|
105
220
|
/**
|
|
106
221
|
* Authoritative zones must answer apex NS queries or their delegation is
|
|
107
|
-
* lame — Let's Encrypt's DNS-01 resolver SERVFAILs on such zones.
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
222
|
+
* lame — Let's Encrypt's DNS-01 resolver SERVFAILs on such zones. This is the
|
|
223
|
+
* only place generated apex NS records come from, for every authoritative
|
|
224
|
+
* zone: they are served from options.dnsNsDomains unless explicit apex NS
|
|
225
|
+
* records exist in the DB. DnsServerRuntime used to emit a static set for
|
|
226
|
+
* bootstrap `dnsScopes` zones, which could neither appear for a zone verified
|
|
227
|
+
* after startup nor disappear for one whose authority was revoked.
|
|
228
|
+
*
|
|
229
|
+
* Ownership is verified first. smartdns marks any answer a registered handler
|
|
230
|
+
* produces as authoritative (`aa`) regardless of `authoritativeZones`, so
|
|
231
|
+
* registering this handler for a zone we cannot prove we own makes dcrouter
|
|
232
|
+
* publicly claim authority over somebody else's domain.
|
|
111
233
|
*/
|
|
112
|
-
private async registerAuthoritativeZoneDefaults(
|
|
234
|
+
private async registerAuthoritativeZoneDefaults(
|
|
235
|
+
domain: DomainDoc,
|
|
236
|
+
ownershipZonesArg?: IDomainOwnershipZone[],
|
|
237
|
+
): Promise<void> {
|
|
113
238
|
if (!this.dnsServer) {
|
|
114
239
|
return;
|
|
115
240
|
}
|
|
@@ -118,10 +243,29 @@ export class DnsManager {
|
|
|
118
243
|
return;
|
|
119
244
|
}
|
|
120
245
|
const zoneName = domain.name.toLowerCase();
|
|
121
|
-
//
|
|
122
|
-
if (this.
|
|
246
|
+
// Already serving this zone's generated apex NS — do not stack duplicate handlers.
|
|
247
|
+
if (this.runtimeRegistrations.get(domain.id)?.get(this.rrsetKey(zoneName, 'NS'))?.origin
|
|
248
|
+
=== 'generated-default') {
|
|
123
249
|
return;
|
|
124
250
|
}
|
|
251
|
+
|
|
252
|
+
const ownership = ownershipZonesArg
|
|
253
|
+
? resolveDomainOwnership({
|
|
254
|
+
fqdn: zoneName,
|
|
255
|
+
zones: ownershipZonesArg,
|
|
256
|
+
authorityZones: this.getAuthorityZones(),
|
|
257
|
+
})
|
|
258
|
+
: await this.resolveDomainOwnership(zoneName);
|
|
259
|
+
if (!ownership.verified) {
|
|
260
|
+
logger.log(
|
|
261
|
+
'error',
|
|
262
|
+
`DnsManager: refusing to serve authoritative apex NS records for ${zoneName} — ${ownership.detail}. `
|
|
263
|
+
+ 'dcrouter answers with the aa flag for every handler it registers, so this zone stays unserved (REFUSED) until ownership is verified.',
|
|
264
|
+
{ zone: 'dns', domainId: domain.id, ownershipReason: ownership.reason },
|
|
265
|
+
);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
125
269
|
const existingRecords = await DnsRecordDoc.findByDomainId(domain.id);
|
|
126
270
|
const hasExplicitApexNs = existingRecords.some(
|
|
127
271
|
(rec) => rec.type === 'NS' && rec.name.toLowerCase() === zoneName,
|
|
@@ -143,12 +287,165 @@ export class DnsManager {
|
|
|
143
287
|
return null;
|
|
144
288
|
});
|
|
145
289
|
}
|
|
290
|
+
this.trackRuntimeRegistration(domain.id, zoneName, 'NS', 'generated-default');
|
|
146
291
|
logger.log(
|
|
147
292
|
'info',
|
|
148
|
-
`DnsManager: serving default apex NS records for authoritative zone ${zoneName}
|
|
293
|
+
`DnsManager: serving default apex NS records for authoritative zone ${zoneName} `
|
|
294
|
+
+ `(${nsDomains.join(', ')}); ownership verified via ${ownership.method} (${ownership.evidence})`,
|
|
149
295
|
);
|
|
150
296
|
}
|
|
151
297
|
|
|
298
|
+
// ==========================================================================
|
|
299
|
+
// Domain ownership
|
|
300
|
+
// ==========================================================================
|
|
301
|
+
|
|
302
|
+
/** Ownership inputs for every managed zone, fetched once per evaluation pass. */
|
|
303
|
+
public async listOwnershipZones(): Promise<IDomainOwnershipZone[]> {
|
|
304
|
+
const domains = await DomainDoc.findAll();
|
|
305
|
+
return domains.map((domainArg) => ({
|
|
306
|
+
name: domainArg.name,
|
|
307
|
+
source: domainArg.source,
|
|
308
|
+
providerId: domainArg.providerId,
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The authority set used as an ownership proof: the delegation-verified zones
|
|
314
|
+
* held in the database, and nothing else. Empty until the resolver is wired,
|
|
315
|
+
* which fails closed — an unwired manager proves ownership of nothing rather
|
|
316
|
+
* than falling back to a declared list that no longer exists.
|
|
317
|
+
*/
|
|
318
|
+
public getAuthorityZones(): string[] {
|
|
319
|
+
return this.authorityZonesResolver?.() || [];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Supply the effective authority set. Injected rather than imported so
|
|
324
|
+
* DnsManager keeps a single direction of dependency and stays unit-testable.
|
|
325
|
+
*/
|
|
326
|
+
public setAuthorityZonesResolver(resolver?: () => string[]): void {
|
|
327
|
+
this.authorityZonesResolver = resolver;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Re-derive every generated apex NS registration against the current authority
|
|
332
|
+
* set. Zones that gained proof start being served; zones that lost it are torn
|
|
333
|
+
* down — both in-process, with no restart.
|
|
334
|
+
*
|
|
335
|
+
* Throws on the first failure so the caller can roll the change back rather
|
|
336
|
+
* than leave the router half-converted.
|
|
337
|
+
*/
|
|
338
|
+
public async reconcileAuthoritativeZones(): Promise<{ registered: string[]; unregistered: string[] }> {
|
|
339
|
+
const registered: string[] = [];
|
|
340
|
+
const unregistered: string[] = [];
|
|
341
|
+
if (!this.dnsServer) {
|
|
342
|
+
return { registered, unregistered };
|
|
343
|
+
}
|
|
344
|
+
const allDomains = await DomainDoc.findAll();
|
|
345
|
+
const ownershipZones: IDomainOwnershipZone[] = allDomains.map((domainArg) => ({
|
|
346
|
+
name: domainArg.name,
|
|
347
|
+
source: domainArg.source,
|
|
348
|
+
providerId: domainArg.providerId,
|
|
349
|
+
}));
|
|
350
|
+
|
|
351
|
+
for (const domain of allDomains) {
|
|
352
|
+
if (domain.source !== 'dcrouter') continue;
|
|
353
|
+
const zoneName = domain.name.toLowerCase();
|
|
354
|
+
const ownership = resolveDomainOwnership({
|
|
355
|
+
fqdn: zoneName,
|
|
356
|
+
zones: ownershipZones,
|
|
357
|
+
authorityZones: this.getAuthorityZones(),
|
|
358
|
+
});
|
|
359
|
+
const hasGenerated = this.runtimeRegistrations
|
|
360
|
+
.get(domain.id)?.get(this.rrsetKey(zoneName, 'NS'))?.origin === 'generated-default';
|
|
361
|
+
|
|
362
|
+
if (!ownership.verified && hasGenerated) {
|
|
363
|
+
// Authority lost: stop answering for it immediately.
|
|
364
|
+
this.untrackRuntimeRegistration(domain.id, zoneName, 'NS');
|
|
365
|
+
this.dnsServer.unregisterHandler(zoneName, ['NS']);
|
|
366
|
+
unregistered.push(zoneName);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (ownership.verified && !hasGenerated) {
|
|
370
|
+
await this.registerAuthoritativeZoneDefaults(domain, ownershipZones);
|
|
371
|
+
if (this.runtimeRegistrations
|
|
372
|
+
.get(domain.id)?.get(this.rrsetKey(zoneName, 'NS'))?.origin === 'generated-default') {
|
|
373
|
+
registered.push(zoneName);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Persisted records of newly-authoritative zones are already registered by
|
|
379
|
+
// applyDcrouterDomainsToDnsServer(); only the generated defaults are gated on
|
|
380
|
+
// ownership, so nothing else needs rebuilding here.
|
|
381
|
+
if (registered.length || unregistered.length) {
|
|
382
|
+
logger.log(
|
|
383
|
+
'info',
|
|
384
|
+
`DnsManager: authority reconciliation registered ${registered.length} and unregistered ${unregistered.length} generated apex NS zone(s)`,
|
|
385
|
+
{ zone: 'dns' },
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
return { registered, unregistered };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Keep `DomainDoc.authoritative` honest after an authority change. */
|
|
392
|
+
public async syncAuthoritativeFlags(): Promise<number> {
|
|
393
|
+
const allDomains = await DomainDoc.findAll();
|
|
394
|
+
const ownershipZones: IDomainOwnershipZone[] = allDomains.map((domainArg) => ({
|
|
395
|
+
name: domainArg.name,
|
|
396
|
+
source: domainArg.source,
|
|
397
|
+
providerId: domainArg.providerId,
|
|
398
|
+
}));
|
|
399
|
+
const authorityZones = this.getAuthorityZones();
|
|
400
|
+
let changed = 0;
|
|
401
|
+
for (const domain of allDomains) {
|
|
402
|
+
if (domain.source !== 'dcrouter') continue;
|
|
403
|
+
const ownership = resolveDomainOwnership({
|
|
404
|
+
fqdn: domain.name,
|
|
405
|
+
zones: ownershipZones,
|
|
406
|
+
authorityZones,
|
|
407
|
+
});
|
|
408
|
+
if (domain.authoritative === ownership.verified) continue;
|
|
409
|
+
domain.authoritative = ownership.verified;
|
|
410
|
+
domain.updatedAt = Date.now();
|
|
411
|
+
await domain.save();
|
|
412
|
+
changed++;
|
|
413
|
+
}
|
|
414
|
+
return changed;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Can we prove the zone containing `fqdn` is ours? This gates certificate
|
|
419
|
+
* requirements and authoritative DNS. See ts/dns/domain-ownership.ts.
|
|
420
|
+
*/
|
|
421
|
+
public async resolveDomainOwnership(fqdn: string): Promise<TDomainOwnership> {
|
|
422
|
+
return resolveDomainOwnership({
|
|
423
|
+
fqdn,
|
|
424
|
+
zones: await this.listOwnershipZones(),
|
|
425
|
+
authorityZones: this.getAuthorityZones(),
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Ownership of a zone that is about to become dcrouter-hosted, evaluated
|
|
431
|
+
* against its post-write shape. Create and provider→dcrouter migration must
|
|
432
|
+
* not read their own pre-write row: the provider link they are removing would
|
|
433
|
+
* otherwise still count as the proof.
|
|
434
|
+
*/
|
|
435
|
+
private async resolveOwnershipForPendingDcrouterZone(
|
|
436
|
+
zoneName: string,
|
|
437
|
+
): Promise<TDomainOwnership> {
|
|
438
|
+
const name = zoneName.trim().toLowerCase();
|
|
439
|
+
const zones: IDomainOwnershipZone[] = (await this.listOwnershipZones())
|
|
440
|
+
.filter((zoneArg) => zoneArg.name.trim().toLowerCase() !== name);
|
|
441
|
+
zones.push({ name, source: 'dcrouter' });
|
|
442
|
+
return resolveDomainOwnership({
|
|
443
|
+
fqdn: name,
|
|
444
|
+
zones,
|
|
445
|
+
authorityZones: this.getAuthorityZones(),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
152
449
|
/**
|
|
153
450
|
* Register a single record with the embedded DnsServer. The handler closure
|
|
154
451
|
* captures the record fields, so updates require a re-register cycle.
|
|
@@ -167,13 +464,22 @@ export class DnsManager {
|
|
|
167
464
|
}
|
|
168
465
|
return null;
|
|
169
466
|
});
|
|
467
|
+
// Tracked here rather than at each call site so the direct registration in
|
|
468
|
+
// migrateToDcrouter() is covered as well.
|
|
469
|
+
this.trackRuntimeRegistration(rec.domainId, rec.name, rec.type, 'persisted');
|
|
170
470
|
}
|
|
171
471
|
|
|
172
472
|
private rrsetKey(name: string, type: TDnsRecordType): string {
|
|
173
473
|
return `${name.toLowerCase()}|${type}`;
|
|
174
474
|
}
|
|
175
475
|
|
|
176
|
-
/**
|
|
476
|
+
/**
|
|
477
|
+
* Rebuild one authoritative RRset atomically from persisted rows.
|
|
478
|
+
*
|
|
479
|
+
* The unregister-then-rebuild cycle also replaces a generated default on the
|
|
480
|
+
* same key (an explicit apex NS row supersedes the generated one), so the
|
|
481
|
+
* registry is updated to match what is actually registered afterwards.
|
|
482
|
+
*/
|
|
177
483
|
private async refreshLocalRrset(
|
|
178
484
|
domainId: string,
|
|
179
485
|
name: string,
|
|
@@ -181,6 +487,7 @@ export class DnsManager {
|
|
|
181
487
|
): Promise<void> {
|
|
182
488
|
if (!this.dnsServer) return;
|
|
183
489
|
this.dnsServer.unregisterHandler(name, [type]);
|
|
490
|
+
this.untrackRuntimeRegistration(domainId, name, type);
|
|
184
491
|
const records = await DnsRecordDoc.findByDomainId(domainId);
|
|
185
492
|
for (const record of records) {
|
|
186
493
|
if (record.name.toLowerCase() === name.toLowerCase() && record.type === type) {
|
|
@@ -511,8 +818,14 @@ export class DnsManager {
|
|
|
511
818
|
}
|
|
512
819
|
|
|
513
820
|
/**
|
|
514
|
-
* Create a dcrouter-hosted
|
|
515
|
-
*
|
|
821
|
+
* Create a dcrouter-hosted domain. dcrouter serves DNS records for it via the
|
|
822
|
+
* embedded smartdns.DnsServer.
|
|
823
|
+
*
|
|
824
|
+
* `authoritative` reflects whether ownership can actually be proven — it used
|
|
825
|
+
* to be hard-coded to `true`, which let an ops-API caller self-assert authority
|
|
826
|
+
* over any zone on the internet. Creating the record still succeeds (it is the
|
|
827
|
+
* container records and provider migration need), but an unverified zone is
|
|
828
|
+
* recorded as non-authoritative and gets no generated apex NS handler.
|
|
516
829
|
*/
|
|
517
830
|
public async createDcrouterDomain(args: {
|
|
518
831
|
name: string;
|
|
@@ -527,17 +840,25 @@ export class DnsManager {
|
|
|
527
840
|
if (existing) {
|
|
528
841
|
throw new Error(`domain ${name} already exists`);
|
|
529
842
|
}
|
|
843
|
+
const ownership = await this.resolveOwnershipForPendingDcrouterZone(name);
|
|
530
844
|
const now = Date.now();
|
|
531
845
|
const doc = new DomainDoc();
|
|
532
846
|
doc.id = plugins.uuid.v4();
|
|
533
847
|
doc.name = name;
|
|
534
848
|
doc.source = 'dcrouter';
|
|
535
|
-
doc.authoritative =
|
|
849
|
+
doc.authoritative = ownership.verified;
|
|
536
850
|
doc.description = args.description;
|
|
537
851
|
doc.createdAt = now;
|
|
538
852
|
doc.updatedAt = now;
|
|
539
853
|
doc.createdBy = args.createdBy;
|
|
540
854
|
await doc.save();
|
|
855
|
+
if (!ownership.verified) {
|
|
856
|
+
logger.log(
|
|
857
|
+
'error',
|
|
858
|
+
`DnsManager: created dcrouter-hosted domain ${name} as NON-authoritative — ${ownership.detail}`,
|
|
859
|
+
{ zone: 'dns', domainId: doc.id, ownershipReason: ownership.reason },
|
|
860
|
+
);
|
|
861
|
+
}
|
|
541
862
|
await this.registerAuthoritativeZoneDefaults(doc);
|
|
542
863
|
return doc.id;
|
|
543
864
|
}
|
|
@@ -621,26 +942,33 @@ export class DnsManager {
|
|
|
621
942
|
* For dcrouter-hosted domains, also unregisters records from the embedded
|
|
622
943
|
* DnsServer.
|
|
623
944
|
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
*
|
|
945
|
+
* The unregister path is complete in-process: every runtime handler this
|
|
946
|
+
* manager registered for the domain — persisted RRsets and generated defaults
|
|
947
|
+
* alike — is removed from the registry, so deletion never leaves stale
|
|
948
|
+
* authority answering `aa` until an unrelated restart. Nameserver glue
|
|
949
|
+
* handlers and other domains' handlers are never touched, because only the
|
|
950
|
+
* domain's own recorded registrations are removed.
|
|
628
951
|
*/
|
|
629
952
|
public async deleteDomain(id: string): Promise<boolean> {
|
|
630
953
|
const doc = await DomainDoc.findById(id);
|
|
631
954
|
if (!doc) return false;
|
|
955
|
+
// Tear runtime state down before the durable rows go away: the registry is
|
|
956
|
+
// the authority on what to remove, so this does not depend on the records
|
|
957
|
+
// still being readable.
|
|
958
|
+
this.tearDownDomainRuntimeRegistrations(id);
|
|
632
959
|
const records = await DnsRecordDoc.findByDomainId(id);
|
|
633
|
-
const localRrsets = new Set(records.map((record) => this.rrsetKey(record.name, record.type)));
|
|
634
960
|
for (const r of records) {
|
|
635
961
|
await r.delete();
|
|
636
962
|
}
|
|
637
|
-
if (doc.source === 'dcrouter' && this.dnsServer) {
|
|
638
|
-
for (const rrset of localRrsets) {
|
|
639
|
-
const [name, type] = rrset.split('|') as [string, TDnsRecordType];
|
|
640
|
-
this.dnsServer.unregisterHandler(name, [type]);
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
963
|
await doc.delete();
|
|
964
|
+
const leftover = this.listRuntimeRegistrations(id);
|
|
965
|
+
if (leftover.length > 0) {
|
|
966
|
+
logger.log(
|
|
967
|
+
'error',
|
|
968
|
+
`DnsManager: ${leftover.length} runtime DNS handler key(s) are still registered for deleted domain ${doc.name}`,
|
|
969
|
+
{ zone: 'dns', domainId: id },
|
|
970
|
+
);
|
|
971
|
+
}
|
|
644
972
|
return true;
|
|
645
973
|
}
|
|
646
974
|
|
|
@@ -843,6 +1171,13 @@ export class DnsManager {
|
|
|
843
1171
|
}
|
|
844
1172
|
}
|
|
845
1173
|
|
|
1174
|
+
// The provider is authoritative from here on, so every runtime handler we
|
|
1175
|
+
// still own for this zone goes away — including the generated apex NS and any
|
|
1176
|
+
// record whose individual migration above threw. Leaving them registered
|
|
1177
|
+
// would keep answering `aa` for a zone we no longer serve: the same
|
|
1178
|
+
// stale-authority leak that deletion had.
|
|
1179
|
+
this.tearDownDomainRuntimeRegistrations(domain.id);
|
|
1180
|
+
|
|
846
1181
|
// Update domain metadata
|
|
847
1182
|
domain.source = 'provider';
|
|
848
1183
|
domain.authoritative = false;
|
|
@@ -879,15 +1214,25 @@ export class DnsManager {
|
|
|
879
1214
|
}
|
|
880
1215
|
}
|
|
881
1216
|
|
|
882
|
-
// Update domain metadata
|
|
1217
|
+
// Update domain metadata. Losing the provider link means the credentialed
|
|
1218
|
+
// zone listing no longer proves ownership, so authority is re-derived rather
|
|
1219
|
+
// than assumed.
|
|
883
1220
|
domain.source = 'dcrouter';
|
|
884
|
-
domain.authoritative = true;
|
|
885
1221
|
domain.providerId = undefined;
|
|
886
1222
|
domain.externalZoneId = undefined;
|
|
887
1223
|
domain.nameservers = undefined;
|
|
888
1224
|
domain.lastSyncedAt = undefined;
|
|
889
1225
|
domain.updatedAt = Date.now();
|
|
1226
|
+
const ownership = await this.resolveOwnershipForPendingDcrouterZone(domain.name);
|
|
1227
|
+
domain.authoritative = ownership.verified;
|
|
890
1228
|
await domain.save();
|
|
1229
|
+
if (!ownership.verified) {
|
|
1230
|
+
logger.log(
|
|
1231
|
+
'error',
|
|
1232
|
+
`DnsManager: domain ${domain.name} migrated to dcrouter as NON-authoritative — ${ownership.detail}`,
|
|
1233
|
+
{ zone: 'dns', domainId: domain.id, ownershipReason: ownership.reason },
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
891
1236
|
await this.registerAuthoritativeZoneDefaults(domain);
|
|
892
1237
|
|
|
893
1238
|
logger.log('info', `Domain ${domain.name} migrated to dcrouter (${migrated} records)`);
|
|
@@ -1077,6 +1422,7 @@ export class DnsManager {
|
|
|
1077
1422
|
* Unregister a record's handler from the embedded DnsServer.
|
|
1078
1423
|
*/
|
|
1079
1424
|
public unregisterRecordFromDnsServer(rec: DnsRecordDoc): void {
|
|
1425
|
+
this.untrackRuntimeRegistration(rec.domainId, rec.name, rec.type);
|
|
1080
1426
|
if (!this.dnsServer) return;
|
|
1081
1427
|
this.dnsServer.unregisterHandler(rec.name, [rec.type]);
|
|
1082
1428
|
}
|
package/ts/errors/error.codes.ts
CHANGED
|
@@ -80,6 +80,10 @@ export const DCR_ROUTING_ERROR = 'DCR_ROUTING_ERROR';
|
|
|
80
80
|
export const DCR_CONFIGURATION_ERROR = 'DCR_CONFIGURATION_ERROR';
|
|
81
81
|
export const DCR_PROXY_ERROR = 'DCR_PROXY_ERROR';
|
|
82
82
|
export const DCR_DOMAIN_ERROR = 'DCR_DOMAIN_ERROR';
|
|
83
|
+
/** A certificate requirement or authoritative DNS zone was requested for a domain we cannot prove we own. */
|
|
84
|
+
export const DCR_DOMAIN_OWNERSHIP_UNVERIFIED = 'DCR_DOMAIN_OWNERSHIP_UNVERIFIED';
|
|
85
|
+
/** ACME failed for a configuration cause that no amount of retrying can resolve. */
|
|
86
|
+
export const DCR_ACME_PERMANENT_FAILURE = 'DCR_ACME_PERMANENT_FAILURE';
|
|
83
87
|
|
|
84
88
|
// SMS service errors (SMS_*)
|
|
85
89
|
export const SMS_SERVICE_ERROR = 'SMS_SERVICE_ERROR';
|
|
@@ -37,6 +37,7 @@ export class OpsServer {
|
|
|
37
37
|
private usersHandler!: handlers.UsersHandler;
|
|
38
38
|
private dnsProviderHandler!: handlers.DnsProviderHandler;
|
|
39
39
|
private domainHandler!: handlers.DomainHandler;
|
|
40
|
+
private dnsAuthorityHandler!: handlers.DnsAuthorityHandler;
|
|
40
41
|
private dnsRecordHandler!: handlers.DnsRecordHandler;
|
|
41
42
|
private acmeConfigHandler!: handlers.AcmeConfigHandler;
|
|
42
43
|
private emailDomainHandler!: handlers.EmailDomainHandler;
|
|
@@ -104,6 +105,7 @@ export class OpsServer {
|
|
|
104
105
|
this.usersHandler = new handlers.UsersHandler(this);
|
|
105
106
|
this.dnsProviderHandler = new handlers.DnsProviderHandler(this);
|
|
106
107
|
this.domainHandler = new handlers.DomainHandler(this);
|
|
108
|
+
this.dnsAuthorityHandler = new handlers.DnsAuthorityHandler(this);
|
|
107
109
|
this.dnsRecordHandler = new handlers.DnsRecordHandler(this);
|
|
108
110
|
this.acmeConfigHandler = new handlers.AcmeConfigHandler(this);
|
|
109
111
|
this.emailDomainHandler = new handlers.EmailDomainHandler(this);
|
|
@@ -66,6 +66,13 @@ export class AcmeConfigHandler {
|
|
|
66
66
|
},
|
|
67
67
|
userId,
|
|
68
68
|
);
|
|
69
|
+
// The SmartAcme startup budget does not re-arm on a timer, so a
|
|
70
|
+
// corrected ACME configuration must re-arm it explicitly. Otherwise a
|
|
71
|
+
// permanently-failed or exhausted provider stays down until an
|
|
72
|
+
// unrelated SmartProxy rebuild or a restart (30–60 s of outage).
|
|
73
|
+
this.opsServerRef.dcRouterRef.smartAcmeLifecycle?.rearm(
|
|
74
|
+
'ACME configuration updated',
|
|
75
|
+
);
|
|
69
76
|
return { success: true, config: updated };
|
|
70
77
|
} catch (err: unknown) {
|
|
71
78
|
return { success: false, message: (err as Error).message };
|