Dependency-free NAT port mapping for Node.js via UPnP, NAT-PMP, and PCP.
npm install portcullis
DISCLAIMER: For now this is for personal use and may have flaws, it's entirely built by claude (with some guidance) so my confidence is not super high. It works well for me right now.
import { createClient } from "portcullis";
const client = await createClient();
// Map external port 8080 to local port 8080 (TCP)
const mapping = await client.map({ publicPort: 8080 });
console.log(`Mapped ${mapping.publicPort} -> ${mapping.privatePort} (${mapping.natProtocol})`);
// Check your public IP
const ip = await client.externalIp();
console.log(`Reachable at ${ip}:${mapping.publicPort}`);
// Clean up
await client.unmap({ publicPort: 8080 });
await client.close();Auto-detects the gateway and probes protocols in order: PCP, NAT-PMP, UPnP. Returns the first client that successfully communicates with the gateway.
Options:
gateway?: string— Gateway IP. Auto-detected if omitted.timeout?: number— Timeout in ms for network operations (default: 10000).signal?: AbortSignal— Cancel detection/probing.logger?: { debug(msg: string): void }— Optional debug logger.
Create a port mapping on the gateway.
publicPort: number— External port (0-65535).privatePort?: number— Internal port (defaults topublicPort).protocol?: "tcp" | "udp"— Transport protocol (default:"tcp").ttl?: number— Lifetime in seconds (default: 7200).description?: string— Human-readable label (UPnP only — silently ignored by NAT-PMP and PCP).
Remove a port mapping.
publicPort: number— External port to unmap.protocol?: "tcp" | "udp"— Transport protocol (default:"tcp").
Get the external (public) IP address of the gateway.
Release resources (abort in-flight UDP requests, clear state).
Use these when you want to target a specific protocol:
import { createUpnpClient, createPmpClient, createPcpClient, detectGateway } from "portcullis";
const gateway = await detectGateway();
const upnp = await createUpnpClient(gateway); // UPnP (most widely supported)
const pmp = createPmpClient(gateway); // NAT-PMP
const pcp = createPcpClient(gateway); // PCP (most modern)import {
createClient,
GatewayNotFoundError,
NoProtocolError,
TimeoutError,
ProtocolError,
} from "portcullis";
try {
const client = await createClient();
await client.map({ publicPort: 8080 });
} catch (err) {
if (err instanceof GatewayNotFoundError) {
// Not connected to a network / can't find router
} else if (err instanceof NoProtocolError) {
// Router doesn't support UPnP, NAT-PMP, or PCP
} else if (err instanceof TimeoutError) {
// Network operation timed out
} else if (err instanceof ProtocolError) {
// Protocol-level error (e.g., port already mapped)
}
}See docs/examples.md for detailed usage patterns including mapping renewal, HTTP server integration, and protocol detection.
- Node.js 20+