summaryrefslogtreecommitdiffstats
path: root/src/ssh-generator
diff options
context:
space:
mode:
Diffstat (limited to 'src/ssh-generator')
-rw-r--r--src/ssh-generator/20-systemd-ssh-proxy.conf.in18
-rw-r--r--src/ssh-generator/meson.build29
-rw-r--r--src/ssh-generator/ssh-generator.c495
-rw-r--r--src/ssh-generator/ssh-proxy.c102
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);