From 864a5e9ded1a15b90520ebb11f92b6ab45626a94 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 12 Jun 2025 14:34:13 +0200 Subject: [PATCH 1/2] getty: support /run/issue.d/ By default agetty will not display /run/issue.d/ if /etc/issue exists. This is quite unfortunate and has actually been fixed upstream in: https://github.com/util-linux/util-linux/commit/508fb0e7ac103b68531a59db2a4473897853ab52 However, no release has been tagged with this yet, and it doesn't look like this will happen any time soon. Hence, for now, let's add a work-around and manually override the issue files to include. This should be reverted once a new util-linux/agetty release has been tagged, and found its way into the relevant distributions. Given this is mostly about cosmetics we do not have to precisely sync the package updates on this, but only roughly. --- TODO | 4 ++++ units/console-getty.service.in | 2 +- units/container-getty@.service.in | 2 +- units/getty@.service.in | 2 +- units/serial-getty@.service.in | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/TODO b/TODO index f3cfbcbaef..d3cb7873d4 100644 --- a/TODO +++ b/TODO @@ -118,6 +118,10 @@ Deprecations and removals: * Consider removing root=gpt-auto, and push people to use root=dissect instead. +* Once + https://github.com/util-linux/util-linux/commit/508fb0e7ac103b68531a59db2a4473897853ab52 + has hit the prominent distributions, revert --issue-file= hack in units/*getty*sevice.in + Features: * replace bootctl's PE version check to actually use APIs from pe-binary.[ch] diff --git a/units/console-getty.service.in b/units/console-getty.service.in index 2182109009..967d8337ab 100644 --- a/units/console-getty.service.in +++ b/units/console-getty.service.in @@ -20,7 +20,7 @@ Before=getty.target ConditionPathExists=/dev/console [Service] -ExecStart=-/sbin/agetty --noreset --noclear --keep-baud 115200,57600,38400,9600 - ${TERM} +ExecStart=-/sbin/agetty --noreset --noclear --issue-file=/etc/issue:/etc/issue.d:/run/issue.d:/usr/lib/issue.d --keep-baud 115200,57600,38400,9600 - ${TERM} Type=idle Restart=always UtmpIdentifier=cons diff --git a/units/container-getty@.service.in b/units/container-getty@.service.in index f24bb73fc1..e0b27613df 100644 --- a/units/container-getty@.service.in +++ b/units/container-getty@.service.in @@ -25,7 +25,7 @@ Conflicts=rescue.service Before=rescue.service [Service] -ExecStart=-/sbin/agetty --noreset --noclear - ${TERM} +ExecStart=-/sbin/agetty --noreset --noclear --issue-file=/etc/issue:/etc/issue.d:/run/issue.d:/usr/lib/issue.d - ${TERM} Type=idle Restart=always RestartSec=0 diff --git a/units/getty@.service.in b/units/getty@.service.in index a43e01144f..104c4acc96 100644 --- a/units/getty@.service.in +++ b/units/getty@.service.in @@ -34,7 +34,7 @@ Before=rescue.service ConditionPathExists=/dev/tty0 [Service] -ExecStart=-/sbin/agetty --noreset --noclear - ${TERM} +ExecStart=-/sbin/agetty --noreset --noclear --issue-file=/etc/issue:/etc/issue.d:/run/issue.d:/usr/lib/issue.d - ${TERM} Type=idle Restart=always RestartSec=0 diff --git a/units/serial-getty@.service.in b/units/serial-getty@.service.in index c0adaf7bdb..0134c83d48 100644 --- a/units/serial-getty@.service.in +++ b/units/serial-getty@.service.in @@ -30,7 +30,7 @@ Conflicts=rescue.service Before=rescue.service [Service] -ExecStart=-/sbin/agetty --noreset --noclear --keep-baud 115200,57600,38400,9600 - ${TERM} +ExecStart=-/sbin/agetty --noreset --noclear --issue-file=/etc/issue:/etc/issue.d:/run/issue.d:/usr/lib/issue.d --keep-baud 115200,57600,38400,9600 - ${TERM} Type=idle Restart=always UtmpIdentifier=%I From cfd89202435751a0c46a46a81a6d83fb92f4b4db Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 12 Jun 2025 11:38:21 +0200 Subject: [PATCH 2/2] ssh-generator: generate /etc/issue.d/ with VSOCK ssh info data I find myself trying to log into a fresh ParticleOS VM started via systemd-vmspawn all the time, but I don't know its CID. Let's show it on the getty screen, to make it immediately visible. --- man/rules/meson.build | 1 + man/systemd-ssh-issue.xml | 99 ++++++++++++ src/ssh-generator/meson.build | 4 + src/ssh-generator/ssh-generator.c | 9 ++ src/ssh-generator/ssh-issue.c | 242 ++++++++++++++++++++++++++++++ 5 files changed, 355 insertions(+) create mode 100644 man/systemd-ssh-issue.xml create mode 100644 src/ssh-generator/ssh-issue.c diff --git a/man/rules/meson.build b/man/rules/meson.build index b5dfdb925c..12e9fd97aa 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1118,6 +1118,7 @@ manpages = [ ['systemd-socket-proxyd', '8', [], ''], ['systemd-soft-reboot.service', '8', [], ''], ['systemd-ssh-generator', '8', [], ''], + ['systemd-ssh-issue', '1', [], ''], ['systemd-ssh-proxy', '1', [], ''], ['systemd-stdio-bridge', '1', [], ''], ['systemd-storagetm.service', '8', ['systemd-storagetm'], 'ENABLE_STORAGETM'], diff --git a/man/systemd-ssh-issue.xml b/man/systemd-ssh-issue.xml new file mode 100644 index 0000000000..4e88779676 --- /dev/null +++ b/man/systemd-ssh-issue.xml @@ -0,0 +1,99 @@ + + + + + + + + systemd-ssh-issue + systemd + + + + systemd-ssh-issue + 1 + + + + systemd-ssh-issue + Generator for SSH login prompt drop-ins + + + + + /usr/lib/systemd/systemd-ssh-issue + /usr/lib/systemd/systemd-ssh-issue + + + + + Description + + systemd-ssh-issue is a small tool that generates a + /run/issue.d/50-ssh-vsock.issue drop-in file in case AF_VSOCK + support is available in the kernel and the VM environment. The file contains brief information about how + to contact the local system via SSH-over-AF_VSOCK, in particular it reports the + system's AF_VSOCK CID. The file is typically read and displayed by agetty8 on + console or serial login prompts. + + This tool is automatically used by + systemd-ssh-generator8 + in the AF_VSOCK socket units it generates. + + + + Options + The following options are understood: + + + + + Generates the issue file. This command has no effect if called on systems lacking + AF_VSOCK support. + + + + + + + Removes the issue file if it exists. + + + + + + + Changes the path to the issue file to write to/remove. If not specified, defaults to + /run/issue.d/50-ssh-vsock.issue. If specified as empty string or + - writes the issue file contents to standard output. + + + + + + + + + + + Exit status + + On success, 0 is returned, a non-zero failure code + otherwise. + + + + See Also + + systemd1 + systemd-ssh-generator8 + vsock7 + ssh1 + sshd8 + agetty8 + + + diff --git a/src/ssh-generator/meson.build b/src/ssh-generator/meson.build index b14ef46ff5..f281a25184 100644 --- a/src/ssh-generator/meson.build +++ b/src/ssh-generator/meson.build @@ -9,6 +9,10 @@ executables += [ 'name' : 'systemd-ssh-proxy', 'sources' : files('ssh-proxy.c'), }, + libexec_template + { + 'name' : 'systemd-ssh-issue', + 'sources' : files('ssh-issue.c'), + }, ] if conf.get('ENABLE_SSH_PROXY_CONFIG') == 1 diff --git a/src/ssh-generator/ssh-generator.c b/src/ssh-generator/ssh-generator.c index 3f1fd8cc30..0c7eba366a 100644 --- a/src/ssh-generator/ssh-generator.c +++ b/src/ssh-generator/ssh-generator.c @@ -134,6 +134,7 @@ static int write_socket_unit( const char *unit, const char *listen_stream, const char *comment, + const char *extra, bool with_ssh_access_target_dependency) { int r; @@ -172,6 +173,9 @@ static int write_socket_unit( "PollLimitBurst=50\n", listen_stream); + if (extra) + fputs(extra, f); + r = fflush_and_check(f); if (r < 0) return log_error_errno(r, "Failed to write %s SSH socket unit: %m", comment); @@ -245,6 +249,8 @@ static int add_vsock_socket( "sshd-vsock.socket", "vsock::22", "AF_VSOCK", + "ExecStartPost=-/usr/lib/systemd/systemd-ssh-issue --make-vsock\n" + "ExecStopPre=-/usr/lib/systemd/systemd-ssh-issue --rm-vsock\n", /* with_ssh_access_target_dependency= */ true); if (r < 0) return r; @@ -280,6 +286,7 @@ static int add_local_unix_socket( "sshd-unix-local.socket", "/run/ssh-unix-local/socket", "AF_UNIX Local", + /* extra= */ NULL, /* with_ssh_access_target_dependency= */ false); if (r < 0) return r; @@ -336,6 +343,7 @@ static int add_export_unix_socket( "sshd-unix-export.socket", "/run/host/unix-export/ssh", "AF_UNIX Export", + /* extra= */ NULL, /* with_ssh_access_target_dependency= */ true); if (r < 0) return r; @@ -387,6 +395,7 @@ static int add_extra_sockets( socket ?: "sshd-extra.socket", *i, *i, + /* extra= */ NULL, /* with_ssh_access_target_dependency= */ true); if (r < 0) return r; diff --git a/src/ssh-generator/ssh-issue.c b/src/ssh-generator/ssh-issue.c new file mode 100644 index 0000000000..68ad66bfd0 --- /dev/null +++ b/src/ssh-generator/ssh-issue.c @@ -0,0 +1,242 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include + +#include "alloc-util.h" +#include "ansi-color.h" +#include "build.h" +#include "errno-util.h" +#include "fd-util.h" +#include "fs-util.h" +#include "log.h" +#include "main-func.h" +#include "mkdir.h" +#include "parse-argument.h" +#include "pretty-print.h" +#include "socket-util.h" +#include "string-util.h" +#include "tmpfile-util.h" +#include "virt.h" + +static enum { + ACTION_MAKE_VSOCK, + ACTION_RM_VSOCK, +} arg_action = ACTION_MAKE_VSOCK; + +static char* arg_issue_path = NULL; +static bool arg_issue_stdout = false; + +STATIC_DESTRUCTOR_REGISTER(arg_issue_path, freep); + +static int help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("systemd-ssh-issue", "1", &link); + if (r < 0) + return log_oom(); + + printf("%s [OPTIONS...] --make-vsock\n" + "%s [OPTIONS...] --rm-vsock\n" + "\n%sCreate ssh /run/issue.d/ file reporting VSOCK address.%s\n\n" + " -h --help Show this help\n" + " --version Show package version\n" + " --issue-path=PATH Change path to /run/issue.d/50-ssh-vsock.issue\n" + "\nSee the %s for details.\n", + program_invocation_short_name, + program_invocation_short_name, + ansi_highlight(), + ansi_normal(), + link); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + + enum { + ARG_MAKE_VSOCK = 0x100, + ARG_RM_VSOCK, + ARG_ISSUE_PATH, + ARG_VERSION, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "make-vsock", no_argument, NULL, ARG_MAKE_VSOCK }, + { "rm-vsock", no_argument, NULL, ARG_RM_VSOCK }, + { "issue-path", required_argument, NULL, ARG_ISSUE_PATH }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) { + + switch (c) { + + case 'h': + return help(); + + case ARG_VERSION: + return version(); + + case ARG_MAKE_VSOCK: + arg_action = ACTION_MAKE_VSOCK; + break; + + case ARG_RM_VSOCK: + arg_action = ACTION_RM_VSOCK; + break; + + case ARG_ISSUE_PATH: + if (isempty(optarg) || streq(optarg, "-")) { + arg_issue_path = mfree(arg_issue_path); + arg_issue_stdout = true; + break; + } + + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_issue_path); + if (r < 0) + return r; + + arg_issue_stdout = false; + break; + } + } + + if (!arg_issue_path && !arg_issue_stdout) { + arg_issue_path = strdup("/run/issue.d/50-ssh-vsock.issue"); + if (!arg_issue_path) + return log_oom(); + } + + return 1; +} + +static int acquire_cid(unsigned *ret_cid) { + int r; + + assert(ret_cid); + + Virtualization v = detect_virtualization(); + if (v < 0) + return log_error_errno(v, "Failed to detect if we run in a VM: %m"); + if (!VIRTUALIZATION_IS_VM(v)) { + /* NB: if we are running in a container inside a VM, then we'll *not* do AF_VSOCK stuff */ + log_debug("Not running in a VM, not creating issue file."); + *ret_cid = 0; + 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 issue file, since AF_VSOCK is not available."); + *ret_cid = 0; + return 0; + } + + return log_error_errno(errno, "Unable to test if AF_VSOCK is available: %m"); + } + + vsock_fd = safe_close(vsock_fd); + + unsigned local_cid; + r = vsock_get_local_cid(&local_cid); + if (r < 0) { + if (ERRNO_IS_DEVICE_ABSENT(r)) { + log_debug("Not creating issue file, since /dev/vsock is not available (even though AF_VSOCK is)."); + *ret_cid = 0; + return 0; + } + + return log_error_errno(r, "Failed to query local AF_VSOCK CID: %m"); + } + + *ret_cid = local_cid; + return 1; +} + +static int run(int argc, char* argv[]) { + int r; + + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + switch (arg_action) { + case ACTION_MAKE_VSOCK: { + unsigned cid; + + r = acquire_cid(&cid); + if (r < 0) + return r; + if (r == 0) { + log_debug("Not running in a VSOCK enabled VM, skipping."); + break; + } + + _cleanup_(unlink_and_freep) char *t = NULL; + _cleanup_(fclosep) FILE *f = NULL; + FILE *out; + + if (arg_issue_path) { + r = mkdir_parents(arg_issue_path, 0755); + if (r < 0) + return log_error_errno(r, "Failed to create parent directories of '%s': %m", arg_issue_path); + + r = fopen_tmpfile_linkable(arg_issue_path, O_WRONLY|O_CLOEXEC, &t, &f); + if (r < 0) + return log_error_errno(r, "Failed to create '%s': %m", arg_issue_path); + + out = f; + } else + out = stdout; + + fprintf(out, + "Try contacting this VM's SSH server via 'ssh vsock%%%u' from host.\n" + "\n", cid); + + if (f) { + if (fchmod(fileno(f), 0644) < 0) + return log_error_errno(errno, "Failed to adjust access mode of '%s': %m", arg_issue_path); + + r = flink_tmpfile(f, t, arg_issue_path, LINK_TMPFILE_REPLACE); + if (r < 0) + return log_error_errno(r, "Failed to move '%s' into place: %m", arg_issue_path); + } + + break; + } + + case ACTION_RM_VSOCK: + if (arg_issue_path) { + if (unlink(arg_issue_path) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to remove '%s': %m", arg_issue_path); + + log_debug_errno(errno, "File '%s' does not exist, no operation executed.", arg_issue_path); + } else + log_debug("Successfully removed '%s'.", arg_issue_path); + } else + log_notice("STDOUT selected for issue file, not removing."); + + break; + + default: + assert_not_reached(); + } + + return 0; +} + +DEFINE_MAIN_FUNCTION(run);