Wishlist Endpoint Discovery
Learn how to use the recently-added Tailscale, DNS, and Zeroconf endpoint discovery in …
Having your favorite commands available over SSH can be very convenient.
I think I talked about this a couple of times before, but I usually work by SSH-ing from my mac into a Linux machine (a rather chunky one, might I add).
While it allows me to work faster when I’m not home and with a poor internet
connection, it has some drawbacks too. Two of them are the lack of clipboard
integration and the fact that open
(or xdg-open
) won’t work.
In this post I’ll show you how I got around that. It’s worth nothing that I’ll focus more on a macOS to Linux workflow, and will hereby refer to them as client and host.
Enough said, let’s get to it!
One common way to share the clipboard with the client is using the OSC52 ANSI Sequence.
It’s likely your favorite $EDITOR
has a plugin for it, for example:
vim and
neovim.
But, sometimes what you really want is to run some command | pbcopy
and then
paste it somewhere, and in which case OSC52 won’t help you - it can copy to the
client clipboard, but you can’t read it in the host.
An idea to overcome that is to run the native pbcopy
and pbpaste
as services
on the client, and then RemoteForward
them to the host, using nc
to talk
with them.
To do that, on the client, create these 2 file:
~/Library/LaunchAgents/localhost.pbpaste.plist
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>KeepAlive</key>
<dict>
<key>Crashed</key>
<true/>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>Label</key>
<string>localhost.pbpaste</string>
<key>ProcessType</key>
<string>Background</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/pbpaste</string>
</array>
<key>Sockets</key>
<dict>
<key>Listener</key>
<dict>
<key>SockNodeName</key>
<string>127.0.0.1</string>
<key>SockServiceName</key>
<string>2225</string>
</dict>
</dict>
<key>inetdCompatibility</key>
<dict>
<key>Wait</key>
<false/>
</dict>
</dict>
</plist>
~/Library/LaunchAgents/localhost.pbcopy.plist
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>KeepAlive</key>
<dict>
<key>Crashed</key>
<true/>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>Label</key>
<string>localhost.pbcopy</string>
<key>ProcessType</key>
<string>Background</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/pbcopy</string>
</array>
<key>Sockets</key>
<dict>
<key>Listener</key>
<dict>
<key>SockNodeName</key>
<string>127.0.0.1</string>
<key>SockServiceName</key>
<string>2224</string>
</dict>
</dict>
<key>inetdCompatibility</key>
<dict>
<key>Wait</key>
<false/>
</dict>
</dict>
</plist>
Then, start them with:
launchctl start localhost.pbcopy
launchctl start localhost.pbpaste
Now, we can RemoteForward
ports 2224
and 2225
to our host:
ssh -R localhost:2224:127.0.0.1:2224 \
-R localhost:2225:127.0.0.1:2225 \
user@server
Then, we can test it with nc
:
echo "example" | nc -q1 localhost 2224
nc -q1 -d localhost 2225
Okay, this works, and pressing <Cmd>-V
should give you the same output. Go
ahead, try it!
pbcopy
andpbpaste
scripts, which I add to my $PATH
with a higher precedence than
/usr/bin
.
Here are their contents:
#!/bin/bash
# ~/.bin/pbcopy
set -eo pipefail
if test -n "$SSH_TTY"; then
nc -q1 localhost 2224
elif test "$(uname)" = Darwin; then
/usr/bin/pbcopy
else
xsel --clipboard --input
fi
#!/bin/bash
# ~/.bin/pbpaste
set -eo pipefail
if test -n "$SSH_TTY"; then
nc -q1 -d localhost 2225
elif test "$(uname)" = Darwin; then
/usr/bin/pbpaste
else
xsel --clipboard --output
fi
On both scripts, I check if I’m on a SSH connection before using nc
. If I’m
not, I use either the native pbcopy
and pbpaste
or Linux’ xsel
.
This way I can easily pipe into and from pbcopy
and pbpaste
on my host,
sharing the clipboard with the client!
This assumes you add
~/.bin
to your$PATH
, for example, with:export PATH="$HOME/.bin/:$PATH"
open
workopen
(or xdg-open
) is used by many tools and scripts to, among other things,
open a browser at a given URL. It might have surprised you the first time you
tried it over SSH though, as it would have given you an error like this:
Error: no DISPLAY environment variable specified
Which makes sense, as there is indeed no $DISPLAY
available, and even it had,
it wouldn’t open in the client, but rather in the host.
To fix that, we’ll do the same thing we did with the clipboard: run a service on
the client, and using nc
on the host.
There’s no way (that I’m aware of) of running macOS’ open
as a service, so I
wrote a small Go service that listen to income connections, and run open args
.
You can grab it here.
It works on both Linux and macOS, and on macOS you can install it with:
brew install caarlos0/tap/xdg-open-svc
If you install it through brew
, it’ll create the service for you
automatically, otherwise you’ll need to create it yourself. See the config file
bellow.
~/Library/LaunchAgents/localhost.xdg-open.plist
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>KeepAlive</key>
<true/>
<key>Label</key>
<string>localhost.xdg-open</string>
<key>ProgramArguments</key>
<array>
<string>/path/to/bin/xdg-open-svc</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>/Users/USERNAME/.cache/xdg-open-svc.log</string>
<key>StandardOutPath</key>
<string>/Users/USERNAME/.cache/xdg-open-svc.log</string>
</dict>
</plist>
PS: make sure to change the paths above, namely the path to the binary and to the log files.
Then, start the service:
launchctl start localhost.xdg-open
We can then forward it to our host as well:
ssh -R localhost:2224:127.0.0.1:2224 \
-R localhost:2225:127.0.0.1:2225 \
-R localhost:2226:127.0.0.1:2226 \
user@server
Now, you should be able to test it with nc
:
echo "https://carlosbecker.com" | nc localhost 2226
As with the clipboard, we can make our own open
. Here’s the contents of mine:
#!/bin/bash
# ~/.bin/open
# ~/.bin/xdg-open
set -eo pipefail
if test -n "$SSH_TTY"; then
echo "$@" | nc -q1 localhost 2226
else
/usr/bin/open $*
fi
This way, open
works, and if you ln
it to xdg-open
, so does xdg-open
.
And with that, other CLIs should be able to just call open
and it’ll open on
the client machine.
To avoid having to type all those -R
parameters every time, you can add this
to your ~/.ssh/config
:
Host server
User user
RemoteForward [localhost]:2224 [127.0.0.1]:2224
RemoteForward [localhost]:2225 [127.0.0.1]:2225
RemoteForward [localhost]:2226 [127.0.0.1]:2226
So you can simply ssh server
and it’ll always do the RemoteForward
s for you.
ncat
While writing this, GitHub user @pbnj opened an
issue pointing out this is
also possible with ncat
. That binary is usually distributed with the nmap
package, and you can use it instead of xdc-open-svc
if you want.
Main differences would be that xdg-open-svc
logs everything, listens on
localhost
instead of 0.0.0.0
, and if you install it with Homebrew, it’ll set
the service up for you automatically. Other than that, they should work the
same.
If you use nix and home-manager, you can copy my services from here.
You can also install xdg-open-svc
using my
NUR.
Also feel free to check my nix dotfiles.
—
That’s it! Hope you enjoyed this and that it helps you somehow. Cheers!