
v3 of: https://lists.libvirt.org/archives/list/devel@lists.libvirt.org/thread/EXJPQ... diff to v2: - New naming scheme: user@qemu:system/VM and user@qemu:session/VM for choosing explicit connection URI for fetching VM. Old user@qemu/VM is kept for convenience. Michal Prívozník (3): tools: Introduce SSH proxy docs: Document SSH proxy NEWS: Document SSH proxy feature NEWS.rst | 5 + docs/docs.rst | 3 + docs/meson.build | 1 + docs/nss.rst | 7 + docs/ssh-proxy.rst | 85 ++++++ libvirt.spec.in | 33 +++ meson.build | 16 +- meson_options.txt | 2 + po/POTFILES | 1 + tools/meson.build | 2 + tools/ssh-proxy/30-libvirt-ssh-proxy.conf.in | 6 + tools/ssh-proxy/meson.build | 25 ++ tools/ssh-proxy/ssh-proxy.c | 296 +++++++++++++++++++ 13 files changed, 481 insertions(+), 1 deletion(-) create mode 100644 docs/ssh-proxy.rst create mode 100644 tools/ssh-proxy/30-libvirt-ssh-proxy.conf.in create mode 100644 tools/ssh-proxy/meson.build create mode 100644 tools/ssh-proxy/ssh-proxy.c -- 2.43.2

