diff options
Diffstat (limited to 'src/ssh-generator')
-rw-r--r-- | src/ssh-generator/20-systemd-ssh-proxy.conf.in | 18 | ||||
-rw-r--r-- | src/ssh-generator/meson.build | 29 | ||||
-rw-r--r-- | src/ssh-generator/ssh-generator.c | 495 | ||||
-rw-r--r-- | src/ssh-generator/ssh-proxy.c | 102 |
4 files changed, 644 insertions, 0 deletions
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 0000000..b97e0f5 --- /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 new file mode 100644 index 0000000..d5413f7 --- /dev/null +++ b/src/ssh-generator/meson.build @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +executables += [ + generator_template + { + 'name' : 'systemd-ssh-generator', + 'sources' : files('ssh-generator.c'), + }, + libexec_template + { + 'name' : 'systemd-ssh-proxy', + 'sources' : files('ssh-proxy.c'), + }, +] + +if conf.get('ENABLE_SSH_PROXY_CONFIG') == 1 + 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 : sshconfdir.startswith('/usr/') ? sshconfdir : libexecdir / 'ssh_config.d') + + if not sshconfdir.startswith('/usr/') + 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')) + endif +endif diff --git a/src/ssh-generator/ssh-generator.c b/src/ssh-generator/ssh-generator.c new file mode 100644 index 0000000..c671b41 --- /dev/null +++ b/src/ssh-generator/ssh-generator.c @@ -0,0 +1,495 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <fcntl.h> +#include <sys/ioctl.h> +#include <unistd.h> + +#include "creds-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "generator.h" +#include "install.h" +#include "missing_socket.h" +#include "parse-util.h" +#include "path-util.h" +#include "proc-cmdline.h" +#include "socket-netlink.h" +#include "socket-util.h" +#include "special.h" +#include "virt.h" + +/* A small generator binding potentially five or more SSH sockets: + * + * 1. Listen on AF_VSOCK port 22 if we run in a VM with AF_VSOCK enabled + * 2. Listen on AF_UNIX socket /run/host/unix-export/ssh if we run in a container with /run/host/ support + * 3. Listen on AF_UNIX socket /run/ssh-unix-local/socket (always) + * 4. Listen on any socket specified via kernel command line option systemd.ssh_listen= + * 5. Similar, but from system credential ssh.listen + * + * The first two provide a nice way for hosts to connect to containers and VMs they invoke via the usual SSH + * logic, but without waiting for networking or suchlike. The third allows the same for local clients. */ + +static const char *arg_dest = NULL; +static bool arg_auto = true; +static char **arg_listen_extra = NULL; + +static int parse_proc_cmdline_item(const char *key, const char *value, void *data) { + int r; + + assert(key); + + if (proc_cmdline_key_streq(key, "systemd.ssh_auto")) { + r = value ? parse_boolean(value) : 1; + if (r < 0) + log_warning_errno(r, "Failed to parse systemd.ssh_auto switch \"%s\", ignoring: %m", value); + else + arg_auto = r; + + } else if (proc_cmdline_key_streq(key, "systemd.ssh_listen")) { + + if (proc_cmdline_value_missing(key, value)) + return 0; + + SocketAddress sa; + r = socket_address_parse(&sa, value); + if (r < 0) + log_warning_errno(r, "Failed to parse systemd.ssh_listen= expression, ignoring: %s", value); + else { + _cleanup_free_ char *s = NULL; + r = socket_address_print(&sa, &s); + if (r < 0) + return log_error_errno(r, "Failed to format socket address: %m"); + + if (strv_consume(&arg_listen_extra, TAKE_PTR(s)) < 0) + return log_oom(); + } + } + + return 0; +} + +static int make_sshd_template_unit( + const char *dest, + const char *template, + const char *sshd_binary, + const char *found_sshd_template_service, + char **generated_sshd_template_unit) { + + int r; + + assert(dest); + assert(template); + assert(sshd_binary); + assert(generated_sshd_template_unit); + + /* If the system has a suitable template already, symlink it to the name we want to reuse it */ + if (found_sshd_template_service) + return generator_add_symlink( + dest, + template, + /* dep_type= */ NULL, + found_sshd_template_service); + + if (!*generated_sshd_template_unit) { + _cleanup_fclose_ FILE *f = NULL; + + r = generator_open_unit_file_full( + dest, + /* source= */ NULL, + "sshd-generated@.service", /* Give this generated unit a generic name, since we want to use it for both AF_UNIX and AF_VSOCK */ + &f, + generated_sshd_template_unit, + /* ret_temp_path= */ NULL); + if (r < 0) + return r; + + fprintf(f, + "[Unit]\n" + "Description=OpenSSH Per-Connection Server Daemon\n" + "Documentation=man:systemd-ssh-generator(8) man:sshd(8)\n" + "[Service]\n" + "ExecStart=-%s -i -o \"AuthorizedKeysFile ${CREDENTIALS_DIRECTORY}/ssh.ephemeral-authorized_keys-all .ssh/authorized_keys\"\n" + "StandardInput=socket\n" + "ImportCredential=ssh.ephemeral-authorized_keys-all", + sshd_binary); + + r = fflush_and_check(f); + if (r < 0) + return log_error_errno(r, "Failed to write sshd template: %m"); + } + + return generator_add_symlink( + dest, + template, + /* dep_type= */ NULL, + *generated_sshd_template_unit); +} + +static int write_socket_unit( + const char *dest, + const char *unit, + const char *listen_stream, + const char *comment, + bool with_ssh_access_target_dependency) { + + int r; + + assert(dest); + assert(unit); + assert(listen_stream); + assert(comment); + + _cleanup_fclose_ FILE *f = NULL; + r = generator_open_unit_file( + dest, + /* source= */ NULL, + unit, + &f); + if (r < 0) + return r; + + fprintf(f, + "[Unit]\n" + "Description=OpenSSH Server Socket (systemd-ssh-generator, %s)\n" + "Documentation=man:systemd-ssh-generator(8)\n", + comment); + + /* When this is a remotely accessible socket let's mark this with a milestone: ssh-access.target */ + if (with_ssh_access_target_dependency) + fputs("Wants=ssh-access.target\n" + "Before=ssh-access.target\n", + f); + + fprintf(f, + "\n[Socket]\n" + "ListenStream=%s\n" + "Accept=yes\n" + "PollLimitIntervalSec=30s\n" + "PollLimitBurst=50\n", + listen_stream); + + r = fflush_and_check(f); + if (r < 0) + return log_error_errno(r, "Failed to write %s SSH socket unit: %m", comment); + + r = generator_add_symlink( + dest, + SPECIAL_SOCKETS_TARGET, + "wants", + unit); + if (r < 0) + return r; + + return 0; +} + +static int add_vsock_socket( + const char *dest, + const char *sshd_binary, + const char *found_sshd_template_unit, + char **generated_sshd_template_unit) { + + int r; + + assert(dest); + assert(generated_sshd_template_unit); + + Virtualization v = detect_virtualization(); + if (v < 0) + return log_error_errno(v, "Failed to detect if we run in a VM: %m"); + if (!VIRTUALIZATION_IS_VM(v)) { + /* NB: if we are running in a container inside a VM, then we'll *not* do AF_VSOCK stuff */ + log_debug("Not running in a VM, not listening on AF_VSOCK."); + return 0; + } + + _cleanup_close_ int vsock_fd = socket(AF_VSOCK, SOCK_STREAM|SOCK_CLOEXEC, 0); + if (vsock_fd < 0) { + if (ERRNO_IS_NOT_SUPPORTED(errno)) { + log_debug("Not creating AF_VSOCK ssh listener, since AF_VSOCK is not available."); + return 0; + } + + return log_error_errno(errno, "Unable to test if AF_VSOCK is available: %m"); + } + + vsock_fd = safe_close(vsock_fd); + + /* Determine the local CID so that we can log it to help users to connect to this VM */ + unsigned local_cid; + r = vsock_get_local_cid(&local_cid); + if (r < 0) { + if (ERRNO_IS_DEVICE_ABSENT(r)) { + log_debug("Not creating AF_VSOCK ssh listener, since /dev/vsock is not available (even though AF_VSOCK is)."); + return 0; + } + + return log_error_errno(r, "Failed to query local AF_VSOCK CID: %m"); + } + + r = make_sshd_template_unit( + dest, + "sshd-vsock@.service", + sshd_binary, + found_sshd_template_unit, + generated_sshd_template_unit); + if (r < 0) + return r; + + r = write_socket_unit( + dest, + "sshd-vsock.socket", + "vsock::22", + "AF_VSOCK", + /* with_ssh_access_target_dependency= */ true); + if (r < 0) + return r; + + log_info("Binding SSH to AF_VSOCK vsock::22.\n" + "→ connect via 'ssh vsock/%u' from host", local_cid); + return 0; +} + +static int add_local_unix_socket( + const char *dest, + const char *sshd_binary, + const char *found_sshd_template_unit, + char **generated_sshd_template_unit) { + + int r; + + assert(dest); + assert(sshd_binary); + assert(generated_sshd_template_unit); + + r = make_sshd_template_unit( + dest, + "sshd-unix-local@.service", + sshd_binary, + found_sshd_template_unit, + generated_sshd_template_unit); + if (r < 0) + return r; + + r = write_socket_unit( + dest, + "sshd-unix-local.socket", + "/run/ssh-unix-local/socket", + "AF_UNIX Local", + /* with_ssh_access_target_dependency= */ false); + if (r < 0) + return r; + + + log_info("Binding SSH to AF_UNIX socket /run/ssh-unix-local/socket.\n" + "→ connect via 'ssh .host' locally"); + return 0; +} + +static int add_export_unix_socket( + const char *dest, + const char *sshd_binary, + const char *found_sshd_template_unit, + char **generated_sshd_template_unit) { + + int r; + + assert(dest); + assert(sshd_binary); + assert(generated_sshd_template_unit); + + Virtualization v = detect_container(); + if (v < 0) + return log_error_errno(v, "Failed to detect if we run in a container: %m"); + if (v == VIRTUALIZATION_NONE) { + log_debug("Not running in container, not listening on /run/host/unix-export/ssh"); + return 0; + } + + if (access("/run/host/unix-export/", W_OK) < 0) { + if (errno == ENOENT) { + log_debug("Container manager does not provide /run/host/unix-export/ mount, not binding AF_UNIX socket there."); + return 0; + } + if (errno == EROFS || ERRNO_IS_PRIVILEGE(errno)) { + log_debug("Container manager does not provide write access to /run/host/unix-export/, not binding AF_UNIX socket there."); + return 0; + } + + return log_error_errno(errno, "Unable to check if /run/host/unix-export exists: %m"); + } + + r = make_sshd_template_unit( + dest, + "sshd-unix-export@.service", + sshd_binary, + found_sshd_template_unit, + generated_sshd_template_unit); + if (r < 0) + return r; + + r = write_socket_unit( + dest, + "sshd-unix-export.socket", + "/run/host/unix-export/ssh", + "AF_UNIX Export", + /* with_ssh_access_target_dependency= */ true); + if (r < 0) + return r; + + log_info("Binding SSH to AF_UNIX socket /run/host/unix-export/ssh\n" + "→ connect via 'ssh unix/run/systemd/nspawn/unix-export/\?\?\?/ssh' from host"); + + return 0; +} + +static int add_extra_sockets( + const char *dest, + const char *sshd_binary, + const char *found_sshd_template_unit, + char **generated_sshd_template_unit) { + + unsigned n = 1; + int r; + + assert(dest); + assert(sshd_binary); + assert(generated_sshd_template_unit); + + if (strv_isempty(arg_listen_extra)) + return 0; + + STRV_FOREACH(i, arg_listen_extra) { + _cleanup_free_ char *service = NULL, *socket = NULL; + + if (n > 1) { + if (asprintf(&service, "sshd-extra-%u@.service", n) < 0) + return log_oom(); + + if (asprintf(&socket, "sshd-extra-%u.socket", n) < 0) + return log_oom(); + } + + r = make_sshd_template_unit( + dest, + service ?: "sshd-extra@.service", + sshd_binary, + found_sshd_template_unit, + generated_sshd_template_unit); + if (r < 0) + return r; + + r = write_socket_unit( + dest, + socket ?: "sshd-extra.socket", + *i, + *i, + /* with_ssh_access_target_dependency= */ true); + if (r < 0) + return r; + + log_info("Binding SSH to socket %s.", *i); + n++; + } + + return 0; +} + +static int parse_credentials(void) { + _cleanup_free_ char *b = NULL; + size_t sz = 0; + int r; + + r = read_credential_with_decryption("ssh.listen", (void**) &b, &sz); + if (r <= 0) + return r; + + _cleanup_fclose_ FILE *f = NULL; + f = fmemopen_unlocked(b, sz, "r"); + if (!f) + return log_oom(); + + for (;;) { + _cleanup_free_ char *item = NULL; + + r = read_stripped_line(f, LINE_MAX, &item); + if (r == 0) + break; + if (r < 0) { + log_error_errno(r, "Failed to parse credential 'ssh.listen': %m"); + break; + } + + if (startswith(item, "#")) + continue; + + SocketAddress sa; + r = socket_address_parse(&sa, item); + if (r < 0) { + log_warning_errno(r, "Failed to parse systemd.ssh_listen= expression, ignoring: %s", item); + continue; + } + + _cleanup_free_ char *s = NULL; + r = socket_address_print(&sa, &s); + if (r < 0) + return log_error_errno(r, "Failed to format socket address: %m"); + + if (strv_consume(&arg_listen_extra, TAKE_PTR(s)) < 0) + return log_oom(); + } + + return 0; +} + +static int run(const char *dest, const char *dest_early, const char *dest_late) { + int r; + + assert_se(arg_dest = dest); + + r = proc_cmdline_parse(parse_proc_cmdline_item, /* userdata= */ NULL, /* flags= */ 0); + if (r < 0) + log_warning_errno(r, "Failed to parse kernel command line, ignoring: %m"); + + (void) parse_credentials(); + + strv_sort(arg_listen_extra); + strv_uniq(arg_listen_extra); + + if (!arg_auto && strv_isempty(arg_listen_extra)) { + log_debug("Disabling SSH generator logic, because as it has been turned off explicitly."); + return 0; + } + + _cleanup_free_ char *sshd_binary = NULL; + r = find_executable("sshd", &sshd_binary); + if (r == -ENOENT) { + log_info("Disabling SSH generator logic, since sshd is not installed."); + return 0; + } + if (r < 0) + return log_error_errno(r, "Failed to determine if sshd is installed: %m"); + + _cleanup_(lookup_paths_done) LookupPaths lp = {}; + r = lookup_paths_init_or_warn(&lp, RUNTIME_SCOPE_SYSTEM, LOOKUP_PATHS_EXCLUDE_GENERATED, /* root_dir= */ NULL); + if (r < 0) + return r; + + _cleanup_free_ char *found_sshd_template_unit = NULL; + r = unit_file_exists_full(RUNTIME_SCOPE_SYSTEM, &lp, "sshd@.service", &found_sshd_template_unit); + if (r < 0) + return log_error_errno(r, "Unable to detect if sshd@.service exists: %m"); + + _cleanup_free_ char *generated_sshd_template_unit = NULL; + RET_GATHER(r, add_extra_sockets(dest, sshd_binary, found_sshd_template_unit, &generated_sshd_template_unit)); + + if (arg_auto) { + RET_GATHER(r, add_vsock_socket(dest, sshd_binary, found_sshd_template_unit, &generated_sshd_template_unit)); + RET_GATHER(r, add_local_unix_socket(dest, sshd_binary, found_sshd_template_unit, &generated_sshd_template_unit)); + RET_GATHER(r, add_export_unix_socket(dest, sshd_binary, found_sshd_template_unit, &generated_sshd_template_unit)); + } + + return r; +} + +DEFINE_MAIN_GENERATOR_FUNCTION(run); diff --git a/src/ssh-generator/ssh-proxy.c b/src/ssh-generator/ssh-proxy.c new file mode 100644 index 0000000..4884c93 --- /dev/null +++ b/src/ssh-generator/ssh-proxy.c @@ -0,0 +1,102 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if_arp.h> +#include <stdio.h> +#include <unistd.h> + +#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); |