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 ' ], ] 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..e60c58d57f --- /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 + +#include +#include +#include +#include +#include +#include +#include + +#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/virtualMachine + * 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; +}