This allows users to SSH into a domain with a VSOCK device: ssh user@qemu/machineName So far, only QEMU domains are supported AND qemu:///system is looked for the first for 'machineName' followed by qemu:///session. I took an inspiration from SystemD's ssh proxy [1] [2]. To just work out of the box, it requires (yet unreleased) systemd to be running inside the guest to set up a socket activated SSHD on the VSOCK. Alternatively, users can set up the socket activation themselves, or just run a socat that'll forward vsock <-> TCP communication. 1: https://github.com/systemd/systemd/blob/main/src/ssh-generator/ssh-proxy.c 2: https://github.com/systemd/systemd/blob/main/src/ssh-generator/20-systemd-ss... Resolves: https://gitlab.com/libvirt/libvirt/-/issues/579 Signed-off-by: Michal Privoznik <mprivozn@redhat.com> --- libvirt.spec.in | 33 +++ meson.build | 16 +- meson_options.txt | 2 + po/POTFILES | 1 + tools/meson.build | 2 + tools/ssh-proxy/30-libvirt-ssh-proxy.conf.in | 6 + tools/ssh-proxy/meson.build | 25 ++ tools/ssh-proxy/ssh-proxy.c | 296 +++++++++++++++++++ 8 files changed, 380 insertions(+), 1 deletion(-) create mode 100644 tools/ssh-proxy/30-libvirt-ssh-proxy.conf.in create mode 100644 tools/ssh-proxy/meson.build create mode 100644 tools/ssh-proxy/ssh-proxy.c diff --git a/libvirt.spec.in b/libvirt.spec.in index 88c62f6d92..521ecebf05 100644 --- a/libvirt.spec.in +++ b/libvirt.spec.in @@ -91,6 +91,7 @@ # Other optional features %define with_numactl 0%{!?_without_numactl:1} %define with_userfaultfd_sysctl 0%{!?_without_userfaultfd_sysctl:1} +%define with_ssh_proxy 0%{!?_without_ssh_proxy:1} # A few optional bits off by default, we enable later %define with_fuse 0 @@ -903,6 +904,9 @@ Requires: libvirt-daemon-driver-nodedev = %{version}-%{release} Requires: libvirt-daemon-driver-nwfilter = %{version}-%{release} Requires: libvirt-daemon-driver-secret = %{version}-%{release} Requires: libvirt-daemon-driver-storage = %{version}-%{release} + %if %{with_ssh_proxy} +Requires: libvirt-ssh-proxy = %{version}-%{release} + %endif Requires: qemu %description daemon-qemu @@ -931,6 +935,9 @@ Requires: libvirt-daemon-driver-nodedev = %{version}-%{release} Requires: libvirt-daemon-driver-nwfilter = %{version}-%{release} Requires: libvirt-daemon-driver-secret = %{version}-%{release} Requires: libvirt-daemon-driver-storage = %{version}-%{release} + %if %{with_ssh_proxy} +Requires: libvirt-ssh-proxy = %{version}-%{release} + %endif Requires: qemu-kvm %description daemon-kvm @@ -1018,6 +1025,9 @@ Summary: Client side utilities of the libvirt library Requires: libvirt-libs = %{version}-%{release} # Needed by virt-pki-validate script. Requires: gnutls-utils + %if %{with_ssh_proxy} +Recommends: libvirt-ssh-proxy = %{version}-%{release} + %endif # Ensure smooth upgrades Obsoletes: libvirt-bash-completion < 7.3.0 @@ -1100,6 +1110,15 @@ Requires: libvirt-daemon-driver-network = %{version}-%{release} Libvirt plugin for NSS for translating domain names into IP addresses. %endif +%if %{with_ssh_proxy} +%package ssh-proxy +Summary: Libvirt SSH proxy +Requires: libvirt-libs = %{version}-%{release} + +%description ssh-proxy +Allows SSH into domains via VSOCK without need for network. +%endif + %if %{with_mingw32} %package -n mingw32-libvirt Summary: %{summary} @@ -1291,6 +1310,12 @@ exit 1 %define arg_userfaultfd_sysctl -Duserfaultfd_sysctl=disabled %endif +%if %{with_ssh_proxy} + %define arg_ssh_proxy -Dssh_proxy=enabled +%else + %define arg_ssh_proxy -Dssh_proxy=disabled +%endif + %define when %(date +"%%F-%%T") %define where %(hostname) %define who %{?packager}%{!?packager:Unknown} @@ -1372,6 +1397,7 @@ export SOURCE_DATE_EPOCH=$(stat --printf='%Y' %{_specdir}/libvirt.spec) -Dtls_priority=%{tls_priority} \ -Dsysctl_config=enabled \ %{?arg_userfaultfd_sysctl} \ + %{?arg_ssh_proxy} \ %{?enable_werror} \ -Dexpensive_tests=enabled \ -Dinit_script=systemd \ @@ -1456,6 +1482,7 @@ export SOURCE_DATE_EPOCH=$(stat --printf='%Y' %{_specdir}/libvirt.spec) -Dstorage_zfs=disabled \ -Dsysctl_config=disabled \ -Duserfaultfd_sysctl=disabled \ + -Dssh_proxy=disabled \ -Dtests=disabled \ -Dudev=disabled \ -Dwireshark_dissector=disabled \ @@ -2426,6 +2453,12 @@ exit 0 %{_libdir}/libnss_libvirt.so.2 %{_libdir}/libnss_libvirt_guest.so.2 + %if %{with_ssh_proxy} +%files ssh-proxy +%config(noreplace) %{_sysconfdir}/ssh/ssh_config.d/30-libvirt-ssh-proxy.conf +%{_libexecdir}/libvirt-ssh-proxy + %endif + %if %{with_lxc} %files login-shell %attr(4750, root, virtlogin) %{_bindir}/virt-login-shell diff --git a/meson.build b/meson.build index e8b0094b91..f642247794 100644 --- a/meson.build +++ b/meson.build @@ -113,6 +113,11 @@ endif confdir = sysconfdir / meson.project_name() pkgdatadir = datadir / meson.project_name() +sshconfdir = get_option('sshconfdir') +if sshconfdir == '' + sshconfdir = sysconfdir / 'ssh' / 'ssh_config.d' +endif + # generate configmake.h header @@ -690,12 +695,14 @@ if host_machine.system() == 'linux' symbols += [ # process management [ 'sys/syscall.h', 'SYS_pidfd_open' ], + # vsock + [ 'linux/vm_sockets.h', 'struct sockaddr_vm', '#include <sys/socket.h>' ], ] endif foreach symbol : symbols if cc.has_header_symbol(symbol[0], symbol[1], args: '-D_GNU_SOURCE', prefix: symbol.get(2, '')) - conf.set('WITH_DECL_@0@'.format(symbol[1].to_upper()), 1) + conf.set('WITH_DECL_@0@'.format(symbol[1].underscorify().to_upper()), 1) endif endforeach @@ -2033,6 +2040,12 @@ if not get_option('pm_utils').disabled() endif endif +if not get_option('ssh_proxy').disabled() and conf.has('WITH_DECL_STRUCT_SOCKADDR_VM') + conf.set('WITH_SSH_PROXY', 1) +elif get_option('ssh_proxy').enabled() + error('ssh proxy requires vm_sockets.h which wasn\'t found') +endif + if not get_option('sysctl_config').disabled() and host_machine.system() == 'linux' conf.set('WITH_SYSCTL', 1) elif get_option('sysctl_config').enabled() @@ -2344,6 +2357,7 @@ misc_summary = { 'virt-login-shell': conf.has('WITH_LOGIN_SHELL'), 'virt-host-validate': conf.has('WITH_HOST_VALIDATE'), 'TLS priority': conf.get_unquoted('TLS_PRIORITY'), + 'SSH proxy': conf.has('WITH_SSH_PROXY'), 'sysctl config': conf.has('WITH_SYSCTL'), 'userfaultfd sysctl': conf.has('WITH_USERFAULTFD_SYSCTL'), } diff --git a/meson_options.txt b/meson_options.txt index 9d729b3e1f..6258e50c91 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -42,6 +42,7 @@ option('sanlock', type: 'feature', value: 'auto', description: 'sanlock support' option('sasl', type: 'feature', value: 'auto', description: 'sasl support') option('selinux', type: 'feature', value: 'auto', description: 'selinux support') option('selinux_mount', type: 'string', value: '', description: 'set SELinux mount point') +option('sshconfdir', type: 'string', value: '', description: 'directory for SSH client configuration') # dep:pciaccess option('udev', type: 'feature', value: 'auto', description: 'udev support') # dep:driver_remote @@ -126,6 +127,7 @@ option('nbdkit', type: 'feature', value: 'auto', description: 'Build nbdkit stor # dep:nbdkit option('nbdkit_config_default', type: 'feature', value: 'auto', description: 'Whether to use nbdkit storage backend for network disks by default (configurable)') option('pm_utils', type: 'feature', value: 'auto', description: 'use pm-utils for power management') +option('ssh_proxy', type: 'feature', value: 'auto', description: 'Build ssh-proxy for ssh over vsock') option('sysctl_config', type: 'feature', value: 'auto', description: 'Whether to install sysctl configs') # dep:sysctl_config option('userfaultfd_sysctl', type: 'feature', value: 'auto', description: 'Whether to install sysctl config for enabling unprivileged userfaultfd') diff --git a/po/POTFILES b/po/POTFILES index 6fbff4bef2..cec7e4abf4 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -359,6 +359,7 @@ src/vz/vz_utils.c src/vz/vz_utils.h tests/virpolkittest.c tools/libvirt-guests.sh.in +tools/ssh-proxy/ssh-proxy.c tools/virsh-backup.c tools/virsh-checkpoint.c tools/virsh-completer-host.c diff --git a/tools/meson.build b/tools/meson.build index 15be557dfe..1bb84be0be 100644 --- a/tools/meson.build +++ b/tools/meson.build @@ -343,3 +343,5 @@ endif if wireshark_dep.found() subdir('wireshark') endif + +subdir('ssh-proxy') diff --git a/tools/ssh-proxy/30-libvirt-ssh-proxy.conf.in b/tools/ssh-proxy/30-libvirt-ssh-proxy.conf.in new file mode 100644 index 0000000000..33712214e0 --- /dev/null +++ b/tools/ssh-proxy/30-libvirt-ssh-proxy.conf.in @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +Host qemu/* qemu:system/* qemu:session/* + ProxyCommand @libexecdir@/libvirt-ssh-proxy %h %p + ProxyUseFdpass yes + CheckHostIP no diff --git a/tools/ssh-proxy/meson.build b/tools/ssh-proxy/meson.build new file mode 100644 index 0000000000..e9f312fa25 --- /dev/null +++ b/tools/ssh-proxy/meson.build @@ -0,0 +1,25 @@ +if conf.has('WITH_SSH_PROXY') + executable( + 'libvirt-ssh-proxy', + [ + 'ssh-proxy.c' + ], + dependencies: [ + src_dep, + ], + link_with: [ + libvirt_lib, + ], + install: true, + install_dir: libexecdir, + install_rpath: libvirt_rpath, + ) + + configure_file( + input : '30-libvirt-ssh-proxy.conf.in', + output: '@BASENAME@', + configuration: tools_conf, + install: true, + install_dir : sshconfdir, + ) +endif diff --git a/tools/ssh-proxy/ssh-proxy.c b/tools/ssh-proxy/ssh-proxy.c new file mode 100644 index 0000000000..f04160ccad --- /dev/null +++ b/tools/ssh-proxy/ssh-proxy.c @@ -0,0 +1,296 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * For given domain and port create a VSOCK socket and pass it onto STDOUT. + */ + +#include <config.h> + +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/socket.h> +#include <linux/vm_sockets.h> + +#include "internal.h" +#include "virsocket.h" +#include "virstring.h" +#include "virfile.h" +#include "datatypes.h" +#include "virgettext.h" +#include "virxml.h" + +#define VIR_FROM_THIS VIR_FROM_NONE + +#define SYS_ERROR(...) \ +do { \ + int err = errno; \ + fprintf(stderr, "ERROR %s:%d : ", __FUNCTION__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, " : %s\n", g_strerror(err)); \ + fprintf(stderr, "\n"); \ +} while (0) + +#define ERROR(...) \ +do { \ + fprintf(stderr, "ERROR %s:%d : ", __FUNCTION__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n"); \ +} while (0) + +#define HOSTNAME_PREFIX "qemu" +#define QEMU_SYSTEM_URI "qemu:///system" +#define QEMU_SESSION_URI "qemu:///session" + +static void +dummyErrorHandler(void *opaque G_GNUC_UNUSED, + virErrorPtr error G_GNUC_UNUSED) +{ + +} + +static void +printUsage(const char *argv0) +{ + const char *progname; + + if (!(progname = strrchr(argv0, '/'))) + progname = argv0; + else + progname++; + + printf(_("\n" + "Usage:\n" + "%1$s hostname port\n" + "\n" + "Hostname should be in one of the following forms:\n" + "\n" + " qemu:system/$domname\t\tfor domains under qemu:///system\n" + " qemu:session/$domname\t\tfor domains under qemu:///session\n" + " qemu/$domname\t\t\ttries looking up $domname under system followed by session URI\n"), + progname); +} + +static int +parseArgs(int argc, + char *argv[], + const char **uriRet, + const char **domname, + unsigned int *port) +{ + const char *uri = NULL; + + /* Accepted URIs are: + * + * qemu/virtulMachine + * qemu:system/virtualMachine + * qemu:session/virtualMachine + * + * The last two result in system or session connection URIs passed to + * virConnectOpen(), the first one tries to find the machine under system + * connection first, followed by session connection. + */ + if (argc != 3 || + !(uri = STRSKIP(argv[1], HOSTNAME_PREFIX))) { + ERROR(_("Bad usage")); + printUsage(argv[0]); + return -1; + } + + if (*uri == ':') { + const char *tmp = NULL; + + uri++; + if ((tmp = STRSKIP(uri, "system"))) { + *uriRet = QEMU_SYSTEM_URI; + } else if ((tmp = STRSKIP(uri, "session"))) { + *uriRet = QEMU_SESSION_URI; + } else { + ERROR(_("Unknown connection URI: '%1$s'"), uri); + printUsage(argv[0]); + return -1; + } + + uri = tmp; + } else { + *uriRet = NULL; + } + + if (!(*domname = STRSKIP(uri, "/")) || + **domname == '\0') { + ERROR(_("Bad usage")); + printUsage(argv[0]); + return -1; + } + + if (virStrToLong_ui(argv[2], NULL, 10, port) < 0) { + ERROR(_("Unable to parse port: %1$s"), argv[2]); + printUsage(argv[0]); + return -1; + } + + return 0; +} + + +#define VSOCK_CID_XPATH "/domain/devices/vsock/cid" + +static int +extractCID(virDomainPtr dom, + unsigned long long *cidRet) +{ + g_autofree char *domxml = NULL; + g_autoptr(xmlDoc) doc = NULL; + g_autoptr(xmlXPathContext) ctxt = NULL; + g_autofree xmlNodePtr *nodes = NULL; + int nnodes = 0; + size_t i; + + if (!(domxml = virDomainGetXMLDesc(dom, 0))) + return -1; + + doc = virXMLParseStringCtxtWithIndent(domxml, "domain", &ctxt); + if (!doc) + return -1; + + if ((nnodes = virXPathNodeSet(VSOCK_CID_XPATH, ctxt, &nodes)) < 0) { + return -1; + } + + for (i = 0; i < nnodes; i++) { + unsigned long long cid; + + if (virXMLPropULongLong(nodes[i], "address", 10, 0, &cid) > 0) { + *cidRet = cid; + return 0; + } + } + + return -1; +} + +#undef VSOCK_CID_XPATH + + +static int +lookupDomainAndFetchCID(const char *uri, + const char *domname, + unsigned long long *cid) +{ + g_autoptr(virConnect) conn = NULL; + g_autoptr(virDomain) dom = NULL; + + if (!(conn = virConnectOpenReadOnly(uri))) + return -1; + + dom = virDomainLookupByName(conn, domname); + if (!dom) + dom = virDomainLookupByUUIDString(conn, domname); + if (!dom) { + int id; + + if (virStrToLong_i(domname, NULL, 10, &id) >= 0) + dom = virDomainLookupByID(conn, id); + } + if (!dom) + return -1; + + return extractCID(dom, cid); +} + + +static int +findDomain(const char *domname, + unsigned long long *cid) +{ + const char *uris[] = {QEMU_SYSTEM_URI, QEMU_SESSION_URI}; + const uid_t userid = geteuid(); + size_t i; + + for (i = 0; i < G_N_ELEMENTS(uris); i++) { + if (userid == 0 && + STREQ(uris[i], "qemu:///session")) { + continue; + } + + if (lookupDomainAndFetchCID(uris[i], domname, cid) >= 0) + return 0; + } + + return -1; +} + + +static int +processVsock(const char *uri, + const char *domname, + unsigned int port) +{ + struct sockaddr_vm sa = { + .svm_family = AF_VSOCK, + .svm_port = port, + }; + VIR_AUTOCLOSE fd = -1; + unsigned long long cid = -1; + + if (uri) { + lookupDomainAndFetchCID(uri, domname, &cid); + } else { + findDomain(domname, &cid); + } + + if (cid == -1) { + ERROR(_("No usable vsock found")); + return -1; + } + + sa.svm_cid = cid; + + fd = socket(AF_VSOCK, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (fd < 0) { + SYS_ERROR(_("Failed to allocate AF_VSOCK socket")); + return -1; + } + + if (connect(fd, (const struct sockaddr *)&sa, sizeof(sa)) < 0) { + SYS_ERROR(_("Failed to connect to vsock (cid=%1$llu port=%2$u)"), + cid, port); + return -1; + } + + /* OpenSSH wants us to send a single byte along with the file descriptor, + * hence do so. */ + if (virSocketSendFD(STDOUT_FILENO, fd) < 0) { + SYS_ERROR(_("Failed to send file descriptor %1$d"), fd); + return -1; + } + + return 0; +} + +int main(int argc, char *argv[]) +{ + const char *uri = NULL; + const char *domname = NULL; + unsigned int port; + + if (virGettextInitialize() < 0) + return EXIT_FAILURE; + + if (virInitialize() < 0) { + ERROR(_("Failed to initialize libvirt")); + return EXIT_FAILURE; + } + + virSetErrorFunc(NULL, dummyErrorHandler); + + if (parseArgs(argc, argv, &uri, &domname, &port) < 0) + return EXIT_FAILURE; + + if (processVsock(uri, domname, port) < 0) + return EXIT_FAILURE; + + return EXIT_SUCCESS; +} -- 2.43.2

