Skip to content

OrangeTide/netchan

Repository files navigation

netchan

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 Idea

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

Layout

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

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.

Build

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 only

Binaries 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.

Documentation

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-tests

Contributions 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.

Using It

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.

Adding Encryption

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.

Adding a Login

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.

Examples

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.

Portability

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.

Status

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 | sh

Provenance

netchan grew out of a series of research articles. Those articles are the explanation; this repository is the maintained library.

Licence

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.

About

Networking Protocol for real-time gaming

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages