[PATCH v2 1/3] tools: Introduce SSH proxy

Michal Privoznik posted 3 patches 1 year, 9 months ago
There is a newer version of this series
[PATCH v2 1/3] tools: Introduce SSH proxy
Posted by Michal Privoznik 1 year, 9 months ago
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@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@'.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..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
_______________________________________________
Devel mailing list -- devel@lists.libvirt.org
To unsubscribe send an email to devel-leave@lists.libvirt.org
Re: [PATCH v2 1/3] tools: Introduce SSH proxy
Posted by Daniel P. Berrangé 1 year, 9 months ago
On Tue, May 07, 2024 at 01:08:00PM +0200, 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
> [1] [2].

Thinking again, I'm not too comfortable about having
both system and session on the same namespace, as when
there is an inevitable naming clash, it is tedious to
resolve (no human likes using UUIDs).

How about allowing:

   ssh user@qemu:system/machineName
   ssh user@qemu:session/machineName

as unambiguous options, while leaving

   ssh user@qemu/machineName

as the simplified "(mostly) do the right thing" option

> 
> 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@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
> 

With regards,
Daniel
-- 
|: https://berrange.com      -o-    https://www.flickr.com/photos/dberrange :|
|: https://libvirt.org         -o-            https://fstop138.berrange.com :|
|: https://entangle-photo.org    -o-    https://www.instagram.com/dberrange :|
_______________________________________________
Devel mailing list -- devel@lists.libvirt.org
To unsubscribe send an email to devel-leave@lists.libvirt.org