/** Result of an external-IP detection cycle. */ export interface ExternalIpResult { /** IP reported by the upstream router (UPnP/NAT-PMP gateway), if reachable. */ routerIp: string | null; /** IP reported by a public IP echo service. */ publicIp: string | null; /** * True when both probes succeeded AND returned different addresses, which * is the textbook CGNAT signature: the router thinks its WAN address is * private to the carrier. */ cgnat: boolean; /** When this snapshot was taken. */ detectedAt: Date; } /** Abstract router-side probe (gateway WAN address). */ export type RouterIpProbe = () => Promise; export interface ExternalIpDetectorOptions { /** Probe the local router's external IP (typically via the port mapper). */ routerProbe?: RouterIpProbe; /** Public IP service endpoints. */ publicIpUrls?: ReadonlyArray; /** Fetch override (tests stub this). Defaults to global fetch. */ fetch?: typeof fetch; /** Clock override for tests. */ now?: () => Date; /** Per-request timeout in ms (default 5000). */ requestTimeoutMs?: number; } /** * External-IP detector. * * Two independent sources: * - The router (queried via the port mapper's gateway interface). May be * unavailable if UPnP is off or the upstream device doesn't expose * WANIPConnection. * - A public HTTPS echo service (api.ipify.org and friends). May be * unreachable on a firewalled / offline machine. * * When both succeed AND disagree, we flag CGNAT. The check is heuristic — a * fast-changing IP, a sliced VPN, or an IPv6-only public probe could all * trigger a false positive. The verdict is informational, not enforced. */ export declare class ExternalIpDetector { private readonly routerProbe; private readonly publicIpUrls; private readonly fetchImpl; private readonly nowFn; private readonly requestTimeoutMs; constructor(opts?: ExternalIpDetectorOptions); /** Run both probes in parallel and return the snapshot. */ detect(): Promise; private runRouterProbe; private runPublicProbe; private tryPublicUrl; } /** * Liberal IP validation — accepts both IPv4 and IPv6 in dotted/colon form. * Tighter validation lives at the consumer (`nat-service` checks for IPv4 * before building an `/ip4/` multiaddr). */ export declare function isPlausibleIp(s: string): boolean;