From 0d470fd462559c82bbfce895ca7a7312472c2c5b Mon Sep 17 00:00:00 2001 From: Roman Janota Date: Tue, 7 Apr 2026 15:35:51 +0200 Subject: [PATCH 1/7] session UPDATE wait for ssh channel close Wait until we receiver SSH EOF from the peer, which avoids needlessly printing socket exception callbacks from libssh. 100ms should be more than enough to receive this message, if not just free as before. Also possibly fix some race conditions by locking io_lock before sending the EOF. Fixes #589 --- src/session.c | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/session.c b/src/session.c index 9b65374c..6454bdd0 100644 --- a/src/session.c +++ b/src/session.c @@ -796,11 +796,8 @@ nc_session_free_transport(struct nc_session *session, int *multisession) #ifdef NC_ENABLED_SSH_TLS case NC_TI_SSH: { int r; + struct timespec ts; - 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 +805,28 @@ 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 (connected) { + /* send EOF to the peer, but do not close the channel yet, wait for the peer to send EOF too. + * 100ms timeout should be enough for the peer to react and send EOF, + * if not, just continue with freeing the session and closing the channel. + * This is done to avoid libssh WRN log about reading from a closed channel */ + ssh_channel_send_eof(session->ti.libssh.channel); + nc_timeouttime_get(&ts, 100); + while (!ssh_channel_is_eof(session->ti.libssh.channel)) { + /* poll for the EOF, non-blocking */ + if (ssh_channel_poll(session->ti.libssh.channel, 0) == SSH_ERROR) { + /* if poll fails, just break and continue with freeing the session, it will be closed anyway */ + break; + } + if (nc_timeouttime_cur_diff(&ts) < 1) { + /* waited long enough, continue with freeing */ + break; + } + usleep(NC_TIMEOUT_STEP); + } + 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) { @@ -869,7 +888,7 @@ nc_session_free_transport(struct nc_session *session, int *multisession) sock = nc_tls_get_fd_wrap(session); if (connected) { - /* notify the peer that we're shutting down */ + /* notify the peer that we're shutting down, we don't need to wait for the peer's response */ nc_tls_close_notify_wrap(session->ti.tls.session); } From 4de931fcc365081982f5285d0f9712afbf53a24d Mon Sep 17 00:00:00 2001 From: Roman Janota Date: Wed, 8 Apr 2026 13:45:33 +0200 Subject: [PATCH 2/7] session BUGFIX unlock chlock in session free --- src/session.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/session.c b/src/session.c index 6454bdd0..33230785 100644 --- a/src/session.c +++ b/src/session.c @@ -939,7 +939,7 @@ nc_session_free(struct nc_session *session, void (*data_free)(void *)) 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__); } From 32c6b351d47f04c40b8764c4c093c03c665f974a Mon Sep 17 00:00:00 2001 From: Roman Janota Date: Wed, 8 Apr 2026 13:49:03 +0200 Subject: [PATCH 3/7] io UPDATE dont fail on SSH_EOF in nc_write If we receiver SSH_EOF in nc_write, it means that the peer won't be sending anything anymore, but it can still receive, so we shouldn't fail. This doesn't apply to nc_read, where it's still valid to fail. --- src/io.c | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/io.c b/src/io.c index df9af9fb..278ffad1 100644 --- a/src/io.c +++ b/src/io.c @@ -603,12 +603,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; From bd9c664e4f386594c4b8b0d6b209a652c9ddc387 Mon Sep 17 00:00:00 2001 From: Roman Janota Date: Wed, 8 Apr 2026 13:51:37 +0200 Subject: [PATCH 4/7] io UPDATE unify SSH connected check Perform SSH connection aliveness check just like with other transports and don't rely on libssh internal state tracking. --- src/io.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/io.c b/src/io.c index 278ffad1..8d8e398c 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; From 49b9f0cdc49ec4a2b2cb5a7ceb34a0208211ccb6 Mon Sep 17 00:00:00 2001 From: Roman Janota Date: Wed, 8 Apr 2026 13:53:48 +0200 Subject: [PATCH 5/7] session UPDATE properly close SSH sessions When a client DCs, send + SSH EOF before waiting for reply. When server goes to free its session it polls for the SSH_EOF and this avoids socket exception warnings from libssh. --- src/io.c | 6 + src/session.c | 290 +++++++++++++++++++++++---------------- src/session_client.c | 62 ++++++++- src/session_p.h | 24 +++- src/session_server_ssh.c | 12 +- 5 files changed, 265 insertions(+), 129 deletions(-) diff --git a/src/io.c b/src/io.c index 8d8e398c..c1ad4626 100644 --- a/src/io.c +++ b/src/io.c @@ -859,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 33230785..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,21 +883,16 @@ 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 timespec ts; + struct nc_session *siter; /* 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 @@ -805,24 +901,17 @@ 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 (connected) { - /* send EOF to the peer, but do not close the channel yet, wait for the peer to send EOF too. - * 100ms timeout should be enough for the peer to react and send EOF, - * if not, just continue with freeing the session and closing the channel. - * This is done to avoid libssh WRN log about reading from a closed channel */ - ssh_channel_send_eof(session->ti.libssh.channel); - nc_timeouttime_get(&ts, 100); - while (!ssh_channel_is_eof(session->ti.libssh.channel)) { - /* poll for the EOF, non-blocking */ - if (ssh_channel_poll(session->ti.libssh.channel, 0) == SSH_ERROR) { - /* if poll fails, just break and continue with freeing the session, it will be closed anyway */ - break; + 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."); } - if (nc_timeouttime_cur_diff(&ts) < 1) { - /* waited long enough, continue with freeing */ - break; - } - usleep(NC_TIMEOUT_STEP); } ssh_channel_free(session->ti.libssh.channel); } @@ -853,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); @@ -887,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, we don't need to wait for the peer's response */ - 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); @@ -918,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) { @@ -935,6 +1015,7 @@ 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)) { @@ -954,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; @@ -985,52 +1056,19 @@ 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__); + /* 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->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); - } - } - - if (session->data && data_free) { - data_free(session->data); } if ((session->side == NC_SERVER) && (session->flags & NC_SESSION_CALLHOME)) { @@ -1069,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; } From 64bc16f0eca00c81ce3c91055f974cd8c8175fc5 Mon Sep 17 00:00:00 2001 From: Roman Janota Date: Tue, 7 Apr 2026 15:38:58 +0200 Subject: [PATCH 6/7] SOVERSION bump to version 5.3.5 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index da1460d0..3dd0bb5c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) From c3c443ba44f22616bb98309504cee666caab1470 Mon Sep 17 00:00:00 2001 From: Roman Janota Date: Tue, 7 Apr 2026 15:39:08 +0200 Subject: [PATCH 7/7] VERSION bump to version 4.2.16 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3dd0bb5c..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