import { type DdnsProviderInfo, type ManualForwardPatch, type NatHandlers, type NatSettingsFile, type NatStatusSnapshot } from './types.js'; import { NatStore } from './nat-store.js'; import { ExternalIpDetector } from './external-ip.js'; import { type PortMapper } from './port-mapper.js'; import { type SecretsStore } from './secrets/index.js'; import { type NodeAddressesStaleListener } from './address-watch.js'; import type { ManagedNodeInfo, NodeStateListener } from '../orchestrator/types.js'; /** * How long a node may sit `stopped` before its mappings are released. Longer * than the hosted-node supervisor's whole respawn backoff (150 s), so a crash * followed by a respawn keeps its mapping. */ export declare const NAT_UNMAP_GRACE_MS: number; /** The backstop reconcile cadence; state-change events trigger passes between ticks. */ export declare const NAT_RECONCILE_INTERVAL_MS = 60000; /** * How often the external IP is re-detected, and how often gateway discovery is * retried while UPnP is on and no router has answered. */ export declare const NAT_IP_REDETECT_INTERVAL_MS: number; /** * The orchestrator surface the service needs: the node list and cadre-host's * own state-change subscription. `HostProcessOrchestrator` satisfies this; the * hosted-node supervisor declares the same slice. */ export interface NatNodeSource { listNodes(): ManagedNodeInfo[]; onStateChange(listener: NodeStateListener): () => void; } /** Listener notified when the status snapshot changed in a way the UI should follow. */ export type NatChangeListener = (snapshot: NatStatusSnapshot) => void; export interface NatServiceOptions { /** Cadre-host root directory (same one the orchestrator and the hosted-node store use). */ rootDir: string; /** Where the hosted nodes and their ports come from. */ nodeSource: NatNodeSource; /** Clock override for tests. */ now?: () => Date; /** Fetch override for tests. */ fetch?: typeof fetch; /** Test-only: stub the keytar/file-store. Default: createSecretsStore. */ secretsStore?: SecretsStore; /** Test-only: stub the router. Default: `UpnpPortMapper`. */ portMapper?: PortMapper; /** Test-only: stub the external-IP detector. */ externalIpDetector?: ExternalIpDetector; /** Optional: re-use an existing NatStore (mostly for tests). */ store?: NatStore; /** Lease TTL asked of the router. Default 1 h. */ leaseTtlMs?: number; /** Test-only: stub the interface read behind `network: auto`. Default: `publicInterfaceAddress()`. */ publicInterfaceAddress?: () => string | null; } /** * NatService — one mapping table for every node the host runs, keyed by node * id, with a route per port (TCP and WebSocket); external-IP detection; DDNS; * and the public addresses a node announces. * * Lifecycle: * - `start()`: load settings, discover the router (bounded), detect the * external IP, start DDNS, then map the running nodes in the background. * - `stop()`: clear timers only — mappings outlive the host process. * - `getStatus()`: snapshot from cached state (cheap). * - `publicAddressesFor(nodeId, ports)`: a node's public multiaddrs, also * from cached state, predicting the identity mapping for a port that has * no route yet. * - `onNodeAddressesStale(listener)`: asked to restart a running node whose * announced addresses no longer match `publicAddressesFor`, checked after * every pass and every settings write (see `AddressWatch`). * * One reconcile pass against the node source behind three triggers (start, * node state change, a 1-minute timer), plus one renewal pass for every lease * every `DEFAULT_REFRESH_MS`. Passes are serialized on one promise tail, so a * state change during a timer pass can neither map a port twice nor unmap a * port the other pass is mapping. * * NOTE: this file holds the settings/DDNS glue, the mapping table and the status * assembly together (about 890 lines; the stale-address check lives in * `address-watch.ts` for that reason); when the next capability lands here, move * the mapping table (`NodeEntry` through `releaseUpnpRoutes`) into its own module. */ export declare class NatService { private readonly nodeSource; private readonly store; private readonly nowFn; private readonly fetchImpl; private readonly portMapper; private readonly leaseTtlMs; private readonly secretsRootDir; private secretsStore; /** Caller-supplied detector — used as-is when present (skips the router-probe stitch). */ private readonly injectedDetector; private detector; private ddnsUpdater; private currentSettings; private gateway; /** The public IPv4 an interface holds (`publicInterfaceAddress`), re-read at start and on every probe. */ private publicInterfaceIp; private readonly readInterfaces; private latestIp; private lastTestedAt; private readonly table; private started; private tail; /** True while an event-triggered pass is queued and not yet started (crash storms coalesce into it). */ private eventPassQueued; private timers; private unsubscribe; private readonly changeListeners; private lastSignature; private readonly addressWatch; constructor(opts: NatServiceOptions); /** * Discover the router and detect the external IP (both awaited, both * bounded), start DDNS, then map re-attached running nodes in the background * so the management API comes up promptly. Idempotent. */ start(): Promise; /** * Clear timers and release client resources. Idempotent. * * NOTE: deliberately unmaps nothing. Hosted children are detached and keep * running across a host restart (an update restart included), so their * mappings must outlive this process; they expire on their own within the * lease TTL if the host stays down, and the next `start()` re-maps the * re-attached nodes (re-mapping the same internal port is idempotent on the * router). */ stop(): Promise; /** Cheap read of the current snapshot. */ getStatus(): NatStatusSnapshot; /** Current settings (cheap read; for handlers to surface to UI). */ getSettings(): NatSettingsFile; /** * The public multiaddrs for a node, from cached state. A port with no route * yet is predicted to land on the identity mapping when UPnP is enabled and * a gateway was discovered — callers ask at spawn time, before the mapping * for a brand-new node exists. A port whose mapping attempt failed is not * predicted. */ publicAddressesFor(nodeId: string, ports: { p2p: number; ws?: number; }): string[]; /** * Register a listener fired whenever a node's routes, the gateway, the * external IP, the CGNAT flag, the UPnP toggle or the DDNS settings change. * Returns an unsubscribe fn. */ onChange(listener: NatChangeListener): () => void; /** * Register a listener asked to restart a running node whose announced addresses * (`ManagedNodeInfo.announcedAddrs`) differ from `publicAddressesFor` — at most * once per node per `NAT_ADDRESS_RESTART_MIN_INTERVAL_MS`. Returns an unsubscribe fn. */ onNodeAddressesStale(listener: NodeAddressesStaleListener): () => void; /** List available DDNS providers (UI listing for configuration). */ listDdnsProviders(): DdnsProviderInfo[]; /** * One reconcile pass against the node source: map running nodes, release * nodes stopped past the grace, forget terminated ones. Serialized against * every other pass, so a caller may get a queued pass rather than an * immediate one. */ reconcile(): Promise; /** * One renewal pass: re-request every UPnP-eligible mapping of every running * node, failed ones included. Each renewal is isolated; one failure does not * stop the pass. A router that answers with another external port updates * the route. */ renewMappings(): Promise; /** * The 5-minute probe pass: re-read the interfaces and re-route every node when * the mode `auto` lands on changes; while UPnP is in use and no router has * answered, search again and map the running nodes as soon as one does — a * host that boots before its network, or a router that comes up later, is * found here rather than at the next restart; then re-detect the external IP. */ redetect(): Promise; /** Re-discover the router if needed, re-map everything, re-detect the IP, then return a fresh snapshot. */ testReachability(): Promise; /** Replace settings; persists to disk and reconfigures the runtime. */ putSettings(patch: Partial>): Promise; /** * Set DDNS configuration and persist secret values into the SecretsStore. * Throws if the provider is unknown or required secrets are missing. */ putDdns(body: { providerId: string; hostname: string; config: Record; externallyManaged?: boolean; }): Promise; /** * Record the external ports the user forwarded by hand for one node (`null` * clears a port). A port that became manual releases its UPnP mapping; a * port whose entry was cleared is re-requested over UPnP. */ putForward(nodeId: string, patch: ManualForwardPatch): Promise; private enqueue; /** Fire-and-forget pass for the timer / event / startup triggers. */ private sweep; /** * One pass per burst of events: an event while a pass is queued joins it, * and an event during a running pass queues exactly one more, so a node that * spawned after the running pass listed the nodes is mapped by the next pass * rather than by the timer. */ private queueEventPass; private reconcileOnce; /** * A node no longer listed was terminated (`removeContainer`): release its * mappings at once and drop its manual forward. A forward left behind for a * node that vanished while the host was down goes the same way. */ private forgetMissingNodes; private reconcileNode; /** * Bring one node's routes in line with the settings: manual wins over UPnP; * UPnP off releases every `upnp` route; with `mapMissing`, a port with no * route and no recorded failure (or an expired lease) is requested. */ private reconcileRoutes; private renewOnce; /** * Ask the router for one port. A refusal records `error` on that port only. * * NOTE: a router that stops answering costs each attempt its 10 s timeout, so a * pass over N mapped ports can take 10 s × N; passes are serialized, so they queue * rather than overlap. If that ever shows, skip ports whose last failure was a * timeout until the next renewal pass. */ private mapRoute; private unmapRoute; private releaseUpnpRoutes; private kindsOf; private leaseExpired; /** A UPnP route whose lease ran out reads as no route, keeping any recorded failure. */ private effectiveRoute; private discoverGateway; /** * Detect the external IP. A detection that finds nothing (or throws), or that * lost the public echo the previous one had, keeps the previous result: one * failed probe must not drop every node's public address, and a router-only * answer must not flip the CGNAT flag the two probes had agreed on. */ private detectIp; private knowsLessThanPrevious; private ipDetector; /** The detected external IP for display and DDNS: the public probe's, else the router's. */ private detectedIp; /** The IP used in `/ip4/` addresses: only a public IPv4 counts. Public mode prefers the interface's own. */ private publicIpv4; /** * Re-read the interfaces (a VPS gains an address, a laptop moves networks); true when that * changed the mode in effect, so the caller re-routes every node in the same pass. */ private refreshNetworkMode; /** The mode in effect: the `network` setting, or what `auto` reads from the interfaces. */ private networkMode; /** UPnP is asked for nothing in public mode: there is no router to ask. */ private usesUpnp; /** An interface holding the public address rules out carrier-grade NAT, whatever the probes say. */ private cgnatDetected; private describeNode; private hostKnown; private addressesFor; private routeOrPrediction; private ddnsStatus; /** * The end of every pass and every settings write: tell the change listeners * when the snapshot moved, then ask for a restart of every running node whose * announced addresses went stale. Running the check here rather than only on a * change is what lets a difference deferred by the restart rate limit fire on the * first timer pass after the limit ends. */ private publish; /** Fire the change listeners when the snapshot differs (timestamps aside) from the last one they saw. */ private notifyIfChanged; } /** * Wrap a NatService into the typed handler shape consumed by the local UI * server. Same shape as `createUpdateHandlers`. */ export declare function createNatHandlers(service: NatService): NatHandlers;