Status: pre-alpha, verified through the real dispatch path
GPU forwarding, hardware encoding, audio — all live-tested
Not assumed from the docs: a real vkcube and a real
Firefox window, GPU selected on the remote end, verified
rendering on screen. Hardware H.264 encoding confirmed via a
real encode queue on the sending GPU — and confirmed to
matter: on an actual video-playing forward, software frame
compression cost ~90% of one CPU core (the single largest
consumer on the box, ahead of the app itself) and was a
reproduced cause of audio stutter in a separate real-time audio
tunnel running alongside it. Hardware encoding dropped that to
~6% for the identical workload — why it's the default, not
an opt-in.
A real course correction, kept in the docs on purpose
The first design assumed a third-party SSH client's documented
EnableWaypipe feature would handle waypipe's
lifecycle. It doesn't exist in the version this was actually
built against — every invocation was silently accepted
and did nothing, and the "working" forward was really just an
app running invisibly on the remote machine's own screen. Clean
exit codes were never proof of anything; only watching the
actual pixels caught it. Rebuilt around waypipe's own
ssh mode, which needs no third-party client at all.
No test suite yet
Every claim on this page was verified live, by hand, against two real machines — not by an automated suite. The address cascade's fallback behavior, orphan reaping, and audio routing have each been forced into their failure path at least once and watched recover; none of that is codified as a test yet.
What it does
Address cascade
Declare an ordered list of candidate addresses per peer —
a fast LAN IP first, an overlay/VPN address as fallback.
Rendered into native OpenSSH Match ... exec blocks
with a bundled TCP-probe script, so a plain ssh or
the generated wrapper resolves to whichever address actually
answers, with no client-side logic of its own.
Hardware video encoding
video = "h264" by default (also vp9,
av1, or none) moves motion content off
CPU compression and onto the sending GPU's dedicated encode
engine — confirmed live to be the difference between a
usable forward and one that visibly starves everything else
running alongside it.
Audio that follows you
waypipe forwards Wayland only — audio has nothing to do
with it, so a forwarded app's sound plays out of the peer's own
speakers by default. audio.localAddress has every
launch resolve which of the peer's PipeWire-mesh-mirrored sinks
corresponds to your current default output, and routes there
instead. Best-effort: no mesh, no match, no problem — it
just falls back to today's behavior.
<app>@<peer> dispatch
Optional fish integration: firefox@some-peer works
for any app name, matching the tmux@<host> /
zellij@<host> convention already in use
here — no need to pre-declare every app you might
ever forward.
Orphan reaping
waypipe's own cleanup only works when the forwarded app exits on
its own — killing the local wrapper (or a dropped network
path) leaves the remote side running, a known upstream
limitation. nixremote-reap-<peer> finds and
kills exactly the orphaned process trees left behind, safe to
run any time.
No AUR, no pacman, no dependency
Package provisioning comes straight from nixpkgs via
home.packages. Only nixpkgs +
home-manager required — usable on any
Wayland-capable, home-manager-managed system, not just an
Arch/nixarch box.
Get started
1. Add the flake input
inputs.nixremote.url = "github:julian-corbet/nixremote-corbet-ch";
2. Import and declare a peer
imports = [ inputs.nixremote.homeManagerModules.forward ];
nixremote.forward.some-peer.addresses = [
{ address = "192.168.1.10"; }
{ address = "100.64.0.10"; probeTimeoutMs = 500; }
];
3. One manual, one-time step
This module deliberately never touches ~/.ssh/config
itself — add this as its first line, once, on each machine:
Include ~/.ssh/conf.d/nixremote.conf
Commands
waypipe@some-peer firefox | Forward a native window from some-peer, wherever it currently answers. |
firefox@some-peer | Same, via the optional fishDispatch integration — any app name, no pre-declaration. |
nixremote-reap-some-peer | Find and kill orphaned remote process trees left by a killed local wrapper. Safe on demand. |
Roadmap (planned, not yet built)
A merged local/remote app library
Invoke an app by name and it transparently runs wherever it actually lives — local if present, else forwarded from whichever peer has it — without the caller needing to know or care which. Whether this is actually needed in practice is genuinely open.
Declarative known_hosts pinning
A programs.ssh.knownHosts-style option, if the
standard first-connection trust prompt (now consolidated to once
per peer alias) turns out to be more friction than it's worth.
A NixOS-module mirror
For parity with how sibling projects export both layers, if a system-level piece of this ever turns out to be needed — nothing here currently requires root.
Integration tests
Exercising the address cascade against simulated network-partition scenarios, not just eval-time type checks.