On a Thursday in 2024, Michal Privoznik wrote:
This allows users to SSH into a domain with a VSOCK device:
ssh user@qemu/machineName
So far, only QEMU domains are supported AND qemu:///system is looked for the first for 'machineName' followed by qemu:///session. I took an inspiration from SystemD's ssh proxy
s/SystemD/systemd/ See the "Spelling" section of https://www.freedesktop.org/wiki/Software/systemd/
[1] [2].
To just work out of the box, it requires (yet unreleased) systemd to be running inside the guest to set up a socket activated SSHD on the VSOCK. Alternatively, users can set up the socket activation themselves, or just run a socat that'll forward vsock <-> TCP communication.
1: https://github.com/systemd/systemd/blob/main/src/ssh-generator/ssh-proxy.c 2: https://github.com/systemd/systemd/blob/main/src/ssh-generator/20-systemd-ss...
Resolves: https://gitlab.com/libvirt/libvirt/-/issues/579 Signed-off-by: Michal Privoznik <mprivozn@redhat.com> --- libvirt.spec.in | 33 +++ meson.build | 16 +- meson_options.txt | 2 + po/POTFILES | 1 + tools/meson.build | 2 + tools/ssh-proxy/30-libvirt-ssh-proxy.conf.in | 6 + tools/ssh-proxy/meson.build | 25 ++ tools/ssh-proxy/ssh-proxy.c | 296 +++++++++++++++++++ 8 files changed, 380 insertions(+), 1 deletion(-) create mode 100644 tools/ssh-proxy/30-libvirt-ssh-proxy.conf.in create mode 100644 tools/ssh-proxy/meson.build create mode 100644 tools/ssh-proxy/ssh-proxy.c
diff --git a/tools/ssh-proxy/ssh-proxy.c b/tools/ssh-proxy/ssh-proxy.c new file mode 100644 index 0000000000..f04160ccad --- /dev/null +++ b/tools/ssh-proxy/ssh-proxy.c +static int +parseArgs(int argc, + char *argv[], + const char **uriRet, + const char **domname, + unsigned int *port) +{ + const char *uri = NULL; + + /* Accepted URIs are: + * + * qemu/virtulMachine
s/virtul/virtual/
+ * qemu:system/virtualMachine + * qemu:session/virtualMachine + * + * The last two result in system or session connection URIs passed to + * virConnectOpen(), the first one tries to find the machine under system + * connection first, followed by session connection. + */ + if (argc != 3 || + !(uri = STRSKIP(argv[1], HOSTNAME_PREFIX))) { + ERROR(_("Bad usage")); + printUsage(argv[0]); + return -1; + } +
Reviewed-by: Ján Tomko <jtomko@redhat.com> Jano

Signed-off-by: Michal Privoznik <mprivozn@redhat.com> --- docs/docs.rst | 3 ++ docs/meson.build | 1 + docs/nss.rst | 7 ++++ docs/ssh-proxy.rst | 85 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+) create mode 100644 docs/ssh-proxy.rst diff --git a/docs/docs.rst b/docs/docs.rst index f57164b9e3..1a958e9cc7 100644 --- a/docs/docs.rst +++ b/docs/docs.rst @@ -47,6 +47,9 @@ Deployment / operation `Hooks <hooks.html>`__ Hooks for system specific management +`SSH Proxy <ssh-proxy.html>`__ + Enable SSH into guests over a VSOCK + `NSS module <nss.html>`__ Enable domain host name translation to IP addresses diff --git a/docs/meson.build b/docs/meson.build index 2dfe98ef62..53b518f987 100644 --- a/docs/meson.build +++ b/docs/meson.build @@ -97,6 +97,7 @@ docs_rst_files = [ 'python', 'remote', 'securityprocess', + 'ssh-proxy', 'storage', 'strategy', 'styleguide', diff --git a/docs/nss.rst b/docs/nss.rst index 8f98330221..53955a3278 100644 --- a/docs/nss.rst +++ b/docs/nss.rst @@ -152,3 +152,10 @@ If there's no record for either of the aforementioned commands, it's very likely that NSS module won't find anything and vice versa. As of ``v3.0.0`` libvirt provides ``libvirt_guest`` NSS module that doesn't have this limitation. However, the statement is still true for the ``libvirt`` NSS module. + +Alternatives +------------ + +As of ``v10.3.0`` libvirt implements an `SSH proxy <ssh-proxy.html>`__ which +doesn't require any network interface to SSH into the guest as SSH flows +through a VSOCK device. diff --git a/docs/ssh-proxy.rst b/docs/ssh-proxy.rst new file mode 100644 index 0000000000..014737e967 --- /dev/null +++ b/docs/ssh-proxy.rst @@ -0,0 +1,85 @@ +================= +Libvirt SSH proxy +================= + +Sometimes it's necessary to run some commands inside a guest. While libvirt +already provides a `NSS module <nss.html>`__ that can translate guest name to +IP address it has some limitations (e.g. guest has to have a network interface +plugged into a libvirt managed network). To resolve some of these limitations, +libvirt offers a SSH proxy. It consists of a SSH client config file +(``/etc/ssh/ssh_config.d/30-libvirt-ssh-proxy.conf``) and a small binary. Both +are automatically installed by ``libvirt-ssh-proxy`` package which is dragged +in by ``libvirt-client``, ``libvirt-daemon-qemu`` and/or ``daemon-kvm`` RPM +packages. After running either of: + +:: + + ssh user@qemu:system/virtualMachine + ssh user@qemu:session/virtualMachine + +the configuration file instructs SSH client to start the binary helper which +finds a VSOCK device inside the ``virtualMachine`` and establishes a connection +to it. + +For now, only QEMU domains are implemented and the lookup of the +``virtualMachine`` is done under ``qemu:///system`` URI for ``qemu:system`` or +under ``qemu:///session`` URI for ``qemu:session``. + +For convenience, there's also another alternative: + +:: + + ssh user@qemu/virtualMachine + +where the ``virtualMachine`` is looked up under ``qemu:///system`` first, +possibly followed by ``qemu:///session`` (for cases where ssh client runs as +non-root, since there's no ``qemu:///session`` for root). + +Accepted values for ``virtualMachine`` are: domain name (as reported by e.g. +`virsh list`), domain UUID and finally domain ID. + +Guest OS requirements +--------------------- + +It is obvious that the SSH daemon inside the guest needs to be configured to +listen for incoming connections on a VSOCK. There are couple of ways to achieve +this: + +* Run systemd-v256 or newer inside the guest. + + In this release, systemd started to deploy ``systemd-ssh-generator`` which + should configure socket activation for SSHD automagically. + +* Set up socket activation for VSOCK. + + We can take an inspiration in the unit file generated by + ``systemd-ssh-generator``: + +:: + + [Unit] + Description=OpenSSH Server Socket (systemd-ssh-generator, AF_VSOCK) + Documentation=man:systemd-ssh-generator(8) + Wants=ssh-access.target + Before=ssh-access.target + + [Socket] + ListenStream=vsock::22 + Accept=yes + PollLimitIntervalSec=30s + PollLimitBurst=50 + +* Run a service that forwards VSOCK <=> SSHD communication + + For instance: + +:: + + socat VSOCK-LISTEN:22,reuseaddr,fork TCP:localhost:22 + +Libvirt domain XML configuration +-------------------------------- + +Since the SSH proxy uses a VSOCK to communicate with the SSH daemon running +inside the guest, it is a must to configure VSOCK in the `domain XML +<formatdomain.html#vsock>`__. -- 2.43.2

On a Thursday in 2024, Michal Privoznik wrote:
Signed-off-by: Michal Privoznik <mprivozn@redhat.com> --- docs/docs.rst | 3 ++ docs/meson.build | 1 + docs/nss.rst | 7 ++++ docs/ssh-proxy.rst | 85 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+) create mode 100644 docs/ssh-proxy.rst
diff --git a/docs/ssh-proxy.rst b/docs/ssh-proxy.rst new file mode 100644 index 0000000000..014737e967 --- /dev/null +++ b/docs/ssh-proxy.rst @@ -0,0 +1,85 @@ +================= +Libvirt SSH proxy +================= + +Sometimes it's necessary to run some commands inside a guest. While libvirt +already provides a `NSS module <nss.html>`__ that can translate guest name to +IP address it has some limitations (e.g. guest has to have a network interface +plugged into a libvirt managed network). To resolve some of these limitations,
I would hyphenate this: libvirt-managed
+libvirt offers a SSH proxy. It consists of a SSH client config file +(``/etc/ssh/ssh_config.d/30-libvirt-ssh-proxy.conf``) and a small binary. Both +are automatically installed by ``libvirt-ssh-proxy`` package which is dragged +in by ``libvirt-client``, ``libvirt-daemon-qemu`` and/or ``daemon-kvm`` RPM +packages. After running either of: + +:: + + ssh user@qemu:system/virtualMachine + ssh user@qemu:session/virtualMachine + +the configuration file instructs SSH client to start the binary helper which +finds a VSOCK device inside the ``virtualMachine`` and establishes a connection +to it.
Reviewed-by: Ján Tomko <jtomko@redhat.com> Jano

Signed-off-by: Michal Privoznik <mprivozn@redhat.com> --- NEWS.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/NEWS.rst b/NEWS.rst index d72c15bf10..ef0e4f8b51 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -22,6 +22,11 @@ v10.4.0 (unreleased) It is now possible to set on/off ``ras`` feature in the domain XML for virt (Arm) machine type as ``<ras state='on'/>``. + * SSH proxy for VM + + Libvirt now installs a binary helper that allows connecting to QEMU domains + via SSH using the following scheme: ``ssh user@qemu/virtualMachine``. + * **Improvements** * **Bug fixes** -- 2.43.2
participants (2)
-
Ján Tomko
-
Michal Privoznik