Skip to content

softwarity/plug

Repository files navigation

plug

release docker docker pulls license arch CLI CI

Run a local process as if it were inside your cluster — cluster DNS names resolve, cluster services are reachable, with no code change and no proxy config in your app.

plug npm run start:dev
# your local NestJS/Spring/Quarkus now resolves and reaches
# http://my-service:8080 like any workload in the cluster

How it works

One mechanism: a userspace TUN, over an SSH tunnel. plug captures the child's cluster traffic at the IP layer and splices it, by name, to a tiny agent running in the cluster.

┌─ your machine ─────────────────┐        ┌─ cluster ───────────────────┐
│  plug <cmd>   (root / helper)  │        │  plug agent (alpine + sshd) │
│   └ userspace TUN (gVisor)     │        │                             │
│      ├ DNS answered in-stack   │──ssh───┼─→ direct-tcpip: sshd        │
│      │  on a fake IP:53        │  :2222 │   resolves the name and     │
│      └ splices each flow ──────┼────────┼─→ dials service:port from   │
│  <cmd> runs unchanged;         │        │   inside the cluster        │
│  its socket is never touched   │        │                             │
└────────────────────────────────┘        └─────────────────────────────┘
  • A wireguard-go device (/dev/net/tun, utun, WinTUN) feeds a gVisor userspace netstack. DNS is answered in-stack on a dedicated fake IP (198.18.<N>.53:53), minting a fake IP per single-label cluster name; the OS routes that range into the TUN, so the child's connect() surfaces as a packet plug reads, terminates, and splices to the SSH tunnel by name.
  • Because capture is at the IP layer, the child's socket is never touched — so it covers every runtime with no config: Node, JVM (Spring/Quarkus/Netty), Python, Ruby, PHP, curl, Go and other statically-linked binaries, and gRPC included (the cases an LD_PRELOAD/proxy approach cannot do).
  • Split-horizon by name shape: single-label names (my-service, rabbitmq) go to the cluster; dotted FQDNs (api.github.com) and localhost resolve and connect directly, so your app keeps normal internet access.
  • Self-healing: the tunnel survives a VPN reconnect, a laptop sleep, or an agent restart.

It needs root (create the TUN + set routes + repoint DNS) — granted once at install so day-to-day plug <cmd> runs with no sudo (see below).

Install

On the cluster — add the agent to your stack; it joins the stack network:

services:
  plug:
    image: docker.io/softwarity/plug:latest
    ports: ["2222:22"]

Standalone agent for several stacks: deploy/plug-stack.yml. Kubernetes: see Kubernetes below.

On each dev machine — install straight from the cluster, one line. The agent serves the right binary; the installer reads the cluster address from your ssh command and saves a profile named after that host, so plug is ready immediately.

# Linux / macOS — the agent regenerates its host key at each start (not a secret
# in plug's model), so skip the host-key check:
ssh -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null get@<cluster-host> install | sh

The install grants plug its privilege once so future runs need no sudo — this is the OS-native equivalent of the same thing:

OS Privilege granted at install Day-to-day
Linux setcap cap_net_admin,cap_sys_admin,cap_net_bind_service (one sudo) plug <cmd> — no sudo
macOS setuid-root helper (chown root:wheel + chmod u+s, one sudo); plug starts euid 0 to hold the TUN + DNS, then drops your command back to your user plug <cmd> — no sudo
Windows see Windows Administrator per session (helper WIP)

On macOS the data path lives in a small per-cluster daemon (started on demand, detached): because macOS repoints DNS machine-wide, the datapath must survive each plug <cmd>. Restart your processes freely — resolution survives; the daemon tears down and restores your DNS 30 s after the last plug of the cluster exits, and plug down stops it now.

Build from source instead: go build -o plug ./cli.

Versions — the launcher model

plug is a small launcher (like nvm/rustup). On each run it asks the agent which version it speaks and executes that exact version from ~/.plug/versions/, downloading it once if missing. Each cluster runs its own matching version. plug versions lists what's cached; plug self-update refreshes the launcher.

Usage

plug npm run start:dev
plug ./mvnw spring-boot:run
plug curl http://my-service:8080/health

