@serve.zone/dcrouter 15.6.0 → 15.7.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.
Files changed (50) hide show
  1. package/deno.json +1 -1
  2. package/dist_serve/bundle.js +423 -407
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.dcrouter.d.ts +4 -1
  5. package/dist_ts/classes.dcrouter.js +45 -1
  6. package/dist_ts/db/documents/classes.email-server-settings.doc.d.ts +2 -1
  7. package/dist_ts/db/documents/classes.email-server-settings.doc.js +8 -2
  8. package/dist_ts/db/documents/classes.remote-ingress-edge.doc.d.ts +2 -1
  9. package/dist_ts/db/documents/classes.remote-ingress-edge.doc.js +8 -2
  10. package/dist_ts/email/classes.email-settings.manager.d.ts +5 -1
  11. package/dist_ts/email/classes.email-settings.manager.js +24 -1
  12. package/dist_ts/opsserver/handlers/config.handler.js +2 -1
  13. package/dist_ts/opsserver/handlers/email-settings.handler.js +2 -1
  14. package/dist_ts/opsserver/handlers/remoteingress.handler.js +3 -2
  15. package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +6 -2
  16. package/dist_ts/remoteingress/classes.remoteingress-manager.js +65 -2
  17. package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +6 -0
  18. package/dist_ts/remoteingress/classes.tunnel-manager.js +36 -1
  19. package/dist_ts_apiclient/classes.remoteingress.d.ts +10 -0
  20. package/dist_ts_apiclient/classes.remoteingress.js +23 -1
  21. package/dist_ts_interfaces/data/email-settings.d.ts +4 -0
  22. package/dist_ts_interfaces/data/remoteingress.d.ts +18 -0
  23. package/dist_ts_interfaces/requests/config.d.ts +1 -0
  24. package/dist_ts_interfaces/requests/remoteingress.d.ts +3 -1
  25. package/dist_ts_migrations/index.d.ts +1 -0
  26. package/dist_ts_migrations/index.js +80 -9
  27. package/dist_ts_web/00_commitinfo_data.js +1 -1
  28. package/dist_ts_web/appstate/remoteingress.d.ts +2 -0
  29. package/dist_ts_web/appstate/remoteingress.js +3 -1
  30. package/dist_ts_web/elements/email/ops-view-email-domains.js +7 -1
  31. package/dist_ts_web/elements/network/ops-view-remoteingress.d.ts +2 -0
  32. package/dist_ts_web/elements/network/ops-view-remoteingress.js +54 -2
  33. package/package.json +5 -5
  34. package/readme.md +22 -0
  35. package/ts/00_commitinfo_data.ts +1 -1
  36. package/ts/classes.dcrouter.ts +56 -4
  37. package/ts/db/documents/classes.email-server-settings.doc.ts +4 -1
  38. package/ts/db/documents/classes.remote-ingress-edge.doc.ts +4 -1
  39. package/ts/email/classes.email-settings.manager.ts +29 -0
  40. package/ts/opsserver/handlers/config.handler.ts +1 -0
  41. package/ts/opsserver/handlers/email-settings.handler.ts +1 -0
  42. package/ts/opsserver/handlers/remoteingress.handler.ts +2 -0
  43. package/ts/remoteingress/classes.remoteingress-manager.ts +78 -3
  44. package/ts/remoteingress/classes.tunnel-manager.ts +46 -0
  45. package/ts_apiclient/classes.remoteingress.ts +26 -0
  46. package/ts_apiclient/readme.md +8 -0
  47. package/ts_web/00_commitinfo_data.ts +1 -1
  48. package/ts_web/appstate/remoteingress.ts +4 -0
  49. package/ts_web/elements/email/ops-view-email-domains.ts +6 -0
  50. package/ts_web/elements/network/ops-view-remoteingress.ts +55 -1
@@ -14,6 +14,8 @@ import {
14
14
  type IExtendedSmtpSession,
15
15
  type IMessageAcceptanceContext,
16
16
  type IMessageAcceptanceDecision,
17
+ type IOutboundConnectionProxy,
18
+ type IOutboundConnectionProxyContext,
17
19
  } from '@push.rocks/smartmta';
18
20
  import { logger } from './logger.js';
19
21
  import { StorageBackedCertManager } from './classes.storage-cert-manager.js';
