Emacs remote file editing over TRAMP

Posted by Michał ‘mina86’ Nazarewicz on 31st of January 2021

I often develop software on remote machines; logged in via SSH to a workstation where all source code reside. In those situations, I like to have things work the same way regardless of which host I’m on. Since more often than not I open files from shell rather than from within my editor, this in particular means having the same command opening files in Emacs available on all computers. emacsclient filename works locally but gets a bit tricky over SSH.

Running Emacs in a terminal is of course possible, but graphical interface provides minor benefits which I like to keep. X forwarding is another option but gets sluggish over high-latency connections. And besides, having multiple Emacs instance running (one local and one remote) is not the way.

Fortunately, by utilising SSH remote forwarding, Emacs can be configured to edit remote files and accept server commands from within an SSH session. Herein I will describe how to accomplish that.

Starting Emacs server

First step is to get edit server running. Edit server is a feature of Emacs which allows the editor to be controlled by a separate process, namely emacsclient. Its most obvious benefit is opening files in an existing Emacs instance rather than having to launch a new copy each time. The simplest way to start a server is by calling start-server command inside of Emacs by typing M-x server-start RET. If the server is working correctly, executing emacsclient -e emacs-version in a terminal should result in Emacs version being printed.

$ emacsclient -e '
(if server-use-tcp (error "Using TCP")
  (concat server-socket-dir "/" server-name))

There are other ways to start a server. Whichever method one picks, the important points are to i) make sure TCP is not used and ii) note where the UNIX-domain socket is created. The former is the case if server-use-tcp is nil; the later is indicated by server-socket-dir and server-name variables. Their values can be inspected by typing C-h v variable-name RET (which runs describe-variable command).

VariableExpected value

Note that where exactly the socket ends up may vary. To keep things simple, this article assumes values as listed in the table on the right. Depending on particular system, commands and configuration described further down may need adjustments to match where the server’s socket is actually located.

Security implications

Before going further, there’s an important security consideration. The edit server protocol allows for Elisp forms to be sent for evaluation. This means that whoever connects to the editor can execute arbitrary code with privileges of the user Emacs runs under. Normally this is not an issue since only the user running the server is able to connect to it.

However, just like with X and agent forwarding, remote socket forwarding gives remote host’s administrator access to local socket which can be exploited to run arbitrary code on local host. Solution described herein is therefore not recommended unless administrator of the remote host is trusted.

Forwarding the socket

Rather than talking abstracting about local and remote hosts, it might be more fun to pick some names for machines in question. Naming things is the most difficult things in it, but let us settle on the local machine being earth.local while the remote one phobos.mars.uac.

To allow emacsclient running on Phobos to communication with Emacs back on Earth, socket created by the edit server needs to be forwarded. Fortunately, analogous to TCP, OpenSSH supports UNIX-domain socket forwarding. To know what to forward, two paths need to be determined: i) location of the UNIX-domain socket which local Emacs is listening on; and ii) path where remote emacsclient will look for a socket. Both typically lay under user’s XDG runtime directory and thus can be determined with a little help from xdg-user-dir tool:

user@earth$ local_socket=$(xdg-user-dir RUNTIME)/emacs/server
user@earth$ remote_socket=$(ssh -aknxSnone phobos.mars.uac \
                          xdg-user-dir RUNTIME)/emacs/earth
user@earth$ test -S "$local_socket" || echo Missing local socket
user@earth$ printf '‘%s’\n' "$local_socket" "$remote_socket"

It is good idea to use a non-standard server name on remote side to avoid clashes if an unrelated Emacs server happened to be running there already. The snippet above uses the default ‘server’ on local side while ‘earth’ on remote side indicating which computer the socket leads to. With the paths determined, the next step is to enable socket forwarding when opening a new SSH session. Options and syntax for UNIX-domain socket forwarding are the same as for TCP port forwarding. In particular the -R switch is used on command line used as follows:

user@earth$ ssh -R "$remote_socket:$local_socket" phobos.mars.uac

Once authenticated, SSH will create $remote_socket on Phobos and forward all incoming connections to $local_socket on Earth. With that, remote emacsclient will be able to talk to local Emacs through that socket. This can be tested by executing the following inside of the SSH session:

user@phobos$ emacsclient -s earth -e '(system-name)'
user@phobos$ emacsclient -s earth -T /ssh:phobos.mars.uac: filename

Thanks to TRAMP (which enables transparent access to remote files from within Emacs), Emacs running on Earth will open filename located on Phobos. If instead it tries to open a local file, there may be issues with TRAMP configuration. Adding (require 'tramp) to Emacs’ initialisation file should fix that issue.

Making it permanent

Explicitly specifying the -R flag each time would be rather tedious. On top of that, things break once SSH session is terminated and new one established. The socket on remote side is never cleaned and as a result a new tunnel cannot be created. Both of those issues can be easily solved with some configuration. To get it sorted, the following section should be added to ~/.ssh/config:

Host phobos
    HostName phobos.mars.uac
    RemoteForward /run/user/1234/emacs/earth /run/user/100/emacs/server
    StreamLocalBindUnlink yes
    ControlMaster auto
    ControlPersist 30m  # or 1

Host defines an alias for the host such that full domain name does not need to by typed each time. Instead, HostName specifies the full domain. RemoteForward cakes care of the -R switch. Paths given in the example may, of course, need to be adjusted. StreamLocalBindUnlink yes addresses the issue of stale sockets blocking new forwarding attempts.

Lastly, ControlMaster and ControlPersist configures multiplexing which makes SSH reuse a single TCP connection rather than creating a new one each time a new session is requested. This avoids superfluous socket forwarding tunnels being created if one is already available. The 30m persist timeout means that control master will keep the connection open for half an hour after last SSH session is closed. If thirty minutes is too long for some reason, the timeout can be brought down to one. I do not recommend disabling it altogether though, because then the first ssh invocation (which establishes the multiplexing connection and became control master) will hang for as long as any SSH sessions are active.

With all that set up, the configuration can be tested with a much more concise set of commands:

user@earth$ ssh -R "$remote_socket:$local_socket" phobos.mars.uac
user@phobos$ emacsclient -s earth -T /ssh:phobos: filename

To make the setup even more convenient, one can create an alias on remote host for the emacsclient invocation. For example by adding the following to ~/.bashrc (or equivalent shell’s runtime configuration file):

alias e='emacsclient -s earth -T /ssh:phobos:'

And that’s it. If you’re satisfied with TRAMP this is the entire setup and there is no need to fiddle with it. However, I found it to be a bit slow on high-latency connections and use SSHFS instead. I’ve described this alternative approach in another article.