@serve.zone/dcrouter 15.6.1 → 15.8.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 +423 -407
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +4 -1
- package/dist_ts/classes.dcrouter.js +45 -1
- package/dist_ts/db/documents/classes.email-server-settings.doc.d.ts +2 -1
- package/dist_ts/db/documents/classes.email-server-settings.doc.js +8 -2
- package/dist_ts/db/documents/classes.remote-ingress-edge.doc.d.ts +2 -1
- package/dist_ts/db/documents/classes.remote-ingress-edge.doc.js +8 -2
- package/dist_ts/email/classes.email-settings.manager.d.ts +5 -1
- package/dist_ts/email/classes.email-settings.manager.js +24 -1
- package/dist_ts/opsserver/handlers/config.handler.js +2 -1
- package/dist_ts/opsserver/handlers/email-settings.handler.js +2 -1
- package/dist_ts/opsserver/handlers/remoteingress.handler.js +3 -2
- package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +6 -2
- package/dist_ts/remoteingress/classes.remoteingress-manager.js +65 -2
- package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +6 -0
- package/dist_ts/remoteingress/classes.tunnel-manager.js +42 -1
- package/dist_ts_apiclient/classes.remoteingress.d.ts +10 -0
- package/dist_ts_apiclient/classes.remoteingress.js +23 -1
- package/dist_ts_interfaces/data/email-settings.d.ts +4 -0
- package/dist_ts_interfaces/data/remoteingress.d.ts +18 -0
- package/dist_ts_interfaces/requests/config.d.ts +1 -0
- package/dist_ts_interfaces/requests/remoteingress.d.ts +3 -1
- package/dist_ts_migrations/index.d.ts +1 -0
- package/dist_ts_migrations/index.js +80 -9
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate/remoteingress.d.ts +2 -0
- package/dist_ts_web/appstate/remoteingress.js +3 -1
- package/dist_ts_web/elements/email/ops-view-email-domains.js +7 -1
- package/dist_ts_web/elements/network/ops-view-remoteingress.d.ts +2 -0
- package/dist_ts_web/elements/network/ops-view-remoteingress.js +54 -2
- package/package.json +5 -5
- package/readme.md +22 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +56 -4
- package/ts/db/documents/classes.email-server-settings.doc.ts +4 -1
- package/ts/db/documents/classes.remote-ingress-edge.doc.ts +4 -1
- package/ts/email/classes.email-settings.manager.ts +29 -0
- package/ts/opsserver/handlers/config.handler.ts +1 -0
- package/ts/opsserver/handlers/email-settings.handler.ts +1 -0
- package/ts/opsserver/handlers/remoteingress.handler.ts +2 -0
- package/ts/remoteingress/classes.remoteingress-manager.ts +78 -3
- package/ts/remoteingress/classes.tunnel-manager.ts +61 -0
- package/ts_apiclient/classes.remoteingress.ts +26 -0
- package/ts_apiclient/readme.md +8 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate/remoteingress.ts +4 -0
- package/ts_web/elements/email/ops-view-email-domains.ts +6 -0
- package/ts_web/elements/network/ops-view-remoteingress.ts +55 -1
|
@@ -63,6 +63,7 @@ export class RemoteIngressHandler {
|
|
|
63
63
|
dataArg.tags,
|
|
64
64
|
dataArg.autoDerivePorts ?? true,
|
|
65
65
|
dataArg.performance,
|
|
66
|
+
dataArg.egress,
|
|
66
67
|
));
|
|
67
68
|
return { success: true, edge };
|
|
68
69
|
} catch (err: unknown) {
|
|
@@ -117,6 +118,7 @@ export class RemoteIngressHandler {
|
|
|
117
118
|
autoDerivePorts: dataArg.autoDerivePorts,
|
|
118
119
|
enabled: dataArg.enabled,
|
|
119
120
|
performance: dataArg.performance,
|
|
121
|
+
egress: dataArg.egress,
|
|
120
122
|
tags: dataArg.tags,
|
|
121
123
|
});
|
|
122
124
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
|
-
import type { IDcRouterRouteConfig, IRemoteIngress, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig, TRemoteIngressHubSettingsUpdate, TRemoteIngressPerformanceProfile } from '../../ts_interfaces/data/remoteingress.js';
|
|
2
|
+
import type { IDcRouterRouteConfig, IRemoteIngress, IRemoteIngressEgressPolicyConfig, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig, TRemoteIngressHubSettingsUpdate, TRemoteIngressPerformanceProfile } from '../../ts_interfaces/data/remoteingress.js';
|
|
3
3
|
import { RemoteIngressEdgeDoc, RemoteIngressHubSettingsDoc } from '../db/index.js';
|
|
4
4
|
|
|
5
5
|
export interface IRemoteIngressFirewallConfig {
|
|
@@ -30,6 +30,7 @@ const performanceIntegerMaxByField: Record<TPerformanceIntegerField, number> = {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
const maxServerFirstPorts = 128;
|
|
33
|
+
const maxRemoteIngressEgressListEntries = 128;
|
|
33
34
|
const defaultTunnelPort = 8443;
|
|
34
35
|
|
|
35
36
|
function hasOwn(objectArg: object, keyArg: string): boolean {
|
|
@@ -79,6 +80,7 @@ export class RemoteIngressManager {
|
|
|
79
80
|
enabled: doc.enabled,
|
|
80
81
|
autoDerivePorts: doc.autoDerivePorts,
|
|
81
82
|
performance: doc.performance,
|
|
83
|
+
egress: this.normalizeEgressPolicy(doc.egress),
|
|
82
84
|
tags: doc.tags,
|
|
83
85
|
createdAt: doc.createdAt,
|
|
84
86
|
updatedAt: doc.updatedAt,
|
|
@@ -274,6 +276,7 @@ export class RemoteIngressManager {
|
|
|
274
276
|
tags?: string[],
|
|
275
277
|
autoDerivePorts: boolean = true,
|
|
276
278
|
performance?: IRemoteIngressPerformanceConfig,
|
|
279
|
+
egress?: IRemoteIngressEgressPolicyConfig,
|
|
277
280
|
): Promise<IRemoteIngress> {
|
|
278
281
|
const id = plugins.uuid.v4();
|
|
279
282
|
const secret = plugins.crypto.randomBytes(32).toString('hex');
|
|
@@ -287,6 +290,7 @@ export class RemoteIngressManager {
|
|
|
287
290
|
enabled: true,
|
|
288
291
|
autoDerivePorts,
|
|
289
292
|
performance,
|
|
293
|
+
egress: this.normalizeEgressPolicy(egress),
|
|
290
294
|
tags: tags || [],
|
|
291
295
|
createdAt: now,
|
|
292
296
|
updatedAt: now,
|
|
@@ -324,6 +328,7 @@ export class RemoteIngressManager {
|
|
|
324
328
|
autoDerivePorts?: boolean;
|
|
325
329
|
enabled?: boolean;
|
|
326
330
|
performance?: IRemoteIngressPerformanceConfig;
|
|
331
|
+
egress?: IRemoteIngressEgressPolicyConfig;
|
|
327
332
|
tags?: string[];
|
|
328
333
|
},
|
|
329
334
|
): Promise<IRemoteIngress | null> {
|
|
@@ -337,6 +342,7 @@ export class RemoteIngressManager {
|
|
|
337
342
|
if (updates.autoDerivePorts !== undefined) edge.autoDerivePorts = updates.autoDerivePorts;
|
|
338
343
|
if (updates.enabled !== undefined) edge.enabled = updates.enabled;
|
|
339
344
|
if (updates.performance !== undefined) edge.performance = updates.performance;
|
|
345
|
+
if (updates.egress !== undefined) edge.egress = this.normalizeEgressPolicy(updates.egress);
|
|
340
346
|
if (updates.tags !== undefined) edge.tags = updates.tags;
|
|
341
347
|
edge.updatedAt = Date.now();
|
|
342
348
|
|
|
@@ -405,12 +411,13 @@ export class RemoteIngressManager {
|
|
|
405
411
|
* Get the list of allowed edges (enabled only) for the Rust hub.
|
|
406
412
|
* Includes listenPortsUdp when routes with transport 'udp' or 'all' are present.
|
|
407
413
|
*/
|
|
408
|
-
public getAllowedEdges(): Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig; performance?: IRemoteIngressPerformanceConfig }> {
|
|
409
|
-
const result: Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig; performance?: IRemoteIngressPerformanceConfig }> = [];
|
|
414
|
+
public getAllowedEdges(): Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig; performance?: IRemoteIngressPerformanceConfig; egress?: IRemoteIngressEgressPolicyConfig }> {
|
|
415
|
+
const result: Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig; performance?: IRemoteIngressPerformanceConfig; egress?: IRemoteIngressEgressPolicyConfig }> = [];
|
|
410
416
|
for (const edge of this.edges.values()) {
|
|
411
417
|
if (edge.enabled) {
|
|
412
418
|
const listenPortsUdp = this.getEffectiveListenPortsUdp(edge);
|
|
413
419
|
const performance = edge.performance && Object.keys(edge.performance).length > 0 ? edge.performance : undefined;
|
|
420
|
+
const egress = edge.egress?.enabled ? edge.egress : undefined;
|
|
414
421
|
result.push({
|
|
415
422
|
id: edge.id,
|
|
416
423
|
secret: edge.secret,
|
|
@@ -418,12 +425,80 @@ export class RemoteIngressManager {
|
|
|
418
425
|
...(listenPortsUdp.length > 0 ? { listenPortsUdp } : {}),
|
|
419
426
|
...(this.firewallConfig ? { firewallConfig: this.firewallConfig } : {}),
|
|
420
427
|
...(performance ? { performance } : {}),
|
|
428
|
+
...(egress ? { egress } : {}),
|
|
421
429
|
});
|
|
422
430
|
}
|
|
423
431
|
}
|
|
424
432
|
return result;
|
|
425
433
|
}
|
|
426
434
|
|
|
435
|
+
private normalizeEgressPolicy(
|
|
436
|
+
egress?: IRemoteIngressEgressPolicyConfig,
|
|
437
|
+
): IRemoteIngressEgressPolicyConfig | undefined {
|
|
438
|
+
if (!egress) {
|
|
439
|
+
return undefined;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const enabled = Boolean(egress.enabled);
|
|
443
|
+
const next: IRemoteIngressEgressPolicyConfig = { enabled };
|
|
444
|
+
if (!enabled) {
|
|
445
|
+
return next;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const allowedPorts = this.normalizeEgressAllowedPorts(egress.allowedPorts || [25]);
|
|
449
|
+
next.allowedPorts = allowedPorts;
|
|
450
|
+
|
|
451
|
+
if (egress.allowPrivateRanges !== undefined) {
|
|
452
|
+
next.allowPrivateRanges = Boolean(egress.allowPrivateRanges);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (egress.deniedCidrs !== undefined) {
|
|
456
|
+
if (!Array.isArray(egress.deniedCidrs) || egress.deniedCidrs.length > maxRemoteIngressEgressListEntries) {
|
|
457
|
+
throw new Error(`RemoteIngress egress deniedCidrs must contain at most ${maxRemoteIngressEgressListEntries} entries`);
|
|
458
|
+
}
|
|
459
|
+
const deniedCidrs = [...new Set(egress.deniedCidrs.map((cidr) => `${cidr}`.trim()).filter(Boolean))];
|
|
460
|
+
if (deniedCidrs.length > 0) {
|
|
461
|
+
next.deniedCidrs = deniedCidrs;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (egress.allowedHostPatterns !== undefined) {
|
|
466
|
+
if (!Array.isArray(egress.allowedHostPatterns) || egress.allowedHostPatterns.length > maxRemoteIngressEgressListEntries) {
|
|
467
|
+
throw new Error(`RemoteIngress egress allowedHostPatterns must contain at most ${maxRemoteIngressEgressListEntries} entries`);
|
|
468
|
+
}
|
|
469
|
+
const allowedHostPatterns = [...new Set(egress.allowedHostPatterns.map((pattern) => `${pattern}`.trim()).filter(Boolean))];
|
|
470
|
+
if (allowedHostPatterns.length > 0) {
|
|
471
|
+
next.allowedHostPatterns = allowedHostPatterns;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (egress.maxConcurrentStreams !== undefined) {
|
|
476
|
+
const maxConcurrentStreams = Number(egress.maxConcurrentStreams);
|
|
477
|
+
if (!Number.isSafeInteger(maxConcurrentStreams) || maxConcurrentStreams < 1 || maxConcurrentStreams > 100_000) {
|
|
478
|
+
throw new Error('RemoteIngress egress maxConcurrentStreams must be a positive safe integer no greater than 100000');
|
|
479
|
+
}
|
|
480
|
+
next.maxConcurrentStreams = maxConcurrentStreams;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return next;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private normalizeEgressAllowedPorts(ports: number[]): number[] {
|
|
487
|
+
if (!Array.isArray(ports) || ports.length === 0 || ports.length > maxRemoteIngressEgressListEntries) {
|
|
488
|
+
throw new Error(`RemoteIngress egress allowedPorts must contain 1-${maxRemoteIngressEgressListEntries} ports`);
|
|
489
|
+
}
|
|
490
|
+
const allowedPorts = [...new Set(ports.map((port) => Number(port)))].sort((a, b) => a - b);
|
|
491
|
+
for (const port of allowedPorts) {
|
|
492
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
493
|
+
throw new Error('RemoteIngress egress allowedPorts must contain valid TCP ports');
|
|
494
|
+
}
|
|
495
|
+
if (port !== 25) {
|
|
496
|
+
throw new Error('RemoteIngress egress is currently limited to outbound SMTP port 25');
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return allowedPorts;
|
|
500
|
+
}
|
|
501
|
+
|
|
427
502
|
private normalizeHubSettingsUpdate(
|
|
428
503
|
updates: TRemoteIngressHubSettingsUpdate,
|
|
429
504
|
): TRemoteIngressHubSettingsUpdate {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
|
+
import { logger } from '../logger.js';
|
|
2
3
|
import type { IRemoteIngressPerformanceConfig, IRemoteIngressStatus } from '../../ts_interfaces/data/remoteingress.js';
|
|
3
4
|
import type { RemoteIngressManager } from './classes.remoteingress-manager.js';
|
|
4
5
|
|
|
@@ -12,6 +13,10 @@ export interface ITunnelManagerConfig {
|
|
|
12
13
|
performance?: IRemoteIngressPerformanceConfig;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
export type TRemoteIngressEgressProxyRequest = Omit<plugins.remoteingress.IEgressTcpProxyRequest, 'edgeId'> & {
|
|
17
|
+
edgeId?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
15
20
|
/**
|
|
16
21
|
* Manages the RemoteIngressHub instance and tracks connected edge statuses.
|
|
17
22
|
*/
|
|
@@ -62,6 +67,20 @@ export class TunnelManager {
|
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
});
|
|
70
|
+
|
|
71
|
+
this.hub.on('egressConnection', (data: {
|
|
72
|
+
proxyId: string;
|
|
73
|
+
edgeId: string;
|
|
74
|
+
logicalHost: string;
|
|
75
|
+
port: number;
|
|
76
|
+
outcome: string;
|
|
77
|
+
resolvedIp?: string;
|
|
78
|
+
error?: string;
|
|
79
|
+
}) => {
|
|
80
|
+
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
|
+
const isExpectedLifecycleEvent = data.outcome === 'connected' || data.outcome === 'closed';
|
|
82
|
+
logger.log(isExpectedLifecycleEvent ? 'info' : 'warn', message);
|
|
83
|
+
});
|
|
65
84
|
}
|
|
66
85
|
|
|
67
86
|
/**
|
|
@@ -181,9 +200,51 @@ export class TunnelManager {
|
|
|
181
200
|
await run;
|
|
182
201
|
}
|
|
183
202
|
|
|
203
|
+
public async startEgressTcpProxy(
|
|
204
|
+
request: TRemoteIngressEgressProxyRequest,
|
|
205
|
+
): Promise<plugins.remoteingress.IEgressTcpProxyInfo> {
|
|
206
|
+
if (this.stopped) {
|
|
207
|
+
throw new Error('RemoteIngress hub is not running');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const status = await this.hub.getStatus();
|
|
211
|
+
if (this.stopped) {
|
|
212
|
+
throw new Error('RemoteIngress hub is not running');
|
|
213
|
+
}
|
|
214
|
+
const connectedEdges = Array.isArray(status?.connectedEdges) ? status.connectedEdges : [];
|
|
215
|
+
const eligibleEdges = connectedEdges.filter((edge: any) => {
|
|
216
|
+
const hasEgressCapability = Array.isArray(edge.capabilities) && edge.capabilities.includes('egressTcpV1');
|
|
217
|
+
const usesQuic = edge.transportMode === 'quic'
|
|
218
|
+
|| (edge.transportMode === 'quicWithFallback' && edge.fallbackUsed === false);
|
|
219
|
+
return edge.egressEnabled === true && hasEgressCapability && usesQuic;
|
|
220
|
+
});
|
|
221
|
+
const selectedEdge = request.edgeId
|
|
222
|
+
? eligibleEdges.find((edge: any) => edge.edgeId === request.edgeId)
|
|
223
|
+
: eligibleEdges[0];
|
|
224
|
+
|
|
225
|
+
if (!selectedEdge) {
|
|
226
|
+
const edgeHint = request.edgeId ? ` for edge ${request.edgeId}` : '';
|
|
227
|
+
throw new Error(`No eligible RemoteIngress QUIC egress edge is connected${edgeHint}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return await this.hub.startEgressTcpProxy({
|
|
231
|
+
...request,
|
|
232
|
+
edgeId: selectedEdge.edgeId,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
public async stopEgressTcpProxy(proxyId: string): Promise<boolean> {
|
|
237
|
+
if (this.stopped) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
return await this.hub.stopEgressTcpProxy(proxyId);
|
|
241
|
+
}
|
|
242
|
+
|
|
184
243
|
private applyRustStatus(status: IRemoteIngressStatus, rustEdge: any): void {
|
|
185
244
|
status.transportMode = rustEdge.transportMode;
|
|
186
245
|
status.fallbackUsed = rustEdge.fallbackUsed;
|
|
246
|
+
status.capabilities = rustEdge.capabilities;
|
|
247
|
+
status.egressEnabled = rustEdge.egressEnabled;
|
|
187
248
|
status.performance = rustEdge.performance;
|
|
188
249
|
status.flowControl = rustEdge.flowControl;
|
|
189
250
|
status.queues = rustEdge.queues;
|
|
@@ -19,6 +19,8 @@ export class RemoteIngress {
|
|
|
19
19
|
public effectiveListenPortsUdp?: number[];
|
|
20
20
|
public manualPorts?: number[];
|
|
21
21
|
public derivedPorts?: number[];
|
|
22
|
+
public performance?: interfaces.data.IRemoteIngressPerformanceConfig;
|
|
23
|
+
public egress?: interfaces.data.IRemoteIngressEgressPolicyConfig;
|
|
22
24
|
|
|
23
25
|
constructor(clientRef: DcRouterApiClient, data: interfaces.data.IRemoteIngress) {
|
|
24
26
|
this.clientRef = clientRef;
|
|
@@ -36,6 +38,8 @@ export class RemoteIngress {
|
|
|
36
38
|
this.effectiveListenPortsUdp = data.effectiveListenPortsUdp;
|
|
37
39
|
this.manualPorts = data.manualPorts;
|
|
38
40
|
this.derivedPorts = data.derivedPorts;
|
|
41
|
+
this.performance = data.performance;
|
|
42
|
+
this.egress = data.egress;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
public async update(changes: {
|
|
@@ -44,6 +48,8 @@ export class RemoteIngress {
|
|
|
44
48
|
autoDerivePorts?: boolean;
|
|
45
49
|
enabled?: boolean;
|
|
46
50
|
tags?: string[];
|
|
51
|
+
performance?: interfaces.data.IRemoteIngressPerformanceConfig;
|
|
52
|
+
egress?: interfaces.data.IRemoteIngressEgressPolicyConfig;
|
|
47
53
|
}): Promise<void> {
|
|
48
54
|
const response = await this.clientRef.request<interfaces.requests.IReq_UpdateRemoteIngress>(
|
|
49
55
|
'updateRemoteIngress',
|
|
@@ -65,6 +71,8 @@ export class RemoteIngress {
|
|
|
65
71
|
this.effectiveListenPortsUdp = edge.effectiveListenPortsUdp;
|
|
66
72
|
this.manualPorts = edge.manualPorts;
|
|
67
73
|
this.derivedPorts = edge.derivedPorts;
|
|
74
|
+
this.performance = edge.performance;
|
|
75
|
+
this.egress = edge.egress;
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
public async delete(): Promise<void> {
|
|
@@ -106,6 +114,8 @@ export class RemoteIngressBuilder {
|
|
|
106
114
|
private edgeName: string = '';
|
|
107
115
|
private edgeListenPorts?: number[];
|
|
108
116
|
private edgeAutoDerivePorts?: boolean;
|
|
117
|
+
private edgePerformance?: interfaces.data.IRemoteIngressPerformanceConfig;
|
|
118
|
+
private edgeEgress?: interfaces.data.IRemoteIngressEgressPolicyConfig;
|
|
109
119
|
private edgeTags?: string[];
|
|
110
120
|
|
|
111
121
|
constructor(clientRef: DcRouterApiClient) {
|
|
@@ -132,6 +142,16 @@ export class RemoteIngressBuilder {
|
|
|
132
142
|
return this;
|
|
133
143
|
}
|
|
134
144
|
|
|
145
|
+
public setPerformance(performance: interfaces.data.IRemoteIngressPerformanceConfig): this {
|
|
146
|
+
this.edgePerformance = performance;
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public setEgress(egress: interfaces.data.IRemoteIngressEgressPolicyConfig): this {
|
|
151
|
+
this.edgeEgress = egress;
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
|
|
135
155
|
public async save(): Promise<RemoteIngress> {
|
|
136
156
|
const response = await this.clientRef.request<interfaces.requests.IReq_CreateRemoteIngress>(
|
|
137
157
|
'createRemoteIngress',
|
|
@@ -139,6 +159,8 @@ export class RemoteIngressBuilder {
|
|
|
139
159
|
name: this.edgeName,
|
|
140
160
|
listenPorts: this.edgeListenPorts,
|
|
141
161
|
autoDerivePorts: this.edgeAutoDerivePorts,
|
|
162
|
+
performance: this.edgePerformance,
|
|
163
|
+
egress: this.edgeEgress,
|
|
142
164
|
tags: this.edgeTags,
|
|
143
165
|
}) as any,
|
|
144
166
|
);
|
|
@@ -176,11 +198,15 @@ export class RemoteIngressManager {
|
|
|
176
198
|
name: string;
|
|
177
199
|
listenPorts?: number[];
|
|
178
200
|
autoDerivePorts?: boolean;
|
|
201
|
+
performance?: interfaces.data.IRemoteIngressPerformanceConfig;
|
|
202
|
+
egress?: interfaces.data.IRemoteIngressEgressPolicyConfig;
|
|
179
203
|
tags?: string[];
|
|
180
204
|
}): Promise<RemoteIngress> {
|
|
181
205
|
const builder = this.build().setName(options.name);
|
|
182
206
|
if (options.listenPorts) builder.setListenPorts(options.listenPorts);
|
|
183
207
|
if (options.autoDerivePorts !== undefined) builder.setAutoDerivePorts(options.autoDerivePorts);
|
|
208
|
+
if (options.performance) builder.setPerformance(options.performance);
|
|
209
|
+
if (options.egress) builder.setEgress(options.egress);
|
|
184
210
|
if (options.tags) builder.setTags(options.tags);
|
|
185
211
|
return builder.save();
|
|
186
212
|
}
|
package/ts_apiclient/readme.md
CHANGED
|
@@ -113,6 +113,12 @@ const edge = await client.remoteIngress.build()
|
|
|
113
113
|
.setName('edge-eu-1')
|
|
114
114
|
.setListenPorts([80, 443])
|
|
115
115
|
.setAutoDerivePorts(true)
|
|
116
|
+
.setEgress({
|
|
117
|
+
enabled: true,
|
|
118
|
+
allowedPorts: [25],
|
|
119
|
+
allowedHostPatterns: ['*.example.com'],
|
|
120
|
+
maxConcurrentStreams: 100,
|
|
121
|
+
})
|
|
116
122
|
.setTags(['production', 'eu'])
|
|
117
123
|
.save();
|
|
118
124
|
|
|
@@ -120,6 +126,8 @@ const connectionToken = await edge.getConnectionToken();
|
|
|
120
126
|
console.log(token.tokenValue, connectionToken);
|
|
121
127
|
```
|
|
122
128
|
|
|
129
|
+
RemoteIngress resources expose `performance` and `egress` fields. `create()` and `update()` accept the same fields as the builder. Egress is default-disabled; dcrouter currently accepts outbound SMTP egress on port `25` only.
|
|
130
|
+
|
|
123
131
|
## What This Package Is Not
|
|
124
132
|
|
|
125
133
|
- It does not start dcrouter.
|
|
@@ -95,6 +95,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
|
|
95
95
|
listenPorts?: number[];
|
|
96
96
|
autoDerivePorts?: boolean;
|
|
97
97
|
performance?: interfaces.data.IRemoteIngressPerformanceConfig;
|
|
98
|
+
egress?: interfaces.data.IRemoteIngressEgressPolicyConfig;
|
|
98
99
|
tags?: string[];
|
|
99
100
|
}>(async (statePartArg, dataArg, actionContext): Promise<IRemoteIngressState> => {
|
|
100
101
|
const context = getActionContext();
|
|
@@ -111,6 +112,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
|
|
111
112
|
listenPorts: dataArg.listenPorts,
|
|
112
113
|
autoDerivePorts: dataArg.autoDerivePorts,
|
|
113
114
|
performance: dataArg.performance,
|
|
115
|
+
egress: dataArg.egress,
|
|
114
116
|
tags: dataArg.tags,
|
|
115
117
|
});
|
|
116
118
|
|
|
@@ -164,6 +166,7 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
|
|
|
164
166
|
listenPorts?: number[];
|
|
165
167
|
autoDerivePorts?: boolean;
|
|
166
168
|
performance?: interfaces.data.IRemoteIngressPerformanceConfig;
|
|
169
|
+
egress?: interfaces.data.IRemoteIngressEgressPolicyConfig;
|
|
167
170
|
tags?: string[];
|
|
168
171
|
}>(async (statePartArg, dataArg, actionContext): Promise<IRemoteIngressState> => {
|
|
169
172
|
const context = getActionContext();
|
|
@@ -181,6 +184,7 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
|
|
|
181
184
|
listenPorts: dataArg.listenPorts,
|
|
182
185
|
autoDerivePorts: dataArg.autoDerivePorts,
|
|
183
186
|
performance: dataArg.performance,
|
|
187
|
+
egress: dataArg.egress,
|
|
184
188
|
tags: dataArg.tags,
|
|
185
189
|
});
|
|
186
190
|
|
|
@@ -330,6 +330,11 @@ export class OpsViewEmailDomains extends DeesElement {
|
|
|
330
330
|
.description=${'Public hostname used in SMTP banners and DNS records'}
|
|
331
331
|
.value=${settings?.hostname || ''}
|
|
332
332
|
></dees-input-text>
|
|
333
|
+
<dees-input-checkbox
|
|
334
|
+
.key=${'outboundRemoteIngress'}
|
|
335
|
+
.label=${'Route outbound SMTP through RemoteIngress'}
|
|
336
|
+
.value=${settings?.outboundMode === 'remoteIngress'}
|
|
337
|
+
></dees-input-checkbox>
|
|
333
338
|
<dees-input-text
|
|
334
339
|
.key=${'ports'}
|
|
335
340
|
.label=${'Public ports'}
|
|
@@ -370,6 +375,7 @@ export class OpsViewEmailDomains extends DeesElement {
|
|
|
370
375
|
{
|
|
371
376
|
enabled: Boolean(data.enabled),
|
|
372
377
|
hostname: String(data.hostname || '').trim() || null,
|
|
378
|
+
outboundMode: Boolean(data.outboundRemoteIngress) ? 'remoteIngress' : 'direct',
|
|
373
379
|
ports: this.parsePortList(String(data.ports || '')),
|
|
374
380
|
portMapping: this.parsePortMapping(String(data.portMapping || '')),
|
|
375
381
|
maxMessageSize: maxMessageSizeRaw ? Number.parseInt(maxMessageSizeRaw, 10) : null,
|
|
@@ -259,6 +259,7 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|
|
259
259
|
transport: this.getTransportHtml(edge.id),
|
|
260
260
|
publicIp: this.getEdgePublicIp(edge.id),
|
|
261
261
|
ports: this.getPortsHtml(edge),
|
|
262
|
+
smtpEgress: this.getEgressHtml(edge),
|
|
262
263
|
tunnels: this.getEdgeTunnelCount(edge.id),
|
|
263
264
|
maxConnections: this.getMaxConnectionsHtml(edge),
|
|
264
265
|
window: this.getWindowHtml(edge.id),
|
|
@@ -281,6 +282,9 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|
|
281
282
|
<dees-input-text .key=${'listenPorts'} .label=${'Manual Ports'} .description=${'Comma-separated port numbers, optional'}></dees-input-text>
|
|
282
283
|
<dees-input-checkbox .key=${'autoDerivePorts'} .label=${'Auto-derive ports from routes'} .value=${true}></dees-input-checkbox>
|
|
283
284
|
<dees-input-text .key=${'maxStreamsPerEdge'} .label=${'Max Connections'} .description=${'Optional maximum concurrent client connections for this edge. Leave empty to use the hub default.'}></dees-input-text>
|
|
285
|
+
<dees-input-checkbox .key=${'egressEnabled'} .label=${'Allow outbound SMTP egress'} .value=${false}></dees-input-checkbox>
|
|
286
|
+
<dees-input-text .key=${'egressAllowedHostPatterns'} .label=${'SMTP Egress Host Patterns'} .description=${'Optional comma-separated MX host patterns. Leave empty to allow public MX hosts on port 25.'}></dees-input-text>
|
|
287
|
+
<dees-input-text .key=${'egressMaxConcurrentStreams'} .label=${'SMTP Egress Max Streams'} .description=${'Optional concurrent outbound SMTP stream cap for this edge.'}></dees-input-text>
|
|
284
288
|
<dees-input-text .key=${'tags'} .label=${'Tags'} .description=${'Comma-separated, optional'}></dees-input-text>
|
|
285
289
|
</dees-form>
|
|
286
290
|
`,
|
|
@@ -305,8 +309,10 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|
|
305
309
|
: undefined;
|
|
306
310
|
const autoDerivePorts = formData.autoDerivePorts !== false;
|
|
307
311
|
let performance: interfaces.data.IRemoteIngressPerformanceConfig | undefined;
|
|
312
|
+
let egress: interfaces.data.IRemoteIngressEgressPolicyConfig;
|
|
308
313
|
try {
|
|
309
314
|
performance = this.collectPerformanceOverride(formData);
|
|
315
|
+
egress = this.collectEgressPolicy(formData);
|
|
310
316
|
} catch (err: unknown) {
|
|
311
317
|
const { DeesToast } = await import('@design.estate/dees-catalog');
|
|
312
318
|
DeesToast.show({ message: (err as Error).message, type: 'error', duration: 4000 });
|
|
@@ -317,7 +323,7 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|
|
317
323
|
: undefined;
|
|
318
324
|
await appstate.remoteIngressStatePart.dispatchAction(
|
|
319
325
|
appstate.createRemoteIngressAction,
|
|
320
|
-
{ name, listenPorts, autoDerivePorts, performance, tags },
|
|
326
|
+
{ name, listenPorts, autoDerivePorts, performance, egress, tags },
|
|
321
327
|
);
|
|
322
328
|
await modalArg.destroy();
|
|
323
329
|
},
|
|
@@ -375,6 +381,9 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|
|
375
381
|
<dees-input-text .key=${'listenPorts'} .label=${'Manual Ports'} .description=${'Comma-separated port numbers'} .value=${(edge.listenPorts || []).join(', ')}></dees-input-text>
|
|
376
382
|
<dees-input-checkbox .key=${'autoDerivePorts'} .label=${'Auto-derive ports from routes'} .value=${edge.autoDerivePorts !== false}></dees-input-checkbox>
|
|
377
383
|
<dees-input-text .key=${'maxStreamsPerEdge'} .label=${'Max Connections'} .description=${'Optional maximum concurrent client connections for this edge. Leave empty to use the hub default.'} .value=${edge.performance?.maxStreamsPerEdge?.toString() || ''}></dees-input-text>
|
|
384
|
+
<dees-input-checkbox .key=${'egressEnabled'} .label=${'Allow outbound SMTP egress'} .value=${edge.egress?.enabled === true}></dees-input-checkbox>
|
|
385
|
+
<dees-input-text .key=${'egressAllowedHostPatterns'} .label=${'SMTP Egress Host Patterns'} .description=${'Optional comma-separated MX host patterns. Leave empty to allow public MX hosts on port 25.'} .value=${(edge.egress?.allowedHostPatterns || []).join(', ')}></dees-input-text>
|
|
386
|
+
<dees-input-text .key=${'egressMaxConcurrentStreams'} .label=${'SMTP Egress Max Streams'} .description=${'Optional concurrent outbound SMTP stream cap for this edge.'} .value=${edge.egress?.maxConcurrentStreams?.toString() || ''}></dees-input-text>
|
|
378
387
|
<dees-input-text .key=${'tags'} .label=${'Tags'} .description=${'Comma-separated'} .value=${(edge.tags || []).join(', ')}></dees-input-text>
|
|
379
388
|
</dees-form>
|
|
380
389
|
`,
|
|
@@ -397,8 +406,10 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|
|
397
406
|
: [];
|
|
398
407
|
const autoDerivePorts = formData.autoDerivePorts !== false;
|
|
399
408
|
let performance: interfaces.data.IRemoteIngressPerformanceConfig | undefined;
|
|
409
|
+
let egress: interfaces.data.IRemoteIngressEgressPolicyConfig;
|
|
400
410
|
try {
|
|
401
411
|
performance = this.collectPerformanceOverride(formData, edge.performance);
|
|
412
|
+
egress = this.collectEgressPolicy(formData, edge.egress);
|
|
402
413
|
} catch (err: unknown) {
|
|
403
414
|
const { DeesToast } = await import('@design.estate/dees-catalog');
|
|
404
415
|
DeesToast.show({ message: (err as Error).message, type: 'error', duration: 4000 });
|
|
@@ -415,6 +426,7 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|
|
415
426
|
listenPorts,
|
|
416
427
|
autoDerivePorts,
|
|
417
428
|
performance,
|
|
429
|
+
egress,
|
|
418
430
|
tags,
|
|
419
431
|
},
|
|
420
432
|
);
|
|
@@ -534,6 +546,16 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|
|
534
546
|
`;
|
|
535
547
|
}
|
|
536
548
|
|
|
549
|
+
private getEgressHtml(edge: interfaces.data.IRemoteIngress): TemplateResult | string {
|
|
550
|
+
if (!edge.egress?.enabled) return '-';
|
|
551
|
+
return html`
|
|
552
|
+
<div class="metricStack">
|
|
553
|
+
<strong>SMTP</strong>
|
|
554
|
+
<span class="metricMuted">port ${(edge.egress.allowedPorts || [25]).join(', ')}</span>
|
|
555
|
+
</div>
|
|
556
|
+
`;
|
|
557
|
+
}
|
|
558
|
+
|
|
537
559
|
private getTransportHtml(edgeId: string): TemplateResult | string {
|
|
538
560
|
const status = this.getEdgeStatus(edgeId);
|
|
539
561
|
if (!status?.connected) return '-';
|
|
@@ -618,6 +640,38 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|
|
618
640
|
return base ? {} : undefined;
|
|
619
641
|
}
|
|
620
642
|
|
|
643
|
+
private collectEgressPolicy(
|
|
644
|
+
formData: Record<string, any>,
|
|
645
|
+
base?: interfaces.data.IRemoteIngressEgressPolicyConfig,
|
|
646
|
+
): interfaces.data.IRemoteIngressEgressPolicyConfig {
|
|
647
|
+
const enabled = formData.egressEnabled !== false && Boolean(formData.egressEnabled);
|
|
648
|
+
if (!enabled) {
|
|
649
|
+
return { enabled: false };
|
|
650
|
+
}
|
|
651
|
+
const next: interfaces.data.IRemoteIngressEgressPolicyConfig = {
|
|
652
|
+
...(base || {}),
|
|
653
|
+
enabled: true,
|
|
654
|
+
allowedPorts: [25],
|
|
655
|
+
};
|
|
656
|
+
const hostPatternsText = `${formData.egressAllowedHostPatterns || ''}`.trim();
|
|
657
|
+
if (hostPatternsText) {
|
|
658
|
+
next.allowedHostPatterns = hostPatternsText.split(',').map((pattern) => pattern.trim()).filter(Boolean);
|
|
659
|
+
} else {
|
|
660
|
+
delete next.allowedHostPatterns;
|
|
661
|
+
}
|
|
662
|
+
const maxStreamsText = `${formData.egressMaxConcurrentStreams || ''}`.trim();
|
|
663
|
+
if (maxStreamsText) {
|
|
664
|
+
const maxConcurrentStreams = Number.parseInt(maxStreamsText, 10);
|
|
665
|
+
if (!Number.isInteger(maxConcurrentStreams) || maxConcurrentStreams < 1) {
|
|
666
|
+
throw new Error('SMTP Egress Max Streams must be a positive integer');
|
|
667
|
+
}
|
|
668
|
+
next.maxConcurrentStreams = maxConcurrentStreams;
|
|
669
|
+
} else {
|
|
670
|
+
delete next.maxConcurrentStreams;
|
|
671
|
+
}
|
|
672
|
+
return next;
|
|
673
|
+
}
|
|
674
|
+
|
|
621
675
|
private async showHubSettingsDialog(): Promise<void> {
|
|
622
676
|
const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
|
|
623
677
|
const hubSettings = this.riState.hubSettings;
|