mirror of
https://github.com/libvirt/libvirt.git
synced 2025-02-25 18:55:26 -06:00
tools: Introduce SSH proxy
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> Reviewed-by: Ján Tomko <jtomko@redhat.com>
This commit is contained in:
296
tools/ssh-proxy/ssh-proxy.c
Normal file
296
tools/ssh-proxy/ssh-proxy.c
Normal file
@@ -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/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;
|
||||
}
|
||||
Reference in New Issue
Block a user