diff --git a/CMakeLists.txt b/CMakeLists.txt index da1460d0..36f36352 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,7 +58,7 @@ set(CMAKE_MACOSX_RPATH TRUE) # micro version is changed with a set of small changes or bugfixes anywhere in the project. set(LIBNETCONF2_MAJOR_VERSION 4) set(LIBNETCONF2_MINOR_VERSION 2) -set(LIBNETCONF2_MICRO_VERSION 15) +set(LIBNETCONF2_MICRO_VERSION 16) set(LIBNETCONF2_VERSION ${LIBNETCONF2_MAJOR_VERSION}.${LIBNETCONF2_MINOR_VERSION}.${LIBNETCONF2_MICRO_VERSION}) # Version of the library @@ -66,7 +66,7 @@ set(LIBNETCONF2_VERSION ${LIBNETCONF2_MAJOR_VERSION}.${LIBNETCONF2_MINOR_VERSION # with backward compatible change and micro version is connected with any internal change of the library. set(LIBNETCONF2_MAJOR_SOVERSION 5) set(LIBNETCONF2_MINOR_SOVERSION 3) -set(LIBNETCONF2_MICRO_SOVERSION 4) +set(LIBNETCONF2_MICRO_SOVERSION 5) set(LIBNETCONF2_SOVERSION_FULL ${LIBNETCONF2_MAJOR_SOVERSION}.${LIBNETCONF2_MINOR_SOVERSION}.${LIBNETCONF2_MICRO_SOVERSION}) set(LIBNETCONF2_SOVERSION ${LIBNETCONF2_MAJOR_SOVERSION}) diff --git a/src/io.c b/src/io.c index df9af9fb..c1ad4626 100644 --- a/src/io.c +++ b/src/io.c @@ -528,7 +528,8 @@ nc_session_is_connected(const struct nc_session *session) break; #ifdef NC_ENABLED_SSH_TLS case NC_TI_SSH: - return ssh_is_connected(session->ti.libssh.session); + fds.fd = ssh_get_fd(session->ti.libssh.session); + break; case NC_TI_TLS: fds.fd = nc_tls_get_fd_wrap(session); break; @@ -603,12 +604,8 @@ nc_write(struct nc_session *session, const void *buf, uint32_t count) #ifdef NC_ENABLED_SSH_TLS case NC_TI_SSH: - if (ssh_channel_is_closed(session->ti.libssh.channel) || ssh_channel_is_eof(session->ti.libssh.channel)) { - if (ssh_channel_is_closed(session->ti.libssh.channel)) { - ERR(session, "SSH channel unexpectedly closed."); - } else { - ERR(session, "SSH channel unexpected EOF."); - } + if (ssh_channel_is_closed(session->ti.libssh.channel)) { + ERR(session, "SSH channel unexpectedly closed."); session->status = NC_STATUS_INVALID; session->term_reason = NC_SESSION_TERM_DROPPED; return -1; @@ -862,6 +859,12 @@ nc_write_msg_io(struct nc_session *session, int io_timeout, int type, ...) op = va_arg(ap, struct lyd_node *); attrs = va_arg(ap, const char *); + if (session->ctx != LYD_CTX(op)) { + ERR(session, "RPC \"%s\" was created in different context than that of the session.", LYD_NAME(op)); + ret = NC_MSG_ERROR; + goto cleanup; + } + /* open */ count = asprintf(&buf, "", NC_NS_BASE, session->opts.client.msgid + 1, attrs ? attrs : ""); diff --git a/src/session.c b/src/session.c index 9b65374c..4bf474a5 100644 --- a/src/session.c +++ b/src/session.c @@ -694,48 +694,118 @@ nc_session_is_callhome(const struct nc_session *session) return 0; } -NC_MSG_TYPE -nc_send_msg_io(struct nc_session *session, int io_timeout, struct lyd_node *op) +/** + * @brief Send a transport shutdown indication to the peer. + * + * Depending on the transport type, this may involve sending transport-specific + * shutdown signaling so the peer can detect no more outgoing data are expected. + * + * @param[in] session Closing NETCONF session. + */ +static void +nc_session_free_send_transport_shutdown(struct nc_session *session) { - if (session->ctx != LYD_CTX(op)) { - ERR(session, "RPC \"%s\" was created in different context than that of the session.", LYD_NAME(op)); - return NC_MSG_ERROR; + switch (session->ti_type) { + case NC_TI_FD: + case NC_TI_UNIX: + /* nothing needed - the transport will be closed by caller */ + break; +#ifdef NC_ENABLED_SSH_TLS + case NC_TI_SSH: + if (session->ti.libssh.channel) { + if (session->side == NC_SERVER) { + /* send SSH channel close - we will not be reading nor writing anymore */ + ssh_channel_close(session->ti.libssh.channel); + } else if (session->side == NC_CLIENT) { + /* send SSH channel EOF - we can still receive data from the server, but not send. + * we will close the channel later, after receiving the server acknowledges our EOF, + * since we are the one initiating it */ + ssh_channel_send_eof(session->ti.libssh.channel); + } + } + break; + case NC_TI_TLS: + /* send TLS close_notify alert - we can still receive data from the peer, but not send */ + if (session->ti.tls.session) { + nc_tls_close_notify_wrap(session->ti.tls.session); + } + break; +#endif + default: + break; } - - return nc_write_msg_io(session, io_timeout, NC_MSG_RPC, op, NULL); } /** - * @brief Send \ and read the reply on a session. + * @brief Send \ to the peer. * * @param[in] session Closing NETCONF session. + * @param[out] close_rpc The sent \ RPC, caller must free. + * @return 0 on success, 1 on failure (RPC not sent). */ -static void -nc_session_free_close_session(struct nc_session *session) +static int +nc_session_free_send_close_session(struct nc_session *session, struct lyd_node **close_rpc) { - struct ly_in *msg; - struct lyd_node *close_rpc, *envp; - const struct lys_module *ietfnc; + struct lys_module *ietfnc; + struct lyd_node *rpc = NULL; + NC_MSG_TYPE msg_type; + + *close_rpc = NULL; ietfnc = ly_ctx_get_module_implemented(session->ctx, "ietf-netconf"); if (!ietfnc) { WRN(session, "Missing ietf-netconf module in context, unable to send ."); - return; + return 1; } - if (lyd_new_inner(NULL, ietfnc, "close-session", 0, &close_rpc)) { + if (lyd_new_inner(NULL, ietfnc, "close-session", 0, &rpc)) { WRN(session, "Failed to create RPC."); - return; + return 1; } /* send the RPC */ - nc_send_msg_io(session, NC_SESSION_FREE_LOCK_TIMEOUT, close_rpc); + msg_type = nc_write_msg_io(session, NC_SESSION_FREE_LOCK_TIMEOUT, NC_MSG_RPC, rpc, NULL); + if (msg_type != NC_MSG_RPC) { + WRN(session, "Failed to send RPC."); + lyd_free_tree(rpc); + return 1; + } else { + *close_rpc = rpc; + return 0; + } +} + +/** + * @brief Wait for the \ reply from the server and process it. + * + * @note Waits for at most ::NC_CLOSE_REPLY_TIMEOUT ms. + * + * @param[in] session Closing NETCONF session. + * @param[in] close_rpc The sent \ RPC. + */ +static void +nc_session_free_wait_close_session_reply(struct nc_session *session, struct lyd_node *close_rpc) +{ + int32_t timeout; + struct timespec ts_end; + struct ly_in *msg = NULL; + struct lyd_node *envp = NULL; + + nc_timeouttime_get(&ts_end, NC_CLOSE_REPLY_TIMEOUT); read_msg: - switch (nc_read_msg_poll_io(session, NC_CLOSE_REPLY_TIMEOUT, &msg)) { + timeout = nc_timeouttime_cur_diff(&ts_end); + if (timeout <= 0) { + /* avoid waiting for the reply for long in case of notification flooding */ + WRN(session, "Timeout for receiving a reply to elapsed."); + return; + } + + switch (nc_read_msg_poll_io(session, timeout, &msg)) { case 1: if (!strncmp(ly_in_memory(msg, NULL), "ctx, close_rpc, msg, LYD_XML, LYD_TYPE_REPLY_NETCONF, LYD_PARSE_STRICT, &envp, NULL)) { @@ -744,7 +814,9 @@ nc_session_free_close_session(struct nc_session *session) WRN(session, "Reply to was not as expected."); } lyd_free_tree(envp); + envp = NULL; ly_in_free(msg, 1); + msg = NULL; break; case 0: WRN(session, "Timeout for receiving a reply to elapsed."); @@ -756,6 +828,38 @@ nc_session_free_close_session(struct nc_session *session) /* cannot happen */ break; } +} + +/** + * @brief Gracefully close a client session on NETCONF and transport levels. + * + * Sends the \ RPC to close the NETCONF layer and then sends + * a transport shutdown indication. + * + * @param[in] session Closing NETCONF session. + */ +static void +nc_session_free_client_close_graceful(struct nc_session *session) +{ + int r; + struct ly_in *msg; + struct lyd_node *close_rpc = NULL; + + /* receive any leftover messages */ + while (nc_read_msg_poll_io(session, 0, &msg) == 1) { + ly_in_free(msg, 1); + } + + /* send the RPC */ + r = nc_session_free_send_close_session(session, &close_rpc); + + /* regardless of RPC-send result, send transport shutdown indication */ + nc_session_free_send_transport_shutdown(session); + + if (!r) { + /* if we sent the RPC successfully, wait for the server reply */ + nc_session_free_wait_close_session_reply(session, close_rpc); + } lyd_free_tree(close_rpc); } @@ -768,12 +872,9 @@ nc_session_free_close_session(struct nc_session *session) static void nc_session_free_transport(struct nc_session *session, int *multisession) { - int connected; /* flag to indicate whether the transport socket is still connected */ int sock = -1; - struct nc_session *siter; *multisession = 0; - connected = nc_session_is_connected(session); /* transport implementation cleanup */ switch (session->ti_type) { @@ -782,25 +883,17 @@ nc_session_free_transport(struct nc_session *session, int *multisession) * so it is up to the caller to close them correctly * TODO use callbacks */ - /* just to avoid compiler warning */ - (void)connected; - (void)siter; break; case NC_TI_UNIX: sock = session->ti.unixsock.sock; - (void)connected; - (void)siter; break; #ifdef NC_ENABLED_SSH_TLS case NC_TI_SSH: { int r; + struct nc_session *siter; - if (connected) { - ssh_channel_send_eof(session->ti.libssh.channel); - ssh_channel_free(session->ti.libssh.channel); - } /* There can be multiple NETCONF sessions on the same SSH session (NETCONF session maps to * SSH channel). So destroy the SSH session only if there is no other NETCONF session using * it. Also, avoid concurrent free by multiple threads of sessions that share the SSH session. @@ -808,6 +901,21 @@ nc_session_free_transport(struct nc_session *session, int *multisession) /* SESSION IO LOCK */ r = nc_mutex_lock(session->io_lock, NC_SESSION_FREE_LOCK_TIMEOUT, __func__); + if (session->ti.libssh.channel) { + if ((session->side == NC_CLIENT) || + ((session->side == NC_SERVER) && (session->term_reason == NC_SESSION_TERM_CLOSED))) { + /* NC_SERVER: session was properly closed by the client, so he should have sent SSH channel EOF. + * Polling here should properly set libssh internal state and avoid libssh WRN log about writing + * to a closed channel in ssh_channel_free(). + * NC_CLIENT: we are waiting for the server to acknowledge our SSH channel EOF + * by sending us its own SSH channel EOF. */ + if (ssh_channel_poll_timeout(session->ti.libssh.channel, NC_SESSION_FREE_SSH_POLL_EOF_TIMEOUT, 0) != SSH_EOF) { + WRN(session, "Timeout for receiving SSH channel EOF from the peer elapsed."); + } + } + ssh_channel_free(session->ti.libssh.channel); + } + if (session->ti.libssh.next) { for (siter = session->ti.libssh.next; siter != session; siter = siter->ti.libssh.next) { if (siter->status != NC_STATUS_STARTING) { @@ -834,16 +942,15 @@ nc_session_free_transport(struct nc_session *session, int *multisession) free(siter); } while (session->ti.libssh.next != session); } - if (connected) { - /* remember sock so we can close it */ - sock = ssh_get_fd(session->ti.libssh.session); - /* clears sock but does not close it if passed via options (libssh >= 0.10) */ - ssh_disconnect(session->ti.libssh.session); + /* remember sock so we can close it */ + sock = ssh_get_fd(session->ti.libssh.session); + + /* clears sock but does not close it if passed via options (libssh >= 0.10) */ + ssh_disconnect(session->ti.libssh.session); #if (LIBSSH_VERSION_MAJOR == 0 && LIBSSH_VERSION_MINOR < 10) - sock = -1; + sock = -1; #endif - } /* closes sock if set */ ssh_free(session->ti.libssh.session); @@ -868,11 +975,6 @@ nc_session_free_transport(struct nc_session *session, int *multisession) case NC_TI_TLS: sock = nc_tls_get_fd_wrap(session); - if (connected) { - /* notify the peer that we're shutting down */ - nc_tls_close_notify_wrap(session->ti.tls.session); - } - nc_tls_ctx_destroy_wrap(&session->ti.tls.ctx); memset(&session->ti.tls.ctx, 0, sizeof session->ti.tls.ctx); nc_tls_session_destroy_wrap(session->ti.tls.session); @@ -899,12 +1001,9 @@ nc_session_free_transport(struct nc_session *session, int *multisession) API void nc_session_free(struct nc_session *session, void (*data_free)(void *)) { - int r, i, rpc_locked = 0, msgs_locked = 0; + int r, i, rpc_locked = 0; int multisession = 0; /* flag for more NETCONF sessions on a single SSH session */ - struct nc_msg_cont *contiter; - struct ly_in *msg; struct timespec ts; - void *p; NC_STATUS status; if (!session) { @@ -916,11 +1015,12 @@ nc_session_free(struct nc_session *session, void (*data_free)(void *)) r = nc_mutex_lock(&session->opts.server.ch_lock, NC_SESSION_CH_LOCK_TIMEOUT, __func__); } + /* store status, so we can check if this session is already closing */ status = session->status; if ((session->side == NC_SERVER) && (session->flags & NC_SESSION_CALLHOME)) { /* CH UNLOCK */ - if (!r) { + if (r == 1) { /* only if we locked it */ nc_mutex_unlock(&session->opts.server.ch_lock, __func__); } @@ -935,25 +1035,15 @@ nc_session_free(struct nc_session *session, void (*data_free)(void *)) /* remove the session from the monitored list */ nc_client_monitoring_session_stop(session, 1); } - } - - /* stop notification threads if any */ - if ((session->side == NC_CLIENT) && ATOMIC_LOAD_RELAXED(session->opts.client.ntf_thread_running)) { - /* let the threads know they should quit */ - ATOMIC_STORE_RELAXED(session->opts.client.ntf_thread_running, 0); - /* wait for them */ - nc_timeouttime_get(&ts, NC_SESSION_FREE_LOCK_TIMEOUT); - while (ATOMIC_LOAD_RELAXED(session->opts.client.ntf_thread_count)) { - usleep(NC_TIMEOUT_STEP); - if (nc_timeouttime_cur_diff(&ts) < 1) { - ERR(session, "Waiting for notification thread exit failed (timed out)."); - break; - } + if (ATOMIC_LOAD_RELAXED(session->opts.client.ntf_thread_running)) { + /* stop notification threads if any */ + nc_client_notification_threads_stop(session); } } if (session->side == NC_SERVER) { + /* RPC LOCK, not to receive new RPCs while we're freeing the session, continue on error */ r = nc_session_rpc_lock(session, NC_SESSION_FREE_LOCK_TIMEOUT, __func__); if (r == -1) { return; @@ -966,54 +1056,21 @@ nc_session_free(struct nc_session *session, void (*data_free)(void *)) } if (session->side == NC_CLIENT) { - /* MSGS LOCK */ - r = nc_mutex_lock(&session->opts.client.msgs_lock, NC_SESSION_FREE_LOCK_TIMEOUT, __func__); - if (r == -1) { - return; - } else if (r) { - msgs_locked = 1; - } else { - /* else failed to lock it, too bad */ - ERR(session, "Freeing a session while messages are being received."); - } - - /* cleanup message queue */ - for (contiter = session->opts.client.msgs; contiter; ) { - ly_in_free(contiter->msg, 1); - - p = contiter; - contiter = contiter->next; - free(p); - } - - if (msgs_locked) { - /* MSGS UNLOCK */ - nc_mutex_unlock(&session->opts.client.msgs_lock, __func__); - } - - if ((session->status == NC_STATUS_RUNNING) && nc_session_is_connected(session)) { - /* receive any leftover messages */ - while (nc_read_msg_poll_io(session, 0, &msg) == 1) { - ly_in_free(msg, 1); - } - - /* send closing info to the other side */ - nc_session_free_close_session(session); - } - - /* list of server's capabilities */ - if (session->opts.client.cpblts) { - for (i = 0; session->opts.client.cpblts[i]; i++) { - free(session->opts.client.cpblts[i]); - } - free(session->opts.client.cpblts); + /* free queued messages */ + nc_client_msgs_free(session); + } + + if (session->status == NC_STATUS_RUNNING) { + /* notify the peer that we're closing the session */ + if (session->side == NC_CLIENT) { + /* graceful close: + transport shutdown indication */ + nc_session_free_client_close_graceful(session); + } else if (session->side == NC_SERVER) { + /* only send transport shutdown indication to the peer */ + nc_session_free_send_transport_shutdown(session); } } - if (session->data && data_free) { - data_free(session->data); - } - if ((session->side == NC_SERVER) && (session->flags & NC_SESSION_CALLHOME)) { /* CH LOCK */ nc_mutex_lock(&session->opts.server.ch_lock, NC_SESSION_CH_LOCK_TIMEOUT, __func__); @@ -1050,6 +1107,20 @@ nc_session_free(struct nc_session *session, void (*data_free)(void *)) free(session->host); free(session->path); + if (session->side == NC_CLIENT) { + /* list of server's capabilities */ + if (session->opts.client.cpblts) { + for (i = 0; session->opts.client.cpblts[i]; i++) { + free(session->opts.client.cpblts[i]); + } + free(session->opts.client.cpblts); + } + } + + if (session->data && data_free) { + data_free(session->data); + } + if (session->side == NC_SERVER) { pthread_mutex_destroy(&session->opts.server.ntf_status_lock); if (rpc_locked) { diff --git a/src/session_client.c b/src/session_client.c index e78048cf..c39a99f8 100644 --- a/src/session_client.c +++ b/src/session_client.c @@ -2207,6 +2207,41 @@ recv_msg(struct nc_session *session, int timeout, NC_MSG_TYPE expected, struct l return ret; } +void +nc_client_msgs_free(struct nc_session *session) +{ + int r; + struct nc_msg_cont *contiter, *p; + + if (!session || (session->side != NC_CLIENT)) { + return; + } + + /* MSGS LOCK */ + r = nc_mutex_lock(&session->opts.client.msgs_lock, NC_SESSION_FREE_LOCK_TIMEOUT, __func__); + if (r == -1) { + return; + } else if (!r) { + /* else failed to lock it, too bad */ + ERR(session, "Freeing a session while messages are being received."); + } + + /* cleanup message queue */ + for (contiter = session->opts.client.msgs; contiter; ) { + ly_in_free(contiter->msg, 1); + + p = contiter; + contiter = contiter->next; + free(p); + } + session->opts.client.msgs = NULL; + + if (r == 1) { + /* MSGS UNLOCK */ + nc_mutex_unlock(&session->opts.client.msgs_lock, __func__); + } +} + static NC_MSG_TYPE recv_reply(struct nc_session *session, int timeout, struct lyd_node *op, uint64_t msgid, struct lyd_node **envp) { @@ -2575,7 +2610,7 @@ nc_recv_notif_dispatch_data(struct nc_session *session, nc_notif_dispatch_clb no ret = pthread_create(&tid, NULL, nc_recv_notif_thread, ntarg); if (ret) { - ERR(session, "Failed to create a new thread (%s).", strerror(errno)); + ERR(session, "Failed to create a new thread (%s).", strerror(ret)); free(ntarg); if (ATOMIC_DEC_RELAXED(session->opts.client.ntf_thread_count) == 1) { ATOMIC_STORE_RELAXED(session->opts.client.ntf_thread_running, 0); @@ -2586,6 +2621,29 @@ nc_recv_notif_dispatch_data(struct nc_session *session, nc_notif_dispatch_clb no return 0; } +void +nc_client_notification_threads_stop(struct nc_session *session) +{ + struct timespec ts; + + if (!session || (session->side != NC_CLIENT)) { + return; + } + + /* let the threads know they should quit */ + ATOMIC_STORE_RELAXED(session->opts.client.ntf_thread_running, 0); + + /* wait for them */ + nc_timeouttime_get(&ts, NC_SESSION_FREE_LOCK_TIMEOUT); + while (ATOMIC_LOAD_RELAXED(session->opts.client.ntf_thread_count)) { + usleep(NC_TIMEOUT_STEP); + if (nc_timeouttime_cur_diff(&ts) < 1) { + ERR(session, "Waiting for notification thread exit failed (timed out)."); + break; + } + } +} + static const char * nc_wd2str(NC_WD_MODE wd) { @@ -3204,7 +3262,7 @@ nc_send_rpc(struct nc_session *session, struct nc_rpc *rpc, int timeout, uint64_ } /* send RPC, store its message ID */ - r = nc_send_msg_io(session, timeout, data); + r = nc_write_msg_io(session, timeout, NC_MSG_RPC, data, NULL); cur_msgid = session->opts.client.msgid; if (dofree) { diff --git a/src/session_p.h b/src/session_p.h index 0d4d11b1..1cebecf1 100644 --- a/src/session_p.h +++ b/src/session_p.h @@ -85,6 +85,11 @@ extern struct nc_server_opts server_opts; */ #define NC_SESSION_FREE_LOCK_TIMEOUT 1000 +/** + * Timeout in msec to poll for SSH channel EOF response when closing a session. + */ +#define NC_SESSION_FREE_SSH_POLL_EOF_TIMEOUT 100 + /** * Timeout in msec for a thread to wait for its turn to work with a pollsession structure. */ @@ -1049,6 +1054,23 @@ int nc_client_monitoring_session_start(struct nc_session *session); */ void nc_client_monitoring_session_stop(struct nc_session *session, int lock); +/** + * @brief Stop all notification threads for a client session. + * + * The function asks threads to stop and waits for their termination for at + * most ::NC_SESSION_FREE_LOCK_TIMEOUT milliseconds. + * + * @param[in] session Session to stop notification threads for. + */ +void nc_client_notification_threads_stop(struct nc_session *session); + +/** + * @brief Free messages stored in the client session's message queue. + * + * @param[in] session Session to free messages for. + */ +void nc_client_msgs_free(struct nc_session *session); + /** * @brief Get current client context. * @@ -1106,8 +1128,6 @@ struct passwd *nc_getpw(uid_t uid, const char *username, struct passwd *pwd_buf, */ struct group *nc_getgr(gid_t gid, const char *grpname, struct group *grp_buf, char **buf, size_t *buf_size); -NC_MSG_TYPE nc_send_msg_io(struct nc_session *session, int io_timeout, struct lyd_node *op); - /** * @brief Get current clock (uses COMPAT_CLOCK_ID) time with an offset. * diff --git a/src/session_server_ssh.c b/src/session_server_ssh.c index 22a69380..c92befff 100644 --- a/src/session_server_ssh.c +++ b/src/session_server_ssh.c @@ -679,8 +679,8 @@ nc_server_ssh_kbdint_get_nanswers(const struct nc_session *session, ssh_session /* wait for answers from the client */ do { - if (!nc_session_is_connected(session)) { - ERR(NULL, "SSH communication socket unexpectedly closed."); + if (!ssh_is_connected(session->ti.libssh.session)) { + ERR(NULL, "SSH communication socket unexpectedly closed while waiting for keyboard-interactive authentication answers."); ret = -1; goto cleanup; } @@ -1846,8 +1846,8 @@ nc_accept_ssh_session_open_netconf_channel(struct nc_session *session, struct nc nc_timeouttime_get(&ts_timeout, timeout * 1000); } while (1) { - if (!nc_session_is_connected(session)) { - ERR(session, "Communication SSH socket unexpectedly closed."); + if (!ssh_is_connected(session->ti.libssh.session)) { + ERR(session, "Communication SSH socket unexpectedly closed while waiting for \"netconf\" subsystem request."); return -1; } @@ -1929,8 +1929,8 @@ nc_accept_ssh_session_auth(struct nc_session *session, struct nc_server_ssh_opts nc_timeouttime_get(&ts_timeout, opts->auth_timeout * 1000); } while (1) { - if (!nc_session_is_connected(session)) { - ERR(session, "Communication SSH socket unexpectedly closed."); + if (!ssh_is_connected(session->ti.libssh.session)) { + ERR(session, "Communication SSH socket unexpectedly closed while waiting for authentication."); return -1; }