varlink: add "ssh:" transport

This uses openssh 9.4's -W support for AF_UNIX. Unfortunately older versions
don't work with this, and I couldn#t figure a way that would work for
older versions too, would not be racy and where we'd still could keep
track of the forked off ssh process.

Unfortunately, on older versions -W will just hang (because it tries to
resolve the AF_UNIX path as regular host name), which sucks, but hopefully this
issue will go away sooner or later on its own, as distributions update.

Fedora is still stuck at 9.3 at the time of posting this (even on
Fedora), even though 9.4, 9.5, 9.6 have all already been released by
now.

Example:
        varlinkctl call -j ssh:root@somehost:/run/systemd/io.systemd.Credentials io.systemd.Credentials.Encrypt '{"text":"foobar"}'
This commit is contained in:
Lennart Poettering 2024-01-08 22:26:17 +01:00
parent 07dca3c4b0
commit a1bb30de7f
5 changed files with 136 additions and 17 deletions

View file

@ -610,3 +610,8 @@ SYSTEMD_HOME_DEBUG_SUFFIX=foo \
latter two via the environment variable unless `systemd-storagetm` is invoked
to expose a single device only, since those identifiers better should be kept
unique.
Tools using the Varlink protocol, such as `varlinkctl`:
* `$SYSTEMD_SSH` the ssh binary to invoke when the `ssh:` transport is
used. May be a filename (which is searched for in `$PATH`) or absolute path.

View file

@ -71,11 +71,16 @@
<itemizedlist>
<listitem><para>A Varlink service reference starting with the <literal>unix:</literal> string, followed
by an absolute <constant>AF_UNIX</constant> path, or by <literal>@</literal> and an arbitrary string
by an absolute <constant>AF_UNIX</constant> socket path, or by <literal>@</literal> and an arbitrary string
(the latter for referencing sockets in the abstract namespace).</para></listitem>
<listitem><para>A Varlink service reference starting with the <literal>exec:</literal> string, followed
by an absolute path of a binary to execute.</para></listitem>
<listitem><para>A Varlink service reference starting with the <literal>ssh:</literal> string, followed
by an SSH host specification, followed by <literal>:</literal>, followed by an absolute
<constant>AF_UNIX</constant> socket path. (This requires OpenSSH 9.4 or newer on the server side,
abstract namespace sockets are not supported.)</para></listitem>
</itemizedlist>
<para>For convenience these two simpler (redundant) service address syntaxes are also supported:</para>

View file

