@serve.zone/dcrouter 15.11.0 → 16.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 +34 -34
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/classes.dcrouter.d.ts +10 -3
- package/dist_ts/classes.dcrouter.js +39 -43
- package/dist_ts/db/documents/classes.cached.email.d.ts +32 -0
- package/dist_ts/db/documents/classes.cached.email.js +67 -3
- package/dist_ts/db/documents/classes.email-domain.doc.d.ts +2 -1
- package/dist_ts/db/documents/classes.email-domain.doc.js +8 -2
- package/dist_ts/db/documents/classes.email-server-settings.doc.js +2 -2
- package/dist_ts/db/documents/classes.remote-ingress-edge.doc.d.ts +3 -0
- package/dist_ts/db/documents/classes.remote-ingress-edge.doc.js +20 -2
- package/dist_ts/dns/classes.dns-server-runtime.js +3 -1
- package/dist_ts/email/classes.accepted-email-spool.d.ts +5 -1
- package/dist_ts/email/classes.accepted-email-spool.js +9 -1
- package/dist_ts/email/classes.email-domain.manager.d.ts +17 -1
- package/dist_ts/email/classes.email-domain.manager.js +100 -17
- package/dist_ts/email/classes.email-route-builder.js +5 -2
- package/dist_ts/email/classes.email-settings.manager.js +3 -3
- package/dist_ts/email/classes.mail-dns-sync.d.ts +27 -0
- package/dist_ts/email/classes.mail-dns-sync.js +196 -0
- package/dist_ts/email/classes.mail-egress-coordinator.d.ts +37 -0
- package/dist_ts/email/classes.mail-egress-coordinator.js +113 -0
- package/dist_ts/email/index.d.ts +2 -0
- package/dist_ts/email/index.js +3 -1
- package/dist_ts/opsserver/handlers/config.handler.js +4 -2
- package/dist_ts/opsserver/handlers/email-domain.handler.js +3 -1
- package/dist_ts/opsserver/handlers/email-ops.handler.d.ts +20 -3
- package/dist_ts/opsserver/handlers/email-ops.handler.js +141 -19
- package/dist_ts/opsserver/handlers/email-settings.handler.js +11 -6
- package/dist_ts/opsserver/handlers/remoteingress.handler.js +16 -7
- package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +33 -1
- package/dist_ts/remoteingress/classes.remoteingress-manager.js +94 -2
- package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +18 -0
- package/dist_ts/remoteingress/classes.tunnel-manager.js +114 -12
- package/dist_ts/remoteingress/edge-derivation-routes.d.ts +10 -0
- package/dist_ts/remoteingress/edge-derivation-routes.js +28 -0
- package/dist_ts/remoteingress/index.d.ts +1 -0
- package/dist_ts/remoteingress/index.js +2 -1
- package/dist_ts_interfaces/data/email-domain.d.ts +11 -1
- package/dist_ts_interfaces/data/email-settings.d.ts +11 -0
- package/dist_ts_interfaces/data/remoteingress.d.ts +10 -0
- package/dist_ts_interfaces/data/remoteingress.js +5 -2
- package/dist_ts_interfaces/requests/email-domains.d.ts +5 -1
- package/dist_ts_interfaces/requests/email-ops.d.ts +4 -0
- package/dist_ts_interfaces/requests/remoteingress.d.ts +11 -0
- package/dist_ts_migrations/index.js +91 -9
- package/dist_ts_web/00_commitinfo_data.js +2 -2
- package/package.json +4 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +41 -48
- package/ts/db/documents/classes.cached.email.ts +72 -1
- package/ts/db/documents/classes.email-domain.doc.ts +4 -0
- package/ts/db/documents/classes.email-server-settings.doc.ts +1 -1
- package/ts/db/documents/classes.remote-ingress-edge.doc.ts +9 -0
- package/ts/dns/classes.dns-server-runtime.ts +2 -0
- package/ts/email/classes.accepted-email-spool.ts +12 -1
- package/ts/email/classes.email-domain.manager.ts +118 -17
- package/ts/email/classes.email-route-builder.ts +4 -1
- package/ts/email/classes.email-settings.manager.ts +2 -2
- package/ts/email/classes.mail-dns-sync.ts +218 -0
- package/ts/email/classes.mail-egress-coordinator.ts +125 -0
- package/ts/email/index.ts +2 -0
- package/ts/opsserver/handlers/config.handler.ts +3 -1
- package/ts/opsserver/handlers/email-domain.handler.ts +2 -0
- package/ts/opsserver/handlers/email-ops.handler.ts +156 -18
- package/ts/opsserver/handlers/email-settings.handler.ts +10 -5
- package/ts/opsserver/handlers/remoteingress.handler.ts +15 -5
- package/ts/remoteingress/classes.remoteingress-manager.ts +111 -0
- package/ts/remoteingress/classes.tunnel-manager.ts +125 -10
- package/ts/remoteingress/edge-derivation-routes.ts +29 -0
- package/ts/remoteingress/index.ts +1 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
2
|
import { logger } from '../logger.js';
|
|
3
3
|
import type { IRemoteIngressPerformanceConfig, IRemoteIngressStatus } from '../../ts_interfaces/data/remoteingress.js';
|
|
4
|
+
import type { IEmailOutboundEgressStatus } from '../../ts_interfaces/data/email-settings.js';
|
|
4
5
|
import type { RemoteIngressManager } from './classes.remoteingress-manager.js';
|
|
5
6
|
|
|
6
7
|
export interface ITunnelManagerConfig {
|
|
@@ -15,8 +16,13 @@ export interface ITunnelManagerConfig {
|
|
|
15
16
|
|
|
16
17
|
export type TRemoteIngressEgressProxyRequest = Omit<plugins.remoteingress.IEgressTcpProxyRequest, 'edgeId'> & {
|
|
17
18
|
edgeId?: string;
|
|
19
|
+
/** Restrict selection to edges whose id or tags match any entry. */
|
|
20
|
+
edgeFilter?: string[];
|
|
18
21
|
};
|
|
19
22
|
|
|
23
|
+
/** Cooldown applied to an edge after a failed egress open before it is deprioritized. */
|
|
24
|
+
const egressFailureCooldownMs = 30_000;
|
|
25
|
+
|
|
20
26
|
/**
|
|
21
27
|
* Manages the RemoteIngressHub instance and tracks connected edge statuses.
|
|
22
28
|
*/
|
|
@@ -29,6 +35,9 @@ export class TunnelManager {
|
|
|
29
35
|
private syncChain: Promise<void> = Promise.resolve();
|
|
30
36
|
private reconcileChain: Promise<void> = Promise.resolve();
|
|
31
37
|
private stopped = true;
|
|
38
|
+
private egressProxyEdgeByProxyId: Map<string, string> = new Map();
|
|
39
|
+
private activeEgressProxiesByEdge: Map<string, number> = new Map();
|
|
40
|
+
private egressFailureCooldownUntil: Map<string, number> = new Map();
|
|
32
41
|
|
|
33
42
|
constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) {
|
|
34
43
|
this.manager = manager;
|
|
@@ -80,6 +89,10 @@ export class TunnelManager {
|
|
|
80
89
|
const message = `RemoteIngress egress ${data.outcome}: edge=${data.edgeId} proxy=${data.proxyId} target=${data.logicalHost}:${data.port}${data.resolvedIp ? ` resolvedIp=${data.resolvedIp}` : ''}${data.error ? ` error=${data.error}` : ''}`;
|
|
81
90
|
const isExpectedLifecycleEvent = data.outcome === 'connected' || data.outcome === 'closed';
|
|
82
91
|
logger.log(isExpectedLifecycleEvent ? 'info' : 'warn', message);
|
|
92
|
+
if (!isExpectedLifecycleEvent) {
|
|
93
|
+
// Connection-level failures steer subsequent selections to other edges.
|
|
94
|
+
this.egressFailureCooldownUntil.set(data.edgeId, Date.now() + egressFailureCooldownMs);
|
|
95
|
+
}
|
|
83
96
|
});
|
|
84
97
|
}
|
|
85
98
|
|
|
@@ -137,6 +150,9 @@ export class TunnelManager {
|
|
|
137
150
|
this.hub.removeAllListeners();
|
|
138
151
|
await this.hub.stop();
|
|
139
152
|
this.edgeStatuses.clear();
|
|
153
|
+
this.egressProxyEdgeByProxyId.clear();
|
|
154
|
+
this.activeEgressProxiesByEdge.clear();
|
|
155
|
+
this.egressFailureCooldownUntil.clear();
|
|
140
156
|
}
|
|
141
157
|
|
|
142
158
|
/**
|
|
@@ -212,31 +228,130 @@ export class TunnelManager {
|
|
|
212
228
|
throw new Error('RemoteIngress hub is not running');
|
|
213
229
|
}
|
|
214
230
|
const connectedEdges = Array.isArray(status?.connectedEdges) ? status.connectedEdges : [];
|
|
215
|
-
const
|
|
231
|
+
const candidates = this.selectEgressCandidates(connectedEdges, request);
|
|
232
|
+
|
|
233
|
+
if (candidates.length === 0) {
|
|
234
|
+
throw new Error(`No eligible RemoteIngress QUIC egress edge is connected${this.describeEgressScope(request)}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const { edgeId: _ignored, edgeFilter: _alsoIgnored, ...proxyRequest } = request;
|
|
238
|
+
let lastError: Error | undefined;
|
|
239
|
+
for (const candidate of candidates) {
|
|
240
|
+
try {
|
|
241
|
+
const proxy = await this.hub.startEgressTcpProxy({
|
|
242
|
+
...proxyRequest,
|
|
243
|
+
edgeId: candidate.edgeId,
|
|
244
|
+
});
|
|
245
|
+
this.egressProxyEdgeByProxyId.set(proxy.proxyId, candidate.edgeId);
|
|
246
|
+
this.activeEgressProxiesByEdge.set(
|
|
247
|
+
candidate.edgeId,
|
|
248
|
+
(this.activeEgressProxiesByEdge.get(candidate.edgeId) || 0) + 1,
|
|
249
|
+
);
|
|
250
|
+
return proxy;
|
|
251
|
+
} catch (error: unknown) {
|
|
252
|
+
lastError = error as Error;
|
|
253
|
+
this.egressFailureCooldownUntil.set(candidate.edgeId, Date.now() + egressFailureCooldownMs);
|
|
254
|
+
logger.log('warn', `RemoteIngress egress open failed on edge ${candidate.edgeId}, trying next candidate: ${lastError.message}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
throw new Error(
|
|
259
|
+
`All eligible RemoteIngress egress edges failed${this.describeEgressScope(request)}: ${lastError?.message || 'unknown error'}`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Eligibility (connected + egressEnabled + egressTcpV1 + native QUIC), scoped by
|
|
265
|
+
* edgeId/edgeFilter, capped by per-edge maxConcurrentStreams, ordered deterministically:
|
|
266
|
+
* not-in-cooldown first, then fewest active tunnels, then edgeId.
|
|
267
|
+
*/
|
|
268
|
+
private selectEgressCandidates(
|
|
269
|
+
connectedEdges: any[],
|
|
270
|
+
request: TRemoteIngressEgressProxyRequest,
|
|
271
|
+
): Array<{ edgeId: string; activeStreams: number }> {
|
|
272
|
+
const eligible = connectedEdges.filter((edge: any) => {
|
|
216
273
|
const hasEgressCapability = Array.isArray(edge.capabilities) && edge.capabilities.includes('egressTcpV1');
|
|
217
274
|
const usesQuic = edge.transportMode === 'quic'
|
|
218
275
|
|| (edge.transportMode === 'quicWithFallback' && edge.fallbackUsed === false);
|
|
219
276
|
return edge.egressEnabled === true && hasEgressCapability && usesQuic;
|
|
220
277
|
});
|
|
221
|
-
const selectedEdge = request.edgeId
|
|
222
|
-
? eligibleEdges.find((edge: any) => edge.edgeId === request.edgeId)
|
|
223
|
-
: eligibleEdges[0];
|
|
224
278
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
279
|
+
let scoped = eligible;
|
|
280
|
+
if (request.edgeId) {
|
|
281
|
+
scoped = eligible.filter((edge: any) => edge.edgeId === request.edgeId);
|
|
282
|
+
} else if (request.edgeFilter && request.edgeFilter.length > 0) {
|
|
283
|
+
const allowedIds = new Set(
|
|
284
|
+
this.manager.resolveEdgesByFilter(request.edgeFilter).map((edge) => edge.id),
|
|
285
|
+
);
|
|
286
|
+
scoped = eligible.filter((edge: any) => allowedIds.has(edge.edgeId));
|
|
228
287
|
}
|
|
229
288
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
289
|
+
const underCap = scoped.filter((edge: any) => {
|
|
290
|
+
const cap = this.manager.getEdge(edge.edgeId)?.egress?.maxConcurrentStreams;
|
|
291
|
+
if (!cap) return true;
|
|
292
|
+
return (this.activeEgressProxiesByEdge.get(edge.edgeId) || 0) < cap;
|
|
233
293
|
});
|
|
294
|
+
|
|
295
|
+
const now = Date.now();
|
|
296
|
+
const inCooldown = (edgeId: string) => (this.egressFailureCooldownUntil.get(edgeId) || 0) > now;
|
|
297
|
+
return underCap
|
|
298
|
+
.map((edge: any) => ({ edgeId: edge.edgeId as string, activeStreams: Number(edge.activeStreams) || 0 }))
|
|
299
|
+
.sort((a, b) => {
|
|
300
|
+
const cooldownDelta = Number(inCooldown(a.edgeId)) - Number(inCooldown(b.edgeId));
|
|
301
|
+
if (cooldownDelta !== 0) return cooldownDelta;
|
|
302
|
+
if (a.activeStreams !== b.activeStreams) return a.activeStreams - b.activeStreams;
|
|
303
|
+
return a.edgeId.localeCompare(b.edgeId);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private describeEgressScope(request: TRemoteIngressEgressProxyRequest): string {
|
|
308
|
+
if (request.edgeId) return ` (pinned edge: ${request.edgeId})`;
|
|
309
|
+
if (request.edgeFilter && request.edgeFilter.length > 0) return ` (filter: ${request.edgeFilter.join(', ')})`;
|
|
310
|
+
return '';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Readiness of the outbound egress path for a given edge scope —
|
|
315
|
+
* consumed by the ops API and the email-setup preflight.
|
|
316
|
+
*/
|
|
317
|
+
public getEgressReadiness(edgeFilter: string[]): IEmailOutboundEgressStatus {
|
|
318
|
+
const allowedIds = new Set(
|
|
319
|
+
this.manager.resolveEdgesByFilter(edgeFilter).map((edge) => edge.id),
|
|
320
|
+
);
|
|
321
|
+
let eligibleEdgeCount = 0;
|
|
322
|
+
for (const status of this.edgeStatuses.values()) {
|
|
323
|
+
if (!status.connected) continue;
|
|
324
|
+
if (!allowedIds.has(status.edgeId)) continue;
|
|
325
|
+
const hasEgressCapability = status.capabilities?.includes('egressTcpV1') ?? false;
|
|
326
|
+
const usesQuic = status.transportMode === 'quic'
|
|
327
|
+
|| (status.transportMode === 'quicWithFallback' && status.fallbackUsed === false);
|
|
328
|
+
if (status.egressEnabled === true && hasEgressCapability && usesQuic) {
|
|
329
|
+
eligibleEdgeCount++;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (eligibleEdgeCount > 0) {
|
|
333
|
+
return { ready: true, eligibleEdgeCount };
|
|
334
|
+
}
|
|
335
|
+
const reason = allowedIds.size === 0
|
|
336
|
+
? `no enabled edge matches filter [${edgeFilter.join(', ')}]`
|
|
337
|
+
: 'no connected matching edge with native QUIC egress capability';
|
|
338
|
+
return { ready: false, eligibleEdgeCount: 0, reason };
|
|
234
339
|
}
|
|
235
340
|
|
|
236
341
|
public async stopEgressTcpProxy(proxyId: string): Promise<boolean> {
|
|
237
342
|
if (this.stopped) {
|
|
238
343
|
return false;
|
|
239
344
|
}
|
|
345
|
+
const edgeId = this.egressProxyEdgeByProxyId.get(proxyId);
|
|
346
|
+
if (edgeId) {
|
|
347
|
+
this.egressProxyEdgeByProxyId.delete(proxyId);
|
|
348
|
+
const current = this.activeEgressProxiesByEdge.get(edgeId) || 0;
|
|
349
|
+
if (current <= 1) {
|
|
350
|
+
this.activeEgressProxiesByEdge.delete(edgeId);
|
|
351
|
+
} else {
|
|
352
|
+
this.activeEgressProxiesByEdge.set(edgeId, current - 1);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
240
355
|
return await this.hub.stopEgressTcpProxy(proxyId);
|
|
241
356
|
}
|
|
242
357
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
|
|
2
|
+
import { REMOTE_INGRESS_DNS_TAG } from '../../ts_interfaces/data/remoteingress.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Synthetic routes that exist ONLY for RemoteIngress edge port derivation and
|
|
6
|
+
* must never reach SmartProxy (smartdns owns host-side 53; a SmartProxy listener
|
|
7
|
+
* on 53 would collide). Forwards public TCP+UDP 53 on dns-tagged edges to the
|
|
8
|
+
* hub's authoritative DNS server.
|
|
9
|
+
*/
|
|
10
|
+
export function buildEdgeOnlyDerivationRoutes(options: { dnsNsDomains?: string[] }): IDcRouterRouteConfig[] {
|
|
11
|
+
if (!options.dnsNsDomains?.length) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
return [{
|
|
15
|
+
name: 'dns-53-edge-forwarding',
|
|
16
|
+
match: {
|
|
17
|
+
ports: [53],
|
|
18
|
+
transport: 'all',
|
|
19
|
+
},
|
|
20
|
+
action: {
|
|
21
|
+
type: 'forward',
|
|
22
|
+
targets: [{ host: '127.0.0.1', port: 53 }],
|
|
23
|
+
},
|
|
24
|
+
remoteIngress: {
|
|
25
|
+
enabled: true,
|
|
26
|
+
edgeFilter: [REMOTE_INGRESS_DNS_TAG],
|
|
27
|
+
},
|
|
28
|
+
} as IDcRouterRouteConfig];
|
|
29
|
+
}
|