@serve.zone/dcrouter 15.0.0 → 15.0.2

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 (53) hide show
  1. package/deno.json +1 -1
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/acme/classes.smartacme-lifecycle.d.ts +25 -0
  4. package/dist_ts/acme/classes.smartacme-lifecycle.js +144 -0
  5. package/dist_ts/acme/index.d.ts +1 -0
  6. package/dist_ts/acme/index.js +2 -1
  7. package/dist_ts/classes.dcrouter.d.ts +21 -139
  8. package/dist_ts/classes.dcrouter.js +71 -1585
  9. package/dist_ts/dns/classes.dns-server-runtime.d.ts +37 -0
  10. package/dist_ts/dns/classes.dns-server-runtime.js +449 -0
  11. package/dist_ts/dns/index.d.ts +1 -0
  12. package/dist_ts/dns/index.js +2 -1
  13. package/dist_ts/email/classes.accepted-email-spool.d.ts +55 -0
  14. package/dist_ts/email/classes.accepted-email-spool.js +345 -0
  15. package/dist_ts/email/classes.email-route-builder.d.ts +28 -0
  16. package/dist_ts/email/classes.email-route-builder.js +260 -0
  17. package/dist_ts/email/index.d.ts +2 -0
  18. package/dist_ts/email/index.js +3 -1
  19. package/dist_ts/opsserver/handlers/gatewayclient.handler.js +10 -8
  20. package/dist_ts/remoteingress/classes.hub-lifecycle.d.ts +27 -0
  21. package/dist_ts/remoteingress/classes.hub-lifecycle.js +241 -0
  22. package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +1 -2
  23. package/dist_ts/remoteingress/index.d.ts +1 -0
  24. package/dist_ts/remoteingress/index.js +2 -1
  25. package/dist_ts/security/classes.route-policy-augmenter.d.ts +22 -0
  26. package/dist_ts/security/classes.route-policy-augmenter.js +120 -0
  27. package/dist_ts/security/index.d.ts +1 -0
  28. package/dist_ts/security/index.js +2 -1
  29. package/dist_ts/vpn/classes.vpn-access-resolver.d.ts +34 -0
  30. package/dist_ts/vpn/classes.vpn-access-resolver.js +101 -0
  31. package/dist_ts/vpn/index.d.ts +1 -0
  32. package/dist_ts/vpn/index.js +2 -1
  33. package/dist_ts_migrations/index.js +92 -9
  34. package/dist_ts_web/00_commitinfo_data.js +1 -1
  35. package/package.json +2 -2
  36. package/ts/00_commitinfo_data.ts +1 -1
  37. package/ts/acme/classes.smartacme-lifecycle.ts +155 -0
  38. package/ts/acme/index.ts +1 -0
  39. package/ts/classes.dcrouter.ts +118 -1919
  40. package/ts/dns/classes.dns-server-runtime.ts +525 -0
  41. package/ts/dns/index.ts +1 -0
  42. package/ts/email/classes.accepted-email-spool.ts +434 -0
  43. package/ts/email/classes.email-route-builder.ts +312 -0
  44. package/ts/email/index.ts +2 -0
  45. package/ts/opsserver/handlers/gatewayclient.handler.ts +9 -7
  46. package/ts/remoteingress/classes.hub-lifecycle.ts +278 -0
  47. package/ts/remoteingress/classes.remoteingress-manager.ts +1 -1
  48. package/ts/remoteingress/index.ts +1 -0
  49. package/ts/security/classes.route-policy-augmenter.ts +140 -0
  50. package/ts/security/index.ts +1 -0
  51. package/ts/vpn/classes.vpn-access-resolver.ts +126 -0
  52. package/ts/vpn/index.ts +1 -0
  53. package/ts_web/00_commitinfo_data.ts +1 -1
@@ -0,0 +1,312 @@
1
+ import * as plugins from '../plugins.js';
2
+ import { logger } from '../logger.js';
3
+ import type { IUnifiedEmailServerOptions } from '@push.rocks/smartmta';
4
+ import type { IRoute } from '../../ts_interfaces/data/route-management.js';
5
+ import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
6
+ import type { DcRouter } from '../classes.dcrouter.js';
7
+
8
+ type TInboundProxyProtocolPolicy = NonNullable<plugins.smartproxy.IRouteMatch['inboundProxyProtocol']>;
9
+
10
+ /**
11
+ * Generates SmartProxy routes for the email ports and hydrates persisted
12
+ * email routes into runtime routes: server-first SMTP ports get a raw
13
+ * socket-handler proxy that injects PROXY protocol toward the backend.
14
+ */
15
+ export class EmailRouteBuilder {
16
+ constructor(private dcRouterRef: DcRouter) {}
17
+
18
+ public generateEmailRoutes(emailConfig: IUnifiedEmailServerOptions): IDcRouterRouteConfig[] {
19
+ const emailRoutes: IDcRouterRouteConfig[] = [];
20
+
21
+ // Create routes for each email port
22
+ for (const port of emailConfig.ports) {
23
+ // Create a descriptive name for the route based on the port
24
+ let routeName = 'email-route';
25
+ let tlsMode: 'terminate' | undefined;
26
+
27
+ // Handle different email ports differently
28
+ switch (port) {
29
+ case 25: // SMTP
30
+ routeName = 'smtp-route';
31
+ break;
32
+
33
+ case 587: // Submission
34
+ routeName = 'submission-route';
35
+ break;
36
+
37
+ case 465: // SMTPS
38
+ routeName = 'smtps-route';
39
+ tlsMode = 'terminate'; // SmartProxy owns public TLS; backend remains server-first SMTP
40
+ break;
41
+
42
+ default:
43
+ routeName = `email-port-${port}-route`;
44
+
45
+ // Check if we have specific settings for this port
46
+ if (this.dcRouterRef.options.emailPortConfig?.portSettings &&
47
+ this.dcRouterRef.options.emailPortConfig.portSettings[port]) {
48
+ const portSettings = this.dcRouterRef.options.emailPortConfig.portSettings[port];
49
+
50
+ // If this port requires TLS termination, set the mode accordingly
51
+ if (portSettings.terminateTls) {
52
+ tlsMode = 'terminate';
53
+ }
54
+
55
+ // Override the route name if specified
56
+ if (portSettings.routeName) {
57
+ routeName = portSettings.routeName;
58
+ }
59
+ }
60
+ break;
61
+ }
62
+
63
+ // Create forward action to route to internal email server ports
64
+ const defaultPortMapping: Record<number, number> = {
65
+ 25: 10025, // SMTP
66
+ 587: 10587, // Submission
67
+ 465: 10465 // SMTPS
68
+ };
69
+
70
+ const portMapping = this.dcRouterRef.options.emailPortConfig?.portMapping || defaultPortMapping;
71
+ const internalPort = portMapping[port] || port + 10000;
72
+
73
+ let action: any = {
74
+ type: 'forward',
75
+ sendProxyProtocol: true,
76
+ targets: [{
77
+ host: 'localhost', // Forward to internal email server
78
+ port: internalPort,
79
+ sendProxyProtocol: true,
80
+ }]
81
+ };
82
+
83
+ // Plain SMTP/STARTTLS ports must not carry TLS metadata, or SmartProxy waits for TLS/SNI first.
84
+ if (tlsMode === 'terminate') {
85
+ action.tls = {
86
+ mode: tlsMode,
87
+ certificate: 'auto',
88
+ };
89
+ }
90
+
91
+ // Create the route configuration
92
+ const routeConfig: IDcRouterRouteConfig = {
93
+ name: routeName,
94
+ match: {
95
+ ports: [port],
96
+ transport: 'tcp',
97
+ },
98
+ action: action
99
+ };
100
+
101
+ if (this.dcRouterRef.isRemoteIngressHubEnabled()) {
102
+ routeConfig.remoteIngress = { enabled: true };
103
+ const inboundProxyProtocol = this.getRemoteIngressEmailInboundProxyPolicy(port);
104
+ if (inboundProxyProtocol) {
105
+ routeConfig.match.inboundProxyProtocol = inboundProxyProtocol;
106
+ }
107
+ }
108
+
109
+ // Add the route to our list
110
+ emailRoutes.push(routeConfig);
111
+ }
112
+
113
+ return emailRoutes;
114
+ }
115
+
116
+ public getRuntimeEmailRoutes(emailRoutes: IDcRouterRouteConfig[]): plugins.smartproxy.IRouteConfig[] {
117
+ return emailRoutes.map((route) => this.createServerFirstEmailRuntimeRoute(route) || route);
118
+ }
119
+
120
+ /**
121
+ * Hydrate a persisted route into its runtime form: generated email routes
122
+ * get the server-first socket-handler treatment, DoH routes get the DNS
123
+ * socket handler. Returns undefined when the stored route runs as-is.
124
+ */
125
+ public hydrateStoredRouteForRuntime(storedRoute: IRoute): plugins.smartproxy.IRouteConfig | undefined {
126
+ const routeName = storedRoute.route.name || '';
127
+ const isDohRoute = storedRoute.origin === 'dns'
128
+ && storedRoute.route.action?.type === 'socket-handler'
129
+ && routeName.startsWith('dns-over-https-');
130
+
131
+ if (!isDohRoute) {
132
+ if (this.shouldHydrateGeneratedEmailRoute(storedRoute)) {
133
+ return this.createServerFirstEmailRuntimeRoute(storedRoute.route);
134
+ }
135
+ return undefined;
136
+ }
137
+
138
+ return {
139
+ ...storedRoute.route,
140
+ action: {
141
+ ...storedRoute.route.action,
142
+ type: 'socket-handler' as any,
143
+ socketHandler: this.dcRouterRef.dnsServerRuntime.createSocketHandler(),
144
+ } as any,
145
+ };
146
+ }
147
+
148
+ private getCurrentGeneratedEmailRouteNames(): Set<string> {
149
+ if (this.dcRouterRef.options.dbConfig?.enabled === false) {
150
+ return new Set();
151
+ }
152
+ const sourceRoutes = this.dcRouterRef.seedEmailRoutes.length > 0
153
+ ? this.dcRouterRef.seedEmailRoutes
154
+ : this.dcRouterRef.options.emailConfig
155
+ ? this.generateEmailRoutes(this.dcRouterRef.options.emailConfig)
156
+ : [];
157
+ return new Set(sourceRoutes.map((route) => route.name).filter(Boolean) as string[]);
158
+ }
159
+
160
+ private shouldHydrateGeneratedEmailRoute(storedRoute: IRoute): boolean {
161
+ if (storedRoute.origin !== 'email') {
162
+ return false;
163
+ }
164
+ const routeName = storedRoute.route.name;
165
+ if (!routeName || !this.getCurrentGeneratedEmailRouteNames().has(routeName)) {
166
+ return false;
167
+ }
168
+ const expectedSystemKey = `email:${routeName}`;
169
+ return !storedRoute.systemKey || storedRoute.systemKey === expectedSystemKey;
170
+ }
171
+
172
+ private createServerFirstEmailRuntimeRoute(
173
+ route: plugins.smartproxy.IRouteConfig,
174
+ ): plugins.smartproxy.IRouteConfig | undefined {
175
+ const action = route.action as any;
176
+ if (action?.type !== 'forward') {
177
+ return undefined;
178
+ }
179
+ const tlsMode = action.tls?.mode;
180
+ if (tlsMode === 'terminate-and-reencrypt') {
181
+ return undefined;
182
+ }
183
+ const routePorts = plugins.smartproxy.expandPortRange(route.match?.ports as any) as number[];
184
+ if (routePorts.length !== 1) {
185
+ return undefined;
186
+ }
187
+
188
+ const target = action.targets?.[0];
189
+ if (!target || action.targets.length !== 1 || typeof target.port !== 'number') {
190
+ return undefined;
191
+ }
192
+ if (typeof target.host !== 'string') {
193
+ return undefined;
194
+ }
195
+
196
+ const targetHost = target.host === 'localhost' ? '127.0.0.1' : target.host;
197
+ const inboundProxyProtocol = this.getRemoteIngressEmailInboundProxyPolicy(routePorts[0]);
198
+ return {
199
+ ...route,
200
+ match: {
201
+ ...route.match,
202
+ ...(inboundProxyProtocol
203
+ ? { inboundProxyProtocol }
204
+ : {}),
205
+ },
206
+ action: {
207
+ type: 'socket-handler' as any,
208
+ ...(action.tls
209
+ ? { tls: action.tls }
210
+ : {}),
211
+ socketHandler: this.createEmailSocketProxyHandler(targetHost, target.port),
212
+ } as any,
213
+ };
214
+ }
215
+
216
+ private getRemoteIngressEmailInboundProxyPolicy(
217
+ port: number,
218
+ ): TInboundProxyProtocolPolicy | undefined {
219
+ if (!this.dcRouterRef.isRemoteIngressHubEnabled()) {
220
+ return undefined;
221
+ }
222
+ return { mode: port === 25 || port === 587 ? 'required' : 'optional' };
223
+ }
224
+
225
+ private createEmailSocketProxyHandler(
226
+ targetHost: string,
227
+ targetPort: number,
228
+ ): NonNullable<plugins.smartproxy.IRouteConfig['action']['socketHandler']> {
229
+ return (clientSocket, context) => {
230
+ let backendSocket: plugins.net.Socket | undefined;
231
+ let connectTimeout: ReturnType<typeof setTimeout> & { unref?: () => void };
232
+ let cleanupDone = false;
233
+
234
+ const cleanup = () => {
235
+ if (cleanupDone) return;
236
+ cleanupDone = true;
237
+ clearTimeout(connectTimeout);
238
+ clientSocket.removeListener('timeout', cleanup);
239
+ clientSocket.removeListener('error', cleanup);
240
+ clientSocket.removeListener('end', cleanup);
241
+ clientSocket.removeListener('close', cleanup);
242
+ backendSocket?.removeListener('timeout', cleanup);
243
+ backendSocket?.removeListener('error', cleanup);
244
+ backendSocket?.removeListener('end', cleanup);
245
+ backendSocket?.removeListener('close', cleanup);
246
+ clientSocket.destroy();
247
+ backendSocket?.destroy();
248
+ };
249
+
250
+ connectTimeout = setTimeout(() => {
251
+ cleanup();
252
+ }, 30_000);
253
+ connectTimeout.unref?.();
254
+
255
+ clientSocket.setTimeout(300_000);
256
+ clientSocket.on('timeout', cleanup);
257
+ clientSocket.on('error', cleanup);
258
+ clientSocket.on('end', cleanup);
259
+ clientSocket.on('close', cleanup);
260
+
261
+ backendSocket = plugins.net.connect(targetPort, targetHost, () => {
262
+ clearTimeout(connectTimeout);
263
+ backendSocket?.setTimeout(300_000);
264
+ const proxyHeader = this.createProxyProtocolV1Header(
265
+ context?.clientIp,
266
+ targetHost,
267
+ 0,
268
+ targetPort,
269
+ );
270
+ if (!proxyHeader) {
271
+ cleanup();
272
+ return;
273
+ }
274
+ backendSocket!.write(proxyHeader, () => {
275
+ clientSocket.pipe(backendSocket!);
276
+ backendSocket!.pipe(clientSocket);
277
+ });
278
+ });
279
+ backendSocket.setTimeout(30_000);
280
+ backendSocket.on('timeout', cleanup);
281
+ backendSocket.on('error', cleanup);
282
+ backendSocket.on('end', cleanup);
283
+ backendSocket.on('close', cleanup);
284
+ };
285
+ }
286
+
287
+ private createProxyProtocolV1Header(
288
+ sourceIp: string | undefined,
289
+ destinationIp: string,
290
+ sourcePort: number,
291
+ destinationPort: number,
292
+ ): string | undefined {
293
+ if (!sourceIp || !plugins.net.isIP(sourceIp)) {
294
+ logger.log('warn', `Cannot create email PROXY protocol header for invalid source IP: ${sourceIp || 'unknown'}`);
295
+ return undefined;
296
+ }
297
+ const sourceFamily = plugins.net.isIP(sourceIp);
298
+ const destinationAddress = destinationIp === 'localhost' || destinationIp === '127.0.0.1' || destinationIp === '::1'
299
+ ? sourceFamily === 6 ? '::1' : '127.0.0.1'
300
+ : destinationIp;
301
+ const destinationFamily = plugins.net.isIP(destinationAddress);
302
+ if (!destinationFamily) {
303
+ logger.log('warn', `Cannot create email PROXY protocol header for invalid destination IP: ${destinationIp}`);
304
+ return undefined;
305
+ }
306
+ if (sourceFamily !== destinationFamily) {
307
+ return undefined;
308
+ }
309
+ const protocol = sourceFamily === 6 ? 'TCP6' : 'TCP4';
310
+ return `PROXY ${protocol} ${sourceIp} ${destinationAddress} ${sourcePort} ${destinationPort}\r\n`;
311
+ }
312
+ }
package/ts/email/index.ts CHANGED
@@ -1,4 +1,6 @@
1
+ export * from './classes.accepted-email-spool.js';
1
2
  export * from './classes.email-domain.manager.js';
