Remote Dbus Notifications over SSH

Posted on Mar 29, 2023

I often tack on notify-send at the end of a long running command to get a desktop notification when the command is done.

./long-running-build-command; notify-send "Build done"

notify-send uses the Desktop Notification spec that relies on DBus to propagate a request to show a notification. This is a widely supported standard in the Linux ecosystem powering all notifications, regardless of which desktop environment you use.

However my simple approach doesn’t work as soon as I’m logged into a remote machine. At my current job, I’m nearly always logged into a remote machine.

Fortunately, because of how DBus is implemented, it turns out to be trivial to have a remote notify-send execution show a local desktop notification! Here is how.

Assumptions

  1. Both local and remote machines are running Linux.
  2. You are using SSH to access the remote machine.
  3. You have root access on the remote machine to tweak a sshd configuration value.

For the rest of this post, “local” means the computer you are physically at, “remote” is the one you are accessing via SSH.

My instructions work for me on a laptop (local) and desktop (remote) that are both stock Ubuntu 18.04 systems. As with all things Linux, YMMV.

Just the instructions

  1. SSH into the remote.
  2. Edit /etc/ssh/sshd_config as root on the remote. Add the line below to the end of the file.
StreamLocalBindUnlink yes
  1. Restart sshd on the remote
sudo systemctl restart sshd
  1. Add the following to your ~/.bashrc (or ~/.zshrc) on the remote. Modify as required for other shells.
# If the shell is running over SSH, override the session DBus socket to point to the one forwarded over SSH.
if  [ -n $SSH_CONNECTION ]; then
  export DBUS_SESSION_BUS_ADDRESS=unix:path=/tmp/ssh_dbus.sock
fi
  1. Close the SSH connection.
  2. In a local shell, run echo $DBUS_SESSION_BUS_ADDRESS. Note down this value. For me it is unix:path=/run/user/1000/bus.
  3. Edit your local ~/.ssh/config to add a Host entry (or you may already have one). Tweak some of the settings as appropriate.
Host remote-workstation
    # IP address/hostname of the remote.
    Hostname 192.168.34.11
    # ... Any other settings you may have like User or IdentityFile.

    # The crucial bits. It is confusing, but the remote socket comes first.
    # Replace `/run/user/1000/bus` by the value you got from step 6.
    RemoteForward /tmp/ssh_dbus.sock /run/user/1000/bus
  1. SSH into the remote again. Remember to ssh remote-workstation, instead of SSHing by IP, if you were doing that before. That tells SSH to use the settings we just added.
  2. If everything worked correctly, /tmp/ssh_dbus.sock should now exist on the remote. Also confirm that echo $DBUS_SESSION_BUS_ADDRESS is now /tmp/ssh_dbus.sock on the remote.
  3. Run notify-send "Hello there" on the remote.
  4. You should see a desktop notification on your local machine!
  5. All of this should Just Work for any future SSH connections to the same remote.

How this works

DBus works by having a single daemon that uses a Unix domain socket as the transport mechanism. Since different users can be logged in at the same time (and for other reasons), each user session typically gets a different daemon. The environment variable DBUS_SESSION_BUS_ADDRESS is set whenever a session bus is available. It tells all programs that want to use DBus what socket to use for communication.

Note that while theoretically DBUS_SESSION_BUS_ADDRESS could change every time you log in, in practice I’ve not seen it change, so I’ve hardcoded it in all my instructions above.

The actual program that shows the notification (e.g. Whatever KDE and Gnome have, dunst, rofication) all implement the standard service org.freedesktop.Notify over DBus. notify-send sends a DBus message to the bus, addressed to this service, with the notification contents.

To get the remote notify-send to show a local notification, we want to change the session bus it uses to be the one running on the local computer. Part of it is easily done by setting the bus to /tmp/ssh_dbus.sock. However, we still need to set up a channel so that any data written to or read from that socket is transparently sent to the local DBus.

Enter SSH port forwarding. SSH has this nifty feature that can be used for all sorts of service redirections. See this blog post for the details and example uses. In our case, we want to forward the local DBus' Unix domain socket (/run/user/1000/bus in my case) to /tmp/ssh_dbus.sock) on the remote. That is what the RemoteForward entries in the config file accomplish.

The final wrinkle is to address an issue where, when the SSH connection is closed (or terminates due to a disconnection), SSH won’t remove /tmp/ssh_dbus.sock on the remote by default. Subsequent attempts will then fail to establish the tunnel due to the socket already existing. The sshd_config setting StreamLocalBindUnlink fixes that.

As a side note, I spent some time tinkering with xdg-dbus-proxy1 and socat to “copy the socket” before realizing that they were redundant since SSH can already forward domain sockets. Hell, I was about to write a program to solve this problem. This turned into a classic case of how thinking more about the problem and prototyping with options often leads to finding a simple solution using extremely common existing tools.

References

  1. A Unix StackExchange answer that had some of the pieces.
  2. Another Unix StackExchange answer about fixing the socket cleanup issue.
  3. I’ve linked the others inline above.

  1. Including building it from source on Bionic with some Meson tweaks since it wasn’t available for older Ubuntus. ↩︎