diff --git a/patches/libnetconf2/4.1.2/0001-server_config-use-YANG-identity-derivation-for-key-f.patch b/patches/libnetconf2/4.1.2/0001-server_config-use-YANG-identity-derivation-for-key-f.patch new file mode 100644 index 00000000..30fa8129 --- /dev/null +++ b/patches/libnetconf2/4.1.2/0001-server_config-use-YANG-identity-derivation-for-key-f.patch @@ -0,0 +1,217 @@ +From dfa37a99b335b99e89df1be87b240123178cf1f0 Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Thu, 19 Mar 2026 15:15:52 +0100 +Subject: [PATCH 1/2] server_config: use YANG identity derivation for key + formats +Organization: Wires + +Replace plain string comparison of public/private key format identities +with proper derived-from-or-self() semantics. This allows vendor-extended +identities (e.g. an identity with base ct:subject-public-key-info-format) +to be silently mapped to the correct internal type without any warning. + +Also silence the spurious warning for symmetric-keys: the feature is +intentionally disabled for NETCONF, but a shared keystore may legitimately +contain symmetric keys used by other applications, e.g, Wi-Fi passphrases, +WireGuard PSKs, etc. + +Signed-off-by: Joachim Wiberg +--- + src/server_config.c | 131 ++++++++++++++++++++++++++++++++++++-------- + 1 file changed, 107 insertions(+), 24 deletions(-) + +diff --git a/src/server_config.c b/src/server_config.c +index 9f85655..54c470c 100644 +--- a/src/server_config.c ++++ b/src/server_config.c +@@ -110,6 +110,64 @@ nc_lyd_find_child(const struct lyd_node *node, const char *child, int fail_if_no + return 0; + } + ++/** ++ * @brief Recursively check if @p ident is in the derived tree of @p base. ++ * ++ * @param[in] base Base identity to search from. ++ * @param[in] ident Identity to find. ++ * @return 1 if @p ident is @p base or derived from it, 0 otherwise. ++ */ ++static int ++nc_ident_derived_or_self(const struct lysc_ident *base, const struct lysc_ident *ident) ++{ ++ LY_ARRAY_COUNT_TYPE i; ++ ++ if (base == ident) ++ return 1; ++ ++ LY_ARRAY_FOR(base->derived, i) { ++ if (nc_ident_derived_or_self(base->derived[i], ident)) ++ return 1; ++ } ++ ++ return 0; ++} ++ ++/** ++ * @brief Check if a YANG identity derives from (or is) a named identity in a given module. ++ * ++ * Implements YANG derived-from-or-self() semantics so that vendor-extended ++ * format identities (e.g. an identity with base ct:subject-public-key-info-format) ++ * are treated the same as their standard base. ++ * ++ * @param[in] ctx libyang context used to look up the base module. ++ * @param[in] ident Identity value from the data node. ++ * @param[in] base_mod Name of the module that defines the base identity. ++ * @param[in] base_name Name of the base identity. ++ * @return 1 if @p ident is or derives from the named base, 0 otherwise. ++ */ ++static int ++nc_ident_is_derived_from(const struct ly_ctx *ctx, const struct lysc_ident *ident, ++ const char *base_mod, const char *base_name) ++{ ++ const struct lysc_ident *base; ++ const struct lys_module *mod; ++ LY_ARRAY_COUNT_TYPE i; ++ ++ mod = ly_ctx_get_module_implemented(ctx, base_mod); ++ if (!mod) ++ return 0; ++ ++ LY_ARRAY_FOR(mod->identities, i) { ++ if (!strcmp(mod->identities[i].name, base_name)) { ++ base = &mod->identities[i]; ++ return nc_ident_derived_or_self(base, ident); ++ } ++ } ++ ++ return 0; ++} ++ + #ifdef NC_ENABLED_SSH_TLS + + /** +@@ -746,23 +804,32 @@ static int + config_pubkey_format(const struct lyd_node *node, enum nc_operation parent_op, struct nc_public_key *pubkey) + { + enum nc_operation op; +- const char *format; ++ const struct lysc_ident *ident; + + NC_NODE_GET_OP(node, parent_op, &op); + + if (op == NC_OP_DELETE) { + pubkey->type = NC_PUBKEY_FORMAT_UNKNOWN; + } else if ((op == NC_OP_CREATE) || (op == NC_OP_REPLACE)) { +- format = ((struct lyd_node_term *)node)->value.ident->name; +- assert(format); +- if (!strcmp(format, "ssh-public-key-format")) { ++ ident = ((struct lyd_node_term *)node)->value.ident; ++ assert(ident); ++ ++ /* ++ * Use derived-from-or-self semantics so that vendor-extended identities ++ * (e.g. an identity with base ct:ssh-public-key-format) are correctly ++ * mapped without requiring explicit registration in libnetconf2. ++ */ ++ if (nc_ident_is_derived_from(LYD_CTX(node), ident, "ietf-crypto-types", "ssh-public-key-format")) { + pubkey->type = NC_PUBKEY_FORMAT_SSH; +- } else if (!strcmp(format, "subject-public-key-info-format")) { ++ } else if (nc_ident_is_derived_from(LYD_CTX(node), ident, "ietf-crypto-types", "subject-public-key-info-format")) { + pubkey->type = NC_PUBKEY_FORMAT_X509; + } else { +- /* do not fail, the key may still be usable, or it may have come from a keystore/truststore +- * and have a different purpose other than NETCONF */ +- WRN(NULL, "Public key format \"%s\" not supported. The key may not be usable.", format); ++ /* ++ * The key format is not one used by NETCONF - it may be present in the ++ * keystore for other purposes (e.g. TLS, WireGuard, Wi-Fi). Do not ++ * fail; just mark it unusable for NETCONF and continue. ++ */ ++ VRB(NULL, "Public key format \"%s\" not used by NETCONF, ignoring.", ident->name); + pubkey->type = NC_PUBKEY_FORMAT_UNKNOWN; + } + } +@@ -792,29 +859,35 @@ config_pubkey_data(const struct lyd_node *node, enum nc_operation parent_op, str + static int + config_privkey_format(const struct lyd_node *node, enum nc_operation parent_op, struct nc_private_key *privkey) + { ++ const struct lysc_ident *ident; + enum nc_operation op; +- const char *format; + + NC_NODE_GET_OP(node, parent_op, &op); + + if (op == NC_OP_DELETE) { + privkey->type = NC_PRIVKEY_FORMAT_UNKNOWN; + } else if ((op == NC_OP_CREATE) || (op == NC_OP_REPLACE)) { +- format = ((struct lyd_node_term *)node)->value.ident->name; +- assert(format); +- +- if (!strcmp(format, "rsa-private-key-format")) { ++ ident = ((struct lyd_node_term *)node)->value.ident; ++ assert(ident); ++ /* ++ * Use derived-from-or-self semantics so that vendor-extended identities ++ * are correctly mapped without requiring explicit registration here. ++ */ ++ if (nc_ident_is_derived_from(LYD_CTX(node), ident, "ietf-crypto-types", "rsa-private-key-format")) { + privkey->type = NC_PRIVKEY_FORMAT_RSA; +- } else if (!strcmp(format, "ec-private-key-format")) { ++ } else if (nc_ident_is_derived_from(LYD_CTX(node), ident, "ietf-crypto-types", "ec-private-key-format")) { + privkey->type = NC_PRIVKEY_FORMAT_EC; +- } else if (!strcmp(format, "private-key-info-format")) { ++ } else if (nc_ident_is_derived_from(LYD_CTX(node), ident, "ietf-crypto-types", "private-key-info-format")) { + privkey->type = NC_PRIVKEY_FORMAT_X509; +- } else if (!strcmp(format, "openssh-private-key-format")) { ++ } else if (nc_ident_is_derived_from(LYD_CTX(node), ident, "ietf-crypto-types", "openssh-private-key-format")) { + privkey->type = NC_PRIVKEY_FORMAT_OPENSSH; + } else { +- /* do not fail, the key may still be usable, or it may have come from a keystore/truststore +- * and have a different purpose other than NETCONF */ +- WRN(NULL, "Private key format \"%s\" not supported. The key may not be usable.", format); ++ /* ++ * The key format is not one used by NETCONF - it may be present in the ++ * keystore for other purposes. Do not fail; just mark it unusable for ++ * NETCONF and continue. ++ */ ++ VRB(NULL, "Private key format \"%s\" not used by NETCONF, ignoring.", ident->name); + privkey->type = NC_PRIVKEY_FORMAT_UNKNOWN; + } + } +@@ -4223,9 +4296,14 @@ config_asymmetric_key(const struct lyd_node *node, enum nc_operation parent_op, + NC_CHECK_RET(config_encrypted_privkey_data(encrypted, op)); + } + +- /* config asymmetric key certificates */ +- NC_CHECK_RET(nc_lyd_find_child(node, "certificates", 1, &n)); +- NC_CHECK_RET(config_asymmetric_key_certs(n, op, entry)); ++ /* ++ * config asymmetric key certificates (optional: keys shared with other ++ * applications may legitimately have no certificates node) ++ */ ++ NC_CHECK_RET(nc_lyd_find_child(node, "certificates", 0, &n)); ++ if (n) { ++ NC_CHECK_RET(config_asymmetric_key_certs(n, op, entry)); ++ } + + /* config generate csr */ + NC_CHECK_RET(nc_lyd_find_child(node, "generate-csr", 0, &n)); +@@ -4261,9 +4339,14 @@ config_asymmetric_keys(const struct lyd_node *node, enum nc_operation parent_op, + } + + static int +-config_symmetric_keys(const struct lyd_node *node, enum nc_operation UNUSED(parent_op)) ++config_symmetric_keys(const struct lyd_node *UNUSED(node), enum nc_operation UNUSED(parent_op)) + { +- CONFIG_LOG_UNSUPPORTED(node); ++ /* ++ * Symmetric keys are not supported for NETCONF. They may legitimately ++ * be present in a shared keystore used by other applications, e.g., ++ * Wi-Fi passphrases, WireGuard PSKs. Silently ignore them rather than ++ * emitting a warning that would confuse operators. ++ */ + return 0; + } + +-- +2.43.0 + diff --git a/patches/libnetconf2/4.1.2/0002-session-silence-spurious-errors-on-client-disconnect.patch b/patches/libnetconf2/4.1.2/0002-session-silence-spurious-errors-on-client-disconnect.patch new file mode 100644 index 00000000..8d7af8a4 --- /dev/null +++ b/patches/libnetconf2/4.1.2/0002-session-silence-spurious-errors-on-client-disconnect.patch @@ -0,0 +1,83 @@ +From 7988dc8bb8b5dfd3ef1c783164f067a05dd5861b Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Thu, 19 Mar 2026 16:38:33 +0100 +Subject: [PATCH 2/2] session: silence spurious errors on client disconnect +Organization: Wires + +When a client drops the TCP connection, ssh_channel_poll_timeout() +returns SSH_ERROR with "Socket error: disconnected". This is normal +and should not be logged as an error. + +Use ssh_is_connected() to distinguish a peer disconnect from a real +channel error. On disconnect, set the termination reason to DROPPED +and omit the SESSION_ERROR flag so no ERR message is emitted. Real +channel errors retain the existing ERR log and TERM_OTHER reason. + +SSH_EOF (clean channel close by the peer) is treated the same way: +downgraded from ERR to VRB, since it too is an expected condition. + +Signed-off-by: Joachim Wiberg +--- + src/io.c | 11 ++++++++--- + src/session_server.c | 16 +++++++++++----- + 2 files changed, 19 insertions(+), 8 deletions(-) + +diff --git a/src/io.c b/src/io.c +index b9837c6..9d55a0f 100644 +--- a/src/io.c ++++ b/src/io.c +@@ -376,12 +376,17 @@ nc_read_poll(struct nc_session *session, int io_timeout) + /* EINTR is handled, it resumes waiting */ + ret = ssh_channel_poll_timeout(session->ti.libssh.channel, io_timeout, 0); + if (ret == SSH_ERROR) { +- ERR(session, "SSH channel poll error (%s).", ssh_get_error(session->ti.libssh.session)); ++ if (!ssh_is_connected(session->ti.libssh.session)) { ++ VRB(session, "SSH session disconnected."); ++ session->term_reason = NC_SESSION_TERM_DROPPED; ++ } else { ++ ERR(session, "SSH channel poll error (%s).", ssh_get_error(session->ti.libssh.session)); ++ session->term_reason = NC_SESSION_TERM_OTHER; ++ } + session->status = NC_STATUS_INVALID; +- session->term_reason = NC_SESSION_TERM_OTHER; + return -1; + } else if (ret == SSH_EOF) { +- ERR(session, "SSH channel unexpected EOF."); ++ VRB(session, "SSH channel closed by the other side."); + session->status = NC_STATUS_INVALID; + session->term_reason = NC_SESSION_TERM_DROPPED; + return -1; +diff --git a/src/session_server.c b/src/session_server.c +index 929f8bf..710d66c 100644 +--- a/src/session_server.c ++++ b/src/session_server.c +@@ -2097,15 +2097,21 @@ nc_ps_poll_session_io(struct nc_session *session, int io_timeout, time_t now_mon + + r = ssh_channel_poll_timeout(session->ti.libssh.channel, 0, 0); + if (r == SSH_EOF) { +- sprintf(msg, "SSH channel unexpected EOF"); ++ sprintf(msg, "SSH channel closed by the other side"); + session->status = NC_STATUS_INVALID; + session->term_reason = NC_SESSION_TERM_DROPPED; +- ret = NC_PSPOLL_SESSION_TERM | NC_PSPOLL_SESSION_ERROR; ++ ret = NC_PSPOLL_SESSION_TERM; + } else if (r == SSH_ERROR) { +- sprintf(msg, "SSH channel poll error (%s)", ssh_get_error(session->ti.libssh.session)); ++ if (!ssh_is_connected(session->ti.libssh.session)) { ++ sprintf(msg, "SSH session disconnected"); ++ session->term_reason = NC_SESSION_TERM_DROPPED; ++ ret = NC_PSPOLL_SESSION_TERM; ++ } else { ++ sprintf(msg, "SSH channel poll error (%s)", ssh_get_error(session->ti.libssh.session)); ++ session->term_reason = NC_SESSION_TERM_OTHER; ++ ret = NC_PSPOLL_SESSION_TERM | NC_PSPOLL_SESSION_ERROR; ++ } + session->status = NC_STATUS_INVALID; +- session->term_reason = NC_SESSION_TERM_OTHER; +- ret = NC_PSPOLL_SESSION_TERM | NC_PSPOLL_SESSION_ERROR; + } else if (!r) { + /* no application data received */ + ret = NC_PSPOLL_TIMEOUT; +-- +2.43.0 + diff --git a/patches/libnetconf2/4.1.2/do-not-fail-without-certficates.patch b/patches/libnetconf2/4.1.2/do-not-fail-without-certficates.patch deleted file mode 100644 index 95c684a9..00000000 --- a/patches/libnetconf2/4.1.2/do-not-fail-without-certficates.patch +++ /dev/null @@ -1,22 +0,0 @@ -diff --git a/src/server_config.c b/src/server_config.c -index 48d362e..dc9f59d 100644 ---- a/src/server_config.c -+++ b/src/server_config.c -@@ -4151,6 +4151,8 @@ config_asymmetric_key_certs(const struct lyd_node *node, enum nc_operation paren - struct lyd_node *n; - enum nc_operation op; - -+ if (!node) -+ return 0; - NC_NODE_GET_OP(node, parent_op, &op); - - /* configure all certificates */ -@@ -4225,7 +4227,7 @@ config_asymmetric_key(const struct lyd_node *node, enum nc_operation parent_op, - } - - /* config asymmetric key certificates */ -- NC_CHECK_RET(nc_lyd_find_child(node, "certificates", 1, &n)); -+ NC_CHECK_RET(nc_lyd_find_child(node, "certificates", 0, &n)); - NC_CHECK_RET(config_asymmetric_key_certs(n, op, entry)); - - /* config generate csr */