Four nfltr repositories are now open source: link, relay, relay-core and a quic-go fork
nfltr connects machines through a relay that each machine dials out to, so none of them needs an open port: tunnels and shares, services reached by remote clients, and Claude Code agents run on your own machines by nfltr orch. Until now every line of that was closed source. To write your own agent for the relay you had to use our CLI, and if you wanted the parts underneath it for your own tunnel or proxy, you could only read about them on this blog.
On October 29, 2026 four repositories under github.com/nfltr became public: link (a stream multiplexer), relay (the relay's client SDK and wire protocol), relay-core (the relay's session core) and quic-go (our fork of quic-go). The first three are Apache-2.0; the fork keeps upstream's MIT. They are the bottom of the stack: relay-core builds on the other three and on public modules only. What runs nfltr.xyz on top of them, the relay server's hosted parts, the nfltr CLI and orchestration, stays closed. Each repository starts from a fresh history, and the module versions we depend on still verify against the go.sum lines written while they were private. All four are pre-1.0.
The problem
A program that carries many byte streams between two machines, such as a tunnel, a relay, a proxy or an RPC fabric, needs the same few things: many sessions over one connection, so it does not pay a handshake per stream; flow control per session, so one slow reader does not stall the others; and a bound on how long a dead peer goes unnoticed. gRPC streams and the existing Go multiplexers give some of these, and the first link post measured what they cost us: a peer that vanished took 80 s to 2 h to be noticed over gRPC, and one stuck reader held every other session on its stream.
We built those pieces for the relay, and they were useful only inside nfltr. A Go developer who wanted to talk to the relay directly (expose a service from their own program, call another agent) had no SDK to import, and anyone curious whether our numbers held could not run the code behind them. Opening the bottom of the stack fixes both, without opening the product built on it.
What is now public
| Repository | Go modules, tags at publication | What it is | License |
|---|---|---|---|
| nfltr/link | github.com/nfltr/link v0.10.17, peerlink/quiclink v0.4.17, peerlink/grpclink v0.3.3, linkvet v0.1.1 |
Many byte-stream sessions, each a net.Conn with its own flow control, over connections you dial and secure; QUIC and gRPC transports behind one interface; a go vet tool |
Apache-2.0 |
| nfltr/relay | github.com/nfltr/relay/proto v0.19.3, github.com/nfltr/relay/client v0.19.9 |
The relay's wire protocol (protos and hand-written codecs) and the Go client SDK | Apache-2.0 |
| nfltr/relay-core | github.com/nfltr/relay-core v0.2.1 |
The relay's session core as a library, plus a small command that runs one relay | Apache-2.0 |
| nfltr/quic-go | github.com/nfltr/quic-go v0.63.1-nfltr.2 |
quic-go with fixes link's QUIC transport needs | MIT (upstream's) |
link
link multiplexes sessions over one connection you give it (plain TCP, TLS, mutual TLS). Each session is a net.Conn with its own credit, so a reader that stops holds only its own session; both ends ping once they hear nothing, so a dead peer is noticed within a bound you choose; and a Pool keeps several connections per peer and drains them make-before-break. The main module imports only the Go standard library. Around it: peerlink, a transport-neutral interface for sessions between named peers with a conformance suite any implementation can run; quiclink, the same interface over QUIC (no head-of-line blocking across sessions, and a connection that survives the dialler's address changing); grpclink, the same over gRPC streams; and linkvet, a go vet tool that finds sessions left open, read loops with no deadline, and five misuses in gRPC clients. The wire format is specified with test vectors and a recorded session, checked byte for byte on every release. Why we built it and how its API was designed are parts 1 and 2 of the link series.
relay
Two modules, versioned on their own. proto holds the v1 protos (the relay, blobs, mailboxes, presence) with their generated Go and the codecs both ends speak: agent metadata, the data plane, WebSocket frames, the WireGuard endpoint protocol. client is the SDK: agent exposes a local service through a relay, rpcclient makes agent-to-agent calls, and there are clients for mailboxes, blobs, presence, peer-to-peer and WireGuard. The nfltr CLI is built on these modules. The protocol is additive within a major version: a relay advertises its features, and a client that needs one the relay lacks fails closed and names it.
relay-core
The relay's session core: agents behind NAT dial out to it and register; clients holding the same key open byte sessions to those agents' ports, or send them HTTP requests. Every connection is authenticated through a chain of identity providers (a key file by default, or your own identity.Provider), with HTTP ingress, SNI and link frontends. It runs one replica with an in-process directory: no database, no Redis, no hosted services. It is a library first (compose.Serve, a test helper relaycoretest.New(t) in the manner of httptest.NewServer, examples for your own authentication and your own frontend) and ships nfltr-relay-core, a small command that runs it. Off loopback it serves TLS only, and refuses to listen in plaintext.
quic-go fork
Branch nfltr is upstream quic-go at one commit plus a few fixes, each its own commit with its test: aborting a stream after the peer's STOP_SENDING no longer panics; HyStart leaves slow start only while the sender is limited by its congestion window, which fixed a direction that crawled after a burst the other way (0.7 MB/s before, 17-20 MB/s after, at 100 ms RTT and a 20 MB/s bottleneck, measured in link's QUIC tests); a late ACK restores the congestion window also after a reordered burst of more than 64 packets; and a send that fails because the path is briefly gone (a NAT rebinding, a host changing networks) counts as a lost packet instead of closing the connection. Branch master mirrors upstream. The fork exists for quiclink; if upstream takes the fixes, quiclink goes back to upstream.
How they fit, and what nfltr builds on top
The dependency runs one way. link imports nothing of ours. relay's data plane runs on link, and the SDK's agents carry their sessions over a link connection to the relay when the relay offers one, with gRPC streams as the fallback. relay-core imports link (with quiclink, hence the fork), relay's proto and client, and public modules only; its README lists every module it links and a test fails on any other.
On top, closed:
- The relay server builds on relay-core through its exported packages only, and adds what a hosted relay needs: the link between replicas, WireGuard, the mailbox, blob and accounts services, sign-in and the dashboard. It is both the relay behind nfltr.xyz and
nfltr relay serve, the relay you can run on your own network. Taking orchestration out of it so that it could stand on a smaller core is part 3 of this series. - The
nfltrCLI uses the relay SDK for its agents, shares and tunnels, and link for the connect sidecar that gives remote clients a local socket to a service behind NAT. nfltr orch, the hub and its nodes, talks to the relay through the same SDK: mailboxes for completions and hooks, presence for nodes, blobs for artifacts.
So a bug fixed in link or relay-core reaches nfltr.xyz by a version bump in the closed repositories, and the open repositories' tests are the ones that guard it.
Why a fresh history
Each public repository starts with a new history: one commit per published release, plus a branch head (the fork sits on upstream's public history). The private histories had commit metadata and old trees that cited internal documents and named a closed repository, and once a repository is public, GitHub keeps every commit ever pushed reachable by its hash; rewriting the history in place would not have removed them. So the private repositories were renamed and kept private, and new public repositories were created from the cleaned trees.
What makes that safe for Go modules: the checksum in go.sum covers a module version's files, not its commit. Each published tag's tree is byte for byte the tree of the private tag of the same name, so the same version downloads to the same hash, and every go.sum that already named those versions (relay-core's, and our closed repositories') still verifies. Earlier versions were not published, because their trees still carried the citations; consumers were moved to the published versions first.
Evidence: the public tags give the hashes go.sum already had
- Setup
- Before publication, the same
go mod download -jsonwas run twice in an empty module cache, once against each private repository and once against its public copy with no credentials, for quic-go v0.63.1-nfltr.2, link v0.10.17, quiclink v0.4.17, linkvet v0.1.1, grpclink v0.3.3, relay proto v0.19.3, client v0.19.9 and relay-core v0.2.1. After publication: a fresh clone of relay-core, an empty module cache,proxy.golang.org, no private settings, one Mac, October 29, 2026. - Numbers
SumandGoModSumequal for all 8 versions, and equal to thego.sumlines of relay-core and of our closed repositories. After publication,go mod downloadin the relay-core clone exits 0 andgo mod verifyprints "all modules verified"; link v0.10.17 downloads ash1:Xln2D/iXZ5qjKZB3kTsj8VDrvnxew+FkPPVR635Wd8o=, the line relay-core'sgo.sumhas.- Reproduce
git clone https://github.com/nfltr/relay-core && cd relay-core && GOMODCACHE=$(mktemp -d) go mod download && go mod verify, thengo mod download -json github.com/nfltr/link@v0.10.17and compare itsSumwith thego.sumline.- Limits
- The comparison against the private repositories cannot be rerun from outside: it is our record. What you can check is that the public versions agree with the
go.sumfiles published beside them and, from now on, with the Go checksum database. Ago mod downloadof a version that was not published (link before v0.10.17, relay client before v0.19.9 and proto before v0.19.3, relay-core v0.1.x and v0.2.0) fails.
What is open and what is not
| Open | Closed | |
|---|---|---|
| Multiplexing and transports | link, quiclink, grpclink, linkvet, the quic-go fork | |
| Wire protocol | relay proto |
|
| Client side | relay client (the SDK) |
the nfltr CLI and nfltr-orch |
| Relay | relay-core (one replica, key file or your own identity provider) | the relay server: replicas and the link between them, WireGuard, services, sign-in, dashboard; the hosted relay at nfltr.xyz |
| Orchestration | the hub, nodes, workers |
The protocol and SDK being open means you can write an agent or a client in Go without the CLI. It does not make the relay server open: relay-core is a relay you can run, with the limits listed below, not the one nfltr.xyz runs.
Try each
link: the quick start runs two peers in one process, each opening a session to the other on one connection:
go run github.com/nfltr/link/examples/quickstart@latest
It prints b got "hello" (greeting) from a and a got "hello" (greeting) from b. In your own module: go get github.com/nfltr/link@latest. The linter: go install github.com/nfltr/link/linkvet/cmd/linkvet@latest, then go vet -vettool=$(command -v linkvet) ./....
relay: go get github.com/nfltr/relay/client@latest, then the README's short program exposes a local HTTP service through the hosted relay as an agent (it needs an API key from nfltr.xyz), reconnecting on its own when the relay restarts or the network drops. rpcclient calls it from another program on the same account.
relay-core: clone, build, run, and send bytes through it, with nothing configured (loopback only; it writes a key file with mode 0600):
git clone https://github.com/nfltr/relay-core && cd relay-core
go build -o bin/ ./cmd/... ./examples/quickstart
bin/nfltr-relay-core serve 2>relay.log &
trap "kill $!" EXIT
bin/quickstart -key-file nfltr-relay-core.keys -agents 127.0.0.1:9090 -clients 127.0.0.1:8080
It prints echoed: hello through nfltr-relay-core. As a library: go get github.com/nfltr/relay-core@latest.
quic-go fork: through quiclink you already get it. Its module path is its own, so a program that wants it directly imports github.com/nfltr/quic-go (go get github.com/nfltr/quic-go@v0.63.1-nfltr.2) instead of upstream's path; a replace of upstream's path does not work.
Evidence: from a fresh module cache to running code
- Setup
- One Mac (Apple M5, 10 cores, load average about 10 during the link run), Go 1.27.3, public proxies only, October 29, 2026. The relay-core figures were measured before publication on the same Mac with warm Go caches, against the build of the same core inside our closed relay server, interleaved: medians of 3 idle windows of 60 s and 5 startups.
- Numbers
- link:
go run github.com/nfltr/link/examples/quickstart@latest10.3 s wall from an empty module cache, both lines printed. relay-core:git cloneto the echoed bytes 10.7 s with warm caches before publication, and 25.4 s after it from the public repository at a load average of 8 to 12 (module cache filled by thego.sumcheck above);nfltr-relay-corestripped for linux/amd64 20.7 MB; idle resident memory 23.5 MiB, 18 goroutines, no CPU used in 60 s idle; startup about 0.5 s. - Reproduce
- the link command above; in a relay-core clone,
scripts/time-to-first-byte.shtimes its README quickstart from clone to first byte, andGOOS=linux GOARCH=amd64 go build -trimpath -ldflags "-s -w" -o bin/linux/ ./cmd/...gives the binary. - Limits
- One machine, one or a few runs; the relay-core time depends on your network and caches, and the first
go rundownloads the Go 1.27 toolchain when yours is older. The size includes gRPC, Prometheus and quic-go, which the core links (QUIC is off unless a composition turns it on).
Measured, and where it is measured
The numbers that matter for choosing these libraries are in their repositories, each with its method:
- link throughput, one connection, in process, Apple M5, medians of 8 alternating runs (link's README and its performance document): 64-byte messages over 8 sessions 335 MB/s over TCP and 295 MB/s over TLS 1.3; 4 KiB over one session 1,946 and 1,402 MB/s; 1 MiB over one session 9,701 and 2,915 MB/s; no allocation per message; a 64-byte echo on a held session 20 µs at the median. Reproduce with
make checkand the benchmarks in the link repository. - link against gRPC between relay replicas: a dead peer found in 15 s by default (4.5 s with the fast preset) against 80 s to 2 h, and 8 of 8 other downloads finishing beside a stuck reader against 0 of 8 (part 1 of the link series, with its own evidence blocks).
- quiclink against link over TCP (quiclink's README): measured with link v0.7.1 and quic-go v0.63.0, QUIC moves fewer bytes per core (934 against 2,140 MB/s for one session on Linux), but with 3 s of 5 % loss at 50 ms the longest gap per session falls from 1,010 to 170 ms at the median, and a download survives the dialler's address changing with a 20-23 ms gap, where TCP loses the session.
- The relay against frp, rathole, chisel and bore: part 2 of this series, measured with the relay server, which runs on this core.
Evidence: link throughput in its repository
- Setup
- link v0.10.9 against v0.10.8, one connection, both peers in one process, Apple M5 (10 CPUs), medians of 8 alternating runs on a shared machine, October 17, 2026; every cell, the method and each optimization are in link's performance document.
- Numbers
- TCP / TLS 1.3 MB/s: 64 B × 1 session 32 / 28; 64 B × 8 sessions 335 / 295; 1 KiB × 8 1,180 / 884; 4 KiB × 1 1,946 / 1,402; 64 KiB × 8 7,953 / 3,003; 1 MiB × 1 9,701 / 2,915. 0 allocations per message on every row. Held 64-byte echo 20 µs p50.
- Reproduce
- in a link clone,
make checkruns the tests that hold the allocation and frame counts, andscripts/perf.sh ab OLD NEW OUTcompares two refs with the same method. - Limits
- In process, one machine, no network between the peers: these numbers are the library's own cost, not what a WAN path gives. The 64 KiB and 1 MiB rows differ within the noise of a shared machine. A comparison with yamux is in link's
compare/directory; there is none yet against other multiplexers under the same harness.
How link compares
We ran link against the two multiplexers most Go tunnels use: hashicorp/yamux v0.1.2 and muxado v2.0.2, the one ngrok's agent uses. All three run the same benchmark code over TCP loopback, each with its own defaults, since defaults are what most users get. Those defaults differ: link's window per stream is 1 MiB and the other two use 256 KiB. link pings after 5 s of silence, yamux every 30 s, and muxado not at all unless you add its heartbeat wrapper.
| (medians of 8 alternating passes) | link | yamux | muxado |
|---|---|---|---|
| one stream, bulk | 8.2 GiB/s | 3.8 GiB/s | 3.6 GiB/s |
| 64 streams at once, bulk | 6.6 GiB/s | 5.3 GiB/s | 3.8 GiB/s |
| one stream, 40 ms RTT | 24 MiB/s | 6 MiB/s | 6 MiB/s |
| new stream: open, 64 B echo, close (p50) | 28 µs | 40 µs | 32 µs |
| idle stream, heap for both ends | 2.0 KiB | 3.9 KiB | 3.1 KiB |
| slow reader: other stream stalls? | no | no | no |
link wins on a single stream, on opening streams and on memory per idle stream. In our first run it lost clearly with 64 streams writing at once: link 2.4, yamux 6.2 and muxado 4.5 GiB/s. The cause was link's write path: every writer waiting for room in a connection's write buffer was woken on each frame, and now only the one whose turn it is wakes. We fixed it after that run, in link v0.10.18; link alone, before and after the change alternating, went from 2.35 to 8.06 GiB/s at 64 streams, with the other rows unchanged. The rerun of all three is the table above. Read the 64-stream lead with care: the spread on this shared machine is wide, and yamux's median moved from 6.2 to 5.3 GiB/s between the two runs with no change of its own.
The 40 ms row comes from the default window size, not from better code. Each library sends one window per round trip, and link's default window is four times larger. On a held stream, the echo latency of the three is within noise.
The slow-reader test does not separate them. When one stream's reader stops for 1 s or 5 s, the other stream never pauses for even 5 ms in any of the three. In fact it speeds up, because it gets the whole connection. Every library here has flow control per stream. The head-of-line stall in the first link post was gRPC's.
Evidence: link, yamux and muxado with their defaults
- Setup
- link at 1d4804e (the write path change, released in v0.10.18), yamux v0.1.2, muxado v2.0.2; TCP loopback with both ends in one process; Apple M5 (10 CPUs), Go 1.27.0; 8 passes, each running every benchmark for all three libraries in rotating order; load average 5.3 to 8.4 on a shared machine; October 29, 2026. The first run, link at 92a1618 before the change, had load average 5.9 to 11.
- Numbers
- in the table above; every cell, with benchstat's spread and p-values, and the first run's 64-stream row are in link's compare/defaults/README.md. link alone before and after the change, 64 streams: 2.35 -> 8.06 GiB/s, in link's docs/performance.md, P9.
- Reproduce
- in a link clone,
compare/defaults/run.sh OUT 8. - Limits
- Plain TCP only, no TLS, one machine, no loss or netem, and both ends share one scheduler. Bulk throughput spread ±16-39 % across passes (benchstat), and yamux's 64-stream median moved 6.2 -> 5.3 GiB/s between the two runs with no change of its own.
How the repositories are checked
Each repository has a make ci that runs the steps of its GitHub workflow, read from the workflow file so the two cannot drift: build, vet, tests with -race, short fuzzing of every decoder, gosec, semgrep, govulncheck, and an API check against the last release (make check-api, apidiff) in link and relay-core; the fork runs vet and upstream's unit tests on Linux. The workflows run on GitHub on every push and pull request, with public dependencies only and no secrets. Before publication, a clean container with no credentials, resolving only these four repositories from their public copies, ran relay-core's README quickstart and the make ci of relay-core and relay (pass, relay-core in 301 s).
The first GitHub runs were not all green: relay's first two runs, for the tag and a manual run, failed at the build step ("go.work requires go >= 1.27.2 (running go 1.27.0)"): the workflow took its Go version from a module that names an older Go than the workspace needs; the next commit sets Go up from the workspace file, and its runs since pass. GitHub Actions is an extra signal for us; the gate is make ci before every push to main or a tag, and a Linux runner that runs it on every new commit.
Limits and known gaps
- All pre-1.0. link's wire format is the stronger promise (version 2, held by its test vectors); its Go API may change in a minor release, with the migration in its changelog. relay's protocol is additive within v1, its modules are v0. relay-core is v0.2.
- link's maturity targets are not met yet: fuzzing has 4.85 cumulative hours without a crasher against a 24 h target, the fault soak 2.89 h against 24 h, and the v1 API review is open. Its README keeps that table current.
- relay-core is one replica, with no database, no ACME (bring your own certificate), no OIDC (a key file or your own identity provider). Configuration is process-wide, so one process runs one configuration. The relay server's replica link, services and dashboard are not in it.
- The fork's fixes are not upstream yet. Until they are, quiclink depends on the fork, and the fork follows upstream by hand.
- Old versions are gone from the public module path. A build that pinned an unpublished version of these modules (link before v0.10.17, and so on) must move to a published one.
- Contributions. Issues are welcome in all four. Pull-request terms are in each repository's CONTRIBUTING file: relay and relay-core have one (relay-core's takes pull requests by arrangement, with DCO sign-off, and says the policy may change); link and the fork do not have one yet, so open an issue there first. Security reports go to each repository's SECURITY.md where it has one.
- Logos. Each repository's README carries its own mark; this post does not.
Is it for you?
Fits if you write Go that carries many streams between two machines and want per-session backpressure and a bound on dead-peer detection without building them (link, maybe with quiclink for paths that change address or lose packets); you want your own Go program to be an agent or a client of an nfltr relay without our CLI (relay); or you want a small relay for agents behind NAT on your own machines, or a core to build your own relay on, and one replica with a key file is enough (relay-core).
Not ready for anyone who needs a v1 API promise today, a relay with several replicas, sign-in or a dashboard from open code, or the hosted features (shares, WireGuard, orchestration) without nfltr: those stay in the closed relay server and CLI. Languages other than Go have no SDK; the protocol is open, so one could be written against proto.
Try next: go run github.com/nfltr/link/examples/quickstart@latest, then the link, relay and relay-core READMEs. Why link exists is the first link post; the relay these build up to, run on your own network, is part 1 of this series. What link's dead-peer bound means for a gRPC application behind the connect sidecar is gRPC over the connect sidecar, and the process that checks these repositories is How we build reliable Go software. If something breaks, open an issue in the repository it belongs to.