@serve.zone/dcrouter 15.6.1 → 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.
- 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 +36 -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 +4 -4
- 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 +46 -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
package/ts/classes.dcrouter.ts
CHANGED
|
@@ -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
|
}
|
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,
|