Dead‑simple WireGuard userspace launcher for Linux + macOS that keeps routes/DNS untouched. It’s designed to run on its own or alongside existing VPNs without disrupting their operations (see Non‑interference guarantees below).
- Creates a dedicated WG interface via
wireguard-go(userspace). - Sets a local /32 address.
- Configures peers and only host (/32) routes for peer WG IPs.
- Does not touch default routes or DNS.
- Optionally runs a local control-plane daemon that publishes/fetches signed endpoint records through a rendezvous service.
wireguard-goinPATH- Linux:
ipcommand (iproute2) - macOS:
ifconfigandroute
On Linux, the kernel WireGuard driver is used automatically if available. wireguard-go is only needed when the kernel driver is missing.
WireGuard can run either in-kernel (fastest, preferred on Linux) or in userspace (wireguard-go, works everywhere but uses more CPU).
To confirm the Linux kernel driver is available, try ip link add dev wgtest type wireguard (should succeed), or modprobe wireguard followed by lsmod | grep wireguard.
If the kernel driver isn’t available, install wireguard-go and ensure it’s in PATH (Linux: sudo apt install wireguard-go or your distro’s equivalent; macOS: brew install wireguard-go).
Wing will use the kernel driver when it exists and fall back to userspace otherwise.
In this example, we are going to connect a couple peers. We could be connecting three or more just as easily. This is a good setup for servers, or anyone who is not behind a NATted gateway, and not changing ip addresses too often.
Host A:
wing -setup
wing -exportcopy exported peer definition
Host B:
wing -setup
wing -importpaste exported peer definition
wing -exportcopy exported peer definition
Host A (again):
wing -importpaste exported peer definition
At any time, launch a leg of the peer relationship using:
sudo wing -detachHere, we setup one or more nodes as rendezvous registration and location nodes, and use stun for traversal.
wing -rendezvous-status all --graph
The first thing we do is generate a root key pair. This will be used to sign control packages, preventing rogue peers from joining your network. Do not lose your private key!
wing -genrootkeyNow, for every peer that we "invite" to our network, we generate a new key control key pair:
wing -issuepeerkey -root-private-key <root private key>-> this displays the peer's configuration: copy the output
then, on the corresponding peer, run
wing -setupprovide the required information; paste the above invite data when prompted.
We are now ready to build our network. Start one or more rendezvous nodes:
wing -serve-rendezvous -rendezvous-listen :8787 -rendezvous-trusted-roots <root public key> [-debug]On each node, start wing in daemon mode and it will "call home:"
sudo wing -daemonYou can query the list of registered peers at any time:
wing -rendezvous-status all [--json|--graph]sudo wing [-config config.example.json]If -config is not provided, wing will use ~/.wing/self.json by default.
If the config file does not exist (either the default or a specified path), wing will create it with defaults (same as -init) and then exit.
Quick setup (interactive defaults):
wing -setupNon-interactive setup:
wing -setup -address 10.7.0.1 -listen-port 51821 -mtu 1420-setup also prompts for my_endpoint in host:port form, and will use the current value as the default.
It also prompts for rendezvous URLs as a comma-separated list.
It then asks whether you want to edit the issued rendezvous identity block. If you say yes, you can paste the -issuepeerkey output; if you paste one, it replaces the local private_key through identity_signature fields in self.json.
If you set a local name, that name is also published in rendezvous records so directory listings can show something human-readable.
List peers:
wing -list-peersAdd a peer (interactive):
wing -add-peerRemove a peer (interactive):
wing -remove-peerExport this node as a peer JSON block:
wing -exportImport a peer JSON block:
wing -importIf wireguard-go is installed but not in PATH:
sudo wing -wireguard-go /full/path/to/wireguard-go -config config.example.jsonIf your distro installs it as wireguard, you can point to that binary:
sudo wing -wireguard-go /full/path/to/wireguard -config config.example.jsonTake down a lingering interface (Linux):
sudo wing -config config.example.json -downTake down a lingering interface (macOS):
sudo wing -config config.example.json -down
# If auto-detection fails:
sudo wing -config config.example.json -down -os-iface utunXTake down all interfaces created by wing (uses state files):
sudo wing -down-allShow status:
wing -config config.example.json -statusInspect rendezvous records for yourself or a peer:
wing -rendezvous-status self
wing -rendezvous-status all
wing -rendezvous-status peer-nameRun the local daemon:
sudo wing -daemonRun the rendezvous service:
wing -serve-rendezvous -rendezvous-listen :8787 -rendezvous-trusted-roots ROOT_A_PUB,ROOT_B_PUBEnable request/event logging on the rendezvous node:
wing -serve-rendezvous -debug -rendezvous-listen :8787 -rendezvous-trusted-roots ROOT_A_PUB,ROOT_B_PUBOr load trusted roots from config:
wing -config rendezvous.json -serve-rendezvousReturn to prompt and leave the interface up:
sudo wing -config config.example.json -detach
# Use -down to tear it down later.Generate keys without wg:
wing -genkey
wing -genrootkey
wing -issuepeerkey -root-private-key YOUR_BASE64_ROOT_PRIVATE_KEY
wing -genpskRoot-issued peer flow:
# central secure location
wing -genrootkey
wing -issuepeerkey -root-private-key YOUR_BASE64_ROOT_PRIVATE_KEY-issuepeerkey outputs a JSON bundle containing:
private_keypublic_keycontrol_private_keycontrol_public_keyroot_public_keyidentity_signature
That bundle is what you distribute to the peer. The root signature binds the peer's WireGuard public key and control public key to the trusted root.
Linux without full root (userspace):
sudo setcap cap_net_admin,cap_net_raw+ep ./wing
wing -config config.example.jsonconfig.example.json shows the shape. Only /32 IPv4 allowed_ips are accepted in this minimal version.
New control-plane fields:
control_private_key/control_public_key: Ed25519 keypair used to sign endpoint records.root_public_key/identity_signature: root-issued identity proof binding the peer's WireGuard and control public keys to a trusted root signer.name: optional local node name published in rendezvous records and used as the default export name.daemon.stun_servers: STUN servers used for reflexive endpoint discovery.rendezvous.urls: HTTP base URLs for redundant rendezvous services.rendezvous.trusted_root_public_keys: root public keys that a rendezvous server will accept for peer registrations.peer.control_public_key: trusted signing key for a peer's dynamic endpoint records.peer.root_public_key/peer.identity_signature: root-issued proof for the peer identity bundle.peer.dynamic_endpoint: allow the daemon to replace the peer endpoint from rendezvous records.-rendezvous-status: query each configured rendezvous server directly and show the newest record it sees.-rendezvous-status all: list every currently registered peer each configured rendezvous server knows about, plus the merged newest set across servers. Those records now include the peer name, endpoint, and allowed IPs when the publishing node has them configured.-genrootkey: generate the trusted root signing keypair.-issuepeerkey: generate a peer identity bundle and sign it with the root private key.-rendezvous-trusted-roots: comma-separated root public keys trusted by-serve-rendezvous.-debug: when combined with-serve-rendezvous, log registrations, lookup hits/misses, rejections, and record summaries.
So, is WireGuard not cooperating? Or is it?
The most common issue is forgetting to import the remote node on both peers.
- macOS:
wireguard-gocreates autunXinterface. This tool detects the newly createdutunand configures it. Don’t start multiplewireguard-goinstances at the same time. -reuseis supported on Linux only; macOS cannot reliably map an existing WG device to autunXinterface.- For a mesh, each node lists the other peers with their WG IPs and endpoints.
- Wing stores state files under
~/.wing(orWING_STATE_DIR) for-down-all. -down-allonly affects interfaces that were created by wing (state files are written only when wing creates a device).-initwrites~/.wing/self.jsonand includespublic_keyfor sharing with peers.-daemonis intended to run under launchd/systemd or another service manager; it stays in the foreground.- STUN discovery is best-effort. On the first daemon start it probes using the configured WG port; later refreshes may publish a guessed port if the live WG socket already owns the port.
- With multiple rendezvous servers configured, the daemon publishes to all of them and accepts the newest valid signed record it can fetch from the set.
- Redundant rendezvous fetches are done in parallel with a short timeout window, so one slow server does not hold up fresher records from faster ones.
-serve-rendezvousnow rejects records unless they carry a root-issued identity signed by one of the configured trusted roots, and it rejects attempts to replace an existing peer slot with a different identity binding.
- No default route changes.
- No DNS changes.
- Only a dedicated WG interface is created and host routes are added for peer WG IPs.
