diff --git a/changelog.md b/changelog.md index 2eb0a5f..b5fa764 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-04-26 - 13.22.0 - feat(remoteingress) +add remote ingress performance configuration and expose tunnel transport metrics + +- upgrade @serve.zone/remoteingress to support performance tuning and richer tunnel status data +- pass remote ingress performance settings through router startup and config APIs +- serialize allowed-edge sync operations and await route update hooks to avoid tunnel sync races +- expose UDP listen ports and transport, flow control, queue, and traffic metrics in remote ingress APIs and ops UI + ## 2026-04-26 - 13.21.1 - fix(deps) bump @push.rocks/smartproxy to ^27.8.1 diff --git a/package.json b/package.json index 21b7d9a..f5362b2 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@push.rocks/taskbuffer": "^8.0.2", "@serve.zone/catalog": "^2.12.4", "@serve.zone/interfaces": "^5.4.3", - "@serve.zone/remoteingress": "^4.15.3", + "@serve.zone/remoteingress": "^4.17.0", "@tsclass/tsclass": "^9.5.0", "@types/qrcode": "^1.5.6", "lru-cache": "^11.3.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 87af21e..02b661a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,8 +111,8 @@ importers: specifier: ^5.4.3 version: 5.4.3 '@serve.zone/remoteingress': - specifier: ^4.15.3 - version: 4.15.3 + specifier: ^4.17.0 + version: 4.17.0 '@tsclass/tsclass': specifier: ^9.5.0 version: 9.5.0 @@ -1594,8 +1594,8 @@ packages: '@serve.zone/interfaces@5.4.3': resolution: {integrity: sha512-9ijFhHoC7GYyyAUJbBoDYmcoCmIXTFPiD6fI3x68SWiC0xA+2LG0nOe14D32c1QN9X/3i2Ac5/1sUibfjHsIGg==} - '@serve.zone/remoteingress@4.15.3': - resolution: {integrity: sha512-kg/bmR+qcFRFuigTDr5Fao72cb7m/mSkI5APm7KZDKSUYTFuytNoj6KCIE0ICkc3Nh34y8oDwFJsS6oFo64AyQ==} + '@serve.zone/remoteingress@4.17.0': + resolution: {integrity: sha512-q1g2Zm1Yh825cMiF8/W1iQlOLGqgmWBrtzDqNgF5hH31HP2zHHtC2+XPyB+1kEphsztlXzPMlcRpfCRwuQUexA==} '@sindresorhus/is@5.6.0': resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} @@ -6933,7 +6933,7 @@ snapshots: '@push.rocks/smartlog-interfaces': 3.0.2 '@tsclass/tsclass': 9.5.0 - '@serve.zone/remoteingress@4.15.3': + '@serve.zone/remoteingress@4.17.0': dependencies: '@push.rocks/qenv': 6.1.3 '@push.rocks/smartnftables': 1.1.0 diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index cff5a1f..3b0b8c4 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '13.21.1', + version: '13.22.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts/classes.dcrouter.ts b/ts/classes.dcrouter.ts index 59a0896..47cbb39 100644 --- a/ts/classes.dcrouter.ts +++ b/ts/classes.dcrouter.ts @@ -178,6 +178,8 @@ export interface IDcRouterOptions { certPath?: string; keyPath?: string; }; + /** Performance profile and limits for remote ingress hub/edge tunnels. */ + performance?: import('../ts_interfaces/data/remoteingress.js').IRemoteIngressPerformanceConfig; }; /** @@ -570,12 +572,16 @@ export class DcRouter { this.referenceResolver, // Sync routes to RemoteIngressManager whenever routes change, // then push updated derived ports to the Rust hub binary - (routes) => { + async (routes) => { if (this.remoteIngressManager) { this.remoteIngressManager.setRoutes(routes as any[]); } if (this.tunnelManager) { - this.tunnelManager.syncAllowedEdges(); + try { + await this.tunnelManager.syncAllowedEdges(); + } catch (err: unknown) { + logger.log('error', `Failed to sync Remote Ingress allowed edges: ${(err as Error).message}`); + } } }, undefined, @@ -1120,7 +1126,12 @@ export class DcRouter { // to SmartProxy with PROXY protocol v1 headers to preserve client IPs. if (this.options.remoteIngressConfig?.enabled) { smartProxyConfig.acceptProxyProtocol = true; - smartProxyConfig.proxyIPs = ['127.0.0.1']; + if (!smartProxyConfig.proxyIPs) { + smartProxyConfig.proxyIPs = []; + } + if (!smartProxyConfig.proxyIPs.includes('127.0.0.1')) { + smartProxyConfig.proxyIPs.push('127.0.0.1'); + } } // VPN uses socket mode with PP v2 — SmartProxy must accept proxy protocol from localhost @@ -2270,6 +2281,7 @@ export class DcRouter { tunnelPort: riCfg.tunnelPort ?? 8443, targetHost: '127.0.0.1', tls: tlsConfig, + performance: riCfg.performance, }); await this.tunnelManager.start(); diff --git a/ts/config/classes.route-config-manager.ts b/ts/config/classes.route-config-manager.ts index 504a4b5..7d993ea 100644 --- a/ts/config/classes.route-config-manager.ts +++ b/ts/config/classes.route-config-manager.ts @@ -59,7 +59,7 @@ export class RouteConfigManager { private getHttp3Config?: () => IHttp3Config | undefined, private getVpnClientIpsForRoute?: (route: IDcRouterRouteConfig, routeId?: string) => TIpAllowEntry[], private referenceResolver?: ReferenceResolver, - private onRoutesApplied?: (routes: plugins.smartproxy.IRouteConfig[]) => void, + private onRoutesApplied?: (routes: plugins.smartproxy.IRouteConfig[]) => void | Promise, private getRuntimeRoutes?: () => plugins.smartproxy.IRouteConfig[], private hydrateStoredRoute?: (storedRoute: IRoute) => plugins.smartproxy.IRouteConfig | undefined, ) {} @@ -540,7 +540,7 @@ export class RouteConfigManager { // Notify listeners (e.g. RemoteIngressManager) of the route set if (this.onRoutesApplied) { - this.onRoutesApplied(enabledRoutes); + await this.onRoutesApplied(enabledRoutes); } logger.log('info', `Applied ${enabledRoutes.length} routes to SmartProxy (${this.routes.size} total)`); diff --git a/ts/opsserver/handlers/config.handler.ts b/ts/opsserver/handlers/config.handler.ts index 0623170..f3d1a05 100644 --- a/ts/opsserver/handlers/config.handler.ts +++ b/ts/opsserver/handlers/config.handler.ts @@ -206,6 +206,7 @@ export class ConfigHandler { hubDomain: riCfg?.hubDomain || null, tlsMode, connectedEdgeIps, + performance: riCfg?.performance, }; return { diff --git a/ts/opsserver/handlers/remoteingress.handler.ts b/ts/opsserver/handlers/remoteingress.handler.ts index a50f7de..e576660 100644 --- a/ts/opsserver/handlers/remoteingress.handler.ts +++ b/ts/opsserver/handlers/remoteingress.handler.ts @@ -29,6 +29,7 @@ export class RemoteIngressHandler { ...e, secret: '********', // Never expose secrets via API effectiveListenPorts: manager.getEffectiveListenPorts(e), + effectiveListenPortsUdp: manager.getEffectiveListenPortsUdp(e), manualPorts: breakdown.manual, derivedPorts: breakdown.derived, }; @@ -133,6 +134,7 @@ export class RemoteIngressHandler { ...edge, secret: '********', effectiveListenPorts: manager.getEffectiveListenPorts(edge), + effectiveListenPortsUdp: manager.getEffectiveListenPortsUdp(edge), manualPorts: breakdown.manual, derivedPorts: breakdown.derived, }, diff --git a/ts/remoteingress/classes.tunnel-manager.ts b/ts/remoteingress/classes.tunnel-manager.ts index 7d88af2..c0f193f 100644 --- a/ts/remoteingress/classes.tunnel-manager.ts +++ b/ts/remoteingress/classes.tunnel-manager.ts @@ -9,6 +9,7 @@ export interface ITunnelManagerConfig { certPem?: string; keyPem?: string; }; + performance?: import('../../ts_interfaces/data/remoteingress.js').IRemoteIngressPerformanceConfig; } /** @@ -20,6 +21,7 @@ export class TunnelManager { private config: ITunnelManagerConfig; private edgeStatuses: Map = new Map(); private reconcileInterval: ReturnType | null = null; + private syncChain: Promise = Promise.resolve(); constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) { this.manager = manager; @@ -66,7 +68,8 @@ export class TunnelManager { 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(); @@ -107,20 +110,23 @@ export class TunnelManager { if (existing) { existing.activeTunnels = rustEdge.activeStreams; existing.lastHeartbeat = Date.now(); + this.applyRustStatus(existing, rustEdge); // Update peer address if available from Rust hub if (rustEdge.peerAddr) { existing.publicIp = rustEdge.peerAddr; } } else { // Missed edgeConnected event — add entry - this.edgeStatuses.set(rustEdge.edgeId, { + const status: IRemoteIngressStatus = { edgeId: rustEdge.edgeId, connected: true, publicIp: rustEdge.peerAddr || null, activeTunnels: rustEdge.activeStreams, lastHeartbeat: Date.now(), connectedAt: rustEdge.connectedAt * 1000, - }); + }; + this.applyRustStatus(status, rustEdge); + this.edgeStatuses.set(rustEdge.edgeId, status); } } @@ -137,8 +143,22 @@ export class TunnelManager { * Call this after creating/deleting/updating edges. */ public async syncAllowedEdges(): Promise { - const edges = this.manager.getAllowedEdges(); - await this.hub.updateAllowedEdges(edges); + const run = this.syncChain.catch(() => {}).then(async () => { + const edges = this.manager.getAllowedEdges(); + await this.hub.updateAllowedEdges(edges as any); + }); + this.syncChain = run; + await run; + } + + private applyRustStatus(status: IRemoteIngressStatus, rustEdge: any): void { + status.transportMode = rustEdge.transportMode; + status.fallbackUsed = rustEdge.fallbackUsed; + status.performance = rustEdge.performance; + status.flowControl = rustEdge.flowControl; + status.queues = rustEdge.queues; + status.traffic = rustEdge.traffic; + status.udp = rustEdge.udp; } /** diff --git a/ts_apiclient/classes.remoteingress.ts b/ts_apiclient/classes.remoteingress.ts index 0a43221..31504ba 100644 --- a/ts_apiclient/classes.remoteingress.ts +++ b/ts_apiclient/classes.remoteingress.ts @@ -9,12 +9,14 @@ export class RemoteIngress { public name: string; public secret: string; public listenPorts: number[]; + public listenPortsUdp?: number[]; public enabled: boolean; public autoDerivePorts: boolean; public tags?: string[]; public createdAt: number; public updatedAt: number; public effectiveListenPorts?: number[]; + public effectiveListenPortsUdp?: number[]; public manualPorts?: number[]; public derivedPorts?: number[]; @@ -24,12 +26,14 @@ export class RemoteIngress { this.name = data.name; this.secret = data.secret; this.listenPorts = data.listenPorts; + this.listenPortsUdp = data.listenPortsUdp; this.enabled = data.enabled; this.autoDerivePorts = data.autoDerivePorts; this.tags = data.tags; this.createdAt = data.createdAt; this.updatedAt = data.updatedAt; this.effectiveListenPorts = data.effectiveListenPorts; + this.effectiveListenPortsUdp = data.effectiveListenPortsUdp; this.manualPorts = data.manualPorts; this.derivedPorts = data.derivedPorts; } @@ -52,11 +56,13 @@ export class RemoteIngress { const edge = response.edge; this.name = edge.name; this.listenPorts = edge.listenPorts; + this.listenPortsUdp = edge.listenPortsUdp; this.enabled = edge.enabled; this.autoDerivePorts = edge.autoDerivePorts; this.tags = edge.tags; this.updatedAt = edge.updatedAt; this.effectiveListenPorts = edge.effectiveListenPorts; + this.effectiveListenPortsUdp = edge.effectiveListenPortsUdp; this.manualPorts = edge.manualPorts; this.derivedPorts = edge.derivedPorts; } diff --git a/ts_interfaces/data/remoteingress.ts b/ts_interfaces/data/remoteingress.ts index 3de8bf9..86fc437 100644 --- a/ts_interfaces/data/remoteingress.ts +++ b/ts_interfaces/data/remoteingress.ts @@ -36,6 +36,64 @@ export interface IRemoteIngressStatus { activeTunnels: number; lastHeartbeat: number | null; connectedAt: number | null; + transportMode?: 'tcpTls' | 'quic' | 'quicWithFallback'; + fallbackUsed?: boolean; + performance?: IRemoteIngressPerformanceEffective; + flowControl?: IRemoteIngressFlowControlStatus; + queues?: IRemoteIngressQueueStatus; + traffic?: IRemoteIngressTrafficStatus; + udp?: IRemoteIngressUdpStatus; +} + +export type TRemoteIngressPerformanceProfile = 'balanced' | 'throughput' | 'highConcurrency'; + +export interface IRemoteIngressPerformanceConfig { + profile?: TRemoteIngressPerformanceProfile; + maxStreamsPerEdge?: number; + totalWindowBudgetBytes?: number; + minStreamWindowBytes?: number; + maxStreamWindowBytes?: number; + sustainedStreamWindowBytes?: number; + quicDatagramReceiveBufferBytes?: number; +} + +export interface IRemoteIngressPerformanceEffective { + profile: TRemoteIngressPerformanceProfile; + maxStreamsPerEdge: number; + totalWindowBudgetBytes: number; + minStreamWindowBytes: number; + maxStreamWindowBytes: number; + sustainedStreamWindowBytes: number; + quicDatagramReceiveBufferBytes: number; +} + +export interface IRemoteIngressFlowControlStatus { + applies: boolean; + currentWindowBytes: number; + minWindowBytes: number; + maxWindowBytes: number; + totalWindowBudgetBytes: number; + estimatedInFlightBytes: number; + stalledStreams: number; +} + +export interface IRemoteIngressQueueStatus { + ctrlQueueDepth: number; + dataQueueDepth: number; + sustainedQueueDepth: number; +} + +export interface IRemoteIngressTrafficStatus { + bytesIn: number; + bytesOut: number; + streamsOpenedTotal: number; + streamsClosedTotal: number; + rejectedStreams: number; +} + +export interface IRemoteIngressUdpStatus { + activeSessions: number; + droppedDatagrams: number; } /** diff --git a/ts_interfaces/requests/config.ts b/ts_interfaces/requests/config.ts index 40d4309..b8ac0d1 100644 --- a/ts_interfaces/requests/config.ts +++ b/ts_interfaces/requests/config.ts @@ -71,6 +71,7 @@ export interface IConfigData { hubDomain: string | null; tlsMode: 'custom' | 'acme' | 'self-signed'; connectedEdgeIps: string[]; + performance?: import('../data/remoteingress.js').IRemoteIngressPerformanceConfig; }; } diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index cff5a1f..3b0b8c4 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '13.21.1', + version: '13.22.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts_web/elements/network/ops-view-remoteingress.ts b/ts_web/elements/network/ops-view-remoteingress.ts index 3025e92..ed74fba 100644 --- a/ts_web/elements/network/ops-view-remoteingress.ts +++ b/ts_web/elements/network/ops-view-remoteingress.ts @@ -125,6 +125,18 @@ export class OpsViewRemoteIngress extends DeesElement { color: ${cssManager.bdTheme('#047857', '#34d399')}; border: 1px dashed ${cssManager.bdTheme('#6ee7b7', '#065f46')}; } + + .metricStack { + display: flex; + flex-direction: column; + gap: 2px; + font-size: 12px; + line-height: 1.35; + } + + .metricMuted { + color: var(--text-muted, #6b7280); + } `, ]; @@ -226,9 +238,13 @@ export class OpsViewRemoteIngress extends DeesElement { .displayFunction=${(edge: interfaces.data.IRemoteIngress) => ({ name: edge.name, status: this.getEdgeStatusHtml(edge), + transport: this.getTransportHtml(edge.id), publicIp: this.getEdgePublicIp(edge.id), ports: this.getPortsHtml(edge), tunnels: this.getEdgeTunnelCount(edge.id), + window: this.getWindowHtml(edge.id), + queues: this.getQueuesHtml(edge.id), + traffic: this.getTrafficHtml(edge.id), lastHeartbeat: this.getLastHeartbeat(edge.id), })} .dataActions=${[ @@ -459,6 +475,46 @@ export class OpsViewRemoteIngress extends DeesElement { return status?.activeTunnels || 0; } + private getTransportHtml(edgeId: string): TemplateResult | string { + const status = this.getEdgeStatus(edgeId); + if (!status?.connected) return '-'; + const mode = status.transportMode || 'unknown'; + const label = mode === 'quic' ? 'QUIC' : mode === 'tcpTls' ? 'TCP/TLS' : mode; + return html`
${label}${status.fallbackUsed ? 'fallback' : status.performance?.profile || 'default'}
`; + } + + private getWindowHtml(edgeId: string): TemplateResult | string { + const status = this.getEdgeStatus(edgeId); + if (!status?.connected || !status.flowControl) return '-'; + if (!status.flowControl.applies) { + return html`
native QUICmax ${status.performance?.maxStreamsPerEdge || '-'} streams
`; + } + return html` +
+ ${this.formatBytes(status.flowControl.currentWindowBytes)} window + ${this.formatBytes(status.flowControl.estimatedInFlightBytes)} est. in-flight +
+ `; + } + + private getQueuesHtml(edgeId: string): TemplateResult | string { + const status = this.getEdgeStatus(edgeId); + if (!status?.connected || !status.queues) return '-'; + return html`
C ${status.queues.ctrlQueueDepth} / D ${status.queues.dataQueueDepth}S ${status.queues.sustainedQueueDepth}
`; + } + + private getTrafficHtml(edgeId: string): TemplateResult | string { + const status = this.getEdgeStatus(edgeId); + if (!status?.connected || !status.traffic) return '-'; + const drops = (status.traffic.rejectedStreams || 0) + (status.udp?.droppedDatagrams || 0); + return html` +
+ ${this.formatBytes(status.traffic.bytesIn)} in / ${this.formatBytes(status.traffic.bytesOut)} out + ${drops} rejected/dropped +
+ `; + } + private getLastHeartbeat(edgeId: string): string { const status = this.getEdgeStatus(edgeId); if (!status?.lastHeartbeat) return '-'; @@ -467,4 +523,16 @@ export class OpsViewRemoteIngress extends DeesElement { if (ago < 3600000) return `${Math.floor(ago / 60000)}m ago`; return `${Math.floor(ago / 3600000)}h ago`; } + + private formatBytes(bytes: number): string { + if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let value = bytes; + let unitIndex = 0; + while (value >= 1024 && unitIndex < units.length - 1) { + value = value / 1024; + unitIndex++; + } + return `${value >= 10 || unitIndex === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unitIndex]}`; + } }