From 78cdb9b50aa51906f53d45b20bf982117ae5cb54 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 27 Nov 2023 16:26:18 +0100 Subject: [PATCH 01/11] mkosi: also add ssh client, to make it easier to test ssh logins via AF_UNIX/AF_VSOCK --- mkosi.images/system/mkosi.conf.d/10-centos-fedora.conf | 1 + mkosi.images/system/mkosi.conf.d/10-debian-ubuntu.conf | 1 + mkosi.images/system/mkosi.conf.d/10-opensuse.conf | 1 + 3 files changed, 3 insertions(+) diff --git a/mkosi.images/system/mkosi.conf.d/10-centos-fedora.conf b/mkosi.images/system/mkosi.conf.d/10-centos-fedora.conf index 67d46432d40..871186d5ca9 100644 --- a/mkosi.images/system/mkosi.conf.d/10-centos-fedora.conf +++ b/mkosi.images/system/mkosi.conf.d/10-centos-fedora.conf @@ -18,6 +18,7 @@ Packages= libcap-ng-utils netcat openssh-server + openssh-clients p11-kit pam passwd diff --git a/mkosi.images/system/mkosi.conf.d/10-debian-ubuntu.conf b/mkosi.images/system/mkosi.conf.d/10-debian-ubuntu.conf index 588f833c8f4..348bdb2992c 100644 --- a/mkosi.images/system/mkosi.conf.d/10-debian-ubuntu.conf +++ b/mkosi.images/system/mkosi.conf.d/10-debian-ubuntu.conf @@ -18,6 +18,7 @@ Packages= libcap-ng-utils netcat-openbsd openssh-server + openssh-client passwd policykit-1 procps diff --git a/mkosi.images/system/mkosi.conf.d/10-opensuse.conf b/mkosi.images/system/mkosi.conf.d/10-opensuse.conf index 60a2b6dbfc5..71434b4560b 100644 --- a/mkosi.images/system/mkosi.conf.d/10-opensuse.conf +++ b/mkosi.images/system/mkosi.conf.d/10-opensuse.conf @@ -14,6 +14,7 @@ Packages= kernel-kvmsmall libcap-ng-utils openssh-server + openssh-clients python3 python3-pefile python3-psutil From 7ceb76b63c20bb21c422ec297e09e562e10e2219 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jan 2024 18:37:38 +0100 Subject: [PATCH 02/11] generator: optionally return resulting unit file path in generator_open_unit_file_full() This is useful if we want to make symlinks to it later. --- src/network/generator/main.c | 24 +++++++++++++++++++++--- src/shared/generator.c | 5 +++++ src/shared/generator.h | 4 ++-- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/network/generator/main.c b/src/network/generator/main.c index 540b6df4fc5..4a3ccd6a992 100644 --- a/src/network/generator/main.c +++ b/src/network/generator/main.c @@ -28,7 +28,13 @@ static int network_save(Network *network, const char *dest_dir) { assert(network); - r = generator_open_unit_file_full(dest_dir, NULL, NULL, &f, &temp_path); + r = generator_open_unit_file_full( + dest_dir, + /* source= */ NULL, + /* name= */ NULL, + &f, + /* ret_final_path= */ NULL, + &temp_path); if (r < 0) return r; @@ -56,7 +62,13 @@ static int netdev_save(NetDev *netdev, const char *dest_dir) { assert(netdev); - r = generator_open_unit_file_full(dest_dir, NULL, NULL, &f, &temp_path); + r = generator_open_unit_file_full( + dest_dir, + /* source= */ NULL, + /* name= */ NULL, + &f, + /* ret_final_path= */ NULL, + &temp_path); if (r < 0) return r; @@ -81,7 +93,13 @@ static int link_save(Link *link, const char *dest_dir) { assert(link); - r = generator_open_unit_file_full(dest_dir, NULL, NULL, &f, &temp_path); + r = generator_open_unit_file_full( + dest_dir, + /* source= */ NULL, + /* name= */ NULL, + &f, + /* ret_final_path= */ NULL, + &temp_path); if (r < 0) return r; diff --git a/src/shared/generator.c b/src/shared/generator.c index fe58021f000..d183eb9654f 100644 --- a/src/shared/generator.c +++ b/src/shared/generator.c @@ -29,6 +29,7 @@ int generator_open_unit_file_full( const char *source, const char *fn, FILE **ret_file, + char **ret_final_path, char **ret_temp_path) { _cleanup_free_ char *p = NULL; @@ -72,6 +73,10 @@ int generator_open_unit_file_full( program_invocation_short_name); *ret_file = f; + + if (ret_final_path) + *ret_final_path = TAKE_PTR(p); + return 0; } diff --git a/src/shared/generator.h b/src/shared/generator.h index d97d6edc676..c17feafacc2 100644 --- a/src/shared/generator.h +++ b/src/shared/generator.h @@ -6,10 +6,10 @@ #include "macro.h" #include "main-func.h" -int generator_open_unit_file_full(const char *dest, const char *source, const char *name, FILE **ret_file, char **ret_temp_path); +int generator_open_unit_file_full(const char *dest, const char *source, const char *name, FILE **ret_file, char **ret_final_path, char **ret_temp_path); static inline int generator_open_unit_file(const char *dest, const char *source, const char *name, FILE **ret_file) { - return generator_open_unit_file_full(dest, source, name, ret_file, NULL); + return generator_open_unit_file_full(dest, source, name, ret_file, NULL, NULL); } int generator_add_symlink_full(const char *dir, const char *dst, const char *dep_type, const char *src, const char *instance); From 3392079e4b54dca7830050ee852df70ad66036aa Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jan 2024 18:38:28 +0100 Subject: [PATCH 03/11] generator: teach generator_add_symlink_full() to optionally make alias symlinks rather than just .wants/ style symlinks --- src/shared/generator.c | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/shared/generator.c b/src/shared/generator.c index d183eb9654f..b96715c59cc 100644 --- a/src/shared/generator.c +++ b/src/shared/generator.c @@ -80,7 +80,6 @@ int generator_open_unit_file_full( return 0; } - int generator_add_symlink_full( const char *dir, const char *dst, @@ -93,11 +92,13 @@ int generator_add_symlink_full( assert(dir); assert(dst); - assert(dep_type); assert(src); - /* Adds a symlink from ./ to (if src is absolute) or ../ (otherwise). If - * is specified, then must be a template unit name, and we'll instantiate it. */ + /* If 'dep_type' is specified adds a symlink from ./ to (if src is absolute) or ../ (otherwise). + * + * If 'dep_type' is NULL, it will create a symlink to (i.e. create an alias. + * + * If is specified, then must be a template unit name, and we'll instantiate it. */ r = path_extract_directory(src, &dn); if (r < 0 && r != -EDESTADDRREQ) /* EDESTADDRREQ → just a file name was passed */ @@ -115,11 +116,19 @@ int generator_add_symlink_full( return log_error_errno(r, "Failed to instantiate '%s' for '%s': %m", fn, instance); } - from = path_join(dn ?: "..", fn); - if (!from) - return log_oom(); + if (dep_type) { /* Create a .wants/ style dep */ + from = path_join(dn ?: "..", fn); + if (!from) + return log_oom(); - to = strjoin(dir, "/", dst, ".", dep_type, "/", instantiated ?: fn); + to = strjoin(dir, "/", dst, ".", dep_type, "/", instantiated ?: fn); + } else { /* or create an alias */ + from = dn ? path_join(dn, fn) : strdup(fn); + if (!from) + return log_oom(); + + to = strjoin(dir, "/", dst); + } if (!to) return log_oom(); From e09c255d2e7bc5ec3af8271b2bcc08ed6877fbdf Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jan 2024 18:38:50 +0100 Subject: [PATCH 04/11] install: optionally return discovered unit file path in unit_file_exists() --- src/shared/install.c | 31 +++++++++++++++++++++++++++---- src/shared/install.h | 6 +++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/shared/install.c b/src/shared/install.c index ad30e9b49c1..fabf5db7ed2 100644 --- a/src/shared/install.c +++ b/src/shared/install.c @@ -3142,8 +3142,10 @@ int unit_file_get_state( return unit_file_lookup_state(scope, &lp, name, ret); } -int unit_file_exists(RuntimeScope scope, const LookupPaths *lp, const char *name) { - _cleanup_(install_context_done) InstallContext c = { .scope = scope }; +int unit_file_exists_full(RuntimeScope scope, const LookupPaths *lp, const char *name, char **ret_path) { + _cleanup_(install_context_done) InstallContext c = { + .scope = scope, + }; int r; assert(lp); @@ -3152,12 +3154,33 @@ int unit_file_exists(RuntimeScope scope, const LookupPaths *lp, const char *name if (!unit_name_is_valid(name, UNIT_NAME_ANY)) return -EINVAL; - r = install_info_discover(&c, lp, name, 0, NULL, NULL, NULL); - if (r == -ENOENT) + InstallInfo *info = NULL; + r = install_info_discover( + &c, + lp, + name, + /* flags= */ 0, + ret_path ? &info : NULL, + /* changes= */ NULL, + /* n_changes= */ NULL); + if (r == -ENOENT) { + if (ret_path) + *ret_path = NULL; return 0; + } if (r < 0) return r; + if (ret_path) { + assert(info); + + _cleanup_free_ char *p = strdup(info->path); + if (!p) + return -ENOMEM; + + *ret_path = TAKE_PTR(p); + } + return 1; } diff --git a/src/shared/install.h b/src/shared/install.h index bc0c6db828d..3e2ada45f49 100644 --- a/src/shared/install.h +++ b/src/shared/install.h @@ -193,7 +193,11 @@ int unit_file_lookup_state( UnitFileState *ret); int unit_file_get_state(RuntimeScope scope, const char *root_dir, const char *filename, UnitFileState *ret); -int unit_file_exists(RuntimeScope scope, const LookupPaths *paths, const char *name); + +int unit_file_exists_full(RuntimeScope scope, const LookupPaths *paths, const char *name, char **ret_path); +static inline int unit_file_exists(RuntimeScope scope, const LookupPaths *paths, const char *name) { + return unit_file_exists_full(scope, paths, name, NULL); +} int unit_file_get_list(RuntimeScope scope, const char *root_dir, Hashmap *h, char **states, char **patterns); From 045f7b8fe40f3ddd338c3c870885882e0175e99f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 5 Jan 2024 16:32:43 +0100 Subject: [PATCH 05/11] iovec-util: add helper for a single NUL byte iovec --- src/basic/iovec-util.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/basic/iovec-util.h b/src/basic/iovec-util.h index bc7a67054ad..8cfa5717dc4 100644 --- a/src/basic/iovec-util.h +++ b/src/basic/iovec-util.h @@ -8,6 +8,12 @@ #include "alloc-util.h" #include "macro.h" +/* An iovec pointing to a single NUL byte */ +#define IOVEC_NUL_BYTE (const struct iovec) { \ + .iov_base = (void*) (const uint8_t[1]) { 0 }, \ + .iov_len = 1, \ + } + size_t iovec_total_size(const struct iovec *iovec, size_t n); bool iovec_increment(struct iovec *iovec, size_t n, size_t k); From 0e3220684c6184a2f70396d991200ae207a25377 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jan 2024 18:39:03 +0100 Subject: [PATCH 06/11] ssh-generator: add simple new generator --- man/kernel-command-line.xml | 12 + man/rules/meson.build | 1 + man/systemd-ssh-generator.xml | 141 +++++++++ man/systemd.system-credentials.xml | 11 + meson.build | 1 + src/ssh-generator/meson.build | 8 + src/ssh-generator/ssh-generator.c | 476 +++++++++++++++++++++++++++++ 7 files changed, 650 insertions(+) create mode 100644 man/systemd-ssh-generator.xml create mode 100644 src/ssh-generator/meson.build create mode 100644 src/ssh-generator/ssh-generator.c diff --git a/man/kernel-command-line.xml b/man/kernel-command-line.xml index 25ad770dcd0..ded41ffc36c 100644 --- a/man/kernel-command-line.xml +++ b/man/kernel-command-line.xml @@ -138,6 +138,18 @@ + + systemd.ssh_auto= + systemd.ssh_listen= + + These parameters are interpreted by + systemd-ssh-generator8 + and may be used to control SSH sockets the system shall be reachable on. + + + + + systemd.volatile= diff --git a/man/rules/meson.build b/man/rules/meson.build index 3592b862f71..84555df9057 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1054,6 +1054,7 @@ manpages = [ ['systemd-socket-activate', '1', [], ''], ['systemd-socket-proxyd', '8', [], ''], ['systemd-soft-reboot.service', '8', [], ''], + ['systemd-ssh-generator', '8', [], ''], ['systemd-stdio-bridge', '1', [], ''], ['systemd-storagetm.service', '8', ['systemd-storagetm'], 'ENABLE_STORAGETM'], ['systemd-stub', diff --git a/man/systemd-ssh-generator.xml b/man/systemd-ssh-generator.xml new file mode 100644 index 00000000000..04c3263c375 --- /dev/null +++ b/man/systemd-ssh-generator.xml @@ -0,0 +1,141 @@ + + + +%entities; +]> + + + + + systemd-ssh-generator + systemd + + + + systemd-ssh-generator + 8 + + + + systemd-ssh-generator + Generator for binding a socket-activated SSH server to local AV_VSOCK + and AF_UNIX sockets + + + + /usr/lib/systemd/system-generators/systemd-ssh-generator + + + + Description + + systemd-ssh-generator binds a socket-activated SSH server to local + AV_VSOCK and AF_UNIX sockets under certain conditions. It only + has an effect if the sshd8 binary is + installed. Specifically, it does the following: + + + If invoked in a VM with AF_VSOCK support, a socket-activated SSH + per-connection service is bound to AF_VSOCK port 22. + + If invoked in a container environment with a writable directory + /run/host/unix-export/ pre-mounted it binds SSH to an AF_UNIX + socket /run/host/unix-export/ssh. The assumption is that this directory is bind + mounted to the host side as well, and can be used to connect to the container from there. See Container Interface for more information about + this interface. + + A local AF_UNIX socket + /run/ssh-unix-local/socket is also bound, unconditionally. This may be used for + SSH communication from the host to itself, without involving networking, for example to traverse + security boundaries safely and with secure authentication. + + Additional AF_UNIX and AF_VSOCK sockets are + optionally bound, based on the systemd.ssh_listen= kernel command line option or the + ssh.listen system credential (see below). + + + See + systemd-ssh-proxy1 for + details on how to connect to these sockets via the ssh client. + + The generator will use a packaged sshd@.service service template file if one + exists, and otherwise generate a suitable service template file. + + systemd-ssh-generator implements + systemd.generator7. + + + + Kernel Command Line + + systemd-ssh-generator understands the following + kernel-command-line7 + parameters: + + + + systemd.ssh_auto= + + This option takes an optional boolean argument, and defaults to yes. If enabled, the + automatic binding to the AF_VSOCK and AF_UNIX sockets + listed above is done. If disable, this is not done, except for those explicitly requested via + systemd.ssh_listen= on the kernel command line or via the + ssh.listen system credential. + + + + + + systemd.ssh_listen= + + This option configures an additional socket to bind SSH to. It may be used multiple + times to bind multiple sockets. The syntax should follow the one of ListenStream=, + see + systemd.socket5 + for details. This functionality supports all socket families systemd supports, including + AF_INET and AF_INET6. + + + + + + + + Credentials + + systemd-ssh-generator supports the system credentials logic. The following + credentials are used when passed in: + + + + ssh.listen + + This credential should be a text file, with each line referencing one additional + socket to bind SSH to. The syntax should follow the one of ListenStream=, see + systemd.socket5 + for details. This functionality supports all socket families systemd supports, including + AF_INET and AF_INET6. + + + + + + + + See Also + + systemd1 + kernel-command-line7 + systemd.system-credentials7 + vsock7 + unix7 + ssh1 + sshd8 + + + diff --git a/man/systemd.system-credentials.xml b/man/systemd.system-credentials.xml index eb4c94c47f1..c1c8e97f0c3 100644 --- a/man/systemd.system-credentials.xml +++ b/man/systemd.system-credentials.xml @@ -217,6 +217,17 @@ + + ssh.listen + + May be used to configure SSH sockets the system shall be reachable on. See + systemd-ssh-generator8 + for details. + + + + + sysusers.extra diff --git a/meson.build b/meson.build index d2d255391d5..9074f14069f 100644 --- a/meson.build +++ b/meson.build @@ -2206,6 +2206,7 @@ subdir('src/shutdown') subdir('src/sleep') subdir('src/socket-activate') subdir('src/socket-proxy') +subdir('src/ssh-generator') subdir('src/stdio-bridge') subdir('src/sulogin-shell') subdir('src/sysctl') diff --git a/src/ssh-generator/meson.build b/src/ssh-generator/meson.build new file mode 100644 index 00000000000..d21a7a36d95 --- /dev/null +++ b/src/ssh-generator/meson.build @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +executables += [ + generator_template + { + 'name' : 'systemd-ssh-generator', + 'sources' : files('ssh-generator.c'), + }, +] diff --git a/src/ssh-generator/ssh-generator.c b/src/ssh-generator/ssh-generator.c new file mode 100644 index 00000000000..feb967be747 --- /dev/null +++ b/src/ssh-generator/ssh-generator.c @@ -0,0 +1,476 @@ +/* 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\n" + "StandardInput=socket", + 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) { + + 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" + "\n[Socket]\n" + "ListenStream=%s\n" + "Accept=yes\n" + "PollLimitIntervalSec=30s\n" + "PollLimitBurst=50\n", + comment, + 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_vm(); + if (v < 0) + return log_error_errno(v, "Failed to detect if we run in a VM: %m"); + if (v == VIRTUALIZATION_NONE) { + 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) + 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"); + 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"); + 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_debug_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"); + 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); + 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; + if (r == 0) + return 0; + + _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_free) 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); From 0abd510f7f628d0369f0814b671302e93c62b161 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jan 2024 23:31:51 +0100 Subject: [PATCH 07/11] 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 From 613fb4b6010932efcaa93375eb4e5a06d4087052 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 5 Jan 2024 16:43:41 +0100 Subject: [PATCH 08/11] nspawn: expose a dir in the container where it can bind AF_UNIX sockets that will appear on the host --- src/nspawn/nspawn.c | 120 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 117 insertions(+), 3 deletions(-) diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 4df0a0092d2..7ec9889870c 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -2,7 +2,6 @@ #include #include -#include #include #if HAVE_SELINUX #include @@ -10,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +17,8 @@ #include #include +#include /* Must be included after */ + #include "sd-bus.h" #include "sd-daemon.h" #include "sd-id128.h" @@ -3608,6 +3610,102 @@ static int setup_notify_child(void) { return TAKE_FD(fd); } +static int setup_unix_export_dir_outside(char **ret) { + int r; + + assert(ret); + + _cleanup_free_ char *p = NULL; + p = path_join("/run/systemd/nspawn/unix-export", arg_machine); + if (!p) + return log_oom(); + + r = path_is_mount_point(p, /* root= */ NULL, 0); + if (r > 0) + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Mount point '%s' exists already, refusing.", p); + if (r < 0 && r != -ENOENT) + return log_error_errno(r, "Failed to detect if '%s' is a mount point: %m", p); + + r = mkdir_p(p, 0755); + if (r < 0) + return log_error_errno(r, "Failed to create '%s': %m", p); + + _cleanup_(rmdir_and_freep) char *q = TAKE_PTR(p); + + /* Mount the "unix export" directory really tiny, just 64 inodes. We mark the superblock writable + * (since the container shall bind sockets into it). */ + r = mount_nofollow_verbose( + LOG_ERR, + "tmpfs", + q, + "tmpfs", + MS_NODEV|MS_NOEXEC|MS_NOSUID|ms_nosymfollow_supported(), + "size=4M,nr_inodes=64,mode=0755"); + if (r < 0) + return r; + + _cleanup_(umount_and_rmdir_and_freep) char *w = TAKE_PTR(q); + + /* After creating the superblock we change the bind mount to be read-only. This means that the fs + * itself is writable, but not through the mount accessible from the host. */ + r = mount_nofollow_verbose( + LOG_ERR, + /* source= */ NULL, + w, + /* fstype= */ NULL, + MS_BIND|MS_REMOUNT|MS_RDONLY|MS_NODEV|MS_NOEXEC|MS_NOSUID|ms_nosymfollow_supported(), + /* options= */ NULL); + if (r < 0) + return r; + + *ret = TAKE_PTR(w); + return 0; +} + +static int setup_unix_export_host_inside(const char *directory, const char *unix_export_path) { + int r; + + assert(directory); + assert(unix_export_path); + + r = make_run_host(directory); + if (r < 0) + return r; + + _cleanup_free_ char *p = path_join(directory, "run/host/unix-export"); + if (!p) + return log_oom(); + + if (mkdir(p, 0755) < 0) + return log_error_errno(errno, "Failed to create '%s': %m", p); + + r = mount_nofollow_verbose( + LOG_ERR, + unix_export_path, + p, + /* fstype= */ NULL, + MS_BIND, + /* options= */ NULL); + if (r < 0) + return r; + + r = mount_nofollow_verbose( + LOG_ERR, + /* source= */ NULL, + p, + /* fstype= */ NULL, + MS_BIND|MS_REMOUNT|MS_NODEV|MS_NOEXEC|MS_NOSUID|ms_nosymfollow_supported(), + /* options= */ NULL); + if (r < 0) + return r; + + r = userns_lchown(p, 0, 0); + if (r < 0) + return log_error_errno(r, "Failed to chown '%s': %m", p); + + return 0; +} + static int outer_child( Barrier *barrier, const char *directory, @@ -3615,7 +3713,8 @@ static int outer_child( int fd_outer_socket, int fd_inner_socket, FDSet *fds, - int netns_fd) { + int netns_fd, + const char *unix_export_path) { _cleanup_(bind_user_context_freep) BindUserContext *bind_user_context = NULL; _cleanup_strv_free_ char **os_release_pairs = NULL; @@ -3909,6 +4008,10 @@ static int outer_child( p = prefix_roota(directory, "/run/host"); (void) make_inaccessible_nodes(p, arg_uid_shift, arg_uid_shift); + r = setup_unix_export_host_inside(directory, unix_export_path); + if (r < 0) + return r; + r = setup_pts(directory); if (r < 0) return r; @@ -4760,6 +4863,7 @@ static int run_container( _cleanup_close_ int notify_socket = -EBADF, mntns_fd = -EBADF, fd_kmsg_fifo = -EBADF; _cleanup_(barrier_destroy) Barrier barrier = BARRIER_NULL; _cleanup_(sd_event_source_unrefp) sd_event_source *notify_event_source = NULL; + _cleanup_(umount_and_rmdir_and_freep) char *unix_export_host_dir = NULL; _cleanup_(sd_event_unrefp) sd_event *event = NULL; _cleanup_(pty_forward_freep) PTYForward *forward = NULL; _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; @@ -4775,6 +4879,11 @@ static int run_container( assert_se(sigemptyset(&mask_chld) == 0); assert_se(sigaddset(&mask_chld, SIGCHLD) == 0); + /* Set up the unix export host directory on the host first */ + r = setup_unix_export_dir_outside(&unix_export_host_dir); + if (r < 0) + return r; + if (arg_userns_mode == USER_NAMESPACE_PICK) { /* When we shall pick the UID/GID range, let's first lock /etc/passwd, so that we can safely * check with getpwuid() if the specific user already exists. Note that /etc might be @@ -4845,7 +4954,8 @@ static int run_container( fd_outer_socket_pair[1], fd_inner_socket_pair[1], fds, - child_netns_fd); + child_netns_fd, + unix_export_host_dir); if (r < 0) _exit(EXIT_FAILURE); @@ -5919,6 +6029,10 @@ finish: p = strjoina("/run/systemd/nspawn/propagate/", arg_machine); (void) rm_rf(p, REMOVE_ROOT); + + p = strjoina("/run/systemd/nspawn/unix-export/", arg_machine); + (void) umount2(p, MNT_DETACH|UMOUNT_NOFOLLOW); + (void) rmdir(p); } expose_port_flush(&fw_ctx, arg_expose_ports, AF_INET, &expose_args.address4); From 62b3e5fd417e19c7292b5bc116098aa9bf456afa Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 5 Jan 2024 18:34:01 +0100 Subject: [PATCH 09/11] doc: document new /run/host/ inodes in container interface doc --- docs/CONTAINER_INTERFACE.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/CONTAINER_INTERFACE.md b/docs/CONTAINER_INTERFACE.md index 7fa8558c7cf..dcecdecc3eb 100644 --- a/docs/CONTAINER_INTERFACE.md +++ b/docs/CONTAINER_INTERFACE.md @@ -273,6 +273,30 @@ care should be taken to avoid naming conflicts. `systemd` (and in particular 7. The `/run/host/credentials/` directory is a good place to pass credentials into the container, using the `$CREDENTIALS_DIRECTORY` protocol, see above. +8. The `/run/host/unix-export/` directory shall be writable from the container + payload, and is where container payload can bind `AF_UNIX` sockets in that + shall be *exported* to the host, so that the host can connect to them. The + container manager should bind mount this directory on the host side + (read-only ideally), so that the host can connect to contained sockets. This + is most prominently used by `systemd-ssh-generator` when run in such a + container to automatically bind an SSH socket into that directory, which + then can be used to connect to the container. + +9. The `/run/host/unix-export/ssh` `AF_UNIX` socket will be automatically bound + by `systemd-ssh-generator` in the container if possible, and can be used to + connect to the container. + +10. The `/run/host/userdb/` directory may be used to drop-in additional JSON + user records that `nss-systemd` inside the container shall include in the + system's user database. This is useful to make host users and their home + directories automatically accessible to containers in transitive + fashion. See `nss-systemd(8)` for details. + +11. The `/run/host/home/` directory may be used to bind mount host home + directories of users that shall be made available in the container to. This + may be used in combination with `/run/host/userdb/` above: one defines the + user record, the other contains the user's home directory. + ## What You Shouldn't Do 1. Do not drop `CAP_MKNOD` from the container. `PrivateDevices=` is a commonly From 52d863defcd05d0b676ce743b9eaf1060131f9a1 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 10 Jan 2024 11:28:23 +0100 Subject: [PATCH 10/11] test: add testcase for ssh generator --- test/TEST-74-AUX-UTILS/test.sh | 14 +++++++- test/units/testsuite-74.ssh.sh | 58 ++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100755 test/units/testsuite-74.ssh.sh diff --git a/test/TEST-74-AUX-UTILS/test.sh b/test/TEST-74-AUX-UTILS/test.sh index 2d17630d29e..d870d57dccf 100755 --- a/test/TEST-74-AUX-UTILS/test.sh +++ b/test/TEST-74-AUX-UTILS/test.sh @@ -11,6 +11,10 @@ NSPAWN_ARGUMENTS="--private-network" # (Hopefully) a temporary workaround for https://github.com/systemd/systemd/issues/30573 KERNEL_APPEND="${KERNEL_APPEND:-} SYSTEMD_DEFAULT_MOUNT_RATE_LIMIT_BURST=100" +# Make sure vsock is available in the VM +CID=$((RANDOM + 3)) +QEMU_OPTIONS+=" -device vhost-vsock-pci,guest-cid=$CID" + test_append_files() { local workspace="${1:?}" @@ -26,7 +30,15 @@ test_append_files() { generate_module_dependencies fi - image_install socat + inst_binary socat + inst_binary ssh + inst_binary sshd + inst_binary ssh-keygen + inst_binary usermod + instmods vmw_vsock_virtio_transport + instmods vsock_loopback + instmods vmw_vsock_vmci_transport + generate_module_dependencies } do_test "$@" diff --git a/test/units/testsuite-74.ssh.sh b/test/units/testsuite-74.ssh.sh new file mode 100755 index 00000000000..bf87a9bd3a6 --- /dev/null +++ b/test/units/testsuite-74.ssh.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +if ! command -v ssh &> /dev/null || ! command -v sshd &> /dev/null ; then + echo "ssh/sshd not found, skipping test." >&2 + exit 0 +fi + +systemctl -q is-active sshd-unix-local.socket + +if test -e /dev/vsock ; then + systemctl -q is-active sshd-vsock.socket +fi + +if test -d /run/host/unix-export ; then + systemctl -q is-active sshd-unix-export.socket +fi + +# FIXME: sshd seems to crash inside asan currently, skip the actual ssh test hence +if [[ -v ASAN_OPTIONS ]] ; then + exit 0 +fi + +ROOTID=$(mktemp -u) + +removesshid() { + rm -f "$ROOTID" "$ROOTID".pub +} + +ssh-keygen -N '' -C '' -t rsa -f "$ROOTID" + +mkdir -p 0700 /root/.ssh +cat "$ROOTID".pub >> /root/.ssh/authorized_keys + +# set root pw to "foo", just to set it to something valid +# shellcheck disable=SC2016 +usermod -p '$5$AAy6BYJ6rzz.QELv$6LpVEU3/RQmVz.svHu/33qoJWWWzZuJ3DM2fo9JgcUD' root +usermod -U root + +mkdir -p /etc/ssh +test -f /etc/ssh/ssh_host_rsa_key || ssh-keygen -t rsa -C '' -N '' -f /etc/ssh/ssh_host_rsa_key +echo "PermitRootLogin yes" >> /etc/ssh/sshd_config +echo "LogLevel DEBUG3" >> /etc/ssh/sshd_config + +test -f /etc/ssh/ssh_config || echo 'Include /etc/ssh/ssh_config.d/*.conf' > /etc/ssh/ssh_config + +# ssh wants this dir around, but distros cannot agree on a common name for it, let's just create all that are aware of distros use +mkdir -p /usr/share/empty.sshd /var/empty /var/empty/sshd + +ssh -o StrictHostKeyChecking=no -v -i "$ROOTID" .host cat /etc/machine-id | cmp - /etc/machine-id +ssh -o StrictHostKeyChecking=no -v -i "$ROOTID" unix/run/ssh-unix-local/socket cat /etc/machine-id | cmp - /etc/machine-id + +modprobe vsock_loopback ||: +if test -e /dev/vsock -a -d /sys/module/vsock_loopback ; then + ssh -o StrictHostKeyChecking=no -v -i "$ROOTID" vsock/1 cat /etc/machine-id | cmp - /etc/machine-id +fi From 9a1dc7549f39d47527619532ec080618c5c78f8a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 5 Jan 2024 14:52:35 +0100 Subject: [PATCH 11/11] update TODO --- TODO | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/TODO b/TODO index 9b6a52f5cfb..10ebbeba64d 100644 --- a/TODO +++ b/TODO @@ -137,6 +137,13 @@ Features: to read them from. This way the data doesn't remain in the SMBIOS blob during runtime, but only in the credentials fs. +* machined: make machine registration available via varlink to simplify + nspawn/vmspawn, and to have an extensible way to register VM/machine metadata + +* ssh-proxy: add support for "ssh machine/foobar" to automatically connect to + machined registered machine "foobar". Requires updating machined to track CID + and unix-export dir of containers. + * add a new ExecStart= flag that inserts the configured user's shell as first word in the command line. (maybe use character '.'). Usecase: tool such as uid0 can use that to spawn the target user's default shell. @@ -301,15 +308,6 @@ Features: the realized cgroup, to pin it (and later execute all cgroup operations over, once we drop cgroupv1 compat). -* add new "systemd-ssh-generator", which allows basic ssh config via - credentials (host key). It generates sshd.socket for IP, but also - sshd-vsock.socket for listening on AF_VSOCK when running in a VM, and - sshd-unix.socket on AF_UNIX when running in a container. It also generates a - matching sshd.service file with a host key passed in on the cmdline via - credentials. Then, add a ssh_config drop-in that matches some suitable - hostname pattern and has a ProxyCommand set that allows connecting to any - local VM/container that way without any networking configured. - * Varlinkification of the following command line tools, to open them up to other programs via IPC: - bootctl