[PATCH 2/4] tools: Introduce SSH proxy

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



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-ssh-proxy.conf.in

Resolves: https://gitlab.com/libvirt/libvirt/-/issues/579
Signed-off-by: Michal Privoznik <mprivozn@xxxxxxxxxx>
---
 libvirt.spec.in                              |  27 +++
 meson.build                                  |  16 +-
 meson_options.txt                            |   2 +
 po/POTFILES                                  |   1 +
 tools/meson.build                            |   2 +
 tools/ssh-proxy/30-libvirt-ssh-proxy.conf.in |  10 +
 tools/ssh-proxy/meson.build                  |  25 ++
 tools/ssh-proxy/ssh-proxy.c                  | 233 +++++++++++++++++++
 8 files changed, 315 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 64018192b6..9069b3792f 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
@@ -1017,6 +1018,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
@@ -1099,6 +1103,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}
@@ -1290,6 +1303,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}
@@ -1371,6 +1390,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 \
@@ -1455,6 +1475,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 \
@@ -2425,6 +2446,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 1518afa1cb..b19f9b1ed1 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 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..9a022826f7
--- /dev/null
+++ b/tools/ssh-proxy/30-libvirt-ssh-proxy.conf.in
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+Host qemu/*
+    ProxyCommand @libexecdir@/libvirt-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
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..90f30d4f49
--- /dev/null
+++ b/tools/ssh-proxy/ssh-proxy.c
@@ -0,0 +1,233 @@
+/*
+ * 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;
+    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 (!(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
_______________________________________________
Devel mailing list -- devel@xxxxxxxxxxxxxxxxx
To unsubscribe send an email to devel-leave@xxxxxxxxxxxxxxxxx




[Index of Archives]     [Virt Tools]     [Libvirt Users]     [Lib OS Info]     [Fedora Users]     [Fedora Desktop]     [Fedora SELinux]     [Big List of Linux Books]     [Yosemite News]     [KDE Users]     [Fedora Tools]

  Powered by Linux