diff --git a/TODO b/TODO
index 9b6a52f5cf..10ebbeba64 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
diff --git a/docs/CONTAINER_INTERFACE.md b/docs/CONTAINER_INTERFACE.md
index 7fa8558c7c..dcecdecc3e 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
diff --git a/man/kernel-command-line.xml b/man/kernel-command-line.xml
index 25ad770dcd..ded41ffc36 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 3592b862f7..c43bffde69 100644
--- a/man/rules/meson.build
+++ b/man/rules/meson.build
@@ -1054,6 +1054,8 @@ manpages = [
['systemd-socket-activate', '1', [], ''],
['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-generator.xml b/man/systemd-ssh-generator.xml
new file mode 100644
index 0000000000..04c3263c37
--- /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-ssh-proxy.xml b/man/systemd-ssh-proxy.xml
new file mode 100644
index 0000000000..d9615ff62c
--- /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/man/systemd.system-credentials.xml b/man/systemd.system-credentials.xml
index eb4c94c47f..c1c8e97f0c 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 d2d255391d..53a3d966ff 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)
@@ -2206,6 +2212,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')
@@ -2688,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 c677c7f420..b74f949189 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/mkosi.images/system/mkosi.conf.d/10-centos-fedora.conf b/mkosi.images/system/mkosi.conf.d/10-centos-fedora.conf
index 67d46432d4..871186d5ca 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 588f833c8f..348bdb2992 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 60a2b6dbfc..71434b4560 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
diff --git a/src/basic/iovec-util.h b/src/basic/iovec-util.h
index bc7a67054a..8cfa5717dc 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);
diff --git a/src/network/generator/main.c b/src/network/generator/main.c
index 540b6df4fc..4a3ccd6a99 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/nspawn/nspawn.c b/src/nspawn/nspawn.c
index 4df0a0092d..7ec9889870 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);
diff --git a/src/shared/generator.c b/src/shared/generator.c
index fe58021f00..b96715c59c 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,10 +73,13 @@ 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;
}
-
int generator_add_symlink_full(
const char *dir,
const char *dst,
@@ -88,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 */
@@ -110,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();
diff --git a/src/shared/generator.h b/src/shared/generator.h
index d97d6edc67..c17feafacc 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);
diff --git a/src/shared/install.c b/src/shared/install.c
index ad30e9b49c..fabf5db7ed 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 bc0c6db828..3e2ada45f4 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);
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 0000000000..b97e0f5340
--- /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 0000000000..70a706f2aa
--- /dev/null
+++ b/src/ssh-generator/meson.build
@@ -0,0 +1,25 @@
+# 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'),
+ },
+]
+
+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-generator.c b/src/ssh-generator/ssh-generator.c
new file mode 100644
index 0000000000..feb967be74
--- /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);
diff --git a/src/ssh-generator/ssh-proxy.c b/src/ssh-generator/ssh-proxy.c
new file mode 100644
index 0000000000..4884c934d7
--- /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/test/TEST-74-AUX-UTILS/test.sh b/test/TEST-74-AUX-UTILS/test.sh
index 2d17630d29..d870d57dcc 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 0000000000..bf87a9bd3a
--- /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
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 0000000000..033379ec7a
--- /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 390076b6d5..d05ea94c16 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