Skip to content

Instantly share code, notes, and snippets.

@dcode
Created January 25, 2024 16:08
Show Gist options
  • Save dcode/912be128174c468a2dc28f263bdb7328 to your computer and use it in GitHub Desktop.
Save dcode/912be128174c468a2dc28f263bdb7328 to your computer and use it in GitHub Desktop.
Automatic waypipe setup for SSH remote forwarding of clipboard (via wl-copy/paste) and other Wayland clients

README

The goal of this gist is to setup waypipe to automically run locally on user login and remotely when you connect to it. In both cases, this is managed by systemd user session and assumes that is running. This took way too long to figure out, so I hope it helps someone (or me) in the future.

Instructions

Local

On your local system, copy the waypipe-client.service to the user systemd directory.

install -d 0755 ${HOME}/.config/systemd/user/
install waypipe-client.service ${HOME}/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now waypipe-client.service

Configure SSH Remote Forward by creating or amending an entry for your remote host. It should look like the ssh_config file in this gist.

Remote

On the remote system, do similarly with waypipe-server.service.

install -d 0755 ${HOME}/.config/systemd/user/
install waypipe-server.service ${HOME}/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now waypipe-server.service
# Append to ${HOME}/.ssh/config
Host my-remote
# other relevant configuration
RemoteForward ${XDG_RUNTIME_DIR}/waypipe-server.sock:${XDG_RUNTIME_DIR}/waypipe-client.sock
# ${HOME}/.config/systemd/user/waypipe-client.service
[Unit]
Description=Runs waypipe on startup to support SSH forwarding
[Service]
ExecStart=/usr/bin/waypipe --socket %t/%N.sock client
[Install]
WantedBy=default.target
# ${HOME}/.config/systemd/user/waypipe-server.service
[Unit]
Description=Runs waypipe on startup to support SSH forwarding
[Service]
Type=simple
ExecStart=/usr/bin/waypipe --socket %t/%N.sock --no-gpu --display %t/wayland-remote server -- sleep inf
[Install]
WantedBy=default.target
@dcode
Copy link
Author

dcode commented Jan 25, 2024

I tried to make this work with systemd socket activation, but it seems that just isn't possible yet. I filed a feature request.

@b2ag
Copy link

b2ag commented Aug 21, 2024

I read the configs above as only working with one "waypipe-client". So connecting from two different clients at the same time would pose a problem. Please correct me if I'm wrong. Thanks for the examples given.

@dcode
Copy link
Author

dcode commented Jun 25, 2025

sorry for the lag, but @b2ag you're absolutely correct. I'm not entirely sure how you would handle multiple SSH clients at the same time for the same user... it might be possible to spawn a template service unit based on some SSH session-specific environment variable, if such a thing exists....or perhaps leveraging RemoteCommand on the client-side of your SSH connection to spawn a specific instance.

@zelch
Copy link

zelch commented Sep 20, 2025

@dcode / @b2ag I have a vaguely working solution, but it has two glaring catches, which combine to make it far easier to just say 'always use waypipe ssh'. (And possibly alias ssh in your shell to make that more practical.)

The first issue with my solution is that the remote server must allow the WAYLAND_DESKTOP environment variable to be set by the client. This is, frankly, the biggest issue, as it means adding it to /etc/sshd_config AcceptEnv.

The second issue is that there's no way to dynamically set WAYLAND_DESKTOP in your ~/.ssh/config, this one is more minor, but it's a little annoying.

You do get free use of multiplexed SSH connections though.

With all of those warnings given, it looks like this:

Host some_system
    Tag waypipe

Match Tagged waypipe
    SetEnv WAYLAND_DISPLAY=suzy_example@example

Match Tagged master
    ControlPath %d/.ssh/control/%r@%h-%p
    ControlMaster yes
    ControlPersist 5s
    StdinNull yes
    ForkAfterAuthentication yes
    SessionType None

Match Tagged control
    ControlPath %d/.ssh/control/%r@%h-%p
    ExitOnForwardFailure yes

Match Tagged !master,!control,waypipe Exec "waypipe_preflight -p %p %r@%n"
    ControlPath %d/.ssh/control/%r@%h-%p

With the following as waypipe_preflight somewhere in your PATH:

#! /bin/bash

debug() {
    echo "$@"
}