3
+ export * from './classes.email-route-builder.js';
2
4
  export * from './classes.email-settings.manager.js';
3
5
  export * from './classes.smartmta-storage-manager.js';
4
6
  export * from './classes.workapp-mail-manager.js';
@@ -654,7 +654,7 @@ export class GatewayClientHandler {
654
654
 
655
655
  const sourceBindings = this.getManagedRouteSourceBindings();
656
656
  if (!sourceBindings) {
657
- return { success: false, message: 'STANDARD source profile not found' };
657
+ return { success: false, message: 'PUBLIC source profile not found' };
658
658
  }
659
659
 
660
660
  const metadata: interfaces.data.IRouteMetadata = {
@@ -709,16 +709,18 @@ export class GatewayClientHandler {
709
709
 
710
710
  private getManagedRouteSourceBindings(): interfaces.data.IRouteSourceBinding[] | undefined {
711
711
  const resolver = this.opsServerRef.dcRouterRef.referenceResolver;
712
- const standardProfile = resolver?.listProfiles().find((profile: interfaces.data.ISourceProfile) => {
713
- return profile.id.trim().toLowerCase() === 'standard'
714
- || profile.name.trim().toLowerCase() === 'standard';
712
+ const profiles = resolver?.listProfiles() || [];
713
+ const publicProfile = profiles.find((profile: interfaces.data.ISourceProfile) => {
714
+ return profile.id.trim().toLowerCase() === 'public';
715
+ }) || profiles.find((profile: interfaces.data.ISourceProfile) => {
716
+ return profile.name.trim().toLowerCase() === 'public';
715
717
  });
716
- if (!standardProfile) {
718
+ if (!publicProfile) {
717
719
  return undefined;
718
720
  }
719
721
  return [{
720
- sourceProfileRef: standardProfile.id,
721
- sourceProfileName: standardProfile.name,
722
+ sourceProfileRef: publicProfile.id,
723
+ sourceProfileName: publicProfile.name,
722
724
  }];
723
725
  }
724
726
  }
@@ -0,0 +1,278 @@
1
+ import * as plugins from '../plugins.js';
2
+ import { logger } from '../logger.js';
3
+ import { ProxyCertDoc } from '../db/index.js';
4
+ import { RemoteIngressManager, type IRemoteIngressFirewallConfig } from './classes.remoteingress-manager.js';
5
+ import { TunnelManager } from './classes.tunnel-manager.js';
6
+ import type { IDcRouterRouteConfig, IRemoteIngressHubSettings, TRemoteIngressHubSettingsUpdate } from '../../ts_interfaces/data/remoteingress.js';
7
+ import type { DcRouter } from '../classes.dcrouter.js';
8
+
9
+ /**
10
+ * Generation-guarded lifecycle for the RemoteIngress tunnel hub: serialized
11
+ * start/stop/restart of the Rust hub, edge mutations, route/firewall pushes,
12
+ * and hub-settings updates that may require a SmartProxy restart.
13
+ */
14
+ export class RemoteIngressHubLifecycle {
15
+ private lifecycleChain: Promise<void> = Promise.resolve();
16
+ private stopping = false;
17
+ private generation = 0;
18
+
19
+ constructor(private dcRouterRef: DcRouter) {}
20
+
21
+ public async setup(): Promise<void> {
22
+ const remoteIngressManager = this.dcRouterRef.remoteIngressManager;
23
+ if (!remoteIngressManager) {
24
+ return;
25
+ }
26
+
27
+ const hubSettings = remoteIngressManager.getHubSettings();
28
+ if (!hubSettings.enabled) {
29
+ logger.log('info', 'Remote Ingress hub is disabled in DB settings');
30
+ return;
31
+ }
32
+
33
+ logger.log('info', 'Setting up Remote Ingress hub...');
34
+ this.stopping = false;
35
+ const generation = ++this.generation;
36
+
37
+ const firewallConfig = await this.dcRouterRef.securityPolicyManager?.compileRemoteIngressFirewall();
38
+ if (!this.isGenerationCurrent(generation, remoteIngressManager)) {
39
+ return;
40
+ }
41
+ remoteIngressManager.setFirewallConfig(firewallConfig);
42
+
43
+ // Pass current bootstrap routes so the manager can derive edge ports initially.
44
+ // Once RouteConfigManager applies the full DB set, the onRoutesApplied callback
45
+ // will push the complete merged routes here.
46
+ remoteIngressManager.setRoutes(this.dcRouterRef.getRemoteIngressBootstrapRoutes() as any[]);
47
+
48
+ // If ConfigManagers finished before us, re-apply routes
49
+ // so the callback delivers the full DB set to our newly-created remoteIngressManager.
50
+ if (this.dcRouterRef.routeConfigManager) {
51
+ await this.dcRouterRef.routeConfigManager.applyRoutes();
52
+ }
53
+ if (!this.isGenerationCurrent(generation, remoteIngressManager)) {
54
+ return;
55
+ }
56
+
57
+ await this.queueTask(async () => {
58
+ await this.startTunnelHubLocked(generation);
59
+ });
60
+ if (!this.isGenerationCurrent(generation, remoteIngressManager)) {
61
+ return;
62
+ }
63
+
64
+ const edgeCount = remoteIngressManager.getAllEdges().length;
65
+ logger.log('info', `Remote Ingress hub started on port ${hubSettings.tunnelPort} with ${edgeCount} registered edge(s)`);
66
+ }
67
+
68
+ public async stop(): Promise<void> {
69
+ this.stopping = true;
70
+ this.generation++;
71
+ await this.queueTask(async () => {
72
+ const currentTunnelManager = this.dcRouterRef.tunnelManager;
73
+ if (currentTunnelManager) {
74
+ await currentTunnelManager.stop();
75
+ if (this.dcRouterRef.tunnelManager === currentTunnelManager) {
76
+ this.dcRouterRef.tunnelManager = undefined;
77
+ }
78
+ }
79
+ });
80
+ }
81
+
82
+ public async mutateEdges<T>(
83
+ mutation: (manager: RemoteIngressManager) => Promise<T>,
84
+ syncAllowedEdges = true,
85
+ ): Promise<T> {
86
+ return await this.queueTask(async () => {
87
+ if (this.stopping) {
88
+ throw new Error('RemoteIngress is stopping');
89
+ }
90
+ const manager = this.dcRouterRef.remoteIngressManager;
91
+ if (!manager) {
92
+ throw new Error('RemoteIngress not configured');
93
+ }
94
+ const result = await mutation(manager);
95
+ if (syncAllowedEdges && this.dcRouterRef.tunnelManager) {
96
+ await this.dcRouterRef.tunnelManager.syncAllowedEdges();
97
+ }
98
+ return result;
99
+ });
100
+ }
101
+
102
+ public async updateRoutes(routes: IDcRouterRouteConfig[]): Promise<void> {
103
+ await this.queueTask(async () => {
104
+ if (this.stopping) return;
105
+ if (this.dcRouterRef.remoteIngressManager) {
106
+ this.dcRouterRef.remoteIngressManager.setRoutes(routes);
107
+ }
108
+ if (this.dcRouterRef.tunnelManager) {
109
+ await this.dcRouterRef.tunnelManager.syncAllowedEdges();
110
+ }
111
+ });
112
+ }
113
+
114
+ public async applyFirewallConfig(firewallConfig: IRemoteIngressFirewallConfig | undefined): Promise<void> {
115
+ await this.queueTask(async () => {
116
+ if (this.stopping) return;
117
+ if (this.dcRouterRef.remoteIngressManager) {
118
+ this.dcRouterRef.remoteIngressManager.setFirewallConfig(firewallConfig);
119
+ }
120
+ if (this.dcRouterRef.tunnelManager) {
121
+ await this.dcRouterRef.tunnelManager.syncAllowedEdges();
122
+ }
123
+ });
124
+ }
125
+
126
+ public async updateHubSettings(
127
+ updates: TRemoteIngressHubSettingsUpdate,
128
+ updatedBy: string,
129
+ ): Promise<IRemoteIngressHubSettings> {
130
+ const manager = this.dcRouterRef.remoteIngressManager;
131
+ if (!manager) {
132
+ throw new Error('RemoteIngress is not configured');
133
+ }
134
+
135
+ const previousSettings = manager.getHubSettings();
136
+ const settings = await manager.updateHubSettings(updates, updatedBy);
137
+ const enabledChanged = previousSettings.enabled !== settings.enabled;
138
+
139
+ if (!settings.enabled) {
140
+ await this.queueTask(async () => {
141
+ await this.stopTunnelHubLocked();
142
+ });
143
+ }
144
+
145
+ if (enabledChanged) {
146
+ await this.dcRouterRef.restartSmartProxyForRemoteIngressSettings();
147
+ }
148
+
149
+ if (settings.enabled) {
150
+ await this.queueTask(async () => {
151
+ await this.restartTunnelHubLocked();
152
+ });
153
+ }
154
+
155
+ return settings;
156
+ }
157
+
158
+ private isGenerationCurrent(generation: number, manager: RemoteIngressManager): boolean {
159
+ return !this.stopping
160
+ && generation === this.generation
161
+ && this.dcRouterRef.remoteIngressManager === manager;
162
+ }
163
+
164
+ private queueTask<T>(task: () => Promise<T>): Promise<T> {
165
+ const run = this.lifecycleChain.then(task);
166
+ this.lifecycleChain = run.then(() => undefined, () => undefined);
167
+ return run;
168
+ }
169
+
170
+ private async stopTunnelHubLocked(): Promise<void> {
171
+ this.generation++;
172
+ const currentTunnelManager = this.dcRouterRef.tunnelManager;
173
+ if (currentTunnelManager) {
174
+ await currentTunnelManager.stop();
175
+ if (this.dcRouterRef.tunnelManager === currentTunnelManager) {
176
+ this.dcRouterRef.tunnelManager = undefined;
177
+ }
178
+ }
179
+ }
180
+
181
+ private async restartTunnelHubLocked(): Promise<void> {
182
+ const generation = ++this.generation;
183
+ const hubSettings = this.dcRouterRef.remoteIngressManager?.getHubSettings();
184
+ if (!this.dcRouterRef.remoteIngressManager || !hubSettings?.enabled || this.stopping) {
185
+ return;
186
+ }
187
+
188
+ const currentTunnelManager = this.dcRouterRef.tunnelManager;
189
+ if (currentTunnelManager) {
190
+ await currentTunnelManager.stop();
191
+ if (this.dcRouterRef.tunnelManager === currentTunnelManager) {
192
+ this.dcRouterRef.tunnelManager = undefined;
193
+ }
194
+ }
195
+
196
+ if (this.stopping || generation !== this.generation) {
197
+ return;
198
+ }
199
+ await this.startTunnelHubLocked(generation);
200
+ }
201
+
202
+ private async startTunnelHubLocked(generation: number): Promise<void> {
203
+ const manager = this.dcRouterRef.remoteIngressManager;
204
+ const hubSettings = manager?.getHubSettings();
205
+ if (!manager || !hubSettings?.enabled || this.stopping || generation !== this.generation) {
206
+ return;
207
+ }
208
+
209
+ const firewallConfig = await this.dcRouterRef.securityPolicyManager?.compileRemoteIngressFirewall();
210
+ if (this.stopping || generation !== this.generation || this.dcRouterRef.remoteIngressManager !== manager) {
211
+ return;
212
+ }
213
+ manager.setFirewallConfig(firewallConfig);
214
+
215
+ const tlsConfig = await this.resolveTlsConfig(hubSettings.hubDomain);
216
+ if (this.stopping || generation !== this.generation || this.dcRouterRef.remoteIngressManager !== manager) {
217
+ return;
218
+ }
219
+
220
+ const tunnelManager = new TunnelManager(manager, {
221
+ tunnelPort: hubSettings.tunnelPort,
222
+ targetHost: '127.0.0.1',
223
+ tls: tlsConfig,
224
+ performance: manager.getHubPerformanceConfig(),
225
+ });
226
+ try {
227
+ await tunnelManager.start();
228
+ } catch (err) {
229
+ await tunnelManager.stop().catch(() => {});
230
+ throw err;
231
+ }
232
+
233
+ if (this.stopping || generation !== this.generation || this.dcRouterRef.remoteIngressManager !== manager) {
234
+ await tunnelManager.stop().catch((err) => {
235
+ logger.log('warn', `Failed to stop stale RemoteIngress tunnel hub: ${(err as Error).message}`);
236
+ });
237
+ return;
238
+ }
239
+ this.dcRouterRef.tunnelManager = tunnelManager;
240
+ }
241
+
242
+ private async resolveTlsConfig(
243
+ hubDomain?: string,
244
+ ): Promise<{ certPem: string; keyPem: string } | undefined> {
245
+ // Resolve TLS certs for tunnel: explicit paths > ACME for hubDomain > self-signed (Rust default)
246
+ let tlsConfig: { certPem: string; keyPem: string } | undefined;
247
+
248
+ // Priority 1: Explicit cert/key file paths
249
+ const explicitTls = this.dcRouterRef.options.remoteIngressConfig?.tls;
250
+ if (explicitTls?.certPath && explicitTls?.keyPath) {
251
+ try {
252
+ const certPem = plugins.fs.readFileSync(explicitTls.certPath, 'utf8');
253
+ const keyPem = plugins.fs.readFileSync(explicitTls.keyPath, 'utf8');
254
+ tlsConfig = { certPem, keyPem };
255
+ logger.log('info', 'Using explicit TLS cert/key for RemoteIngress tunnel');
256
+ } catch (err: unknown) {
257
+ logger.log('warn', `Failed to read RemoteIngress TLS cert/key files: ${(err as Error).message}`);
258
+ }
259
+ }
260
+
261
+ // Priority 2: Existing cert from SmartProxy cert store for hubDomain
262
+ if (!tlsConfig && hubDomain) {
263
+ try {
264
+ const stored = await ProxyCertDoc.findByDomain(hubDomain);
265
+ if (stored?.publicKey && stored?.privateKey) {
266
+ tlsConfig = { certPem: stored.publicKey, keyPem: stored.privateKey };
267
+ logger.log('info', `Using stored ACME cert for RemoteIngress tunnel TLS: ${hubDomain}`);
268
+ }
269
+ } catch { /* no stored cert, fall through */ }
270
+ }
271
+
272
+ if (!tlsConfig) {
273
+ logger.log('info', 'No TLS cert configured for RemoteIngress tunnel — using auto-generated self-signed');
274
+ }
275
+
276
+ return tlsConfig;
277
+ }
278
+ }
@@ -2,7 +2,7 @@ import * as plugins from '../plugins.js';
2
2
  import type { IDcRouterRouteConfig, IRemoteIngress, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig, TRemoteIngressHubSettingsUpdate, TRemoteIngressPerformanceProfile } from '../../ts_interfaces/data/remoteingress.js';
3
3
  import { RemoteIngressEdgeDoc, RemoteIngressHubSettingsDoc } from '../db/index.js';
4
4
 
5
- interface IRemoteIngressFirewallConfig {
5
+ export interface IRemoteIngressFirewallConfig {
6
6
  blockedIps?: string[];
7
7
  }
8
8
 
@@ -1,2 +1,3 @@
1
1
  export * from './classes.remoteingress-manager.js';
2
2
  export * from './classes.tunnel-manager.js';
3
+ export * from './classes.hub-lifecycle.js';