@serve.zone/dcrouter 17.10.1 → 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/classes.gateway-route-dns-reconciler.d.ts +7 -0
- package/dist_ts/dns/classes.gateway-route-dns-reconciler.js +37 -6
- 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_migrations/index.js +122 -9
- 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/classes.gateway-route-dns-reconciler.ts +41 -6
- 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
|
@@ -60,9 +60,14 @@ export function getOciContainerConfig() {
|
|
|
60
60
|
if (nsDomains) {
|
|
61
61
|
options.dnsNsDomains = nsDomains;
|
|
62
62
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
// DCROUTER_DNS_SCOPES is intentionally not read. DNS authority is
|
|
64
|
+
// delegation-verified state in the database, not deployment configuration.
|
|
65
|
+
// Called out rather than dropped silently, because a container still setting
|
|
66
|
+
// it would otherwise look configured and answer for nothing.
|
|
67
|
+
if (process.env.DCROUTER_DNS_SCOPES) {
|
|
68
|
+
console.warn('[OCI Container] DCROUTER_DNS_SCOPES is set but no longer honored: dcrouter derives DNS authority from the database, '
|
|
69
|
+
+ 'where a zone is admitted by having its public NS records observed naming DCROUTER_DNS_NS_DOMAINS. '
|
|
70
|
+
+ 'Verify each zone through the ops API (dns-authority:write) instead.');
|
|
66
71
|
}
|
|
67
72
|
if (process.env.DCROUTER_DNS_BIND_INTERFACE) {
|
|
68
73
|
options.dnsBindInterface = process.env.DCROUTER_DNS_BIND_INTERFACE;
|
|
@@ -111,4 +116,4 @@ export function getOciContainerConfig() {
|
|
|
111
116
|
}
|
|
112
117
|
return options;
|
|
113
118
|
}
|
|
114
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
119
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90c19vY2lfY29udGFpbmVyL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sY0FBYyxDQUFDO0FBR3hDOzs7R0FHRztBQUNILFNBQVMsbUJBQW1CLENBQUMsTUFBMEI7SUFDckQsSUFBSSxDQUFDLE1BQU0sSUFBSSxNQUFNLENBQUMsSUFBSSxFQUFFLEtBQUssRUFBRTtRQUFFLE9BQU8sU0FBUyxDQUFDO0lBQ3RELE9BQU8sTUFBTSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsQ0FBQztBQUNoRSxDQUFDO0FBRUQ7OztHQUdHO0FBQ0gsU0FBUywwQkFBMEIsQ0FBQyxNQUEwQjtJQUM1RCxNQUFNLEtBQUssR0FBRyxtQkFBbUIsQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUMxQyxJQUFJLENBQUMsS0FBSztRQUFFLE9BQU8sU0FBUyxDQUFDO0lBQzdCLE9BQU8sS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUMsUUFBUSxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUNwRSxDQUFDO0FBRUQ7Ozs7O0dBS0c7QUFDSCxNQUFNLFVBQVUscUJBQXFCO0lBQ25DLElBQUksT0FBTyxHQUFxQixFQUFFLENBQUM7SUFFbkMscUNBQXFDO0lBQ3JDLE1BQU0sVUFBVSxHQUFHLE9BQU8sQ0FBQyxHQUFHLENBQUMsb0JBQW9CLENBQUM7SUFDcEQsSUFBSSxVQUFVLElBQUksT0FBTyxDQUFDLEVBQUUsQ0FBQyxVQUFV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
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@serve.zone/dcrouter',
|
|
6
|
-
version: '
|
|
6
|
+
version: '18.0.0',
|
|
7
7
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
|
8
8
|
};
|
|
9
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
9
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHNfd2ViLzAwX2NvbW1pdGluZm9fZGF0YS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7R0FFRztBQUNILE1BQU0sQ0FBQyxNQUFNLFVBQVUsR0FBRztJQUN4QixJQUFJLEVBQUUsc0JBQXNCO0lBQzVCLE9BQU8sRUFBRSxRQUFRO0lBQ2pCLFdBQVcsRUFBRSwwRUFBMEU7Q0FDeEYsQ0FBQSJ9
|
package/package.json
CHANGED
package/readme.hints.md
CHANGED
|
@@ -1,5 +1,417 @@
|
|
|
1
1
|
# Implementation Hints and Learnings
|
|
2
2
|
|
|
3
|
+
## Derived state must name its invalidation trigger (2026-07-25)
|
|
4
|
+
|
|
5
|
+
Three separate production failures on one day had the same shape: **state was
|
|
6
|
+
mutated in one layer and the mirrored state in another layer was left behind.**
|
|
7
|
+
|
|
8
|
+
1. `DomainDoc` deleted → the generated apex NS handler stayed registered in the
|
|
9
|
+
embedded smartdns server, still answering with the `aa` flag until dcrouter was
|
|
10
|
+
restarted, while record queries already answered REFUSED.
|
|
11
|
+
2. A certificate was successfully re-issued and stored → SmartProxy's private
|
|
12
|
+
per-domain `certFailureAt` cooldown still gated the domain, so every route
|
|
13
|
+
reapply skipped it and the proxy kept serving the expired certificate. No
|
|
14
|
+
error, no log (the sweep returns before its summary line when every domain is
|
|
15
|
+
skipped).
|
|
16
|
+
3. A null sentinel was translated for validation but persisted raw.
|
|
17
|
+
|
|
18
|
+
### The rule
|
|
19
|
+
|
|
20
|
+
> Every cache, negative cache, or derived registration must name, in code, the
|
|
21
|
+
> mutation that invalidates it — and the invalidation must run inside the same
|
|
22
|
+
> operation that mutates the source of truth.
|
|
23
|
+
|
|
24
|
+
Corollaries, all of which are review-blocking:
|
|
25
|
+
|
|
26
|
+
- A registration is torn down by the same operation that removes what it was
|
|
27
|
+
derived from. Track what you actually registered; do not recompute "would I
|
|
28
|
+
have registered this?" at teardown time. `DnsServer.unregisterHandler(pattern,
|
|
29
|
+
types)` removes **every** handler for that pattern/type pair, so a recomputed
|
|
30
|
+
guess can tear down another owner's records — or miss its own when the inputs
|
|
31
|
+
have drifted. `DnsManager.runtimeRegistrations` is the pattern: a per-domain
|
|
32
|
+
registry of `name|type` keys, each tagged `persisted` or `generated-default`,
|
|
33
|
+
torn down by `tearDownDomainRuntimeRegistrations(domainId)`.
|
|
34
|
+
- A negative cache is invalidated by the event that **resolves its cause**, not
|
|
35
|
+
only by elapsed time. Time-only expiry plus a skip-while-cooling-down rule is a
|
|
36
|
+
deadlock: the sweep that would clear the entry is the sweep the entry blocks.
|
|
37
|
+
- **Issued is not served.** An operation that advances one layer must verify the
|
|
38
|
+
layer that actually serves traffic before reporting success. The certificate
|
|
39
|
+
overview reports `expiryDate` (issued/stored) and `served` (what the Rust engine
|
|
40
|
+
presents) separately and downgrades the combined `status` to `failed` with
|
|
41
|
+
`servedMismatch` when they disagree; `reprovisionCertificateDomain` refuses to
|
|
42
|
+
return success while the engine still serves an invalid certificate.
|
|
43
|
+
- If the layer that owns the derived state exposes no invalidation API, that is
|
|
44
|
+
the defect. A periodic sweep that blindly clears caches, or a shortened TTL, is
|
|
45
|
+
a workaround that hides it — fix the owning layer. Reaching into another
|
|
46
|
+
package's private state (e.g. `smartProxy.bridge`) is an internal bypass and
|
|
47
|
+
must not ship.
|
|
48
|
+
|
|
49
|
+
### Known open instance (needs an upstream change)
|
|
50
|
+
|
|
51
|
+
`@push.rocks/smartproxy` (27.20.0) keeps `certValidity` and `certFailureAt` as
|
|
52
|
+
**private** in-memory maps and exposes no per-domain invalidation. The only two
|
|
53
|
+
public methods that look relevant, `provisionCertificate(routeName)` and
|
|
54
|
+
`renewCertificate(routeName)`, both go through the Rust ACME bridge, which
|
|
55
|
+
SmartProxy force-disables whenever `certProvisionFunction` is set — so a consumer
|
|
56
|
+
using `certProvisionFunction` has no supported way to clear the cooldown or
|
|
57
|
+
hot-load a certificate for one domain. Smallest correct upstream addition:
|
|
58
|
+
`invalidateCertProvisionCooldown(domain: string): boolean` (plus, ideally, a
|
|
59
|
+
`'skip'` member of `TSmartProxyCertProvisionObject` so a consumer can decline an
|
|
60
|
+
attempt without SmartProxy recording it as a failure and re-arming the cooldown).
|
|
61
|
+
Until that ships, the only operator remedy is toggling the single affected route
|
|
62
|
+
off and on; a dcrouter restart costs 30–60 s of total public outage.
|
|
63
|
+
|
|
64
|
+
## DNS authority is proven by delegation, not declared (2026-07-25)
|
|
65
|
+
|
|
66
|
+
`dnsScopes` was the last un-migrated bootstrap option in an otherwise DB-driven
|
|
67
|
+
router: read once from the OCI container config at startup, with no mutation
|
|
68
|
+
path, no watcher, no reload. Claiming a newly delegated zone therefore required a
|
|
69
|
+
restart — 30–60 s of total public outage.
|
|
70
|
+
|
|
71
|
+
**The fix is not a `setDnsScopes` mutation.** The domain-ownership predicate
|
|
72
|
+
accepts scope coverage as *proof* precisely because only a deploy can change it.
|
|
73
|
+
An editable declared list is self-assertion, and would have re-opened the exact
|
|
74
|
+
hole that `createDcrouterDomain` had. Instead a zone earns authority by its
|
|
75
|
+
**public delegation naming our `dnsNsDomains`** — unforgeable through the ops API,
|
|
76
|
+
because you cannot repoint a domain's NS records without controlling the domain.
|
|
77
|
+
That is also the comparison that was missing: declared authority versus real
|
|
78
|
+
delegation, made the mechanism rather than an audit bolted on afterwards.
|
|
79
|
+
|
|
80
|
+
- `ts_interfaces/data/dns-authority.ts` — the contract.
|
|
81
|
+
- `ts/db/documents/classes.dns-authority.doc.ts` — singleton doc holding only
|
|
82
|
+
*verified* zones, following the `AcmeConfigDoc` pattern.
|
|
83
|
+
- `ts/dns/manager.dns-authority.ts` — probe, mutations, drift audit.
|
|
84
|
+
- `ts/opsserver/handlers/dns-authority.handler.ts` — `dns-authority:read` /
|
|
85
|
+
`dns-authority:write`, admin identity required on write.
|
|
86
|
+
|
|
87
|
+
**Precedence: there is none, because there is one source.** `dnsScopes` is gone
|
|
88
|
+
— the option, the `DCROUTER_DNS_SCOPES` env override, and the bootstrap floor it
|
|
89
|
+
contributed. The authority set is the delegation-verified zones in the database
|
|
90
|
+
and nothing else.
|
|
91
|
+
|
|
92
|
+
The union it replaced was a floor: always in effect, changeable only by a
|
|
93
|
+
redeploy, and **not revocable through the API**. Those are the same three
|
|
94
|
+
properties that produced the outage this path exists to prevent, so keeping them
|
|
95
|
+
as a safety net kept the disease as the cure.
|
|
96
|
+
|
|
97
|
+
Removing the floor makes the *unreadable database* case load-bearing, which is
|
|
98
|
+
what the original union was really defending against. The defence was right; the
|
|
99
|
+
mechanism was not. Three situations, three answers — and the whole point is that
|
|
100
|
+
they are three, not two:
|
|
101
|
+
|
|
102
|
+
| Situation | Authority set | Behaviour |
|
|
103
|
+
| --- | --- | --- |
|
|
104
|
+
| document missing | known-empty | claim nothing, log at `error`, DNS server still starts |
|
|
105
|
+
| document present, zero zones | known-empty | same, different message — somebody revoked everything, and that is an instruction |
|
|
106
|
+
| document unreadable | **unknown** | `start()` throws; `DnsServer` depends on `DnsManager`, so it does not come up claiming nothing |
|
|
107
|
+
|
|
108
|
+
Rendering an unknown as "claim nothing" is precisely how a transient database
|
|
109
|
+
fault would take every zone off the air. Fail closed on *authority*; never
|
|
110
|
+
silently on *service*. With an empty set the DNS server still binds :53, still
|
|
111
|
+
serves DoH, still runs the private-route overlay, and picks a zone up the moment
|
|
112
|
+
it is verified — REFUSED is a fast, honest "not ours", where an unbound port is
|
|
113
|
+
a timeout.
|
|
114
|
+
|
|
115
|
+
**The probe must not use the system resolver.** `smartdns.getNameServers()` calls
|
|
116
|
+
`dns.resolveNs`, i.e. the system resolver, which on a dcrouter host may be
|
|
117
|
+
dcrouter itself — it would answer with the very NS records we generated, making
|
|
118
|
+
the probe self-confirming. Use
|
|
119
|
+
`new plugins.smartdns.dnsClientMod.Smartdns({ strategy: 'doh', allowDohFallback: false })`
|
|
120
|
+
and `queryRecords(zone, 'NS')`: one DNS-over-HTTPS attempt against a public
|
|
121
|
+
resolver, no system fallback.
|
|
122
|
+
|
|
123
|
+
**Three verdicts, never two.** `delegated` / `not-delegated` / `undeterminable`.
|
|
124
|
+
A timeout is not evidence that a zone is not ours. A mutation refuses on
|
|
125
|
+
`undeterminable` (never assume either way); the startup drift audit *skips* it.
|
|
126
|
+
|
|
127
|
+
**The drift audit is advisory and never mutates.** It reports three directions —
|
|
128
|
+
`delegated-but-unclaimed` (a live zone we could serve but do not),
|
|
129
|
+
`claimed-but-not-delegated` (authority we still assert after delegation moved
|
|
130
|
+
away), and `verified-but-unhosted` (authority with no `DomainDoc` behind it, so
|
|
131
|
+
nothing is served for it at all). It must not revoke: a resolver blip at boot would otherwise take every
|
|
132
|
+
zone off the air, which is the outage this path exists to avoid.
|
|
133
|
+
|
|
134
|
+
### Deploy sequence — seeding is mandatory, not advisable
|
|
135
|
+
|
|
136
|
+
Startup ordering is already correct: the service chain is `DcRouterDb` →
|
|
137
|
+
`DnsManager` → `SmartProxy` → `DnsServer`, and `DnsAuthorityManager.start()`
|
|
138
|
+
(which loads the stored verified zones) runs inside `DnsManager`'s `withStart`,
|
|
139
|
+
strictly before `dnsServerRuntime.setup()` reads the set or registers any
|
|
140
|
+
handler. So the stored set is applied on the **first** registration pass —
|
|
141
|
+
restart itself introduces no reconciliation gap.
|
|
142
|
+
|
|
143
|
+
**Seed before the first restart on this trio.** With `dnsScopes` gone there is no
|
|
144
|
+
floor left to fall back on: a zone that is not in `DnsAuthorityDoc` when the new
|
|
145
|
+
build starts is REFUSED outright, and that now includes the zone that used to be
|
|
146
|
+
declared in `dnsScopes` and was the only healthy one. Insert the singleton
|
|
147
|
+
`DnsAuthorityDoc` (`settingsId: 'dns-authority-settings'`) with one
|
|
148
|
+
`{ zone, origin: 'verified', verifiedAt, observedNameservers, verifiedBy }`
|
|
149
|
+
entry per delegated zone *before* starting the new build. The first startup then
|
|
150
|
+
has proof already, and the startup drift audit re-probes each entry and reports
|
|
151
|
+
any that no longer hold, so a wrong seed is self-correcting rather than silently
|
|
152
|
+
trusted.
|
|
153
|
+
|
|
154
|
+
The production database is an embedded single-writer `LocalSmartDb` reached over
|
|
155
|
+
a Unix socket in `os.tmpdir()`, so it is not reachable off-host and the seed must
|
|
156
|
+
run on the dcrouter host with dcrouter stopped: stop → seed → start the new
|
|
157
|
+
build. `.nogit/debug/seed-dns-authority.ts` does this; it probes every zone over
|
|
158
|
+
DoH first and writes nothing on an unproven zone, and it is re-runnable (it
|
|
159
|
+
replaces the verified set with what it just proved, so dropping a zone from the
|
|
160
|
+
list drops it from authority).
|
|
161
|
+
|
|
162
|
+
That script lives under `.nogit/` and is therefore **not committed** — it is a
|
|
163
|
+
one-off operational tool for this specific upgrade, not product code, and it is
|
|
164
|
+
deliberately not a smartmigration step (see the next section for why). So it
|
|
165
|
+
cannot be the only record of the step: the changelog spells out the document
|
|
166
|
+
shape (`settingsId: 'dns-authority-settings'`, one
|
|
167
|
+
`{ zone, origin: 'verified', verifiedAt, observedNameservers, verifiedBy }` per
|
|
168
|
+
zone) inline, and the same result is reachable through `verifyDnsAuthorityZone`
|
|
169
|
+
after startup with no restart. The script is a convenience, not a dependency.
|
|
170
|
+
|
|
171
|
+
**If a zone was missed** — claim it after startup, no restart needed. Order:
|
|
172
|
+
|
|
173
|
+
1. Confirm the manager loaded: `DnsAuthorityManager: N delegation-verified zone(s) loaded`.
|
|
174
|
+
An empty set logs at `error` instead, naming the remediation.
|
|
175
|
+
2. Read the startup drift audit. Every `delegated-but-unclaimed` line is a zone
|
|
176
|
+
delegated to us that we are refusing to serve — that is the worklist.
|
|
177
|
+
3. Per zone, `probeDnsAuthorityZone` first (read-only, no mutation), then
|
|
178
|
+
`verifyDnsAuthorityZone` only if the verdict is `delegated`.
|
|
179
|
+
4. `getDnsAuthority` — expect one `verified` entry per claimed zone, each
|
|
180
|
+
carrying `observedNameservers`.
|
|
181
|
+
5. `getDnsAuthorityDrift` — expect `[]`.
|
|
182
|
+
6. Query each claimed zone's apex NS directly against our nameserver and expect an
|
|
183
|
+
authoritative answer naming `dnsNsDomains`. Query `AAAA` and `SOA` too, not
|
|
184
|
+
just `A`: a zone missing from `authoritativeZones` answers `A` and REFUSES the
|
|
185
|
+
rest, which is the defect this work fixes and the only query pattern that
|
|
186
|
+
distinguishes it.
|
|
187
|
+
7. `getMergedRoutes` — expect no `unverified-domain-ownership` warnings.
|
|
188
|
+
8. `getCertificateOverview` — expect no entry with `servedMismatch: true`.
|
|
189
|
+
|
|
190
|
+
Healthy is: drift empty, every claimed zone answering `aa` for its apex NS, no
|
|
191
|
+
ownership route warnings, no served/stored certificate mismatch.
|
|
192
|
+
|
|
193
|
+
A `verdict: 'undeterminable'` means the host could not reach a public DoH
|
|
194
|
+
resolver. That is an egress problem to fix, not something to retry blindly — and
|
|
195
|
+
the zone stays unserved until it is resolved, which is the correct fail-closed
|
|
196
|
+
behaviour.
|
|
197
|
+
|
|
198
|
+
### `authoritativeZones` is reconciled on a running server
|
|
199
|
+
|
|
200
|
+
An earlier revision recorded this as a known limitation: smartdns takes
|
|
201
|
+
`authoritativeZones` in the `DnsServer` constructor, so a zone verified at
|
|
202
|
+
runtime did not join it until the next restart, and it also meant the `DnsServer`
|
|
203
|
+
service only registered when bootstrap `dnsScopes` was non-empty — a
|
|
204
|
+
verified-only configuration never started the DNS server at all. Both are fixed.
|
|
205
|
+
|
|
206
|
+
It was not a cosmetic limitation. **`authoritativeZones` decides the response
|
|
207
|
+
kind, and it is the entire production defect.** smartdns answers a name inside a
|
|
208
|
+
configured zone and REFUSES a name outside every configured zone — *even when a
|
|
209
|
+
handler is registered and answers other qtypes for that exact name*. In
|
|
210
|
+
production that produced, from `212.95.99.130`:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
social.io A=NOERROR (aa) AAAA=REFUSED SOA=REFUSED
|
|
214
|
+
hard.global A=NOERROR (aa) AAAA=REFUSED SOA=REFUSED
|
|
215
|
+
central.eu A=NOERROR (aa) AAAA=NOERROR SOA=NOERROR <- the only declared zone
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Public recursives turn the REFUSED arm into SERVFAIL — `AAAA social.io` SERVFAILs
|
|
219
|
+
on 1.1.1.1, 8.8.8.8 and 9.9.9.9 — and `curl` does a dual A+AAAA lookup and blocks
|
|
220
|
+
on it, so dual-stack clients see a degraded site. Registering handlers for a
|
|
221
|
+
verified zone without moving this set would have reproduced the identical defect
|
|
222
|
+
for every runtime-verified zone.
|
|
223
|
+
|
|
224
|
+
**How it is reconciled without a restart.** `DnsServerRuntime` owns the array
|
|
225
|
+
instance it passes as `authoritativeZones`. smartdns keeps the options object by
|
|
226
|
+
reference and re-reads the array on every query, so `syncAuthorityZones()`
|
|
227
|
+
mutates it in place and a live server changes its authority set. The alternative
|
|
228
|
+
is re-creating the `DnsServer`, i.e. dropping UDP/TCP 53 — the outage this path
|
|
229
|
+
exists to remove. `assertLiveAuthorityZones()` checks the reference is still
|
|
230
|
+
shared right after construction and logs at `error` if it is not, so a future
|
|
231
|
+
smartdns that copies the array fails loudly instead of silently freezing
|
|
232
|
+
authority at boot; a test asserts the identity against the installed smartdns.
|
|
233
|
+
|
|
234
|
+
`dnssecZone` cannot be reconciled the same way — it is baked into the Rust config
|
|
235
|
+
at start — so it is taken from the boot-time set, which is **sorted** and
|
|
236
|
+
therefore stable across restarts rather than dependent on database insertion
|
|
237
|
+
order. When the set is empty it becomes `no-authority.invalid`: smartdns falls
|
|
238
|
+
back to `[dnssecZone]` when `authoritativeZones` is empty, and `.invalid` is
|
|
239
|
+
reserved by RFC 2606, so an unseeded database provably cannot claim a real name.
|
|
240
|
+
|
|
241
|
+
### Why removing `dnsScopes` needs no smartmigration step
|
|
242
|
+
|
|
243
|
+
`AGENTS.md` requires DB schema migrations to live exclusively in
|
|
244
|
+
`ts_migrations/index.ts`. This change adds none, deliberately, and the reason is
|
|
245
|
+
worth stating so its absence does not read as an oversight:
|
|
246
|
+
|
|
247
|
+
- **Nothing in the database changes shape.** `DnsAuthorityDoc` is untouched. The
|
|
248
|
+
only type change is narrowing `TDnsAuthorityZoneOrigin` from
|
|
249
|
+
`'bootstrap' | 'verified'` to `'verified'`, and no stored document ever
|
|
250
|
+
carried `'bootstrap'` — `persist()` only ever wrote `verifiedZones`, and
|
|
251
|
+
`verifyZone()` is the only thing that builds an entry. The bootstrap entries
|
|
252
|
+
existed solely as a read-time projection inside `getSettings()`.
|
|
253
|
+
Stronger still: `DnsAuthorityDoc` was introduced in `0d82361`, after the newest
|
|
254
|
+
tag, so no released build has ever created the collection.
|
|
255
|
+
- **Seeding from `dnsScopes` would forge the proof.** A migration step *could*
|
|
256
|
+
read it — `createMigrationRunner` runs in-process at startup and already takes
|
|
257
|
+
deployment-derived seeds — but a `dnsScopes` entry carries no delegation
|
|
258
|
+
evidence. Persisting one as `origin: 'verified'` would manufacture exactly the
|
|
259
|
+
proof this change makes unforgeable, and would reintroduce self-asserted
|
|
260
|
+
authority through the back door.
|
|
261
|
+
|
|
262
|
+
The other candidate — derive authority by probing every dcrouter-hosted
|
|
263
|
+
`DomainDoc` at upgrade time — was rejected because its outcome would depend on
|
|
264
|
+
whether a public DoH resolver happened to be reachable at that moment, so the
|
|
265
|
+
same upgrade would claim different zones on different runs, and a migration does
|
|
266
|
+
not re-run to correct itself. Nondeterministic authority is worse than no
|
|
267
|
+
automation.
|
|
268
|
+
|
|
269
|
+
The upgrade path is instead **seed, then start** (above), backed by three
|
|
270
|
+
independent loud signals if it is skipped: `DnsAuthorityManager.start()` logs at
|
|
271
|
+
`error` on an empty set, `DnsServerRuntime.setup()` logs at `error`, and the
|
|
272
|
+
startup drift audit enumerates precisely the zones to claim. Every one of them is
|
|
273
|
+
remediable through `dns-authority:write` without a restart, which is the property
|
|
274
|
+
that makes a missed seed recoverable rather than an outage.
|
|
275
|
+
|
|
276
|
+
### Sequenced behind an unreleased smartdns
|
|
277
|
+
|
|
278
|
+
Three items cannot be written against the installed smartdns (7.12.1) and must
|
|
279
|
+
land in the same change that takes the bump. All are recorded at their code
|
|
280
|
+
sites so they cannot be lost.
|
|
281
|
+
|
|
282
|
+
1. **The private-route overlay must declare itself non-authoritative.** It
|
|
283
|
+
registers `*` for `A` (`registerPrivateRouteHandler`). The hardened smartdns
|
|
284
|
+
authority model *suppresses* a default-`authoritative` handler for any name
|
|
285
|
+
outside every configured zone, so overlay hostnames proven by `provider-zone`
|
|
286
|
+
— owned, but not delegation-verified, therefore not in `authoritativeZones` —
|
|
287
|
+
would silently stop being answered. It needs
|
|
288
|
+
`{ authority: 'non-authoritative', owner: 'private-route-overlay' }`, which
|
|
289
|
+
is a fourth `registerHandler` parameter that does not exist yet.
|
|
290
|
+
|
|
291
|
+
2. **A supported way to re-zone a running server.** `DnsServerRuntime` currently
|
|
292
|
+
keeps the array it passed as `authoritativeZones` and mutates it in place,
|
|
293
|
+
then verifies through a cast that smartdns still holds the same reference.
|
|
294
|
+
That is a consumer-side shim on a dependency's storage behaviour, and it is
|
|
295
|
+
named as one: `authoritativeZones` is a public option, but "the array is kept
|
|
296
|
+
by reference and re-read per query" is not a documented contract. It is used
|
|
297
|
+
because the alternative — re-creating the `DnsServer` to change its zones —
|
|
298
|
+
drops UDP/TCP 53, which is the outage the whole path exists to remove, and
|
|
299
|
+
because a `setAuthoritativeZones()` cannot be added without releasing
|
|
300
|
+
smartdns first. Guarded two ways in the meantime:
|
|
301
|
+
`assertLiveAuthorityZones()` logs at `error` at startup if the reference is
|
|
302
|
+
no longer shared, and a test asserts the identity against the installed
|
|
303
|
+
smartdns so a copying release fails the suite rather than production. Replace
|
|
304
|
+
both with the supported setter when it ships.
|
|
305
|
+
|
|
306
|
+
3. **Per-registration teardown.** `unregisterHandler(pattern, types)` is coarse:
|
|
307
|
+
two owners on the identical pattern and type cannot be separated. Nothing
|
|
308
|
+
does that today — `DnsManager` is the single owner of apex NS, and
|
|
309
|
+
`DnsServerRuntime`'s glue records use different patterns — but the registry
|
|
310
|
+
in `DnsManager` exists to work around exactly this. `unregisterHandlerById` /
|
|
311
|
+
`unregisterHandlersByOwner` and the handle returned by `registerHandler` are
|
|
312
|
+
the released fix.
|
|
313
|
+
|
|
314
|
+
### Generated apex NS has exactly one owner
|
|
315
|
+
|
|
316
|
+
`DnsServerRuntime` used to emit a static apex NS set per `dnsScopes` entry, and
|
|
317
|
+
`DnsManager` skipped those zones to avoid duplicates. Under database-sourced
|
|
318
|
+
authority that is wrong in both directions — a zone verified after startup would
|
|
319
|
+
never get them, and a zone whose authority was revoked would keep them until an
|
|
320
|
+
unrelated restart. `DnsManager.registerAuthoritativeZoneDefaults()` is now the
|
|
321
|
+
only source, for every authoritative zone, reconciled in-process both ways.
|
|
322
|
+
|
|
323
|
+
Consequence worth knowing: generated apex NS hangs off a dcrouter-hosted
|
|
324
|
+
`DomainDoc`, so a verified zone without one serves nothing at all. That is
|
|
325
|
+
reported as `verified-but-unhosted` drift, because it is otherwise invisible —
|
|
326
|
+
such a zone neither REFUSES nor answers.
|
|
327
|
+
|
|
328
|
+
## Domain ownership gates certificates and authoritative DNS (2026-07-25)
|
|
329
|
+
|
|
330
|
+
`ts/dns/domain-ownership.ts` is the single predicate. Ownership is proven by
|
|
331
|
+
exactly two things, neither forgeable through the ops API:
|
|
332
|
+
|
|
333
|
+
- `provider-zone` — a `DomainDoc` with `source === 'provider'` and a `providerId`.
|
|
334
|
+
It can only get there via `importDomainsFromProvider()`, which requires the zone
|
|
335
|
+
to be listed by a credentialed provider account.
|
|
336
|
+
- `delegation-verified-zone` — the FQDN is covered by a zone in the DNS authority
|
|
337
|
+
set (exact or a subzone). A zone only enters that set by having its public NS
|
|
338
|
+
records observed naming our nameservers, which an ops-API caller cannot
|
|
339
|
+
arrange. This replaced `configured-dns-scope`, which read `options.dnsScopes`:
|
|
340
|
+
deployment configuration was a real trust boundary, but the price was a second,
|
|
341
|
+
un-reconcilable representation of DNS authority.
|
|
342
|
+
|
|
343
|
+
Everything else — including a `source === 'dcrouter'` `DomainDoc` outside the
|
|
344
|
+
authority set — is operator self-assertion and fails closed.
|
|
345
|
+
|
|
346
|
+
**`DomainDoc.authoritative` has been recording intent, not fact.** In production
|
|
347
|
+
all four of `gated.one`, `hard.global`, `shoppinglist.app` and `social.io` carry
|
|
348
|
+
`authoritative: true` with `providerId: no` — a flag nobody ever proved, written
|
|
349
|
+
unconditionally by the old `createDcrouterDomain`. Do not read it as evidence of
|
|
350
|
+
anything. The predicate ignores it and derives authority from proof, and
|
|
351
|
+
`DnsManager.syncAuthoritativeFlags()` now rewrites the stored flag to match the
|
|
352
|
+
current verdict whenever the authority set changes, so the column converges on
|
|
353
|
+
the truth instead of preserving a stale claim.
|
|
354
|
+
|
|
355
|
+
**Known gap — no persisted lifecycle state.** `DomainDoc.source` is only
|
|
356
|
+
`dcrouter | provider` and `updateDomain` can change only `description`, so
|
|
357
|
+
"we intend to manage this later" still cannot be represented distinctly from
|
|
358
|
+
"we serve this authoritatively now"; the invariants above are enforced by
|
|
359
|
+
recomputing ownership at every decision point instead. The recommended target is
|
|
360
|
+
a four-state machine — `pending`/`unverified`, `active-provider`,
|
|
361
|
+
`active-dcrouter`, `suspended`/`revoked` — with a generation-fenced
|
|
362
|
+
`verifyDomain`/`activateDomain` that proves delegation against the configured
|
|
363
|
+
public nameserver identities, `createDomain` entering `pending`, and
|
|
364
|
+
`deactivateDomain` tearing down runtime state. Two properties to preserve when it
|
|
365
|
+
lands: in `active-provider`, `authoritative` stays **false** and loss of provider
|
|
366
|
+
proof must **suspend** writes rather than fall back to local authority; and
|
|
367
|
+
synthetic NS/SOA is installed only after activation, never on intent. That work
|
|
368
|
+
needs a persisted status field, a release-version-matched `ts_migrations` step,
|
|
369
|
+
new ops API methods, and UI — it is a feature program, not a defect fix.
|
|
370
|
+
|
|
371
|
+
Why this matters: **smartdns marks any answer a registered handler produces as
|
|
372
|
+
authoritative (`aa`) regardless of `authoritativeZones`.** See
|
|
373
|
+
`classes.dnsserver.js`: a handler hit sends `'answer'`, a miss inside an
|
|
374
|
+
authoritative zone sends `'authoritativeNegative'`, and a miss outside sends
|
|
375
|
+
`'refused'`. So registering *any* handler for a zone is a public claim of
|
|
376
|
+
authority over it, and dcrouter listens on a public UDP/TCP 53.
|
|
377
|
+
|
|
378
|
+
Enforcement points:
|
|
379
|
+
|
|
380
|
+
- `RouteConfigManager.createRoute` / `updateRoute` — refuse an enabled route whose
|
|
381
|
+
`action.tls.certificate === 'auto'` covers an unverified hostname. Disabling is
|
|
382
|
+
never gated. Already-stored routes are **surfaced** as
|
|
383
|
+
`unverified-domain-ownership` route warnings plus an `error` log, not refused,
|
|
384
|
+
because refusing at startup would take down routes that are serving fine.
|
|
385
|
+
- `DnsManager.registerAuthoritativeZoneDefaults` — refuse the generated apex NS
|
|
386
|
+
handler for an unverified zone; `createDcrouterDomain` / `migrateToDcrouter`
|
|
387
|
+
derive `authoritative` from the verdict instead of hard-coding `true`.
|
|
388
|
+
- `DnsServerRuntime.syncPrivateRouteOverrides` — the private-route A overlay is
|
|
389
|
+
"internal" by intent only, never by mechanism. Ungated, one route was enough to
|
|
390
|
+
make dcrouter publicly hand out an RFC1918 address, authoritatively, for a
|
|
391
|
+
third party's domain.
|
|
392
|
+
- `certProvisionFunction` and `reprovisionCertificateDomain` — refuse before
|
|
393
|
+
starting an ACME order that cannot succeed.
|
|
394
|
+
|
|
395
|
+
## ACME retry budgets are three independent mechanisms (2026-07-25)
|
|
396
|
+
|
|
397
|
+
Do not assume they share semantics, caps, or re-arm behaviour:
|
|
398
|
+
|
|
399
|
+
| | `SmartAcmeLifecycle` | `CertProvisionScheduler` | SmartProxy `certFailureAt` |
|
|
400
|
+
|---|---|---|---|
|
|
401
|
+
| scope | provider startup | per domain | per domain |
|
|
402
|
+
| increments on | failed `smartAcme.start()` | `certProvisionFunction` throw | `certProvisionFunction` throw |
|
|
403
|
+
| backoff | 5 s→1 h, ±20% jitter | `min(failures², 24 h)` | flat 30 min |
|
|
404
|
+
| cap | 20 attempts, then gives up | **none** — grows forever | n/a |
|
|
405
|
+
| persisted | no | yes (`CertBackoffDoc`) | no (in-memory, private) |
|
|
406
|
+
| re-armed by | `startInBackground()` only: a SmartProxy rebuild, `rearm()`, or a process restart. **Never on a timer.** | time | time, or a successful provision it cannot reach |
|
|
407
|
+
|
|
408
|
+
`classifyAcmeFailure()` (`ts/acme/acme-failure-classification.ts`) decides which
|
|
409
|
+
causes may enter a budget at all. A configuration cause (no managed domain, no
|
|
410
|
+
provider zone, unverified ownership, bad ACME account, CAA) is terminal: it is
|
|
411
|
+
raised as `AcmePermanentFailureError` / `DomainOwnershipError` and never consumes
|
|
412
|
+
a budget. Unrecognised causes stay **transient** on purpose — guessing "permanent"
|
|
413
|
+
would strand domains a retry would have fixed.
|
|
414
|
+
|
|
3
415
|
## smartmta Migration (2026-02-11)
|
|
4
416
|
|
|
5
417
|
### Overview
|
package/readme.md
CHANGED
|
@@ -25,7 +25,7 @@ Highlights:
|
|
|
25
25
|
| --- | --- |
|
|
26
26
|
| Proxying | SmartProxy routes for HTTP, HTTPS, TCP, SNI, TLS termination, passthrough, backend forwarding, source policies, rate limits, and browser challenges |
|
|
27
27
|
| Route ownership | Constructor routes, generated email/DNS routes, and API-created routes with explicit origins |
|
|
28
|
-
| DNS |
|
|
28
|
+
| DNS | Delegation-verified authoritative zones, generated NS records, static DNS records, provider-backed domains, and DoH endpoints |
|
|
29
29
|
| Email | UnifiedEmailServer startup, email-domain management, route-backed delivery actions, received mail operations, managed app address bindings, and outbound SMTP submission identities |
|
|
30
30
|
| Certificates | ACME config, managed-domain DNS-01 challenges, HTTP-01 fallback, stored certificate metadata, provisioning backoff, and certificate status reporting |
|
|
31
31
|
| Edge access | Remote ingress hub, edge registrations, derived edge ports, pushed firewall rules, VPN-only route access |
|
|
@@ -116,8 +116,7 @@ Bootstrap behavior:
|
|
|
116
116
|
| `emailOutboundMode` | Outbound SMTP mode. Defaults to `direct`; `remoteIngress` routes outbound SMTP through a RemoteIngress egress edge. |
|
|
117
117
|
| `emailPortConfig` | External-to-internal email port mapping and received-email storage path. |
|
|
118
118
|
| `tls` | Legacy/static TLS and ACME contact settings used to seed certificate config. |
|
|
119
|
-
| `dnsNsDomains` | Nameserver hostnames used for generated NS records and DoH routes. |
|
|
120
|
-
| `dnsScopes` | Authoritative domains served by the embedded DNS server. |
|
|
119
|
+
| `dnsNsDomains` | Nameserver hostnames used for generated NS records and DoH routes. A zone becomes authoritative by having its public NS records observed naming one of these. |
|
|
121
120
|
| `dnsRecords` | Constructor-defined DNS records. |
|
|
122
121
|
| `publicIp` / `proxyIps` | IPs used for generated A records and proxy-aware DNS exposure. |
|
|
123
122
|
| `dbConfig` | Smartdata persistence via embedded LocalSmartDb or external MongoDB. |
|
|
@@ -130,7 +129,7 @@ Bootstrap behavior:
|
|
|
130
129
|
Important runtime behavior:
|
|
131
130
|
|
|
132
131
|
- `dbConfig.enabled` defaults to enabled. Without `mongoDbUrl`, dcrouter uses embedded LocalSmartDb.
|
|
133
|
-
- If the DB is disabled, constructor-defined proxy traffic can still run, but persistent API routes, tokens, managed domains, and stored certificate state are unavailable.
|
|
132
|
+
- If the DB is disabled, constructor-defined proxy traffic can still run, but persistent API routes, tokens, managed domains, and stored certificate state are unavailable. The embedded DNS server is also skipped entirely, because DNS authority is delegation-verified database state — the DoH routes are still generated, but nothing answers behind them.
|
|
134
133
|
- Qualifying HTTPS forward routes on port `443` are HTTP/3-augmented unless `http3.enabled === false` or the route opts out.
|
|
135
134
|
- DNS-over-HTTPS routes are generated on the first `dnsNsDomains` entry at `/dns-query` and `/resolve`.
|
|
136
135
|
- Email listener ports can be remapped internally, for example public `25`, `587`, and `465` to unprivileged internal ports.
|
|
@@ -176,6 +175,47 @@ dcrouter keeps generated and operator-created routes separate so automation can
|
|
|
176
175
|
|
|
177
176
|
System routes are persisted with stable `systemKey` values. Ordinary operator-created API routes are editable through generic route CRUD. Routes carrying managed ownership metadata, including Special Forwards and gateway-client routes, reject generic structural updates and deletion so their owning workflow keeps canonical match, priority, source-policy, and ownership fields intact.
|
|
178
177
|
|
|
178
|
+
## DNS Authority
|
|
179
|
+
|
|
180
|
+
Which zones the embedded DNS server may answer for authoritatively is database
|
|
181
|
+
state, not deployment configuration. There is no `dnsScopes` option.
|
|
182
|
+
|
|
183
|
+
A zone enters the authority set exactly one way: its **public delegation must
|
|
184
|
+
name one of `dnsNsDomains`**, observed through a single DNS-over-HTTPS lookup
|
|
185
|
+
against a public resolver. The system resolver is never used for this — on a
|
|
186
|
+
dcrouter host it may be dcrouter itself, which would answer with the very NS
|
|
187
|
+
records dcrouter generated and make the proof self-confirming. An ops-API caller
|
|
188
|
+
cannot repoint somebody else's delegation, so writing the record is not the same
|
|
189
|
+
as manufacturing the proof.
|
|
190
|
+
|
|
191
|
+
| Operation | Scope | Effect |
|
|
192
|
+
| --- | --- | --- |
|
|
193
|
+
| `getDnsAuthority` | `dns-authority:read` | The authority set, its evidence, and whether it was readable. |
|
|
194
|
+
| `probeDnsAuthorityZone` | `dns-authority:read` | Read-only delegation probe. Never mutates. |
|
|
195
|
+
| `verifyDnsAuthorityZone` | `dns-authority:write` | Claims a zone, only against a `delegated` verdict. |
|
|
196
|
+
| `revokeDnsAuthorityZone` | `dns-authority:write` | Drops a zone. Every zone is revocable. |
|
|
197
|
+
| `getDnsAuthorityDrift` | `dns-authority:read` | Advisory comparison of claimed authority against reality. |
|
|
198
|
+
|
|
199
|
+
Writes accept an admin identity or an API token carrying `dns-authority:write`;
|
|
200
|
+
a non-admin identity is refused. Probes return three verdicts, never two: `delegated`, `not-delegated`, and
|
|
201
|
+
`undeterminable`. A timeout is not evidence that a zone is not ours, so a
|
|
202
|
+
mutation refuses on `undeterminable` rather than assuming either way.
|
|
203
|
+
|
|
204
|
+
Claiming or revoking a zone takes effect in-process, with no restart: it moves
|
|
205
|
+
the running DNS server's authoritative zone set, its generated apex NS records,
|
|
206
|
+
route certificate warnings, and the private-route overlay together.
|
|
207
|
+
|
|
208
|
+
**Cold start.** A database with no authority document is a legitimate state, and
|
|
209
|
+
it means dcrouter is authoritative for nothing and REFUSES every query. The DNS
|
|
210
|
+
server still starts — so DoH keeps serving and a zone verified a moment later
|
|
211
|
+
takes effect immediately — and the condition is logged at `error` alongside a
|
|
212
|
+
startup drift audit listing every dcrouter-hosted zone delegated to us that is
|
|
213
|
+
not being served.
|
|
214
|
+
An authority document that cannot be *read* is treated differently: the set is
|
|
215
|
+
unknown rather than empty, so the DNS services fail and retry instead of quietly
|
|
216
|
+
revoking every zone. dcrouter itself still comes up — both services are optional
|
|
217
|
+
— so the rest of the router keeps running while DNS stays deliberately down.
|
|
218
|
+
|
|
179
219
|
## Route Source Bindings
|
|
180
220
|
|
|
181
221
|
API-created route records pass ordered `metadata.sourceBindings[]` alongside the SmartProxy route config to express source and path policy variants without duplicating whole routes by hand. Each binding points at a source profile id through `sourceProfileRef`. Dashboard presets resolve seeded profile names to ids before saving.
|
|
@@ -361,7 +401,9 @@ const router = new DcRouter({
|
|
|
361
401
|
},
|
|
362
402
|
emailOutboundMode: 'remoteIngress',
|
|
363
403
|
dnsNsDomains: ['ns1.example.com', 'ns2.example.com'],
|
|
364
|
-
|
|
404
|
+
// Which zones the embedded DNS server answers for is not configured here.
|
|
405
|
+
// A zone earns authority by its public delegation naming dnsNsDomains, and
|
|
406
|
+
// that proof lives in the database — see the "DNS Authority" section above.
|
|
365
407
|
publicIp: '203.0.113.10',
|
|
366
408
|
remoteIngressConfig: {
|
|
367
409
|
enabled: true,
|
|
@@ -473,7 +515,7 @@ Supported environment overrides include:
|
|
|
473
515
|
| `DCROUTER_BASE_DIR` | Runtime data root. |
|
|
474
516
|
| `DCROUTER_TLS_EMAIL` / `DCROUTER_TLS_DOMAIN` | TLS/ACME seed settings. |
|
|
475
517
|
| `DCROUTER_PUBLIC_IP` / `DCROUTER_PROXY_IPS` | Public/proxy IP exposure settings. |
|
|
476
|
-
| `DCROUTER_DNS_NS_DOMAINS`
|
|
518
|
+
| `DCROUTER_DNS_NS_DOMAINS` | Nameserver hostnames. `DCROUTER_DNS_SCOPES` is no longer honored — DNS authority is delegation-verified database state, and a container still setting it is warned at startup. |
|
|
477
519
|
| `DCROUTER_EMAIL_HOSTNAME` / `DCROUTER_EMAIL_PORTS` | Email server seed settings. |
|
|
478
520
|
| `DCROUTER_CACHE_ENABLED` | Enables or disables DB-backed persistence. |
|
|
479
521
|
| `DCROUTER_MAX_CONNECTIONS`, `DCROUTER_MAX_CONNECTIONS_PER_IP`, `DCROUTER_CONNECTION_RATE_LIMIT` | SmartProxy capacity and rate-limit overrides. |
|