/** * Core relay capability — boot-time sanity check. * * # Why this exists * * `nodeRole: 'core'` is purely self-declared today. A node can advertise itself * as a Core relay and immediately fail at the job because none of its bound * interfaces are reachable from the public internet. **beacon-01** in the * v10.0.0-rc.10 incident was the canonical case: it bound only to its Tailscale * CGNAT address (`100.99.142.87`) and could not have functioned as a relay * regardless of slot state. With this check in place, that misconfiguration * surfaces at boot in the operator's logs as `[CORE-PREREQ] looks degraded` * instead of being a network-wide silent failure. * * # Design * * Pure functions, no I/O, no libp2p dependency at this layer. Callers provide * the inputs (`listenAddresses`, `os.networkInterfaces()` snapshot, etc.) and * decide what to do with the result (`looksDegraded` + structured `reasons`). * The classifier is the testable kernel; the lifecycle wiring is one layer up. * * # Two-pass usage * * - **Pre-start pass**: classify the host's interfaces + the configured * listenAddresses *before* `libp2p.start()` resolves wildcards. Catches * "no public interface on the host" early — operator sees the warning * before paying the boot cost. * - **Post-start pass**: re-run with the transport manager's listener * addresses — those are the actually-bound addresses after libp2p's * address-resolver has done its thing (NAT64, `/ip4/0.0.0.0` → * per-interface expansion, etc.). This is the authoritative pass; the * pre-start pass is just a heads-up. Do not use `libp2p.getMultiaddrs()` * here: that is the advertised self-address set and can include public * announce addresses or `/p2p-circuit` relay reservations. * * # Order of classifier rules (matters) * * Wildcard rule first — `/ip4/0.0.0.0` and `/ip6/::` bind to every interface, * so the address's class is the *best* class across the host's interfaces (if * any interface is public, the binding is effectively public). Without the * host-interface lookup, the wildcard alone is unclassifiable. Then the * specific ranges in narrowing order — loopback → CGNAT → RFC1918 → * link-local → ULAv6 → reserved (0/8, 198.18/15, 240/4 etc.) — so a * more-specific class wins over a less-specific one. The IPv4 classifier is * "reject if matches any reserved/non-global range, else public" rather than * a small allow-list, so blocks like `198.18.0.0/15` (RFC 2544 benchmarking) * and `240.0.0.0/4` (RFC 1112 reserved) don't sneak through as `public`. * * DNS multiaddrs deliberately classify as `dns` rather than resolving at * boot (DNS resolution is async + flaky + the answer can change). DNS listen * addresses are not themselves proof of public reachability. DNS announce * addresses with a non-reserved hostname rescue an otherwise-degraded * listener as **indeterminate** — we can't prove it's public, but it's not a * footgun like a private IP either, so the verdict is `looksDegraded: false` * with `indeterminate: true` so strict (`allowDegradedRelay: false`) operators * don't refuse-to-boot on a stable DNS deployment. Reserved DNS names * (`localhost`, `*.local`, `*.test`, `*.example`, `*.invalid`, single-label) * are excluded from indeterminate rescue — they can't resolve to a public IP. */ import type { NetworkInterfaceInfo } from 'node:os'; export type AddrClassification = 'public' | 'rfc1918' | 'cgnat' | 'loopback' | 'linkLocal' | 'ulaIpv6' | 'dns' | 'multicast' | 'relayed' | 'reserved' | 'wildcardNoPublicInterface' | 'unknown'; export interface CorePrereqResult { /** Multiaddrs that classify as `public` (or wildcard-with-a-public-interface). */ publicListenAddresses: string[]; /** Everything else, with each entry's resolved class. */ nonRoutableAddresses: Array<{ addr: string; class: AddrClassification; }>; /** * True iff `publicListenAddresses` is empty AND no `announceAddresses` entry * can rescue the result. Operators with literal public announce addresses * (the VPS-with-static-IP case) are not degraded when they still have at * least one bound wildcard / private-LAN listen address. DNS announce * addresses with a non-reserved hostname also rescue (as indeterminate — * see `indeterminate` below) so a stable DNS deployment doesn't refuse to * boot under strict `allowDegradedRelay: false`. A node with zero bound * listen addresses is always degraded: an announce address cannot make an * unbound transport serve relay traffic. */ looksDegraded: boolean; /** * True when the result is non-degraded only because a DNS announce * address rescued it. The pure checker can't prove the DNS hostname * resolves to a publicly reachable IP, so it lets the boot proceed but * marks the verdict as soft. Strict-mode lifecycle layers MAY surface * a clear warning when this is set instead of treating it as an OK * verdict; resolving DNS in the lifecycle layer would let them upgrade * to a hard verdict either way. */ indeterminate: boolean; /** * Human-readable reasons (one per failure mode hit). Empty when * `looksDegraded === false` and `indeterminate === false`. Logged verbatim * by the lifecycle wiring. */ reasons: string[]; } export interface CheckCoreRelayPrereqsOpts { /** * Multiaddrs the daemon configured itself to listen on. Pre-start pass: * the values from the config (may include `/ip4/0.0.0.0/...` wildcards). * Post-start pass: pass transport listener addresses here instead — those * are the post-wildcard-expansion addresses libp2p actually bound. */ listenAddresses: string[]; /** * `os.networkInterfaces()` snapshot. Injected (not read here) so tests * can run deterministically without touching the real host. The wildcard * classifier walks this list to figure out whether any interface gives * the wildcard binding a public reach. Empty / undefined means "no * interface info available" — wildcards then classify as * `wildcardNoPublicInterface` since we have no positive evidence of a * public interface. */ hostInterfaces?: ReadonlyArray; /** * Optional multiaddrs the daemon advertises to the network (the VPS / * cloud case where the public IP isn't bound to a local interface). * A literal public-IP announce address rescues an otherwise-degraded result * only when the node also has at least one wildcard/private-LAN listener. * DNS announce addresses are intentionally not resolved here (DNS is * async + flaky); a non-reserved DNS hostname triggers an *indeterminate* * rescue (`looksDegraded: false, indeterminate: true`) so strict-mode * operators don't refuse-to-boot on stable DNS deployments. Reserved DNS * hostnames (`localhost`, `*.local`, `*.test`, `*.example`, `*.invalid`, * single-label) are excluded from indeterminate rescue. */ announceAddresses?: string[]; /** * Only `'core'` nodes get a degraded verdict — `'edge'` nodes are clients * and don't need to serve traffic, so the check is informational at most. * Pass through for callers that want the classification regardless. */ nodeRole: 'core' | 'edge'; } /** * Classify a single multiaddr string. * * Multiaddr format is `/proto1/value1/proto2/value2/...` — we only need the * first two segments (transport address; the rest is port + circuit/relay * tail). String split keeps us free of a runtime multiaddr-parser * dependency. */ export declare function classifyMultiaddr(addr: string, hostInterfaces?: ReadonlyArray): AddrClassification; /** * Run the full prereq check. Returns the structured result; never throws. * * Callers (lifecycle.ts) format the result for operator logs and decide * whether to escalate via the `core.allowDegradedRelay` config gate * (default `true` — warn-only, no behaviour change for backcompat). */ export declare function checkCoreRelayPrereqs(opts: CheckCoreRelayPrereqsOpts): CorePrereqResult; //# sourceMappingURL=core-prereq-check.d.ts.map