@@ -39,7 +41,7 @@ import { AcmeConfigManager } from './acme/manager.acme-config.js';
39
41
  import { SmartAcmeLifecycle } from './acme/classes.smartacme-lifecycle.js';
40
42
  import { AcceptedEmailSpool, EmailDomainManager, EmailRouteBuilder, EmailSettingsManager, SmartMtaStorageManager, WorkAppMailManager, type TSmartMtaQueueItemLike } from './email/index.js';
41
43
  import type { IRoute } from '../ts_interfaces/data/route-management.js';
42
- import type { IEmailPortConfig, IEmailServerSettings, IEmailServerSettingsSeed, TEmailServerSettingsUpdate } from '../ts_interfaces/data/email-settings.js';
44
+ import type { IEmailPortConfig, IEmailServerSettings, IEmailServerSettingsSeed, TEmailOutboundMode, TEmailServerSettingsUpdate } from '../ts_interfaces/data/email-settings.js';
43
45
  import type { IDcRouterRouteConfig, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig, TRemoteIngressHubSettingsUpdate } from '../ts_interfaces/data/remoteingress.js';
44
46
  import type { ISecurityCompiledPolicy } from '../ts_interfaces/data/security-policy.js';
45
47
 
@@ -55,20 +57,23 @@ export interface IDcRouterOptions {
55
57
 
56
58
  /** Legacy name for coreTrafficConfig. */
57
59
  smartProxyConfig?: plugins.smartproxy.ISmartProxyOptions;
58
-
60
+
59
61
  /**
60
62
  * Email server configuration
61
63
  * This enables all email handling with pattern-based routing
62
64
  */
63
65
  emailConfig?: IUnifiedEmailServerOptions;
64
-
66
+
67
+ /** Outbound SMTP connection mode. Defaults to direct unless DB settings override it. */
68
+ emailOutboundMode?: TEmailOutboundMode;
69
+
65
70
  /**
66
71
  * Custom email port configuration
67
72
  * Allows configuring specific ports for email handling
68
73
  * This overrides the default port mapping in the emailConfig
69
74
  */
70
75
  emailPortConfig?: IEmailPortConfig;
71
-
76
+
72
77
  /** TLS/certificate configuration */
73
78
  tls?: {
74
79
  /** Contact email for ACME certificates */
@@ -841,6 +846,9 @@ export class DcRouter {
841
846
  seed.enabled = true;
842
847
  seed.emailConfig = JSON.parse(JSON.stringify(this.options.emailConfig));
843
848
  }
849
+ if (this.options.emailOutboundMode) {
850
+ seed.outboundMode = this.options.emailOutboundMode;
851
+ }
844
852
  if (this.options.emailPortConfig) {
845
853
  seed.emailPortConfig = JSON.parse(JSON.stringify(this.options.emailPortConfig));
846
854
  }
@@ -1591,6 +1599,9 @@ export class DcRouter {
1591
1599
  // Create config with mapped ports
1592
1600
  const baseEmailConfig = this.options.emailConfig;
1593
1601
  const configuredMessageDataHook = baseEmailConfig.hooks?.onMessageData;
1602
+ const outboundMode = this.emailSettingsManager?.getOutboundMode()
1603
+ || this.options.emailOutboundMode
1604
+ || 'direct';
1594
1605
  const mappedEmailPorts = baseEmailConfig.ports.map(port => portMapping[port] || port + 10000);
1595
1606
  const configuredSecurePort = baseEmailConfig.smtp?.securePort;
1596
1607
  const mappedSecurePort = configuredSecurePort === undefined
@@ -1606,6 +1617,12 @@ export class DcRouter {
1606
1617
  ...this.options.emailConfig.queue,
1607
1618
  storageType: 'memory',
1608
1619
  },
1620
+ outbound: {
1621
+ ...baseEmailConfig.outbound,
1622
+ ...(outboundMode === 'remoteIngress'
1623
+ ? { connectionProxyProvider: async (context) => this.createRemoteIngressEmailConnectionProxy(context) }
1624
+ : {}),
1625
+ },
1609
1626
  smtp: {
1610
1627
  ...baseEmailConfig.smtp,
1611
1628
  ...(mappedSecurePort !== undefined
@@ -1755,6 +1772,41 @@ export class DcRouter {
1755
1772
  logger.log('info', `Email server started on ports: ${emailConfig.ports.join(', ')}`);
1756
1773
  }
1757
1774
 
1775
+ private async createRemoteIngressEmailConnectionProxy(
1776
+ context: IOutboundConnectionProxyContext,
1777
+ ): Promise<IOutboundConnectionProxy> {
1778
+ const tunnelManager = this.tunnelManager;
1779
+ if (!tunnelManager) {
1780
+ throw new Error('RemoteIngress outbound SMTP egress is enabled, but the hub is not running');
1781
+ }
1782
+
1783
+ const connectionTimeoutMs = this.options.emailConfig?.outbound?.connectionTimeout ?? 30_000;
1784
+ const proxy = await tunnelManager.startEgressTcpProxy({
1785
+ logicalHost: context.host,
1786
+ port: context.port,
1787
+ serverFirst: !context.secure,
1788
+ connectTimeoutMs: connectionTimeoutMs,
1789
+ acceptTimeoutMs: connectionTimeoutMs,
1790
+ });
1791
+ let closed = false;
1792
+ return {
1793
+ connectHost: proxy.listenHost,
1794
+ connectPort: proxy.listenPort,
1795
+ poolKey: `remoteIngress:${proxy.edgeId}:${context.host}:${context.port}`,
1796
+ close: async () => {
1797
+ if (closed) {
1798
+ return;
1799
+ }
1800
+ closed = true;
1801
+ try {
1802
+ await tunnelManager.stopEgressTcpProxy(proxy.proxyId);
1803
+ } catch (err: unknown) {
1804
+ logger.log('warn', `Failed to stop RemoteIngress egress proxy ${proxy.proxyId}: ${(err as Error).message}`);
1805
+ }
1806
+ },
1807
+ };
1808
+ }
1809
+
1758
1810
 
1759
1811
  /**
1760
1812
  * Update the unified email configuration
@@ -1,7 +1,7 @@
1
1
  import type { IUnifiedEmailServerOptions } from '@push.rocks/smartmta';
2
2
  import * as plugins from '../../plugins.js';
3
3
  import { DcRouterDb } from '../classes.dcrouter-db.js';
4
- import type { IEmailPortConfig } from '../../../ts_interfaces/data/email-settings.js';
4
+ import type { IEmailPortConfig, TEmailOutboundMode } from '../../../ts_interfaces/data/email-settings.js';
5
5
 
6
6
  const getDb = () => DcRouterDb.getInstance().getDb();
7
7
 
@@ -17,6 +17,9 @@ export class EmailServerSettingsDoc extends plugins.smartdata.SmartDataDbDoc<Ema
17
17
  @plugins.smartdata.svDb()
18
18
  public emailConfig?: IUnifiedEmailServerOptions;
19
19
 
20
+ @plugins.smartdata.svDb()
21
+ public outboundMode: TEmailOutboundMode = 'direct';
22
+
20
23
  @plugins.smartdata.svDb()
21
24
  public emailPortConfig?: IEmailPortConfig;
22
25
 
@@ -1,6 +1,6 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import { DcRouterDb } from '../classes.dcrouter-db.js';
3
- import type { IRemoteIngressPerformanceConfig } from '../../../ts_interfaces/data/remoteingress.js';
3
+ import type { IRemoteIngressEgressPolicyConfig, IRemoteIngressPerformanceConfig } from '../../../ts_interfaces/data/remoteingress.js';
4
4
 
5
5
  const getDb = () => DcRouterDb.getInstance().getDb();
6
6
 
@@ -31,6 +31,9 @@ export class RemoteIngressEdgeDoc extends plugins.smartdata.SmartDataDbDoc<Remot
31
31
  @plugins.smartdata.svDb()
32
32
  public performance?: IRemoteIngressPerformanceConfig;
33
33
 
34
+ @plugins.smartdata.svDb()
35
+ public egress?: IRemoteIngressEgressPolicyConfig;
36
+
34
37
  @plugins.smartdata.svDb()
35
38
  public tags!: string[];
36
39
 
@@ -4,6 +4,7 @@ import type { IDcRouterOptions } from '../classes.dcrouter.js';
4
4
  import type {
5
5
  IEmailPortConfig,
6
6
  IEmailServerSettings,
7
+ TEmailOutboundMode,
7
8
  TEmailServerSettingsUpdate,
8
9
  } from '../../ts_interfaces/data/email-settings.js';
9
10
 
@@ -21,6 +22,7 @@ function hasOwn(objectArg: object, keyArg: string): boolean {
21
22
  export class EmailSettingsManager {
22
23
  private cachedEmailConfig?: IUnifiedEmailServerOptions;
23
24
  private cachedEmailPortConfig?: IEmailPortConfig;
25
+ private outboundMode: TEmailOutboundMode = 'direct';
24
26
  private enabled = false;
25
27
  private updatedAt = 0;
26
28
  private updatedBy = 'default';
@@ -61,12 +63,17 @@ export class EmailSettingsManager {
61
63
  return this.isEnabled() ? clonePlain(this.cachedEmailPortConfig) : undefined;
62
64
  }
63
65
 
66
+ public getOutboundMode(): TEmailOutboundMode {
67
+ return this.outboundMode;
68
+ }
69
+
64
70
  public getPublicSettings(): IEmailServerSettings {
65
71
  const emailConfig = this.cachedEmailConfig;
66
72
  const emailPortConfig = this.cachedEmailPortConfig;
67
73
  return {
68
74
  enabled: this.isEnabled(),
69
75
  hostname: emailConfig?.hostname || null,
76
+ outboundMode: this.outboundMode,
70
77
  ports: [...(emailConfig?.ports || [])],
71
78
  portMapping: emailPortConfig?.portMapping ? { ...emailPortConfig.portMapping } : null,
72
79
  receivedEmailsPath: emailPortConfig?.receivedEmailsPath || null,
@@ -92,10 +99,12 @@ export class EmailSettingsManager {
92
99
  const nextEnabled = hasOwn(updates, 'enabled') ? Boolean(updates.enabled) : doc.enabled;
93
100
  const nextEmailConfig = this.patchEmailConfig(doc.emailConfig, updates, nextEnabled);
94
101
  const nextEmailPortConfig = this.patchEmailPortConfig(doc.emailPortConfig, updates);
102
+ const nextOutboundMode = this.patchOutboundMode(doc.outboundMode, updates);
95
103
 
96
104
  doc.enabled = nextEnabled;
97
105
  doc.emailConfig = nextEmailConfig;
98
106
  doc.emailPortConfig = nextEmailPortConfig;
107
+ doc.outboundMode = nextOutboundMode;
99
108
  doc.updatedAt = Date.now();
100
109
  doc.updatedBy = updatedBy;
101
110
  await doc.save();
@@ -109,6 +118,7 @@ export class EmailSettingsManager {
109
118
  this.enabled = doc.enabled;
110
119
  this.cachedEmailConfig = clonePlain(doc.emailConfig);
111
120
  this.cachedEmailPortConfig = clonePlain(doc.emailPortConfig);
121
+ this.outboundMode = this.normalizeOutboundMode(doc.outboundMode);
112
122
  this.updatedAt = doc.updatedAt;
113
123
  this.updatedBy = doc.updatedBy;
114
124
  }
@@ -116,6 +126,25 @@ export class EmailSettingsManager {
116
126
  private applyToRuntimeOptions(): void {
117
127
  this.options.emailConfig = this.getEmailConfig();
118
128
  this.options.emailPortConfig = this.getEmailPortConfig();
129
+ this.options.emailOutboundMode = this.outboundMode;
130
+ }
131
+
132
+ private patchOutboundMode(
133
+ existingMode: TEmailOutboundMode | undefined,
134
+ updates: TEmailServerSettingsUpdate,
135
+ ): TEmailOutboundMode {
136
+ if (hasOwn(updates, 'outboundMode') && updates.outboundMode !== undefined) {
137
+ return this.normalizeOutboundMode(updates.outboundMode);
138
+ }
139
+ return this.normalizeOutboundMode(existingMode);
140
+ }
141
+
142
+ private normalizeOutboundMode(mode: TEmailOutboundMode | undefined): TEmailOutboundMode {
143
+ if (!mode) return 'direct';
144
+ if (mode !== 'direct' && mode !== 'remoteIngress') {
145
+ throw new Error('Invalid email outbound mode');
146
+ }
147
+ return mode;
119
148
  }
120
149
 
121
150
  private patchEmailConfig(
@@ -107,6 +107,7 @@ export class ConfigHandler {
107
107
  ports: emailSettings?.ports || opts.emailConfig?.ports || [],
108
108
  portMapping,
109
109
  hostname: emailSettings?.hostname || opts.emailConfig?.hostname || null,
110
+ outboundMode: emailSettings?.outboundMode || opts.emailOutboundMode || 'direct',
110
111
  domains: emailDomains,
111
112
  emailRouteCount: emailSettings?.routeCount ?? opts.emailConfig?.routes?.length ?? 0,
112
113
  receivedEmailsPath: emailSettings?.receivedEmailsPath || opts.emailPortConfig?.receivedEmailsPath || null,
@@ -58,6 +58,7 @@ export class EmailSettingsHandler {
58
58
  return {
59
59
  enabled: Boolean(emailConfig),
60
60
  hostname: emailConfig?.hostname || null,
61
+ outboundMode: this.opsServerRef.dcRouterRef.options.emailOutboundMode || 'direct',
61
62
  ports: [...(emailConfig?.ports || [])],
62
63
  portMapping: emailPortConfig?.portMapping ? { ...emailPortConfig.portMapping } : null,
63
64
  receivedEmailsPath: emailPortConfig?.receivedEmailsPath || null,
@@ -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 {
@@ -12,6 +12,10 @@ export interface ITunnelManagerConfig {
12
12
  performance?: IRemoteIngressPerformanceConfig;
13
13
  }
14
14
 
15
+ export type TRemoteIngressEgressProxyRequest = Omit<plugins.remoteingress.IEgressTcpProxyRequest, 'edgeId'> & {
16
+ edgeId?: string;
17
+ };
18
+
15
19
  /**
16
20
  * Manages the RemoteIngressHub instance and tracks connected edge statuses.
17
21
  */
@@ -181,9 +185,51 @@ export class TunnelManager {
181
185
  await run;
182
186
  }
183
187
 
188
+ public async startEgressTcpProxy(
189
+ request: TRemoteIngressEgressProxyRequest,
190
+ ): Promise<plugins.remoteingress.IEgressTcpProxyInfo> {
191
+ if (this.stopped) {
192
+ throw new Error('RemoteIngress hub is not running');
193
+ }
194
+
195
+ const status = await this.hub.getStatus();
196
+ if (this.stopped) {
197
+ throw new Error('RemoteIngress hub is not running');
198
+ }
199
+ const connectedEdges = Array.isArray(status?.connectedEdges) ? status.connectedEdges : [];
200
+ const eligibleEdges = connectedEdges.filter((edge: any) => {
201
+ const hasEgressCapability = Array.isArray(edge.capabilities) && edge.capabilities.includes('egressTcpV1');
202
+ const usesQuic = edge.transportMode === 'quic'
203
+ || (edge.transportMode === 'quicWithFallback' && edge.fallbackUsed === false);
204
+ return edge.egressEnabled === true && hasEgressCapability && usesQuic;
205
+ });
206
+ const selectedEdge = request.edgeId
207
+ ? eligibleEdges.find((edge: any) => edge.edgeId === request.edgeId)
208
+ : eligibleEdges[0];
209
+
210
+ if (!selectedEdge) {
211
+ const edgeHint = request.edgeId ? ` for edge ${request.edgeId}` : '';
212
+ throw new Error(`No eligible RemoteIngress QUIC egress edge is connected${edgeHint}`);
213
+ }
214
+
215
+ return await this.hub.startEgressTcpProxy({
216
+ ...request,
217
+ edgeId: selectedEdge.edgeId,
218
+ });
219
+ }
220
+
221
+ public async stopEgressTcpProxy(proxyId: string): Promise<boolean> {
222
+ if (this.stopped) {
223
+ return false;
224
+ }
225
+ return await this.hub.stopEgressTcpProxy(proxyId);
226
+ }
227
+
184
228
  private applyRustStatus(status: IRemoteIngressStatus, rustEdge: any): void {
185
229
  status.transportMode = rustEdge.transportMode;
186
230
  status.fallbackUsed = rustEdge.fallbackUsed;
231
+ status.capabilities = rustEdge.capabilities;
232
+ status.egressEnabled = rustEdge.egressEnabled;
187
233
  status.performance = rustEdge.performance;
188
234
  status.flowControl = rustEdge.flowControl;
189
235
  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
  }
@@ -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.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '15.6.0',
6
+ version: '15.7.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -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,