p2p-netcat
v3.3.0
Published
TCP/QUIC netcat-like CLI addressed by libp2p PeerId instead of IP
Maintainers
Readme
p2p-netcat
English | Русский
p2p-nc is a JavaScript command-line utility inspired by netcat. Instead of
the server's IP address, it uses a cryptographic libp2p PeerId. Its transport
is compatible with the IPFS networking stack: QUIC v1, TCP, Noise, Yamux, IPFS
Amino DHT, mDNS, and Circuit Relay v2.
Documentation
- Project context and maintainer handoff — current packages, decisions, architecture, invariants, limitations, source map, and continuation checklist for developers and language models;
- Detailed architecture and connection algorithm — identity, discovery, route selection, CLI, browser, relay, encryption, backpressure, and failure handling;
- Private pairing and wire protocol — canonical CBOR token, rotating rendezvous, encrypted signaling, mutual admission, RouteRecord, interoperability vectors, and the Go port boundary;
- Installation guide — npm one-liner, first connection, updates, removal, identities, and troubleshooting;
- Native WebRTC migration record — completed dependency removal, signaling design, and soak-test procedure;
- Browser PWA guide — build, GitHub Pages,
network-config.json, and WSS relay configuration; - Shared JavaScript library API — browser-safe
p2p-netcat-core, also available to CLI consumers asp2p-netcat/core; - Programmatic Circuit Relay API — start and stop a relay
from another Node.js application through
p2p-netcat/relay. - gs-netcat-compatible modes —
-d,-p,-q,-S,-T, and-i, including forwarding, SOCKS, PTY, and Tor routing. - Publishing guide — release order, npm verification, and clean-install checks for the CLI, core, and prebuilt web PWA.
What already works
- Bidirectional, binary-transparent stdin/stdout transfer;
- QUIC v1 as the preferred direct transport, with automatic TCP fallback;
- A stable server PeerId derived from a local Ed25519 key;
- Logical ports, allowing one PeerId to expose multiple services;
- PeerId-only discovery through mDNS, signed GossipSub announcements, and the IPFS Amino DHT;
- optional private pairing tokens with rotating DHT keys, encrypted signaling, mutual admission, and no public PeerId lookup;
- a project-owned direct WebRTC path that races signed public Nostr relays and WebTorrent trackers in both Node.js and the browser;
- Nostr trickle ICE and WebTorrent full-SDP fallback implemented in the project-owned core without a third-party WebRTC orchestration runtime;
- a shared nine-endpoint STUN pool for CLI and browser WebRTC NAT traversal;
- bounded end-to-end flow control from
node-ptythrough the transport and Web Worker to xterm; - automatic recovery of a temporarily lost WebRTC peer for 120 seconds without terminating the existing PTY;
- Circuit Relay v2 connections for nodes behind NAT;
- A built-in relay mode;
- gs-netcat-style TCP forwarding, SOCKS4/4a/5, quiet, Tor, and true PTY modes;
-l,-k,-w,-d,-p,-q,-S,-T,-i,-z,-e,-4,-6, and verbose mode;- Authenticated encryption through QUIC TLS 1.3 or Noise, including connections through a relay.
Netcat-style UDP datagram mode (-u) is deliberately rejected for now. QUIC
does use UDP underneath, but it still provides a reliable, ordered stream.
Installation
Node.js 22 or newer is required. The QUIC transport uses a native N-API module
with prebuilt binaries for mainstream macOS, Linux, and Windows platforms.
Interactive -i uses the native node-pty module; on Linux, installing from
npm may require Python, make, and a C/C++ compiler for node-gyp.
The current js-libp2p dependency set does not support Node.js 18. On an older Ubuntu installation, upgrade before running the CLI:
nvm install 22
nvm use 22
node --versionThe CLI checks this before importing libp2p and reports the required version
instead of a secondary CustomEvent, Promise.withResolvers, or PubSub cleanup
error.
Install the published CLI from npm:
npm install --global p2p-netcatFor development from source:
npm install
npm linkAfter npm link, both commands are available: p2p-nc and the shorter pnc.
QUIC is enabled by default. Use --no-quic only for diagnostics or on a system
where the native QUIC module is unavailable. -p now follows gs-netcat and is
used for TCP forwarding. The internal libp2p TCP/UDP listen port is configured
with --transport-port; when it is 0, the operating system chooses each port
independently. -i now means interactive PTY, so the short identity option is
-I while --identity remains unchanged.
WebRTC is native-only in every mode. The automated local matrix is available
through npm run soak:webrtc; profiles and limitations are documented in the
native WebRTC migration record.
Quick start
On the first computer:
p2p-nc -l 8080The command prints the persistent PeerId to stderr, for example:
[p2p-nc] слушатель:8080 PeerId: 12D3KooWLs9pvVfwbo6yHsYB66kRLq2RJwfH3bBQZhp94kerbFd9On the second computer, copy the printed PeerId and run:
p2p-nc 12D3KooWLs9pvVfwbo6yHsYB66kRLq2RJwfH3bBQZhp94kerbFd9 8080Once connected, everything typed in either terminal is sent to the other one. The PeerId above is illustrative: the connection command must contain the value printed by your own listener.
To print the PeerId without starting a listener:
p2p-nc idPrivate pairing
Generate a bearer token from the same persistent identity used by the listener:
p2p-nc token 8080 --identity ~/.config/p2p-netcat/identity.keyTransfer the resulting pnc1_... value through a trusted channel. On both
computers, prefer an environment variable so the secret is not present in the
process arguments:
export P2P_NETCAT_TOKEN='pnc1_...'The listener and client can now omit PeerId and port:
p2p-nc -l -i
p2p-nc -iRun those commands on different computers. Private mode publishes rotating secret-derived provider CIDs, encrypts native WebRTC signaling, and performs mutual token admission before PTY or stream data is accepted. See the wire specification.
The server key is created at ~/.config/p2p-netcat/identity.key with 0600
permissions. Deleting the key creates a new PeerId. The client identity is
ephemeral unless --identity is supplied explicitly.
Verifiable local example without manual copying
The following commands run entirely on one computer:
DEMO_KEY=/tmp/p2p-netcat-demo.key
DEMO_ID="$(p2p-nc id --identity "$DEMO_KEY")"
p2p-nc -l 8080 --identity "$DEMO_KEY" > /tmp/p2p-netcat-received.txt &
DEMO_PID=$!
sleep 2
printf 'hello over PeerId\n' | p2p-nc "$DEMO_ID" 8080
wait "$DEMO_PID"
cat /tmp/p2p-netcat-received.txtNodes behind NAT: running your own relay
The public IPFS DHT is used for discovery, but public IPFS nodes do not promise unlimited relaying of arbitrary streams. For predictable connectivity between two nodes behind NAT, run a relay once on a VPS with both TCP and UDP port 9090 open:
p2p-nc relay -4 -p 9090 \
--announce /ip4/203.0.113.10/udp/9090/quic-v1 \
--announce /ip4/203.0.113.10/tcp/9090In a real command, --announce must contain the VPS's public address. The relay
prints its complete multiaddr. Assume it prints:
/ip4/203.0.113.10/udp/9090/quic-v1/p2p/12D3KooWK4bicbvfPNGzfuMBf6xE43ecgB26NHDZRTfLM7CNh9yw
/ip4/203.0.113.10/tcp/9090/p2p/12D3KooWK4bicbvfPNGzfuMBf6xE43ecgB26NHDZRTfLM7CNh9ywServer behind NAT:
p2p-nc -l 8080 --relay /ip4/203.0.113.10/udp/9090/quic-v1/p2p/12D3KooWK4bicbvfPNGzfuMBf6xE43ecgB26NHDZRTfLM7CNh9ywThe client behind NAT uses the same relay but addresses the server only by its PeerId:
p2p-nc --relay /ip4/203.0.113.10/udp/9090/quic-v1/p2p/12D3KooWK4bicbvfPNGzfuMBf6xE43ecgB26NHDZRTfLM7CNh9yw 12D3KooWJ7satLo5LXjhSZBMVTWRG1AZ77sQYtX81qHHf2VtscdL 8080Traffic between the client and server remains end-to-end encrypted. The relay can see the participants' PeerIds and traffic volume and timing, but it cannot read the contents.
The same relay can be embedded into another Node.js process without spawning the CLI:
import { startRelay } from 'p2p-netcat/relay'
const relay = await startRelay({
identityPath: './data/p2p-netcat-relay.key',
localPort: 9090,
websocketPort: 9091,
enableMdns: false
})
console.log(relay.peerId, relay.addresses)
await relay.stop()See the programmatic relay guide for every option, persistent identity, signal handling, public announce addresses, and WSS.
Browser PWA client
The web directory contains a fully static React client without SSR, API
routes, a database, or server-side scripts. The libp2p networking stack runs in
a Web Worker. A Service Worker caches the application shell for offline PWA
startup and updates it in the background. The page includes a terminal widget,
text and file transfer, EOF, and binary-stream download.
Build the static files:
cd web
npm install
npm run buildThe result is written to web/dist and can be placed on any HTTPS static file
host; the application needs no backend. Opening it through file:// is not a
supported mode because browsers require a secure context—HTTPS or localhost—
for Service Workers and PWA installation.
A ready-to-use .github/workflows/pages.yml workflow is included for GitHub
Pages. In the repository settings, select Settings → Pages → Source → GitHub
Actions. The workflow automatically uses the repository subpath as Vite's
base URL. The deployed page is English by default and links to the complete
Russian interface at ?lang=ru.
The browser does not require a relay address by default. It starts native WebRTC signaling through signed Nostr events and public WebTorrent trackers in parallel with libp2p lookup through signed GossipSub announcements, HTTP Delegated Routing, and IPFS Amino DHT fallback. The first authenticated channel wins. Discovered libp2p multiaddrs are also raced.
GossipSub discovery is enabled by default in CLI nodes, relays, and the browser
Worker. Use --no-pubsub on the CLI or relay command to disable it. It is an
additional discovery path, not a global rendezvous guarantee: both peers need
connectivity to a compatible GossipSub mesh. Native WebRTC uses the shared STUN
pool to improve direct NAT traversal; STUN alone cannot replace TURN or Circuit
Relay when both sides are behind restrictive NAT.
If the server advertises only TCP/QUIC addresses or automatic discovery cannot find a usable route, expand the advanced settings and provide a WebSocket relay:
p2p-nc relay -4 -p 9090 --websocket-port 9091Locally, the browser can use the printed address containing
/tcp/9091/ws/p2p/. When the page is hosted on HTTPS, publish the relay over
WSS, typically through a TLS reverse proxy on port 443. See
web/README.md for the complete guide.
Shared JavaScript library
Logic that must behave identically in the CLI and browser lives in the
p2p-netcat-core npm package. It uses no Node.js APIs and
owns logical-port and protocol-ID rules, PeerId/multiaddr validation, WS/WSS
relay validation, Circuit Relay route planning, and transport preference.
Applications that already depend on the CLI package can use its subpath export:
import { createRelayDialPlan, protocolForService } from 'p2p-netcat/core'Browser-only packages should keep using the smaller standalone
p2p-netcat-core package to avoid installing Node-only CLI transports. The CLI
imports the same core, while the browser imports it from its
Web Worker. Platform adapters remain separate: stdin/stdout, local identities,
QUIC, and the DHT belong to the CLI; DOM integration, Worker RPC, and the PWA
lifecycle belong to web. This gives future discovery and fallback strategies
one shared interface instead of two diverging implementations.
import { createRelayDialPlan, protocolForService } from 'p2p-netcat-core'
const protocol = protocolForService(31337)
const plan = createRelayDialPlan({
peerId: '12D3KooWQ3uxpHgjDKE6vGmvzKS8RPbxUDLwJ7XCLaD6YXdUfbR9',
service: 31337,
relay: '/dns4/relay.example/tcp/443/wss/p2p/12D3KooWEqeQRAJ61HSv9yMPk8yzjke7NxmTFcvFt4GzwXxzVjXW',
requireWebSocket: true,
secureContext: true
})gs-netcat-style modes
Expose 192.168.6.7:22 on the server side and listen locally on client port
2222. Every local TCP connection receives an independent P2P stream:
# Server side
p2p-nc -l -d 192.168.6.7 -p 22 31337
# Client side
p2p-nc -p 2222 12D3KooWQ3uxpHgjDKE6vGmvzKS8RPbxUDLwJ7XCLaD6YXdUfbR9 31337
ssh -p 2222 [email protected]Run a SOCKS4/4a/5 proxy on the remote node and expose it as local port 1080:
# Server side
p2p-nc -l -S 31337
# Client side
p2p-nc -p 1080 12D3KooWQ3uxpHgjDKE6vGmvzKS8RPbxUDLwJ7XCLaD6YXdUfbR9 31337
curl --proxy socks5h://127.0.0.1:1080 https://example.com/Open a real PTY login shell. Terminal resize and control bytes are forwarded;
press Ctrl-e q to disconnect the client:
p2p-nc -l -i 31337
p2p-nc -i 12D3KooWQ3uxpHgjDKE6vGmvzKS8RPbxUDLwJ7XCLaD6YXdUfbR9 31337Before connecting the browser client to this listener, enable
Interactive PTY -i. The web terminal renders ANSI and forwards keyboard and
resize events through the shared p2p-netcat-core PTY codec.
-q suppresses diagnostics on stderr without changing application bytes.
-T re-executes the client under torsocks, disables direct QUIC/WebRTC and
automatic discovery, and therefore requires an explicit TCP/WS/WSS Circuit
Relay route:
P2P_NETCAT_TOR_HOST=127.0.0.1 \
P2P_NETCAT_TOR_PORT=9050 \
p2p-nc -T \
--relay /ip4/203.0.113.10/tcp/9090/p2p/12D3KooWEqeQRAJ61HSv9yMPk8yzjke7NxmTFcvFt4GzwXxzVjXW \
12D3KooWQ3uxpHgjDKE6vGmvzKS8RPbxUDLwJ7XCLaD6YXdUfbR9 31337Tor, torsocks, and a reachable relay are required. See the
complete compatibility guide for mode validation,
security boundaries, and environment variables.
Equivalents of common netcat commands
Check reachability without exchanging data:
p2p-nc -z -v 12D3KooWLs9pvVfwbo6yHsYB66kRLq2RJwfH3bBQZhp94kerbFd9 8080Transfer a file:
p2p-nc -l 9000 > received.tar.gz
p2p-nc 12D3KooWLs9pvVfwbo6yHsYB66kRLq2RJwfH3bBQZhp94kerbFd9 9000 < archive.tar.gzRun a command for each connection:
p2p-nc -l -k 7000 -e '/bin/sh -i'The -e option gives the connected user the privileges of the local process.
Use it only with trusted PeerIds and inside an isolated environment.
Complete command reference:
p2p-nc --help
p2p-nc relay --helpHow a connection is established
- The listener loads its persistent key; the hash of the public key becomes its PeerId.
- Logical port
8080becomes the libp2p protocol/p2p-netcat/1.0.0/8080. - The server publishes a provider record for its PeerId CID to the Amino DHT.
- The client searches known addresses, mDNS, signed GossipSub announcements, that provider record, and the DHT.
- With
--relay, it builds arelay/p2p-circuit/p2p/serverroute. - Without an explicit route, native Nostr/WebTorrent WebRTC and libp2p lookup run in parallel.
- QUIC TLS 1.3, Noise, or a signed WebRTC challenge authenticates the PeerId.
- stdin/stdout or PTY frames are transferred with bounded backpressure. The browser acknowledges output only after xterm has rendered it.
- A transient WebRTC disconnect enters a 120-second reconnecting state. The existing PTY and bounded output queue survive when the same peer returns.
With -v, the CLI reports the active libp2p and WebRTC search branches, open
Nostr relay/WebTorrent tracker counts, elapsed search time, candidate
authentication, the selected signaling strategy, WebRTC/ICE state changes, and
session recovery or grace-period expiry.
The architecture document describes every CLI and browser branch, timeout, cache, concurrent address race, and trust boundary in detail.
HTTP/3 is not part of the application protocol. Direct peers use raw libp2p QUIC streams, avoiding unnecessary HTTP request, header, and CONNECT semantics.
Practical MVP limitations
- PeerId-only internet connections work after a provider record containing a
public or relayed server address has been published. The first publication
may take about a minute;
-vreports when it succeeds. - Servers behind CGNAT must use an available Circuit Relay v2. The most reliable
option is to pass the same
--relayto both the server and client. - WebRTC improves direct connectivity, but symmetric NAT or blocked UDP may still require TURN or Circuit Relay; public trackers provide no SLA.
- The native adapters connect to several public Nostr relays and WebTorrent trackers and automatically reconnect. Third-party infrastructure cannot guarantee 100% availability.
- A backgrounded page can resume the same PTY while its JavaScript context remains alive. A browser that fully discards or reloads the page creates a new ephemeral signaling identity and therefore starts a new PTY session.
- PubSub improves discovery only after a node reaches a compatible mesh member; it is not a global rendezvous service.
- An IPFS HTTP gateway is not a relay and cannot carry this protocol.
- Networks that block UDP automatically fall back to TCP when both addresses
are known;
--no-quicdisables QUIC explicitly. - In
-kmode without-e, shared stdin is inconvenient for concurrent clients; incoming streams are limited to one active session. - PeerId allowlist authorization and datagram mode are natural next steps, but they are not implemented in this version.
Development
npm test
npm run lint
npm --prefix packages/core test
cd web && npm ci && npm test && npm run lint