[PATCH v3 1/3] 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                              |  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
_______________________________________________
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