Every service reachable from every remote client, with no inbound ports
An organization with many services and many remote clients (customer sites, edge agents, laptops, CI runners, partners) pays for every connection twice: once when someone opens a port, issues a certificate, adds a VPN route and writes the client code, and again for as long as it has to stay open, renewed and patched. nfltr replaces that with one layer. Every machine connects out to a relay, and any client your account allows reaches any service by name, in both directions. Developers keep their gRPC and HTTP code. The platform team stops running per-service plumbing.
What changes
- No inbound ports to open or scan. In a .NET gRPC fleet we migrated, listeners reachable from the network went 1 → 0, read from every machine while it ran. Machines behind NAT need no rule.
- No certificate authority to run. Private CAs 1 → 0, certificates to renew 1 → 0. Every hop to the relay is TLS; end-to-end encryption uses a pinned key, not a CA.
- A one-line client change. The gRPC channel registration changed by one line and the 51-line TLS and keepalive client was deleted. Protobuf definitions and business logic: untouched.
- Calls in both directions. The control plane now calls agents behind NAT by name; before, it could not reach them at all. An agent killed mid-command answered again within 347 ms with no operator action.
- Reliable connections come from the layer, not from each team's code. Keepalives, reconnects, per-session flow control, drains through deploys and typed failures work the same for every service and language, and a cut session never ends cleanly with bytes missing. See what your code no longer has to get right.
- Grows by adding replicas. 3 → 5 relay replicas carried 1.51× the traffic with 0 failed requests.
For developers: reaching a remote service is a line of configuration, not a ticket, a VPN profile and TLS code in each language, and it works the same from a laptop.
For the organization: the recurring work that grows with every new service and every new client (ports, DNS names, certificates, VPN accounts, connection code) goes away, and one /status and /metrics per machine shows every remote session.
Connectivity that grows with the sum, not the product
Each new service needs a way in: a listener, a DNS name, a certificate, a firewall or load balancer rule. Each new remote client needs a way through: a VPN account, an allow-list entry, a trust bundle. Each pair then needs both to line up, and someone has to keep them lined up. The work grows with services times clients.
With nfltr, each machine runs a small sidecar that connects out to the relay. A service is published under a name such as edge-1/execute; a client maps that name to a local socket, or opens a session with nfltr dial. The relay carries the application's bytes without parsing them: gRPC, HTTP, a database's protocol. A new service or client adds one outbound connection, not a row in every firewall, VPN and certificate inventory, and a control plane can call an agent behind NAT.
What your code no longer has to get right
Most network bugs live in the code each team writes around its calls: keepalives, reconnects, buffering, shutdown, telling a dropped connection from a finished one. With nfltr that work is done once, in the layer under every service and client.
| Failure case | What the layer does, and what you see |
|---|---|
| Machines behind NAT; idle connections dropped by middleboxes | Every machine connects out, and every connection pings while idle, so a NAT keeps its mapping and a silent peer is declared dead within a known bound (presets such as nat). |
| The relay restarts or the network drops | Agents redial on their own. A client's mapping keeps its local endpoint; sessions on the lost connection end with reset, and the next connection opens a new session. |
| One slow or stalled reader | Every session has its own credit and a bounded buffer: a reader that stops stalls only its own sender; other sessions keep flowing. |
| Half close | Passed through both ways: a client done sending still reads the whole answer. |
| Where did it fail? | An open is answered before any byte flows: it reached the target, or it fails with a typed reason naming the hop that refused, and a trace id. gRPC through a mapping gets UNAVAILABLE at once; nfltr dial exits 3 not found, 4 refused, 5 unreachable, 6 reset, 7 pin mismatch. |
| Relay deploys | A leaving relay sends new sessions elsewhere and lets open ones finish up to its drain deadline; one still open then ends with the reason drain_deadline. |
| A cut that looks like a clean end | No hop turns a cut into a clean close: a cut session ends with an error at both ends; a clean end means every byte arrived. Killing, partitioning and draining the middle hop of 5-hop chains: 0 integrity failures in 630 TCP and 69 mixed QUIC and TCP runs. |
| A relay that reads or swaps traffic | Optional: with e2ee and a pin, a wrong key fails the handshake, exit 7, nothing delivered. |
| Seeing it | Per mapping, in /status and /metrics: state, open sessions, failures by code and reason, bytes. |
What stays yours:
- Deadlines, retries and idempotency of your own calls. The sidecar fails a call at once rather than queueing it, and a cut session is not replayed, so the .NET fleet keeps its retry on UNAVAILABLE and its reconnect loop with backoff.
- Your protocol and its authorization. The relay carries bytes; what they mean and who may send them is yours.
The layer does not make failures disappear. It makes the transport's failures explicit and typed, so the retry logic you keep is short and correct. It is proven by link's fault tests, the sidecar's conformance suite (real binaries through every exit code, half close, a drain and a relay that swaps keys) and the soaks below, whose recovery is not yet instant; the migration post shows it on one fleet.
What a developer does to reach a remote service
Most of the cost of reaching a service on another network is not code. It is waiting on other teams, then repeating the same connection code in each language.
The mapping is a few lines in the sidecar's file:
connect:
ledger: { target: billing/ledger, listen: "unix:/run/nfltr/ledger.sock" }
The application connects to that socket instead of a host name; stubs, streaming, deadlines and metadata stay the same. The same file runs on a laptop or in a CI job, against the real remote service.
What the platform team stops maintaining
The same migration, counted from the platform team's side. Every item on the left repeats per service, per client or per language, and most of it has a calendar: renewals, rotations, account reviews.
What moves out is what existed only because the network had to be opened and trusted: in the .NET example, the client code that trusted a private CA, checked a server name and kept NATs from dropping idle connections.
Security and control
- Outbound only. No machine accepts a connection from the network, so there is nothing to expose, scan or put behind a load balancer.
- Access belongs to the owner. An account's key reaches only that account's agents, and only the services each agent's own file declares; anyone else reaches a service only through a share. The sidecar adds no policy of its own.
- End-to-end encryption, per service. With
e2ee: trueand a pin, the two sidecars run TLS 1.3 inside the session and accept only the certificate whose SHA-256 is the pin. A relay that swaps in its own key fails the handshake. The relay still sees who talks to which service, when, and how many bytes. - Why it is optional. An organization that runs its own relay may need to inspect traffic for compliance or intrusion detection, and some applications already run their own TLS, which passes through unchanged. The relay's operator sets the policy:
require,alloworforbid, and every session reports whether it is encrypted, so plaintext is visible, never silent.
How it holds up under change
- Scale-out. On a Kubernetes cluster, going from 3 to 5 relay replicas carried 1.51× the traffic with no failed request. Through the connect sidecar, gRPC reached 11,556 calls per second with the relays at under one core each; the load generators ran out first.
- Faults. A 7-hour soak injected 26 faults (rolling restarts, pod kills, a Redis restart, partitions, clock skew, node drains, a frozen relay) under 2.57 million requests. All 24,958 of 24,958 mailbox messages arrived exactly once, and the relay fleet returned to its resource baseline after every fault. Recovery was not instant: most faults took more than 50 seconds to fully clear, and that work is open.
Adopting it one service at a time
Nothing has to move at once. Install the sidecar on a few machines and publish one service next to its existing listener. Ship a client build that can use either path and let configuration pick one per client; setting the old value back rolls that client back. When every client of a service is on the new path, close its public listener, its firewall rule and its certificate.
gRPC stays gRPC. A Go client changes its target to unix:///run/nfltr/ledger.sock, Python with grpcio uses the same address, and the .NET fleet changed one line of channel registration. The step-by-step migration, with the diff and the measured cost per call, is in Remote gRPC with no open ports, no VPN and no CA to run.
Limits
- One name is one agent. Calls to a service name are not spread over several agents yet; put a load balancer behind one agent.
- The relay authenticates the account key, not the machine. A key reaches every service of the account, so keep the application's own per-caller credential and check it.
- The relay is on the path. A call adds the network distance to the relay, plus 0.1 to 0.3 ms at p50 for the two sidecars and the relay, measured on one host.
- Pins follow certificates. If an agent's certificate changes, encrypted clients fail closed until they are given the new pin.
Try it
Install nfltr and sign in on both machines:
curl -fsSL https://nfltr.xyz/install.sh | sh
nfltr config login
On the service's machine, publish a service under an agent name and note the pin it prints:
# home.yaml
agent: home
services:
db: { tcp: 5432, e2ee: true }
nfltr up -c home.yaml
On the client, map it to a local endpoint and point the application there:
# app.yaml
connect:
db: { target: home/db, listen: "127.0.0.1:5432", pin: "<pin>" }
status: "127.0.0.1:7801"
nfltr up -c app.yaml
curl -s http://127.0.0.1:7801/status
or open one session from a script:
nfltr dial home/db --pin <pin> < query.bin > answer.bin
The nfltr up and nfltr dial reference has every flag.
Where the numbers come from
The fleet numbers are from two runs of the .NET 8 gRPC fleet's end-to-end test (make test-dotnet-fleet-example) on October 16, 2026, on one machine with Docker; listeners were read from each machine's /proc/net/tcp and /proc/net/tcp6 while it ran, sidecars included, relay not counted. The scale-out, sidecar and soak numbers are from a three- to five-replica relay on a managed Kubernetes cluster in October 2026, with relay CPU limited to 1.5 cores per replica. The chain integrity numbers are link's fault tests on 5-hop chains, from link v0.10.7 on.