import * as plugins from '../plugins.js'; import type { IDcRouterRouteConfig, IRemoteIngress, IRemoteIngressEgressPolicyConfig, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig, TRemoteIngressHubSettingsUpdate, TRemoteIngressPerformanceProfile } from '../../ts_interfaces/data/remoteingress.js'; import { REMOTE_INGRESS_MAIL_TAG } from '../../ts_interfaces/data/remoteingress.js'; import { RemoteIngressEdgeDoc, RemoteIngressHubSettingsDoc } from '../db/index.js'; /** Operator-declared host identity of an edge (public IPs + PTR-aligned mail hostname). */ export interface IRemoteIngressEdgeHostInfo { publicIp?: string; publicIpV6?: string; mailHostname?: string; } const fqdnRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i; export interface IRemoteIngressFirewallConfig { blockedIps?: string[]; } type TPerformanceIntegerField = | 'maxStreamsPerEdge' | 'totalWindowBudgetBytes' | 'minStreamWindowBytes' | 'maxStreamWindowBytes' | 'sustainedStreamWindowBytes' | 'quicDatagramReceiveBufferBytes' | 'streamFramePayloadBytes' | 'firstDataConnectTimeoutMs' | 'clientWriteTimeoutMs'; const performanceIntegerMaxByField: Record = { 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; const maxRemoteIngressEgressListEntries = 128; const defaultTunnelPort = 8443; function hasOwn(objectArg: object, keyArg: string): boolean { return Object.prototype.hasOwnProperty.call(objectArg, keyArg); } function extractPorts(portRange: plugins.smartproxy.IRouteConfig['match']['ports']): number[] { const ports = new Set(plugins.smartproxy.expandPortRange(portRange) as number[]); 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[] = []; private firewallConfig?: IRemoteIngressFirewallConfig; private hubPublicIps: string[] = []; private onEdgesChanged?: (reason: string) => void; private hubSettings: IRemoteIngressHubSettings = { enabled: false, tunnelPort: defaultTunnelPort, updatedAt: 0, updatedBy: 'default', }; constructor() {} /** * Set the hub's own public IPs. Edge publicIp values equal to any of these * are rejected — the hub IP must never become an edge-fronted DNS target. */ public setHubPublicIps(ips: Array): void { this.hubPublicIps = ips.filter((ip): ip is string => Boolean(ip)); } /** * Register a callback fired after every edge mutation (create/update/delete) * so dependent state — mail DNS records — can reconcile. */ public setOnEdgesChanged(callback: (reason: string) => void): void { this.onEdgesChanged = callback; } private notifyEdgesChanged(reason: string): void { this.onEdgesChanged?.(reason); } /** * 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, performance: doc.performance, egress: this.normalizeEgressPolicy(doc.egress), tags: doc.tags, publicIp: doc.publicIp, publicIpV6: doc.publicIpV6, mailHostname: doc.mailHostname, createdAt: doc.createdAt, updatedAt: doc.updatedAt, }; this.edges.set(edge.id, edge); } await this.initializeHubSettings(); } private async initializeHubSettings(): Promise { let doc = await RemoteIngressHubSettingsDoc.load(); if (!doc) { doc = new RemoteIngressHubSettingsDoc(); doc.settingsId = 'remote-ingress-hub-settings'; doc.enabled = false; doc.tunnelPort = defaultTunnelPort; doc.hubDomain = ''; doc.updatedAt = Date.now(); doc.updatedBy = 'default'; await doc.save(); } this.hubSettings = this.toHubSettings(doc); } /** * Store the current route configs for port derivation. */ public setRoutes(routes: IDcRouterRouteConfig[]): void { this.routes = routes; } /** * Set the full desired firewall snapshot pushed to all edges. */ public setFirewallConfig(firewallConfig?: IRemoteIngressFirewallConfig): void { 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: TRemoteIngressHubSettingsUpdate, updatedBy: string, ): Promise { let doc = await RemoteIngressHubSettingsDoc.load(); if (!doc) { doc = new RemoteIngressHubSettingsDoc(); doc.settingsId = 'remote-ingress-hub-settings'; doc.enabled = false; doc.tunnelPort = defaultTunnelPort; } const normalized = this.normalizeHubSettingsUpdate(updates); if (hasOwn(normalized, 'enabled')) { doc.enabled = normalized.enabled; } if (hasOwn(normalized, 'tunnelPort')) { doc.tunnelPort = normalized.tunnelPort; } if (hasOwn(updates, 'hubDomain')) { doc.hubDomain = normalized.hubDomain || ''; } if (hasOwn(updates, 'performance')) { doc.performance = normalized.performance || undefined; } 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. * 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, performance?: IRemoteIngressPerformanceConfig, egress?: IRemoteIngressEgressPolicyConfig, hostInfo?: IRemoteIngressEdgeHostInfo, ): 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, performance, egress: this.normalizeEgressPolicy(egress), tags: tags || [], publicIp: this.normalizeEdgePublicIp(hostInfo?.publicIp, 4), publicIpV6: this.normalizeEdgePublicIp(hostInfo?.publicIpV6, 6), mailHostname: this.normalizeMailHostname(hostInfo?.mailHostname), createdAt: now, updatedAt: now, }; const doc = new RemoteIngressEdgeDoc(); Object.assign(doc, edge); await doc.save(); this.edges.set(id, edge); this.notifyEdgesChanged(`edge created: ${name}`); 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; performance?: IRemoteIngressPerformanceConfig; egress?: IRemoteIngressEgressPolicyConfig; tags?: string[]; /** null clears the stored value. */ publicIp?: string | null; publicIpV6?: string | null; mailHostname?: string | null; }, ): 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.performance !== undefined) edge.performance = updates.performance; if (updates.egress !== undefined) edge.egress = this.normalizeEgressPolicy(updates.egress); if (updates.tags !== undefined) edge.tags = updates.tags; if (updates.publicIp !== undefined) { edge.publicIp = updates.publicIp === null ? undefined : this.normalizeEdgePublicIp(updates.publicIp, 4); } if (updates.publicIpV6 !== undefined) { edge.publicIpV6 = updates.publicIpV6 === null ? undefined : this.normalizeEdgePublicIp(updates.publicIpV6, 6); } if (updates.mailHostname !== undefined) { edge.mailHostname = updates.mailHostname === null ? undefined : this.normalizeMailHostname(updates.mailHostname); } edge.updatedAt = Date.now(); const doc = await RemoteIngressEdgeDoc.findById(id); if (doc) { Object.assign(doc, edge); await doc.save(); } this.edges.set(id, edge); this.notifyEdgesChanged(`edge updated: ${edge.name}`); 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); this.notifyEdgesChanged(`edge deleted: ${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[]; firewallConfig?: IRemoteIngressFirewallConfig; performance?: IRemoteIngressPerformanceConfig; egress?: IRemoteIngressEgressPolicyConfig }> { const result: Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig; performance?: IRemoteIngressPerformanceConfig; egress?: IRemoteIngressEgressPolicyConfig }> = []; for (const edge of this.edges.values()) { if (edge.enabled) { const listenPortsUdp = this.getEffectiveListenPortsUdp(edge); const performance = edge.performance && Object.keys(edge.performance).length > 0 ? edge.performance : undefined; const egress = edge.egress?.enabled ? edge.egress : undefined; result.push({ id: edge.id, secret: edge.secret, listenPorts: this.getEffectiveListenPorts(edge), ...(listenPortsUdp.length > 0 ? { listenPortsUdp } : {}), ...(this.firewallConfig ? { firewallConfig: this.firewallConfig } : {}), ...(performance ? { performance } : {}), ...(egress ? { egress } : {}), }); } } return result; } /** * Resolve enabled edges whose id or tags match any entry in the filter * (same semantics as route-level edgeFilter in derivePortsForEdge). */ public resolveEdgesByFilter(filter: string[]): IRemoteIngress[] { if (!Array.isArray(filter) || filter.length === 0) { return []; } const result: IRemoteIngress[] = []; for (const edge of this.edges.values()) { if (!edge.enabled) continue; const idMatch = filter.includes(edge.id); const tagMatch = edge.tags?.some((tag) => filter.includes(tag)) ?? false; if (idMatch || tagMatch) { result.push(edge); } } return result; } /** Enabled edges carrying the well-known 'mail' tag. */ public getMailEdges(): IRemoteIngress[] { return this.resolveEdgesByFilter([REMOTE_INGRESS_MAIL_TAG]); } private normalizeEdgePublicIp(value: string | undefined, family: 4 | 6): string | undefined { if (value === undefined) { return undefined; } const ip = `${value}`.trim(); if (!ip) { return undefined; } if (plugins.smartnetwork.getIpVersion(ip) !== family) { throw new Error(`Edge publicIp${family === 6 ? 'V6' : ''} must be a valid IPv${family} address`); } if (this.hubPublicIps.includes(ip)) { throw new Error(`Edge public IP ${ip} equals the hub public IP — the hub IP must never front edge traffic`); } return ip; } private normalizeMailHostname(value: string | undefined): string | undefined { if (value === undefined) { return undefined; } const hostname = `${value}`.trim().toLowerCase().replace(/\.$/, ''); if (!hostname) { return undefined; } if (hostname.length > 253 || !fqdnRegex.test(hostname)) { throw new Error('Edge mailHostname must be a fully qualified domain name'); } return hostname; } private normalizeEgressPolicy( egress?: IRemoteIngressEgressPolicyConfig, ): IRemoteIngressEgressPolicyConfig | undefined { if (!egress) { return undefined; } const enabled = Boolean(egress.enabled); const next: IRemoteIngressEgressPolicyConfig = { enabled }; if (!enabled) { return next; } const allowedPorts = this.normalizeEgressAllowedPorts(egress.allowedPorts || [25]); next.allowedPorts = allowedPorts; if (egress.allowPrivateRanges !== undefined) { next.allowPrivateRanges = Boolean(egress.allowPrivateRanges); } if (egress.deniedCidrs !== undefined) { if (!Array.isArray(egress.deniedCidrs) || egress.deniedCidrs.length > maxRemoteIngressEgressListEntries) { throw new Error(`RemoteIngress egress deniedCidrs must contain at most ${maxRemoteIngressEgressListEntries} entries`); } const deniedCidrs = [...new Set(egress.deniedCidrs.map((cidr) => `${cidr}`.trim()).filter(Boolean))]; if (deniedCidrs.length > 0) { next.deniedCidrs = deniedCidrs; } } if (egress.allowedHostPatterns !== undefined) { if (!Array.isArray(egress.allowedHostPatterns) || egress.allowedHostPatterns.length > maxRemoteIngressEgressListEntries) { throw new Error(`RemoteIngress egress allowedHostPatterns must contain at most ${maxRemoteIngressEgressListEntries} entries`); } const allowedHostPatterns = [...new Set(egress.allowedHostPatterns.map((pattern) => `${pattern}`.trim()).filter(Boolean))]; if (allowedHostPatterns.length > 0) { next.allowedHostPatterns = allowedHostPatterns; } } if (egress.maxConcurrentStreams !== undefined) { const maxConcurrentStreams = Number(egress.maxConcurrentStreams); if (!Number.isSafeInteger(maxConcurrentStreams) || maxConcurrentStreams < 1 || maxConcurrentStreams > 100_000) { throw new Error('RemoteIngress egress maxConcurrentStreams must be a positive safe integer no greater than 100000'); } next.maxConcurrentStreams = maxConcurrentStreams; } return next; } private normalizeEgressAllowedPorts(ports: number[]): number[] { if (!Array.isArray(ports) || ports.length === 0 || ports.length > maxRemoteIngressEgressListEntries) { throw new Error(`RemoteIngress egress allowedPorts must contain 1-${maxRemoteIngressEgressListEntries} ports`); } const allowedPorts = [...new Set(ports.map((port) => Number(port)))].sort((a, b) => a - b); for (const port of allowedPorts) { if (!Number.isInteger(port) || port < 1 || port > 65535) { throw new Error('RemoteIngress egress allowedPorts must contain valid TCP ports'); } if (port !== 25) { throw new Error('RemoteIngress egress is currently limited to outbound SMTP port 25'); } } return allowedPorts; } private normalizeHubSettingsUpdate( updates: TRemoteIngressHubSettingsUpdate, ): TRemoteIngressHubSettingsUpdate { const next: TRemoteIngressHubSettingsUpdate = {}; if (hasOwn(updates, 'enabled') && updates.enabled !== undefined) { next.enabled = Boolean(updates.enabled); } if (hasOwn(updates, 'tunnelPort') && updates.tunnelPort !== undefined) { const tunnelPort = Number(updates.tunnelPort); if (!Number.isInteger(tunnelPort) || tunnelPort < 1 || tunnelPort > 65535) { throw new Error('tunnelPort must be a valid TCP port'); } next.tunnelPort = tunnelPort; } if (hasOwn(updates, 'hubDomain')) { const hubDomain = `${updates.hubDomain || ''}`.trim(); next.hubDomain = hubDomain || undefined; } if (hasOwn(updates, 'performance')) { next.performance = updates.performance === null ? undefined : this.normalizePerformanceConfig(updates.performance || undefined); } return next; } 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)[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 { enabled: doc.enabled ?? false, tunnelPort: doc.tunnelPort ?? defaultTunnelPort, hubDomain: doc.hubDomain || undefined, performance: doc.performance, updatedAt: doc.updatedAt, updatedBy: doc.updatedBy, }; } }