Waypipe over WireGuard
The most direct way to run native Wayland applications remotely is with Waypipe. The simplest way to do this is over an SSH connection; but you can instead use WireGuard, with the help of a tool like Socat or Netcat.
In this case, WireGuard provides the transport security, while Socat or Netcat forwards the Waypipe streams between the computer the application is running on and the computer the application is displayed on. I’ll call the application runner the “server”, and the application displayer the “client”. Note, however, that this is backwards from the traditional X Windows architecture, where the application runner is considered the client, and the displayer is the server. This is also reflected by Waypipe (and Wayland itself), where what I’m calling the client creates a new socket to listen on, and the server connects to that existing socket (the reverse of normal client-server behavior).
WireGuard
First, we’ll set up WireGuard just like the WireGuard Point to Point Configuration article (refer to it for a detailed explanation of each config setting). Endpoint A from this article will be our client (the computer that displays the Wayland application), and Endpoint B from this article will be our server (the computer that runs the Wayland application).
On the client, we’ll use the following WireGuard config:
# /etc/wireguard/wg0.conf
# local settings for Endpoint A
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Address = 10.0.0.1/32
ListenPort = 51821
# remote settings for Endpoint B
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
Endpoint = 203.0.113.2:51822
AllowedIPs = 10.0.0.2/32
On the server, we’ll use the following WireGuard config:
# /etc/wireguard/wg0.conf
# local settings for Endpoint B
[Interface]
PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=
Address = 10.0.0.2/32
ListenPort = 51822
# remote settings for Endpoint A
[Peer]
PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
AllowedIPs = 10.0.0.1/32
Because the server’s WireGuard config uses 10.0.0.2
as the IP address for its wg0
interface, that’s the IP we’ll use later on to connect from the Waypipe client to the Waypipe server. This private IP address is only accessible through the WireGuard tunnel from Endpoint A in our scenario.
Wayland
On the client, start up a Wayland compositor (such as GNOME Shell, or your preferred window manager with Wayland support).
On both the client and the server, install Waypipe (its package name will be waypipe
on most Linux distros).
On the server, install the Wayland application (you don’t need to run a Wayland compositor or window manager on the server). For this example, we’ll use the weston-flower
program from the Weston project. You can install it via the weston-demo
package on most distros (or if the distro doesn’t have a weston-demo
package, it’s probably part of the main weston
package). This example program displays a window with a simple “flower” rendering; and changes the flower every time your mouse cursor enters or leaves the window.
Socat
The Waypipe client and server communicate via a UNIX domain socket (created by the client). So unless you run the client and server on the same machine, you need to run another program on both the client and the server to forward the UNIX socket through a network socket that connects the client and server machines. This is exactly what Socat is designed to do.
On both the client and the server, install Socat (its package name will simply be socat
on most distros).
On the server, run the following command to start Socat (using the -dd
option for some basic debug logging):
$ socat -dd UNIX-LISTEN:$HOME/.cache/flower.sock TCP-LISTEN:5999,bind=10.0.0.2 2025/10/07 12:52:56 socat[1150492] N listening on AF=1 "/home/guiuser/.cache/flower.sock"
This will start Socat listening on a UNIX socket at $HOME/.cache/flower.sock
, and a TCP socket at 10.0.0.2:5999
; and bidirectionally pipe each to the other. The TCP socket is listening on the server’s private WireGuard IP address of 10.0.0.2
, so only members of its WireGuard network (in this case only Endpoint A) will be able to access it; and using port 5999
, which is an arbitrary port I chose for this example (you can use any port you want).
In this example we’re using the $HOME/.cache/flower.sock
file descriptor to identify the UNIX socket, which is an arbitrary file path (you can use any file path you want — just make sure your user, and only your user, has access to its containing directory).
Next, in a different terminal on the server, run the following command with the same file descriptor to start up Waypipe:
$ waypipe --socket $HOME/.cache/flower.sock server -- weston-flower
Include the full command and arguments of the application to run after the --
indicator; in this case, the example weston-flower
program. This will start the application running, connected to Waypipe (through a new Wayland display socket managed behind the scene by Waypipe), and Waypipe will start forwarding its Wayland commands and data through the $HOME/.cache/flower.sock
socket, which will be buffered (up to a point) by Socat as it waits for a connection from the client.
If you switch back to the Socat terminal on the server, you should see it has printed out a couple more lines of logging, showing that it’s accepted the connection on the UNIX socket, and is ready to accept a connection on the TCP socket:
2025/10/07 12:53:05 socat[1150492] N accepting connection from AF=1 "<anon>" on AF=1 "/home/guiuser/.cache/flower.sock" 2025/10/07 12:53:05 socat[1150492] N listening on AF=2 10.0.0.2:5999
Now on the client, run the following command to start up Waypipe (and note on the server, we must start Socat first, then Waypipe; but on the client, we must start Waypipe first):
$ waypipe --socket $HOME/.cache/flower.sock client
This will start Waypipe listening on a UNIX socket with the client’s $HOME/.cache/flower.sock
file descriptor. This is an arbitrary file path that doesn’t need to match the server’s socket path — it just must match the file path of the Socat command we run next.
Finally, in a different terminal on the client, run the following command to start Socat and complete the connection between client and server:
$ socat -dd UNIX-CONNECT:$HOME/.cache/flower.sock TCP-CONNECT:10.0.0.2:5999 2025/10/07 12:53:10 socat[1443349] N opening connection to AF=1 "/home/justin/.cache/flower.sock" 2025/10/07 12:53:10 socat[1443349] N successfully connected from local address AF=1 "\xAEh" 2025/10/07 12:53:10 socat[1443349] N opening connection to AF=2 10.0.0.2:5999 2025/10/07 12:53:10 socat[1443349] N successfully connected from local address AF=2 10.0.0.1:48838 2025/10/07 12:53:10 socat[1443349] N starting data transfer loop with FDs [5,5] and [6,6] 2025/10/07 12:53:10 socat[1443349] N write(5, 0x59731af30000, 44) completed ...
This will start Socat, and connect it to the UNIX socket at $HOME/.cache/flower.sock
that was started by Waypipe in the other terminal; and to the TCP socket on the server at port 5999
. Since it uses the server’s private WireGuard IP address of 10.0.0.2
, all of its traffic will be encrypted and sent through the WireGuard tunnel we previously established between the client and server.
When Socat on the client connects to Socat on the server, the server will start sending Wayland commands and data through this connection to the Waypipe instance on the client — which should result in a new window opening on the client, and displaying the weston-flower
program running on the server.
In the terminal on the server running Socat, you should see much more logging output, starting with the following lines to show that it’s accepted a connection on its own private WireGuard IP address (10.0.0.2
) from the client’s private WireGuard IP address (10.0.0.1
):
2025/10/07 12:53:11 socat[1150492] N accepting connection from AF=2 10.0.0.1:48838 on AF=2 10.0.0.2:5999 2025/10/07 12:53:11 socat[1150492] N starting data transfer loop with FDs [6,6] and [7,7] 2025/10/07 12:53:11 socat[1150492] N write(7, 0x62d0757e4000, 44) completed ...
You can shut down the weston-flower
program by closing its window on your client; or by shutting down any of the other 5 processes we started between the client and server:
-
Socat on the server
-
Waypipe on the server
-
Weston-flower on the server
-
Waypipe on the client
-
Socat on the client
The only process that will survive the shutdown of the others is #4, Waypipe on the client — so you will need to shut it down explicitly to clean it up.
Here’s a generic shell script that you can use to start up the 3 processes on the server:
#!/bin/sh -eu # starts Waypipe server listener and Wayland application # server's WireGuard IP address address=10.0.0.2 # server's Waypipe port port=5999 # command to run application cmd=weston-flower # creates a temporary directory in which to create the Waypipe socket sock_dir=$(mktemp -d) sock=$sock_dir/waypipe.sock # cleans up the temporary socket directory and background jobs on exit trap "exit" INT TERM trap "rm -rf $sock_dir; kill 0" EXIT # starts the server listener as a background job socat UNIX-LISTEN:$sock TCP-LISTEN:$port,bind=$address & sleep 0.1 # starts the Wayland application waypipe --socket $sock server -- $cmd
And a generic shell script that you can use to start up the other 2 processes on the client:
#!/bin/sh -eu # starts Waypipe client and connects to a remote Wayland application # server's WireGuard IP address address=10.0.0.2 # server's Waypipe port port=5999 # creates a temporary directory in which to create the Waypipe socket sock_dir=$(mktemp -d) sock=$sock_dir/waypipe.sock # cleans up the temporary socket directory and background jobs on exit trap "exit" INT TERM trap "rm -rf $sock_dir; kill 0" EXIT # starts the Waypipe client as a background job waypipe --socket $sock client & sleep 0.1 # connects to the Wayland application socat UNIX-CONNECT:$sock TCP-CONNECT:$address:$port
Netcat
If you don’t have Socat installed on the client or server, you can make do with Netcat. You’ll need a version of Netcat that supports the -U
option (for UNIX sockets); usually this is the default when you install the netcat
package of a Linux distro (but on some distros you may need to install a different package, such as the netcat-openbsd
package).
While Socat allows you to connect two sockets bidirectionally, Netcat only allows you to pipe data to and from a single socket: Netcat’s stdin is piped into the socket, and the socket’s output is piped out of Netcat’s stdout. So to approimate Socat’s functionality, we need two separate instances of Netcat, each connected to a separate socket, and using a named piped (aka FIFO) for return traffic between them.
With this trick, we can substitute our FIFO and two Netcats for the single Socat on both client and server, and display the weston-flower
program on the client with a similar set of steps as we used with Socat.
First, on the server, run the following commands to create the named pipe and the two Netcats (optionally including the -v
Netcat option for some basic debug logging):
$ mkfifo $HOME/.cache/flower.fifo $ nc -lv 10.0.0.2 5999 < $HOME/.cache/flower.fifo | nc -lUv $HOME/.cache/flower.sock > $HOME/.cache/flower.fifo Bound on home/guiuser/.cache/flower.sock Listening on home/guiuser/.cache/flower.sock Listening on 10.0.0.2 5999
This sets up the named pipe on the server with a file descriptor of $HOME/.cache/flower.fifo
(like the socket path, an arbitrary path I chose for this example), and pipes it into the first Netcat instance, which listens on TCP port 5999
of the server’s private WireGuard IP address (10.0.0.2
). Output from the first Netcat instance is piped directly into the second Netcat instance, which listens on the UNIX socket with a $HOME/.cache/flower.sock
file descriptor; and the second Netcat instance pipes its output into the named pipe.
Next, in a different terminal on the server, run the following command with the same UNIX socket to start up Waypipe (the exact same command we used with Socat):
$ waypipe --socket $HOME/.cache/flower.sock server -- weston-flower
If you switch back to the Netcat terminal on the server, you should see it has printed out another line of logging, showing that it’s accepted the connection on the UNIX socket:
Connection received on /home/guiuser/.cache/flower.sock
Now on the client, run the following command to start up Waypipe (with the exact same command we used with Socat):
$ waypipe --socket $HOME/.cache/flower.sock client
Finally, in a different terminal on the client, run the following commands to create the named pipe and the two Netcats on the client to complete its connection to the server:
$ mkfifo $HOME/.cache/flower.fifo $ nc -v 10.0.0.2 5999 < $HOME/.cache/flower.fifo | nc -Uv $HOME/.cache/flower.sock > $HOME/.cache/flower.fifo Connection to 10.0.0.2 5999 port [tcp/*] succeeded!
This will connect the first Netcat to the TCP listener on the server, and the second to the socket the Waypipe client created. The first Netcat will pipe its output directly into the second, and the second will return its output through the named pipe.
When the first Netcat on the client connects to the first Netcat on the server, the server will start sending Wayland commands and data through the connection, which will be piped to the other Netcat instance on the client, and on to the Waypipe client — which should result in a new window opening on the client, and displaying the weston-flower
program.
In the terminal on the server running the two Netcats, you should see one more log line of output, showing that it’s accepted the connection from the client’s private WireGuard IP address (10.0.0.1
):
Connection received on 10.0.0.1 38176
Like with Socat, you can shut down the weston-flower
program by closing its window on your client; or by shutting down any of the other processes we started on the client and server. And like with our Socat example, the only process that will survive the shutdown of the others is Waypipe on the client (so you will need to shut it down explicitly to clean up).
However, unlike Socat, Netcat will not clean up the UNIX socket file it creates on the server — so you will have to delete in manually (eg rm $HOME/.cache/flower.sock
) after shutting down the Netcats. Also, you’ll have to manually delete the named pipes on both client and server (eg rm $HOME/.cache/flower.fifo
); although unlike the socket file, the FIFO files can be reused later by other processes if you do keep them around.
Systemd
For further convenience, you can set up a suite of systemd services on the server to listen on a fixed TCP port and start up Waypipe and the Wayland application on demand, whenever a client tries to connect. This requires 3 unit files:
-
A socket file for the TCP listener (eg
flower-listener.socket
) -
A service file for Socat (eg
flower-listener.service
) -
A service file for Waypipe & the application (eg
flower.service
)
For the example weston-flower
program, we’d first create a socket unit as the /etc/systemd/system/flower-listener.socket
file:
# /etc/systemd/system/flower-listener.socket [Unit] Description=socket for flower-listener service [Socket] ListenStream=5999 BindToDevice=wg0 [Install] WantedBy=sockets.target
When this unit is started, systemd will create a TCP listener on port 5999
, listening exclusively on the server’s wg0
WireGuard interface. When a client tries to connect to port 5999
, systemd will expect a service unit with a matching name (flower-listener.service
) to exist, and try to start it and pass it the connected socket.
So next, we need to create this service unit, via the /etc/systemd/system/flower-listener.service
file:
# /etc/systemd/system/flower-listener.service [Unit] Description=listener for connections to forward to weston-flower BindsTo=flower.service Before=flower.service [Service] ExecStart=socat ACCEPT-FD:3 UNIX-LISTEN:/run/flower/waypipe.sock User=guiuser Group=guiuser RuntimeDirectory=flower
When this unit is started, systemd will create a new /run/flower
directory for the service (based on its RuntimeDirectory=flower
directive), with the directory’s user and group set to guiuser
; and then run Socat as the guiuser
user, passing it the socket from the socket-listener.socket
unit as its file descriptor 3. We need to make sure we first create this guiuser
and group (eg sudo useradd --create-home --shell /bin/bash guiuser
); this will also be the user with which the Wayland application is run.
The BindsTo=flower.service
and Before=flower.service
directives in the unit indicate to systemd that after successfully starting the flower-listener.service
unit, it must start the flower.service
unit. So we’ll also need to create this service unit, via the /etc/systemd/system/flower.service
file:
# /etc/systemd/system/flower.service [Unit] Description=waypipe running weston-flower [Service] ExecStart=waypipe --socket /run/flower/waypipe.sock server -- weston-flower Environment=XDG_RUNTIME_DIR=/run/flower User=guiuser Group=guiuser
This unit will run Waypipe with the example weston-flower
program, connected to the other end of the /run/flower/waypipe.sock
UNIX socket created by Socat in the flower-listener.service
. It will be run as the guiuser
user, with its $XDG_RUNTIME_DIR
environment variable set to the same /run/flower
directory that was created by systemd in the flower-listener.service
(when you log in as a user, $XDG_RUNTIME_DIR
is normally set to /run/user/$UID
and created automatically as part of the log-in process; this does not happen when running a systemd service, however, so we need to set $XDG_RUNTIME_DIR
explicitly to a directory in which Waypipe can create its own isolated Wayland display socket, to which the Wayland application will connect — and /run/flower
is convenient for this purpose).
After creating these three unit files, run the following commands to load them up, and start the socket listening:
$ sudo systemctl daemon-reload $ sudo systemctl start flower-listener.socket
This will start the socket unit, but not the two service units.
Tip
|
To configure the socket to start listening automatically whenever the system boots up, also run the following command:
|
On the client, run the Waypipe client in one terminal:
$ waypipe --socket $HOME/.cache/flower.sock client
And then the Socat client in another terminal:
$ socat UNIX-CONNECT:$HOME/.cache/flower.sock TCP-CONNECT:10.0.0.2:5999
When the Socat client connects to the socket defined by the flower-listener.socket
unit on the server, systemd should automatically start the flower-listener.service
unit, followed by the flower.service
unit; which should start up the weston-flower
program on the server, connect it to Waypipe on the server, and start sending Wayland commands and data through TCP socket tunneled through WireGuard between the two Socat programs, and on to the Waypipe client; and the Waypipe client should display the window for the weston-flower
program via the client’s Wayland compositor.
You can shut down the weston-flower
program by closing its window on your client; or by killing Socat or Waypipe on the client; or by stopping any of the 3 units on server (but the best unit to stop would be the flower.service
).
As long as you don’t kill the Waypipe client or the flower-listener.socket
unit, they should remain running; whereas the Socat process on the client will exit and the flower-listener.service
and flower.service
units on the server will stop if you kill any of the processes, or stop any of the units. So if you were to simply close the weston-flower
window on the client, you could start the weston-flower
program back up again by running Socat again on the client:
$ socat UNIX-CONNECT:$HOME/.cache/flower.sock TCP-CONNECT:10.0.0.2:5999
If you use the client script shown at the end of the Socat section above, this makes running and attaching to the remote Wayland application on the server just as easy as if it were running locally on the client.
Security
The biggest area of concern when using Waypipe is that it allows whatever remote application to which you’ve connected unfettered access to your local Wayland compositor (ie your window manager), as if it were a local application running on your local computer. So if your local compositor allows any application to take screenshots of your screen, or to get & set the content of your clipboard, a malicious remote application to which you’ve connected through Waypipe could do the same. Furthermore, if your local compositor suffers from memory safety or other security-related bugs, a malicious remote application may able to exploit those bugs.
So in general, you should not use Waypipe to connect from a highly-trusted local computer to a less-trusted remote computer; you should only use Waypipe to connect between computers with the same level of trust. When you need to connect to a less-trusted remote computer to run a GUI application, it would probably be better to use VNC (or similar remote-desktop protocol), as it adds another layer of indirection between your local Wayland compositor and the remote application (forcing an adversary to navigate through that additional layer in order to attack vulnerabilities in your compositor).
Also note that for its 0.10.0 release, Waypipe was rewritten in Rust, hardening it against most memory-safety issues; however, most Linux distributions still package an older C-based version of it.
Firewall
The way we’ve configured the TCP listener on the server with Socat, Netcat, and Systemd above — setting it up to listen only on the server’s private WireGuard IP address — ensures that the only way to connect remotely to the application is through WireGuard, regardless of firewall settings.
However, if your WireGuard network allows more than one WireGuard client to connect to the server running the remote application, you may want to configure the server’s firewall to apply fine-grained access control to allow only certain WireGuard clients to connect to the application.
For example, if the server had the following WireGuard configuration, with multiple WireGuard clients able to connect to it:
# /etc/wireguard/wg0.conf
# local settings for remote server
[Interface]
PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=
Address = 10.0.0.2/32
ListenPort = 51822
# remote settings for Justin's Laptop
[Peer]
PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
AllowedIPs = 10.0.0.1/32
# remote settings for Mail Server
[Peer]
PublicKey = jUd41n3XYa3yXBzyBvWqlLhYgRef5RiBD7jwo70U+Rw=
AllowedIPs = 10.0.0.3/32
# remote settings for Web Server
[Peer]
PublicKey = MMsBeTT9v/7XNWB8a/jSMn9O8olPVNduUwvUPJ6eB14=
AllowedIPs = 10.0.0.4/32
# remote settings for Justin's Desktop
[Peer]
PublicKey = kAYKIv6gpBDqueaVNOsY8ddKmIC2dnnXJQ0iKzWxfBI=
AllowedIPs = 10.0.0.5/32
And I wanted only my laptop and desktop to be able to access the Wayland application exposed via TCP port 5999, I could set up the following nftables ruleset on the server to do so:
#!/usr/sbin/nft -f
flush ruleset
define pub_iface = "eth0"
define wg_port = 51822
define justins_laptop = 10.0.0.1
define justins_desktop = 10.0.0.5
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
# accept all loopback packets
iif "lo" accept
# accept all icmp/icmpv6 packets
meta l4proto { icmp, ipv6-icmp } accept
# accept all packets that are part of an already-established connection
ct state vmap { invalid : drop, established : accept, related : accept }
# drop new connections over rate limit
ct state new limit rate over 1/second burst 10 packets drop
# accept all DHCPv6 packets received at a link-local address
ip6 daddr fe80::/64 udp dport dhcpv6-client accept
# accept all SSH packets received on a public interface
iifname $pub_iface tcp dport ssh accept
# accept all WireGuard packets received on a public interface
iifname $pub_iface udp dport $wg_port accept
# accept Waypipe connections from specific WireGuard clients
ip saddr { $justins_laptop, $justins_desktop } tcp dport 5999 accept
# reject with polite "port unreachable" icmp response
reject
}
chain forward {
type filter hook forward priority 0; policy drop;
reject with icmpx type host-unreachable
}
}
This ruleset starts with the nftables base configuration from the How to Use WireGuard With Nftables article, and adds the following rule to its input
chain to expose TCP port 5999 only to my laptop and desktop machines (using the define justins_laptop = 10.0.0.1
and define justins_desktop = 10.0.0.5
variable definitions at the top of the rulset):
# accept Waypipe connections from specific WireGuard clients
ip saddr { $justins_laptop, $justins_desktop } tcp dport 5999 accept
This would block all WireGuard clients except for the two I explicitly authorized from connecting to the Wayland application.