libvirt/src/rpc/virnetlibsshsession.c

1499 lines
42 KiB
C
Raw Normal View History

/*
* 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
} virNetLibsshAuthMethods;
typedef struct _virNetLibsshAuthMethod virNetLibsshAuthMethod;
typedef virNetLibsshAuthMethod *virNetLibsshAuthMethodPtr;
struct _virNetLibsshAuthMethod {
virNetLibsshAuthMethods method;
int ssh_flags; /* SSH_AUTH_METHOD_* for this auth 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;
size_t nauths;
virNetLibsshAuthMethodPtr *auths;
/* channel stuff */
char *channelCommand;
int channelCommandReturnValue;
/* read cache */
char rbuf[VIR_NET_LIBSSH_BUFFER_SIZE];
size_t bufUsed;
size_t bufStart;
};
static void
virNetLibsshSessionAuthMethodsFree(virNetLibsshSessionPtr sess)
{
size_t i;
for (i = 0; i < sess->nauths; i++) {
VIR_DISPOSE_STRING(sess->auths[i]->password);
VIR_FREE(sess->auths[i]->filename);
VIR_FREE(sess->auths[i]);
}
VIR_FREE(sess->auths);
sess->nauths = 0;
}
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 (!VIR_CLASS_NEW(virNetLibsshSession, virClassForObjectLockable()))
return -1;
if (ssh_init() < 0) {
virReportError(VIR_ERR_LIBSSH, "%s",
_("failed to initialize libssh"));
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)
{
virNetLibsshAuthMethodPtr auth;
if (VIR_ALLOC(auth) < 0)
goto error;
if (VIR_EXPAND_N(sess->auths, sess->nauths, 1) < 0)
goto error;
sess->auths[sess->nauths - 1] = auth;
return auth;
error:
VIR_FREE(auth);
return NULL;
}
/* string representation of public key of remote server */
static char *
virLibsshServerKeyAsString(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 SHA1 algorithm that is
* usual in OpenSSH. The returned value must be freed */
ret = ssh_get_publickey_hash(key, SSH_PUBLICKEY_HASH_SHA1,
&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)
{
size_t 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 = virLibsshServerKeyAsString(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;
int cred_type;
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;
}
cred_type = virCredTypeForPrompt(sess->cred, 1 /* echo */);
if (cred_type == -1) {
virReportError(VIR_ERR_LIBSSH, "%s",
_("no suitable callback for host key "
"verification"));
return -1;
}
/* prepare data for the callback */
memset(&askKey, 0, sizeof(virConnectCredential));
askKey.type = cred_type;
keyhashstr = virLibsshServerKeyAsString(sess);
if (!keyhashstr)
return -1;
if (virAsprintf(&tmp,
_("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;
}
askKey.prompt = tmp;
if (sess->cred->cb(&askKey, 1, sess->cred->cbdata)) {
virReportError(VIR_ERR_LIBSSH, "%s",
_("failed to retrieve decision to accept "
"host key"));
VIR_FREE(tmp);
ssh_string_free_char(keyhashstr);
return -1;
}
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 specified */
if (sess->knownHostsFile) {
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 state of the remote server SSH key"));
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;
int cred_type;
char *actual_prompt = NULL;
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;
}
cred_type = virCredTypeForPrompt(sess->cred, echo);
if (cred_type == -1) {
virReportError(VIR_ERR_LIBSSH, "%s",
_("no suitable callback for input of key passphrase"));
goto error;
}
if (VIR_STRNDUP(actual_prompt, prompt,
virLengthForPromptString(prompt)) < 0)
goto error;
memset(&retr_passphrase, 0, sizeof(virConnectCredential));
retr_passphrase.type = cred_type;
retr_passphrase.prompt = actual_prompt;
if (sess->cred->cb(&retr_passphrase, 1, sess->cred->cbdata)) {
virReportError(VIR_ERR_LIBSSH, "%s",
_("failed to retrieve private key passphrase: "
"callback has failed"));
goto error;
}
p = virStrncpy(buf, retr_passphrase.result,
retr_passphrase.resultlen, len);
VIR_DISPOSE_STRING(retr_passphrase.result);
if (!p) {
virReportError(VIR_ERR_LIBSSH, "%s",
_("passphrase is too long for the buffer"));
goto error;
}
VIR_FREE(actual_prompt);
return 0;
error:
VIR_FREE(actual_prompt);
return -1;
}
static int
virNetLibsshImportPrivkey(virNetLibsshSessionPtr sess,
virNetLibsshAuthMethodPtr priv,
ssh_key *ret_key)
{
int err;
int ret;
ssh_key key;
/* try open the key with the password set, first; since it can
* fail with SSH_ERROR also without the callback being called,
* reset the error so it is possible to check whether the callback
* failed or libssh did.
*/
virResetLastError();
ret = ssh_pki_import_privkey_file(priv->filename, priv->password,
virNetLibsshAuthenticatePrivkeyCb,
sess, &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) {
virErrorPtr vir_err = virGetLastError();
if (!vir_err || !vir_err->code) {
virReportError(VIR_ERR_AUTH_FAILED,
_("error while opening private key '%s', wrong "
"passphrase?"),
priv->filename);
}
err = SSH_AUTH_ERROR;
goto error;
}
*ret_key = key;
return SSH_AUTH_SUCCESS;
error:
return err;
}
/* 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);
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) {
/* load the private key */
err = virNetLibsshImportPrivkey(sess, priv, &private_key);
if (err != SSH_AUTH_SUCCESS)
goto error;
/* 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'"),
priv->filename);
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;
}
/* load the private key, if it was not loaded yet */
if (private_key == NULL) {
err = virNetLibsshImportPrivkey(sess, priv, &private_key);
if (err != SSH_AUTH_SUCCESS)
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_DISPOSE_STRING(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_DISPOSE_STRING(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 (virBufferUse(&buff) > 0)
virBufferAddChar(&buff, '\n');
if (virBufferCheckError(&buff) < 0)
return -1;
for (iprompt = 0; iprompt < nprompts; ++iprompt) {
virConnectCredential retr_passphrase;
const char *promptStr;
int promptStrLen;
char echo;
char *prompt = NULL;
int cred_type;
/* get the prompt */
promptStr = ssh_userauth_kbdint_getprompt(sess->session, iprompt,
&echo);
promptStrLen = virLengthForPromptString(promptStr);
cred_type = virCredTypeForPrompt(sess->cred, echo);
if (cred_type == -1) {
virReportError(VIR_ERR_LIBSSH, "%s",
_("no suitable callback for input of keyboard "
"response"));
goto prompt_error;
}
/* create the prompt for the user, using the instruction
* buffer if specified
*/
if (virBufferUse(&buff) > 0) {
virBuffer prompt_buff = VIR_BUFFER_INITIALIZER;
virBufferAddBuffer(&prompt_buff, &buff);
virBufferAdd(&prompt_buff, promptStr, promptStrLen);
if (virBufferCheckError(&prompt_buff) < 0)
goto prompt_error;
prompt = virBufferContentAndReset(&prompt_buff);
} else {
if (VIR_STRNDUP(prompt, promptStr, promptStrLen) < 0)
goto prompt_error;
}
memset(&retr_passphrase, 0, sizeof(virConnectCredential));
retr_passphrase.type = cred_type;
retr_passphrase.prompt = prompt;
if (retr_passphrase.type == -1) {
virReportError(VIR_ERR_LIBSSH, "%s",
_("no suitable callback for input of key "
"passphrase"));
goto prompt_error;
}
if (sess->cred->cb(&retr_passphrase, 1, sess->cred->cbdata)) {
virReportError(VIR_ERR_LIBSSH, "%s",
_("failed to retrieve keyboard interactive "
"result: callback has failed"));
goto prompt_error;
}
VIR_FREE(prompt);
ret = ssh_userauth_kbdint_setanswer(sess->session, iprompt,
retr_passphrase.result);
VIR_DISPOSE_STRING(retr_passphrase.result);
if (ret < 0) {
errmsg = ssh_get_error(sess->session);
virReportError(VIR_ERR_AUTH_FAILED,
_("authentication failed: %s"), errmsg);
goto prompt_error;
}
continue;
prompt_error:
VIR_FREE(prompt);
virBufferFreeAndReset(&buff);
return SSH_AUTH_ERROR;
}
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 no_method = false;
bool auth_failed = false;
const char *errmsg;
int ret;
int methods;
size_t i;
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);
for (i = 0; i < sess->nauths; i++) {
auth = sess->auths[i];
if ((methods & auth->ssh_flags) == 0) {
no_method = true;
continue;
}
ret = SSH_AUTH_DENIED;
switch (auth->method) {
case VIR_NET_LIBSSH_AUTH_KEYBOARD_INTERACTIVE:
/* try to authenticate using the keyboard interactive way */
ret = virNetLibsshAuthenticateKeyboardInteractive(sess, auth);
break;
case VIR_NET_LIBSSH_AUTH_AGENT:
/* try to authenticate using ssh-agent */
ret = ssh_userauth_agent(sess->session, NULL);
if (ret == SSH_AUTH_ERROR) {
errmsg = ssh_get_error(sess->session);
virReportError(VIR_ERR_LIBSSH,
_("failed to authenticate using agent: %s"),
errmsg);
}
break;
case VIR_NET_LIBSSH_AUTH_PRIVKEY:
/* try to authenticate using the provided ssh key */
ret = virNetLibsshAuthenticatePrivkey(sess, auth);
break;
case VIR_NET_LIBSSH_AUTH_PASSWORD:
/* try to authenticate with password */
ret = virNetLibsshAuthenticatePassword(sess, auth);
break;
}
if (ret == SSH_AUTH_ERROR) {
/* virReportError is called already */
return -1;
} else if (ret == SSH_AUTH_SUCCESS) {
/* authenticated */
return 0;
}
auth_failed = true;
}
if (sess->nauths == 1) {
errmsg = ssh_get_error(sess->session);
virReportError(VIR_ERR_LIBSSH,
_("failed to authenticate: %s"),
errmsg);
} else if (no_method && !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 < sess->nauths; ++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;
}
int
virNetLibsshSessionAuthAddPasswordAuth(virNetLibsshSessionPtr sess,
virURIPtr uri)
{
int ret;
virNetLibsshAuthMethodPtr auth;
if (uri) {
VIR_FREE(sess->authPath);
if (virAuthGetConfigFilePathURI(uri, &sess->authPath) < 0) {
ret = -1;
goto cleanup;
}
}
virObjectLock(sess);
if (!(auth = virNetLibsshSessionAuthMethodNew(sess))) {
ret = -1;
goto cleanup;
}
auth->method = VIR_NET_LIBSSH_AUTH_PASSWORD;
auth->ssh_flags = SSH_AUTH_METHOD_PASSWORD;
ret = 0;
cleanup:
virObjectUnlock(sess);
return ret;
}
int
virNetLibsshSessionAuthAddAgentAuth(virNetLibsshSessionPtr sess)
{
int ret;
virNetLibsshAuthMethodPtr auth;
virObjectLock(sess);
if (!(auth = virNetLibsshSessionAuthMethodNew(sess))) {
ret = -1;
goto cleanup;
}
auth->method = VIR_NET_LIBSSH_AUTH_AGENT;
auth->ssh_flags = SSH_AUTH_METHOD_PUBLICKEY;
ret = 0;
cleanup:
virObjectUnlock(sess);
return ret;
}
int
virNetLibsshSessionAuthAddPrivKeyAuth(virNetLibsshSessionPtr sess,
const char *keyfile,
const char *password)
{
int ret;
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"));
ret = -1;
goto error;
}
virObjectLock(sess);
if (VIR_STRDUP(file, keyfile) < 0 ||
VIR_STRDUP(pass, password) < 0) {
ret = -1;
goto error;
}
if (!(auth = virNetLibsshSessionAuthMethodNew(sess))) {
ret = -1;
goto error;
}
auth->password = pass;
auth->filename = file;
auth->method = VIR_NET_LIBSSH_AUTH_PRIVKEY;
auth->ssh_flags = SSH_AUTH_METHOD_PUBLICKEY;
ret = 0;
cleanup:
virObjectUnlock(sess);
return ret;
error:
VIR_DISPOSE_STRING(pass);
VIR_FREE(file);
goto cleanup;
}
int
virNetLibsshSessionAuthAddKeyboardAuth(virNetLibsshSessionPtr sess,
int tries)
{
int ret;
virNetLibsshAuthMethodPtr auth;
virObjectLock(sess);
if (!(auth = virNetLibsshSessionAuthMethodNew(sess))) {
ret = -1;
goto cleanup;
}
auth->tries = tries;
auth->method = VIR_NET_LIBSSH_AUTH_KEYBOARD_INTERACTIVE;
auth->ssh_flags = SSH_AUTH_METHOD_INTERACTIVE;
ret = 0;
cleanup:
virObjectUnlock(sess);
return ret;
}
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 specified */
if (hostsfile) {
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;
} else {
/* libssh does not support trying no known_host file at all:
* hence use /dev/null here, without storing it as file */
if (ssh_options_set(sess->session, SSH_OPTIONS_KNOWNHOSTS, "/dev/null") < 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_nonblocking(sess->channel,
buf + read_n,
len - read_n,
0);
if (ret == SSH_EOF || (ret == 0 && ssh_channel_is_eof(sess->channel)))
goto eof;
if (ret == SSH_AGAIN)
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_nonblocking(sess->channel,
sess->rbuf,
VIR_NET_LIBSSH_BUFFER_SIZE,
0);
if (ret == SSH_EOF || (ret == 0 && ssh_channel_is_eof(sess->channel)))
goto eof;
if (ret == SSH_AGAIN)
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_nonblocking(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)) {
eof:
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;
}