From 0abd510f7f628d0369f0814b671302e93c62b161 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jan 2024 23:31:51 +0100 Subject: [PATCH] ssh-proxy: add ssh ProxyCommand tool that can connect to AF_UNIX + AF_VSOCK sockets This adds a tiny binary that is hooked into SSH client config via ProxyCommand and which simply connects to an AF_UNIX or AF_VSOCK socket of choice. The syntax is as simple as this: ssh unix/some/path # (this connects to AF_UNIX socket /some/path) or: ssh vsock/4711 I used "/" as separator of the protocol ID and the value since ":" is already taken by SSH itself when doing sftp. And "@" is already taken for separating the user name. --- man/rules/meson.build | 1 + man/systemd-ssh-proxy.xml | 116 ++++++++++++++++++ meson.build | 9 +- meson_options.txt | 2 + .../20-systemd-ssh-proxy.conf.in | 18 +++ src/ssh-generator/meson.build | 17 +++ src/ssh-generator/ssh-proxy.c | 102 +++++++++++++++ tmpfiles.d/20-systemd-ssh-generator.conf.in | 10 ++ tmpfiles.d/meson.build | 1 + 9 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 man/systemd-ssh-proxy.xml create mode 100644 src/ssh-generator/20-systemd-ssh-proxy.conf.in create mode 100644 src/ssh-generator/ssh-proxy.c create mode 100644 tmpfiles.d/20-systemd-ssh-generator.conf.in diff --git a/man/rules/meson.build b/man/rules/meson.build index 84555df9057..c43bffde693 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1055,6 +1055,7 @@ manpages = [ ['systemd-socket-proxyd', '8', [], ''], ['systemd-soft-reboot.service', '8', [], ''], ['systemd-ssh-generator', '8', [], ''], + ['systemd-ssh-proxy', '1', [], ''], ['systemd-stdio-bridge', '1', [], ''], ['systemd-storagetm.service', '8', ['systemd-storagetm'], 'ENABLE_STORAGETM'], ['systemd-stub', diff --git a/man/systemd-ssh-proxy.xml b/man/systemd-ssh-proxy.xml new file mode 100644 index 00000000000..d9615ff62ca --- /dev/null +++ b/man/systemd-ssh-proxy.xml @@ -0,0 +1,116 @@ + + + + + + + + systemd-ssh-proxy + systemd + + + + systemd-ssh-proxy + 1 + + + + systemd-ssh-proxy + SSH client plugin for connecting to AF_VSOCK and + AF_UNIX sockets + + + + +Host unix/* vsock/* + ProxyCommand /usr/lib/systemd/systemd-ssh-proxy %h %p + ProxyUseFdpass yes + + + /usr/lib/systemd/systemd-ssh-proxy ADDRESS PORT + + + + + + Description + + systemd-ssh-proxy is a small "proxy" plugin for the ssh1 + tool that allows connecting to AF_UNIX and AF_VSOCK sockets. It + implements the interface defined by ssh's ProxyCommand + configuration option. It's supposed to be used with an ssh_config5 + configuration fragment like the following: + + +Host unix/* vsock/* + ProxyCommand /usr/lib/systemd/systemd-ssh-proxy %h %p + ProxyUseFdpass yes + CheckHostIP no + +Host .host + ProxyCommand /usr/lib/systemd/systemd-ssh-proxy unix/run/ssh-unix-local/socket %p + ProxyUseFdpass yes + CheckHostIP no + + + A configuration fragment along these lines is by default installed into + /etc/ssh/ssh_config.d/20-systemd-ssh-proxy.conf.in. + + With this in place, SSH connections to host string unix/ followed by an absolute + AF_UNIX file system path to a socket will be directed to the specified socket, which + must be of type SOCK_STREAM. Similar, SSH connections to vsock/ + followed by an AF_VSOCK CID will result in an SSH connection made to that + CID. Moreover connecting to .host will connect to the local host via SSH, without + involving networking. + + This tool is supposed to be used together with + systemd-ssh-generator8 + which when run inside a VM or container will bind SSH to suitable + addresses. systemd-ssh-generator is supposed to run in the container of VM guest, and + systemd-ssh-proxy is run on the host, in order to connect to the container or VM + guest. + + + + Exit status + + On success, 0 is returned, a non-zero failure code + otherwise. + + + + Examples + + + Talk to a local VM with CID 4711 + + ssh vsock/4711 + + + + Talk to the local host via ssh + + ssh .host + + or equivalent: + + ssh unix/run/ssh-unix-local/socket + + + + + See Also + + systemd1 + systemd-ssh-generator8 + vsock7 + unix7 + ssh1 + sshd8 + + + diff --git a/meson.build b/meson.build index 9074f14069f..53a3d966ff4 100644 --- a/meson.build +++ b/meson.build @@ -199,6 +199,11 @@ if pamconfdir == '' pamconfdir = prefixdir / 'lib/pam.d' endif +sshconfdir = get_option('sshconfdir') +if sshconfdir == '' + sshconfdir = sysconfdir / 'ssh/ssh_config.d' +endif + sshdconfdir = get_option('sshdconfdir') if sshdconfdir == '' sshdconfdir = sysconfdir / 'ssh/sshd_config.d' @@ -235,6 +240,7 @@ conf.set_quoted('PREFIX_NOSLASH', prefixdir_noslash) conf.set_quoted('RANDOM_SEED', randomseeddir / 'random-seed') conf.set_quoted('RANDOM_SEED_DIR', randomseeddir) conf.set_quoted('RC_LOCAL_PATH', get_option('rc-local')) +conf.set_quoted('SSHCONFDIR', sshconfdir) conf.set_quoted('SSHDCONFDIR', sshdconfdir) conf.set_quoted('SYSCONF_DIR', sysconfdir) conf.set_quoted('SYSCTL_DIR', sysctldir) @@ -2689,7 +2695,8 @@ summary({ 'SysV rc?.d directories' : sysvrcnd_path, 'PAM modules directory' : pamlibdir, 'PAM configuration directory' : pamconfdir, - 'ssh configuration directory' : sshdconfdir, + 'ssh server configuration directory' : sshdconfdir, + 'ssh client configuration directory' : sshconfdir, 'libcryptsetup plugins directory' : libcryptsetup_plugins_dir, 'RPM macros directory' : rpmmacrosdir, 'modprobe.d directory' : modprobedir, diff --git a/meson_options.txt b/meson_options.txt index c677c7f4202..b74f9491896 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -211,6 +211,8 @@ option('pamlibdir', type : 'string', description : 'directory for PAM modules') option('pamconfdir', type : 'string', description : 'directory for PAM configuration ["no" disables]') +option('sshconfdir', type : 'string', + description : 'directory for SSH client configuration ["no" disables]') option('sshdconfdir', type : 'string', description : 'directory for SSH server configuration ["no" disables]') option('libcryptsetup-plugins-dir', type : 'string', diff --git a/src/ssh-generator/20-systemd-ssh-proxy.conf.in b/src/ssh-generator/20-systemd-ssh-proxy.conf.in new file mode 100644 index 00000000000..b97e0f5340b --- /dev/null +++ b/src/ssh-generator/20-systemd-ssh-proxy.conf.in @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Make sure unix/* and vsock/* can be used to connect to AF_UNIX and AF_VSOCK paths +# +Host unix/* vsock/* + ProxyCommand {{LIBEXECDIR}}/systemd-ssh-proxy %h %p + ProxyUseFdpass yes + CheckHostIP no + + # Disable all kinds of host identity checks, since these addresses are generally ephemeral. + StrictHostKeyChecking no + UserKnownHostsFile /dev/null + +# Allow connecting to the local host directly via ".host" +Host .host + ProxyCommand {{LIBEXECDIR}}/systemd-ssh-proxy unix/run/ssh-unix-local/socket %p + ProxyUseFdpass yes + CheckHostIP no diff --git a/src/ssh-generator/meson.build b/src/ssh-generator/meson.build index d21a7a36d95..70a706f2aa8 100644 --- a/src/ssh-generator/meson.build +++ b/src/ssh-generator/meson.build @@ -5,4 +5,21 @@ executables += [ 'name' : 'systemd-ssh-generator', 'sources' : files('ssh-generator.c'), }, + libexec_template + { + 'name' : 'systemd-ssh-proxy', + 'sources' : files('ssh-proxy.c'), + }, ] + +custom_target( + '20-systemd-ssh-proxy.conf', + input : '20-systemd-ssh-proxy.conf.in', + output : '20-systemd-ssh-proxy.conf', + command : [jinja2_cmdline, '@INPUT@', '@OUTPUT@'], + install : true, + install_dir : libexecdir / 'ssh_config.d') + +install_emptydir(sshconfdir) + +meson.add_install_script(sh, '-c', + ln_s.format(libexecdir / 'ssh_config.d' / '20-systemd-ssh-proxy.conf', sshconfdir / '20-systemd-ssh-proxy.conf')) diff --git a/src/ssh-generator/ssh-proxy.c b/src/ssh-generator/ssh-proxy.c new file mode 100644 index 00000000000..4884c934d77 --- /dev/null +++ b/src/ssh-generator/ssh-proxy.c @@ -0,0 +1,102 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include + +#include "fd-util.h" +#include "iovec-util.h" +#include "log.h" +#include "main-func.h" +#include "missing_socket.h" +#include "parse-util.h" +#include "socket-util.h" +#include "string-util.h" +#include "strv.h" + +static int process_vsock(const char *host, const char *port) { + int r; + + assert(host); + assert(port); + + union sockaddr_union sa = { + .vm.svm_family = AF_VSOCK, + }; + + r = vsock_parse_cid(host, &sa.vm.svm_cid); + if (r < 0) + return log_error_errno(r, "Failed to parse vsock cid: %s", host); + + r = vsock_parse_port(port, &sa.vm.svm_port); + if (r < 0) + return log_error_errno(r, "Failed to parse vsock port: %s", port); + + _cleanup_close_ int fd = socket(AF_VSOCK, SOCK_STREAM|SOCK_CLOEXEC, 0); + if (fd < 0) + return log_error_errno(errno, "Failed to allocate AF_VSOCK socket: %m"); + + if (connect(fd, &sa.sa, SOCKADDR_LEN(sa)) < 0) + return log_error_errno(errno, "Failed to connect to vsock:%u:%u: %m", sa.vm.svm_cid, sa.vm.svm_port); + + /* OpenSSH wants us to send a single byte along with the file descriptor, hence do so */ + r = send_one_fd_iov(STDOUT_FILENO, fd, &IOVEC_NUL_BYTE, /* n_iovec= */ 1, /* flags= */ 0); + if (r < 0) + return log_error_errno(r, "Failed to send socket via STDOUT: %m"); + + log_debug("Successfully sent AF_VSOCK socket via STDOUT."); + return 0; +} + +static int process_unix(const char *path) { + int r; + + assert(path); + + /* We assume the path is absolute unless it starts with a dot (or is already explicitly absolute) */ + _cleanup_free_ char *prefixed = NULL; + if (!STARTSWITH_SET(path, "/", "./")) { + prefixed = strjoin("/", path); + if (!prefixed) + return log_oom(); + + path = prefixed; + } + + _cleanup_close_ int fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0); + if (fd < 0) + return log_error_errno(errno, "Failed to allocate AF_UNIX socket: %m"); + + r = connect_unix_path(fd, AT_FDCWD, path); + if (r < 0) + return log_error_errno(r, "Failed to connect to AF_UNIX socket %s: %m", path); + + r = send_one_fd_iov(STDOUT_FILENO, fd, &IOVEC_NUL_BYTE, /* n_iovec= */ 1, /* flags= */ 0); + if (r < 0) + return log_error_errno(r, "Failed to send socket via STDOUT: %m"); + + log_debug("Successfully sent AF_UNIX socket via STDOUT."); + return 0; +} + +static int run(int argc, char* argv[]) { + + log_setup(); + + if (argc != 3) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected two arguments: host and port."); + + const char *host = argv[1], *port = argv[2]; + + const char *p = startswith(host, "vsock/"); + if (p) + return process_vsock(p, port); + + p = startswith(host, "unix/"); + if (p) + return process_unix(p); + + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Don't know how to parse host name specification: %s", host); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/tmpfiles.d/20-systemd-ssh-generator.conf.in b/tmpfiles.d/20-systemd-ssh-generator.conf.in new file mode 100644 index 00000000000..033379ec7a4 --- /dev/null +++ b/tmpfiles.d/20-systemd-ssh-generator.conf.in @@ -0,0 +1,10 @@ +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +# See tmpfiles.d(5) for details + +L {{SSHCONFDIR}}/20-systemd-ssh-proxy.conf - - - - {{LIBEXECDIR}}/ssh_config.d/20-systemd-ssh-proxy.conf diff --git a/tmpfiles.d/meson.build b/tmpfiles.d/meson.build index 390076b6d50..d05ea94c160 100644 --- a/tmpfiles.d/meson.build +++ b/tmpfiles.d/meson.build @@ -35,6 +35,7 @@ in_files = [['etc.conf', ''], ['systemd.conf', ''], ['var.conf', ''], ['20-systemd-userdb.conf', 'ENABLE_USERDB'], + ['20-systemd-ssh-generator.conf', ''], ] foreach pair : in_files -- 2.47.3