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...
Resolves:
https://gitlab.com/libvirt/libvirt/-/issues/579
Signed-off-by: Michal Privoznik <mprivozn(a)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 | 239 +++++++++++++++++++
8 files changed, 323 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(a)'.format(symbol[1].to_upper()), 1)
+ conf.set('WITH_DECL_@0(a)'.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 ed91d97abf..35af27306d 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -40,6 +40,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')
option('udev', type: 'feature', value: 'auto', description:
'udev support')
option('wireshark_dissector', type: 'feature', value: 'auto',
description: 'wireshark support')
option('wireshark_plugindir', type: 'string', value: '',
description: 'wireshark plugins directory for use when installing wireshark
plugin')
@@ -107,6 +108,7 @@ option('numad', type: 'feature', value:
'auto', description: 'use numad to manag
option('nbdkit', type: 'feature', value: 'auto', description:
'Build nbdkit storage backend')
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')
option('userfaultfd_sysctl', type: 'feature', value: 'auto',
description: 'Whether to install sysctl config for enabling unprivileged
userfaultfd')
option('tls_priority', type: 'string', value: 'NORMAL',
description: 'set the default TLS session priority string')
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..cd19bdbc95
--- /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/*
+ 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..207d0488fb
--- /dev/null
+++ b/tools/ssh-proxy/ssh-proxy.c
@@ -0,0 +1,239 @@
+/*
+ * 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/"
+
+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 form '%2$s$domname'\n"),
+ progname, HOSTNAME_PREFIX);
+}
+
+static int
+parseArgs(int argc,
+ char *argv[],
+ const char **domname,
+ unsigned int *port)
+{
+ if (argc != 3 ||
+ !(*domname = STRSKIP(argv[1], HOSTNAME_PREFIX))) {
+ 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;
+}
+
+static virDomainPtr
+lookupDomain(const char *domname,
+ const char *uri,
+ virConnectPtr *connRet)
+{
+ g_autoptr(virConnect) conn = NULL;
+ virDomainPtr dom = NULL;
+
+ if (!(conn = virConnectOpenReadOnly(uri)))
+ return NULL;
+
+ 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 NULL;
+
+ *connRet = g_steal_pointer(&conn);
+ return dom;
+}
+
+
+#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
+processVsock(const char *domname,
+ unsigned int port)
+{
+ const char *uris[] = {"qemu:///system", "qemu:///session"};
+ struct sockaddr_vm sa = {
+ .svm_family = AF_VSOCK,
+ .svm_port = port,
+ };
+ VIR_AUTOCLOSE fd = -1;
+ const uid_t userid = geteuid();
+ unsigned long long cid = -1;
+ size_t i;
+
+ for (i = 0; i < G_N_ELEMENTS(uris); i++) {
+ g_autoptr(virConnect) conn = NULL;
+ g_autoptr(virDomain) dom = NULL;
+
+ if (userid == 0 &&
+ STREQ(uris[i], "qemu:///session")) {
+ continue;
+ }
+
+ if (!(dom = lookupDomain(domname, uris[i], &conn)))
+ continue;
+
+ if (extractCID(dom, &cid) >= 0)
+ break;
+ }
+
+ 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 *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, &domname, &port) < 0)
+ return EXIT_FAILURE;
+
+ if (processVsock(domname, port) < 0)
+ return EXIT_FAILURE;
+
+ return EXIT_SUCCESS;
+}
--
2.43.2