run() {
    if [[ -n "${DBG}" ]]; then
        set -x
    fi
    LOCAL_SOCK="${XDG_RUNTIME_DIR}/waypipe-client.sock"

    if ! pgrep -f 'waypipe.*client' >/dev/null; then
        rm -f "${LOCAL_SOCK}"
        waypipe --socket "${LOCAL_SOCK}" client &
    fi

    ID="$(whoami)@$(hostname)"
    REMOTE_SOCK="\${XDG_RUNTIME_DIR}/waypipe-server-${ID}.sock"

    if ! ssh -P control -O check "$@"; then
        debug "multiplexer not running..."
        ssh ${DBG} -P master "$@"
        RESOLVED="$(ssh -P control "$@" echo "${REMOTE_SOCK}")"
        debug "Resolved sock: '${RESOLVED}'"
        debug "Local sock: '${LOCAL_SOCK}'"
        ssh ${DBG} -P control "$@" rm -f "${RESOLVED}" "\${XDG_RUNTIME_DIR}/${ID}"
        ssh ${DBG} -P control "$@" pgrep -a -f "'waypipe --display ${ID}'"
        ssh ${DBG} -P control -O forward -R "${RESOLVED}":"${LOCAL_SOCK}" "$@"

        # We really want waypipe to die properly if/when the connection dies.
        # But we don't want it to die otherwise.
        # This seems to work, but I really wish waypipe just had a way to run without a child process.
        (tail -f /dev/null | ssh -P control -tt "$@" waypipe --display "${ID}" --no-gpu --socket "${RESOLVED}" server -- cat) &
    fi
    debug "After multiplexer."
}

if false; then
    DBG=-vvv
    run "$@" >>/tmp/ssh.log 2>&1
else
    DBG=""
    run "$@"
fi

@b2ag
Copy link

b2ag commented Sep 26, 2025

Took me some time to read and understand. Nice match + exec hack there.

I read your solution is never exiting the "control" tagged ssh process. Correct me if I'm wrong. I guess only if the connection is interrupted by something. Not sure if I like that. I tried some -P control or master or nothing -O stop host commands but they only spawned more ssh processes.

Regarding the tail |ssh host cat I usually do a ssh host sleep inf or ssh host sleep infinity (whatever argument works) in situations like these like dcode did.

I guess I'll steal your idea of having a setup step upfront by using exec in the match line. But I'll try to use it to trigger a template version of the waypipe-server.service to start the individualized remote waypipe server. I'll share my code when it's done.

Thank you very much for sharing your solution!

@b2ag
Copy link

b2ag commented Sep 26, 2025

My solution is now based on @dcode's and @zelch's.

I changed the waypipe-server.service to [email protected] with following content.

[Service]
ExecStart=waypipe --socket %t/%N.sock --unlink-socket --no-gpu --display wayland-%i server -- sleep inf
ExecStartPre=-rm '%t/wayland-%i'
ExecStopPost=-rm '%t/wayland-%i'
Restart=on-failure
Slice=session.slice

[Unit]
Description=Runs waypipe server for %I

And use waypipe-client.service almost unchanged.

Than I tagged my waypipe hosts in ~/.ssh/config but did the setup step upfront like follows. Assuming [email protected] is available on the remote host.

Host some_system
  Tag waypipe

Match Tagged waypipe,!waypipe-setup Exec "ssh -P waypipe-setup -p %p %r@%n systemctl --user start waypipe-server@%u@%L --no-block || true"
  RemoteForward ${XDG_RUNTIME_DIR}/waypipe-server@%u@%L.sock:${XDG_RUNTIME_DIR}/waypipe-client.sock
  SetEnv WAYLAND_DISPLAY=wayland-%u@%L

EDIT: Just noticed that XDG_RUNTIME_DIR locally and on the remote side could differ. That won't happen for my setups but could happen in general and is NOT handled correctly here. Sorry for that.

Thanks again to both of you! ❤️

@zelch
Copy link

zelch commented Sep 27, 2025

@b2ag I started with sleep inf, but found that after connections died or were killed I had 'sleep inf' processes hanging around on the server.

That wasn't very friendly, thus the tail | ssh host cat hack.

And ssh -O exit host does the job of killing the connection cleanly. :)

I'm going to have to play with your service, that looks pretty nice.

@b2ag
Copy link

b2ag commented Sep 27, 2025

I started with sleep inf, but found that after connections died or were killed I had 'sleep inf' processes hanging around on the server.
That wasn't very friendly, thus the tail | ssh host cat hack.

I see. In situations like that I usually did a sleep 1d or similar. Nice idea to use cat as a detection for a closed connection/stdin.

And ssh -O exit host does the job of killing the connection cleanly. :)

Understood. Not used to using exit. Stop usually worked for me 😅

I'm going to have to play with your service, that looks pretty nice.

Give it a shot!

More thoughts:

Automatically stopping the waypipe server services after use would be nice. Tracking all potential users (e.g. ssh connections) or tracking by activity would be quite hard though. A sleep 1d instead of a sleep inf would ensure the services stops, but is NOT guarantied to NOT happen in the middle of your work session the next day. Something to reset a stop timer timeout is missing here.

Also I put a || true in the exec statement to ensure it never fails because it's a hack and not really a qualifier script or something. Though if the service isn't present at the remote host, without this || true, the matching should fail, which would prevent the remote forwarding and setting of WAYLAND_DISPLAY. A desired outcome prevented by my addition I guess 🤷

Happy hacking and have a nice weekend!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment