[libvirt] [PATCH v2 0/2] New libssh transport

Hi, this series introduces a new libssh transport in libvirt, based on the libssh C library. This library supports what libssh2 does, and more: - easier API for known_hosts handling (there's a ticket upstream to request extensions for it, but what is implemented now works well) - potential GSSAPI authentication (not enabled yet because of a libssh bug [1]) - easier API for ssh-agent support The implementation for the new transport is based on the libssh2 one, hence it shares origin and style. [1] https://red.libssh.org/issues/242 Thanks, Changes from v1 to v2: - implemented keyboard interactive - code polish, and fixes Pino Toscano (2): virNetSocket: allow to not close FD libssh_transport: add new libssh-based transport config-post.h | 2 + configure.ac | 3 + include/libvirt/virterror.h | 2 + m4/virt-libssh.m4 | 26 + src/Makefile.am | 21 +- src/libvirt_libssh.syms | 22 + src/remote/remote_driver.c | 41 ++ src/rpc/virnetclient.c | 123 ++++ src/rpc/virnetclient.h | 13 + src/rpc/virnetlibsshsession.c | 1424 +++++++++++++++++++++++++++++++++++++++++ src/rpc/virnetlibsshsession.h | 80 +++ src/rpc/virnetsocket.c | 184 +++++- src/rpc/virnetsocket.h | 13 + src/util/virerror.c | 9 +- 14 files changed, 1959 insertions(+), 4 deletions(-) create mode 100644 m4/virt-libssh.m4 create mode 100644 src/libvirt_libssh.syms create mode 100644 src/rpc/virnetlibsshsession.c create mode 100644 src/rpc/virnetlibsshsession.h -- 2.7.4

Add an internal variable to mark the FD as "not owned" by the virNetSocket, in case the internal implementation takes the actual ownership of the descriptor; this avoids a warning when closing the socket, as the FD would be invalid. --- src/rpc/virnetsocket.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/rpc/virnetsocket.c b/src/rpc/virnetsocket.c index 405f5ba..05f20a5 100644 --- a/src/rpc/virnetsocket.c +++ b/src/rpc/virnetsocket.c @@ -77,6 +77,7 @@ struct _virNetSocket { pid_t pid; int errfd; bool client; + bool ownsFd; /* Event callback fields */ virNetSocketIOFunc func; @@ -248,6 +249,7 @@ static virNetSocketPtr virNetSocketNew(virSocketAddrPtr localAddr, sock->errfd = errfd; sock->pid = pid; sock->watch = -1; + sock->ownsFd = true; /* Disable nagle for TCP sockets */ if (sock->localAddr.data.sa.sa_family == AF_INET || @@ -1202,7 +1204,8 @@ void virNetSocketDispose(void *obj) virObjectUnref(sock->sshSession); #endif - VIR_FORCE_CLOSE(sock->fd); + if (sock->ownsFd) + VIR_FORCE_CLOSE(sock->fd); VIR_FORCE_CLOSE(sock->errfd); virProcessAbort(sock->pid); -- 2.7.4

Implement a new libssh transport, which uses libssh to communicate with remote hosts, and use it in virNetSockets. This new transport supports all the common ssh authentication methods, making use of libvirt's auth callbacks for interaction with the user. Most of the functionalities and implementation are based on the libssh2 transport. --- config-post.h | 2 + configure.ac | 3 + include/libvirt/virterror.h | 2 + m4/virt-libssh.m4 | 26 + src/Makefile.am | 21 +- src/libvirt_libssh.syms | 22 + src/remote/remote_driver.c | 41 ++ src/rpc/virnetclient.c | 123 ++++ src/rpc/virnetclient.h | 13 + src/rpc/virnetlibsshsession.c | 1424 +++++++++++++++++++++++++++++++++++++++++ src/rpc/virnetlibsshsession.h | 80 +++ src/rpc/virnetsocket.c | 179 ++++++ src/rpc/virnetsocket.h | 13 + src/util/virerror.c | 9 +- 14 files changed, 1955 insertions(+), 3 deletions(-) create mode 100644 m4/virt-libssh.m4 create mode 100644 src/libvirt_libssh.syms create mode 100644 src/rpc/virnetlibsshsession.c create mode 100644 src/rpc/virnetlibsshsession.h diff --git a/config-post.h b/config-post.h index 6eb63db..090cc28 100644 --- a/config-post.h +++ b/config-post.h @@ -36,6 +36,7 @@ # undef WITH_DTRACE_PROBES # undef WITH_GNUTLS # undef WITH_GNUTLS_GCRYPT +# undef WITH_LIBSSH # undef WITH_MACVTAP # undef WITH_NUMACTL # undef WITH_SASL @@ -60,6 +61,7 @@ # undef WITH_DTRACE_PROBES # undef WITH_GNUTLS # undef WITH_GNUTLS_GCRYPT +# undef WITH_LIBSSH # undef WITH_MACVTAP # undef WITH_NUMACTL # undef WITH_SASL diff --git a/configure.ac b/configure.ac index dfc536f..f526f41 100644 --- a/configure.ac +++ b/configure.ac @@ -218,6 +218,7 @@ if test "$with_remote" = "no" ; then with_gnutls=no with_ssh2=no with_sasl=no + with_libssh=no fi # Stateful drivers are useful only when building the daemon. if test "$with_libvirtd" = "no" ; then @@ -260,6 +261,7 @@ LIBVIRT_CHECK_UDEV LIBVIRT_CHECK_WIRESHARK LIBVIRT_CHECK_NSS LIBVIRT_CHECK_YAJL +LIBVIRT_CHECK_LIBSSH AC_MSG_CHECKING([for CPUID instruction]) AC_COMPILE_IFELSE([AC_LANG_PROGRAM( @@ -2786,6 +2788,7 @@ LIBVIRT_RESULT_DBUS LIBVIRT_RESULT_FUSE LIBVIRT_RESULT_GLUSTER LIBVIRT_RESULT_HAL +LIBVIRT_RESULT_LIBSSH LIBVIRT_RESULT_NETCF LIBVIRT_RESULT_NUMACTL LIBVIRT_RESULT_OPENWSMAN diff --git a/include/libvirt/virterror.h b/include/libvirt/virterror.h index efe83aa..2efee8f 100644 --- a/include/libvirt/virterror.h +++ b/include/libvirt/virterror.h @@ -131,6 +131,7 @@ typedef enum { VIR_FROM_XENXL = 64, /* Error from Xen xl config code */ VIR_FROM_PERF = 65, /* Error from perf */ + VIR_FROM_LIBSSH = 66, /* Error from libssh connection transport */ # ifdef VIR_ENUM_SENTINELS VIR_ERR_DOMAIN_LAST @@ -317,6 +318,7 @@ typedef enum { VIR_ERR_NO_CLIENT = 96, /* Client was not found */ VIR_ERR_AGENT_UNSYNCED = 97, /* guest agent replies with wrong id to guest-sync command */ + VIR_ERR_LIBSSH = 98, /* error in libssh transport driver */ } virErrorNumber; /** diff --git a/m4/virt-libssh.m4 b/m4/virt-libssh.m4 new file mode 100644 index 0000000..88ece21 --- /dev/null +++ b/m4/virt-libssh.m4 @@ -0,0 +1,26 @@ +dnl The libssh.so library +dnl +dnl Copyright (C) 2016 Red Hat, Inc. +dnl +dnl This library is free software; you can redistribute it and/or +dnl modify it under the terms of the GNU Lesser General Public +dnl License as published by the Free Software Foundation; either +dnl version 2.1 of the License, or (at your option) any later version. +dnl +dnl This library is distributed in the hope that it will be useful, +dnl but WITHOUT ANY WARRANTY; without even the implied warranty of +dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +dnl Lesser General Public License for more details. +dnl +dnl You should have received a copy of the GNU Lesser General Public +dnl License along with this library. If not, see +dnl <http://www.gnu.org/licenses/>. +dnl + +AC_DEFUN([LIBVIRT_CHECK_LIBSSH],[ + LIBVIRT_CHECK_PKG([LIBSSH], [libssh], [0.7]) +]) + +AC_DEFUN([LIBVIRT_RESULT_LIBSSH],[ + LIBVIRT_RESULT_LIB([LIBSSH]) +]) diff --git a/src/Makefile.am b/src/Makefile.am index 8ee5567..4a6ccf3 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -2126,6 +2126,12 @@ else ! WITH_ATOMIC_OPS_PTHREAD SYM_FILES += $(srcdir)/libvirt_atomic.syms endif ! WITH_ATOMIC_OPS_PTHREAD +if WITH_LIBSSH +USED_SYM_FILES += $(srcdir)/libvirt_libssh.syms +else ! WITH_LIBSSH +SYM_FILES += $(srcdir)/libvirt_libssh.syms +endif ! WITH_LIBSSH + EXTRA_DIST += \ libvirt_public.syms \ libvirt_lxc.syms \ @@ -2203,7 +2209,8 @@ libvirt_admin_la_CFLAGS += \ $(YAJL_CFLAGS) \ $(SSH2_CFLAGS) \ $(SASL_CFLAGS) \ - $(GNUTLS_CFLAGS) + $(GNUTLS_CFLAGS) \ + $(LIBSSH_CFLAGS) libvirt_admin_la_LIBADD += \ $(CAPNG_LIBS) \ @@ -2212,7 +2219,8 @@ libvirt_admin_la_LIBADD += \ $(LIBXML_LIBS) \ $(SSH2_LIBS) \ $(SASL_LIBS) \ - $(GNUTLS_LIBS) + $(GNUTLS_LIBS) \ + $(LIBSSH_LIBS) ADMIN_SYM_FILES = $(srcdir)/libvirt_admin_private.syms @@ -2789,16 +2797,25 @@ else ! WITH_SASL EXTRA_DIST += \ rpc/virnetsaslcontext.h rpc/virnetsaslcontext.c endif ! WITH_SASL +if WITH_LIBSSH +libvirt_net_rpc_la_SOURCES += \ + rpc/virnetlibsshsession.h rpc/virnetlibsshsession.c +else ! WITH_LIBSSH +EXTRA_DIST += \ + rpc/virnetlibsshsession.h rpc/virnetlibsshsession.c +endif ! WITH_LIBSSH libvirt_net_rpc_la_CFLAGS = \ $(GNUTLS_CFLAGS) \ $(SASL_CFLAGS) \ $(SSH2_CFLAGS) \ + $(LIBSSH_CFLAGS) \ $(XDR_CFLAGS) \ $(AM_CFLAGS) libvirt_net_rpc_la_LDFLAGS = \ $(GNUTLS_LIBS) \ $(SASL_LIBS) \ $(SSH2_LIBS)\ + $(LIBSSH_LIBS) \ $(SECDRIVER_LIBS) \ $(AM_LDFLAGS) \ $(NULL) diff --git a/src/libvirt_libssh.syms b/src/libvirt_libssh.syms new file mode 100644 index 0000000..870050b --- /dev/null +++ b/src/libvirt_libssh.syms @@ -0,0 +1,22 @@ +# +# libssh session - specific symbols +# + +# rpc/virnetlibsshsession.h +virNetLibsshChannelRead; +virNetLibsshChannelWrite; +virNetLibsshSessionAuthAddAgentAuth; +virNetLibsshSessionAuthAddKeyboardAuth; +virNetLibsshSessionAuthAddPasswordAuth; +virNetLibsshSessionAuthAddPrivKeyAuth; +virNetLibsshSessionAuthReset; +virNetLibsshSessionAuthSetCallback; +virNetLibsshSessionConnect; +virNetLibsshSessionHasCachedData; +virNetLibsshSessionSetChannelCommand; +virNetLibsshSessionSetHostKeyVerification; + +# Let emacs know we want case-insensitive sorting +# Local Variables: +# sort-fold-case: t +# End: diff --git a/src/remote/remote_driver.c b/src/remote/remote_driver.c index a3cd7cd..db2bdd4 100644 --- a/src/remote/remote_driver.c +++ b/src/remote/remote_driver.c @@ -673,6 +673,7 @@ remoteConnectSupportsFeatureUnlocked(virConnectPtr conn, * - xxx:/// -> UNIX domain socket * - xxx+ssh:/// -> SSH connection (legacy) * - xxx+libssh2:/// -> SSH connection (using libssh2) + * - xxx+libssh:/// -> SSH connection (using libssh) */ static int doRemoteOpen(virConnectPtr conn, @@ -689,6 +690,7 @@ doRemoteOpen(virConnectPtr conn, trans_libssh2, trans_ext, trans_tcp, + trans_libssh, } transport; #ifndef WIN32 char *daemonPath = NULL; @@ -736,6 +738,8 @@ doRemoteOpen(virConnectPtr conn, transport = trans_ext; } else if (STRCASEEQ(transport_str, "tcp")) { transport = trans_tcp; + } else if (STRCASEEQ(transport_str, "libssh")) { + transport = trans_libssh; } else { virReportError(VIR_ERR_INVALID_ARG, "%s", _("remote_open: transport in URL not recognised " @@ -959,6 +963,43 @@ doRemoteOpen(virConnectPtr conn, priv->is_secure = 1; break; + case trans_libssh: + if (!sockname) { + /* Right now we don't support default session connections */ + if (STREQ_NULLABLE(conn->uri->path, "/session")) { + virReportError(VIR_ERR_OPERATION_UNSUPPORTED, "%s", + _("Connecting to session instance without " + "socket path is not supported by the libssh " + "connection driver")); + goto failed; + } + + if (VIR_STRDUP(sockname, + flags & VIR_DRV_OPEN_REMOTE_RO ? + LIBVIRTD_PRIV_UNIX_SOCKET_RO : LIBVIRTD_PRIV_UNIX_SOCKET) < 0) + goto failed; + } + + VIR_DEBUG("Starting libssh session"); + + priv->client = virNetClientNewLibssh(priv->hostname, + port, + AF_UNSPEC, + username, + keyfile, + knownHosts, + knownHostsVerify, + sshauth, + netcat, + sockname, + auth, + conn->uri); + if (!priv->client) + goto failed; + + priv->is_secure = 1; + break; + #ifndef WIN32 case trans_unix: if (!sockname) { diff --git a/src/rpc/virnetclient.c b/src/rpc/virnetclient.c index 361dc1a..b296aac 100644 --- a/src/rpc/virnetclient.c +++ b/src/rpc/virnetclient.c @@ -505,6 +505,129 @@ virNetClientPtr virNetClientNewLibSSH2(const char *host, } #undef DEFAULT_VALUE +#define DEFAULT_VALUE(VAR, VAL) \ + if (!VAR) \ + VAR = VAL; +virNetClientPtr virNetClientNewLibssh(const char *host, + const char *port, + int family, + const char *username, + const char *privkeyPath, + const char *knownHostsPath, + const char *knownHostsVerify, + const char *authMethods, + const char *netcatPath, + const char *socketPath, + virConnectAuthPtr authPtr, + virURIPtr uri) +{ + virNetSocketPtr sock = NULL; + virNetClientPtr ret = NULL; + + virBuffer buf = VIR_BUFFER_INITIALIZER; + char *nc = NULL; + char *command = NULL; + + char *homedir = virGetUserDirectory(); + char *confdir = virGetUserConfigDirectory(); + char *knownhosts = NULL; + char *privkey = NULL; + + /* Use default paths for known hosts an public keys if not provided */ + if (confdir) { + if (!knownHostsPath) { + if (virFileExists(confdir)) { + virBufferAsprintf(&buf, "%s/known_hosts", confdir); + if (!(knownhosts = virBufferContentAndReset(&buf))) + goto no_memory; + } + } else { + if (VIR_STRDUP(knownhosts, knownHostsPath) < 0) + goto cleanup; + } + } + + if (homedir) { + if (!privkeyPath) { + /* RSA */ + virBufferAsprintf(&buf, "%s/.ssh/id_rsa", homedir); + if (!(privkey = virBufferContentAndReset(&buf))) + goto no_memory; + + if (!(virFileExists(privkey))) + VIR_FREE(privkey); + /* DSA */ + if (!privkey) { + virBufferAsprintf(&buf, "%s/.ssh/id_dsa", homedir); + if (!(privkey = virBufferContentAndReset(&buf))) + goto no_memory; + + if (!(virFileExists(privkey))) + VIR_FREE(privkey); + } + } else { + if (VIR_STRDUP(privkey, privkeyPath) < 0) + goto cleanup; + } + } + + if (!authMethods) { + if (privkey) + authMethods = "agent,privkey,password,keyboard-interactive"; + else + authMethods = "agent,password,keyboard-interactive"; + } + + DEFAULT_VALUE(host, "localhost"); + DEFAULT_VALUE(port, "22"); + DEFAULT_VALUE(username, "root"); + DEFAULT_VALUE(netcatPath, "nc"); + DEFAULT_VALUE(knownHostsVerify, "normal"); + + virBufferEscapeShell(&buf, netcatPath); + if (!(nc = virBufferContentAndReset(&buf))) + goto no_memory; + + virBufferAsprintf(&buf, + "sh -c " + "'if '%s' -q 2>&1 | grep \"requires an argument\" >/dev/null 2>&1; then " + "ARG=-q0;" + "else " + "ARG=;" + "fi;" + "'%s' $ARG -U %s'", + nc, nc, socketPath); + + if (!(command = virBufferContentAndReset(&buf))) + goto no_memory; + + if (virNetSocketNewConnectLibssh(host, port, + family, + username, privkey, + knownhosts, knownHostsVerify, authMethods, + command, authPtr, uri, &sock) != 0) + goto cleanup; + + if (!(ret = virNetClientNew(sock, NULL))) + goto cleanup; + sock = NULL; + + cleanup: + VIR_FREE(command); + VIR_FREE(privkey); + VIR_FREE(knownhosts); + VIR_FREE(homedir); + VIR_FREE(confdir); + VIR_FREE(nc); + virObjectUnref(sock); + return ret; + + no_memory: + virReportOOMError(); + goto cleanup; +} +#undef DEFAULT_VALUE + virNetClientPtr virNetClientNewExternal(const char **cmdargv) { virNetSocketPtr sock; diff --git a/src/rpc/virnetclient.h b/src/rpc/virnetclient.h index c772d0b..9cf3209 100644 --- a/src/rpc/virnetclient.h +++ b/src/rpc/virnetclient.h @@ -67,6 +67,19 @@ virNetClientPtr virNetClientNewLibSSH2(const char *host, virConnectAuthPtr authPtr, virURIPtr uri); +virNetClientPtr virNetClientNewLibssh(const char *host, + const char *port, + int family, + const char *username, + const char *privkeyPath, + const char *knownHostsPath, + const char *knownHostsVerify, + const char *authMethods, + const char *netcatPath, + const char *socketPath, + virConnectAuthPtr authPtr, + virURIPtr uri); + virNetClientPtr virNetClientNewExternal(const char **cmdargv); int virNetClientRegisterAsyncIO(virNetClientPtr client); diff --git a/src/rpc/virnetlibsshsession.c b/src/rpc/virnetlibsshsession.c new file mode 100644 index 0000000..19990ea --- /dev/null +++ b/src/rpc/virnetlibsshsession.c @@ -0,0 +1,1424 @@ +/* + * virnetlibsshsession.c: ssh network transport provider based on libssh + * + * Copyright (C) 2012-2016 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see + * <http://www.gnu.org/licenses/>. + * + * Author: Peter Krempa <pkrempa@redhat.com> + * Author: Pino Toscano <ptoscano@redhat.com> + */ +#include <config.h> +#include <libssh/libssh.h> + +#include "virnetlibsshsession.h" + +#include "internal.h" +#include "viralloc.h" +#include "virlog.h" +#include "configmake.h" +#include "virutil.h" +#include "virerror.h" +#include "virobject.h" +#include "virstring.h" +#include "virauth.h" +#include "virbuffer.h" + +#define VIR_FROM_THIS VIR_FROM_LIBSSH + +VIR_LOG_INIT("rpc.netlibsshsession"); + +#define VIR_NET_LIBSSH_BUFFER_SIZE 1024 + +/* TRACE_LIBSSH=<level> enables tracing in libssh itself. + * The meaning of <level> is described here: + * http://api.libssh.org/master/group__libssh__log.html + * + * The LIBVIRT_LIBSSH_DEBUG environment variable can be used + * to set/override the level of libssh debug. + */ +#define TRACE_LIBSSH 0 + +typedef enum { + VIR_NET_LIBSSH_STATE_NEW, + VIR_NET_LIBSSH_STATE_HANDSHAKE_COMPLETE, + VIR_NET_LIBSSH_STATE_CLOSED, + VIR_NET_LIBSSH_STATE_ERROR, + VIR_NET_LIBSSH_STATE_ERROR_REMOTE, +} virNetLibsshSessionState; + +typedef enum { + VIR_NET_LIBSSH_AUTH_KEYBOARD_INTERACTIVE, + VIR_NET_LIBSSH_AUTH_PASSWORD, + VIR_NET_LIBSSH_AUTH_PRIVKEY, + VIR_NET_LIBSSH_AUTH_AGENT, + VIR_NET_LIBSSH_AUTH_COUNT /* keep it as last, used to count */ +} virNetLibsshAuthMethods; + + +typedef struct _virNetLibsshAuthMethod virNetLibsshAuthMethod; +typedef virNetLibsshAuthMethod *virNetLibsshAuthMethodPtr; + +struct _virNetLibsshAuthMethod { + virNetLibsshAuthMethods method; + char *password; + char *filename; + + int tries; +}; + +struct _virNetLibsshSession { + virObjectLockable parent; + virNetLibsshSessionState state; + + /* libssh internal stuff */ + ssh_session session; + ssh_channel channel; + + /* for host key checking */ + virNetLibsshHostkeyVerify hostKeyVerify; + char *knownHostsFile; + char *hostname; + int port; + + /* authentication stuff */ + char *username; + virConnectAuthPtr cred; + char *authPath; + virNetLibsshAuthMethodPtr auths[VIR_NET_LIBSSH_AUTH_COUNT]; + + /* channel stuff */ + char *channelCommand; + int channelCommandReturnValue; + + /* read cache */ + char rbuf[VIR_NET_LIBSSH_BUFFER_SIZE]; + size_t bufUsed; + size_t bufStart; +}; + +static void +virNetLibsshSessionAuthMethodFree(virNetLibsshAuthMethodPtr auth) +{ + if (!auth) + return; + + VIR_FREE(auth->password); + VIR_FREE(auth->filename); + VIR_FREE(auth); +} + +static void +virNetLibsshSessionAuthMethodsFree(virNetLibsshSessionPtr sess) +{ + size_t i; + + for (i = 0; i < ARRAY_CARDINALITY(sess->auths); ++i) { + virNetLibsshSessionAuthMethodFree(sess->auths[i]); + sess->auths[i] = NULL; + } +} + +static void +virNetLibsshSessionDispose(void *obj) +{ + virNetLibsshSessionPtr sess = obj; + VIR_DEBUG("sess=0x%p", sess); + + if (!sess) + return; + + if (sess->channel) { + ssh_channel_send_eof(sess->channel); + ssh_channel_close(sess->channel); + ssh_channel_free(sess->channel); + } + + if (sess->session) { + ssh_disconnect(sess->session); + ssh_free(sess->session); + } + + virNetLibsshSessionAuthMethodsFree(sess); + + VIR_FREE(sess->channelCommand); + VIR_FREE(sess->hostname); + VIR_FREE(sess->knownHostsFile); + VIR_FREE(sess->authPath); + VIR_FREE(sess->username); +} + +static virClassPtr virNetLibsshSessionClass; +static int +virNetLibsshSessionOnceInit(void) +{ + const char *dbgLevelStr; + + if (!(virNetLibsshSessionClass = virClassNew(virClassForObjectLockable(), + "virNetLibsshSession", + sizeof(virNetLibsshSession), + virNetLibsshSessionDispose))) + return -1; + + if (ssh_init() < 0) + return -1; + +#if TRACE_LIBSSH != 0 + ssh_set_log_level(TRACE_LIBSSH); +#endif + + dbgLevelStr = virGetEnvAllowSUID("LIBVIRT_LIBSSH_DEBUG"); + if (dbgLevelStr) { + int dbgLevel = virParseNumber(&dbgLevelStr); + ssh_set_log_level(dbgLevel); + } + + return 0; +} +VIR_ONCE_GLOBAL_INIT(virNetLibsshSession); + +static virNetLibsshAuthMethodPtr +virNetLibsshSessionAuthMethodNew(virNetLibsshSessionPtr sess, + virNetLibsshAuthMethods method) +{ + virNetLibsshAuthMethodPtr auth; + + virNetLibsshSessionAuthMethodFree(sess->auths[method]); + if (VIR_ALLOC(auth) < 0) + return NULL; + + sess->auths[method] = auth; + auth->method = method; + + return auth; +} + +/* string representation of public key of remote server */ +static char* +virSshServerKeyAsString(virNetLibsshSessionPtr sess) +{ + int ret; + ssh_key key; + unsigned char *keyhash; + size_t keyhashlen; + char *str; + + if (ssh_get_publickey(sess->session, &key) != SSH_OK) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("failed to get the key of the current " + "session")); + return NULL; + } + + /* calculate remote key hash, using MD5 algorithm that is + * usual in OpenSSH. The returned value must be freed */ + ret = ssh_get_publickey_hash(key, SSH_PUBLICKEY_HASH_MD5, + &keyhash, &keyhashlen); + ssh_key_free(key); + if (ret < 0) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("failed to calculate ssh host key hash")); + return NULL; + } + /* format the host key into a nice userfriendly string. */ + str = ssh_get_hexa(keyhash, keyhashlen); + ssh_clean_pubkey_hash(&keyhash); + + return str; +} + +static int +virCredTypeForPrompt(virConnectAuthPtr cred, char echo) +{ + unsigned int i; + + for (i = 0; i < cred->ncredtype; ++i) { + int type = cred->credtype[i]; + if (echo) { + if (type == VIR_CRED_ECHOPROMPT) { + return type; + } + } else { + if (type == VIR_CRED_PASSPHRASE || + type == VIR_CRED_NOECHOPROMPT) { + return type; + } + } + } + + return -1; +} + +static int +virLengthForPromptString(const char *str) +{ + int len = strlen(str); + + while (len > 0 && (str[len-1] == ' ' || str[len-1] == ':')) + --len; + + return len; +} + +/* check session host keys + * + * this function checks the known host database and verifies the key + * errors are raised in this func + * + * return value: 0 on success, -1 on error + */ +static int +virNetLibsshCheckHostKey(virNetLibsshSessionPtr sess) +{ + int state; + char *keyhashstr; + const char *errmsg; + + if (sess->hostKeyVerify == VIR_NET_LIBSSH_HOSTKEY_VERIFY_IGNORE) + return 0; + + state = ssh_is_server_known(sess->session); + + switch (state) { + case SSH_SERVER_KNOWN_OK: + /* host key matches */ + return 0; + + case SSH_SERVER_FOUND_OTHER: + case SSH_SERVER_KNOWN_CHANGED: + keyhashstr = virSshServerKeyAsString(sess); + if (!keyhashstr) + return -1; + + /* host key verification failed */ + virReportError(VIR_ERR_AUTH_FAILED, + _("!!! SSH HOST KEY VERIFICATION FAILED !!!: " + "Identity of host '%s:%d' differs from stored identity. " + "Please verify the new host key '%s' to avoid possible " + "man in the middle attack. The key is stored in '%s'."), + sess->hostname, sess->port, + keyhashstr, sess->knownHostsFile); + + ssh_string_free_char(keyhashstr); + return -1; + + case SSH_SERVER_FILE_NOT_FOUND: + case SSH_SERVER_NOT_KNOWN: + /* key was not found, query to add it to database */ + if (sess->hostKeyVerify == VIR_NET_LIBSSH_HOSTKEY_VERIFY_NORMAL) { + virConnectCredential askKey; + char *tmp; + + /* ask to add the key */ + if (!sess->cred || !sess->cred->cb) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("No user interaction callback provided: " + "Can't verify the session host key")); + return -1; + } + + /* prepare data for the callback */ + memset(&askKey, 0, sizeof(virConnectCredential)); + askKey.type = virCredTypeForPrompt(sess->cred, 1 /* echo */); + + if (askKey.type == -1) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("no suitable callback for host key " + "verification")); + return -1; + } + + keyhashstr = virSshServerKeyAsString(sess); + if (!keyhashstr) + return -1; + + if (virAsprintf((char **)&askKey.prompt, + _("Accept SSH host key with hash '%s' for " + "host '%s:%d' (%s/%s)?"), + keyhashstr, + sess->hostname, sess->port, + "y", "n") < 0) { + ssh_string_free_char(keyhashstr); + return -1; + } + + if (sess->cred->cb(&askKey, 1, sess->cred->cbdata)) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("failed to retrieve decision to accept " + "host key")); + tmp = (char*)askKey.prompt; + VIR_FREE(tmp); + ssh_string_free_char(keyhashstr); + return -1; + } + + tmp = (char*)askKey.prompt; + VIR_FREE(tmp); + + if (!askKey.result || + STRCASENEQ(askKey.result, "y")) { + virReportError(VIR_ERR_LIBSSH, + _("SSH host key for '%s' (%s) was not accepted"), + sess->hostname, keyhashstr); + ssh_string_free_char(keyhashstr); + VIR_FREE(askKey.result); + return -1; + } + ssh_string_free_char(keyhashstr); + VIR_FREE(askKey.result); + } + + /* write the host key file */ + if (ssh_write_knownhost(sess->session) < 0) { + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_LIBSSH, + _("failed to write known_host file '%s': %s"), + sess->knownHostsFile, + errmsg); + return -1; + } + /* key was accepted and added */ + return 0; + + case SSH_SERVER_ERROR: + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_LIBSSH, + _("failed to validate SSH host key: %s"), + errmsg); + return -1; + + default: /* should never happen (tm) */ + virReportError(VIR_ERR_LIBSSH, "%s", _("Unknown error value")); + return -1; + } + + return -1; +} + +/* callback for ssh_pki_import_privkey_file, used to get the passphrase + * of a private key + */ +static int +virNetLibsshAuthenticatePrivkeyCb(const char *prompt, char *buf, size_t len, + int echo, int verify ATTRIBUTE_UNUSED, + void *userdata) +{ + virNetLibsshSessionPtr sess = userdata; + virConnectCredential retr_passphrase; + virBuffer buff = VIR_BUFFER_INITIALIZER; + char *p; + + /* request user's key password */ + if (!sess->cred || !sess->cred->cb) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("No user interaction callback provided: " + "Can't retrieve private key passphrase")); + return -1; + } + + virBufferAdd(&buff, prompt, virLengthForPromptString(prompt)); + + if (virBufferCheckError(&buff) < 0) { + virBufferFreeAndReset(&buff); + return -1; + } + + memset(&retr_passphrase, 0, sizeof(virConnectCredential)); + retr_passphrase.type = virCredTypeForPrompt(sess->cred, echo); + retr_passphrase.prompt = virBufferCurrentContent(&buff); + + if (retr_passphrase.type == -1) { + virBufferFreeAndReset(&buff); + virReportError(VIR_ERR_LIBSSH, "%s", + _("no suitable callback for input of key passphrase")); + return -1; + } + + if (sess->cred->cb(&retr_passphrase, 1, sess->cred->cbdata)) { + virBufferFreeAndReset(&buff); + virReportError(VIR_ERR_LIBSSH, "%s", + _("failed to retrieve private key passphrase: " + "callback has failed")); + return -1; + } + + virBufferFreeAndReset(&buff); + + p = virStrncpy(buf, retr_passphrase.result, + retr_passphrase.resultlen, len); + VIR_FREE(retr_passphrase.result); + if (!p) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("authentication buffer too long for provided " + "passphrase")); + return -1; + } + + return 0; +} + +/* perform private key authentication + * + * returns SSH_AUTH_* values + */ +static int +virNetLibsshAuthenticatePrivkey(virNetLibsshSessionPtr sess, + virNetLibsshAuthMethodPtr priv) +{ + int err; + int ret; + char *tmp = NULL; + ssh_key public_key = NULL; + ssh_key private_key = NULL; + + VIR_DEBUG("sess=%p", sess); + + /* try open the key with the password set, first */ + ret = ssh_pki_import_privkey_file(priv->filename, priv->password, + virNetLibsshAuthenticatePrivkeyCb, + sess, &private_key); + if (ret == SSH_EOF) { + virReportError(VIR_ERR_AUTH_FAILED, + _("error while reading private key '%s'"), + priv->filename); + err = SSH_AUTH_ERROR; + goto error; + } else if (ret == SSH_ERROR) { + virReportError(VIR_ERR_AUTH_FAILED, + _("error while opening private key '%s', wrong " + "passphrase?"), + priv->filename); + err = SSH_AUTH_ERROR; + goto error; + } + + if (virAsprintf(&tmp, "%s.pub", priv->filename) < 0) { + err = SSH_AUTH_ERROR; + goto error; + } + + /* try to open the public part of the private key */ + ret = ssh_pki_import_pubkey_file(tmp, &public_key); + if (ret == SSH_ERROR) { + virReportError(VIR_ERR_AUTH_FAILED, + _("error while reading public key '%s'"), + tmp); + err = SSH_AUTH_ERROR; + goto error; + } else if (ret == SSH_EOF) { + /* create the public key from the private key */ + ret = ssh_pki_export_privkey_to_pubkey(private_key, &public_key); + if (ret == SSH_ERROR) { + virReportError(VIR_ERR_AUTH_FAILED, + _("cannot export the public key from the " + "private key '%s'"), + tmp); + err = SSH_AUTH_ERROR; + goto error; + } + } + + VIR_FREE(tmp); + + ret = ssh_userauth_try_publickey(sess->session, NULL, public_key); + if (ret != SSH_AUTH_SUCCESS) { + err = SSH_AUTH_DENIED; + goto error; + } + + ret = ssh_userauth_publickey(sess->session, NULL, private_key); + if (ret != SSH_AUTH_SUCCESS) { + err = SSH_AUTH_DENIED; + goto error; + } + + ssh_key_free(private_key); + ssh_key_free(public_key); + + return SSH_AUTH_SUCCESS; + + error: + if (private_key) + ssh_key_free(private_key); + if (public_key) + ssh_key_free(public_key); + VIR_FREE(tmp); + return err; +} + + +/* perform password authentication, either directly or request the password + * + * returns SSH_AUTH_* values + */ +static int +virNetLibsshAuthenticatePassword(virNetLibsshSessionPtr sess, + virNetLibsshAuthMethodPtr priv) +{ + char *password = NULL; + const char *errmsg; + int ret = -1; + + VIR_DEBUG("sess=%p", sess); + + if (priv->password) { + /* tunelled password authentication */ + if ((ret = ssh_userauth_password(sess->session, NULL, + priv->password)) == 0) { + ret = SSH_AUTH_SUCCESS; + goto cleanup; + } + } else { + /* password authentication with interactive password request */ + if (!sess->cred || !sess->cred->cb) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("Can't perform authentication: " + "Authentication callback not provided")); + ret = SSH_AUTH_ERROR; + goto cleanup; + } + + /* Try the authenticating the set amount of times. The server breaks the + * connection if maximum number of bad auth tries is exceeded */ + while (true) { + if (!(password = virAuthGetPasswordPath(sess->authPath, sess->cred, + "ssh", sess->username, + sess->hostname))) { + virReportError(VIR_ERR_INTERNAL_ERROR, "%s", + _("failed to retrieve password")); + ret = SSH_AUTH_ERROR; + goto cleanup; + } + + /* tunelled password authentication */ + if ((ret = ssh_userauth_password(sess->session, NULL, + password)) == 0) { + ret = SSH_AUTH_SUCCESS; + goto cleanup; + } + + VIR_FREE(password); + + if (ret != SSH_AUTH_DENIED) + break; + } + } + + /* error path */ + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_AUTH_FAILED, + _("authentication failed: %s"), errmsg); + + return ret; + + cleanup: + VIR_FREE(password); + return ret; +} + +/* perform keyboard interactive authentication + * + * returns SSH_AUTH_* values + */ +static int +virNetLibsshAuthenticateKeyboardInteractive(virNetLibsshSessionPtr sess, + virNetLibsshAuthMethodPtr priv) +{ + int ret; + const char *errmsg; + int try = 0; + + /* request user's key password */ + if (!sess->cred || !sess->cred->cb) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("No user interaction callback provided: " + "Can't get input from keyboard interactive " + "authentication")); + return SSH_AUTH_ERROR; + } + + again: + ret = ssh_userauth_kbdint(sess->session, NULL, NULL); + while (ret == SSH_AUTH_INFO) { + const char *name, *instruction; + int nprompts, iprompt; + virBuffer buff = VIR_BUFFER_INITIALIZER; + + name = ssh_userauth_kbdint_getname(sess->session); + instruction = ssh_userauth_kbdint_getinstruction(sess->session); + nprompts = ssh_userauth_kbdint_getnprompts(sess->session); + + /* compose the main buffer with name and instruction, if present */ + if (name && name[0]) + virBufferAddStr(&buff, name); + if (instruction && instruction[0]) { + if (virBufferUse(&buff) > 0) + virBufferAddChar(&buff, '\n'); + virBufferAddStr(&buff, instruction); + } + + if (virBufferCheckError(&buff) < 0) { + virBufferFreeAndReset(&buff); + return -1; + } + + for (iprompt = 0; iprompt < nprompts; iprompt++) { + virConnectCredential retr_passphrase; + const char *promptStr; + char echo; + virBuffer prompt = VIR_BUFFER_INITIALIZER; + + /* get the prompt, stripping the trailing newlines and spaces */ + promptStr = ssh_userauth_kbdint_getprompt(sess->session, iprompt, + &echo); + + /* compose the instruction buffer with this prompt */ + if (virBufferUse(&buff) > 0) { + virBufferAddBuffer(&prompt, &buff); + virBufferAddChar(&prompt, '\n'); + } + virBufferAdd(&prompt, promptStr, + virLengthForPromptString(promptStr)); + + if (virBufferCheckError(&prompt) < 0) { + virBufferFreeAndReset(&buff); + virBufferFreeAndReset(&prompt); + return -1; + } + + memset(&retr_passphrase, 0, sizeof(virConnectCredential)); + retr_passphrase.type = virCredTypeForPrompt(sess->cred, echo); + retr_passphrase.prompt = virBufferCurrentContent(&prompt); + + if (retr_passphrase.type == -1) { + virBufferFreeAndReset(&buff); + virBufferFreeAndReset(&prompt); + virReportError(VIR_ERR_LIBSSH, "%s", + _("no suitable callback for input of key " + "passphrase")); + return SSH_AUTH_ERROR; + } + + if (sess->cred->cb(&retr_passphrase, 1, sess->cred->cbdata)) { + virBufferFreeAndReset(&buff); + virBufferFreeAndReset(&prompt); + virReportError(VIR_ERR_LIBSSH, "%s", + _("failed to retrieve keyboard interactive " + "result: callback has failed")); + return SSH_AUTH_ERROR; + } + + ret = ssh_userauth_kbdint_setanswer(sess->session, iprompt, + retr_passphrase.result); + virBufferFreeAndReset(&prompt); + VIR_FREE(retr_passphrase.result); + if (ret < 0) { + virBufferFreeAndReset(&buff); + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_AUTH_FAILED, + _("authentication failed: %s"), errmsg); + return ret; + } + } + + virBufferFreeAndReset(&buff); + + ret = ssh_userauth_kbdint(sess->session, NULL, NULL); + ++try; + if (ret == SSH_AUTH_DENIED && (priv->tries < 0 || try < priv->tries)) + goto again; + } + + if (ret == SSH_AUTH_ERROR) { + /* error path */ + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_AUTH_FAILED, + _("authentication failed: %s"), errmsg); + } + + return ret; +} + +/* select auth method and authenticate */ +static int +virNetLibsshAuthenticate(virNetLibsshSessionPtr sess) +{ + virNetLibsshAuthMethodPtr auth; + bool auth_failed = false; + const char *errmsg; + int ret; + int methods; + + VIR_DEBUG("sess=%p", sess); + + /* At this point, we can assume there is at least one + * authentication method set -- virNetLibsshValidateConfig + * already checked that. + */ + + /* try to authenticate */ + ret = ssh_userauth_none(sess->session, NULL); + if (ret == SSH_AUTH_ERROR) { + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_LIBSSH, + _("Failed to authenticate as 'none': %s"), + errmsg); + return -1; + } + + /* obtain list of supported auth methods */ + methods = ssh_userauth_list(sess->session, NULL); + + if (methods & SSH_AUTH_METHOD_PUBLICKEY) { + /* try to authenticate using ssh-agent first */ + auth = sess->auths[VIR_NET_LIBSSH_AUTH_AGENT]; + if (auth) { + ret = ssh_userauth_publickey_auto(sess->session, NULL, NULL); + if (ret == SSH_AUTH_ERROR) { + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_LIBSSH, + _("failed to authenticate using agent: %s"), + errmsg); + return -1; + } else if (ret == SSH_AUTH_SUCCESS) { + /* authenticated */ + return 0; + } + } + + /* try to authenticate using the provided ssh key, if any */ + auth = sess->auths[VIR_NET_LIBSSH_AUTH_PRIVKEY]; + if (auth) { + ret = virNetLibsshAuthenticatePrivkey(sess, auth); + if (ret == SSH_AUTH_ERROR) { + /* virReportError is called already */ + return -1; + } else if (ret == SSH_AUTH_SUCCESS) { + /* authenticated */ + return 0; + } + } + + auth_failed = true; + } + + if (methods & SSH_AUTH_METHOD_INTERACTIVE) { + /* try to authenticate using the keyboard interactive way */ + auth = sess->auths[VIR_NET_LIBSSH_AUTH_KEYBOARD_INTERACTIVE]; + if (auth) { + ret = virNetLibsshAuthenticateKeyboardInteractive(sess, auth); + if (ret == SSH_AUTH_ERROR) { + /* virReportError is called already */ + return -1; + } else if (ret == SSH_AUTH_SUCCESS) { + /* authenticated */ + return 0; + } + } + + auth_failed = true; + } + + if (methods & SSH_AUTH_METHOD_PASSWORD) { + /* try to authenticate with password */ + auth = sess->auths[VIR_NET_LIBSSH_AUTH_PASSWORD]; + if (auth) { + ret = virNetLibsshAuthenticatePassword(sess, auth); + if (ret == SSH_AUTH_ERROR) { + /* virReportError is called already */ + return -1; + } else if (ret == SSH_AUTH_SUCCESS) { + /* authenticated */ + return 0; + } + } + + auth_failed = true; + } + + if (!auth_failed) { + virReportError(VIR_ERR_AUTH_FAILED, "%s", + _("None of the requested authentication methods " + "are supported by the server")); + } else { + virReportError(VIR_ERR_AUTH_FAILED, "%s", + _("All provided authentication methods with credentials " + "were rejected by the server")); + } + + return -1; +} + +/* open channel */ +static int +virNetLibsshOpenChannel(virNetLibsshSessionPtr sess) +{ + const char *errmsg; + + sess->channel = ssh_channel_new(sess->session); + if (!sess->channel) { + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_LIBSSH, + _("failed to create libssh channel: %s"), + errmsg); + return -1; + } + + if (ssh_channel_open_session(sess->channel) != SSH_OK) { + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_LIBSSH, + _("failed to open ssh channel: %s"), + errmsg); + return -1; + } + + if (ssh_channel_request_exec(sess->channel, sess->channelCommand) != SSH_OK) { + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_LIBSSH, + _("failed to execute command '%s': %s"), + sess->channelCommand, + errmsg); + return -1; + } + + /* nonblocking mode */ + ssh_channel_set_blocking(sess->channel, 0); + + /* channel open */ + return 0; +} + +/* validate if all required parameters are configured */ +static int +virNetLibsshValidateConfig(virNetLibsshSessionPtr sess) +{ + size_t i; + bool has_auths = false; + + for (i = 0; i < ARRAY_CARDINALITY(sess->auths); ++i) { + if (sess->auths[i]) { + has_auths = true; + break; + } + } + if (!has_auths) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("No authentication methods and credentials " + "provided")); + return -1; + } + + if (!sess->channelCommand) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("No channel command provided")); + return -1; + } + + if (sess->hostKeyVerify != VIR_NET_LIBSSH_HOSTKEY_VERIFY_IGNORE) { + if (!sess->hostname) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("Hostname is needed for host key verification")); + return -1; + } + } + + /* everything ok */ + return 0; +} + +/* ### PUBLIC API ### */ +int +virNetLibsshSessionAuthSetCallback(virNetLibsshSessionPtr sess, + virConnectAuthPtr auth) +{ + virObjectLock(sess); + sess->cred = auth; + virObjectUnlock(sess); + return 0; +} + +void +virNetLibsshSessionAuthReset(virNetLibsshSessionPtr sess) +{ + virObjectLock(sess); + virNetLibsshSessionAuthMethodsFree(sess); + virObjectUnlock(sess); +} + +int +virNetLibsshSessionAuthAddPasswordAuth(virNetLibsshSessionPtr sess, + virURIPtr uri) +{ + virNetLibsshAuthMethodPtr auth; + + if (uri) { + VIR_FREE(sess->authPath); + + if (virAuthGetConfigFilePathURI(uri, &sess->authPath) < 0) + goto error; + } + + virObjectLock(sess); + + if (!(auth = virNetLibsshSessionAuthMethodNew(sess, + VIR_NET_LIBSSH_AUTH_PASSWORD))) + goto error; + + virObjectUnlock(sess); + return 0; + + error: + virObjectUnlock(sess); + return -1; +} + +int +virNetLibsshSessionAuthAddAgentAuth(virNetLibsshSessionPtr sess) +{ + virNetLibsshAuthMethodPtr auth; + + virObjectLock(sess); + + if (!(auth = virNetLibsshSessionAuthMethodNew(sess, + VIR_NET_LIBSSH_AUTH_AGENT))) + goto error; + + virObjectUnlock(sess); + return 0; + + error: + virObjectUnlock(sess); + return -1; +} + +int +virNetLibsshSessionAuthAddPrivKeyAuth(virNetLibsshSessionPtr sess, + const char *keyfile, + const char *password) +{ + virNetLibsshAuthMethodPtr auth; + + char *pass = NULL; + char *file = NULL; + + if (!keyfile) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("Key file path must be provided " + "for private key authentication")); + return -1; + } + + virObjectLock(sess); + + if (VIR_STRDUP(file, keyfile) < 0 || + VIR_STRDUP(pass, password) < 0) + goto error; + + if (!(auth = virNetLibsshSessionAuthMethodNew(sess, + VIR_NET_LIBSSH_AUTH_PRIVKEY))) + goto error; + + auth->password = pass; + auth->filename = file; + + virObjectUnlock(sess); + return 0; + + error: + VIR_FREE(pass); + VIR_FREE(file); + virObjectUnlock(sess); + return -1; +} + +int +virNetLibsshSessionAuthAddKeyboardAuth(virNetLibsshSessionPtr sess, + int tries) +{ + virNetLibsshAuthMethodPtr auth; + + virObjectLock(sess); + + if (!(auth = virNetLibsshSessionAuthMethodNew(sess, + VIR_NET_LIBSSH_AUTH_KEYBOARD_INTERACTIVE))) + goto error; + + auth->tries = tries; + + virObjectUnlock(sess); + return 0; + + error: + virObjectUnlock(sess); + return -1; + +} + +int +virNetLibsshSessionSetChannelCommand(virNetLibsshSessionPtr sess, + const char *command) +{ + int ret = 0; + virObjectLock(sess); + + VIR_FREE(sess->channelCommand); + + if (VIR_STRDUP(sess->channelCommand, command) < 0) + ret = -1; + + virObjectUnlock(sess); + return ret; +} + +int +virNetLibsshSessionSetHostKeyVerification(virNetLibsshSessionPtr sess, + const char *hostname, + int port, + const char *hostsfile, + virNetLibsshHostkeyVerify opt) +{ + virObjectLock(sess); + + sess->port = port; + sess->hostKeyVerify = opt; + + VIR_FREE(sess->hostname); + + if (VIR_STRDUP(sess->hostname, hostname) < 0) + goto error; + + /* set the hostname */ + if (ssh_options_set(sess->session, SSH_OPTIONS_HOST, sess->hostname) < 0) + goto error; + + /* set the port */ + if (port > 0) { + unsigned int portU = port; + + if (ssh_options_set(sess->session, SSH_OPTIONS_PORT, &portU) < 0) + goto error; + } + + /* set the known hosts file */ + if (ssh_options_set(sess->session, SSH_OPTIONS_KNOWNHOSTS, hostsfile) < 0) + goto error; + + VIR_FREE(sess->knownHostsFile); + if (VIR_STRDUP(sess->knownHostsFile, hostsfile) < 0) + goto error; + + virObjectUnlock(sess); + return 0; + + error: + virObjectUnlock(sess); + return -1; +} + +/* allocate and initialize a libssh session object */ +virNetLibsshSessionPtr virNetLibsshSessionNew(const char *username) +{ + virNetLibsshSessionPtr sess = NULL; + + if (virNetLibsshSessionInitialize() < 0) + goto error; + + if (!(sess = virObjectLockableNew(virNetLibsshSessionClass))) + goto error; + + /* initialize session data */ + if (!(sess->session = ssh_new())) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("Failed to initialize libssh session")); + goto error; + } + + if (VIR_STRDUP(sess->username, username) < 0) + goto error; + + VIR_DEBUG("virNetLibsshSessionPtr: %p, ssh_session: %p", + sess, sess->session); + + /* set blocking mode for libssh until handshake is complete */ + ssh_set_blocking(sess->session, 1); + + if (ssh_options_set(sess->session, SSH_OPTIONS_USER, sess->username) < 0) + goto error; + + /* default states for config variables */ + sess->state = VIR_NET_LIBSSH_STATE_NEW; + sess->hostKeyVerify = VIR_NET_LIBSSH_HOSTKEY_VERIFY_IGNORE; + + return sess; + + error: + virObjectUnref(sess); + return NULL; +} + +int +virNetLibsshSessionConnect(virNetLibsshSessionPtr sess, + int sock) +{ + int ret; + const char *errmsg; + + VIR_DEBUG("sess=%p, sock=%d", sess, sock); + + if (!sess || sess->state != VIR_NET_LIBSSH_STATE_NEW) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("Invalid virNetLibsshSessionPtr")); + return -1; + } + + virObjectLock(sess); + + /* check if configuration is valid */ + if ((ret = virNetLibsshValidateConfig(sess)) < 0) + goto error; + + /* read ~/.ssh/config */ + if ((ret = ssh_options_parse_config(sess->session, NULL)) < 0) + goto error; + + /* set the socket FD for the libssh session */ + if ((ret = ssh_options_set(sess->session, SSH_OPTIONS_FD, &sock)) < 0) + goto error; + + /* open session */ + ret = ssh_connect(sess->session); + /* libssh is in blocking mode, so EAGAIN will never happen */ + if (ret < 0) { + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_NO_CONNECT, + _("SSH session handshake failed: %s"), + errmsg); + goto error; + } + + /* verify the SSH host key */ + if ((ret = virNetLibsshCheckHostKey(sess)) != 0) + goto error; + + /* authenticate */ + if ((ret = virNetLibsshAuthenticate(sess)) != 0) + goto error; + + /* open channel */ + if ((ret = virNetLibsshOpenChannel(sess)) != 0) + goto error; + + /* all set */ + /* switch to nonblocking mode and return */ + ssh_set_blocking(sess->session, 0); + sess->state = VIR_NET_LIBSSH_STATE_HANDSHAKE_COMPLETE; + + virObjectUnlock(sess); + return ret; + + error: + sess->state = VIR_NET_LIBSSH_STATE_ERROR; + virObjectUnlock(sess); + return ret; +} + +/* do a read from a ssh channel, used instead of normal read on socket */ +ssize_t +virNetLibsshChannelRead(virNetLibsshSessionPtr sess, + char *buf, + size_t len) +{ + int ret = -1; + ssize_t read_n = 0; + + virObjectLock(sess); + + if (sess->state != VIR_NET_LIBSSH_STATE_HANDSHAKE_COMPLETE) { + if (sess->state == VIR_NET_LIBSSH_STATE_ERROR_REMOTE) + virReportError(VIR_ERR_LIBSSH, + _("Remote program terminated " + "with non-zero code: %d"), + sess->channelCommandReturnValue); + else + virReportError(VIR_ERR_LIBSSH, "%s", + _("Tried to write socket in error state")); + + virObjectUnlock(sess); + return -1; + } + + if (sess->bufUsed > 0) { + /* copy the rest (or complete) internal buffer to the output buffer */ + memcpy(buf, + sess->rbuf + sess->bufStart, + len > sess->bufUsed ? sess->bufUsed : len); + + if (len >= sess->bufUsed) { + read_n = sess->bufUsed; + + sess->bufStart = 0; + sess->bufUsed = 0; + } else { + read_n = len; + sess->bufUsed -= len; + sess->bufStart += len; + + goto success; + } + } + + /* continue reading into the buffer supplied */ + if (read_n < len) { + ret = ssh_channel_read(sess->channel, + buf + read_n, + len - read_n, + 0); + + if (ret == SSH_AGAIN || (ret == 0 && !ssh_channel_is_eof(sess->channel))) + goto success; + + if (ret < 0) + goto error; + + read_n += ret; + } + + /* try to read something into the internal buffer */ + if (sess->bufUsed == 0) { + ret = ssh_channel_read(sess->channel, + sess->rbuf, + VIR_NET_LIBSSH_BUFFER_SIZE, + 0); + + if (ret == SSH_AGAIN || (ret == 0 && !ssh_channel_is_eof(sess->channel))) + goto success; + + if (ret < 0) + goto error; + + sess->bufUsed = ret; + sess->bufStart = 0; + } + + if (read_n == 0) { + /* get rid of data in stderr stream */ + ret = ssh_channel_read(sess->channel, + sess->rbuf, + VIR_NET_LIBSSH_BUFFER_SIZE - 1, + 1); + if (ret > 0) { + sess->rbuf[ret] = '\0'; + VIR_DEBUG("flushing stderr, data='%s'", sess->rbuf); + } + } + + if (ssh_channel_is_eof(sess->channel)) { + if (ssh_channel_get_exit_status(sess->channel)) { + virReportError(VIR_ERR_LIBSSH, + _("Remote command terminated with non-zero code: %d"), + ssh_channel_get_exit_status(sess->channel)); + sess->channelCommandReturnValue = ssh_channel_get_exit_status(sess->channel); + sess->state = VIR_NET_LIBSSH_STATE_ERROR_REMOTE; + virObjectUnlock(sess); + return -1; + } + + sess->state = VIR_NET_LIBSSH_STATE_CLOSED; + virObjectUnlock(sess); + return -1; + } + + success: + virObjectUnlock(sess); + return read_n; + + error: + sess->state = VIR_NET_LIBSSH_STATE_ERROR; + virObjectUnlock(sess); + return ret; +} + +ssize_t +virNetLibsshChannelWrite(virNetLibsshSessionPtr sess, + const char *buf, + size_t len) +{ + ssize_t ret; + + virObjectLock(sess); + + if (sess->state != VIR_NET_LIBSSH_STATE_HANDSHAKE_COMPLETE) { + if (sess->state == VIR_NET_LIBSSH_STATE_ERROR_REMOTE) + virReportError(VIR_ERR_LIBSSH, + _("Remote program terminated with non-zero code: %d"), + sess->channelCommandReturnValue); + else + virReportError(VIR_ERR_LIBSSH, "%s", + _("Tried to write socket in error state")); + ret = -1; + goto cleanup; + } + + if (ssh_channel_is_eof(sess->channel)) { + if (ssh_channel_get_exit_status(sess->channel)) { + virReportError(VIR_ERR_LIBSSH, + _("Remote program terminated with non-zero code: %d"), + ssh_channel_get_exit_status(sess->channel)); + sess->state = VIR_NET_LIBSSH_STATE_ERROR_REMOTE; + sess->channelCommandReturnValue = ssh_channel_get_exit_status(sess->channel); + + ret = -1; + goto cleanup; + } + + sess->state = VIR_NET_LIBSSH_STATE_CLOSED; + ret = -1; + goto cleanup; + } + + ret = ssh_channel_write(sess->channel, buf, len); + if (ret == SSH_AGAIN) { + ret = 0; + goto cleanup; + } + + if (ret < 0) { + const char *msg; + sess->state = VIR_NET_LIBSSH_STATE_ERROR; + msg = ssh_get_error(sess->session); + virReportError(VIR_ERR_LIBSSH, + _("write failed: %s"), msg); + } + + cleanup: + virObjectUnlock(sess); + return ret; +} + +bool +virNetLibsshSessionHasCachedData(virNetLibsshSessionPtr sess) +{ + bool ret; + + if (!sess) + return false; + + virObjectLock(sess); + + ret = sess->bufUsed > 0; + + virObjectUnlock(sess); + return ret; +} diff --git a/src/rpc/virnetlibsshsession.h b/src/rpc/virnetlibsshsession.h new file mode 100644 index 0000000..9f07495 --- /dev/null +++ b/src/rpc/virnetlibsshsession.h @@ -0,0 +1,80 @@ +/* + * virnetlibsshsession.h: ssh transport provider based on libssh + * + * Copyright (C) 2012-2016 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see + * <http://www.gnu.org/licenses/>. + * + * Author: Peter Krempa <pkrempa@redhat.com> + * Author: Pino Toscano <ptoscano@redhat.com> + */ +#ifndef __VIR_NET_LIBSSH_SESSION_H__ +# define __VIR_NET_LIBSSH_SESSION_H__ + +# include "internal.h" +# include "viruri.h" + +typedef struct _virNetLibsshSession virNetLibsshSession; +typedef virNetLibsshSession *virNetLibsshSessionPtr; + +virNetLibsshSessionPtr virNetLibsshSessionNew(const char *username); +void virNetLibsshSessionFree(virNetLibsshSessionPtr sess); + +typedef enum { + VIR_NET_LIBSSH_HOSTKEY_VERIFY_NORMAL, + VIR_NET_LIBSSH_HOSTKEY_VERIFY_AUTO_ADD, + VIR_NET_LIBSSH_HOSTKEY_VERIFY_IGNORE +} virNetLibsshHostkeyVerify; + +int virNetLibsshSessionSetChannelCommand(virNetLibsshSessionPtr sess, + const char *command); + +void virNetLibsshSessionAuthReset(virNetLibsshSessionPtr sess); + +int virNetLibsshSessionAuthSetCallback(virNetLibsshSessionPtr sess, + virConnectAuthPtr auth); + +int virNetLibsshSessionAuthAddPasswordAuth(virNetLibsshSessionPtr sess, + virURIPtr uri); + +int virNetLibsshSessionAuthAddAgentAuth(virNetLibsshSessionPtr sess); + +int virNetLibsshSessionAuthAddPrivKeyAuth(virNetLibsshSessionPtr sess, + const char *keyfile, + const char *password); + +int virNetLibsshSessionAuthAddKeyboardAuth(virNetLibsshSessionPtr sess, + int tries); + +int virNetLibsshSessionSetHostKeyVerification(virNetLibsshSessionPtr sess, + const char *hostname, + int port, + const char *hostsfile, + virNetLibsshHostkeyVerify opt); + +int virNetLibsshSessionConnect(virNetLibsshSessionPtr sess, + int sock); + +ssize_t virNetLibsshChannelRead(virNetLibsshSessionPtr sess, + char *buf, + size_t len); + +ssize_t virNetLibsshChannelWrite(virNetLibsshSessionPtr sess, + const char *buf, + size_t len); + +bool virNetLibsshSessionHasCachedData(virNetLibsshSessionPtr sess); + +#endif /* ___VIR_NET_LIBSSH_SESSION_H_ */ diff --git a/src/rpc/virnetsocket.c b/src/rpc/virnetsocket.c index 05f20a5..325a7c7 100644 --- a/src/rpc/virnetsocket.c +++ b/src/rpc/virnetsocket.c @@ -65,6 +65,10 @@ # include "virnetsshsession.h" #endif +#if WITH_LIBSSH +# include "virnetlibsshsession.h" +#endif + #define VIR_FROM_THIS VIR_FROM_RPC VIR_LOG_INIT("rpc.netsocket"); @@ -107,6 +111,9 @@ struct _virNetSocket { #if WITH_SSH2 virNetSSHSessionPtr sshSession; #endif +#if WITH_LIBSSH + virNetLibsshSessionPtr libsshSession; +#endif }; @@ -1027,6 +1034,143 @@ virNetSocketNewConnectLibSSH2(const char *host ATTRIBUTE_UNUSED, } #endif /* WITH_SSH2 */ +#if WITH_LIBSSH +int +virNetSocketNewConnectLibssh(const char *host, + const char *port, + int family, + const char *username, + const char *privkey, + const char *knownHosts, + const char *knownHostsVerify, + const char *authMethods, + const char *command, + virConnectAuthPtr auth, + virURIPtr uri, + virNetSocketPtr *retsock) +{ + virNetSocketPtr sock = NULL; + virNetLibsshSessionPtr sess = NULL; + unsigned int verify; + int ret = -1; + int portN; + + char *authMethodNext = NULL; + char *authMethodsCopy = NULL; + char *authMethod; + + /* port number will be verified while opening the socket */ + if (virStrToLong_i(port, NULL, 10, &portN) < 0) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("Failed to parse port number")); + goto error; + } + + /* create ssh session context */ + if (!(sess = virNetLibsshSessionNew(username))) + goto error; + + /* set ssh session parameters */ + if (virNetLibsshSessionAuthSetCallback(sess, auth) != 0) + goto error; + + if (STRCASEEQ("auto", knownHostsVerify)) { + verify = VIR_NET_LIBSSH_HOSTKEY_VERIFY_AUTO_ADD; + } else if (STRCASEEQ("ignore", knownHostsVerify)) { + verify = VIR_NET_LIBSSH_HOSTKEY_VERIFY_IGNORE; + } else if (STRCASEEQ("normal", knownHostsVerify)) { + verify = VIR_NET_LIBSSH_HOSTKEY_VERIFY_NORMAL; + } else { + virReportError(VIR_ERR_INVALID_ARG, + _("Invalid host key verification method: '%s'"), + knownHostsVerify); + goto error; + } + + if (virNetLibsshSessionSetHostKeyVerification(sess, + host, + portN, + knownHosts, + verify) != 0) + goto error; + + if (virNetLibsshSessionSetChannelCommand(sess, command) != 0) + goto error; + + if (VIR_STRDUP(authMethodsCopy, authMethods) < 0) + goto error; + + authMethodNext = authMethodsCopy; + + while ((authMethod = strsep(&authMethodNext, ","))) { + if (STRCASEEQ(authMethod, "keyboard-interactive")) { + ret = virNetLibsshSessionAuthAddKeyboardAuth(sess, -1); + } else if (STRCASEEQ(authMethod, "password")) { + ret = virNetLibsshSessionAuthAddPasswordAuth(sess, uri); + } else if (STRCASEEQ(authMethod, "privkey")) { + ret = virNetLibsshSessionAuthAddPrivKeyAuth(sess, + privkey, + NULL); + } else if (STRCASEEQ(authMethod, "agent")) { + ret = virNetLibsshSessionAuthAddAgentAuth(sess); + } else { + virReportError(VIR_ERR_INVALID_ARG, + _("Invalid authentication method: '%s'"), + authMethod); + ret = -1; + goto error; + } + + if (ret != 0) + goto error; + } + + /* connect to remote server */ + if ((ret = virNetSocketNewConnectTCP(host, port, family, &sock)) < 0) + goto error; + + /* connect to the host using ssh */ + if ((ret = virNetLibsshSessionConnect(sess, virNetSocketGetFD(sock))) != 0) + goto error; + + sock->libsshSession = sess; + /* libssh owns the FD and closes it on its own, and thus + * we must not close it (otherwise there are warnings about + * trying to close an invalid FD). + */ + sock->ownsFd = false; + *retsock = sock; + + VIR_FREE(authMethodsCopy); + return 0; + + error: + virObjectUnref(sock); + virObjectUnref(sess); + VIR_FREE(authMethodsCopy); + return ret; +} +#else +int +virNetSocketNewConnectLibssh(const char *host ATTRIBUTE_UNUSED, + const char *port ATTRIBUTE_UNUSED, + int family ATTRIBUTE_UNUSED, + const char *username ATTRIBUTE_UNUSED, + const char *privkey ATTRIBUTE_UNUSED, + const char *knownHosts ATTRIBUTE_UNUSED, + const char *knownHostsVerify ATTRIBUTE_UNUSED, + const char *authMethods ATTRIBUTE_UNUSED, + const char *command ATTRIBUTE_UNUSED, + virConnectAuthPtr auth ATTRIBUTE_UNUSED, + virURIPtr uri ATTRIBUTE_UNUSED, + virNetSocketPtr *retsock ATTRIBUTE_UNUSED) +{ + virReportSystemError(ENOSYS, "%s", + _("libssh transport support was not enabled")); + return -1; +} +#endif /* WITH_LIBSSH */ + int virNetSocketNewConnectExternal(const char **cmdargv, virNetSocketPtr *retsock) { @@ -1204,6 +1348,10 @@ void virNetSocketDispose(void *obj) virObjectUnref(sock->sshSession); #endif +#if WITH_LIBSSH + virObjectUnref(sock->libsshSession); +#endif + if (sock->ownsFd) VIR_FORCE_CLOSE(sock->fd); VIR_FORCE_CLOSE(sock->errfd); @@ -1534,6 +1682,11 @@ bool virNetSocketHasCachedData(virNetSocketPtr sock ATTRIBUTE_UNUSED) hasCached = true; #endif +#if WITH_LIBSSH + if (virNetLibsshSessionHasCachedData(sock->libsshSession)) + hasCached = true; +#endif + #if WITH_SASL if (sock->saslDecoded) hasCached = true; @@ -1558,6 +1711,22 @@ static ssize_t virNetSocketLibSSH2Write(virNetSocketPtr sock, } #endif +#if WITH_LIBSSH +static ssize_t virNetSocketLibsshRead(virNetSocketPtr sock, + char *buf, + size_t len) +{ + return virNetLibsshChannelRead(sock->libsshSession, buf, len); +} + +static ssize_t virNetSocketLibsshWrite(virNetSocketPtr sock, + const char *buf, + size_t len) +{ + return virNetLibsshChannelWrite(sock->libsshSession, buf, len); +} +#endif + bool virNetSocketHasPendingData(virNetSocketPtr sock ATTRIBUTE_UNUSED) { bool hasPending = false; @@ -1581,6 +1750,11 @@ static ssize_t virNetSocketReadWire(virNetSocketPtr sock, char *buf, size_t len) return virNetSocketLibSSH2Read(sock, buf, len); #endif +#if WITH_LIBSSH + if (sock->libsshSession) + return virNetSocketLibsshRead(sock, buf, len); +#endif + reread: #if WITH_GNUTLS if (sock->tlsSession && @@ -1640,6 +1814,11 @@ static ssize_t virNetSocketWriteWire(virNetSocketPtr sock, const char *buf, size return virNetSocketLibSSH2Write(sock, buf, len); #endif +#if WITH_LIBSSH + if (sock->libsshSession) + return virNetSocketLibsshWrite(sock, buf, len); +#endif + rewrite: #if WITH_GNUTLS if (sock->tlsSession && diff --git a/src/rpc/virnetsocket.h b/src/rpc/virnetsocket.h index ec064bb..56c75c0 100644 --- a/src/rpc/virnetsocket.h +++ b/src/rpc/virnetsocket.h @@ -100,6 +100,19 @@ int virNetSocketNewConnectLibSSH2(const char *host, virURIPtr uri, virNetSocketPtr *retsock); +int virNetSocketNewConnectLibssh(const char *host, + const char *port, + int family, + const char *username, + const char *privkey, + const char *knownHosts, + const char *knownHostsVerify, + const char *authMethods, + const char *command, + virConnectAuthPtr auth, + virURIPtr uri, + virNetSocketPtr *retsock); + int virNetSocketNewConnectExternal(const char **cmdargv, virNetSocketPtr *addr); diff --git a/src/util/virerror.c b/src/util/virerror.c index 2958308..ef17fb5 100644 --- a/src/util/virerror.c +++ b/src/util/virerror.c @@ -137,7 +137,8 @@ VIR_ENUM_IMPL(virErrorDomain, VIR_ERR_DOMAIN_LAST, "Log Manager", "Xen XL Config", - "Perf", + "Perf", /* 65 */ + "Libssh transport layer", ) @@ -1400,6 +1401,12 @@ virErrorMsg(virErrorNumber error, const char *info) else errmsg = _("guest agent replied with wrong id to guest-sync command: %s"); break; + case VIR_ERR_LIBSSH: + if (info == NULL) + errmsg = _("libssh transport error"); + else + errmsg = _("libssh transport error: %s"); + break; } return errmsg; } -- 2.7.4

On Mon, Oct 17, 2016 at 16:24:53 +0200, Pino Toscano wrote:
Implement a new libssh transport, which uses libssh to communicate with remote hosts, and use it in virNetSockets.
This new transport supports all the common ssh authentication methods, making use of libvirt's auth callbacks for interaction with the user.
Most of the functionalities and implementation are based on the libssh2 transport. --- config-post.h | 2 + configure.ac | 3 + include/libvirt/virterror.h | 2 + m4/virt-libssh.m4 | 26 + src/Makefile.am | 21 +- src/libvirt_libssh.syms | 22 + src/remote/remote_driver.c | 41 ++ src/rpc/virnetclient.c | 123 ++++ src/rpc/virnetclient.h | 13 + src/rpc/virnetlibsshsession.c | 1424 +++++++++++++++++++++++++++++++++++++++++ src/rpc/virnetlibsshsession.h | 80 +++ src/rpc/virnetsocket.c | 179 ++++++ src/rpc/virnetsocket.h | 13 + src/util/virerror.c | 9 +- 14 files changed, 1955 insertions(+), 3 deletions(-) create mode 100644 m4/virt-libssh.m4 create mode 100644 src/libvirt_libssh.syms create mode 100644 src/rpc/virnetlibsshsession.c create mode 100644 src/rpc/virnetlibsshsession.h
Is it possible to split out the src/rpc/virnetlibsshsession.(ch) changes (plus the ones necessary to compile it) from adding all the bits that actually make use of it? This patch is very big.
diff --git a/include/libvirt/virterror.h b/include/libvirt/virterror.h index efe83aa..2efee8f 100644 --- a/include/libvirt/virterror.h +++ b/include/libvirt/virterror.h @@ -131,6 +131,7 @@ typedef enum { VIR_FROM_XENXL = 64, /* Error from Xen xl config code */
VIR_FROM_PERF = 65, /* Error from perf */ + VIR_FROM_LIBSSH = 66, /* Error from libssh connection transport */
# ifdef VIR_ENUM_SENTINELS VIR_ERR_DOMAIN_LAST @@ -317,6 +318,7 @@ typedef enum { VIR_ERR_NO_CLIENT = 96, /* Client was not found */ VIR_ERR_AGENT_UNSYNCED = 97, /* guest agent replies with wrong id to guest-sync command */ + VIR_ERR_LIBSSH = 98, /* error in libssh transport driver */
These error handling changes can be split to a separate patch as well.
} virErrorNumber;
/**
[...]
diff --git a/src/remote/remote_driver.c b/src/remote/remote_driver.c index a3cd7cd..db2bdd4 100644 --- a/src/remote/remote_driver.c +++ b/src/remote/remote_driver.c @@ -673,6 +673,7 @@ remoteConnectSupportsFeatureUnlocked(virConnectPtr conn, * - xxx:/// -> UNIX domain socket * - xxx+ssh:/// -> SSH connection (legacy) * - xxx+libssh2:/// -> SSH connection (using libssh2) + * - xxx+libssh:/// -> SSH connection (using libssh) */ static int doRemoteOpen(virConnectPtr conn, @@ -689,6 +690,7 @@ doRemoteOpen(virConnectPtr conn, trans_libssh2, trans_ext, trans_tcp, + trans_libssh, } transport; #ifndef WIN32 char *daemonPath = NULL; @@ -736,6 +738,8 @@ doRemoteOpen(virConnectPtr conn, transport = trans_ext; } else if (STRCASEEQ(transport_str, "tcp")) { transport = trans_tcp; + } else if (STRCASEEQ(transport_str, "libssh")) { + transport = trans_libssh; } else { virReportError(VIR_ERR_INVALID_ARG, "%s", _("remote_open: transport in URL not recognised " @@ -959,6 +963,43 @@ doRemoteOpen(virConnectPtr conn, priv->is_secure = 1; break;
+ case trans_libssh: + if (!sockname) { + /* Right now we don't support default session connections */ + if (STREQ_NULLABLE(conn->uri->path, "/session")) { + virReportError(VIR_ERR_OPERATION_UNSUPPORTED, "%s", + _("Connecting to session instance without " + "socket path is not supported by the libssh " + "connection driver")); + goto failed; + } + + if (VIR_STRDUP(sockname, + flags & VIR_DRV_OPEN_REMOTE_RO ? + LIBVIRTD_PRIV_UNIX_SOCKET_RO : LIBVIRTD_PRIV_UNIX_SOCKET) < 0) + goto failed; + } + + VIR_DEBUG("Starting libssh session"); + + priv->client = virNetClientNewLibssh(priv->hostname, + port, + AF_UNSPEC, + username, + keyfile, + knownHosts, + knownHostsVerify, + sshauth, + netcat, + sockname, + auth, + conn->uri); + if (!priv->client) + goto failed; + + priv->is_secure = 1; + break; + #ifndef WIN32 case trans_unix: if (!sockname) {
All of these are actually using this as the transport so a new patch will be appropriate for those.
diff --git a/src/rpc/virnetclient.c b/src/rpc/virnetclient.c index 361dc1a..b296aac 100644 --- a/src/rpc/virnetclient.c +++ b/src/rpc/virnetclient.c @@ -505,6 +505,129 @@ virNetClientPtr virNetClientNewLibSSH2(const char *host, } #undef DEFAULT_VALUE
+#define DEFAULT_VALUE(VAR, VAL) \ + if (!VAR) \ + VAR = VAL; +virNetClientPtr virNetClientNewLibssh(const char *host, + const char *port, + int family, + const char *username, + const char *privkeyPath, + const char *knownHostsPath, + const char *knownHostsVerify, + const char *authMethods, + const char *netcatPath, + const char *socketPath, + virConnectAuthPtr authPtr, + virURIPtr uri) +{ + virNetSocketPtr sock = NULL; + virNetClientPtr ret = NULL; + + virBuffer buf = VIR_BUFFER_INITIALIZER; + char *nc = NULL; + char *command = NULL; + + char *homedir = virGetUserDirectory(); + char *confdir = virGetUserConfigDirectory(); + char *knownhosts = NULL; + char *privkey = NULL; + + /* Use default paths for known hosts an public keys if not provided */
So is libssh able to handle e.g. ECDSA keys in known hosts? Libssh2 was not and truncated the known hosts file which was not acceptable.
+ if (confdir) { + if (!knownHostsPath) { + if (virFileExists(confdir)) { + virBufferAsprintf(&buf, "%s/known_hosts", confdir); + if (!(knownhosts = virBufferContentAndReset(&buf)))
Use virAsprintf instead of the two lines above.
+ goto no_memory; + } + } else { + if (VIR_STRDUP(knownhosts, knownHostsPath) < 0) + goto cleanup; + } + } + + if (homedir) { + if (!privkeyPath) { + /* RSA */ + virBufferAsprintf(&buf, "%s/.ssh/id_rsa", homedir); + if (!(privkey = virBufferContentAndReset(&buf)))
Same here.
+ goto no_memory; + + if (!(virFileExists(privkey))) + VIR_FREE(privkey); + /* DSA */ + if (!privkey) { + virBufferAsprintf(&buf, "%s/.ssh/id_dsa", homedir); + if (!(privkey = virBufferContentAndReset(&buf)))
Same here.
+ goto no_memory; + + if (!(virFileExists(privkey))) + VIR_FREE(privkey); + } + } else { + if (VIR_STRDUP(privkey, privkeyPath) < 0) + goto cleanup; + } + } + + if (!authMethods) { + if (privkey) + authMethods = "agent,privkey,password,keyboard-interactive"; + else + authMethods = "agent,password,keyboard-interactive"; + } + + DEFAULT_VALUE(host, "localhost"); + DEFAULT_VALUE(port, "22"); + DEFAULT_VALUE(username, "root"); + DEFAULT_VALUE(netcatPath, "nc"); + DEFAULT_VALUE(knownHostsVerify, "normal"); + + virBufferEscapeShell(&buf, netcatPath); + if (!(nc = virBufferContentAndReset(&buf))) + goto no_memory; + + virBufferAsprintf(&buf,
Again, virAsprintf should be enough here.
+ "sh -c " + "'if '%s' -q 2>&1 | grep \"requires an argument\" >/dev/null 2>&1; then " + "ARG=-q0;" + "else " + "ARG=;" + "fi;" + "'%s' $ARG -U %s'", + nc, nc, socketPath); + + if (!(command = virBufferContentAndReset(&buf))) + goto no_memory;
... and it report OOM errors directly.
+ + if (virNetSocketNewConnectLibssh(host, port, + family, + username, privkey, + knownhosts, knownHostsVerify, authMethods, + command, authPtr, uri, &sock) != 0) + goto cleanup; + + if (!(ret = virNetClientNew(sock, NULL))) + goto cleanup; + sock = NULL; + + cleanup: + VIR_FREE(command); + VIR_FREE(privkey); + VIR_FREE(knownhosts); + VIR_FREE(homedir); + VIR_FREE(confdir); + VIR_FREE(nc); + virObjectUnref(sock); + return ret; + + no_memory: + virReportOOMError(); + goto cleanup; +} +#undef DEFAULT_VALUE + virNetClientPtr virNetClientNewExternal(const char **cmdargv) { virNetSocketPtr sock; diff --git a/src/rpc/virnetclient.h b/src/rpc/virnetclient.h index c772d0b..9cf3209 100644 --- a/src/rpc/virnetclient.h +++ b/src/rpc/virnetclient.h @@ -67,6 +67,19 @@ virNetClientPtr virNetClientNewLibSSH2(const char *host, virConnectAuthPtr authPtr, virURIPtr uri);
+virNetClientPtr virNetClientNewLibssh(const char *host, + const char *port, + int family, + const char *username, + const char *privkeyPath, + const char *knownHostsPath, + const char *knownHostsVerify, + const char *authMethods, + const char *netcatPath, + const char *socketPath, + virConnectAuthPtr authPtr, + virURIPtr uri); + virNetClientPtr virNetClientNewExternal(const char **cmdargv);
int virNetClientRegisterAsyncIO(virNetClientPtr client);
As said, the client impl should be split from the transport impl.
diff --git a/src/rpc/virnetlibsshsession.c b/src/rpc/virnetlibsshsession.c new file mode 100644 index 0000000..19990ea --- /dev/null +++ b/src/rpc/virnetlibsshsession.c @@ -0,0 +1,1424 @@ +/* + * virnetlibsshsession.c: ssh network transport provider based on libssh + * + * Copyright (C) 2012-2016 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see + * <http://www.gnu.org/licenses/>. + * + * Author: Peter Krempa <pkrempa@redhat.com> + * Author: Pino Toscano <ptoscano@redhat.com> + */ +#include <config.h> +#include <libssh/libssh.h> + +#include "virnetlibsshsession.h" + +#include "internal.h" +#include "viralloc.h" +#include "virlog.h" +#include "configmake.h" +#include "virutil.h" +#include "virerror.h" +#include "virobject.h" +#include "virstring.h" +#include "virauth.h" +#include "virbuffer.h" + +#define VIR_FROM_THIS VIR_FROM_LIBSSH + +VIR_LOG_INIT("rpc.netlibsshsession"); + +#define VIR_NET_LIBSSH_BUFFER_SIZE 1024 + +/* TRACE_LIBSSH=<level> enables tracing in libssh itself. + * The meaning of <level> is described here: + * http://api.libssh.org/master/group__libssh__log.html + * + * The LIBVIRT_LIBSSH_DEBUG environment variable can be used + * to set/override the level of libssh debug. + */ +#define TRACE_LIBSSH 0 + +typedef enum { + VIR_NET_LIBSSH_STATE_NEW, + VIR_NET_LIBSSH_STATE_HANDSHAKE_COMPLETE, + VIR_NET_LIBSSH_STATE_CLOSED, + VIR_NET_LIBSSH_STATE_ERROR, + VIR_NET_LIBSSH_STATE_ERROR_REMOTE, +} virNetLibsshSessionState; + +typedef enum { + VIR_NET_LIBSSH_AUTH_KEYBOARD_INTERACTIVE, + VIR_NET_LIBSSH_AUTH_PASSWORD, + VIR_NET_LIBSSH_AUTH_PRIVKEY, + VIR_NET_LIBSSH_AUTH_AGENT, + VIR_NET_LIBSSH_AUTH_COUNT /* keep it as last, used to count */ +} virNetLibsshAuthMethods; + + +typedef struct _virNetLibsshAuthMethod virNetLibsshAuthMethod; +typedef virNetLibsshAuthMethod *virNetLibsshAuthMethodPtr; + +struct _virNetLibsshAuthMethod { + virNetLibsshAuthMethods method; + char *password; + char *filename; + + int tries; +}; + +struct _virNetLibsshSession { + virObjectLockable parent; + virNetLibsshSessionState state; + + /* libssh internal stuff */ + ssh_session session; + ssh_channel channel; + + /* for host key checking */ + virNetLibsshHostkeyVerify hostKeyVerify; + char *knownHostsFile; + char *hostname; + int port; + + /* authentication stuff */ + char *username; + virConnectAuthPtr cred; + char *authPath; + virNetLibsshAuthMethodPtr auths[VIR_NET_LIBSSH_AUTH_COUNT]; + + /* channel stuff */ + char *channelCommand; + int channelCommandReturnValue; + + /* read cache */ + char rbuf[VIR_NET_LIBSSH_BUFFER_SIZE]; + size_t bufUsed; + size_t bufStart; +}; + +static void +virNetLibsshSessionAuthMethodFree(virNetLibsshAuthMethodPtr auth) +{ + if (!auth) + return; + + VIR_FREE(auth->password); + VIR_FREE(auth->filename); + VIR_FREE(auth); +} + +static void +virNetLibsshSessionAuthMethodsFree(virNetLibsshSessionPtr sess) +{ + size_t i; + + for (i = 0; i < ARRAY_CARDINALITY(sess->auths); ++i) { + virNetLibsshSessionAuthMethodFree(sess->auths[i]); + sess->auths[i] = NULL; + } +} + +static void +virNetLibsshSessionDispose(void *obj) +{ + virNetLibsshSessionPtr sess = obj; + VIR_DEBUG("sess=0x%p", sess); + + if (!sess) + return; + + if (sess->channel) { + ssh_channel_send_eof(sess->channel); + ssh_channel_close(sess->channel); + ssh_channel_free(sess->channel); + } + + if (sess->session) { + ssh_disconnect(sess->session); + ssh_free(sess->session); + } + + virNetLibsshSessionAuthMethodsFree(sess); + + VIR_FREE(sess->channelCommand); + VIR_FREE(sess->hostname); + VIR_FREE(sess->knownHostsFile); + VIR_FREE(sess->authPath); + VIR_FREE(sess->username); +} + +static virClassPtr virNetLibsshSessionClass; +static int +virNetLibsshSessionOnceInit(void) +{ + const char *dbgLevelStr; + + if (!(virNetLibsshSessionClass = virClassNew(virClassForObjectLockable(), + "virNetLibsshSession", + sizeof(virNetLibsshSession), + virNetLibsshSessionDispose))) + return -1; + + if (ssh_init() < 0) + return -1; + +#if TRACE_LIBSSH != 0 + ssh_set_log_level(TRACE_LIBSSH); +#endif + + dbgLevelStr = virGetEnvAllowSUID("LIBVIRT_LIBSSH_DEBUG"); + if (dbgLevelStr) { + int dbgLevel = virParseNumber(&dbgLevelStr); + ssh_set_log_level(dbgLevel); + } + + return 0; +} +VIR_ONCE_GLOBAL_INIT(virNetLibsshSession); + +static virNetLibsshAuthMethodPtr +virNetLibsshSessionAuthMethodNew(virNetLibsshSessionPtr sess, + virNetLibsshAuthMethods method) +{ + virNetLibsshAuthMethodPtr auth; + + virNetLibsshSessionAuthMethodFree(sess->auths[method]); + if (VIR_ALLOC(auth) < 0) + return NULL; + + sess->auths[method] = auth; + auth->method = method; + + return auth; +} + +/* string representation of public key of remote server */ +static char* +virSshServerKeyAsString(virNetLibsshSessionPtr sess) +{ + int ret; + ssh_key key; + unsigned char *keyhash; + size_t keyhashlen; + char *str; + + if (ssh_get_publickey(sess->session, &key) != SSH_OK) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("failed to get the key of the current " + "session")); + return NULL; + } + + /* calculate remote key hash, using MD5 algorithm that is + * usual in OpenSSH. The returned value must be freed */ + ret = ssh_get_publickey_hash(key, SSH_PUBLICKEY_HASH_MD5, + &keyhash, &keyhashlen); + ssh_key_free(key); + if (ret < 0) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("failed to calculate ssh host key hash")); + return NULL; + } + /* format the host key into a nice userfriendly string. */ + str = ssh_get_hexa(keyhash, keyhashlen); + ssh_clean_pubkey_hash(&keyhash); + + return str; +} + +static int +virCredTypeForPrompt(virConnectAuthPtr cred, char echo) +{ + unsigned int i; + + for (i = 0; i < cred->ncredtype; ++i) { + int type = cred->credtype[i]; + if (echo) { + if (type == VIR_CRED_ECHOPROMPT) { + return type; + } + } else { + if (type == VIR_CRED_PASSPHRASE || + type == VIR_CRED_NOECHOPROMPT) { + return type; + } + } + } + + return -1; +} + +static int +virLengthForPromptString(const char *str) +{ + int len = strlen(str); + + while (len > 0 && (str[len-1] == ' ' || str[len-1] == ':')) + --len; + + return len; +} + +/* check session host keys + * + * this function checks the known host database and verifies the key + * errors are raised in this func + * + * return value: 0 on success, -1 on error + */ +static int +virNetLibsshCheckHostKey(virNetLibsshSessionPtr sess) +{ + int state; + char *keyhashstr; + const char *errmsg; + + if (sess->hostKeyVerify == VIR_NET_LIBSSH_HOSTKEY_VERIFY_IGNORE) + return 0; + + state = ssh_is_server_known(sess->session); + + switch (state) { + case SSH_SERVER_KNOWN_OK: + /* host key matches */ + return 0; + + case SSH_SERVER_FOUND_OTHER: + case SSH_SERVER_KNOWN_CHANGED: + keyhashstr = virSshServerKeyAsString(sess); + if (!keyhashstr) + return -1; + + /* host key verification failed */ + virReportError(VIR_ERR_AUTH_FAILED, + _("!!! SSH HOST KEY VERIFICATION FAILED !!!: " + "Identity of host '%s:%d' differs from stored identity. " + "Please verify the new host key '%s' to avoid possible " + "man in the middle attack. The key is stored in '%s'."), + sess->hostname, sess->port, + keyhashstr, sess->knownHostsFile); + + ssh_string_free_char(keyhashstr); + return -1; + + case SSH_SERVER_FILE_NOT_FOUND: + case SSH_SERVER_NOT_KNOWN: + /* key was not found, query to add it to database */ + if (sess->hostKeyVerify == VIR_NET_LIBSSH_HOSTKEY_VERIFY_NORMAL) { + virConnectCredential askKey; + char *tmp; + + /* ask to add the key */ + if (!sess->cred || !sess->cred->cb) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("No user interaction callback provided: " + "Can't verify the session host key")); + return -1; + } + + /* prepare data for the callback */ + memset(&askKey, 0, sizeof(virConnectCredential)); + askKey.type = virCredTypeForPrompt(sess->cred, 1 /* echo */); + + if (askKey.type == -1) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("no suitable callback for host key " + "verification")); + return -1; + } + + keyhashstr = virSshServerKeyAsString(sess); + if (!keyhashstr) + return -1; + + if (virAsprintf((char **)&askKey.prompt, + _("Accept SSH host key with hash '%s' for " + "host '%s:%d' (%s/%s)?"), + keyhashstr, + sess->hostname, sess->port, + "y", "n") < 0) { + ssh_string_free_char(keyhashstr); + return -1; + } + + if (sess->cred->cb(&askKey, 1, sess->cred->cbdata)) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("failed to retrieve decision to accept " + "host key")); + tmp = (char*)askKey.prompt; + VIR_FREE(tmp); + ssh_string_free_char(keyhashstr); + return -1; + } + + tmp = (char*)askKey.prompt; + VIR_FREE(tmp); + + if (!askKey.result || + STRCASENEQ(askKey.result, "y")) { + virReportError(VIR_ERR_LIBSSH, + _("SSH host key for '%s' (%s) was not accepted"), + sess->hostname, keyhashstr); + ssh_string_free_char(keyhashstr); + VIR_FREE(askKey.result); + return -1; + } + ssh_string_free_char(keyhashstr); + VIR_FREE(askKey.result); + } + + /* write the host key file */ + if (ssh_write_knownhost(sess->session) < 0) { + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_LIBSSH, + _("failed to write known_host file '%s': %s"), + sess->knownHostsFile, + errmsg); + return -1; + } + /* key was accepted and added */ + return 0; + + case SSH_SERVER_ERROR: + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_LIBSSH, + _("failed to validate SSH host key: %s"), + errmsg); + return -1; + + default: /* should never happen (tm) */ + virReportError(VIR_ERR_LIBSSH, "%s", _("Unknown error value")); + return -1; + } + + return -1; +} + +/* callback for ssh_pki_import_privkey_file, used to get the passphrase + * of a private key + */ +static int +virNetLibsshAuthenticatePrivkeyCb(const char *prompt, char *buf, size_t len, + int echo, int verify ATTRIBUTE_UNUSED, + void *userdata)
One argument per line please.
+{ + virNetLibsshSessionPtr sess = userdata; + virConnectCredential retr_passphrase; + virBuffer buff = VIR_BUFFER_INITIALIZER; + char *p; + + /* request user's key password */ + if (!sess->cred || !sess->cred->cb) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("No user interaction callback provided: " + "Can't retrieve private key passphrase")); + return -1; + } + + virBufferAdd(&buff, prompt, virLengthForPromptString(prompt)); + + if (virBufferCheckError(&buff) < 0) { + virBufferFreeAndReset(&buff);
The buffer is already freed on an error.
+ return -1; + } + + memset(&retr_passphrase, 0, sizeof(virConnectCredential)); + retr_passphrase.type = virCredTypeForPrompt(sess->cred, echo); + retr_passphrase.prompt = virBufferCurrentContent(&buff);
This shouldn't really be used. Please get the content into a variable.
+ + if (retr_passphrase.type == -1) { + virBufferFreeAndReset(&buff);
This is weird. We usually do a cleanup label, so that it does not have to be repeated ...
+ virReportError(VIR_ERR_LIBSSH, "%s", + _("no suitable callback for input of key passphrase")); + return -1; + } + + if (sess->cred->cb(&retr_passphrase, 1, sess->cred->cbdata)) { + virBufferFreeAndReset(&buff);
...
+ virReportError(VIR_ERR_LIBSSH, "%s", + _("failed to retrieve private key passphrase: " + "callback has failed")); + return -1; + } + + virBufferFreeAndReset(&buff);
...
+ + p = virStrncpy(buf, retr_passphrase.result, + retr_passphrase.resultlen, len); + VIR_FREE(retr_passphrase.result);
Since this contains the passprhase, it should be overwritten. Please use one of the VIR_DISPOSE... helpers.
+ if (!p) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("authentication buffer too long for provided " + "passphrase"));
The passphrase is too long for the buffer.
+ return -1; + } + + return 0; +} + +/* perform private key authentication + * + * returns SSH_AUTH_* values + */ +static int +virNetLibsshAuthenticatePrivkey(virNetLibsshSessionPtr sess, + virNetLibsshAuthMethodPtr priv) +{ + int err; + int ret; + char *tmp = NULL; + ssh_key public_key = NULL; + ssh_key private_key = NULL; + + VIR_DEBUG("sess=%p", sess); + + /* try open the key with the password set, first */ + ret = ssh_pki_import_privkey_file(priv->filename, priv->password, + virNetLibsshAuthenticatePrivkeyCb, + sess, &private_key); + if (ret == SSH_EOF) { + virReportError(VIR_ERR_AUTH_FAILED, + _("error while reading private key '%s'"), + priv->filename);
Isn't this overwriting an error reported by the callback trying to open the file?
+ err = SSH_AUTH_ERROR; + goto error; + } else if (ret == SSH_ERROR) { + virReportError(VIR_ERR_AUTH_FAILED, + _("error while opening private key '%s', wrong " + "passphrase?"), + priv->filename);
Same here.
+ err = SSH_AUTH_ERROR; + goto error; + } + + if (virAsprintf(&tmp, "%s.pub", priv->filename) < 0) { + err = SSH_AUTH_ERROR; + goto error; + } + + /* try to open the public part of the private key */ + ret = ssh_pki_import_pubkey_file(tmp, &public_key); + if (ret == SSH_ERROR) { + virReportError(VIR_ERR_AUTH_FAILED, + _("error while reading public key '%s'"), + tmp); + err = SSH_AUTH_ERROR; + goto error; + } else if (ret == SSH_EOF) { + /* create the public key from the private key */ + ret = ssh_pki_export_privkey_to_pubkey(private_key, &public_key); + if (ret == SSH_ERROR) { + virReportError(VIR_ERR_AUTH_FAILED, + _("cannot export the public key from the " + "private key '%s'"), + tmp); + err = SSH_AUTH_ERROR; + goto error; + } + } + + VIR_FREE(tmp); + + ret = ssh_userauth_try_publickey(sess->session, NULL, public_key); + if (ret != SSH_AUTH_SUCCESS) { + err = SSH_AUTH_DENIED; + goto error; + } + + ret = ssh_userauth_publickey(sess->session, NULL, private_key); + if (ret != SSH_AUTH_SUCCESS) { + err = SSH_AUTH_DENIED; + goto error; + } + + ssh_key_free(private_key); + ssh_key_free(public_key); + + return SSH_AUTH_SUCCESS; + + error: + if (private_key) + ssh_key_free(private_key); + if (public_key) + ssh_key_free(public_key); + VIR_FREE(tmp); + return err; +} + + +/* perform password authentication, either directly or request the password + * + * returns SSH_AUTH_* values + */ +static int +virNetLibsshAuthenticatePassword(virNetLibsshSessionPtr sess, + virNetLibsshAuthMethodPtr priv) +{ + char *password = NULL; + const char *errmsg; + int ret = -1; + + VIR_DEBUG("sess=%p", sess); + + if (priv->password) { + /* tunelled password authentication */ + if ((ret = ssh_userauth_password(sess->session, NULL, + priv->password)) == 0) { + ret = SSH_AUTH_SUCCESS; + goto cleanup; + } + } else { + /* password authentication with interactive password request */ + if (!sess->cred || !sess->cred->cb) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("Can't perform authentication: " + "Authentication callback not provided")); + ret = SSH_AUTH_ERROR; + goto cleanup; + } + + /* Try the authenticating the set amount of times. The server breaks the + * connection if maximum number of bad auth tries is exceeded */ + while (true) { + if (!(password = virAuthGetPasswordPath(sess->authPath, sess->cred, + "ssh", sess->username, + sess->hostname))) { + virReportError(VIR_ERR_INTERNAL_ERROR, "%s", + _("failed to retrieve password")); + ret = SSH_AUTH_ERROR; + goto cleanup; + } + + /* tunelled password authentication */ + if ((ret = ssh_userauth_password(sess->session, NULL, + password)) == 0) { + ret = SSH_AUTH_SUCCESS; + goto cleanup; + } + + VIR_FREE(password);
Please destroy the password with VIR_DISPOSE...
+ + if (ret != SSH_AUTH_DENIED) + break; + } + } + + /* error path */ + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_AUTH_FAILED, + _("authentication failed: %s"), errmsg); + + return ret; + + cleanup: + VIR_FREE(password);
See above.
+ return ret; +} + +/* perform keyboard interactive authentication + * + * returns SSH_AUTH_* values + */ +static int +virNetLibsshAuthenticateKeyboardInteractive(virNetLibsshSessionPtr sess, + virNetLibsshAuthMethodPtr priv) +{ + int ret; + const char *errmsg; + int try = 0; + + /* request user's key password */ + if (!sess->cred || !sess->cred->cb) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("No user interaction callback provided: " + "Can't get input from keyboard interactive " + "authentication")); + return SSH_AUTH_ERROR; + } + + again: + ret = ssh_userauth_kbdint(sess->session, NULL, NULL); + while (ret == SSH_AUTH_INFO) { + const char *name, *instruction; + int nprompts, iprompt; + virBuffer buff = VIR_BUFFER_INITIALIZER; + + name = ssh_userauth_kbdint_getname(sess->session); + instruction = ssh_userauth_kbdint_getinstruction(sess->session); + nprompts = ssh_userauth_kbdint_getnprompts(sess->session); + + /* compose the main buffer with name and instruction, if present */ + if (name && name[0]) + virBufferAddStr(&buff, name); + if (instruction && instruction[0]) { + if (virBufferUse(&buff) > 0) + virBufferAddChar(&buff, '\n'); + virBufferAddStr(&buff, instruction); + } + + if (virBufferCheckError(&buff) < 0) { + virBufferFreeAndReset(&buff); + return -1; + } + + for (iprompt = 0; iprompt < nprompts; iprompt++) { + virConnectCredential retr_passphrase; + const char *promptStr; + char echo; + virBuffer prompt = VIR_BUFFER_INITIALIZER; + + /* get the prompt, stripping the trailing newlines and spaces */ + promptStr = ssh_userauth_kbdint_getprompt(sess->session, iprompt, + &echo); + + /* compose the instruction buffer with this prompt */ + if (virBufferUse(&buff) > 0) { + virBufferAddBuffer(&prompt, &buff); + virBufferAddChar(&prompt, '\n'); + } + virBufferAdd(&prompt, promptStr, + virLengthForPromptString(promptStr)); + + if (virBufferCheckError(&prompt) < 0) { + virBufferFreeAndReset(&buff); + virBufferFreeAndReset(&prompt); + return -1; + } + + memset(&retr_passphrase, 0, sizeof(virConnectCredential)); + retr_passphrase.type = virCredTypeForPrompt(sess->cred, echo); + retr_passphrase.prompt = virBufferCurrentContent(&prompt);
Please don't use virBufferCurrentContent. Get the pointer and be responsible for it's lifecycle.
+ + if (retr_passphrase.type == -1) { + virBufferFreeAndReset(&buff); + virBufferFreeAndReset(&prompt);
A lot of shared cleanup code ...
+ virReportError(VIR_ERR_LIBSSH, "%s", + _("no suitable callback for input of key " + "passphrase")); + return SSH_AUTH_ERROR; + } + + if (sess->cred->cb(&retr_passphrase, 1, sess->cred->cbdata)) { + virBufferFreeAndReset(&buff); + virBufferFreeAndReset(&prompt);
...
+ virReportError(VIR_ERR_LIBSSH, "%s", + _("failed to retrieve keyboard interactive " + "result: callback has failed")); + return SSH_AUTH_ERROR; + } + + ret = ssh_userauth_kbdint_setanswer(sess->session, iprompt, + retr_passphrase.result); + virBufferFreeAndReset(&prompt); + VIR_FREE(retr_passphrase.result);
VIR_DISPOSE...
+ if (ret < 0) { + virBufferFreeAndReset(&buff); + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_AUTH_FAILED, + _("authentication failed: %s"), errmsg); + return ret; + } + } + + virBufferFreeAndReset(&buff); + + ret = ssh_userauth_kbdint(sess->session, NULL, NULL); + ++try; + if (ret == SSH_AUTH_DENIED && (priv->tries < 0 || try < priv->tries)) + goto again; + } + + if (ret == SSH_AUTH_ERROR) { + /* error path */ + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_AUTH_FAILED, + _("authentication failed: %s"), errmsg); + } + + return ret; +} + +/* select auth method and authenticate */ +static int +virNetLibsshAuthenticate(virNetLibsshSessionPtr sess) +{ + virNetLibsshAuthMethodPtr auth; + bool auth_failed = false; + const char *errmsg; + int ret; + int methods; + + VIR_DEBUG("sess=%p", sess); + + /* At this point, we can assume there is at least one + * authentication method set -- virNetLibsshValidateConfig + * already checked that. + */ + + /* try to authenticate */ + ret = ssh_userauth_none(sess->session, NULL); + if (ret == SSH_AUTH_ERROR) { + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_LIBSSH, + _("Failed to authenticate as 'none': %s"), + errmsg); + return -1; + } + + /* obtain list of supported auth methods */ + methods = ssh_userauth_list(sess->session, NULL); + + if (methods & SSH_AUTH_METHOD_PUBLICKEY) { + /* try to authenticate using ssh-agent first */ + auth = sess->auths[VIR_NET_LIBSSH_AUTH_AGENT]; + if (auth) { + ret = ssh_userauth_publickey_auto(sess->session, NULL, NULL); + if (ret == SSH_AUTH_ERROR) { + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_LIBSSH, + _("failed to authenticate using agent: %s"), + errmsg); + return -1; + } else if (ret == SSH_AUTH_SUCCESS) { + /* authenticated */ + return 0; + } + } + + /* try to authenticate using the provided ssh key, if any */ + auth = sess->auths[VIR_NET_LIBSSH_AUTH_PRIVKEY]; + if (auth) { + ret = virNetLibsshAuthenticatePrivkey(sess, auth); + if (ret == SSH_AUTH_ERROR) { + /* virReportError is called already */ + return -1; + } else if (ret == SSH_AUTH_SUCCESS) { + /* authenticated */ + return 0; + } + } + + auth_failed = true; + } + + if (methods & SSH_AUTH_METHOD_INTERACTIVE) { + /* try to authenticate using the keyboard interactive way */ + auth = sess->auths[VIR_NET_LIBSSH_AUTH_KEYBOARD_INTERACTIVE]; + if (auth) { + ret = virNetLibsshAuthenticateKeyboardInteractive(sess, auth); + if (ret == SSH_AUTH_ERROR) { + /* virReportError is called already */ + return -1; + } else if (ret == SSH_AUTH_SUCCESS) { + /* authenticated */ + return 0; + } + } + + auth_failed = true; + } + + if (methods & SSH_AUTH_METHOD_PASSWORD) { + /* try to authenticate with password */ + auth = sess->auths[VIR_NET_LIBSSH_AUTH_PASSWORD]; + if (auth) { + ret = virNetLibsshAuthenticatePassword(sess, auth); + if (ret == SSH_AUTH_ERROR) { + /* virReportError is called already */ + return -1; + } else if (ret == SSH_AUTH_SUCCESS) { + /* authenticated */ + return 0; + } + } + + auth_failed = true; + } + + if (!auth_failed) { + virReportError(VIR_ERR_AUTH_FAILED, "%s", + _("None of the requested authentication methods " + "are supported by the server")); + } else { + virReportError(VIR_ERR_AUTH_FAILED, "%s", + _("All provided authentication methods with credentials " + "were rejected by the server")); + } + + return -1; +} + +/* open channel */ +static int +virNetLibsshOpenChannel(virNetLibsshSessionPtr sess) +{ + const char *errmsg; + + sess->channel = ssh_channel_new(sess->session); + if (!sess->channel) { + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_LIBSSH, + _("failed to create libssh channel: %s"), + errmsg); + return -1; + } + + if (ssh_channel_open_session(sess->channel) != SSH_OK) { + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_LIBSSH, + _("failed to open ssh channel: %s"), + errmsg); + return -1; + } + + if (ssh_channel_request_exec(sess->channel, sess->channelCommand) != SSH_OK) { + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_LIBSSH, + _("failed to execute command '%s': %s"), + sess->channelCommand, + errmsg); + return -1; + } + + /* nonblocking mode */ + ssh_channel_set_blocking(sess->channel, 0); + + /* channel open */ + return 0; +} + +/* validate if all required parameters are configured */ +static int +virNetLibsshValidateConfig(virNetLibsshSessionPtr sess) +{ + size_t i; + bool has_auths = false; + + for (i = 0; i < ARRAY_CARDINALITY(sess->auths); ++i) { + if (sess->auths[i]) { + has_auths = true; + break; + } + } + if (!has_auths) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("No authentication methods and credentials " + "provided")); + return -1; + } + + if (!sess->channelCommand) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("No channel command provided")); + return -1; + } + + if (sess->hostKeyVerify != VIR_NET_LIBSSH_HOSTKEY_VERIFY_IGNORE) { + if (!sess->hostname) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("Hostname is needed for host key verification")); + return -1; + } + } + + /* everything ok */ + return 0; +} + +/* ### PUBLIC API ### */ +int +virNetLibsshSessionAuthSetCallback(virNetLibsshSessionPtr sess, + virConnectAuthPtr auth) +{ + virObjectLock(sess); + sess->cred = auth; + virObjectUnlock(sess); + return 0; +} + +void +virNetLibsshSessionAuthReset(virNetLibsshSessionPtr sess) +{ + virObjectLock(sess); + virNetLibsshSessionAuthMethodsFree(sess); + virObjectUnlock(sess); +} + +int +virNetLibsshSessionAuthAddPasswordAuth(virNetLibsshSessionPtr sess, + virURIPtr uri) +{ + virNetLibsshAuthMethodPtr auth; + + if (uri) { + VIR_FREE(sess->authPath); + + if (virAuthGetConfigFilePathURI(uri, &sess->authPath) < 0) + goto error; + } + + virObjectLock(sess); + + if (!(auth = virNetLibsshSessionAuthMethodNew(sess, + VIR_NET_LIBSSH_AUTH_PASSWORD))) + goto error; + + virObjectUnlock(sess); + return 0; + + error: + virObjectUnlock(sess); + return -1;
Both success and failure paths can be merged into one cleanup path by introducing a return variable.
+} + +int +virNetLibsshSessionAuthAddAgentAuth(virNetLibsshSessionPtr sess) +{ + virNetLibsshAuthMethodPtr auth; + + virObjectLock(sess); + + if (!(auth = virNetLibsshSessionAuthMethodNew(sess, + VIR_NET_LIBSSH_AUTH_AGENT))) + goto error; + + virObjectUnlock(sess); + return 0; + + error: + virObjectUnlock(sess); + return -1;
Same here.
+} + +int +virNetLibsshSessionAuthAddPrivKeyAuth(virNetLibsshSessionPtr sess, + const char *keyfile, + const char *password) +{ + virNetLibsshAuthMethodPtr auth; + + char *pass = NULL; + char *file = NULL; + + if (!keyfile) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("Key file path must be provided " + "for private key authentication")); + return -1; + } + + virObjectLock(sess); + + if (VIR_STRDUP(file, keyfile) < 0 || + VIR_STRDUP(pass, password) < 0) + goto error; + + if (!(auth = virNetLibsshSessionAuthMethodNew(sess, + VIR_NET_LIBSSH_AUTH_PRIVKEY))) + goto error; + + auth->password = pass; + auth->filename = file; + + virObjectUnlock(sess); + return 0; + + error: + VIR_FREE(pass); + VIR_FREE(file); + virObjectUnlock(sess); + return -1; +} + +int +virNetLibsshSessionAuthAddKeyboardAuth(virNetLibsshSessionPtr sess, + int tries) +{ + virNetLibsshAuthMethodPtr auth; + + virObjectLock(sess); + + if (!(auth = virNetLibsshSessionAuthMethodNew(sess, + VIR_NET_LIBSSH_AUTH_KEYBOARD_INTERACTIVE))) + goto error; + + auth->tries = tries; + + virObjectUnlock(sess); + return 0; + + error: + virObjectUnlock(sess); + return -1;
Same as above.
+ +} + +int +virNetLibsshSessionSetChannelCommand(virNetLibsshSessionPtr sess, + const char *command) +{ + int ret = 0; + virObjectLock(sess); + + VIR_FREE(sess->channelCommand); + + if (VIR_STRDUP(sess->channelCommand, command) < 0) + ret = -1; + + virObjectUnlock(sess); + return ret; +} + +int +virNetLibsshSessionSetHostKeyVerification(virNetLibsshSessionPtr sess, + const char *hostname, + int port, + const char *hostsfile, + virNetLibsshHostkeyVerify opt) +{ + virObjectLock(sess); + + sess->port = port; + sess->hostKeyVerify = opt; + + VIR_FREE(sess->hostname); + + if (VIR_STRDUP(sess->hostname, hostname) < 0) + goto error; + + /* set the hostname */ + if (ssh_options_set(sess->session, SSH_OPTIONS_HOST, sess->hostname) < 0) + goto error; + + /* set the port */ + if (port > 0) { + unsigned int portU = port; + + if (ssh_options_set(sess->session, SSH_OPTIONS_PORT, &portU) < 0) + goto error; + } + + /* set the known hosts file */ + if (ssh_options_set(sess->session, SSH_OPTIONS_KNOWNHOSTS, hostsfile) < 0) + goto error; + + VIR_FREE(sess->knownHostsFile); + if (VIR_STRDUP(sess->knownHostsFile, hostsfile) < 0) + goto error; + + virObjectUnlock(sess); + return 0; + + error: + virObjectUnlock(sess); + return -1; +} + +/* allocate and initialize a libssh session object */ +virNetLibsshSessionPtr virNetLibsshSessionNew(const char *username) +{ + virNetLibsshSessionPtr sess = NULL; + + if (virNetLibsshSessionInitialize() < 0) + goto error; + + if (!(sess = virObjectLockableNew(virNetLibsshSessionClass))) + goto error; + + /* initialize session data */ + if (!(sess->session = ssh_new())) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("Failed to initialize libssh session")); + goto error; + } + + if (VIR_STRDUP(sess->username, username) < 0) + goto error; + + VIR_DEBUG("virNetLibsshSessionPtr: %p, ssh_session: %p", + sess, sess->session); + + /* set blocking mode for libssh until handshake is complete */ + ssh_set_blocking(sess->session, 1); + + if (ssh_options_set(sess->session, SSH_OPTIONS_USER, sess->username) < 0) + goto error; + + /* default states for config variables */ + sess->state = VIR_NET_LIBSSH_STATE_NEW; + sess->hostKeyVerify = VIR_NET_LIBSSH_HOSTKEY_VERIFY_IGNORE; + + return sess; + + error: + virObjectUnref(sess); + return NULL; +} + +int +virNetLibsshSessionConnect(virNetLibsshSessionPtr sess, + int sock) +{ + int ret; + const char *errmsg; + + VIR_DEBUG("sess=%p, sock=%d", sess, sock); + + if (!sess || sess->state != VIR_NET_LIBSSH_STATE_NEW) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("Invalid virNetLibsshSessionPtr")); + return -1; + } + + virObjectLock(sess); + + /* check if configuration is valid */ + if ((ret = virNetLibsshValidateConfig(sess)) < 0) + goto error; + + /* read ~/.ssh/config */ + if ((ret = ssh_options_parse_config(sess->session, NULL)) < 0) + goto error; + + /* set the socket FD for the libssh session */ + if ((ret = ssh_options_set(sess->session, SSH_OPTIONS_FD, &sock)) < 0)
Is this guaranteed to copy the socket number at call time? Otherwise (similarly to the ones above will not work reliably).
+ goto error; + + /* open session */ + ret = ssh_connect(sess->session); + /* libssh is in blocking mode, so EAGAIN will never happen */ + if (ret < 0) { + errmsg = ssh_get_error(sess->session); + virReportError(VIR_ERR_NO_CONNECT, + _("SSH session handshake failed: %s"), + errmsg); + goto error; + } + + /* verify the SSH host key */ + if ((ret = virNetLibsshCheckHostKey(sess)) != 0) + goto error; + + /* authenticate */ + if ((ret = virNetLibsshAuthenticate(sess)) != 0) + goto error; + + /* open channel */ + if ((ret = virNetLibsshOpenChannel(sess)) != 0) + goto error; + + /* all set */ + /* switch to nonblocking mode and return */ + ssh_set_blocking(sess->session, 0); + sess->state = VIR_NET_LIBSSH_STATE_HANDSHAKE_COMPLETE; + + virObjectUnlock(sess); + return ret; + + error: + sess->state = VIR_NET_LIBSSH_STATE_ERROR; + virObjectUnlock(sess); + return ret; +} + +/* do a read from a ssh channel, used instead of normal read on socket */ +ssize_t +virNetLibsshChannelRead(virNetLibsshSessionPtr sess, + char *buf, + size_t len) +{ + int ret = -1; + ssize_t read_n = 0; + + virObjectLock(sess); + + if (sess->state != VIR_NET_LIBSSH_STATE_HANDSHAKE_COMPLETE) { + if (sess->state == VIR_NET_LIBSSH_STATE_ERROR_REMOTE) + virReportError(VIR_ERR_LIBSSH, + _("Remote program terminated " + "with non-zero code: %d"), + sess->channelCommandReturnValue); + else + virReportError(VIR_ERR_LIBSSH, "%s", + _("Tried to write socket in error state")); + + virObjectUnlock(sess); + return -1; + } + + if (sess->bufUsed > 0) { + /* copy the rest (or complete) internal buffer to the output buffer */ + memcpy(buf, + sess->rbuf + sess->bufStart, + len > sess->bufUsed ? sess->bufUsed : len); + + if (len >= sess->bufUsed) { + read_n = sess->bufUsed; + + sess->bufStart = 0; + sess->bufUsed = 0; + } else { + read_n = len; + sess->bufUsed -= len; + sess->bufStart += len; + + goto success; + } + } + + /* continue reading into the buffer supplied */ + if (read_n < len) { + ret = ssh_channel_read(sess->channel, + buf + read_n, + len - read_n, + 0); + + if (ret == SSH_AGAIN || (ret == 0 && !ssh_channel_is_eof(sess->channel))) + goto success; + + if (ret < 0) + goto error; + + read_n += ret; + } + + /* try to read something into the internal buffer */ + if (sess->bufUsed == 0) { + ret = ssh_channel_read(sess->channel, + sess->rbuf, + VIR_NET_LIBSSH_BUFFER_SIZE, + 0); + + if (ret == SSH_AGAIN || (ret == 0 && !ssh_channel_is_eof(sess->channel))) + goto success; + + if (ret < 0) + goto error; + + sess->bufUsed = ret; + sess->bufStart = 0; + } + + if (read_n == 0) { + /* get rid of data in stderr stream */ + ret = ssh_channel_read(sess->channel, + sess->rbuf, + VIR_NET_LIBSSH_BUFFER_SIZE - 1, + 1); + if (ret > 0) { + sess->rbuf[ret] = '\0'; + VIR_DEBUG("flushing stderr, data='%s'", sess->rbuf); + } + } + + if (ssh_channel_is_eof(sess->channel)) { + if (ssh_channel_get_exit_status(sess->channel)) { + virReportError(VIR_ERR_LIBSSH, + _("Remote command terminated with non-zero code: %d"), + ssh_channel_get_exit_status(sess->channel)); + sess->channelCommandReturnValue = ssh_channel_get_exit_status(sess->channel); + sess->state = VIR_NET_LIBSSH_STATE_ERROR_REMOTE; + virObjectUnlock(sess); + return -1; + } + + sess->state = VIR_NET_LIBSSH_STATE_CLOSED; + virObjectUnlock(sess); + return -1; + } + + success: + virObjectUnlock(sess); + return read_n; + + error: + sess->state = VIR_NET_LIBSSH_STATE_ERROR; + virObjectUnlock(sess); + return ret; +} + +ssize_t +virNetLibsshChannelWrite(virNetLibsshSessionPtr sess, + const char *buf, + size_t len) +{ + ssize_t ret; + + virObjectLock(sess); + + if (sess->state != VIR_NET_LIBSSH_STATE_HANDSHAKE_COMPLETE) { + if (sess->state == VIR_NET_LIBSSH_STATE_ERROR_REMOTE) + virReportError(VIR_ERR_LIBSSH, + _("Remote program terminated with non-zero code: %d"), + sess->channelCommandReturnValue); + else + virReportError(VIR_ERR_LIBSSH, "%s", + _("Tried to write socket in error state")); + ret = -1; + goto cleanup; + } + + if (ssh_channel_is_eof(sess->channel)) { + if (ssh_channel_get_exit_status(sess->channel)) { + virReportError(VIR_ERR_LIBSSH, + _("Remote program terminated with non-zero code: %d"), + ssh_channel_get_exit_status(sess->channel)); + sess->state = VIR_NET_LIBSSH_STATE_ERROR_REMOTE; + sess->channelCommandReturnValue = ssh_channel_get_exit_status(sess->channel); + + ret = -1; + goto cleanup; + } + + sess->state = VIR_NET_LIBSSH_STATE_CLOSED; + ret = -1; + goto cleanup; + } + + ret = ssh_channel_write(sess->channel, buf, len); + if (ret == SSH_AGAIN) { + ret = 0; + goto cleanup; + } + + if (ret < 0) { + const char *msg; + sess->state = VIR_NET_LIBSSH_STATE_ERROR; + msg = ssh_get_error(sess->session); + virReportError(VIR_ERR_LIBSSH, + _("write failed: %s"), msg); + } + + cleanup: + virObjectUnlock(sess); + return ret; +} + +bool +virNetLibsshSessionHasCachedData(virNetLibsshSessionPtr sess) +{ + bool ret; + + if (!sess) + return false; + + virObjectLock(sess); + + ret = sess->bufUsed > 0; + + virObjectUnlock(sess); + return ret; +}
Please make sure you run "make check" and "make syntax-check". I've got the following failures: GEN spacing-check Curly brackets around single-line body: src/rpc/virnetlibsshsession.c:250-252: if (type == VIR_CRED_ECHOPROMPT) { return type; } maint.mk: incorrect formatting, see HACKING for rules --- ./po/POTFILES.in +++ ./po/POTFILES.in @@ -144,6 +144,7 @@ src/rpc/virnetclientprogram.c src/rpc/virnetclientstream.c src/rpc/virnetdaemon.c +src/rpc/virnetlibsshsession.c src/rpc/virnetmessage.c src/rpc/virnetsaslcontext.c src/rpc/virnetserver.c maint.mk: you have changed the set of files with translatable diagnostics; apply the above patch The rest looks reasonable. Peter

On Tuesday, 18 October 2016 15:15:07 CEST Peter Krempa wrote:
On Mon, Oct 17, 2016 at 16:24:53 +0200, Pino Toscano wrote:
Implement a new libssh transport, which uses libssh to communicate with remote hosts, and use it in virNetSockets.
This new transport supports all the common ssh authentication methods, making use of libvirt's auth callbacks for interaction with the user.
Most of the functionalities and implementation are based on the libssh2 transport. --- config-post.h | 2 + configure.ac | 3 + include/libvirt/virterror.h | 2 + m4/virt-libssh.m4 | 26 + src/Makefile.am | 21 +- src/libvirt_libssh.syms | 22 + src/remote/remote_driver.c | 41 ++ src/rpc/virnetclient.c | 123 ++++ src/rpc/virnetclient.h | 13 + src/rpc/virnetlibsshsession.c | 1424 +++++++++++++++++++++++++++++++++++++++++ src/rpc/virnetlibsshsession.h | 80 +++ src/rpc/virnetsocket.c | 179 ++++++ src/rpc/virnetsocket.h | 13 + src/util/virerror.c | 9 +- 14 files changed, 1955 insertions(+), 3 deletions(-) create mode 100644 m4/virt-libssh.m4 create mode 100644 src/libvirt_libssh.syms create mode 100644 src/rpc/virnetlibsshsession.c create mode 100644 src/rpc/virnetlibsshsession.h
Is it possible to split out the src/rpc/virnetlibsshsession.(ch) changes (plus the ones necessary to compile it) from adding all the bits that actually make use of it? This patch is very big.
Yes, it is -- I was not sure how much should the changes be split, especially in cases like this when adding a new "thing" which is used only in a single place later on. Just to be sure, a reasonable split for this patch would be: 1) add libssh bits in virerror 2) add virnetlibsshsession.(ch) + autofoo stuff for libssh 3) add glue code in virNetSocket + virNetClient or were you thinking about something else? (no problems on my side)
diff --git a/src/rpc/virnetclient.c b/src/rpc/virnetclient.c index 361dc1a..b296aac 100644 --- a/src/rpc/virnetclient.c +++ b/src/rpc/virnetclient.c @@ -505,6 +505,129 @@ virNetClientPtr virNetClientNewLibSSH2(const char *host, } #undef DEFAULT_VALUE
+#define DEFAULT_VALUE(VAR, VAL) \ + if (!VAR) \ + VAR = VAL; +virNetClientPtr virNetClientNewLibssh(const char *host, + const char *port, + int family, + const char *username, + const char *privkeyPath, + const char *knownHostsPath, + const char *knownHostsVerify, + const char *authMethods, + const char *netcatPath, + const char *socketPath, + virConnectAuthPtr authPtr, + virURIPtr uri) +{ + virNetSocketPtr sock = NULL; + virNetClientPtr ret = NULL; + + virBuffer buf = VIR_BUFFER_INITIALIZER; + char *nc = NULL; + char *command = NULL; + + char *homedir = virGetUserDirectory(); + char *confdir = virGetUserConfigDirectory(); + char *knownhosts = NULL; + char *privkey = NULL; + + /* Use default paths for known hosts an public keys if not provided */
So is libssh able to handle e.g. ECDSA keys in known hosts? Libssh2 was not and truncated the known hosts file which was not acceptable.
Yes, it is. For example in my tests I'm passing known_hosts=$HOME/.ssh/known_hosts as additional query item to the qemu+libssh URLs, and it works well.
+ if (confdir) { + if (!knownHostsPath) { + if (virFileExists(confdir)) { + virBufferAsprintf(&buf, "%s/known_hosts", confdir); + if (!(knownhosts = virBufferContentAndReset(&buf)))
Use virAsprintf instead of the two lines above.
OK. Small side note: all the glue code in virNetSocket, virNetClient and remote_driver.c was basically a copy&paste from the libssh2 implementation, so all these fixes should be done there too.
+ memset(&retr_passphrase, 0, sizeof(virConnectCredential)); + retr_passphrase.type = virCredTypeForPrompt(sess->cred, echo); + retr_passphrase.prompt = virBufferCurrentContent(&buff);
This shouldn't really be used. Please get the content into a variable.
Not a problem to change this, but could you please explain a bit more on the reasons? (So I can avoid doing the same again.)
+ p = virStrncpy(buf, retr_passphrase.result, + retr_passphrase.resultlen, len); + VIR_FREE(retr_passphrase.result);
Since this contains the passprhase, it should be overwritten. Please use one of the VIR_DISPOSE... helpers.
OK. Note that the libssh2 transport has similar issues.
+ if (!p) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("authentication buffer too long for provided " + "passphrase"));
The passphrase is too long for the buffer.
+ return -1; + } + + return 0; +} + +/* perform private key authentication + * + * returns SSH_AUTH_* values + */ +static int +virNetLibsshAuthenticatePrivkey(virNetLibsshSessionPtr sess, + virNetLibsshAuthMethodPtr priv) +{ + int err; + int ret; + char *tmp = NULL; + ssh_key public_key = NULL; + ssh_key private_key = NULL; + + VIR_DEBUG("sess=%p", sess); + + /* try open the key with the password set, first */ + ret = ssh_pki_import_privkey_file(priv->filename, priv->password, + virNetLibsshAuthenticatePrivkeyCb, + sess, &private_key); + if (ret == SSH_EOF) { + virReportError(VIR_ERR_AUTH_FAILED, + _("error while reading private key '%s'"), + priv->filename);
Isn't this overwriting an error reported by the callback trying to open the file?
Not in this case, ...
+ err = SSH_AUTH_ERROR; + goto error; + } else if (ret == SSH_ERROR) { + virReportError(VIR_ERR_AUTH_FAILED, + _("error while opening private key '%s', wrong " + "passphrase?"), + priv->filename);
Same here.
... but it can happen in this case, and when the callback fails. Would it be acceptable to call virResetLastError() before calling ssh_pki_import_privkey_file(), then check virGetLastError()->code to see whether an error was set and if not set the above one?
+int +virNetLibsshSessionConnect(virNetLibsshSessionPtr sess, + int sock) +{ + int ret; + const char *errmsg; + + VIR_DEBUG("sess=%p, sock=%d", sess, sock); + + if (!sess || sess->state != VIR_NET_LIBSSH_STATE_NEW) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("Invalid virNetLibsshSessionPtr")); + return -1; + } + + virObjectLock(sess); + + /* check if configuration is valid */ + if ((ret = virNetLibsshValidateConfig(sess)) < 0) + goto error; + + /* read ~/.ssh/config */ + if ((ret = ssh_options_parse_config(sess->session, NULL)) < 0) + goto error; + + /* set the socket FD for the libssh session */ + if ((ret = ssh_options_set(sess->session, SSH_OPTIONS_FD, &sock)) < 0)
Is this guaranteed to copy the socket number at call time? Otherwise (similarly to the ones above will not work reliably).
I don't understand this sentence (it seems truncated), can you please rephrase it? All the rest of the issues have been fixed, waiting for the feeback wrt the splitting before sending v3. Thanks, -- Pino Toscano

On Tue, Oct 18, 2016 at 16:42:26 +0200, Pino Toscano wrote:
On Tuesday, 18 October 2016 15:15:07 CEST Peter Krempa wrote:
On Mon, Oct 17, 2016 at 16:24:53 +0200, Pino Toscano wrote:
Implement a new libssh transport, which uses libssh to communicate with remote hosts, and use it in virNetSockets.
This new transport supports all the common ssh authentication methods, making use of libvirt's auth callbacks for interaction with the user.
Most of the functionalities and implementation are based on the libssh2 transport. --- config-post.h | 2 + configure.ac | 3 + include/libvirt/virterror.h | 2 + m4/virt-libssh.m4 | 26 + src/Makefile.am | 21 +- src/libvirt_libssh.syms | 22 + src/remote/remote_driver.c | 41 ++ src/rpc/virnetclient.c | 123 ++++ src/rpc/virnetclient.h | 13 + src/rpc/virnetlibsshsession.c | 1424 +++++++++++++++++++++++++++++++++++++++++ src/rpc/virnetlibsshsession.h | 80 +++ src/rpc/virnetsocket.c | 179 ++++++ src/rpc/virnetsocket.h | 13 + src/util/virerror.c | 9 +- 14 files changed, 1955 insertions(+), 3 deletions(-) create mode 100644 m4/virt-libssh.m4 create mode 100644 src/libvirt_libssh.syms create mode 100644 src/rpc/virnetlibsshsession.c create mode 100644 src/rpc/virnetlibsshsession.h
Is it possible to split out the src/rpc/virnetlibsshsession.(ch) changes (plus the ones necessary to compile it) from adding all the bits that actually make use of it? This patch is very big.
Yes, it is -- I was not sure how much should the changes be split, especially in cases like this when adding a new "thing" which is used only in a single place later on.
Just to be sure, a reasonable split for this patch would be: 1) add libssh bits in virerror 2) add virnetlibsshsession.(ch) + autofoo stuff for libssh 3) add glue code in virNetSocket + virNetClient or were you thinking about something else? (no problems on my side)
No, this is exactly the split I was thinking about.
diff --git a/src/rpc/virnetclient.c b/src/rpc/virnetclient.c index 361dc1a..b296aac 100644 --- a/src/rpc/virnetclient.c +++ b/src/rpc/virnetclient.c @@ -505,6 +505,129 @@ virNetClientPtr virNetClientNewLibSSH2(const char *host, } #undef DEFAULT_VALUE
+#define DEFAULT_VALUE(VAR, VAL) \ + if (!VAR) \ + VAR = VAL; +virNetClientPtr virNetClientNewLibssh(const char *host, + const char *port, + int family, + const char *username, + const char *privkeyPath, + const char *knownHostsPath, + const char *knownHostsVerify, + const char *authMethods, + const char *netcatPath, + const char *socketPath, + virConnectAuthPtr authPtr, + virURIPtr uri) +{ + virNetSocketPtr sock = NULL; + virNetClientPtr ret = NULL; + + virBuffer buf = VIR_BUFFER_INITIALIZER; + char *nc = NULL; + char *command = NULL; + + char *homedir = virGetUserDirectory(); + char *confdir = virGetUserConfigDirectory(); + char *knownhosts = NULL; + char *privkey = NULL; + + /* Use default paths for known hosts an public keys if not provided */
So is libssh able to handle e.g. ECDSA keys in known hosts? Libssh2 was not and truncated the known hosts file which was not acceptable.
Yes, it is. For example in my tests I'm passing known_hosts=$HOME/.ssh/known_hosts as additional query item to the qemu+libssh URLs, and it works well.
Cool!
+ if (confdir) { + if (!knownHostsPath) { + if (virFileExists(confdir)) { + virBufferAsprintf(&buf, "%s/known_hosts", confdir); + if (!(knownhosts = virBufferContentAndReset(&buf)))
Use virAsprintf instead of the two lines above.
OK.
Small side note: all the glue code in virNetSocket, virNetClient and remote_driver.c was basically a copy&paste from the libssh2 implementation, so all these fixes should be done there too.
Hmm, I'm wondering whether that code was refactored or was wrong from the beginning :). Anyways I'll probably visit it and clean it up.
+ memset(&retr_passphrase, 0, sizeof(virConnectCredential)); + retr_passphrase.type = virCredTypeForPrompt(sess->cred, echo); + retr_passphrase.prompt = virBufferCurrentContent(&buff);
This shouldn't really be used. Please get the content into a variable.
Not a problem to change this, but could you please explain a bit more on the reasons? (So I can avoid doing the same again.)
Basically any call any function that uses the buffer invalidates the pointer returned by virBufferCurrentContent (or should be treated as such) which is prone to bugs when later modifying the code. Most of the uses of the function are in special cases (or should be removed).
+ p = virStrncpy(buf, retr_passphrase.result, + retr_passphrase.resultlen, len); + VIR_FREE(retr_passphrase.result);
Since this contains the passprhase, it should be overwritten. Please use one of the VIR_DISPOSE... helpers.
OK.
Note that the libssh2 transport has similar issues.
Yep, I'll try to scrub them in a follow up.
+ if (!p) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("authentication buffer too long for provided " + "passphrase"));
The passphrase is too long for the buffer.
+ return -1; + } + + return 0; +} + +/* perform private key authentication + * + * returns SSH_AUTH_* values + */ +static int +virNetLibsshAuthenticatePrivkey(virNetLibsshSessionPtr sess, + virNetLibsshAuthMethodPtr priv) +{ + int err; + int ret; + char *tmp = NULL; + ssh_key public_key = NULL; + ssh_key private_key = NULL; + + VIR_DEBUG("sess=%p", sess); + + /* try open the key with the password set, first */ + ret = ssh_pki_import_privkey_file(priv->filename, priv->password, + virNetLibsshAuthenticatePrivkeyCb, + sess, &private_key); + if (ret == SSH_EOF) { + virReportError(VIR_ERR_AUTH_FAILED, + _("error while reading private key '%s'"), + priv->filename);
Isn't this overwriting an error reported by the callback trying to open the file?
Not in this case, ...
+ err = SSH_AUTH_ERROR; + goto error; + } else if (ret == SSH_ERROR) { + virReportError(VIR_ERR_AUTH_FAILED, + _("error while opening private key '%s', wrong " + "passphrase?"), + priv->filename);
Same here.
... but it can happen in this case, and when the callback fails. Would it be acceptable to call virResetLastError() before calling ssh_pki_import_privkey_file(), then check virGetLastError()->code to see whether an error was set and if not set the above one?
So the callback is not guaranteed to be called at this point? Resetting the error should not be necessary as you should not get into a situation where the error was reported prior to entering this function. Checking whether an error was reported is fine if you are not certain that the callback worked.
+int +virNetLibsshSessionConnect(virNetLibsshSessionPtr sess, + int sock) +{ + int ret; + const char *errmsg; + + VIR_DEBUG("sess=%p, sock=%d", sess, sock); + + if (!sess || sess->state != VIR_NET_LIBSSH_STATE_NEW) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("Invalid virNetLibsshSessionPtr")); + return -1; + } + + virObjectLock(sess); + + /* check if configuration is valid */ + if ((ret = virNetLibsshValidateConfig(sess)) < 0) + goto error; + + /* read ~/.ssh/config */ + if ((ret = ssh_options_parse_config(sess->session, NULL)) < 0) + goto error; + + /* set the socket FD for the libssh session */ + if ((ret = ssh_options_set(sess->session, SSH_OPTIONS_FD, &sock)) < 0)
Is this guaranteed to copy the socket number at call time? Otherwise (similarly to the ones above will not work reliably).
I don't understand this sentence (it seems truncated), can you please rephrase it?
Sorry I probably got sidetracked and did not finish my thought. My question was whether ssh_options_set accesses the pointer to 'sock' right away and copies it. You are passing a pointer to a stack allocated variable which will get out of scope later, thus the pointer should not be accessed after the call to ssh_options_set returns. It's used at least in two places in this patch.
All the rest of the issues have been fixed, waiting for the feeback wrt the splitting before sending v3.
Thanks, -- Pino Toscano
-- libvir-list mailing list libvir-list@redhat.com https://www.redhat.com/mailman/listinfo/libvir-list

On Wednesday, 19 October 2016 08:33:27 CEST Peter Krempa wrote:
+ memset(&retr_passphrase, 0, sizeof(virConnectCredential)); + retr_passphrase.type = virCredTypeForPrompt(sess->cred, echo); + retr_passphrase.prompt = virBufferCurrentContent(&buff);
This shouldn't really be used. Please get the content into a variable.
Not a problem to change this, but could you please explain a bit more on the reasons? (So I can avoid doing the same again.)
Basically any call any function that uses the buffer invalidates the pointer returned by virBufferCurrentContent (or should be treated as such) which is prone to bugs when later modifying the code.
Most of the uses of the function are in special cases (or should be removed).
I see, thanks for the explanation.
+int +virNetLibsshSessionConnect(virNetLibsshSessionPtr sess, + int sock) +{ + int ret; + const char *errmsg; + + VIR_DEBUG("sess=%p, sock=%d", sess, sock); + + if (!sess || sess->state != VIR_NET_LIBSSH_STATE_NEW) { + virReportError(VIR_ERR_LIBSSH, "%s", + _("Invalid virNetLibsshSessionPtr")); + return -1; + } + + virObjectLock(sess); + + /* check if configuration is valid */ + if ((ret = virNetLibsshValidateConfig(sess)) < 0) + goto error; + + /* read ~/.ssh/config */ + if ((ret = ssh_options_parse_config(sess->session, NULL)) < 0) + goto error; + + /* set the socket FD for the libssh session */ + if ((ret = ssh_options_set(sess->session, SSH_OPTIONS_FD, &sock)) < 0)
Is this guaranteed to copy the socket number at call time? Otherwise (similarly to the ones above will not work reliably).
I don't understand this sentence (it seems truncated), can you please rephrase it?
Sorry I probably got sidetracked and did not finish my thought.
My question was whether ssh_options_set accesses the pointer to 'sock' right away and copies it. You are passing a pointer to a stack allocated variable which will get out of scope later, thus the pointer should not be accessed after the call to ssh_options_set returns.
libssh indeed copies the values passed to ssh_options_set, so the two calls you mentioned (for SSH_OPTIONS_FD and SSH_OPTIONS_PORT) are safe. -- Pino Toscano

On Mon, Oct 17, 2016 at 04:24:53PM +0200, Pino Toscano wrote:
Implement a new libssh transport, which uses libssh to communicate with remote hosts, and use it in virNetSockets.
This new transport supports all the common ssh authentication methods, making use of libvirt's auth callbacks for interaction with the user.
Most of the functionalities and implementation are based on the libssh2 transport. --- config-post.h | 2 + configure.ac | 3 + include/libvirt/virterror.h | 2 + m4/virt-libssh.m4 | 26 + src/Makefile.am | 21 +- src/libvirt_libssh.syms | 22 + src/remote/remote_driver.c | 41 ++ src/rpc/virnetclient.c | 123 ++++ src/rpc/virnetclient.h | 13 + src/rpc/virnetlibsshsession.c | 1424 +++++++++++++++++++++++++++++++++++++++++ src/rpc/virnetlibsshsession.h | 80 +++ src/rpc/virnetsocket.c | 179 ++++++ src/rpc/virnetsocket.h | 13 + src/util/virerror.c | 9 +- 14 files changed, 1955 insertions(+), 3 deletions(-) create mode 100644 m4/virt-libssh.m4 create mode 100644 src/libvirt_libssh.syms create mode 100644 src/rpc/virnetlibsshsession.c create mode 100644 src/rpc/virnetlibsshsession.h
libvirt.spec.in and mingw-libvirt.spec.in need updating too, as well as docs/remote.html.in
+static virClassPtr virNetLibsshSessionClass; +static int +virNetLibsshSessionOnceInit(void) +{ + const char *dbgLevelStr; + + if (!(virNetLibsshSessionClass = virClassNew(virClassForObjectLockable(), + "virNetLibsshSession", + sizeof(virNetLibsshSession), + virNetLibsshSessionDispose))) + return -1; + + if (ssh_init() < 0) + return -1;
You need to report a libvirt error here. Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://entangle-photo.org -o- http://search.cpan.org/~danberr/ :|

On Tuesday, 18 October 2016 14:19:41 CEST Daniel P. Berrange wrote:
On Mon, Oct 17, 2016 at 04:24:53PM +0200, Pino Toscano wrote:
Implement a new libssh transport, which uses libssh to communicate with remote hosts, and use it in virNetSockets.
This new transport supports all the common ssh authentication methods, making use of libvirt's auth callbacks for interaction with the user.
Most of the functionalities and implementation are based on the libssh2 transport. --- config-post.h | 2 + configure.ac | 3 + include/libvirt/virterror.h | 2 + m4/virt-libssh.m4 | 26 + src/Makefile.am | 21 +- src/libvirt_libssh.syms | 22 + src/remote/remote_driver.c | 41 ++ src/rpc/virnetclient.c | 123 ++++ src/rpc/virnetclient.h | 13 + src/rpc/virnetlibsshsession.c | 1424 +++++++++++++++++++++++++++++++++++++++++ src/rpc/virnetlibsshsession.h | 80 +++ src/rpc/virnetsocket.c | 179 ++++++ src/rpc/virnetsocket.h | 13 + src/util/virerror.c | 9 +- 14 files changed, 1955 insertions(+), 3 deletions(-) create mode 100644 m4/virt-libssh.m4 create mode 100644 src/libvirt_libssh.syms create mode 100644 src/rpc/virnetlibsshsession.c create mode 100644 src/rpc/virnetlibsshsession.h
libvirt.spec.in and mingw-libvirt.spec.in need updating too, as well as docs/remote.html.in
OK for libvirt.spec.in and docs/remote.html.in, but libssh for mingw is not available in Fedora. Thanks, -- Pino Toscano

On Tue, Oct 18, 2016 at 04:46:53PM +0200, Pino Toscano wrote:
On Tuesday, 18 October 2016 14:19:41 CEST Daniel P. Berrange wrote:
On Mon, Oct 17, 2016 at 04:24:53PM +0200, Pino Toscano wrote:
Implement a new libssh transport, which uses libssh to communicate with remote hosts, and use it in virNetSockets.
This new transport supports all the common ssh authentication methods, making use of libvirt's auth callbacks for interaction with the user.
Most of the functionalities and implementation are based on the libssh2 transport. --- config-post.h | 2 + configure.ac | 3 + include/libvirt/virterror.h | 2 + m4/virt-libssh.m4 | 26 + src/Makefile.am | 21 +- src/libvirt_libssh.syms | 22 + src/remote/remote_driver.c | 41 ++ src/rpc/virnetclient.c | 123 ++++ src/rpc/virnetclient.h | 13 + src/rpc/virnetlibsshsession.c | 1424 +++++++++++++++++++++++++++++++++++++++++ src/rpc/virnetlibsshsession.h | 80 +++ src/rpc/virnetsocket.c | 179 ++++++ src/rpc/virnetsocket.h | 13 + src/util/virerror.c | 9 +- 14 files changed, 1955 insertions(+), 3 deletions(-) create mode 100644 m4/virt-libssh.m4 create mode 100644 src/libvirt_libssh.syms create mode 100644 src/rpc/virnetlibsshsession.c create mode 100644 src/rpc/virnetlibsshsession.h
libvirt.spec.in and mingw-libvirt.spec.in need updating too, as well as docs/remote.html.in
OK for libvirt.spec.in and docs/remote.html.in, but libssh for mingw is not available in Fedora.
Is it possible to build libssh for mingw ? If so we'll need to look at getting that into Fedora. Regards, Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://entangle-photo.org -o- http://search.cpan.org/~danberr/ :|

On Tuesday, 18 October 2016 15:49:56 CEST Daniel P. Berrange wrote:
On Tue, Oct 18, 2016 at 04:46:53PM +0200, Pino Toscano wrote:
On Tuesday, 18 October 2016 14:19:41 CEST Daniel P. Berrange wrote:
On Mon, Oct 17, 2016 at 04:24:53PM +0200, Pino Toscano wrote:
Implement a new libssh transport, which uses libssh to communicate with remote hosts, and use it in virNetSockets.
This new transport supports all the common ssh authentication methods, making use of libvirt's auth callbacks for interaction with the user.
Most of the functionalities and implementation are based on the libssh2 transport. --- config-post.h | 2 + configure.ac | 3 + include/libvirt/virterror.h | 2 + m4/virt-libssh.m4 | 26 + src/Makefile.am | 21 +- src/libvirt_libssh.syms | 22 + src/remote/remote_driver.c | 41 ++ src/rpc/virnetclient.c | 123 ++++ src/rpc/virnetclient.h | 13 + src/rpc/virnetlibsshsession.c | 1424 +++++++++++++++++++++++++++++++++++++++++ src/rpc/virnetlibsshsession.h | 80 +++ src/rpc/virnetsocket.c | 179 ++++++ src/rpc/virnetsocket.h | 13 + src/util/virerror.c | 9 +- 14 files changed, 1955 insertions(+), 3 deletions(-) create mode 100644 m4/virt-libssh.m4 create mode 100644 src/libvirt_libssh.syms create mode 100644 src/rpc/virnetlibsshsession.c create mode 100644 src/rpc/virnetlibsshsession.h
libvirt.spec.in and mingw-libvirt.spec.in need updating too, as well as docs/remote.html.in
OK for libvirt.spec.in and docs/remote.html.in, but libssh for mingw is not available in Fedora.
Is it possible to build libssh for mingw ? If so we'll need to look at getting that into Fedora.
I don't know -- I can request that to its maintainer once the libssh transport is in (so there is an actual use case for adding mingw support). At least, libssh is supposed to build and work on Windows as well. -- Pino Toscano
participants (3)
-
Daniel P. Berrange
-
Peter Krempa
-
Pino Toscano