Compare commits

...

2 Commits

Author SHA1 Message Date
jkunz 3dc0371f7e v13.41.0
Docker (tags) / release (push) Failing after 1s
Release / build-and-release (push) Successful in 7m5s
2026-05-31 19:42:51 +00:00
jkunz b212662764 feat(remoteingress): add RemoteIngress hub settings management 2026-05-31 19:42:17 +00:00
16 changed files with 779 additions and 132 deletions
+11
View File
@@ -16,6 +16,17 @@
## 2026-05-31 - 13.41.0
### Features
- add RemoteIngress hub settings management (remoteingress)
- Persist hub-level RemoteIngress performance settings with validation and seed defaults from config
- Add typed read/update handlers and web UI controls for hub performance settings
- Restart the tunnel hub after hub setting updates so new performance defaults take effect
- Serialize RemoteIngress lifecycle tasks, edge mutations, route syncs, and stop/start operations to avoid hub race conditions
## 2026-05-31 - 13.40.3
### Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@serve.zone/dcrouter",
"version": "13.40.3",
"version": "13.41.0",
"exports": "./binary/dcrouter.ts",
"compile": {
"include": [
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@serve.zone/dcrouter",
"private": false,
"version": "13.40.3",
"version": "13.41.0",
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module",
"bin": {
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '13.40.3',
version: '13.41.0',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}
+178 -38
View File
@@ -33,6 +33,7 @@ import { DnsManager } from './dns/manager.dns.js';
import { AcmeConfigManager } from './acme/manager.acme-config.js';
import { EmailDomainManager, SmartMtaStorageManager, WorkAppMailManager, buildEmailDnsRecords } from './email/index.js';
import type { IRoute } from '../ts_interfaces/data/route-management.js';
import type { IDcRouterRouteConfig, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig } from '../ts_interfaces/data/remoteingress.js';
import type { ISecurityCompiledPolicy } from '../ts_interfaces/data/security-policy.js';
export interface IDcRouterOptions {
@@ -280,6 +281,9 @@ export class DcRouter {
// Remote Ingress
public remoteIngressManager?: RemoteIngressManager;
public tunnelManager?: TunnelManager;
private remoteIngressHubLifecycleChain: Promise<void> = Promise.resolve();
private remoteIngressHubStopping = false;
private remoteIngressHubGeneration = 0;
// VPN
public vpnManager?: VpnManager;
@@ -613,15 +617,10 @@ export class DcRouter {
// Sync routes to RemoteIngressManager whenever routes change,
// then push updated derived ports to the Rust hub binary
async (routes) => {
if (this.remoteIngressManager) {
this.remoteIngressManager.setRoutes(routes as any[]);
}
if (this.tunnelManager) {
try {
await this.tunnelManager.syncAllowedEdges();
} catch (err: unknown) {
logger.log('error', `Failed to sync Remote Ingress allowed edges: ${(err as Error).message}`);
}
try {
await this.updateRemoteIngressRoutes(routes as IDcRouterRouteConfig[]);
} catch (err: unknown) {
logger.log('error', `Failed to sync Remote Ingress allowed edges: ${(err as Error).message}`);
}
},
undefined,
@@ -739,11 +738,7 @@ export class DcRouter {
await this.setupRemoteIngress();
})
.withStop(async () => {
if (this.tunnelManager) {
await this.tunnelManager.stop();
this.tunnelManager = undefined;
}
this.remoteIngressManager = undefined;
await this.stopRemoteIngress();
})
.withRetry({ maxRetries: 3, baseDelayMs: 2000, maxDelayMs: 30_000 }),
);
@@ -1319,12 +1314,15 @@ export class DcRouter {
}
const firewallConfig = await this.securityPolicyManager.compileRemoteIngressFirewall();
if (this.remoteIngressManager) {
(this.remoteIngressManager as any).setFirewallConfig?.(firewallConfig);
}
if (this.tunnelManager) {
await this.tunnelManager.syncAllowedEdges();
}
await this.queueRemoteIngressHubTask(async () => {
if (this.remoteIngressHubStopping) return;
if (this.remoteIngressManager) {
this.remoteIngressManager.setFirewallConfig(firewallConfig);
}
if (this.tunnelManager) {
await this.tunnelManager.syncAllowedEdges();
}
});
}
private mergeSecurityPolicies(
@@ -2340,28 +2338,180 @@ export class DcRouter {
}
logger.log('info', 'Setting up Remote Ingress hub...');
this.remoteIngressHubStopping = false;
const generation = ++this.remoteIngressHubGeneration;
// Initialize the edge registration manager
this.remoteIngressManager = new RemoteIngressManager();
await this.remoteIngressManager.initialize();
this.remoteIngressManager.setFirewallConfig(
await this.securityPolicyManager?.compileRemoteIngressFirewall(),
);
const remoteIngressManager = new RemoteIngressManager(this.options.remoteIngressConfig.performance);
this.remoteIngressManager = remoteIngressManager;
await remoteIngressManager.initialize();
if (!this.isRemoteIngressHubGenerationCurrent(generation, remoteIngressManager)) {
return;
}
const firewallConfig = await this.securityPolicyManager?.compileRemoteIngressFirewall();
if (!this.isRemoteIngressHubGenerationCurrent(generation, remoteIngressManager)) {
return;
}
remoteIngressManager.setFirewallConfig(firewallConfig);
// Pass current bootstrap routes so the manager can derive edge ports initially.
// Once RouteConfigManager applies the full DB set, the onRoutesApplied callback
// will push the complete merged routes here.
const bootstrapRoutes = [...this.seedConfigRoutes, ...this.seedEmailRoutes, ...this.runtimeDnsRoutes];
this.remoteIngressManager.setRoutes(bootstrapRoutes as any[]);
remoteIngressManager.setRoutes(bootstrapRoutes as any[]);
// If ConfigManagers finished before us, re-apply routes
// so the callback delivers the full DB set to our newly-created remoteIngressManager.
if (this.routeConfigManager) {
await this.routeConfigManager.applyRoutes();
}
if (!this.isRemoteIngressHubGenerationCurrent(generation, remoteIngressManager)) {
return;
}
// Resolve TLS certs for tunnel: explicit paths > ACME for hubDomain > self-signed (Rust default)
await this.queueRemoteIngressHubTask(async () => {
await this.startRemoteIngressTunnelHubLocked(generation);
});
if (!this.isRemoteIngressHubGenerationCurrent(generation, remoteIngressManager)) {
return;
}
const edgeCount = remoteIngressManager.getAllEdges().length;
logger.log('info', `Remote Ingress hub started on port ${this.options.remoteIngressConfig.tunnelPort || 8443} with ${edgeCount} registered edge(s)`);
}
private isRemoteIngressHubGenerationCurrent(generation: number, manager: RemoteIngressManager): boolean {
return !this.remoteIngressHubStopping
&& generation === this.remoteIngressHubGeneration
&& this.remoteIngressManager === manager;
}
private queueRemoteIngressHubTask<T>(task: () => Promise<T>): Promise<T> {
const run = this.remoteIngressHubLifecycleChain.then(task);
this.remoteIngressHubLifecycleChain = run.then(() => undefined, () => undefined);
return run;
}
private async stopRemoteIngress(): Promise<void> {
this.remoteIngressHubStopping = true;
this.remoteIngressHubGeneration++;
await this.queueRemoteIngressHubTask(async () => {
const currentTunnelManager = this.tunnelManager;
this.tunnelManager = undefined;
if (currentTunnelManager) {
await currentTunnelManager.stop();
}
});
this.remoteIngressManager = undefined;
}
public async mutateRemoteIngressEdges<T>(
mutation: (manager: RemoteIngressManager) => Promise<T>,
syncAllowedEdges = true,
): Promise<T> {
return await this.queueRemoteIngressHubTask(async () => {
if (this.remoteIngressHubStopping) {
throw new Error('RemoteIngress is stopping');
}
const manager = this.remoteIngressManager;
if (!manager) {
throw new Error('RemoteIngress not configured');
}
const result = await mutation(manager);
if (syncAllowedEdges && this.tunnelManager) {
await this.tunnelManager.syncAllowedEdges();
}
return result;
});
}
private async updateRemoteIngressRoutes(routes: IDcRouterRouteConfig[]): Promise<void> {
await this.queueRemoteIngressHubTask(async () => {
if (this.remoteIngressHubStopping) return;
if (this.remoteIngressManager) {
this.remoteIngressManager.setRoutes(routes);
}
if (this.tunnelManager) {
await this.tunnelManager.syncAllowedEdges();
}
});
}
public async updateRemoteIngressHubSettings(
updates: { performance?: IRemoteIngressPerformanceConfig },
updatedBy: string,
): Promise<IRemoteIngressHubSettings> {
return await this.queueRemoteIngressHubTask(async () => {
if (this.remoteIngressHubStopping) {
throw new Error('RemoteIngress is stopping');
}
if (!this.remoteIngressManager) {
throw new Error('RemoteIngress is not configured');
}
const settings = await this.remoteIngressManager.updateHubSettings(updates, updatedBy);
if (this.options.remoteIngressConfig?.enabled) {
await this.restartRemoteIngressTunnelHubLocked();
}
return settings;
});
}
private async restartRemoteIngressTunnelHubLocked(): Promise<void> {
const generation = ++this.remoteIngressHubGeneration;
if (!this.remoteIngressManager || !this.options.remoteIngressConfig?.enabled || this.remoteIngressHubStopping) {
return;
}
const currentTunnelManager = this.tunnelManager;
this.tunnelManager = undefined;
if (currentTunnelManager) {
await currentTunnelManager.stop();
}
if (this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration) {
return;
}
await this.startRemoteIngressTunnelHubLocked(generation);
}
private async startRemoteIngressTunnelHubLocked(generation: number): Promise<void> {
const riCfg = this.options.remoteIngressConfig;
const manager = this.remoteIngressManager;
if (!riCfg?.enabled || !manager || this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration) {
return;
}
const tlsConfig = await this.resolveRemoteIngressTlsConfig(riCfg);
if (this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration || this.remoteIngressManager !== manager) {
return;
}
const tunnelManager = new TunnelManager(manager, {
tunnelPort: riCfg.tunnelPort ?? 8443,
targetHost: '127.0.0.1',
tls: tlsConfig,
performance: manager.getHubPerformanceConfig(),
});
try {
await tunnelManager.start();
} catch (err) {
await tunnelManager.stop().catch(() => {});
throw err;
}
if (this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration || this.remoteIngressManager !== manager) {
await tunnelManager.stop();
return;
}
this.tunnelManager = tunnelManager;
}
private async resolveRemoteIngressTlsConfig(
riCfg: NonNullable<IDcRouterOptions['remoteIngressConfig']>,
): Promise<{ certPem: string; keyPem: string } | undefined> {
// Resolve TLS certs for tunnel: explicit paths > ACME for hubDomain > self-signed (Rust default)
let tlsConfig: { certPem: string; keyPem: string } | undefined;
// Priority 1: Explicit cert/key file paths
@@ -2391,17 +2541,7 @@ export class DcRouter {
logger.log('info', 'No TLS cert configured for RemoteIngress tunnel — using auto-generated self-signed');
}
// Create and start the tunnel manager
this.tunnelManager = new TunnelManager(this.remoteIngressManager, {
tunnelPort: riCfg.tunnelPort ?? 8443,
targetHost: '127.0.0.1',
tls: tlsConfig,
performance: riCfg.performance,
});
await this.tunnelManager.start();
const edgeCount = this.remoteIngressManager.getAllEdges().length;
logger.log('info', `Remote Ingress hub started on port ${this.options.remoteIngressConfig.tunnelPort || 8443} with ${edgeCount} registered edge(s)`);
return tlsConfig;
}
/**
@@ -0,0 +1,29 @@
import * as plugins from '../../plugins.js';
import { DcRouterDb } from '../classes.dcrouter-db.js';
import type { IRemoteIngressPerformanceConfig } from '../../../ts_interfaces/data/remoteingress.js';
const getDb = () => DcRouterDb.getInstance().getDb();
@plugins.smartdata.Collection(() => getDb())
export class RemoteIngressHubSettingsDoc extends plugins.smartdata.SmartDataDbDoc<RemoteIngressHubSettingsDoc, RemoteIngressHubSettingsDoc> {
@plugins.smartdata.unI()
@plugins.smartdata.svDb()
public settingsId: string = 'remote-ingress-hub-settings';
@plugins.smartdata.svDb()
public performance?: IRemoteIngressPerformanceConfig;
@plugins.smartdata.svDb()
public updatedAt: number = 0;
@plugins.smartdata.svDb()
public updatedBy: string = '';
constructor() {
super();
}
public static async load(): Promise<RemoteIngressHubSettingsDoc | null> {
return await RemoteIngressHubSettingsDoc.getInstance({ settingsId: 'remote-ingress-hub-settings' });
}
}
+1
View File
@@ -24,6 +24,7 @@ export * from './classes.cert-backoff.doc.js';
// Remote ingress document classes
export * from './classes.remote-ingress-edge.doc.js';
export * from './classes.remote-ingress-hub-settings.doc.js';
// RADIUS document classes
export * from './classes.vlan-mappings.doc.js';
+1 -1
View File
@@ -208,7 +208,7 @@ export class ConfigHandler {
hubDomain: riCfg?.hubDomain || null,
tlsMode,
connectedEdgeIps,
performance: riCfg?.performance,
performance: dcRouter.remoteIngressManager?.getHubPerformanceConfig() || riCfg?.performance,
};
return {
+95 -72
View File
@@ -52,30 +52,21 @@ export class RemoteIngressHandler {
scope: 'remote-ingress:write',
requireAdminIdentity: true,
});
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
if (!manager) {
try {
const edge = await this.opsServerRef.dcRouterRef.mutateRemoteIngressEdges((manager) => manager.createEdge(
dataArg.name,
dataArg.listenPorts || [],
dataArg.tags,
dataArg.autoDerivePorts ?? true,
dataArg.performance,
));
return { success: true, edge };
} catch (err: unknown) {
return {
success: false,
edge: null as any,
};
}
const edge = await manager.createEdge(
dataArg.name,
dataArg.listenPorts || [],
dataArg.tags,
dataArg.autoDerivePorts ?? true,
dataArg.performance,
);
// Sync allowed edges with the hub
if (tunnelManager) {
await tunnelManager.syncAllowedEdges();
}
return { success: true, edge };
},
),
);
@@ -89,21 +80,18 @@ export class RemoteIngressHandler {
scope: 'remote-ingress:write',
requireAdminIdentity: true,
});
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
if (!manager) {
return { success: false, message: 'RemoteIngress not configured' };
}
const deleted = await manager.deleteEdge(dataArg.id);
if (deleted && tunnelManager) {
await tunnelManager.syncAllowedEdges();
}
const deleted = await this.opsServerRef.dcRouterRef.mutateRemoteIngressEdges(
(manager) => manager.deleteEdge(dataArg.id),
).catch((err: unknown) => {
if ((err as Error).message.includes('RemoteIngress')) {
return false;
}
throw err;
});
return {
success: deleted,
message: deleted ? undefined : 'Edge not found',
message: deleted ? undefined : 'Edge not found or RemoteIngress not configured',
};
},
),
@@ -118,42 +106,42 @@ export class RemoteIngressHandler {
scope: 'remote-ingress:write',
requireAdminIdentity: true,
});
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
const result = await this.opsServerRef.dcRouterRef.mutateRemoteIngressEdges(async (manager) => {
const edge = await manager.updateEdge(dataArg.id, {
name: dataArg.name,
listenPorts: dataArg.listenPorts,
autoDerivePorts: dataArg.autoDerivePorts,
enabled: dataArg.enabled,
performance: dataArg.performance,
tags: dataArg.tags,
});
if (!manager) {
return { success: false, edge: null as any };
}
if (!edge) {
return null;
}
const edge = await manager.updateEdge(dataArg.id, {
name: dataArg.name,
listenPorts: dataArg.listenPorts,
autoDerivePorts: dataArg.autoDerivePorts,
enabled: dataArg.enabled,
performance: dataArg.performance,
tags: dataArg.tags,
});
if (!edge) {
return { success: false, edge: null as any };
}
// Sync allowed edges — ports, tags, or enabled may have changed
if (tunnelManager) {
await tunnelManager.syncAllowedEdges();
}
const breakdown = manager.getPortBreakdown(edge);
return {
success: true,
edge: {
const breakdown = manager.getPortBreakdown(edge);
return {
...edge,
secret: '********',
effectiveListenPorts: manager.getEffectiveListenPorts(edge),
effectiveListenPortsUdp: manager.getEffectiveListenPortsUdp(edge),
manualPorts: breakdown.manual,
derivedPorts: breakdown.derived,
},
};
}).catch((err: unknown) => {
if ((err as Error).message.includes('RemoteIngress')) {
return null;
}
throw err;
});
if (!result) {
return { success: false, edge: null as any };
}
return {
success: true,
edge: result,
};
},
),
@@ -168,23 +156,18 @@ export class RemoteIngressHandler {
scope: 'remote-ingress:write',
requireAdminIdentity: true,
});
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
if (!manager) {
return { success: false, secret: '' };
}
const secret = await manager.regenerateSecret(dataArg.id);
const secret = await this.opsServerRef.dcRouterRef.mutateRemoteIngressEdges(
(manager) => manager.regenerateSecret(dataArg.id),
).catch((err: unknown) => {
if ((err as Error).message.includes('RemoteIngress')) {
return null;
}
throw err;
});
if (!secret) {
return { success: false, secret: '' };
}
// Sync allowed edges since secret changed
if (tunnelManager) {
await tunnelManager.syncAllowedEdges();
}
return { success: true, secret };
},
),
@@ -205,6 +188,46 @@ export class RemoteIngressHandler {
),
);
// Get hub-level settings (read)
viewRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressHubSettings>(
'getRemoteIngressHubSettings',
async (dataArg, toolsArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'remote-ingress:read' });
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
return {
settings: manager?.getHubSettings() || {
updatedAt: 0,
updatedBy: 'default',
},
};
},
),
);
// Update hub-level settings (write)
adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateRemoteIngressHubSettings>(
'updateRemoteIngressHubSettings',
async (dataArg, toolsArg) => {
const auth = await requireOpsAuth(this.opsServerRef, dataArg, {
scope: 'remote-ingress:write',
requireAdminIdentity: true,
});
try {
const settings = await this.opsServerRef.dcRouterRef.updateRemoteIngressHubSettings(
{ performance: dataArg.performance },
auth.userId,
);
return { success: true, settings };
} catch (err: unknown) {
return { success: false, message: (err as Error).message };
}
},
),
);
// Get a connection token for an edge (write — exposes secret)
adminRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressConnectionToken>(
@@ -1,11 +1,36 @@
import * as plugins from '../plugins.js';
import type { IRemoteIngress, IRemoteIngressPerformanceConfig, IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
import { RemoteIngressEdgeDoc } from '../db/index.js';
import type { IDcRouterRouteConfig, IRemoteIngress, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig, TRemoteIngressPerformanceProfile } from '../../ts_interfaces/data/remoteingress.js';
import { RemoteIngressEdgeDoc, RemoteIngressHubSettingsDoc } from '../db/index.js';
interface IRemoteIngressFirewallConfig {
blockedIps?: string[];
}
type TPerformanceIntegerField =
| 'maxStreamsPerEdge'
| 'totalWindowBudgetBytes'
| 'minStreamWindowBytes'
| 'maxStreamWindowBytes'
| 'sustainedStreamWindowBytes'
| 'quicDatagramReceiveBufferBytes'
| 'streamFramePayloadBytes'
| 'firstDataConnectTimeoutMs'
| 'clientWriteTimeoutMs';
const performanceIntegerMaxByField: Record<TPerformanceIntegerField, number> = {
maxStreamsPerEdge: 100_000,
totalWindowBudgetBytes: 1_073_741_824,
minStreamWindowBytes: 16_777_216,
maxStreamWindowBytes: 134_217_728,
sustainedStreamWindowBytes: 134_217_728,
quicDatagramReceiveBufferBytes: 67_108_864,
streamFramePayloadBytes: 16_777_216,
firstDataConnectTimeoutMs: 3_600_000,
clientWriteTimeoutMs: 3_600_000,
};
const maxServerFirstPorts = 128;
function extractPorts(portRange: plugins.smartproxy.IRouteConfig['match']['ports']): number[] {
const ports = new Set<number>(plugins.smartproxy.expandPortRange(portRange) as number[]);
return [...ports].sort((a, b) => a - b);
@@ -20,8 +45,12 @@ export class RemoteIngressManager {
private edges: Map<string, IRemoteIngress> = new Map();
private routes: IDcRouterRouteConfig[] = [];
private firewallConfig?: IRemoteIngressFirewallConfig;
private hubSettings: IRemoteIngressHubSettings = {
updatedAt: 0,
updatedBy: 'default',
};
constructor() {
constructor(private seedHubPerformance?: IRemoteIngressPerformanceConfig) {
}
/**
@@ -50,6 +79,28 @@ export class RemoteIngressManager {
};
this.edges.set(edge.id, edge);
}
await this.initializeHubSettings();
}
private async initializeHubSettings(): Promise<void> {
let doc = await RemoteIngressHubSettingsDoc.load();
if (!doc) {
const seedPerformance = this.normalizePerformanceConfig(this.seedHubPerformance);
if (seedPerformance) {
doc = new RemoteIngressHubSettingsDoc();
doc.settingsId = 'remote-ingress-hub-settings';
doc.performance = seedPerformance;
doc.updatedAt = Date.now();
doc.updatedBy = 'seed';
await doc.save();
}
}
this.hubSettings = doc ? this.toHubSettings(doc) : {
updatedAt: 0,
updatedBy: 'default',
};
}
/**
@@ -66,6 +117,38 @@ export class RemoteIngressManager {
this.firewallConfig = firewallConfig;
}
public getHubSettings(): IRemoteIngressHubSettings {
return {
...this.hubSettings,
performance: this.hubSettings.performance ? { ...this.hubSettings.performance } : undefined,
};
}
public getHubPerformanceConfig(): IRemoteIngressPerformanceConfig | undefined {
return this.hubSettings.performance && Object.keys(this.hubSettings.performance).length > 0
? { ...this.hubSettings.performance }
: undefined;
}
public async updateHubSettings(
updates: { performance?: IRemoteIngressPerformanceConfig },
updatedBy: string,
): Promise<IRemoteIngressHubSettings> {
let doc = await RemoteIngressHubSettingsDoc.load();
if (!doc) {
doc = new RemoteIngressHubSettingsDoc();
doc.settingsId = 'remote-ingress-hub-settings';
}
doc.performance = this.normalizePerformanceConfig(updates.performance);
doc.updatedAt = Date.now();
doc.updatedBy = updatedBy;
await doc.save();
this.hubSettings = this.toHubSettings(doc);
return this.getHubSettings();
}
/**
* Derive listen ports for an edge from routes tagged with remoteIngress.enabled.
* When a route specifies edgeFilter, only edges whose id or tags match get that route's ports.
@@ -324,4 +407,90 @@ export class RemoteIngressManager {
}
return result;
}
private normalizePerformanceConfig(
performance?: IRemoteIngressPerformanceConfig,
): IRemoteIngressPerformanceConfig | undefined {
if (!performance) {
return undefined;
}
const next: IRemoteIngressPerformanceConfig = {};
const validProfiles: TRemoteIngressPerformanceProfile[] = ['balanced', 'throughput', 'highConcurrency'];
if (performance.profile !== undefined) {
if (!validProfiles.includes(performance.profile)) {
throw new Error('Invalid RemoteIngress performance profile');
}
next.profile = performance.profile;
}
const assignPositiveInteger = (field: TPerformanceIntegerField) => {
const value = performance[field];
if (value === undefined) {
return;
}
const maxValue = performanceIntegerMaxByField[field];
if (!Number.isSafeInteger(value) || value < 1 || value > maxValue) {
throw new Error(`${field} must be a positive safe integer no greater than ${maxValue}`);
}
(next as Record<string, number>)[field] = value;
};
assignPositiveInteger('maxStreamsPerEdge');
assignPositiveInteger('totalWindowBudgetBytes');
assignPositiveInteger('minStreamWindowBytes');
assignPositiveInteger('maxStreamWindowBytes');
assignPositiveInteger('sustainedStreamWindowBytes');
assignPositiveInteger('quicDatagramReceiveBufferBytes');
assignPositiveInteger('streamFramePayloadBytes');
assignPositiveInteger('firstDataConnectTimeoutMs');
assignPositiveInteger('clientWriteTimeoutMs');
if (
next.minStreamWindowBytes !== undefined
&& next.maxStreamWindowBytes !== undefined
&& next.minStreamWindowBytes > next.maxStreamWindowBytes
) {
throw new Error('minStreamWindowBytes must not exceed maxStreamWindowBytes');
}
if (
next.sustainedStreamWindowBytes !== undefined
&& next.maxStreamWindowBytes !== undefined
&& next.sustainedStreamWindowBytes > next.maxStreamWindowBytes
) {
throw new Error('sustainedStreamWindowBytes must not exceed maxStreamWindowBytes');
}
const configuredServerFirstPorts = performance.serverFirstPorts;
if (configuredServerFirstPorts !== undefined) {
if (!Array.isArray(configuredServerFirstPorts)) {
throw new Error('serverFirstPorts must contain valid port numbers');
}
if (configuredServerFirstPorts.length > maxServerFirstPorts) {
throw new Error(`serverFirstPorts must contain at most ${maxServerFirstPorts} ports`);
}
const serverFirstPorts = [...new Set(configuredServerFirstPorts.map((port) => Number(port)))].sort((a, b) => a - b);
for (const port of serverFirstPorts) {
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error('serverFirstPorts must contain valid port numbers');
}
if (port === 443) {
throw new Error('Port 443 is client-first TLS and must not be listed as server-first');
}
}
if (serverFirstPorts.length > 0) {
next.serverFirstPorts = serverFirstPorts;
}
}
return Object.keys(next).length > 0 ? next : undefined;
}
private toHubSettings(doc: RemoteIngressHubSettingsDoc): IRemoteIngressHubSettings {
return {
performance: doc.performance,
updatedAt: doc.updatedAt,
updatedBy: doc.updatedBy,
};
}
}
+39 -12
View File
@@ -22,6 +22,8 @@ export class TunnelManager {
private edgeStatuses: Map<string, IRemoteIngressStatus> = new Map();
private reconcileInterval: ReturnType<typeof setInterval> | null = null;
private syncChain: Promise<void> = Promise.resolve();
private reconcileChain: Promise<void> = Promise.resolve();
private stopped = true;
constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) {
this.manager = manager;
@@ -64,30 +66,51 @@ export class TunnelManager {
* Start the tunnel hub and load allowed edges.
*/
public async start(): Promise<void> {
await this.hub.start({
tunnelPort: this.config.tunnelPort ?? 8443,
targetHost: this.config.targetHost ?? '127.0.0.1',
tls: this.config.tls,
...(this.config.performance ? { performance: this.config.performance } : {}),
} as any);
this.stopped = false;
try {
await this.hub.start({
tunnelPort: this.config.tunnelPort ?? 8443,
targetHost: this.config.targetHost ?? '127.0.0.1',
tls: this.config.tls,
...(this.config.performance ? { performance: this.config.performance } : {}),
} as any);
// Send allowed edges to the hub
await this.syncAllowedEdges();
if (this.stopped) return;
// Periodically reconcile with authoritative Rust hub status
this.reconcileInterval = setInterval(() => {
this.reconcile().catch(() => {});
}, 15_000);
// Send allowed edges to the hub
await this.syncAllowedEdges();
if (this.stopped) return;
// Periodically reconcile with authoritative Rust hub status
this.reconcileInterval = setInterval(() => {
this.reconcileChain = this.reconcileChain
.catch(() => {})
.then(() => this.reconcile());
this.reconcileChain.catch(() => {});
}, 15_000);
} catch (err) {
await this.stop();
throw err;
}
}
/**
* Stop the tunnel hub.
*/
public async stop(): Promise<void> {
if (this.stopped) {
return;
}
this.stopped = true;
if (this.reconcileInterval) {
clearInterval(this.reconcileInterval);
this.reconcileInterval = null;
}
await Promise.all([
this.syncChain.catch(() => {}),
this.reconcileChain.catch(() => {}),
]);
// Remove event listeners before stopping to prevent leaks
this.hub.removeAllListeners();
await this.hub.stop();
@@ -99,7 +122,9 @@ export class TunnelManager {
* Overwrites event-derived activeTunnels with the real activeStreams count.
*/
private async reconcile(): Promise<void> {
if (this.stopped) return;
const hubStatus = await this.hub.getStatus();
if (this.stopped) return;
if (!hubStatus || !hubStatus.connectedEdges) return;
const rustEdgeIds = new Set<string>();
@@ -144,7 +169,9 @@ export class TunnelManager {
*/
public async syncAllowedEdges(): Promise<void> {
const run = this.syncChain.catch(() => {}).then(async () => {
if (this.stopped) return;
const edges = this.manager.getAllowedEdges();
if (this.stopped) return;
await this.hub.updateAllowedEdges(edges as any);
});
this.syncChain = run;
+6
View File
@@ -63,6 +63,12 @@ export interface IRemoteIngressPerformanceConfig {
serverFirstPorts?: number[];
}
export interface IRemoteIngressHubSettings {
performance?: IRemoteIngressPerformanceConfig;
updatedAt: number;
updatedBy: string;
}
export interface IRemoteIngressPerformanceEffective {
profile: TRemoteIngressPerformanceProfile;
maxStreamsPerEdge: number;
+38 -1
View File
@@ -1,6 +1,6 @@
import * as plugins from '../plugins.js';
import * as authInterfaces from '../data/auth.js';
import type { IRemoteIngress, IRemoteIngressPerformanceConfig, IRemoteIngressStatus } from '../data/remoteingress.js';
import type { IRemoteIngress, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig, IRemoteIngressStatus } from '../data/remoteingress.js';
// ============================================================================
// Remote Ingress Edge Management
@@ -147,3 +147,40 @@ export interface IReq_GetRemoteIngressConnectionToken extends plugins.typedreque
message?: string;
};
}
/**
* Get hub-level RemoteIngress settings.
*/
export interface IReq_GetRemoteIngressHubSettings extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_GetRemoteIngressHubSettings
> {
method: 'getRemoteIngressHubSettings';
request: {
identity?: authInterfaces.IIdentity;
apiToken?: string;
};
response: {
settings: IRemoteIngressHubSettings;
};
}
/**
* Update hub-level RemoteIngress settings.
*/
export interface IReq_UpdateRemoteIngressHubSettings extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_UpdateRemoteIngressHubSettings
> {
method: 'updateRemoteIngressHubSettings';
request: {
identity?: authInterfaces.IIdentity;
apiToken?: string;
performance?: IRemoteIngressPerformanceConfig;
};
response: {
success: boolean;
settings?: IRemoteIngressHubSettings;
message?: string;
};
}
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '13.40.3',
version: '13.41.0',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}
+41 -1
View File
@@ -260,6 +260,7 @@ export const acmeConfigStatePart = await appState.getStatePart<IAcmeConfigState>
export interface IRemoteIngressState {
edges: interfaces.data.IRemoteIngress[];
statuses: interfaces.data.IRemoteIngressStatus[];
hubSettings: interfaces.data.IRemoteIngressHubSettings | null;
selectedEdgeId: string | null;
newEdgeId: string | null;
isLoading: boolean;
@@ -272,6 +273,7 @@ export const remoteIngressStatePart = await appState.getStatePart<IRemoteIngress
{
edges: [],
statuses: [],
hubSettings: null,
selectedEdgeId: null,
newEdgeId: null,
isLoading: false,
@@ -1094,15 +1096,21 @@ export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(asyn
interfaces.requests.IReq_GetRemoteIngressStatus
>('/typedrequest', 'getRemoteIngressStatus');
const [edgesResponse, statusResponse] = await Promise.all([
const hubSettingsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetRemoteIngressHubSettings
>('/typedrequest', 'getRemoteIngressHubSettings');
const [edgesResponse, statusResponse, hubSettingsResponse] = await Promise.all([
edgesRequest.fire({ identity: context.identity }),
statusRequest.fire({ identity: context.identity }),
hubSettingsRequest.fire({ identity: context.identity }),
]);
return {
...currentState,
edges: edgesResponse.edges,
statuses: statusResponse.statuses,
hubSettings: hubSettingsResponse.settings,
isLoading: false,
error: null,
lastUpdated: Date.now(),
@@ -1219,6 +1227,38 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
}
});
export const updateRemoteIngressHubSettingsAction = remoteIngressStatePart.createAction<{
performance?: interfaces.data.IRemoteIngressPerformanceConfig;
}>(async (statePartArg, dataArg, actionContext): Promise<IRemoteIngressState> => {
const context = getActionContext();
const currentState = statePartArg.getState()!;
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_UpdateRemoteIngressHubSettings
>('/typedrequest', 'updateRemoteIngressHubSettings');
const response = await request.fire({
identity: context.identity!,
performance: dataArg.performance,
});
if (!response.success) {
return {
...currentState,
error: response.message || 'Failed to update RemoteIngress hub settings',
};
}
return await actionContext!.dispatch(fetchRemoteIngressAction, null);
} catch (error: unknown) {
return {
...currentState,
error: error instanceof Error ? error.message : 'Failed to update RemoteIngress hub settings',
};
}
});
export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.createAction<string>(
async (statePartArg, edgeId): Promise<IRemoteIngressState> => {
const context = getActionContext();
@@ -12,6 +12,17 @@ import * as interfaces from '../../../dist_ts_interfaces/index.js';
import { viewHostCss } from '../shared/css.js';
import { type IStatsTile } from '@design.estate/dees-catalog';
const performanceProfileOptions = [
{ key: '', option: 'Default' },
{ key: 'balanced', option: 'Balanced' },
{ key: 'throughput', option: 'Throughput' },
{ key: 'highConcurrency', option: 'High concurrency' },
];
function getDropdownKey(value: any): string {
return typeof value === 'string' ? value : value?.key || '';
}
declare global {
interface HTMLElementTagNameMap {
'ops-view-remoteingress': OpsViewRemoteIngress;
@@ -137,6 +148,13 @@ export class OpsViewRemoteIngress extends DeesElement {
.metricMuted {
color: var(--text-muted, #6b7280);
}
.settingsNote {
margin: 12px 0 0;
font-size: 12px;
line-height: 1.5;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
}
`,
];
@@ -308,6 +326,14 @@ export class OpsViewRemoteIngress extends DeesElement {
});
},
},
{
name: 'Hub Settings',
iconName: 'lucide:slidersHorizontal',
type: ['header' as const],
actionFunc: async () => {
await this.showHubSettingsDialog();
},
},
{
name: 'Enable',
iconName: 'lucide:play',
@@ -591,4 +617,142 @@ export class OpsViewRemoteIngress extends DeesElement {
return base ? {} : undefined;
}
private async showHubSettingsDialog(): Promise<void> {
const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
const performance = this.riState.hubSettings?.performance || {};
const selectedProfile = performanceProfileOptions.find((option) => option.key === (performance.profile || '')) || performanceProfileOptions[0];
const updatedAt = this.riState.hubSettings?.updatedAt
? new Date(this.riState.hubSettings.updatedAt).toLocaleString()
: 'not persisted yet';
await DeesModal.createAndShow({
heading: 'RemoteIngress Hub Settings',
content: html`
<dees-form>
<dees-input-dropdown
.key=${'profile'}
.label=${'Performance Profile'}
.options=${performanceProfileOptions}
.selectedOption=${selectedProfile}
></dees-input-dropdown>
<dees-input-text
.key=${'maxStreamsPerEdge'}
.label=${'Max Connections / Edge'}
.description=${'Maximum concurrent client streams per edge. Leave empty for RemoteIngress defaults.'}
.value=${performance.maxStreamsPerEdge?.toString() || ''}
></dees-input-text>
<dees-input-text
.key=${'clientWriteTimeoutMs'}
.label=${'Client Write Timeout'}
.description=${'Milliseconds before idle client writes are timed out. Leave empty for default.'}
.value=${performance.clientWriteTimeoutMs?.toString() || ''}
></dees-input-text>
<dees-input-text
.key=${'firstDataConnectTimeoutMs'}
.label=${'First Data Timeout'}
.description=${'Milliseconds to wait for initial client data before connecting upstream. Leave empty for default.'}
.value=${performance.firstDataConnectTimeoutMs?.toString() || ''}
></dees-input-text>
<dees-input-text
.key=${'serverFirstPorts'}
.label=${'Server-first Ports'}
.description=${'Comma-separated ports such as 21, 22, 25, 110, 143, 587. Do not include 443.'}
.value=${(performance.serverFirstPorts || []).join(', ')}
></dees-input-text>
</dees-form>
<p class="settingsNote">
Saving restarts the RemoteIngress hub so connected edges reconnect and pick up the new defaults.
Last updated: ${updatedAt} by ${this.riState.hubSettings?.updatedBy || 'default'}.
</p>
`,
menuOptions: [
{
name: 'Cancel',
iconName: 'lucide:x',
action: async (modalArg: any) => await modalArg.destroy(),
},
{
name: 'Save',
iconName: 'lucide:check',
action: async (modalArg: any) => {
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
if (!form) return;
const formData = await form.collectFormData();
let performanceSettings: interfaces.data.IRemoteIngressPerformanceConfig | undefined;
try {
performanceSettings = this.collectHubPerformanceSettings(formData);
} catch (err: unknown) {
DeesToast.show({ message: (err as Error).message, type: 'error', duration: 4000 });
return;
}
const nextState = await appstate.remoteIngressStatePart.dispatchAction(
appstate.updateRemoteIngressHubSettingsAction,
{ performance: performanceSettings },
);
if (nextState.error) {
DeesToast.show({ message: nextState.error, type: 'error', duration: 4000 });
return;
}
await modalArg.destroy();
DeesToast.show({ message: 'RemoteIngress hub settings saved', type: 'success', duration: 3000 });
},
},
],
});
}
private collectHubPerformanceSettings(formData: Record<string, any>): interfaces.data.IRemoteIngressPerformanceConfig | undefined {
const next: interfaces.data.IRemoteIngressPerformanceConfig = {};
const profile = getDropdownKey(formData.profile) as interfaces.data.TRemoteIngressPerformanceProfile | '';
if (profile) {
next.profile = profile;
}
this.assignPositiveIntegerSetting(next, 'maxStreamsPerEdge', formData.maxStreamsPerEdge, 'Max Connections / Edge');
this.assignPositiveIntegerSetting(next, 'clientWriteTimeoutMs', formData.clientWriteTimeoutMs, 'Client Write Timeout');
this.assignPositiveIntegerSetting(next, 'firstDataConnectTimeoutMs', formData.firstDataConnectTimeoutMs, 'First Data Timeout');
const serverFirstPorts = this.parsePortList(formData.serverFirstPorts, 'Server-first Ports');
if (serverFirstPorts.length > 0) {
if (serverFirstPorts.includes(443)) {
throw new Error('Port 443 is client-first TLS and must not be listed as server-first');
}
next.serverFirstPorts = serverFirstPorts;
}
return Object.keys(next).length > 0 ? next : undefined;
}
private assignPositiveIntegerSetting(
target: interfaces.data.IRemoteIngressPerformanceConfig,
key: 'maxStreamsPerEdge' | 'clientWriteTimeoutMs' | 'firstDataConnectTimeoutMs',
value: any,
label: string,
): void {
const text = `${value || ''}`.trim();
if (!text) {
return;
}
const parsed = Number.parseInt(text, 10);
if (!Number.isInteger(parsed) || parsed < 1) {
throw new Error(`${label} must be a positive integer`);
}
target[key] = parsed;
}
private parsePortList(value: any, label: string): number[] {
const text = `${value || ''}`.trim();
if (!text) {
return [];
}
const ports = text.split(',').map((part) => Number.parseInt(part.trim(), 10));
for (const port of ports) {
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error(`${label} must contain valid port numbers`);
}
}
return [...new Set(ports)].sort((a, b) => a - b);
}
}