From 69be2295f1e02fe2ef5cc858a7886bd1fc9b9d19 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 17 Feb 2026 10:55:31 +0000 Subject: [PATCH] feat(remoteingress): derive effective remote ingress listen ports from route configs and expose them via ops API --- changelog.md | 9 +++ package.json | 2 +- pnpm-lock.yaml | 10 +-- ts/00_commitinfo_data.ts | 2 +- ts/classes.dcrouter.ts | 13 +++- .../handlers/remoteingress.handler.ts | 5 +- .../classes.remoteingress-manager.ts | 74 ++++++++++++++++++- ts_interfaces/data/remoteingress.ts | 24 ++++++ ts_interfaces/requests/remoteingress.ts | 2 +- ts_web/00_commitinfo_data.ts | 2 +- 10 files changed, 128 insertions(+), 15 deletions(-) diff --git a/changelog.md b/changelog.md index de9c054..fbf8b46 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2026-02-17 - 6.6.0 - feat(remoteingress) +derive effective remote ingress listen ports from route configs and expose them via ops API + +- Derive listen ports from SmartProxy route configs with remoteIngress.enabled; supports optional edgeFilter to target edges by id or tags. +- Add RemoteIngressManager.setRoutes(), derivePortsForEdge(), and getEffectiveListenPorts() which falls back to manual listenPorts when present. +- dcrouter now supplies route configs to RemoteIngressManager during initialization and when updating SmartProxy configuration to keep derived ports in sync. +- Ops API now returns effectiveListenPorts for edges; createRemoteIngress.listenPorts is optional and createEdge defaults listenPorts to an empty array. +- Bump dependency @serve.zone/remoteingress to ^3.0.4 to align types/behavior. + ## 2026-02-16 - 6.5.0 - feat(ops-view-remoteingress) add 'Create Edge Node' header action to remote ingress table and remove duplicate createNewAction diff --git a/package.json b/package.json index 3c69909..cc8c08e 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@push.rocks/smartstate": "^2.0.30", "@push.rocks/smartunique": "^3.0.9", "@serve.zone/interfaces": "^5.3.0", - "@serve.zone/remoteingress": "^3.0.2", + "@serve.zone/remoteingress": "^3.0.4", "@tsclass/tsclass": "^9.3.0", "lru-cache": "^11.2.6", "uuid": "^13.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d0759de..ffe6152 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,8 +96,8 @@ importers: specifier: ^5.3.0 version: 5.3.0 '@serve.zone/remoteingress': - specifier: ^3.0.2 - version: 3.0.2 + specifier: ^3.0.4 + version: 3.0.4 '@tsclass/tsclass': specifier: ^9.3.0 version: 9.3.0 @@ -1340,8 +1340,8 @@ packages: '@serve.zone/interfaces@5.3.0': resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==} - '@serve.zone/remoteingress@3.0.2': - resolution: {integrity: sha512-FnwNVO0Dn9xuNv0t81u6pjCizSeCyMjkRKm6wN5qycCdGFoJmFbBamHqV01KtK1KcgDTd7LX+PowSqKReNrBGw==} + '@serve.zone/remoteingress@3.0.4': + resolution: {integrity: sha512-ZD66Y8fvW7SjealziOlhaC7+Y/3gxQkZlj/X8rxgVHmGhlc/YQtn6H6LNVazbM88BXK5ns004Qo6ongAB6Ho0Q==} '@sindresorhus/is@5.6.0': resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} @@ -6830,7 +6830,7 @@ snapshots: '@push.rocks/smartlog-interfaces': 3.0.2 '@tsclass/tsclass': 9.3.0 - '@serve.zone/remoteingress@3.0.2': + '@serve.zone/remoteingress@3.0.4': dependencies: '@push.rocks/qenv': 6.1.3 '@push.rocks/smartrust': 1.2.1 diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index b133585..4c3a485 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: '6.5.0', + version: '6.6.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 1090773..8799cf9 100644 --- a/ts/classes.dcrouter.ts +++ b/ts/classes.dcrouter.ts @@ -955,10 +955,15 @@ export class DcRouter { // Update configuration this.options.smartProxyConfig = config; - + + // Update routes on RemoteIngressManager so derived ports stay in sync + if (this.remoteIngressManager && config.routes) { + this.remoteIngressManager.setRoutes(config.routes as any[]); + } + // Start new SmartProxy with updated configuration (will include email routes if configured) await this.setupSmartProxy(); - + console.log('SmartProxy configuration updated'); } @@ -1587,6 +1592,10 @@ export class DcRouter { this.remoteIngressManager = new RemoteIngressManager(this.storageManager); await this.remoteIngressManager.initialize(); + // Pass current routes so the manager can derive edge ports from remoteIngress-tagged routes + const currentRoutes = this.options.smartProxyConfig?.routes || []; + this.remoteIngressManager.setRoutes(currentRoutes as any[]); + // Create and start the tunnel manager this.tunnelManager = new TunnelManager(this.remoteIngressManager, { tunnelPort: this.options.remoteIngressConfig.tunnelPort ?? 8443, diff --git a/ts/opsserver/handlers/remoteingress.handler.ts b/ts/opsserver/handlers/remoteingress.handler.ts index afd8e18..2ecd1d0 100644 --- a/ts/opsserver/handlers/remoteingress.handler.ts +++ b/ts/opsserver/handlers/remoteingress.handler.ts @@ -20,10 +20,11 @@ export class RemoteIngressHandler { if (!manager) { return { edges: [] }; } - // Return edges without secrets + // Return edges without secrets, enriched with effective listen ports const edges = manager.getAllEdges().map((e) => ({ ...e, secret: '********', // Never expose secrets via API + effectiveListenPorts: manager.getEffectiveListenPorts(e), })); return { edges }; }, @@ -47,7 +48,7 @@ export class RemoteIngressHandler { const edge = await manager.createEdge( dataArg.name, - dataArg.listenPorts, + dataArg.listenPorts || [], dataArg.tags, ); diff --git a/ts/remoteingress/classes.remoteingress-manager.ts b/ts/remoteingress/classes.remoteingress-manager.ts index bc0dc13..b453bdd 100644 --- a/ts/remoteingress/classes.remoteingress-manager.ts +++ b/ts/remoteingress/classes.remoteingress-manager.ts @@ -1,9 +1,30 @@ import * as plugins from '../plugins.js'; import type { StorageManager } from '../storage/classes.storagemanager.js'; -import type { IRemoteIngress } from '../../ts_interfaces/data/remoteingress.js'; +import type { IRemoteIngress, IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js'; const STORAGE_PREFIX = '/remote-ingress/'; +/** + * Flatten a port range (number | number[] | Array<{from, to}>) to a sorted unique number array. + */ +function extractPorts(portRange: number | number[] | Array<{ from: number; to: number }>): number[] { + const ports = new Set(); + if (typeof portRange === 'number') { + ports.add(portRange); + } else if (Array.isArray(portRange)) { + for (const entry of portRange) { + if (typeof entry === 'number') { + ports.add(entry); + } else if (typeof entry === 'object' && 'from' in entry && 'to' in entry) { + for (let p = entry.from; p <= entry.to; p++) { + ports.add(p); + } + } + } + } + return [...ports].sort((a, b) => a - b); +} + /** * Manages CRUD for remote ingress edge registrations. * Persists edge configs via StorageManager and provides @@ -12,6 +33,7 @@ const STORAGE_PREFIX = '/remote-ingress/'; export class RemoteIngressManager { private storageManager: StorageManager; private edges: Map = new Map(); + private routes: IDcRouterRouteConfig[] = []; constructor(storageManager: StorageManager) { this.storageManager = storageManager; @@ -30,12 +52,60 @@ export class RemoteIngressManager { } } + /** + * Store the current route configs for port derivation. + */ + public setRoutes(routes: IDcRouterRouteConfig[]): void { + this.routes = routes; + } + + /** + * 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. + * When edgeFilter is absent, the route applies to all edges. + */ + public derivePortsForEdge(edgeId: string, edgeTags?: string[]): number[] { + const ports = new Set(); + + for (const route of this.routes) { + if (!route.remoteIngress?.enabled) continue; + + // Apply edge filter if present + const filter = route.remoteIngress.edgeFilter; + if (filter && filter.length > 0) { + const idMatch = filter.includes(edgeId); + const tagMatch = edgeTags?.some((tag) => filter.includes(tag)) ?? false; + if (!idMatch && !tagMatch) continue; + } + + // Extract ports from the route match + if (route.match?.ports) { + for (const p of extractPorts(route.match.ports)) { + ports.add(p); + } + } + } + + return [...ports].sort((a, b) => a - b); + } + + /** + * Get the effective listen ports for an edge. + * Returns manual listenPorts if non-empty, otherwise derives ports from tagged routes. + */ + public getEffectiveListenPorts(edge: IRemoteIngress): number[] { + if (edge.listenPorts && edge.listenPorts.length > 0) { + return edge.listenPorts; + } + return this.derivePortsForEdge(edge.id, edge.tags); + } + /** * Create a new edge registration. */ public async createEdge( name: string, - listenPorts: number[], + listenPorts: number[] = [], tags?: string[], ): Promise { const id = plugins.uuid.v4(); diff --git a/ts_interfaces/data/remoteingress.ts b/ts_interfaces/data/remoteingress.ts index 3fc8b17..93b8b32 100644 --- a/ts_interfaces/data/remoteingress.ts +++ b/ts_interfaces/data/remoteingress.ts @@ -1,3 +1,5 @@ +import type { IRouteConfig } from '@push.rocks/smartproxy'; + /** * A stored remote ingress edge registration. */ @@ -23,3 +25,25 @@ export interface IRemoteIngressStatus { lastHeartbeat: number | null; connectedAt: number | null; } + +/** + * Route-level remote ingress configuration. + * When attached to a route, signals that traffic for this route + * should be accepted from remote edge nodes. + */ +export interface IRouteRemoteIngress { + /** Whether this route receives traffic from edge nodes */ + enabled: boolean; + /** Optional filter: only edges whose id or tags match get this route's ports. + * When absent, the route applies to all edges. */ + edgeFilter?: string[]; +} + +/** + * Extended route config used within dcrouter. + * Adds the optional `remoteIngress` property to SmartProxy's IRouteConfig. + * SmartProxy ignores unknown properties at runtime. + */ +export type IDcRouterRouteConfig = IRouteConfig & { + remoteIngress?: IRouteRemoteIngress; +}; diff --git a/ts_interfaces/requests/remoteingress.ts b/ts_interfaces/requests/remoteingress.ts index 2c6677b..21ff936 100644 --- a/ts_interfaces/requests/remoteingress.ts +++ b/ts_interfaces/requests/remoteingress.ts @@ -17,7 +17,7 @@ export interface IReq_CreateRemoteIngress extends plugins.typedrequestInterfaces request: { identity?: authInterfaces.IIdentity; name: string; - listenPorts: number[]; + listenPorts?: number[]; tags?: string[]; }; response: { diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index b133585..4c3a485 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: '6.5.0', + version: '6.6.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' }