@ -511,39 +511,120 @@ int varlink_connect_exec(Varlink **ret, const char *_command, char **_argv) {
return 0;
}
static int varlink_connect_ssh(Varlink **ret, const char *where) {
_cleanup_close_pair_ int pair[2] = EBADF_PAIR;
_cleanup_(sigkill_waitp) pid_t pid = 0;
int r;
assert_return(ret, -EINVAL);
assert_return(where, -EINVAL);
/* Connects to an SSH server via OpenSSH 9.4's -W switch to connect to a remote AF_UNIX socket. For
* now we do not expose this function directly, but only via varlink_connect_url(). */
const char *ssh = secure_getenv("SYSTEMD_SSH") ?: "ssh";
if (!path_is_valid(ssh))
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "SSH path is not valid, refusing: %s", ssh);
const char *e = strchr(where, ':');
if (!e)
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "SSH specification lacks a : separator between host and path, refusing: %s", where);
_cleanup_free_ char *h = strndup(where, e - where);
if (!h)
return log_oom_debug();
_cleanup_free_ char *c = strdup(e + 1);
if (!c)
return log_oom_debug();
if (!path_is_absolute(c))
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Remote AF_UNIX socket path is not absolute, refusing: %s", c);
_cleanup_free_ char *p = NULL;
r = path_simplify_alloc(c, &p);
if (r < 0)
return r;
if (!path_is_normalized(p))
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Specified path is not normalized, refusing: %s", p);
log_debug("Forking off SSH child process '%s -W %s %s'.", ssh, p, h);
if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0, pair) < 0)
return log_debug_errno(errno, "Failed to allocate AF_UNIX socket pair: %m");
r = safe_fork_full(
"(sd-vlssh)",
/* stdio_fds= */ (int[]) { pair[1], pair[1], STDERR_FILENO },
/* except_fds= */ NULL,
/* n_except_fds= */ 0,
FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_REOPEN_LOG|FORK_LOG|FORK_RLIMIT_NOFILE_SAFE|FORK_REARRANGE_STDIO,
&pid);
if (r < 0)
return log_debug_errno(r, "Failed to spawn process: %m");
if (r == 0) {
/* Child */
execlp(ssh, "ssh", "-W", p, h, NULL);
log_debug_errno(errno, "Failed to invoke %s: %m", ssh);
_exit(EXIT_FAILURE);
}
pair[1] = safe_close(pair[1]);
Varlink *v;
r = varlink_new(&v);
if (r < 0)
return log_debug_errno(r, "Failed to create varlink object: %m");
v->fd = TAKE_FD(pair[0]);
v->af = AF_UNIX;
v->exec_pid = TAKE_PID(pid);
varlink_set_state(v, VARLINK_IDLE_CLIENT);
*ret = v;
return 0;
}
int varlink_connect_url(Varlink **ret, const char *url) {
_cleanup_free_ char *c = NULL;
const char *p;
bool exec;
enum {
SCHEME_UNIX,
SCHEME_EXEC,
SCHEME_SSH,
} scheme;
int r;
assert_return(ret, -EINVAL);
assert_return(url, -EINVAL);
// FIXME: Add support for vsock:, ssh-exec:, ssh-unix: URL schemes here. (The latter with OpenSSH
// 9.4's -W switch for referencing remote AF_UNIX sockets.)
// FIXME: Maybe add support for vsock: and ssh-exec: URL schemes here.
/* The Varlink URL scheme is a bit underdefined. We support only the unix: transport for now, plus an
* exec: transport we made up ourselves. Strictly speaking this shouldn't even be called URL, since
* it has nothing to do with Internet URLs by RFC. */
/* The Varlink URL scheme is a bit underdefined. We support only the spec-defined unix: transport for
* now, plus exec:, ssh: transports we made up ourselves. Strictly speaking this shouldn't even be
* called "URL", since it has nothing to do with Internet URLs by RFC. */
p = startswith(url, "unix:");
if (p)
exec = false;
else {
p = startswith(url, "exec:");
if (!p)
return log_debug_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "URL scheme not supported.");
exec = true;
}
scheme = SCHEME_UNIX;
else if ((p = startswith(url, "exec:")))
scheme = SCHEME_EXEC;
else if ((p = startswith(url, "ssh:")))
scheme = SCHEME_SSH;
else
return log_debug_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "URL scheme not supported.");
/* The varlink.org reference C library supports more than just file system paths. We might want to
* support that one day too. For now simply refuse that. */
if (p[strcspn(p, ";?#")] != '\0')
return log_debug_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "URL parameterization with ';', '?', '#' not supported.");
if (exec || p[0] != '@') { /* no validity checks for abstract namespace */
if (scheme == SCHEME_SSH)
return varlink_connect_ssh(ret, p);
if (scheme == SCHEME_EXEC || p[0] != '@') { /* no path validity checks for abstract namespace sockets */
if (!path_is_absolute(p))
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Specified path not absolute, refusing.");
@ -556,7 +637,7 @@ int varlink_connect_url(Varlink **ret, const char *url) {
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Specified path is not normalized, refusing.");
}
if (exec)
if (scheme == SCHEME_EXEC)
return varlink_connect_exec(ret, c, NULL);
return varlink_connect_address(ret, c ?: p);

View file

@ -25,6 +25,8 @@ test_append_files() {
install_mdadm
generate_module_dependencies
fi
image_install socat
}
do_test "$@"

View file

@ -53,6 +53,32 @@ if [[ -x /usr/lib/systemd/systemd-pcrextend ]]; then
varlinkctl introspect /usr/lib/systemd/systemd-pcrextend io.systemd.PCRExtend
fi
# SSH transport
SSHBINDIR="$(mktemp -d)"
rm_rf_sshbindir() {
rm -rf "$SSHBINDIR"
}
trap rm_rf_sshbindir EXIT
# Create a fake "ssh" binary that validates everything works as expected
cat > "$SSHBINDIR"/ssh <<'EOF'
#!/bin/sh
set -xe
test "$1" = "-W"
test "$2" = "/run/systemd/journal/io.systemd.journal"
test "$3" = "foobar"
exec socat - UNIX-CONNECT:/run/systemd/journal/io.systemd.journal
EOF
chmod +x "$SSHBINDIR"/ssh
SYSTEMD_SSH="$SSHBINDIR/ssh" varlinkctl info ssh:foobar:/run/systemd/journal/io.systemd.journal
# Go through all varlink sockets we can find under /run/systemd/ for some extra coverage
find /run/systemd/ -name "io.systemd*" -type s | while read -r socket; do
varlinkctl info "$socket"