Run a local process as if it were inside your cluster: prefix any command with plug and it resolves the cluster DNS and reaches the cluster services like any workload of the stack - no code change, no proxy configuration in your app. One tiny agent container on the cluster side, one static CLI on yours. Docker, Swarm, Kubernetes and OpenShift.

Coverage matrix

What works where - features × OS. One process run as if it were inside the cluster, via a userspace TUN over an SSH tunnel.

snapshot 2026-08-06 · CI re-proves install → grid → multicluster → reverse path → takeover → crash-recovery on every push, on 3 OSes × 3 cluster families (compose, Swarm, Kubernetes); rows noted bench are runtime-proven locally, not yet in CI

works (proven at runtime) partial - not yet e2e-validated not yet n/a by design

Install & privilege

Feature Linux macOS Windows Notes
Install from the cluster (one-liner) works works works install | sh · install-windows | bash -s -- <host> (Git Bash)
No sudo/admin to install works works works binaries + PATH; one UAC only to create the service
One-time privilege grant at install works works works setcap · setuid helper · SCM SYSTEM service (validated on a real box)
plug <cmd> without sudo/admin works works works Windows: a non-elevated launcher starts the SYSTEM service via its ACL (proven, LIMITED token)
Child runs as you (privilege drop) works works not applicable caps don't cross exec · Credential drop · service is SYSTEM
Pre-create profile at install works works works reads host/port off the live ssh
amd64 and arm64 works works works six binaries; Windows arm64 native, no WSL2, and its WinTUN driver is exercised on a windows-11-arm CI runner
Uninstall works works works unix + Windows (remove-service, wipe) covered

Data path

