@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.
- package/deno.json +1 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/acme/classes.smartacme-lifecycle.d.ts +25 -0
- package/dist_ts/acme/classes.smartacme-lifecycle.js +144 -0
- package/dist_ts/acme/index.d.ts +1 -0
- package/dist_ts/acme/index.js +2 -1
- package/dist_ts/classes.dcrouter.d.ts +21 -139
- package/dist_ts/classes.dcrouter.js +71 -1585
- package/dist_ts/dns/classes.dns-server-runtime.d.ts +37 -0
- package/dist_ts/dns/classes.dns-server-runtime.js +449 -0
- package/dist_ts/dns/index.d.ts +1 -0
- package/dist_ts/dns/index.js +2 -1
- package/dist_ts/email/classes.accepted-email-spool.d.ts +55 -0
- package/dist_ts/email/classes.accepted-email-spool.js +345 -0
- package/dist_ts/email/classes.email-route-builder.d.ts +28 -0
- package/dist_ts/email/classes.email-route-builder.js +260 -0
- package/dist_ts/email/index.d.ts +2 -0
- package/dist_ts/email/index.js +3 -1
- package/dist_ts/opsserver/handlers/gatewayclient.handler.js +10 -8
- package/dist_ts/remoteingress/classes.hub-lifecycle.d.ts +27 -0
- package/dist_ts/remoteingress/classes.hub-lifecycle.js +241 -0
- package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +1 -2
- package/dist_ts/remoteingress/index.d.ts +1 -0
- package/dist_ts/remoteingress/index.js +2 -1
- package/dist_ts/security/classes.route-policy-augmenter.d.ts +22 -0
- package/dist_ts/security/classes.route-policy-augmenter.js +120 -0
- package/dist_ts/security/index.d.ts +1 -0
- package/dist_ts/security/index.js +2 -1
- package/dist_ts/vpn/classes.vpn-access-resolver.d.ts +34 -0
- package/dist_ts/vpn/classes.vpn-access-resolver.js +101 -0
- package/dist_ts/vpn/index.d.ts +1 -0
- package/dist_ts/vpn/index.js +2 -1
- package/dist_ts_migrations/index.js +92 -9
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/package.json +2 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/acme/classes.smartacme-lifecycle.ts +155 -0
- package/ts/acme/index.ts +1 -0
- package/ts/classes.dcrouter.ts +118 -1919
- package/ts/dns/classes.dns-server-runtime.ts +525 -0
- package/ts/dns/index.ts +1 -0
- package/ts/email/classes.accepted-email-spool.ts +434 -0
- package/ts/email/classes.email-route-builder.ts +312 -0
- package/ts/email/index.ts +2 -0
- package/ts/opsserver/handlers/gatewayclient.handler.ts +9 -7
- package/ts/remoteingress/classes.hub-lifecycle.ts +278 -0
- package/ts/remoteingress/classes.remoteingress-manager.ts +1 -1
- package/ts/remoteingress/index.ts +1 -0
- package/ts/security/classes.route-policy-augmenter.ts +140 -0
- package/ts/security/index.ts +1 -0
- package/ts/vpn/classes.vpn-access-resolver.ts +126 -0
- package/ts/vpn/index.ts +1 -0
- 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: '
|
|
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
|
|
713
|
-
|
|
714
|
-
|
|
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 (!
|
|
718
|
+
if (!publicProfile) {
|
|
717
719
|
return undefined;
|
|
718
720
|
}
|
|
719
721
|
return [{
|
|
720
|
-
sourceProfileRef:
|
|
721
|
-
sourceProfileName:
|
|
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
|
|