The Noise swiss army knife
noisecat is a netcat-style networking utility that speaks the Noise Protocol Framework over TCP. It can:
- Open authenticated, encrypted TCP sessions with any Noise handshake pattern (NN, NK, XX, IK, β¦) plus the standard PSK modifiers (psk0βpsk3).
- Talk to Lightning Network nodes over BOLT-8 (
Noise_XK_secp256k1_ChaChaPoly_SHA256with the spec's wire framing β passes Appendix A test vectors byte-for-byte). - Speak the NoiseSocket wire format, with an opt-in Reject/Retry/Switch negotiation layer.
- Speak WhatsApp's multi-device Noise wire protocol β handshake + pinned-certificate verification against the real backend, or peer-to-peer between two noisecat instances.
- Tunnel decrypted traffic to a plain-TCP backend (
-proxy), or run a command on connect (-e) β the classic netcat use cases, with the link encrypted by Noise.
Prebuilt binaries (Linux, macOS, FreeBSD, Windows; amd64 and arm64 where applicable) from the Releases page. Each archive ships noisecat, LICENSE, and README.md, plus a CycloneDX SBOM; a checksums.txt is published alongside and cosign-signed (keyless). Verify it with:
cosign verify-blob \
--certificate checksums.txt.pem \
--signature checksums.txt.sig \
--certificate-identity-regexp 'https://github.com/gedigi/noisecat/.+' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
checksums.txt# macOS Apple Silicon, replace the version with the one you want from the Releases page:
$ V=1.3.0
$ curl -L "https://github.com/gedigi/noisecat/releases/download/v${V}/noisecat_${V}_darwin_arm64.tar.gz" | tar -xz noisecatVia go install (requires Go 1.25+):
go install github.com/gedigi/noisecat/cmd/noisecat@latestBuild from source:
git clone https://github.com/gedigi/noisecat.git
cd noisecat
make linux darwin windows freebsd # cross-compile into bin/noisecat -h:
Usage: noisecat [options] [address] [port]
Options:
-4 use IPv4 only
-6 use IPv6 only
-e command
executes the given command
-k accepts multiple connections (-l && (-e || -proxy) required)
-keygen
generates "-proto" appropriate keypair and prints it to stdout
-l listens for incoming connections
-lstatic file
loads local keypair from file (use -keygen to generate)
-negotiation data
NoiseSocket negotiation_data (only used with -transport noisesocket)
-ns-fallback protocols
noisesocket client: comma-separated fallback protocols accepted on retry/switch
-ns-policy string
noisesocket listener action on unsupported proposal: reject|retry|switch (default reject)
-ns-support protocols
noisesocket listener: comma-separated supported protocols (enables negotiation)
-p port
uses source port (default "0")
-prologue prologue
application prologue mixed into the handshake hash
-proto protocol name
sets protocol name (default "Noise_NN_25519_AESGCM_SHA256")
-proxy address:port
forwards packets to address:port (-l required)
-psk pre-shared key
uses pre-shared key in handshake
-rstatic static key
defines remote static key (32 bytes, base64)
-s address
uses source address
-transport transport
wire transport: raw (default), noisesocket, bolt8 (auto-selected for secp256k1), or whatsapp (default "raw")
-u use UDP (reliable, via KCP) instead of TCP; raw transport + Curve25519 only
-v prints verbose output
-validate key
validate that the base64 key is well-formed for -proto's DH function, then exit
-w seconds
timeout in seconds for connect/handshake and idle connections (0 = none)
Protocol name format: Noise_PT_DH_CP_HS
Where:
PT: Handshake pattern
DH: Diffie-Hellman handshake function
CP: Cipher function
HS: Hash function
e.g. Noise_NN_25519_AESGCM_SHA256
Available handshake patterns:
NN, NK, NX, XN, XK,
XX, KN, KK, KX, IN,
IK, IX (each combinable with the psk0..psk3 modifier)
Available DH functions:
25519, secp256k1
Available Cipher functions:
ChaChaPoly, AESGCM
Available Hash functions:
BLAKE2s, BLAKE2b, SHA256, SHA512
Available transports:
raw, noisesocket, bolt8, whatsapp
The flags mirror traditional netcat:
-l -p 31337listens on TCP port 31337.-e /bin/shruns/bin/shon connect (reverse shell anyone?).-4/-6force the IPv4 or IPv6 address family (mutually exclusive).-w <seconds>bounds the connect + handshake, and closes a connection once it has been idle for that long.0(the default) means no timeout.-uruns over UDP instead of TCP. Noise needs a reliable, ordered stream, so the datagrams are carried by KCP (reliable-UDP ARQ) and the usualrawNoise framing runs on top β wire-compatible withraw-over-TCP at the Noise layer. Curve25519 /rawtransport only. Note: UDP has no TCP-style half-close, so interactive/bidirectional sessions (e.g. a reverse shell) work, but the send-then-EOF batch pattern (printf β¦ | noisecat -u β¦) can drop the trailing response.
The Noise-specific flags:
-protopicks the Noise protocol name.-pskis a base64-encoded 32-byte pre-shared key (e.g.head -c 32 /dev/urandom | base64). Pair it with a psk-modified protocol name such asNoise_NNpsk0_25519_AESGCM_SHA256("PSK before the first message") orNoise_NKpsk2_25519_AESGCM_SHA256("PSK after act 2"). Thepsk[0-3]modifier selects where the PSK token is mixed in, per Noise spec Β§9.2. Passing-pskwithout a psk modifier (or vice versa) is rejected at flag-parsing time.-rstaticis the remote peer's static (public) key β base64-encoded 32 bytes for Curve25519, 33 bytes (compressed) for secp256k1.-lstaticloads a local keypair JSON from disk. Generate one with-keygen.chmod 600it.-validate <key>runs the static-key validator and exits β useful for sanity-checking an-rstaticvalue before opening a connection.
Convenience flags:
-proxytunnels Noise traffic to a plain-TCP backend (client -noise-> noisecat -tcp-> endpoint).-kaccepts multiple connections (likencat -k).
Encrypted chat shell (default protocol, accepts multiple clients):
# Server
$ noisecat -k -e /bin/sh -l -p 4444
# Client
$ noisecat <server-ip> 4444Noise over UDP (reliable, via KCP):
# Server (bind a shell over UDP)
$ noisecat -u -l -p 4444 -e /bin/sh
# Client
$ noisecat -u <server-ip> 4444NoiseSocket round-trip with negotiation_data:
$ noisecat -transport noisesocket -negotiation 'app=demo' -l -p 4444 &
$ noisecat -transport noisesocket -negotiation 'app=demo' 127.0.0.1 4444Lightning Network handshake (BOLT-8):
# 1. Generate (or reuse) a local static keypair
$ noisecat -proto Noise_XK_secp256k1_ChaChaPoly_SHA256 -keygen > node.json
$ chmod 600 node.json
# 2. Connect to a Lightning node β rstatic is the node's compressed pubkey, base64-encoded
$ noisecat -proto Noise_XK_secp256k1_ChaChaPoly_SHA256 \
-lstatic node.json \
-rstatic <base64-encoded-33-byte-compressed-pubkey> \
<host> <port>The BOLT-8 transport is auto-selected when the protocol name contains secp256k1; the lightning prologue is supplied automatically.
Noise-protected TCP proxy:
# Backend (plain TCP)
$ python3 -m http.server 8000
# Noisecat proxy on 19999, forwarding decrypted traffic to the backend
$ noisecat -v -k -l -proxy 127.0.0.1:8000 -p 19999 127.0.0.1
# Client
$ printf 'GET / HTTP/1.0\r\n\r\n' | noisecat 127.0.0.1 19999The proxy uses TCP-style half-close, so a client that sends a request and closes its write side still receives the backend's full response.
noisecat speaks the Noise Protocol Framework over a pluggable transport layer. The same Noise handshake patterns and DH/cipher/hash combinations work regardless of transport β only the on-the-wire framing differs.
-transport |
Wire format | Notes |
|---|---|---|
raw (default) |
2-byte big-endian length prefix + Noise message | noisecat's historical framing; interoperable with itself only. |
noisesocket |
NoiseSocket spec: handshake messages carry negotiation_data, encrypted payloads contain an inner body_len + arbitrary padding, prologue is "NoiseSocketInit1" + neg_data_len + neg_data + app prologue |
Spec-compliant, Accept-only by default. Opt into the noisecat v1 negotiation layer (Reject / Retry / Switch) with the -ns-* flags below. |
bolt8 |
BOLT-8: Noise_XK_secp256k1_ChaChaPoly_SHA256, fixed-size handshake acts (50 / 50 / 66 bytes) with a 1-byte version prefix, encrypted 2-byte length headers + AEAD-tagged payloads, automatic rekey every 1000 messages, prologue defaults to "lightning" |
Auto-selected when -proto uses secp256k1. Interoperable with lnd / cln / eclair β Appendix A test vectors pass byte-for-byte. |
whatsapp |
WhatsApp multi-device: Noise_XX_25519_AESGCM_SHA256, a one-time WA header (prologue) + 3-byte length frames, handshake messages wrapped in protobuf, prologue is the WA header |
Connects to WhatsApp's real backend (handshake + pinned-certificate verification, no login) or peers two noisecat instances. See the WhatsApp section. |
Companion flags that work with any transport:
-prologue <string>mixes the given bytes into the handshake hash. Both peers must use the same value.-negotiation <string>(NoiseSocket only) is the initiator's first-messagenegotiation_data.
By default the noisesocket transport is Accept-only and spec-interoperable: the responder reads the initiator's negotiation_data and proceeds with the proposed protocol. The -ns-* flags activate an opt-in, noisecat-specific negotiation layer that adds the three remaining outcomes. It only interoperates noisecat-to-noisecat (like the raw transport).
-ns-support <p1,p2,β¦>(listener) β protocols the responder accepts, in preference order. Setting this enables negotiation on the listener.-ns-policy reject|retry|switch(listener, defaultreject) β what to do when the initiator proposes a protocol not in-ns-support:rejectβ refuse and close (initiator gets a clear error).retryβ ask the initiator to retry with the responder's preferred protocol.switchβ invert roles: the responder becomes the initiator of its preferred protocol.
-ns-fallback <p1,p2,β¦>(client) β protocols the initiator will accept if asked to retry or switch. Setting this enables negotiation on the client. The-protovalue is the initiator's proposal; a requested protocol outside{proto} βͺ fallbackaborts the handshake.
Each negotiation attempt mixes the prior transcript into the handshake prologue, so a tampered or stripped retry/switch makes the handshake fail (downgrade binding). The chain is capped at 4 attempts. A switch requires both sides to hold the key material the target protocol needs in their inverted roles (e.g. -lstatic/-rstatic), or the handshake aborts with an error.
Example β listener supports only XX, steers an NN client via retry:
# Listener: accept only XX; retry anything else
$ noisecat -transport noisesocket -l -p 4444 \
-ns-support Noise_XX_25519_AESGCM_SHA256 -ns-policy retry
# Client: propose NN, allow falling back to XX
$ noisecat -transport noisesocket 127.0.0.1 4444 \
-proto Noise_NN_25519_AESGCM_SHA256 \
-ns-fallback Noise_XX_25519_AESGCM_SHA256Swap -ns-policy retry for switch to have the listener drive the XX handshake itself, or reject to refuse outright.
WhatsApp's multi-device protocol uses the Noise Protocol Framework β specifically Noise_XX_25519_AESGCM_SHA256 β under a service-specific wire envelope: a one-time WA header (also mixed in as the Noise prologue), 3-byte big-endian length frames, and handshake messages wrapped in protobuf. The whatsapp transport implements that envelope, the way the bolt8 transport implements Lightning's. The wire details were derived from whatsmeow.
The transport has two modes.
With no address, the transport connects to WhatsApp's production websocket (wss://web.whatsapp.com/ws/chat), runs the Noise handshake, and verifies the server certificate chain against WhatsApp's pinned root key (an XEdDSA/Curve25519 chain: root β intermediate β leaf, with the leaf key bound to the handshake's server static).
$ noisecat -v -transport whatsapp
2026/06/13 23:26:45 Connected to ... using transport=whatsappThis proves protocol-level interoperability with the live backend. It does not log in: noisecat sends an empty ClientFinish payload, which the Noise layer accepts but WhatsApp's application layer rejects. Real login requires WhatsApp account credentials / QR pairing and the full Signal-protocol app layer (what whatsmeow/Baileys implement), which is out of scope. Use this for protocol research and interop testing only β not for messaging or automation that would violate WhatsApp's Terms of Service.
You cannot impersonate WhatsApp's real servers (you don't hold their key), but two noisecat instances can speak the WhatsApp framing to each other over plain TCP β like the other transports. With -l or a host port, the transport runs peer-to-peer with no certificate exchange. This gives you, for example, a bind shell tunneled over the WhatsApp wire protocol:
# Listener binds a shell, speaking the WhatsApp framing
$ noisecat -transport whatsapp -l -p 4444 -e /bin/sh
# Client connects (flags must precede the host/port)
$ noisecat -transport whatsapp 127.0.0.1 4444(-keygen/-lstatic work as usual to give an endpoint a fixed Noise identity; otherwise an ephemeral one is generated per connection.)
- No login, no messaging, no Signal/E2E app layer β handshake + certificate verification only against the real backend.
- The real-backend mode is a fixed websocket endpoint; the pinned root key and
WAheader version track whatsmeow and may need updating if WhatsApp rotates them. - The live handshake is covered by a test gated behind
NOISECAT_WA_LIVE=1(it needs network to a third-party service and never runs in CI).
make test # go test -race with coverage
make vet # go vet ./...
make lint # golangci-lint (install separately)
make linux darwin windows freebsd # cross-compileRequires Go 1.25+. CI runs build / vet / test on Linux, macOS, and Windows; lints via golangci-lint; and runs govulncheck for known CVEs on every push and PR. Tagging vX.Y.Z triggers a goreleaser build that publishes archives + sha256 checksums to the corresponding GitHub Release, each archive accompanied by a CycloneDX SBOM and the checksums file cosign-signed via GitHub OIDC. Dependabot keeps Go modules and Actions up to date weekly.
Bug reports, feature suggestions, and pull requests are welcome β open an issue on GitHub or send a PR against master. Please run go test -race ./... and golangci-lint run before submitting.
github.com/flynn/noisefor the Noise Protocol Framework primitives that therawandnoisesockettransports build on.github.com/decred/dcrd/dcrec/secp256k1for the pure-Go secp256k1 implementation underpinning the BOLT-8 transport.github.com/tulir/whatsmeowas the reference for WhatsApp's multi-device Noise wire format, certificate chain, and pinned root key.github.com/coder/websocketfor the WebSocket client the WhatsApp transport dials over, andfilippo.io/edwards25519for the XEdDSA certificate-signature verification.github.com/xtaci/kcp-gofor the reliable-UDP (ARQ) layer the-uUDP mode runs Noise over.github.com/mattn/go-shellwordsfor parsing the-ecommand line.