import * as plugins from '../plugins.js'; import type { IRemoteIngress, IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js'; import { RemoteIngressEdgeDoc } from '../db/index.js'; /** * Flatten a port range (number | number[] | Array<{from, to}>) to a sorted unique number array. */ function extractPorts(portRange: number | Array): 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 smartdata document classes and provides * the allowed edges list for the Rust hub. */ export class RemoteIngressManager { private edges: Map = new Map(); private routes: IDcRouterRouteConfig[] = []; constructor() { } /** * Load all edge registrations from the database into memory. */ public async initialize(): Promise { const docs = await RemoteIngressEdgeDoc.findAll(); for (const doc of docs) { // Migration: old edges without autoDerivePorts default to true if ((doc as any).autoDerivePorts === undefined) { doc.autoDerivePorts = true; await doc.save(); } const edge: IRemoteIngress = { id: doc.id, name: doc.name, secret: doc.secret, listenPorts: doc.listenPorts, listenPortsUdp: doc.listenPortsUdp, enabled: doc.enabled, autoDerivePorts: doc.autoDerivePorts, tags: doc.tags, createdAt: doc.createdAt, updatedAt: doc.updatedAt, }; this.edges.set(edge.id, edge); } } /** * 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); } /** * Derive UDP listen ports for an edge from routes with transport 'udp' or 'all'. * These ports need UDP listeners on the edge (e.g. for QUIC/HTTP3). */ public deriveUdpPortsForEdge(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; } // Only include ports from routes that listen on UDP const transport = route.match?.transport; if (transport === 'udp' || transport === 'all') { 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. * Manual ports are always included. Auto-derived ports are added (union) when autoDerivePorts is true. */ public getEffectiveListenPorts(edge: IRemoteIngress): number[] { const manualPorts = edge.listenPorts || []; const shouldDerive = edge.autoDerivePorts !== false; if (!shouldDerive) return [...manualPorts].sort((a, b) => a - b); const derivedPorts = this.derivePortsForEdge(edge.id, edge.tags); return [...new Set([...manualPorts, ...derivedPorts])].sort((a, b) => a - b); } /** * Get the effective UDP listen ports for an edge. * Manual UDP ports are always included. Auto-derived UDP ports are added when autoDerivePorts is true. */ public getEffectiveListenPortsUdp(edge: IRemoteIngress): number[] { const manualPorts = edge.listenPortsUdp || []; const shouldDerive = edge.autoDerivePorts !== false; if (!shouldDerive) return [...manualPorts].sort((a, b) => a - b); const derivedPorts = this.deriveUdpPortsForEdge(edge.id, edge.tags); return [...new Set([...manualPorts, ...derivedPorts])].sort((a, b) => a - b); } /** * Get manual and derived port breakdown for an edge (used in API responses). * Derived ports exclude any ports already present in the manual list. */ public getPortBreakdown(edge: IRemoteIngress): { manual: number[]; derived: number[] } { const manual = edge.listenPorts || []; const shouldDerive = edge.autoDerivePorts !== false; if (!shouldDerive) return { manual, derived: [] }; const manualSet = new Set(manual); const allDerived = this.derivePortsForEdge(edge.id, edge.tags); const derived = allDerived.filter((p) => !manualSet.has(p)); return { manual, derived }; } /** * Create a new edge registration. */ public async createEdge( name: string, listenPorts: number[] = [], tags?: string[], autoDerivePorts: boolean = true, ): Promise { const id = plugins.uuid.v4(); const secret = plugins.crypto.randomBytes(32).toString('hex'); const now = Date.now(); const edge: IRemoteIngress = { id, name, secret, listenPorts, enabled: true, autoDerivePorts, tags: tags || [], createdAt: now, updatedAt: now, }; const doc = new RemoteIngressEdgeDoc(); Object.assign(doc, edge); await doc.save(); this.edges.set(id, edge); return edge; } /** * Get an edge by ID. */ public getEdge(id: string): IRemoteIngress | undefined { return this.edges.get(id); } /** * Get all edge registrations. */ public getAllEdges(): IRemoteIngress[] { return Array.from(this.edges.values()); } /** * Update an edge registration. */ public async updateEdge( id: string, updates: { name?: string; listenPorts?: number[]; autoDerivePorts?: boolean; enabled?: boolean; tags?: string[]; }, ): Promise { const edge = this.edges.get(id); if (!edge) { return null; } if (updates.name !== undefined) edge.name = updates.name; if (updates.listenPorts !== undefined) edge.listenPorts = updates.listenPorts; if (updates.autoDerivePorts !== undefined) edge.autoDerivePorts = updates.autoDerivePorts; if (updates.enabled !== undefined) edge.enabled = updates.enabled; if (updates.tags !== undefined) edge.tags = updates.tags; edge.updatedAt = Date.now(); const doc = await RemoteIngressEdgeDoc.findById(id); if (doc) { Object.assign(doc, edge); await doc.save(); } this.edges.set(id, edge); return edge; } /** * Delete an edge registration. */ public async deleteEdge(id: string): Promise { if (!this.edges.has(id)) { return false; } const doc = await RemoteIngressEdgeDoc.findById(id); if (doc) { await doc.delete(); } this.edges.delete(id); return true; } /** * Regenerate the secret for an edge. */ public async regenerateSecret(id: string): Promise { const edge = this.edges.get(id); if (!edge) { return null; } edge.secret = plugins.crypto.randomBytes(32).toString('hex'); edge.updatedAt = Date.now(); const doc = await RemoteIngressEdgeDoc.findById(id); if (doc) { Object.assign(doc, edge); await doc.save(); } this.edges.set(id, edge); return edge.secret; } /** * Verify an edge's secret using constant-time comparison. */ public verifySecret(id: string, secret: string): boolean { const edge = this.edges.get(id); if (!edge) { return false; } const expected = Buffer.from(edge.secret); const provided = Buffer.from(secret); if (expected.length !== provided.length) { return false; } return plugins.crypto.timingSafeEqual(expected, provided); } /** * Get the list of allowed edges (enabled only) for the Rust hub. * Includes listenPortsUdp when routes with transport 'udp' or 'all' are present. */ public getAllowedEdges(): Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[] }> { const result: Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[] }> = []; for (const edge of this.edges.values()) { if (edge.enabled) { const listenPortsUdp = this.getEffectiveListenPortsUdp(edge); result.push({ id: edge.id, secret: edge.secret, listenPorts: this.getEffectiveListenPorts(edge), ...(listenPortsUdp.length > 0 ? { listenPortsUdp } : {}), }); } } return result; } }