Feature Linux macOS Windows Notes
Userspace TUN (IP-layer capture) works works works /dev/net/tun · utun · WinTUN
Cluster-name DNS (real apps) works works works private resolv.conf · scutil store · WinTUN search-suffix + NRPT
Single-label name via getaddrinfo works works works the real app path; Windows needs the .plug search suffix to issue a DNS query
Works under a corporate VPN partial works partial macOS: GlobalProtect and OpenVPN Connect, on real corporate networks
Keeps the VPN's own names resolving (split DNS) works works works a VPN adds a resolver scoped to its domain, and plug keeps sending that domain to it - macOS reads SupplementalMatchDomains, Windows the NRPT, and on Linux systemd-resolved applies its routing domains behind the stub plug relays to. In CI on all three OSes: the selftest declares a domain of its own for its fake VPN, asserts a name under it resolves through plug, then stops resolving once the domain is withdrawn
Follows the resolver when a VPN comes up, drops, or the network changes works works works the servers are not a startup fact - in CI on all three OSes: the selftest fabricates a VPN (an extra adapter carrying a resolver that knows a name nothing else knows) and asserts that name resolves through plug, then stops when the VPN goes away. Includes what a VPN does not cause: on macOS one network service serves every SSID, so changing Wi-Fi moves the resolvers without moving the service. plug doctor reports where lookups actually go
Every runtime (Node/JVM/Py/Go/gRPC) works works works IP-level capture, socket never touched
A CONTAINER as a cluster member (--dockerrun) works partial partial an unmodified image joins the cluster through a sidecar that holds the datapath - no Dockerfile change. CI cell on all three cluster families (Linux runners); on macOS and Windows the CLI side (docker found in PATH, native host paths, host.docker.internal for volumes, the container's exit status) is tested in CI against a fake docker, and Docker Desktop itself was verified by hand on a mac
Native selftest (datapath proof) works works works green on all three in CI, plus a windows-11-arm leg opening a real arm64 WinTUN device
e2e protocol matrix (8 protos × 4 langs) works works works native over a Tailscale mesh, against all THREE cluster families - compose, Swarm and Kubernetes (kind) - the same by-name path on Linux, macOS and Windows
Split-horizon (short→cluster, FQDN→direct) works works works decided by the shape of the name - no config
Reverse: serve a local port to the cluster (-s) works works works SSH remote-forward - a cluster workload fetches the runner in CI, path self-verified at startup; re-arm after reconnect in CI (the resilience cell restarts the agent mid-session)
Reverse: external caller → published gateway → runner (HTTP) works works works a POST to a PUBLISHED cluster gateway calls a -s name that lands on the runner's local sink; the correlation id AND the full request path round-trip back (root and a deep path) - the API-gateway use case, proven from outside the cluster
-s name provisioned dynamically (no redeploy) works works works CI serves a name declared nowhere, from a linux/mac/win client, on all three backends: docker-sock signpost container (Compose), Swarm-service signpost on a non-attachable overlay (Swarm), Service through the Services-only RBAC (k8s) - created & torn down per session, swept on agent restart
Takeover of a deployed name (default) works works works a deployed workload owning a -s name is parked for the session and restored on exit - all three backends in CI: containers stopped (Compose), Swarm service scaled to 0 & back to its original replica count (the CI target runs 2), k8s Service repointed via annotation receipt (ClusterIP identical through park/restore). Another session's name stays refused. Boot-gc restore after an agent crash AND the re-park on reconnect are in CI (resilience cell)
Self-heal (VPN / sleep / agent restart) works works works keepalive times out a zombie connection then reconnects & re-provisions - in CI on all three OSes: the resilience cell restarts the agent mid-session and traffic re-parks in seconds (boot-gc restores, the reconnect re-arms)

Daemon / persistence

Feature Linux macOS Windows Notes
Persistent global datapath not applicable works works Linux autonomous · macOS daemon · Windows SCM service (validated)
Survives process restarts works works works per-launch · daemon · service + 30 s self-teardown (validated)
Graft (multi-process, same cluster) works works works flock leader/graft · service is the single owner (3 concurrent sessions proven)
plug down not applicable works works stops the daemon (macOS) / service (Windows)

Multicluster - different clusters at once

Feature Linux macOS Windows Notes
Simultaneous different clusters works works works proven simultaneously in CI on all three - mount-ns · daemon · SYSTEM service
PID-at-connect attribution not applicable works works multiDial shared - proven in CI on macOS and Windows (2 live clusters, same name, right backend)
ppidOf works works works /proc · ps · ToolHelp (unit-tested)
pidForLocalPort works works works /proc/net · lsof · GetExtendedTcpTable (unit-tested)
procStart (recycle guard) works works works ps lstart · /proc stat · GetProcessTimes - rejects a reused PID
N-tunnel global daemon not applicable works works one tunnel per cluster in the daemon/service - proven simultaneously in CI (2 live clusters)

Profiles & CLI

Feature Linux macOS Windows Notes
Profiles ~/.plug/*.conf works works works create by naming · ls · rm · rn/mv · test
-p profile / --host / --port works works works flags override the selected profile
Launcher versions (per-cluster) works works works versions · .exe + wintun.dll handled on Windows
Host-key TOFU pin works works works localhost skipped; pin chowned (macOS)

Agent deployment

ItemSupportNotes
Docker Compose / Swarm works agent image joins the stack network - both in CI (Swarm: a real single-node swarm, the agent as a Swarm service on a non-attachable overlay)
The downloads, as published works the snippet (Compose and Swarm) and deploy/plug-stack.yml (Swarm) deployed as a reader deploys them, image and port aside: a session reaches the app by name and the app reaches the name it serves, on every push. The served copies are checked as pinned before the site is published
Kubernetes - NodePort works deploy/plug-k8s.yaml applied as published (kind) - the CI legs install and run through it on every push
Kubernetes - kubectl port-forward works zero exposed port, API-server RBAC - the k8s cluster job answers the agent contract through a live port-forward on every push
Cross-namespace works via FQDN svc.othernamespace

Design limits (all OS)

ItemSupportNotes
UDP / QUIC / ping not yet TCP only (SSH tunnel); most clients fall back to TCP
IPv6 literal (hard-coded) not yet fake IPs are IPv4; by-name cluster service is fine

What is not proven yet

Two things the grid above cannot claim. Both are marked ! in it, and neither is a gap in what plug does - they are limits of what has been demonstrated.

Linux and Windows under a real corporate VPN client
What a VPN does to DNS is proven in CI on all three OSes: the selftest fabricates an extra adapter carrying a resolver that knows a name nothing else knows, and asserts plug follows it - and follows it back down when the VPN goes away. It also declares a domain of its own for that VPN, the split DNS every corporate client pushes, and asserts a name under it resolves while that domain is declared. What no fabricated VPN proves is a real client on Linux and Windows: split-tunnel routing, MTU, and clients that intercept DNS on a loopback address.
Load, and laptop sleep/wake
Duration itself is covered now: a weekly soak holds one session for four hours against the PUBLISHED image, with traffic that re-resolves the name and opens a fresh connection every couple of seconds, and it asserts a trend rather than a threshold - memory, open descriptors and thread count must not climb between the first half of the run and the second, and every round must answer. What it does not do is LOAD: one connection at a time and short requests, so high connection counts and big transfers remain unexercised. Nor laptop sleep/wake, which no CI runner can perform.
How this matrix is proven. Every CI run installs plug FROM the cluster on all three OSes (the real one-liners and privilege grants), runs the 4-language × 8-protocol grid natively over a mesh, and asserts simultaneous multicluster, outage recovery, env passthrough both ways (your variables reach the command, a parked workload's reach it too and the cluster's value wins over an inherited one - --no-env=KEY holds a key back to yours, and -c --env-of reads a running workload's without taking it over, and a MOUNTED FILE is projected and read at its path - a k8s secret, a Compose secret, a Swarm config), the LIVE MOUNT of the workload's volume (automatic: the variable naming it repointed at the mount; explicit: --mount; a file written through it is served back by the workload - a Compose volume, a Swarm volume, a PersistentVolumeClaim on kind), the reverse direction (a cluster workload - and an external caller through a published gateway - reaches a runner-served name) and launcher/core version compat. See How it works and the roadmap.