/* SPDX-License-Identifier: LGPL-2.1-or-later */ #include #include #include #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);