From 0e44b1f2fe59658b0e0233fbe827fb4769df2e8c Mon Sep 17 00:00:00 2001 From: Roman Janota Date: Fri, 17 Apr 2026 13:43:40 +0200 Subject: [PATCH 1/3] sesion UPDATE ssh banner and protocol string Rename previous banner to protocol string and add an API setter for it. Add actual SSH banner and send/receive it. Deprecated nc_session_ssh_get_banner API by introducing nc_session_ssh_get_protocol_string, to better reflect what it's actually getting Fixes #592 --- ...libnetconf2-netconf-server@2025-11-11.yang | 12 +- src/session.c | 12 +- src/session.h | 11 +- src/session_client_ssh.c | 11 ++ src/session_p.h | 3 +- src/session_server.c | 2 + src/session_server.h | 17 +++ src/session_server_ssh.c | 106 ++++++++++++++++-- tests/test_ssh.c | 93 +++++++++++++-- 9 files changed, 234 insertions(+), 33 deletions(-) diff --git a/modules/libnetconf2-netconf-server@2025-11-11.yang b/modules/libnetconf2-netconf-server@2025-11-11.yang index 6a1e68ef..7872f64e 100644 --- a/modules/libnetconf2-netconf-server@2025-11-11.yang +++ b/modules/libnetconf2-netconf-server@2025-11-11.yang @@ -162,15 +162,15 @@ module libnetconf2-netconf-server { "Grouping for the SSH server banner."; leaf banner { - type string { - length "1..245"; - } + type string; + description - "The banner that will be sent to the client when connecting to the server. - If not set, the libnetconf2 default with its version will be used."; + "SSH banner sent to clients before authentication. + It can be used to provide information about the server or legal notices. + Note that the banner is sent before authentication, so it should not contain any sensitive information."; reference - "RFC 4253: The Secure Shell (SSH) Transport Layer Protocol, section 4.2."; + "RFC 4252: The Secure Shell (SSH) Authentication Protocol, section 5.4."; } } diff --git a/src/session.c b/src/session.c index e260174b..90f1cf44 100644 --- a/src/session.c +++ b/src/session.c @@ -635,24 +635,28 @@ nc_session_get_port(const struct nc_session *session) #ifdef NC_ENABLED_SSH_TLS API const char * -nc_session_ssh_get_banner(const struct nc_session *session) +nc_session_ssh_get_protocol_string(const struct nc_session *session) { NC_CHECK_ARG_RET(NULL, session, NULL); if (session->ti_type != NC_TI_SSH) { - ERR(NULL, "Cannot get the SSH banner of a non-SSH session."); + ERR(NULL, "Cannot get the SSH protocol string of a non-SSH session."); return NULL; } if (session->side == NC_SERVER) { - /* get the banner sent by the client */ return ssh_get_clientbanner(session->ti.libssh.session); } else { - /* get the banner received from the server */ return ssh_get_serverbanner(session->ti.libssh.session); } } +API const char * +nc_session_ssh_get_banner(const struct nc_session *session) +{ + return nc_session_ssh_get_protocol_string(session); +} + #endif API const struct ly_ctx * diff --git a/src/session.h b/src/session.h index 2e677759..e1a05878 100644 --- a/src/session.h +++ b/src/session.h @@ -191,11 +191,20 @@ uint16_t nc_session_get_port(const struct nc_session *session); #ifdef NC_ENABLED_SSH_TLS +/** + * @brief Get the SSH protocol identification string sent by the peer. + * + * @param[in] session Session to get the protocol string from. + * @return SSH protocol identification string on success, NULL on error. + */ +const char *nc_session_ssh_get_protocol_string(const struct nc_session *session); + /** * @brief Get the SSH banner sent by the peer. + * @deprecated Use nc_session_ssh_get_protocol_string() instead. * * @param[in] session Session to get the banner from. - * @return SSH banner on success, NULL on error. + * @return SSH protocol identification string on success, NULL on error. */ const char *nc_session_ssh_get_banner(const struct nc_session *session); diff --git a/src/session_client_ssh.c b/src/session_client_ssh.c index b6c72f39..6468010a 100644 --- a/src/session_client_ssh.c +++ b/src/session_client_ssh.c @@ -1337,6 +1337,17 @@ connect_ssh_session(struct nc_session *session, struct nc_client_ssh_opts *opts, return 1; } +#if (LIBSSH_VERSION_MAJOR > 0) || (LIBSSH_VERSION_MAJOR == 0 && LIBSSH_VERSION_MINOR >= 10) + char *banner; + + /* retrieve the SSH banner if any was sent by the server right before authentication */ + banner = ssh_get_issue_banner(ssh_sess); + if (banner) { + VRB(session, "Received SSH banner:\n%s", banner); + free(banner); + } +#endif + /* check what authentication methods are available */ userauthlist = ssh_userauth_list(ssh_sess, NULL); diff --git a/src/session_p.h b/src/session_p.h index 7162c204..a72b83a0 100644 --- a/src/session_p.h +++ b/src/session_p.h @@ -382,7 +382,7 @@ struct nc_server_ssh_opts { char *kex_algs; /**< Used key exchange algorithms (comma-separated list). */ char *mac_algs; /**< Used MAC algorithms (comma-separated list). */ - char *banner; /**< SSH banner message. */ + char *banner; /**< SSH banner message, sent before authentication. */ uint16_t auth_timeout; /**< Authentication timeout. */ }; @@ -795,6 +795,7 @@ struct nc_server_opts { #ifdef NC_ENABLED_SSH_TLS char *authkey_path_fmt; /**< Path to users' public keys that may contain tokens with special meaning. */ char *pam_config_name; /**< PAM configuration file name. */ + char *ssh_protocol_string; /**< SSH protocol identification string. */ int (*interactive_auth_clb)(const struct nc_session *session, ssh_session ssh_sess, ssh_message msg, void *user_data); void *interactive_auth_data; void (*interactive_auth_data_free)(void *data); diff --git a/src/session_server.c b/src/session_server.c index 43bf0b14..a90556cd 100644 --- a/src/session_server.c +++ b/src/session_server.c @@ -1350,6 +1350,8 @@ nc_server_destroy(void) server_opts.authkey_path_fmt = NULL; free(server_opts.pam_config_name); server_opts.pam_config_name = NULL; + free(server_opts.ssh_protocol_string); + server_opts.ssh_protocol_string = NULL; if (server_opts.interactive_auth_data && server_opts.interactive_auth_data_free) { server_opts.interactive_auth_data_free(server_opts.interactive_auth_data); } diff --git a/src/session_server.h b/src/session_server.h index 39f3e3b7..ff538f5c 100644 --- a/src/session_server.h +++ b/src/session_server.h @@ -576,6 +576,23 @@ int nc_server_ssh_kbdint_get_nanswers(const struct nc_session *session, ssh_sess */ int nc_server_ssh_set_pam_conf_filename(const char *filename); +/** + * @brief Set the SSH protocol identification string. + * + * Creates an SSH identification string (per RFC 4253 Section 4.2) in the format: + * \-libnetconf2_\-libssh_\ + * + * For example: "NETCONF-libnetconf2_5.3.3-libssh_0.9.6" + * + * Maximum length of the resulting string is 245 characters. + * + * If not set, the default identification string is "libnetconf2_\-libssh_\". + * + * @param[in] prefix Prefix string to use at the beginning of the identification string. + * @return 0 on success, 1 on error. + */ +int nc_server_ssh_set_protocol_string(const char *prefix); + /** @} Server SSH */ /** diff --git a/src/session_server_ssh.c b/src/session_server_ssh.c index c92befff..48a3991f 100644 --- a/src/session_server_ssh.c +++ b/src/session_server_ssh.c @@ -1084,6 +1084,68 @@ nc_server_ssh_set_authkey_path_format(const char *path) return ret; } +/** + * @brief Forge the SSH protocol identification string based on the given prefix and the library versions. + * + * @param[in] prefix Optional prefix to include in the protocol string, can be NULL. + * @return Protocol string on success, NULL on error. + */ +static char * +nc_server_ssh_forge_protocol_string(const char *prefix) +{ + int r; + char *protocol_str = NULL; + + if (prefix) { + r = asprintf(&protocol_str, "%s-libnetconf2_%s-libssh_%d.%d.%d", + prefix, NC_VERSION, + LIBSSH_VERSION_MAJOR, LIBSSH_VERSION_MINOR, LIBSSH_VERSION_MICRO); + } else { + r = asprintf(&protocol_str, "libnetconf2_%s-libssh_%d.%d.%d", + NC_VERSION, + LIBSSH_VERSION_MAJOR, LIBSSH_VERSION_MINOR, LIBSSH_VERSION_MICRO); + } + NC_CHECK_ERRMEM_RET(r == -1, NULL); + + if (strlen(protocol_str) > 245) { + ERR(NULL, "SSH protocol identification string too long (max 245 characters)."); + free(protocol_str); + return NULL; + } + + return protocol_str; +} + +API int +nc_server_ssh_set_protocol_string(const char *prefix) +{ + int rc = 0; + char *protocol_str = NULL; + + NC_CHECK_ARG_RET(NULL, prefix, 1); + + protocol_str = nc_server_ssh_forge_protocol_string(prefix); + NC_CHECK_ERRMEM_GOTO(!protocol_str, rc = 1, cleanup); + + /* CONFIG LOCK */ + if (nc_rwlock_lock(&server_opts.config_lock, NC_RWLOCK_WRITE, NC_CONFIG_LOCK_TIMEOUT, __func__) != 1) { + rc = 1; + goto cleanup; + } + + /* transfer ownership */ + free(server_opts.ssh_protocol_string); + server_opts.ssh_protocol_string = protocol_str; + protocol_str = NULL; + + /* CONFIG UNLOCK */ + nc_rwlock_unlock(&server_opts.config_lock, __func__); + +cleanup: + free(protocol_str); + return rc; +} + /** * @brief Get the public key type from binary data. * @@ -1206,22 +1268,42 @@ nc_server_ssh_auth_pubkey_compare_key(ssh_key key, struct nc_public_key *pubkeys /** * @brief Handle authentication request for the None method. * + * @param[in] session NETCONF session. + * @param[in] banner SSH banner to send to the client, if any. * @param[in] local_users_supported Whether the server supports local users. * @param[in] auth_client Configured client's authentication data. * @param[in] msg libssh message. * @return 0 if the authentication was successful, -1 if not (@p msg already replied to). */ static int -nc_server_ssh_auth_none(int local_users_supported, struct nc_auth_client *auth_client, ssh_message msg) +nc_server_ssh_auth_none(struct nc_session *session, const char *banner, int local_users_supported, + struct nc_auth_client *auth_client, ssh_message msg) { assert(!local_users_supported || auth_client); +#if (LIBSSH_VERSION_MAJOR > 0) || (LIBSSH_VERSION_MAJOR == 0 && LIBSSH_VERSION_MINOR >= 10) + ssh_string ban; + + /* send the SSH banner if set, as a client just requested authentication and this is the right time to send it */ + if (banner) { + ban = ssh_string_from_char(banner); + if (ban) { + if (ssh_send_issue_banner(session->ti.libssh.session, ban)) { + ERR(session, "Failed to send SSH banner (%s).", ssh_get_error(session->ti.libssh.session)); + } + ssh_string_free(ban); + } + } +#else + if (banner) { + WRN(session, "SSH banner set but cannot be sent (libssh version 0.10.0 or later required)."); + } +#endif + if (local_users_supported && auth_client->none_enabled) { - /* success */ return 0; } - /* reply and return -1 so that this does not get counted as an unsuccessful authentication attempt */ ssh_message_reply_default(msg); return -1; } @@ -1602,7 +1684,7 @@ nc_server_ssh_auth(struct nc_session *session, struct nc_server_ssh_opts *opts, * configured auth methods, otherwise for system users just one is needed, * 0 return indicates success, 1 fail (msg not yet replied to), -1 fail (msg was replied to) */ if (method == SSH_AUTH_METHOD_NONE) { - ret = nc_server_ssh_auth_none(local_users_supported, auth_client, msg); + ret = nc_server_ssh_auth_none(session, opts->banner, local_users_supported, auth_client, msg); } else if (method == SSH_AUTH_METHOD_PASSWORD) { ret = nc_server_ssh_auth_password(session, local_users_supported, auth_client, msg); } else if (method == SSH_AUTH_METHOD_PUBLICKEY) { @@ -1972,7 +2054,8 @@ nc_accept_ssh_session(struct nc_session *session, struct nc_server_ssh_opts *opt ssh_bind sbind = NULL; int rc = 1, r; struct timespec ts_timeout; - const char *err_msg, *banner; + const char *err_msg; + char *proto_str = NULL, *proto_str_dyn = NULL; /* other transport-specific data */ session->ti_type = NC_TI_SSH; @@ -2031,13 +2114,15 @@ nc_accept_ssh_session(struct nc_session *session, struct nc_server_ssh_opts *opt } } - /* configure the ssh banner */ - if (opts->banner) { - banner = opts->banner; + /* configure the ssh protocol identification string */ + if (server_opts.ssh_protocol_string) { + proto_str = server_opts.ssh_protocol_string; } else { - banner = "libnetconf2-" NC_VERSION; + proto_str_dyn = nc_server_ssh_forge_protocol_string(NULL); + NC_CHECK_ERRMEM_GOTO(!proto_str_dyn, rc = -1, cleanup); + proto_str = proto_str_dyn; } - if (ssh_bind_options_set(sbind, SSH_BIND_OPTIONS_BANNER, banner)) { + if (ssh_bind_options_set(sbind, SSH_BIND_OPTIONS_BANNER, proto_str)) { rc = -1; goto cleanup; } @@ -2112,6 +2197,7 @@ nc_accept_ssh_session(struct nc_session *session, struct nc_server_ssh_opts *opt if (sock > -1) { close(sock); } + free(proto_str_dyn); ssh_bind_free(sbind); return rc; } diff --git a/tests/test_ssh.c b/tests/test_ssh.c index 7ff3eec1..54eabf88 100644 --- a/tests/test_ssh.c +++ b/tests/test_ssh.c @@ -24,20 +24,28 @@ #include #include +#include #include "ln2_test.h" +#include "log.h" +#include "nc_version.h" struct test_ssh_data { const char *username; const char *pubkey_path; const char *privkey_path; - int check_banner; + int check_protocol_string; int expect_fail; }; int TEST_PORT = 10050; const char *TEST_PORT_STR = "10050"; +#if (LIBSSH_VERSION_MAJOR > 0) || (LIBSSH_VERSION_MAJOR == 0 && LIBSSH_VERSION_MINOR >= 10) +static char banner_buffer[512]; +static const char *expected_banner_text = "This is a test SSH banner message"; +#endif + static char * auth_password(const char *username, const char *hostname, void *priv) { @@ -52,15 +60,37 @@ auth_password(const char *username, const char *hostname, void *priv) } } +#if (LIBSSH_VERSION_MAJOR > 0) || (LIBSSH_VERSION_MAJOR == 0 && LIBSSH_VERSION_MINOR >= 10) +static void +banner_log_callback(const struct nc_session *session, NC_VERB_LEVEL level, const char *msg) +{ + (void) session; + (void) level; + + if (strstr(msg, "Received SSH banner:\n") && strstr(msg, expected_banner_text)) { + strncpy(banner_buffer, msg, sizeof(banner_buffer) - 1); + banner_buffer[sizeof(banner_buffer) - 1] = '\0'; + } +} + +#endif + static void -check_banner(const struct nc_session *session) +check_protocol_string(const struct nc_session *session) { - const char *banner; + const char *proto_str; + char *expected; - banner = nc_session_ssh_get_banner(session); - assert_non_null(banner); + proto_str = nc_session_ssh_get_protocol_string(session); + assert_non_null(proto_str); - assert_string_equal(banner, "SSH-2.0-test-banner"); + expected = malloc(256); + assert_non_null(expected); + + sprintf(expected, "SSH-2.0-test-libnetconf2_%s-libssh_%d.%d.%d", NC_VERSION, + LIBSSH_VERSION_MAJOR, LIBSSH_VERSION_MINOR, LIBSSH_VERSION_MICRO); + assert_true(strncmp(proto_str, expected, strlen(expected)) == 0); + free(expected); } static void * @@ -101,8 +131,8 @@ client_thread_ssh(void *arg) assert_non_null(session); - if (test_data->check_banner) { - check_banner(session); + if (test_data->check_protocol_string) { + check_protocol_string(session); } nc_session_free(session, NULL); @@ -259,6 +289,33 @@ test_ed25519_pubkey(void **state) } } +static void +test_protocol_string(void **state) +{ + int ret, i; + pthread_t tids[2]; + struct ln2_test_ctx *test_ctx = *state; + struct test_ssh_data *test_data = test_ctx->test_data; + + ret = nc_server_ssh_set_protocol_string("test"); + assert_int_equal(ret, 0); + + test_data->username = "test_ed25519"; + test_data->pubkey_path = TESTS_DIR "/data/id_ed25519.pub"; + test_data->privkey_path = TESTS_DIR "/data/id_ed25519"; + test_data->check_protocol_string = 1; + + ret = pthread_create(&tids[0], NULL, client_thread_ssh, *state); + assert_int_equal(ret, 0); + ret = pthread_create(&tids[1], NULL, ln2_glob_test_server_thread, *state); + assert_int_equal(ret, 0); + + for (i = 0; i < 2; i++) { + pthread_join(tids[i], NULL); + } +} + +#if (LIBSSH_VERSION_MAJOR > 0) || (LIBSSH_VERSION_MAJOR == 0 && LIBSSH_VERSION_MINOR >= 10) static void test_banner(void **state) { @@ -267,10 +324,13 @@ test_banner(void **state) struct ln2_test_ctx *test_ctx = *state; struct test_ssh_data *test_data = test_ctx->test_data; + nc_verbosity(NC_VERB_VERBOSE); + nc_set_print_clb_session(banner_log_callback); + banner_buffer[0] = '\0'; + test_data->username = "test_ed25519"; test_data->pubkey_path = TESTS_DIR "/data/id_ed25519.pub"; test_data->privkey_path = TESTS_DIR "/data/id_ed25519"; - test_data->check_banner = 1; ret = pthread_create(&tids[0], NULL, client_thread_ssh, *state); assert_int_equal(ret, 0); @@ -280,8 +340,16 @@ test_banner(void **state) for (i = 0; i < 2; i++) { pthread_join(tids[i], NULL); } + + assert_true(strlen(banner_buffer) > 0); + assert_non_null(strstr(banner_buffer, expected_banner_text)); + + nc_verbosity(NC_VERB_ERROR); + nc_set_print_clb_session(NULL); } +#endif + /* BUG various libssh versions and systems are using different defaults of algorithms making this test fail */ void test_transport_params(void **state) @@ -656,11 +724,11 @@ setup_ssh(void **state) assert_int_equal(ret, 0); ret = lyd_new_path(tree, test_ctx->ctx, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='endpt']/ssh/" - "ssh-server-parameters/server-identity/libnetconf2-netconf-server:banner", "test-banner", 0, NULL); + "ssh-server-parameters/client-authentication/users/user[name='test_none']/none", NULL, 0, NULL); assert_int_equal(ret, 0); ret = lyd_new_path(tree, test_ctx->ctx, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='endpt']/ssh/" - "ssh-server-parameters/client-authentication/users/user[name='test_none']/none", NULL, 0, NULL); + "ssh-server-parameters/server-identity/libnetconf2-netconf-server:banner", expected_banner_text, 0, NULL); assert_int_equal(ret, 0); /* add all the default nodes/np containers */ @@ -687,7 +755,10 @@ main(void) cmocka_unit_test_setup_teardown(test_ec384_pubkey, setup_ssh, ln2_glob_test_teardown), cmocka_unit_test_setup_teardown(test_ec521_pubkey, setup_ssh, ln2_glob_test_teardown), cmocka_unit_test_setup_teardown(test_ed25519_pubkey, setup_ssh, ln2_glob_test_teardown), + cmocka_unit_test_setup_teardown(test_protocol_string, setup_ssh, ln2_glob_test_teardown), +#if (LIBSSH_VERSION_MAJOR > 0) || (LIBSSH_VERSION_MAJOR == 0 && LIBSSH_VERSION_MINOR >= 10) cmocka_unit_test_setup_teardown(test_banner, setup_ssh, ln2_glob_test_teardown), +#endif }; /* try to get ports from the environment, otherwise use the default */ From 86325ac83560a5df228e61842bf72ca4f8c48f24 Mon Sep 17 00:00:00 2001 From: Roman Janota Date: Fri, 17 Apr 2026 22:44:46 +0200 Subject: [PATCH 2/3] SOVERSION bump to version 5.3.9 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index aec95476..ad2bdd2e 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 8) +set(LIBNETCONF2_MICRO_SOVERSION 9) set(LIBNETCONF2_SOVERSION_FULL ${LIBNETCONF2_MAJOR_SOVERSION}.${LIBNETCONF2_MINOR_SOVERSION}.${LIBNETCONF2_MICRO_SOVERSION}) set(LIBNETCONF2_SOVERSION ${LIBNETCONF2_MAJOR_SOVERSION}) From 913d77319efc818d469d25c2f6b09783813e8126 Mon Sep 17 00:00:00 2001 From: Roman Janota Date: Fri, 17 Apr 2026 22:44:52 +0200 Subject: [PATCH 3/3] VERSION bump to version 4.3.3 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ad2bdd2e..0cb4a959 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 3) -set(LIBNETCONF2_MICRO_VERSION 2) +set(LIBNETCONF2_MICRO_VERSION 3) set(LIBNETCONF2_VERSION ${LIBNETCONF2_MAJOR_VERSION}.${LIBNETCONF2_MINOR_VERSION}.${LIBNETCONF2_MICRO_VERSION}) # Version of the library