ssh: Improve the error message when all authentication methods fail
[nbdkit.git] / plugins / ssh / ssh.c
blob5a132d8f2319ff34847309759c097aa8ce705b7f
1 /* nbdkit
2 * Copyright (C) 2014-2020 Red Hat Inc.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
11 * * Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
15 * * Neither the name of Red Hat nor the names of its contributors may be
16 * used to endorse or promote products derived from this software without
17 * specific prior written permission.
19 * THIS SOFTWARE IS PROVIDED BY RED HAT AND CONTRIBUTORS ''AS IS'' AND
20 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
21 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
22 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED HAT OR
23 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
26 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
27 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
29 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
30 * SUCH DAMAGE.
33 #include <config.h>
35 #include <stdio.h>
36 #include <stdlib.h>
37 #include <stdbool.h>
38 #include <stdarg.h>
39 #include <stdint.h>
40 #include <inttypes.h>
41 #include <limits.h>
42 #include <string.h>
43 #include <unistd.h>
44 #include <fcntl.h>
45 #include <sys/stat.h>
47 #include <pthread.h>
49 #include <libssh/libssh.h>
50 #include <libssh/sftp.h>
51 #include <libssh/callbacks.h>
53 #include <nbdkit-plugin.h>
55 #include "array-size.h"
56 #include "cleanup.h"
57 #include "const-string-vector.h"
58 #include "minmax.h"
60 static const char *host = NULL;
61 static const char *path = NULL;
62 static const char *port = NULL;
63 static const char *user = NULL;
64 static char *password = NULL;
65 static bool verify_remote_host = true;
66 static const char *known_hosts = NULL;
67 static const_string_vector identities = empty_vector;
68 static uint32_t timeout = 0;
69 static bool compression = false;
70 static bool create = false;
71 static int64_t create_size = -1;
72 static unsigned create_mode = S_IRUSR | S_IWUSR /* 0600 */;
74 /* config can be:
75 * NULL => parse options from default file
76 * "" => do NOT parse options
77 * some filename => parse options from filename
79 static const char *config = NULL;
81 /* Use '-D ssh.log=N' to set. */
82 NBDKIT_DLL_PUBLIC int ssh_debug_log = 0;
84 /* If ssh_debug_log > 0 then the library will call this function with
85 * log messages.
87 static void
88 log_callback (int priority, const char *function, const char *message, void *vp)
90 const char *levels[] =
91 { "none", "warning", "protocol", "packet", "function" };
92 const char *level;
94 if (priority >= 0 && priority < ARRAY_SIZE (levels))
95 level = levels[priority];
96 else
97 level = "unknown";
99 /* NB We don't need to print the function parameter because it is
100 * always prefixed to the message.
102 nbdkit_debug ("libssh: %s: %s", level, message);
105 static void
106 ssh_unload (void)
108 free (identities.ptr);
109 free (password);
112 /* Called for each key=value passed on the command line. */
113 static int
114 ssh_config (const char *key, const char *value)
116 int r;
118 if (strcmp (key, "host") == 0)
119 host = value;
121 else if (strcmp (key, "path") == 0)
122 path = value;
124 else if (strcmp (key, "port") == 0)
125 port = value;
127 else if (strcmp (key, "user") == 0)
128 user = value;
130 else if (strcmp (key, "password") == 0) {
131 free (password);
132 if (nbdkit_read_password (value, &password) == -1)
133 return -1;
136 else if (strcmp (key, "config") == 0)
137 config = value; /* %-expanded, cannot use nbdkit_absolute_path */
139 else if (strcmp (key, "known-hosts") == 0)
140 known_hosts = value; /* %-expanded, cannot use nbdkit_absolute_path */
142 else if (strcmp (key, "identity") == 0) {
143 /* %-expanded, cannot use nbdkit_absolute_path on value */
144 if (const_string_vector_append (&identities, value) == -1) {
145 nbdkit_error ("realloc: %m");
146 return -1;
150 else if (strcmp (key, "verify-remote-host") == 0) {
151 r = nbdkit_parse_bool (value);
152 if (r == -1)
153 return -1;
154 verify_remote_host = r;
157 else if (strcmp (key, "timeout") == 0) {
158 if (nbdkit_parse_uint32_t ("timeout", value, &timeout) == -1)
159 return -1;
160 #if LONG_MAX < UINT32_MAX
161 /* C17 5.2.4.2.1 requires that LONG_MAX is at least 2^31 - 1.
162 * However a large positive number might still exceed the limit.
164 if (timeout > LONG_MAX) {
165 nbdkit_error ("timeout is too large");
166 return -1;
168 #endif
170 else if (strcmp (key, "compression") == 0) {
171 r = nbdkit_parse_bool (value);
172 if (r == -1)
173 return -1;
174 compression = r;
176 else if (strcmp (key, "create") == 0) {
177 r = nbdkit_parse_bool (value);
178 if (r == -1)
179 return -1;
180 create = r;
182 else if (strcmp (key, "create-size") == 0) {
183 create_size = nbdkit_parse_size (value);
184 if (create_size == -1)
185 return -1;
187 else if (strcmp (key, "create-mode") == 0) {
188 r = nbdkit_parse_unsigned (key, value, &create_mode);
189 if (r == -1)
190 return -1;
191 /* OpenSSH checks this too. */
192 if (create_mode > 0777) {
193 nbdkit_error ("create-mode must be <= 0777");
194 return -1;
198 else {
199 nbdkit_error ("unknown parameter '%s'", key);
200 return -1;
203 return 0;
206 /* The host and path parameters are mandatory. */
207 static int
208 ssh_config_complete (void)
210 if (host == NULL || path == NULL) {
211 nbdkit_error ("you must supply the host and path parameters "
212 "after the plugin name on the command line");
213 return -1;
216 /* If create=true, create-size must be supplied. */
217 if (create && create_size == -1) {
218 nbdkit_error ("if using create=true, you must specify the size "
219 "of the new remote file using create-size=SIZE");
220 return -1;
223 return 0;
226 #define ssh_config_help \
227 "host=<HOST> (required) SSH server hostname.\n" \
228 "[path=]<PATH> (required) SSH remote path.\n" \
229 "port=<PORT> SSH protocol port number.\n" \
230 "user=<USER> SSH user name.\n" \
231 "password=<PASSWORD> SSH password.\n" \
232 "config=<CONFIG> Alternate local SSH configuration file.\n" \
233 "known-hosts=<FILENAME> Set location of known_hosts file.\n" \
234 "identity=<FILENAME> Prepend private key (identity) file.\n" \
235 "timeout=SECS Set SSH connection timeout.\n" \
236 "verify-remote-host=false Ignore known_hosts.\n" \
237 "compression=true Enable compression.\n" \
238 "create=true Create the remote file.\n" \
239 "create-mode=MODE Set the permissions of the remote file.\n" \
240 "create-size=SIZE Set the size of the remote file."
242 /* Since we must simulate atomic pread and pwrite using seek +
243 * read/write, calls on each handle must be serialized.
245 #define THREAD_MODEL NBDKIT_THREAD_MODEL_SERIALIZE_REQUESTS
247 /* The per-connection handle. */
248 struct ssh_handle {
249 ssh_session session;
250 sftp_session sftp;
251 sftp_file file;
254 /* Verify the remote host.
255 * See: http://api.libssh.org/master/libssh_tutor_guided_tour.html
257 static int
258 do_verify_remote_host (struct ssh_handle *h)
260 enum ssh_known_hosts_e state;
262 state = ssh_session_is_known_server (h->session);
263 switch (state) {
264 case SSH_KNOWN_HOSTS_OK:
265 /* OK */
266 break;
268 case SSH_KNOWN_HOSTS_CHANGED:
269 nbdkit_error ("host key for server changed");
270 return -1;
272 case SSH_KNOWN_HOSTS_OTHER:
273 nbdkit_error ("host key for server was not found "
274 "but another type of key exists");
275 return -1;
277 case SSH_KNOWN_HOSTS_NOT_FOUND:
278 /* This is not actually an error, but the user must ensure the
279 * host key is set up before using nbdkit so we error out here.
281 nbdkit_error ("could not find known_hosts file");
282 return -1;
284 case SSH_KNOWN_HOSTS_UNKNOWN:
285 /* See: https://gitlab.com/nbdkit/nbdkit/-/issues/10 */
286 nbdkit_error ("either the host key is unknown, you must use ssh "
287 "first and accept the host key; or the known_hosts "
288 "file (usually ~/.ssh/known_hosts) could not be opened, "
289 "check that the file exists and file permissions");
290 return -1;
292 case SSH_KNOWN_HOSTS_ERROR:
293 nbdkit_error ("known hosts error: %s", ssh_get_error (h->session));
294 return -1;
297 return 0;
300 /* Authenticate.
301 * See: http://api.libssh.org/master/libssh_tutor_authentication.html
303 static int
304 authenticate_pubkey (ssh_session session)
306 int rc;
308 rc = ssh_userauth_publickey_auto (session, NULL, NULL);
309 if (rc == SSH_AUTH_ERROR)
310 nbdkit_debug ("public key authentication failed: %s",
311 ssh_get_error (session));
313 return rc;
316 static int
317 authenticate_password (ssh_session session, const char *pass)
319 int rc;
321 rc = ssh_userauth_password (session, NULL, pass);
322 if (rc == SSH_AUTH_ERROR)
323 nbdkit_debug ("password authentication failed: %s",
324 ssh_get_error (session));
325 return rc;
328 static int
329 authenticate (struct ssh_handle *h)
331 int method, rc;
333 rc = ssh_userauth_none (h->session, NULL);
334 if (rc == SSH_AUTH_SUCCESS)
335 return 0;
336 if (rc == SSH_AUTH_ERROR)
337 return -1;
339 method = ssh_userauth_list (h->session, NULL);
340 nbdkit_debug ("authentication methods offered by the server [0x%x]: "
341 "%s%s%s%s%s%s%s",
342 method,
343 method & SSH_AUTH_METHOD_NONE ? " none" : "",
344 method & SSH_AUTH_METHOD_PASSWORD ? " password" : "",
345 method & SSH_AUTH_METHOD_PUBLICKEY ? " publickey" : "",
346 method & SSH_AUTH_METHOD_HOSTBASED ? " hostbased" : "",
347 method & SSH_AUTH_METHOD_INTERACTIVE
348 ? " keyboard-interactive" : "",
349 method & SSH_AUTH_METHOD_GSSAPI_MIC
350 ? " gssapi-with-mic" : "",
351 method & ~0x3f
352 ? " (and other unknown methods)" : "");
354 if (method & SSH_AUTH_METHOD_PUBLICKEY) {
355 rc = authenticate_pubkey (h->session);
356 if (rc == SSH_AUTH_SUCCESS) return 0;
359 if (password != NULL && (method & SSH_AUTH_METHOD_PASSWORD)) {
360 rc = authenticate_password (h->session, password);
361 if (rc == SSH_AUTH_SUCCESS) return 0;
364 /* All compatible methods were tried and none worked. Come up with
365 * an actionable diagnostic message if we recognise the problem.
367 if (!(method & SSH_AUTH_METHOD_PUBLICKEY) && password == NULL) {
368 nbdkit_error ("the server does not offer public key authentication; "
369 "try using the password=... parameter");
370 return -1;
372 if ((method & SSH_AUTH_METHOD_PASSWORD) && password != NULL) {
373 nbdkit_error ("password authentication failed, "
374 "is the username and password correct?");
375 return -1;
377 if (!(method & SSH_AUTH_METHOD_PASSWORD) && password != NULL) {
378 nbdkit_error ("the server does not offer password authentication "
379 "but you tried to use a password; if you have root access "
380 "to the server, try editing 'sshd_config' and setting "
381 "'PasswordAuthentication yes'; otherwise try setting up "
382 "public key authentication");
383 return -1;
386 nbdkit_error ("all possible authentication methods failed");
387 return -1;
390 /* This function opens or creates the remote file (depending on
391 * create=false|true). Parallel connections might call this function
392 * at the same time, and so we must hold a lock to ensure that the
393 * file is created at most once.
395 static pthread_mutex_t create_lock = PTHREAD_MUTEX_INITIALIZER;
397 static sftp_file
398 open_or_create_path (ssh_session session, sftp_session sftp, int readonly)
400 ACQUIRE_LOCK_FOR_CURRENT_SCOPE (&create_lock);
401 int access_type;
402 int r;
403 sftp_file file;
405 access_type = readonly ? O_RDONLY : O_RDWR;
406 if (create) access_type |= O_CREAT | O_TRUNC;
408 file = sftp_open (sftp, path, access_type, S_IRWXU);
409 if (!file) {
410 nbdkit_error ("cannot %s file for %s: %s",
411 create ? "create" : "open",
412 readonly ? "reading" : "writing",
413 ssh_get_error (session));
414 return NULL;
417 if (create) {
418 /* There's no sftp_truncate call. However OpenSSH lets you call
419 * SSH_FXP_SETSTAT + SSH_FILEXFER_ATTR_SIZE which invokes
420 * truncate(2) on the server. Libssh doesn't provide a binding
421 * for SSH_FXP_FSETSTAT so we have to pass the session + path.
423 struct sftp_attributes_struct attrs = {
424 .flags = SSH_FILEXFER_ATTR_SIZE |
425 SSH_FILEXFER_ATTR_PERMISSIONS,
426 .size = create_size,
427 .permissions = create_mode,
430 r = sftp_setstat (sftp, path, &attrs);
431 if (r != SSH_OK) {
432 nbdkit_error ("setstat failed: %s", ssh_get_error (session));
434 /* Best-effort attempt to delete the remote file on failure. */
435 r = sftp_unlink (sftp, path);
436 if (r != SSH_OK)
437 nbdkit_debug ("unlink failed: %s", ssh_get_error (session));
439 return NULL;
443 /* On the next connection, don't create or truncate the file. */
444 create = false;
446 return file;
449 /* Create the per-connection handle. */
450 static void *
451 ssh_open (int readonly)
453 struct ssh_handle *h;
454 const int set = 1;
455 size_t i;
456 int r;
458 h = calloc (1, sizeof *h);
459 if (h == NULL) {
460 nbdkit_error ("calloc: %m");
461 return NULL;
464 /* Set up the SSH session. */
465 h->session = ssh_new ();
466 if (!h->session) {
467 nbdkit_error ("failed to initialize libssh session");
468 goto err;
471 if (ssh_debug_log > 0) {
472 ssh_options_set (h->session, SSH_OPTIONS_LOG_VERBOSITY, &ssh_debug_log);
473 /* Even though this is setting a "global", we must call it every
474 * time we set the session otherwise messages go to stderr.
476 ssh_set_log_callback (log_callback);
479 /* Disable Nagle's algorithm which is recommended by the libssh
480 * developers to improve performance of sftp. Ignore any error if
481 * we fail to set this.
483 ssh_options_set (h->session, SSH_OPTIONS_NODELAY, &set);
485 r = ssh_options_set (h->session, SSH_OPTIONS_HOST, host);
486 if (r != SSH_OK) {
487 nbdkit_error ("failed to set host in libssh session: %s: %s",
488 host, ssh_get_error (h->session));
489 goto err;
491 if (port != NULL) {
492 r = ssh_options_set (h->session, SSH_OPTIONS_PORT_STR, port);
493 if (r != SSH_OK) {
494 nbdkit_error ("failed to set port in libssh session: %s: %s",
495 port, ssh_get_error (h->session));
496 goto err;
499 if (user != NULL) {
500 r = ssh_options_set (h->session, SSH_OPTIONS_USER, user);
501 if (r != SSH_OK) {
502 nbdkit_error ("failed to set user in libssh session: %s: %s",
503 user, ssh_get_error (h->session));
504 goto err;
507 if (known_hosts != NULL) {
508 r = ssh_options_set (h->session, SSH_OPTIONS_KNOWNHOSTS, known_hosts);
509 if (r != SSH_OK) {
510 nbdkit_error ("failed to set known_hosts in libssh session: %s: %s",
511 known_hosts, ssh_get_error (h->session));
512 goto err;
514 /* XXX This is still going to read the global file, and there
515 * seems to be no way to disable that. However it doesn't matter
516 * as this file is rarely present.
519 for (i = 0; i < identities.len; ++i) {
520 r = ssh_options_set (h->session,
521 SSH_OPTIONS_ADD_IDENTITY, identities.ptr[i]);
522 if (r != SSH_OK) {
523 nbdkit_error ("failed to add identity in libssh session: %s: %s",
524 identities.ptr[i], ssh_get_error (h->session));
525 goto err;
528 if (timeout > 0) {
529 long arg = timeout;
530 r = ssh_options_set (h->session, SSH_OPTIONS_TIMEOUT, &arg);
531 if (r != SSH_OK) {
532 nbdkit_error ("failed to set timeout in libssh session: %" PRIu32 ": %s",
533 timeout, ssh_get_error (h->session));
534 goto err;
538 if (compression) {
539 r = ssh_options_set (h->session, SSH_OPTIONS_COMPRESSION, "yes");
540 if (r != SSH_OK) {
541 nbdkit_error ("failed to enable compression in libssh session: %s",
542 ssh_get_error (h->session));
543 goto err;
547 /* Read SSH config or alternative file. Must happen last so that
548 * the hostname has been set already.
550 if (config == NULL) {
551 /* NULL means parse the default files, which are ~/.ssh/config and
552 * /etc/ssh/ssh_config. If either are missing then they are
553 * ignored.
555 r = ssh_options_parse_config (h->session, NULL);
556 if (r != SSH_OK) {
557 nbdkit_error ("failed to parse local SSH configuration: %s",
558 ssh_get_error (h->session));
559 goto err;
562 else if (strcmp (config, "") != 0) {
563 /* User has specified a single file. This function ignores the
564 * case where the file is missing - should we check this? XXX
566 r = ssh_options_parse_config (h->session, config);
567 if (r != SSH_OK) {
568 nbdkit_error ("failed to parse SSH configuration: %s: %s",
569 config, ssh_get_error (h->session));
570 goto err;
574 /* Connect. */
575 r = ssh_connect (h->session);
576 if (r != SSH_OK) {
577 nbdkit_error ("failed to connect to remote host: %s: %s",
578 host, ssh_get_error (h->session));
579 goto err;
582 /* Verify the remote host. */
583 if (verify_remote_host && do_verify_remote_host (h) == -1)
584 goto err;
586 /* Authenticate. */
587 if (authenticate (h) == -1)
588 goto err;
590 /* Open the SFTP connection. */
591 h->sftp = sftp_new (h->session);
592 if (!h->sftp) {
593 nbdkit_error ("failed to allocate sftp session: %s",
594 ssh_get_error (h->session));
595 goto err;
597 r = sftp_init (h->sftp);
598 if (r != SSH_OK) {
599 nbdkit_error ("failed to initialize sftp session: %s",
600 ssh_get_error (h->session));
601 goto err;
604 /* Open or create the remote file. */
605 h->file = open_or_create_path (h->session, h->sftp, readonly);
606 if (!h->file)
607 goto err;
609 nbdkit_debug ("opened libssh handle");
611 return h;
613 err:
614 if (h->file)
615 sftp_close (h->file);
616 if (h->sftp)
617 sftp_free (h->sftp);
618 if (h->session) {
619 ssh_disconnect (h->session);
620 ssh_free (h->session);
622 free (h);
623 return NULL;
626 /* Free up the per-connection handle. */
627 static void
628 ssh_close (void *handle)
630 struct ssh_handle *h = handle;
631 int r;
633 r = sftp_close (h->file);
634 if (r != SSH_OK)
635 nbdkit_error ("cannot close file: %s", ssh_get_error (h->session));
637 sftp_free (h->sftp);
638 ssh_disconnect (h->session);
639 ssh_free (h->session);
640 free (h);
643 /* Get the file size. */
644 static int64_t
645 ssh_get_size (void *handle)
647 struct ssh_handle *h = handle;
648 sftp_attributes attrs;
649 int64_t r;
651 attrs = sftp_fstat (h->file);
652 r = attrs->size;
653 sftp_attributes_free (attrs);
655 return r;
658 /* Read data from the remote server. */
659 static int
660 ssh_pread (void *handle, void *buf, uint32_t count, uint64_t offset)
662 struct ssh_handle *h = handle;
663 int r;
664 ssize_t rs;
666 r = sftp_seek64 (h->file, offset);
667 if (r != SSH_OK) {
668 nbdkit_error ("seek64 failed: %s", ssh_get_error (h->session));
669 return -1;
672 while (count > 0) {
673 rs = sftp_read (h->file, buf, count);
674 if (rs < 0) {
675 nbdkit_error ("read failed: %s (%zd)", ssh_get_error (h->session), rs);
676 return -1;
678 buf += rs;
679 count -= rs;
682 return 0;
685 /* Write data to the remote server. */
686 static int
687 ssh_pwrite (void *handle, const void *buf, uint32_t count, uint64_t offset)
689 struct ssh_handle *h = handle;
690 int r;
691 ssize_t rs;
693 r = sftp_seek64 (h->file, offset);
694 if (r != SSH_OK) {
695 nbdkit_error ("seek64 failed: %s", ssh_get_error (h->session));
696 return -1;
699 while (count > 0) {
700 /* Openssh has a maximum packet size of 256K, so any write
701 * requests larger than this will fail in a peculiar way. (This
702 * limit doesn't seem to include the SFTP protocol overhead).
703 * Therefore if the count is larger than 128K, reduce the size of
704 * the request. I don't know whether 256K is a limit that applies
705 * to all servers.
707 rs = sftp_write (h->file, buf, MIN (count, 128*1024));
708 if (rs < 0) {
709 nbdkit_error ("write failed: %s (%zd)", ssh_get_error (h->session), rs);
710 return -1;
712 buf += rs;
713 count -= rs;
716 return 0;
719 static int
720 ssh_can_flush (void *handle)
722 struct ssh_handle *h = handle;
724 /* I added this extension to openssh 6.5 (April 2013). It may not
725 * be available in other SSH servers.
727 return sftp_extension_supported (h->sftp, "fsync@openssh.com", "1");
730 static int
731 ssh_can_multi_conn (void *handle)
733 struct ssh_handle *h = handle;
735 /* After examining the OpenSSH implementation of sftp-server we
736 * concluded that its write/flush behaviour is safe for advertising
737 * multi-conn. Other servers may not be safe. Use the
738 * fsync@openssh.com feature as a proxy.
740 return sftp_extension_supported (h->sftp, "fsync@openssh.com", "1");
743 static int
744 ssh_flush (void *handle)
746 struct ssh_handle *h = handle;
747 int r;
749 again:
750 r = sftp_fsync (h->file);
751 if (r == SSH_AGAIN)
752 goto again;
753 else if (r != SSH_OK) {
754 nbdkit_error ("fsync failed: %s", ssh_get_error (h->session));
755 return -1;
758 return 0;
761 static struct nbdkit_plugin plugin = {
762 .name = "ssh",
763 .version = PACKAGE_VERSION,
764 .unload = ssh_unload,
765 .config = ssh_config,
766 .config_complete = ssh_config_complete,
767 .config_help = ssh_config_help,
768 .magic_config_key = "path",
769 .open = ssh_open,
770 .close = ssh_close,
771 .get_size = ssh_get_size,
772 .pread = ssh_pread,
773 .pwrite = ssh_pwrite,
774 .can_flush = ssh_can_flush,
775 .flush = ssh_flush,
776 .can_multi_conn = ssh_can_multi_conn,
779 NBDKIT_REGISTER_PLUGIN(plugin)