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.

Before and after: services and remote clients Before: three services and four remote clients, with twelve paths crossing a band of ports, VPN routes and certificates. After: the same seven machines each make one outbound connection to a relay, and calls reach services by name in both directions. Before ports, VPN, certs service A service B service C site agents laptops CI runners partners 3 × 4 = 12 pairs to open and secure After service A service B service C site agents laptops CI runners partners relay hosted or yours 3 + 4 = 7 outbound connections, both ways
Before, every service and client pair crosses an opened port, a VPN route and a certificate; after, each machine makes one outbound connection and reaches any allowed service by name.

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 caseWhat the layer does, and what you see
Machines behind NAT; idle connections dropped by middleboxesEvery 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 dropsAgents 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 readerEvery session has its own credit and a bounded buffer: a reader that stops stalls only its own sender; other sessions keep flowing.
Half closePassed 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 deploysA 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 endNo 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 trafficOptional: with e2ee and a pin, a wrong key fails the handshake, exit 7, nothing delivered.
Seeing itPer 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.

Steps to reach a remote service, before and after Before: five steps, a firewall and DNS ticket, a VPN profile, a certificate from the private CA, TLS and keepalive code in each language, and a staging tunnel. After: two edits, a connect mapping and pointing the client at its local socket, and the same file works from a laptop, CI or a site. Before After Firewall + DNS ticket another team opens a port VPN profile per person and CI job Certificate from a CA trusted, renewed on time TLS, keepalive code 51 lines in the .NET client Staging tunnel to test from a laptop A connect: mapping or nfltr dial in a script Point the client at it same gRPC or HTTP code Same file everywhere laptop, CI, site, partner 2 edits, no ticket no VPN, no port, no cert 5 steps, 3 through other teams → 2 edits
Reaching a remote service goes from five steps, three of them through other teams, to two edits a developer makes alone.

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 the platform team owns, before and after Stops owning: inbound ports, public DNS names, a private CA, certificate renewals, VPN accounts, TLS client code and keepalive settings. Still owns: the relay, hosted or its own; a sidecar per machine; account key files; access, through declared services and shares; status and metrics per machine. Stops owning Still owns Inbound ports firewall, NAT, LB rules Public DNS names one per exposed service A private CA and its key, kept safe Certificate renewals before each one expires VPN accounts per person and CI job TLS client code in every client language Keepalive settings both sides, every client The relay hosted, or your own A sidecar per machine new: one small process Account key files new: 0600, per machine Access declared services, shares Status and metrics one endpoint per machine 7 kinds of recurring work gone; 2 things added per machine
Seven kinds of work that grow with every service and client go away; what remains is one relay and a sidecar with a key file on each machine.

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: true and 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, allow or forbid, 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.

← All posts