Multiplexed reliable and unreliable channels over an unreliable datagram
transport, in portable C11, with optional encryption and an ssh-shaped login.
No dependencies, no package manager, and no build system of its own beyond a
single vendored GNUmakefile.
It is meant to be vendored. Copy the directories you want into your tree and you have a protocol core, a UDP backend, transport encryption, and authentication, each of which you can take or leave. See VENDORING.md.
The protocol core never names a socket. Datagrams arrive through
netchan_feed() and leave through netchan_send_next(), each tagged with an
opaque struct nc_addr that the core copies and compares but never
interprets. Everything below that line is somebody else's problem.
That one seam is what lets the same object file run over UDP on a desktop, a WebSocket in a browser, and an encrypted tunnel in between, without the core knowing which. Encryption is a decorator that wraps datagrams on their way past, and the core is not aware it happened. Authentication is a conversation held on an ordinary reliable channel, and the core is not aware of that either.
application
|
netchan reliable ordered delivery, channels, flow control
|
nc_crypto X25519 + XChaCha20-Poly1305, optional
|
nc_udp the only file that has ever heard of sockaddr
|
the network
| Directory | What it is | Needs |
|---|---|---|
src/ |
the protocol core: netchan.c/h, nc_addr.h |
nothing but libc |
transport/ |
nc_udp (sockets) and nc_ws (RFC 6455 codec) |
nc_addr.h |
crypto/ |
nc_crypto, the encrypted transport decorator |
monocypher |
auth/ |
nc_auth (login state machines), keystore (on-disk formats) |
monocypher |
third_party/ |
vendored monocypher | — |
tests/ |
the test suite | — |
examples/ |
runnable programs, plus a vendored event loop they use | — |
microchan/ |
a second, incompatible library (see below) | — |
Each layer builds without the ones above it. src/ alone is a complete and
useful library.
microchan/ is the same idea sized for a machine
without a megabyte to spare: one allocation per connection and none after
that, every buffer inside it fixed at compile time, Go-Back-N instead of
selective ack, and a core that fits in a 16-bit large-model DOS binary next to
a whole game. It runs over IPX on MS-DOS and over UDP on a host.
It is a separate library, not a build option here. The wire formats differ,
the APIs differ (mc_* against netchan_*), and struct mc_addr and
struct nc_addr pack their bytes differently while meaning roughly the same
thing. Nothing links both. What the two do share is the seam: a core that
never names a socket.
make # library, tests, and examples
make run-tests # the whole suite
make lint # check the tree against docs/coding-style.md
make analyze # build under gcc -fanalyzer
make NETCHAN_EXAMPLES=0 # library and tests onlyBinaries land in _out/<triplet>/bin/. A minimal cc-only build of the core
is kept in Makefile.simple, for anyone who wants to check that the
no-dependencies claim is real.
netchan_feed is the library's whole untrusted surface, so it has a fuzz
harness whose corpus the suite replays on every platform. The manual's
Testing and analysis page
covers that, the sanitizer and static-analysis passes, and what each has
found.
The manual is at https://orangetide.github.io/netchan/: introduction,
architecture, four tutorials, the wire protocol, message encoding, testing,
and an API reference generated from src/netchan.h. Build it locally with
make -C docs serve. Every page carries the version it was built from, which
is the release tag on a tagged build and the tag plus the commit otherwise.
The published site is the last release, not the tip: a push builds and checks
the site but only a v* tag replaces it. That keeps the address people cite
pointing at a version that exists, at the cost of a typo fix waiting for the
next tag.
The suite is clean under AddressSanitizer and UndefinedBehaviorSanitizer:
make clean
make CFLAGS="-fsanitize=address,undefined -g -O1" \
LDFLAGS="-fsanitize=address,undefined"
make run-testsContributions follow docs/coding-style.md, which is
the formatting and convention guide for the project's own C. Vendored code
under third_party/ is exempt and keeps its upstream style.
The application owns the socket. netchan says what to send and when to call it again. How those bytes reach the wire is up to you.
struct netchan_conn *c = netchan_open(0); /* 0 = client, 1 = server */
netchan_connect(c, &server_addr);
struct netchan_chan *ch = netchan_chan_open(c, NETCHAN_RELIABLE,
NETCHAN_DIR_SEND, "state");
netchan_chan_write(ch, msg, len);
uint8_t pkt[1500];
struct nc_addr to;
size_t n;
while (running) {
while ((n = netchan_send_next(c, pkt, sizeof pkt, &to)) > 0)
send_it_however_you_like(pkt, n, &to);
int wait_ms = netchan_service(c, now_ms());
/* ... wait for readability, up to wait_ms ... */
if (got_a_datagram)
netchan_feed(c, rx, rxlen, &from);
struct netchan_event ev;
while (netchan_poll(c, &ev))
if (ev.type == NETCHAN_EV_DATA)
netchan_chan_read(ev.ch, buf, sizeof buf);
}
/* Leaving: say so, send it, then free. */
netchan_disconnect(c);
while ((n = netchan_send_next(c, pkt, sizeof pkt, &to)) > 0)
send_it_however_you_like(pkt, n, &to);
netchan_close(c);netchan_close() frees the connection and tells the peer nothing, and netchan
owns no socket, so the DISCONNECT that netchan_disconnect() queues goes
nowhere until you send it yourself. Skip the flush and the peer keeps the
session alive until its idle timeout expires, thirty seconds by default. A
server handing the slot to the next client waits that out for nothing.
Channels come in three flavours. NETCHAN_RELIABLE gives ordered, acked,
retransmitted datagrams. NETCHAN_UNRELIABLE is fire and forget, which is
what a position update wants. NETCHAN_STREAM is a reliable byte stream.
Several of them share one connection and one socket.
nc_crypto sits between the socket and netchan_feed(). Seal on the way out,
open on the way in, and hand what comes back to the core:
struct nc_crypto cr;
nc_crypto_init(&cr, /*role=*/0, &(struct nc_crypto_cfg){0});
/* Until nc_crypto_ready(), send nc_crypto_handshake_packet() instead of
* netchan's own output. The crypto handshake finishes first, so even
* netchan's SYN travels sealed. */
long n = nc_crypto_seal(&cr, pkt, len, sealed, sizeof sealed);
long m = nc_crypto_open(&cr, rx, rxlen, plain, sizeof plain);
if (m > 0) netchan_feed(c, plain, m, &from);Give the responder a long-term static_sk and the handshake authenticates it
too. A second Diffie-Hellman folds the identity key into the derivation, so
the first packet that opens is proof of possession. There is no signature on
the wire and no extra round trip. In Noise's naming that is NX, and a client
that compares the presented key against one it recorded earlier is applying
NK's verification to NX's message flow, which is what trust on first use
means.
Deciding whether to trust a key is deliberately not this layer's job.
Supply a verify_peer callback and answer it however the application wants.
nc_auth is a pair of state machines with no socket, no netchan, and no event
loop in them. Messages go in, messages come out, and the caller decides what
carries them. keystore reads and writes the five file formats:
known_hosts, host_key, authorized_keys, passwd, and the client key
file. All are plain text with hex fields, so an operator can read, diff, and
edit them.
The client's signature covers the nc_crypto session id, which is what stops
a signature captured by one server being replayed at another.
Wiring the two onto a live connection is application work.
examples/auth/auth_link.c is one way to do it.
| Program | What it shows |
|---|---|
netchan_example |
two peers over plain UDP; the smallest complete thing |
ws_gateway |
relays browser WebSocket clients onto an unmodified UDP server |
echo_server, echo_client |
an encrypted session, no login |
auth_server, auth_client, nc_keygen |
host keys, known_hosts, and an ssh-shaped login |
See examples/README.md for how to run them, including
generating a key, enrolling it, and watching the host-key warning fire.
C11 and the C library. The core needs a monotonic clock and a source of
randomness for its connection ids, and it takes both from whichever platform
it is compiled for: clock_gettime(CLOCK_MONOTONIC) and /dev/urandom on
POSIX, GetTickCount64 and rand_s on Windows. Build with
-D_POSIX_C_SOURCE=200809L under strict -std=c11. Nothing else in src/
reaches past the C library, and nothing in it needs a library on the link
line.
Linux, macOS, and Windows are all built in CI, and the wasm and 16-bit DOS targets are built there too. The Windows job cross-compiles with mingw-w64 and runs the result under wine.
| Layer | POSIX | Windows | wasm |
|---|---|---|---|
src/ core |
yes | yes | yes |
transport/nc_ws |
yes | yes | yes |
transport/nc_udp |
yes | yes, address packing only | no |
crypto/, auth/ |
yes | yes | no |
microchan/ core |
yes | yes | no |
examples/, microchan/transport/mc_udp |
yes | no | no |
The examples own real sockets, an event loop, and a terminal, so they are POSIX and are meant to be. Porting them is a bigger job than porting the library, and the library is the part that gets vendored.
src/, transport/nc_ws.c, and their tests compile to WebAssembly unchanged;
build with CC=emcc CXX=em++ to check. nc_udp, nc_crypto, and auth/ drop
out of a wasm build automatically, because a browser has no BSD sockets and
its transports are already encrypted.
nc_crypto and keystore draw randomness from the OS, picking a backend at
compile time: BCryptGenRandom on Windows, arc4random_buf on macOS and the
BSDs, getrandom(2) on Linux, and /dev/urandom for anything else or for a
Linux kernel too old for the syscall. The Windows backend is the one case that
needs something on the link line, -lbcrypt, which the build adds for
dependents automatically. The core deliberately does not use it, which is what
keeps a vendored src/ free of link-line dependencies.
On Windows, auth/keystore differs in two ways. The 0600 it asks for on its
key files is ignored, since permissions are ACLs, so a key file inherits
whatever its directory grants. Its files are also opened in text mode, so
lines are stored with CRLF. Every format is line-oriented hex and round-trips
either way, and files written on one platform read on the other.
Releases are SemVer, tagged vMAJOR.MINOR.PATCH, and the current one is
whatever the releases page
says. A copy of the source states its own version in src/netchan.h as
NETCHAN_VERSION_STRING.
The protocol is stable enough to build on and is not frozen. Wire
compatibility holds within a minor version and no further; see
CHANGELOG.md. The one break so far is nc_crypto's HELLO
growing from 33 to 65 bytes when identity keys arrived.
To take it into your own tree, see VENDORING.md, or take the latest release with:
curl -fsSL https://raw.githubusercontent.com/OrangeTide/netchan/main/tools/vendor.sh | shnetchan grew out of a series of research articles. Those articles are the explanation; this repository is the maintained library.
PUBLIC DOMAIN (CC0-1.0). See LICENSE.
Vendored monocypher is dual CC0-1.0 / BSD-2-Clause, and the vendored event
loop under examples/iox/ is MIT-0 or public domain. Both are recorded in
VENDORING.md.