Profiles live in ~/.plug/*.conf and are picked automatically: no profile → a short wizard; one profile → used as is; several → interactive, or -p staging. Reaching a new cluster is just naming it — the profile is created on first use:

plug -p staging <command>                 # wizard on first run, then remembered
plug -p staging -H node --port 2222        # define it non-interactively (no wizard)
plug -p staging -H node <command>          # define it and run, in one line
plug test -H node                          # probe an agent without saving anything

--host/--port (or $PLUG_HOST/$PLUG_PORT) target an agent directly. PLUG_DIRECT=<cidr,host,suffix,…> forces extra destinations to bypass the cluster.

CLI: plug ls · plug test · plug rn/rm · plug versions · plug uninstall · plug about. (plug self-update and — on macOS — plug down still work; they're just rarely needed: versions auto-update on connect, the datapath tears itself down.)

Multiple clusters at once

  • Linux: already supported — each launch gets a private resolver in its own mount namespace, so plug -p a <cmd> and plug -p b <cmd> run side by side.
  • macOS / Windows: one active cluster at a time today (the system resolver is machine-wide). Simultaneous different clusters is designed (transparent, bare names, disambiguated at connect() by process ancestry) — see docs/multicluster.md.

Kubernetes

Deploy the agent in the namespace of the services you want to reach:

kubectl -n <your-namespace> apply -f deploy/plug-k8s.yaml

deploy/plug-k8s.yaml is a Deployment + NodePort Service (32222 → container 22). sshd resolves short service names (myservice) via the pod's resolver (CoreDNS) from inside that namespace and dials them itself over an SSH direct-tcpip channel — no subnet or CIDR to declare. Reach it two ways:

  • NodePort, on any node: plug --host <a-node> --port 32222 <cmd>

  • kubectl port-forward — nothing exposed on the cluster; the tunnel rides the API server and is gated by its RBAC:

    kubectl -n <ns> port-forward svc/plug 2222:2222
    plug --host localhost <cmd>
    

Short names only resolve within the agent's namespace — for a service elsewhere, use its FQDN (myservice.othernamespace). See deploy/README.md.

Windows

Native Windows is supported (no WSL2 needed). You need an ssh client; this guide assumes Git for Windows is installed — it ships both ssh and Git Bash, which nearly every Windows dev already has. (Windows' built-in OpenSSH client works too — see the note below.)

1. Install — from Git Bash, one line. Same model as unix, piped into PowerShell instead of sh. The ssh bundled with Git is an MSYS process whose Windows command line the installer can't introspect, so pass the cluster host/port explicitly on the PowerShell side of the pipe — that's what names your profile:

# host key regenerated each start (not a secret) — skip the check, as plug does internally:
ssh -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
    get@<cluster-host> install-windows \
  | PLUG_HOST=<cluster-host> PLUG_PORT=2222 powershell -NoProfile -Command -

This downloads plug.exe and wintun.dll into %LOCALAPPDATA%\Programs\plug, adds it to your PATH, and pre-creates a ~/.plug/<host>.conf profile. Installing needs no administrator rights. Open a new terminal afterwards so the PATH takes effect.

Using Windows' native OpenSSH client instead? Run the same line from a normal PowerShell (with UserKnownHostsFile=NUL) and drop the PLUG_HOST=/PLUG_PORT= prefix — that client's command line is readable, so the host is auto-detected.

2. Run — the one caveat: an elevated terminal. plug's Windows data path is a WinTUN adapter, and creating it plus its routes requires Administrator. Unlike Linux (setcap) or macOS (setuid helper) — where one grant at install lets every later run start unprivileged — Windows has no per-binary privilege bit, and the process holding the adapter is the same one running your command in the foreground. So for now, start plug from an elevated terminal (Run as administrator):

plug <your command>            # several clusters? plug -p <name> <your command>

A future release will move the Windows data path into a persistent SYSTEM service (like the macOS daemon) driven by a non-elevated launcher over IPC — the path to "install once, run without admin" while keeping your command attached to your terminal. A scheduled task alone can't (it runs detached from the console).

Security model — read this

There is deliberately no authentication. The SSH keypair is embedded in this repository and in every plug binary; it is a transport detail, not a secret. Anyone who can reach the agent port has full network access to the attached cluster networks. Only deploy the agent on clusters you already trust; never publish port 2222 on an untrusted network. The agent's host key is pinned on first use (~/.plug/known_hosts) — a changed key aborts the connection (a basic MITM tripwire on top of the no-secret transport).

Limits (by design)

  • TCP only — the SSH tunnel carries TCP, so UDP/QUIC/ping aren't tunnelled (most clients fall back to TCP; HTTP/3 forced to QUIC would not).
  • IPv6 literals — fake IPs are IPv4; an app that connects to a hard-coded IPv6 isn't tunnelled (a cluster service reached by name is fine).
  • Root/helper required — the price of covering every runtime uniformly.

Roadmap

  • Userspace-TUN data path (covers every runtime incl. Go & gRPC), split-horizon routing, self-healing transport, host-key TOFU
  • Install from the cluster + per-cluster launcher versions + one-sudo privilege (setcap on Linux, setuid helper on macOS)
  • macOS DNS at the IP layer (works under a corporate VPN) + persistent per-cluster daemon
  • Kubernetes manifest — NodePort or kubectl port-forward
  • Multicluster on macOS/Windows (PID-at-connect) — design
  • Windows "no-prompt admin" helper (elevated task/service) — see Windows
  • IPv6 fake-pool + v6-literal tunnelling
  • Generalize the multi-protocol selftest per OS

📊 Coverage matrix — what works on which OS (Linux · macOS · Windows), feature by feature.

Distribution is from the cluster only (one source: the agent image) — no Homebrew tap or separate package channel by design.

About

Run a local process as if it were inside your Docker Swarm cluster — cluster DNS and services reachable, zero app config

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors