@serve.zone/dcrouter 13.40.2 → 13.41.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 +3 -3
- package/dist_serve/bundle.js +590 -546
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +15 -0
- package/dist_ts/classes.dcrouter.js +156 -36
- package/dist_ts/db/documents/classes.remote-ingress-hub-settings.doc.d.ts +10 -0
- package/dist_ts/db/documents/classes.remote-ingress-hub-settings.doc.js +88 -0
- package/dist_ts/db/documents/index.d.ts +1 -0
- package/dist_ts/db/documents/index.js +2 -1
- package/dist_ts/opsserver/handlers/config.handler.js +2 -2
- package/dist_ts/opsserver/handlers/remoteingress.handler.js +71 -55
- package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +12 -2
- package/dist_ts/remoteingress/classes.remoteingress-manager.js +136 -3
- package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +2 -0
- package/dist_ts/remoteingress/classes.tunnel-manager.js +45 -13
- package/dist_ts_interfaces/data/remoteingress.d.ts +5 -0
- package/dist_ts_interfaces/requests/remoteingress.d.ts +30 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +4 -0
- package/dist_ts_web/appstate.js +30 -2
- package/dist_ts_web/elements/network/ops-view-remoteingress.d.ts +4 -0
- package/dist_ts_web/elements/network/ops-view-remoteingress.js +148 -1
- package/package.json +3 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +178 -38
- package/ts/db/documents/classes.remote-ingress-hub-settings.doc.ts +29 -0
- package/ts/db/documents/index.ts +1 -0
- package/ts/opsserver/handlers/config.handler.ts +1 -1
- package/ts/opsserver/handlers/remoteingress.handler.ts +97 -74
- package/ts/remoteingress/classes.remoteingress-manager.ts +172 -3
- package/ts/remoteingress/classes.tunnel-manager.ts +41 -14
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +41 -1
- package/ts_web/elements/network/ops-view-remoteingress.ts +164 -0
|
@@ -208,7 +208,7 @@ export class ConfigHandler {
|
|
|
208
208
|
hubDomain: riCfg?.hubDomain || null,
|
|
209
209
|
tlsMode,
|
|
210
210
|
connectedEdgeIps,
|
|
211
|
-
performance: riCfg?.performance,
|
|
211
|
+
performance: dcRouter.remoteIngressManager?.getHubPerformanceConfig() || riCfg?.performance,
|
|
212
212
|
};
|
|
213
213
|
|
|
214
214
|
return {
|
|
@@ -52,30 +52,21 @@ export class RemoteIngressHandler {
|
|
|
52
52
|
scope: 'remote-ingress:write',
|
|
53
53
|
requireAdminIdentity: true,
|
|
54
54
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
try {
|
|
56
|
+
const edge = await this.opsServerRef.dcRouterRef.mutateRemoteIngressEdges((manager) => manager.createEdge(
|
|
57
|
+
dataArg.name,
|
|
58
|
+
dataArg.listenPorts || [],
|
|
59
|
+
dataArg.tags,
|
|
60
|
+
dataArg.autoDerivePorts ?? true,
|
|
61
|
+
dataArg.performance,
|
|
62
|
+
));
|
|
63
|
+
return { success: true, edge };
|
|
64
|
+
} catch (err: unknown) {
|
|
59
65
|
return {
|
|
60
66
|
success: false,
|
|
61
67
|
edge: null as any,
|
|
62
68
|
};
|
|
63
69
|
}
|
|
64
|
-
|
|
65
|
-
const edge = await manager.createEdge(
|
|
66
|
-
dataArg.name,
|
|
67
|
-
dataArg.listenPorts || [],
|
|
68
|
-
dataArg.tags,
|
|
69
|
-
dataArg.autoDerivePorts ?? true,
|
|
70
|
-
dataArg.performance,
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
// Sync allowed edges with the hub
|
|
74
|
-
if (tunnelManager) {
|
|
75
|
-
await tunnelManager.syncAllowedEdges();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return { success: true, edge };
|
|
79
70
|
},
|
|
80
71
|
),
|
|
81
72
|
);
|
|
@@ -89,21 +80,18 @@ export class RemoteIngressHandler {
|
|
|
89
80
|
scope: 'remote-ingress:write',
|
|
90
81
|
requireAdminIdentity: true,
|
|
91
82
|
});
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (deleted && tunnelManager) {
|
|
101
|
-
await tunnelManager.syncAllowedEdges();
|
|
102
|
-
}
|
|
83
|
+
const deleted = await this.opsServerRef.dcRouterRef.mutateRemoteIngressEdges(
|
|
84
|
+
(manager) => manager.deleteEdge(dataArg.id),
|
|
85
|
+
).catch((err: unknown) => {
|
|
86
|
+
if ((err as Error).message.includes('RemoteIngress')) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
throw err;
|
|
90
|
+
});
|
|
103
91
|
|
|
104
92
|
return {
|
|
105
93
|
success: deleted,
|
|
106
|
-
message: deleted ? undefined : 'Edge not found',
|
|
94
|
+
message: deleted ? undefined : 'Edge not found or RemoteIngress not configured',
|
|
107
95
|
};
|
|
108
96
|
},
|
|
109
97
|
),
|
|
@@ -118,42 +106,42 @@ export class RemoteIngressHandler {
|
|
|
118
106
|
scope: 'remote-ingress:write',
|
|
119
107
|
requireAdminIdentity: true,
|
|
120
108
|
});
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (!edge) {
|
|
138
|
-
return { success: false, edge: null as any };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Sync allowed edges — ports, tags, or enabled may have changed
|
|
142
|
-
if (tunnelManager) {
|
|
143
|
-
await tunnelManager.syncAllowedEdges();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const breakdown = manager.getPortBreakdown(edge);
|
|
147
|
-
return {
|
|
148
|
-
success: true,
|
|
149
|
-
edge: {
|
|
109
|
+
const result = await this.opsServerRef.dcRouterRef.mutateRemoteIngressEdges(async (manager) => {
|
|
110
|
+
const edge = await manager.updateEdge(dataArg.id, {
|
|
111
|
+
name: dataArg.name,
|
|
112
|
+
listenPorts: dataArg.listenPorts,
|
|
113
|
+
autoDerivePorts: dataArg.autoDerivePorts,
|
|
114
|
+
enabled: dataArg.enabled,
|
|
115
|
+
performance: dataArg.performance,
|
|
116
|
+
tags: dataArg.tags,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (!edge) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const breakdown = manager.getPortBreakdown(edge);
|
|
124
|
+
return {
|
|
150
125
|
...edge,
|
|
151
126
|
secret: '********',
|
|
152
127
|
effectiveListenPorts: manager.getEffectiveListenPorts(edge),
|
|
153
128
|
effectiveListenPortsUdp: manager.getEffectiveListenPortsUdp(edge),
|
|
154
129
|
manualPorts: breakdown.manual,
|
|
155
130
|
derivedPorts: breakdown.derived,
|
|
156
|
-
}
|
|
131
|
+
};
|
|
132
|
+
}).catch((err: unknown) => {
|
|
133
|
+
if ((err as Error).message.includes('RemoteIngress')) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
throw err;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!result) {
|
|
140
|
+
return { success: false, edge: null as any };
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
success: true,
|
|
144
|
+
edge: result,
|
|
157
145
|
};
|
|
158
146
|
},
|
|
159
147
|
),
|
|
@@ -168,23 +156,18 @@ export class RemoteIngressHandler {
|
|
|
168
156
|
scope: 'remote-ingress:write',
|
|
169
157
|
requireAdminIdentity: true,
|
|
170
158
|
});
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
159
|
+
const secret = await this.opsServerRef.dcRouterRef.mutateRemoteIngressEdges(
|
|
160
|
+
(manager) => manager.regenerateSecret(dataArg.id),
|
|
161
|
+
).catch((err: unknown) => {
|
|
162
|
+
if ((err as Error).message.includes('RemoteIngress')) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
throw err;
|
|
166
|
+
});
|
|
179
167
|
if (!secret) {
|
|
180
168
|
return { success: false, secret: '' };
|
|
181
169
|
}
|
|
182
170
|
|
|
183
|
-
// Sync allowed edges since secret changed
|
|
184
|
-
if (tunnelManager) {
|
|
185
|
-
await tunnelManager.syncAllowedEdges();
|
|
186
|
-
}
|
|
187
|
-
|
|
188
171
|
return { success: true, secret };
|
|
189
172
|
},
|
|
190
173
|
),
|
|
@@ -205,6 +188,46 @@ export class RemoteIngressHandler {
|
|
|
205
188
|
),
|
|
206
189
|
);
|
|
207
190
|
|
|
191
|
+
// Get hub-level settings (read)
|
|
192
|
+
viewRouter.addTypedHandler(
|
|
193
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressHubSettings>(
|
|
194
|
+
'getRemoteIngressHubSettings',
|
|
195
|
+
async (dataArg, toolsArg) => {
|
|
196
|
+
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'remote-ingress:read' });
|
|
197
|
+
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
|
198
|
+
return {
|
|
199
|
+
settings: manager?.getHubSettings() || {
|
|
200
|
+
updatedAt: 0,
|
|
201
|
+
updatedBy: 'default',
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Update hub-level settings (write)
|
|
209
|
+
adminRouter.addTypedHandler(
|
|
210
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateRemoteIngressHubSettings>(
|
|
211
|
+
'updateRemoteIngressHubSettings',
|
|
212
|
+
async (dataArg, toolsArg) => {
|
|
213
|
+
const auth = await requireOpsAuth(this.opsServerRef, dataArg, {
|
|
214
|
+
scope: 'remote-ingress:write',
|
|
215
|
+
requireAdminIdentity: true,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const settings = await this.opsServerRef.dcRouterRef.updateRemoteIngressHubSettings(
|
|
220
|
+
{ performance: dataArg.performance },
|
|
221
|
+
auth.userId,
|
|
222
|
+
);
|
|
223
|
+
return { success: true, settings };
|
|
224
|
+
} catch (err: unknown) {
|
|
225
|
+
return { success: false, message: (err as Error).message };
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
|
|
208
231
|
// Get a connection token for an edge (write — exposes secret)
|
|
209
232
|
adminRouter.addTypedHandler(
|
|
210
233
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressConnectionToken>(
|
|
@@ -1,11 +1,36 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
|
-
import type { IRemoteIngress, IRemoteIngressPerformanceConfig,
|
|
3
|
-
import { RemoteIngressEdgeDoc } from '../db/index.js';
|
|
2
|
+
import type { IDcRouterRouteConfig, IRemoteIngress, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig, TRemoteIngressPerformanceProfile } from '../../ts_interfaces/data/remoteingress.js';
|
|
3
|
+
import { RemoteIngressEdgeDoc, RemoteIngressHubSettingsDoc } from '../db/index.js';
|
|
4
4
|
|
|
5
5
|
interface IRemoteIngressFirewallConfig {
|
|
6
6
|
blockedIps?: string[];
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
type TPerformanceIntegerField =
|
|
10
|
+
| 'maxStreamsPerEdge'
|
|
11
|
+
| 'totalWindowBudgetBytes'
|
|
12
|
+
| 'minStreamWindowBytes'
|
|
13
|
+
| 'maxStreamWindowBytes'
|
|
14
|
+
| 'sustainedStreamWindowBytes'
|
|
15
|
+
| 'quicDatagramReceiveBufferBytes'
|
|
16
|
+
| 'streamFramePayloadBytes'
|
|
17
|
+
| 'firstDataConnectTimeoutMs'
|
|
18
|
+
| 'clientWriteTimeoutMs';
|
|
19
|
+
|
|
20
|
+
const performanceIntegerMaxByField: Record<TPerformanceIntegerField, number> = {
|
|
21
|
+
maxStreamsPerEdge: 100_000,
|
|
22
|
+
totalWindowBudgetBytes: 1_073_741_824,
|
|
23
|
+
minStreamWindowBytes: 16_777_216,
|
|
24
|
+
maxStreamWindowBytes: 134_217_728,
|
|
25
|
+
sustainedStreamWindowBytes: 134_217_728,
|
|
26
|
+
quicDatagramReceiveBufferBytes: 67_108_864,
|
|
27
|
+
streamFramePayloadBytes: 16_777_216,
|
|
28
|
+
firstDataConnectTimeoutMs: 3_600_000,
|
|
29
|
+
clientWriteTimeoutMs: 3_600_000,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const maxServerFirstPorts = 128;
|
|
33
|
+
|
|
9
34
|
function extractPorts(portRange: plugins.smartproxy.IRouteConfig['match']['ports']): number[] {
|
|
10
35
|
const ports = new Set<number>(plugins.smartproxy.expandPortRange(portRange) as number[]);
|
|
11
36
|
return [...ports].sort((a, b) => a - b);
|
|
@@ -20,8 +45,12 @@ export class RemoteIngressManager {
|
|
|
20
45
|
private edges: Map<string, IRemoteIngress> = new Map();
|
|
21
46
|
private routes: IDcRouterRouteConfig[] = [];
|
|
22
47
|
private firewallConfig?: IRemoteIngressFirewallConfig;
|
|
48
|
+
private hubSettings: IRemoteIngressHubSettings = {
|
|
49
|
+
updatedAt: 0,
|
|
50
|
+
updatedBy: 'default',
|
|
51
|
+
};
|
|
23
52
|
|
|
24
|
-
constructor() {
|
|
53
|
+
constructor(private seedHubPerformance?: IRemoteIngressPerformanceConfig) {
|
|
25
54
|
}
|
|
26
55
|
|
|
27
56
|
/**
|
|
@@ -50,6 +79,28 @@ export class RemoteIngressManager {
|
|
|
50
79
|
};
|
|
51
80
|
this.edges.set(edge.id, edge);
|
|
52
81
|
}
|
|
82
|
+
|
|
83
|
+
await this.initializeHubSettings();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async initializeHubSettings(): Promise<void> {
|
|
87
|
+
let doc = await RemoteIngressHubSettingsDoc.load();
|
|
88
|
+
if (!doc) {
|
|
89
|
+
const seedPerformance = this.normalizePerformanceConfig(this.seedHubPerformance);
|
|
90
|
+
if (seedPerformance) {
|
|
91
|
+
doc = new RemoteIngressHubSettingsDoc();
|
|
92
|
+
doc.settingsId = 'remote-ingress-hub-settings';
|
|
93
|
+
doc.performance = seedPerformance;
|
|
94
|
+
doc.updatedAt = Date.now();
|
|
95
|
+
doc.updatedBy = 'seed';
|
|
96
|
+
await doc.save();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.hubSettings = doc ? this.toHubSettings(doc) : {
|
|
101
|
+
updatedAt: 0,
|
|
102
|
+
updatedBy: 'default',
|
|
103
|
+
};
|
|
53
104
|
}
|
|
54
105
|
|
|
55
106
|
/**
|
|
@@ -66,6 +117,38 @@ export class RemoteIngressManager {
|
|
|
66
117
|
this.firewallConfig = firewallConfig;
|
|
67
118
|
}
|
|
68
119
|
|
|
120
|
+
public getHubSettings(): IRemoteIngressHubSettings {
|
|
121
|
+
return {
|
|
122
|
+
...this.hubSettings,
|
|
123
|
+
performance: this.hubSettings.performance ? { ...this.hubSettings.performance } : undefined,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public getHubPerformanceConfig(): IRemoteIngressPerformanceConfig | undefined {
|
|
128
|
+
return this.hubSettings.performance && Object.keys(this.hubSettings.performance).length > 0
|
|
129
|
+
? { ...this.hubSettings.performance }
|
|
130
|
+
: undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public async updateHubSettings(
|
|
134
|
+
updates: { performance?: IRemoteIngressPerformanceConfig },
|
|
135
|
+
updatedBy: string,
|
|
136
|
+
): Promise<IRemoteIngressHubSettings> {
|
|
137
|
+
let doc = await RemoteIngressHubSettingsDoc.load();
|
|
138
|
+
if (!doc) {
|
|
139
|
+
doc = new RemoteIngressHubSettingsDoc();
|
|
140
|
+
doc.settingsId = 'remote-ingress-hub-settings';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
doc.performance = this.normalizePerformanceConfig(updates.performance);
|
|
144
|
+
doc.updatedAt = Date.now();
|
|
145
|
+
doc.updatedBy = updatedBy;
|
|
146
|
+
await doc.save();
|
|
147
|
+
|
|
148
|
+
this.hubSettings = this.toHubSettings(doc);
|
|
149
|
+
return this.getHubSettings();
|
|
150
|
+
}
|
|
151
|
+
|
|
69
152
|
/**
|
|
70
153
|
* Derive listen ports for an edge from routes tagged with remoteIngress.enabled.
|
|
71
154
|
* When a route specifies edgeFilter, only edges whose id or tags match get that route's ports.
|
|
@@ -324,4 +407,90 @@ export class RemoteIngressManager {
|
|
|
324
407
|
}
|
|
325
408
|
return result;
|
|
326
409
|
}
|
|
410
|
+
|
|
411
|
+
private normalizePerformanceConfig(
|
|
412
|
+
performance?: IRemoteIngressPerformanceConfig,
|
|
413
|
+
): IRemoteIngressPerformanceConfig | undefined {
|
|
414
|
+
if (!performance) {
|
|
415
|
+
return undefined;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const next: IRemoteIngressPerformanceConfig = {};
|
|
419
|
+
const validProfiles: TRemoteIngressPerformanceProfile[] = ['balanced', 'throughput', 'highConcurrency'];
|
|
420
|
+
if (performance.profile !== undefined) {
|
|
421
|
+
if (!validProfiles.includes(performance.profile)) {
|
|
422
|
+
throw new Error('Invalid RemoteIngress performance profile');
|
|
423
|
+
}
|
|
424
|
+
next.profile = performance.profile;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const assignPositiveInteger = (field: TPerformanceIntegerField) => {
|
|
428
|
+
const value = performance[field];
|
|
429
|
+
if (value === undefined) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const maxValue = performanceIntegerMaxByField[field];
|
|
433
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > maxValue) {
|
|
434
|
+
throw new Error(`${field} must be a positive safe integer no greater than ${maxValue}`);
|
|
435
|
+
}
|
|
436
|
+
(next as Record<string, number>)[field] = value;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
assignPositiveInteger('maxStreamsPerEdge');
|
|
440
|
+
assignPositiveInteger('totalWindowBudgetBytes');
|
|
441
|
+
assignPositiveInteger('minStreamWindowBytes');
|
|
442
|
+
assignPositiveInteger('maxStreamWindowBytes');
|
|
443
|
+
assignPositiveInteger('sustainedStreamWindowBytes');
|
|
444
|
+
assignPositiveInteger('quicDatagramReceiveBufferBytes');
|
|
445
|
+
assignPositiveInteger('streamFramePayloadBytes');
|
|
446
|
+
assignPositiveInteger('firstDataConnectTimeoutMs');
|
|
447
|
+
assignPositiveInteger('clientWriteTimeoutMs');
|
|
448
|
+
|
|
449
|
+
if (
|
|
450
|
+
next.minStreamWindowBytes !== undefined
|
|
451
|
+
&& next.maxStreamWindowBytes !== undefined
|
|
452
|
+
&& next.minStreamWindowBytes > next.maxStreamWindowBytes
|
|
453
|
+
) {
|
|
454
|
+
throw new Error('minStreamWindowBytes must not exceed maxStreamWindowBytes');
|
|
455
|
+
}
|
|
456
|
+
if (
|
|
457
|
+
next.sustainedStreamWindowBytes !== undefined
|
|
458
|
+
&& next.maxStreamWindowBytes !== undefined
|
|
459
|
+
&& next.sustainedStreamWindowBytes > next.maxStreamWindowBytes
|
|
460
|
+
) {
|
|
461
|
+
throw new Error('sustainedStreamWindowBytes must not exceed maxStreamWindowBytes');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const configuredServerFirstPorts = performance.serverFirstPorts;
|
|
465
|
+
if (configuredServerFirstPorts !== undefined) {
|
|
466
|
+
if (!Array.isArray(configuredServerFirstPorts)) {
|
|
467
|
+
throw new Error('serverFirstPorts must contain valid port numbers');
|
|
468
|
+
}
|
|
469
|
+
if (configuredServerFirstPorts.length > maxServerFirstPorts) {
|
|
470
|
+
throw new Error(`serverFirstPorts must contain at most ${maxServerFirstPorts} ports`);
|
|
471
|
+
}
|
|
472
|
+
const serverFirstPorts = [...new Set(configuredServerFirstPorts.map((port) => Number(port)))].sort((a, b) => a - b);
|
|
473
|
+
for (const port of serverFirstPorts) {
|
|
474
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
475
|
+
throw new Error('serverFirstPorts must contain valid port numbers');
|
|
476
|
+
}
|
|
477
|
+
if (port === 443) {
|
|
478
|
+
throw new Error('Port 443 is client-first TLS and must not be listed as server-first');
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (serverFirstPorts.length > 0) {
|
|
482
|
+
next.serverFirstPorts = serverFirstPorts;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return Object.keys(next).length > 0 ? next : undefined;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private toHubSettings(doc: RemoteIngressHubSettingsDoc): IRemoteIngressHubSettings {
|
|
490
|
+
return {
|
|
491
|
+
performance: doc.performance,
|
|
492
|
+
updatedAt: doc.updatedAt,
|
|
493
|
+
updatedBy: doc.updatedBy,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
327
496
|
}
|
|
@@ -22,6 +22,8 @@ export class TunnelManager {
|
|
|
22
22
|
private edgeStatuses: Map<string, IRemoteIngressStatus> = new Map();
|
|
23
23
|
private reconcileInterval: ReturnType<typeof setInterval> | null = null;
|
|
24
24
|
private syncChain: Promise<void> = Promise.resolve();
|
|
25
|
+
private reconcileChain: Promise<void> = Promise.resolve();
|
|
26
|
+
private stopped = true;
|
|
25
27
|
|
|
26
28
|
constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) {
|
|
27
29
|
this.manager = manager;
|
|
@@ -64,30 +66,51 @@ export class TunnelManager {
|
|
|
64
66
|
* Start the tunnel hub and load allowed edges.
|
|
65
67
|
*/
|
|
66
68
|
public async start(): Promise<void> {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this.
|
|
80
|
-
|
|
69
|
+
this.stopped = false;
|
|
70
|
+
try {
|
|
71
|
+
await this.hub.start({
|
|
72
|
+
tunnelPort: this.config.tunnelPort ?? 8443,
|
|
73
|
+
targetHost: this.config.targetHost ?? '127.0.0.1',
|
|
74
|
+
tls: this.config.tls,
|
|
75
|
+
...(this.config.performance ? { performance: this.config.performance } : {}),
|
|
76
|
+
} as any);
|
|
77
|
+
|
|
78
|
+
if (this.stopped) return;
|
|
79
|
+
|
|
80
|
+
// Send allowed edges to the hub
|
|
81
|
+
await this.syncAllowedEdges();
|
|
82
|
+
|
|
83
|
+
if (this.stopped) return;
|
|
84
|
+
|
|
85
|
+
// Periodically reconcile with authoritative Rust hub status
|
|
86
|
+
this.reconcileInterval = setInterval(() => {
|
|
87
|
+
this.reconcileChain = this.reconcileChain
|
|
88
|
+
.catch(() => {})
|
|
89
|
+
.then(() => this.reconcile());
|
|
90
|
+
this.reconcileChain.catch(() => {});
|
|
91
|
+
}, 15_000);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
await this.stop();
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
81
96
|
}
|
|
82
97
|
|
|
83
98
|
/**
|
|
84
99
|
* Stop the tunnel hub.
|
|
85
100
|
*/
|
|
86
101
|
public async stop(): Promise<void> {
|
|
102
|
+
if (this.stopped) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.stopped = true;
|
|
87
106
|
if (this.reconcileInterval) {
|
|
88
107
|
clearInterval(this.reconcileInterval);
|
|
89
108
|
this.reconcileInterval = null;
|
|
90
109
|
}
|
|
110
|
+
await Promise.all([
|
|
111
|
+
this.syncChain.catch(() => {}),
|
|
112
|
+
this.reconcileChain.catch(() => {}),
|
|
113
|
+
]);
|
|
91
114
|
// Remove event listeners before stopping to prevent leaks
|
|
92
115
|
this.hub.removeAllListeners();
|
|
93
116
|
await this.hub.stop();
|
|
@@ -99,7 +122,9 @@ export class TunnelManager {
|
|
|
99
122
|
* Overwrites event-derived activeTunnels with the real activeStreams count.
|
|
100
123
|
*/
|
|
101
124
|
private async reconcile(): Promise<void> {
|
|
125
|
+
if (this.stopped) return;
|
|
102
126
|
const hubStatus = await this.hub.getStatus();
|
|
127
|
+
if (this.stopped) return;
|
|
103
128
|
if (!hubStatus || !hubStatus.connectedEdges) return;
|
|
104
129
|
|
|
105
130
|
const rustEdgeIds = new Set<string>();
|
|
@@ -144,7 +169,9 @@ export class TunnelManager {
|
|
|
144
169
|
*/
|
|
145
170
|
public async syncAllowedEdges(): Promise<void> {
|
|
146
171
|
const run = this.syncChain.catch(() => {}).then(async () => {
|
|
172
|
+
if (this.stopped) return;
|
|
147
173
|
const edges = this.manager.getAllowedEdges();
|
|
174
|
+
if (this.stopped) return;
|
|
148
175
|
await this.hub.updateAllowedEdges(edges as any);
|
|
149
176
|
});
|
|
150
177
|
this.syncChain = run;
|
package/ts_web/appstate.ts
CHANGED
|
@@ -260,6 +260,7 @@ export const acmeConfigStatePart = await appState.getStatePart<IAcmeConfigState>
|
|
|
260
260
|
export interface IRemoteIngressState {
|
|
261
261
|
edges: interfaces.data.IRemoteIngress[];
|
|
262
262
|
statuses: interfaces.data.IRemoteIngressStatus[];
|
|
263
|
+
hubSettings: interfaces.data.IRemoteIngressHubSettings | null;
|
|
263
264
|
selectedEdgeId: string | null;
|
|
264
265
|
newEdgeId: string | null;
|
|
265
266
|
isLoading: boolean;
|
|
@@ -272,6 +273,7 @@ export const remoteIngressStatePart = await appState.getStatePart<IRemoteIngress
|
|
|
272
273
|
{
|
|
273
274
|
edges: [],
|
|
274
275
|
statuses: [],
|
|
276
|
+
hubSettings: null,
|
|
275
277
|
selectedEdgeId: null,
|
|
276
278
|
newEdgeId: null,
|
|
277
279
|
isLoading: false,
|
|
@@ -1094,15 +1096,21 @@ export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(asyn
|
|
|
1094
1096
|
interfaces.requests.IReq_GetRemoteIngressStatus
|
|
1095
1097
|
>('/typedrequest', 'getRemoteIngressStatus');
|
|
1096
1098
|
|
|
1097
|
-
const
|
|
1099
|
+
const hubSettingsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
1100
|
+
interfaces.requests.IReq_GetRemoteIngressHubSettings
|
|
1101
|
+
>('/typedrequest', 'getRemoteIngressHubSettings');
|
|
1102
|
+
|
|
1103
|
+
const [edgesResponse, statusResponse, hubSettingsResponse] = await Promise.all([
|
|
1098
1104
|
edgesRequest.fire({ identity: context.identity }),
|
|
1099
1105
|
statusRequest.fire({ identity: context.identity }),
|
|
1106
|
+
hubSettingsRequest.fire({ identity: context.identity }),
|
|
1100
1107
|
]);
|
|
1101
1108
|
|
|
1102
1109
|
return {
|
|
1103
1110
|
...currentState,
|
|
1104
1111
|
edges: edgesResponse.edges,
|
|
1105
1112
|
statuses: statusResponse.statuses,
|
|
1113
|
+
hubSettings: hubSettingsResponse.settings,
|
|
1106
1114
|
isLoading: false,
|
|
1107
1115
|
error: null,
|
|
1108
1116
|
lastUpdated: Date.now(),
|
|
@@ -1219,6 +1227,38 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
|
|
|
1219
1227
|
}
|
|
1220
1228
|
});
|
|
1221
1229
|
|
|
1230
|
+
export const updateRemoteIngressHubSettingsAction = remoteIngressStatePart.createAction<{
|
|
1231
|
+
performance?: interfaces.data.IRemoteIngressPerformanceConfig;
|
|
1232
|
+
}>(async (statePartArg, dataArg, actionContext): Promise<IRemoteIngressState> => {
|
|
1233
|
+
const context = getActionContext();
|
|
1234
|
+
const currentState = statePartArg.getState()!;
|
|
1235
|
+
|
|
1236
|
+
try {
|
|
1237
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
1238
|
+
interfaces.requests.IReq_UpdateRemoteIngressHubSettings
|
|
1239
|
+
>('/typedrequest', 'updateRemoteIngressHubSettings');
|
|
1240
|
+
|
|
1241
|
+
const response = await request.fire({
|
|
1242
|
+
identity: context.identity!,
|
|
1243
|
+
performance: dataArg.performance,
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
if (!response.success) {
|
|
1247
|
+
return {
|
|
1248
|
+
...currentState,
|
|
1249
|
+
error: response.message || 'Failed to update RemoteIngress hub settings',
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
return await actionContext!.dispatch(fetchRemoteIngressAction, null);
|
|
1254
|
+
} catch (error: unknown) {
|
|
1255
|
+
return {
|
|
1256
|
+
...currentState,
|
|
1257
|
+
error: error instanceof Error ? error.message : 'Failed to update RemoteIngress hub settings',
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1222
1262
|
export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.createAction<string>(
|
|
1223
1263
|
async (statePartArg, edgeId): Promise<IRemoteIngressState> => {
|
|
1224
1264
|
const context = getActionContext();
|