From 506aa3713b7a572bdce54eb7dadf9ccb11225821 Mon Sep 17 00:00:00 2001 From: Sam Leonard Date: Wed, 10 Apr 2024 16:21:12 +0100 Subject: [PATCH 1/3] vmspawn: add a dropin override to sshd-vsock@.service --- src/vmspawn/vmspawn.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 3279d147e0..116c043709 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -1867,6 +1867,18 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { r = unit_name_to_prefix(trans_scope, &scope_prefix); if (r < 0) return log_error_errno(r, "Failed to strip .scope suffix from scope: %m"); + + /* on distros that provide their own sshd@.service file we need to provide a dropin which + * picks up our public key credential */ + r = machine_credential_set( + &arg_credentials, + "systemd.unit-dropin.sshd-vsock@.service:" + "[Service]\n" + "ExecStart=\n" + "ExecStart=sshd -i -o 'AuthorizedKeysFile=%d/ssh.ephemeral-authorized_keys-all .ssh/authorized_keys'\n" + "ImportCredential=ssh.ephemeral-authorized_keys-all\n"); + if (r < 0) + return log_error_errno(r, "Failed to set credential systemd.unit-dropin.sshd-vsock@.service: %m"); } if (ARCHITECTURE_SUPPORTS_SMBIOS) From 1e83cfff8dfb0fb641711cbfee606207418756e3 Mon Sep 17 00:00:00 2001 From: Sam Leonard Date: Tue, 9 Apr 2024 17:27:21 +0100 Subject: [PATCH 2/3] vmspawn: register with io.systemd.Machine.Register When available, use varlink to register the VM, so that we can talk over SSH to the guest. Enables 'machinectl shell' for vmspawn guests. --- src/vmspawn/vmspawn-register.c | 74 +++++++++++++++++++++++++--------- src/vmspawn/vmspawn-register.h | 10 ++++- src/vmspawn/vmspawn.c | 23 ++++++++--- 3 files changed, 81 insertions(+), 26 deletions(-) diff --git a/src/vmspawn/vmspawn-register.c b/src/vmspawn/vmspawn-register.c index d04c3dafad..35c1455945 100644 --- a/src/vmspawn/vmspawn-register.c +++ b/src/vmspawn/vmspawn-register.c @@ -5,36 +5,72 @@ #include "bus-error.h" #include "bus-locator.h" +#include "json.h" #include "macro.h" #include "process-util.h" +#include "socket-util.h" #include "string-util.h" +#include "varlink.h" #include "vmspawn-register.h" -int register_machine(sd_bus *bus, const char *machine_name, sd_id128_t uuid, const char *service, const char *directory) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; +int register_machine( + sd_bus *bus, + const char *machine_name, + sd_id128_t uuid, + const char *service, + const char *directory, + unsigned cid, + const char *address, + const char *key_path) { + + _cleanup_(varlink_unrefp) Varlink *vl = NULL; int r; - assert(bus); assert(machine_name); assert(service); - r = bus_call_method( - bus, - bus_machine_mgr, - "RegisterMachine", - &error, - NULL, - "sayssus", - machine_name, - SD_BUS_MESSAGE_APPEND_ID128(uuid), - service, - "vm", - (uint32_t) getpid_cached(), - strempty(directory)); - if (r < 0) - return log_error_errno(r, "Failed to register machine: %s", bus_error_message(&error, r)); + /* First try to use varlink, as it provides more features (such as SSH support). */ + r = varlink_connect_address(&vl, "/run/systemd/machine/io.systemd.Machine"); + if (r == -ENOENT || ERRNO_IS_DISCONNECT(r)) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - return 0; + assert(bus); + + /* In case we are running with an older machined, fallback to the existing D-Bus method. */ + r = bus_call_method( + bus, + bus_machine_mgr, + "RegisterMachine", + &error, + NULL, + "sayssus", + machine_name, + SD_BUS_MESSAGE_APPEND_ID128(uuid), + service, + "vm", + (uint32_t) getpid_cached(), + strempty(directory)); + if (r < 0) + return log_error_errno(r, "Failed to register machine: %s", bus_error_message(&error, r)); + + return 0; + } + if (r < 0) + return log_error_errno(r, "Failed to connect to machined on /run/systemd/machine/io.systemd.Machine: %m"); + + + return varlink_callb_and_log(vl, + "io.systemd.Machine.Register", + NULL, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_STRING("name", machine_name), + JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(uuid), "id", JSON_BUILD_ID128(uuid)), + JSON_BUILD_PAIR_STRING("service", service), + JSON_BUILD_PAIR_STRING("class", "vm"), + JSON_BUILD_PAIR_CONDITION(VSOCK_CID_IS_REGULAR(cid), "vsockCid", JSON_BUILD_UNSIGNED(cid)), + JSON_BUILD_PAIR_CONDITION(directory, "rootDirectory", JSON_BUILD_STRING(directory)), + JSON_BUILD_PAIR_CONDITION(address, "sshAddress", JSON_BUILD_STRING(address)), + JSON_BUILD_PAIR_CONDITION(key_path, "sshPrivateKeyPath", JSON_BUILD_STRING(key_path)))); } int unregister_machine(sd_bus *bus, const char *machine_name) { diff --git a/src/vmspawn/vmspawn-register.h b/src/vmspawn/vmspawn-register.h index 7aa82ce849..69f56714b2 100644 --- a/src/vmspawn/vmspawn-register.h +++ b/src/vmspawn/vmspawn-register.h @@ -3,5 +3,13 @@ #include "sd-bus.h" #include "sd-id128.h" -int register_machine(sd_bus *bus, const char *machine_name, sd_id128_t uuid, const char *service, const char *directory); +int register_machine( + sd_bus *bus, + const char *machine_name, + sd_id128_t uuid, + const char *service, + const char *directory, + unsigned cid, + const char *address, + const char *key_path); int unregister_machine(sd_bus *bus, const char *machine_name); diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 116c043709..51f622d0a8 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -1222,12 +1222,6 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { if (r < 0) return r; - if (arg_register) { - r = register_machine(bus, arg_machine, arg_uuid, trans_scope, arg_directory); - if (r < 0) - return r; - } - bool use_kvm = arg_kvm > 0; if (arg_kvm < 0) { r = qemu_check_kvm_support(); @@ -1931,6 +1925,23 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { log_debug("Executing: %s", joined); } + if (arg_register) { + char vm_address[STRLEN("vsock/") + DECIMAL_STR_MAX(unsigned)]; + + xsprintf(vm_address, "vsock/%u", child_cid); + r = register_machine( + bus, + arg_machine, + arg_uuid, + trans_scope, + arg_directory, + child_cid, + child_cid != VMADDR_CID_ANY ? vm_address : NULL, + ssh_private_key_path); + if (r < 0) + return r; + } + assert_se(sigprocmask_many(SIG_BLOCK, /* old_sigset=*/ NULL, SIGCHLD, SIGWINCH) >= 0); _cleanup_(sd_event_source_unrefp) sd_event_source *notify_event_source = NULL; From 521e9286aa54fd9f225e12376a6e0ab538737c2d Mon Sep 17 00:00:00 2001 From: Sam Leonard Date: Thu, 25 Apr 2024 17:16:20 +0100 Subject: [PATCH 3/3] vmspawn: forward signals to VM PID 1 via D-BUS when available Allow to gracefully shutdown by initiating the operation from the guest --- src/vmspawn/vmspawn.c | 136 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 132 insertions(+), 4 deletions(-) diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 51f622d0a8..05661961ed 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -18,6 +18,9 @@ #include "architecture.h" #include "bootspec.h" #include "build.h" +#include "bus-internal.h" +#include "bus-locator.h" +#include "bus-wait-for-jobs.h" #include "chase.h" #include "common-signal.h" #include "copy.h" @@ -58,6 +61,7 @@ #include "signal-util.h" #include "socket-util.h" #include "stat-util.h" +#include "stdio-util.h" #include "string-util.h" #include "strv.h" #include "time-util.h" @@ -71,6 +75,12 @@ #define VM_TAP_HASH_KEY SD_ID128_MAKE(01,d0,c6,4c,2b,df,24,fb,c0,f8,b2,09,7d,59,b2,93) +typedef struct SSHInfo { + unsigned cid; + char *private_key_path; + unsigned port; +} SSHInfo; + static bool arg_quiet = false; static PagerFlags arg_pager_flags = 0; static char *arg_directory = NULL; @@ -697,11 +707,61 @@ static int setup_notify_parent(sd_event *event, int fd, int *exit_status, sd_eve return 0; } +static int bus_open_in_machine(sd_bus **ret, unsigned cid, unsigned port, const char *private_key_path) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_free_ char *ssh_escaped = NULL, *bus_address = NULL; + char port_str[DECIMAL_STR_MAX(unsigned)], cid_str[DECIMAL_STR_MAX(unsigned)]; + int r; + + assert(ret); + assert(private_key_path); + + r = sd_bus_new(&bus); + if (r < 0) + return r; + + const char *ssh = secure_getenv("SYSTEMD_SSH") ?: "ssh"; + ssh_escaped = bus_address_escape(ssh); + if (!ssh_escaped) + return -ENOMEM; + + xsprintf(port_str, "%u", port); + xsprintf(cid_str, "%u", cid); + + bus_address = strjoin( + "unixexec:path=", ssh_escaped, + /* -x: Disable X11 forwarding + * -T: Disable PTY allocation */ + ",argv1=-xT", + ",argv2=-o,argv3=IdentitiesOnly yes", + ",argv4=-o,argv5=IdentityFile=", private_key_path, + ",argv6=-p,argv7=", port_str, + ",argv8=--", + ",argv9=root@vsock/", cid_str, + ",argv10=systemd-stdio-bridge" + ); + if (!bus_address) + return -ENOMEM; + + free_and_replace(bus->address, bus_address); + bus->bus_client = true; + bus->trusted = true; + bus->runtime_scope = RUNTIME_SCOPE_SYSTEM; + bus->is_local = false; + + r = sd_bus_start(bus); + if (r < 0) + return r; + + *ret = TAKE_PTR(bus); + return 0; +} + static int on_orderly_shutdown(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { PidRef *pidref = userdata; int r; - /* TODO: actually talk to qemu and ask the guest to shutdown here */ + /* Backup method to shut down the VM when D-BUS access over SSH is not available */ if (pidref) { r = pidref_kill(pidref, SIGKILL); @@ -718,6 +778,61 @@ static int on_orderly_shutdown(sd_event_source *s, const struct signalfd_siginfo return 0; } +static int forward_signal_to_vm_pid1(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + _cleanup_(bus_wait_for_jobs_freep) BusWaitForJobs *w = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + SSHInfo *ssh_info = ASSERT_PTR(userdata); + const char *vm_pid1; + int r; + + assert(s); + assert(si); + + r = bus_open_in_machine(&bus, ssh_info->cid, ssh_info->port, ssh_info->private_key_path); + if (r < 0) + return log_error_errno(r, "Failed to connect to VM to forward signal: %m"); + + r = bus_wait_for_jobs_new(bus, &w); + if (r < 0) + return log_error_errno(r, "Could not watch job: %m"); + + r = bus_call_method( + bus, + bus_systemd_mgr, + "GetUnitByPID", + &error, + NULL, + ""); + if (r < 0) + return log_error_errno(r, "Failed to get init process of VM: %s", bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "o", &vm_pid1); + if (r < 0) + return bus_log_parse_error(r); + + r = bus_wait_for_jobs_one(w, vm_pid1, /* quiet */ false, NULL); + if (r < 0) + return r; + + r = bus_call_method( + bus, + bus_systemd_mgr, + "KillUnit", + &error, + NULL, + "ssi", + vm_pid1, + "leader", + si->ssi_signo); + if (r < 0) + return log_error_errno(r, "Failed to forward signal to PID 1 of the VM: %s", bus_error_message(&error, r)); + log_info("Sent signal %"PRIu32" to the VM's PID 1.", si->ssi_signo); + + return 0; +} + static int on_child_exit(sd_event_source *s, const siginfo_t *si, void *userdata) { sd_event_exit(sd_event_source_get_event(s), 0); return 0; @@ -1200,6 +1315,7 @@ static int generate_ssh_keypair(const char *key_path, const char *key_type) { } static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { + SSHInfo ssh_info; /* Used when talking to pid1 via SSH, but must survive until the function ends. */ _cleanup_(ovmf_config_freep) OvmfConfig *ovmf_config = NULL; _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; _cleanup_free_ char *machine = NULL, *qemu_binary = NULL, *mem = NULL, *trans_scope = NULL, *kernel = NULL; @@ -1985,9 +2101,21 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { return log_error_errno(r, "Failed to setup event loop to handle VSOCK notify events: %m"); } - /* shutdown qemu when we are shutdown */ - (void) sd_event_add_signal(event, NULL, SIGINT | SD_EVENT_SIGNAL_PROCMASK, on_orderly_shutdown, &child_pidref); - (void) sd_event_add_signal(event, NULL, SIGTERM | SD_EVENT_SIGNAL_PROCMASK, on_orderly_shutdown, &child_pidref); + /* If we have the vsock address and the SSH key, ask pid1 inside the guest to shutdown. */ + if (child_cid != VMADDR_CID_ANY && ssh_private_key_path) { + ssh_info = (SSHInfo) { + .cid = child_cid, + .private_key_path = ssh_private_key_path, + .port = 22, + }; + + (void) sd_event_add_signal(event, NULL, SIGINT | SD_EVENT_SIGNAL_PROCMASK, forward_signal_to_vm_pid1, &ssh_info); + (void) sd_event_add_signal(event, NULL, SIGTERM | SD_EVENT_SIGNAL_PROCMASK, forward_signal_to_vm_pid1, &ssh_info); + } else { + /* As a fallback in case SSH cannot be used, send a shutdown signal to the VMM instead. */ + (void) sd_event_add_signal(event, NULL, SIGINT | SD_EVENT_SIGNAL_PROCMASK, on_orderly_shutdown, &child_pidref); + (void) sd_event_add_signal(event, NULL, SIGTERM | SD_EVENT_SIGNAL_PROCMASK, on_orderly_shutdown, &child_pidref); + } (void) sd_event_add_signal(event, NULL, (SIGRTMIN+18) | SD_EVENT_SIGNAL_PROCMASK, sigrtmin18_handler, NULL);