From 3f7d2b7208d60b26df970134ba95f16dceaeec65 Mon Sep 17 00:00:00 2001 From: Roman Janota Date: Thu, 30 Jul 2026 19:00:51 +0200 Subject: [PATCH 1/3] server config BUGFIX warn/del on YANG violations Deleting the last entry of a min-elements list or emptying a mandatory choice now logs a warning. Presence containers (listen/call-home) and empty cert-bag/client-authentication containers are cleaned up automatically to maintain valid YANG data trees. Resolves #614. --- src/server_config.h | 61 ++++++++ src/server_config_util.c | 44 +++++- src/server_config_util_tls.c | 291 +++++++++++++++++++++++++++++++++-- tests/test_config.c | 65 ++++++++ 4 files changed, 441 insertions(+), 20 deletions(-) diff --git a/src/server_config.h b/src/server_config.h index b580920a..0a5f95a0 100644 --- a/src/server_config.h +++ b/src/server_config.h @@ -139,6 +139,9 @@ int nc_server_config_add_address_port(const struct ly_ctx *ctx, const char *endp * If NULL, all of the endpoints will be deleted. * @param[in,out] config Modified configuration YANG data tree. * @return 0 on success, non-zero otherwise. + * + * @note If the last endpoint is deleted, the parent listen presence container + * is also deleted to maintain a valid YANG data tree. */ int nc_server_config_del_endpt(const char *endpt_name, struct lyd_node **config); @@ -400,6 +403,9 @@ int nc_server_config_add_ssh_hostkey(const struct ly_ctx *ctx, const char *endpt * If NULL, all of the hostkeys on this endpoint will be deleted. * @param[in,out] config Configuration YANG data tree. * @return 0 on success, non-zero otherwise. + * + * @note If the last hostkey is deleted, the configuration becomes invalid. + * Add a new hostkey to make it valid again. */ int nc_server_config_del_ssh_hostkey(const struct ly_ctx *ctx, const char *endpt_name, const char *hostkey_name, struct lyd_node **config); @@ -650,6 +656,9 @@ int nc_server_config_add_tls_server_cert(const struct ly_ctx *ctx, const char *e * @param[in] endpt_name Identifier of an existing endpoint. * @param[in,out] config Modified configuration YANG data tree. * @return 0 on success, non-zero otherwise. + * + * @note Deleting the server certificate makes the configuration invalid. + * Add a new server certificate to make it valid again. */ int nc_server_config_del_tls_server_cert(const char *endpt_name, struct lyd_node **config); @@ -674,6 +683,9 @@ int nc_server_config_add_tls_keystore_ref(const struct ly_ctx *ctx, const char * * @param[in] endpt_name Identifier of an existing endpoint. * @param[in,out] config Modified configuration YANG data tree. * @return 0 on success, non-zero otherwise. + * + * @note Deleting the keystore reference makes the configuration invalid. + * Add a new keystore reference to make it valid again. */ int nc_server_config_del_tls_keystore_ref(const char *endpt_name, struct lyd_node **config); @@ -703,6 +715,10 @@ int nc_server_config_add_tls_client_cert(const struct ly_ctx *ctx, const char *e * If NULL, all of the end-entity certificates on the given endpoint will be deleted. * @param[in,out] config Modified configuration YANG data tree. * @return 0 on success, non-zero otherwise. + * + * @note If the last certificate is deleted, the parent ee-certs container and + * possibly the client-authentication container are also deleted to maintain + * a valid YANG data tree. */ int nc_server_config_del_tls_client_cert(const char *endpt_name, const char *cert_name, struct lyd_node **config); @@ -726,6 +742,9 @@ int nc_server_config_add_tls_client_cert_truststore_ref(const struct ly_ctx *ctx * @param[in] endpt_name Identifier of an existing endpoint. * @param[in,out] config Modified configuration YANG data tree. * @return 0 on success, non-zero otherwise. + * + * @note The parent ee-certs container and possibly the client-authentication + * container are also deleted to maintain a valid YANG data tree. */ int nc_server_config_del_tls_client_cert_truststore_ref(const char *endpt_name, struct lyd_node **config); @@ -763,6 +782,10 @@ int nc_server_config_add_tls_ca_cert(const struct ly_ctx *ctx, const char *endpt * If NULL, all of the CA certificates on the given endpoint will be deleted. * @param[in,out] config Modified configuration YANG data tree. * @return 0 on success, non-zero otherwise. + * + * @note If the last certificate is deleted, the parent ca-certs container and + * possibly the client-authentication container are also deleted to maintain + * a valid YANG data tree. */ int nc_server_config_del_tls_ca_cert(const char *endpt_name, const char *cert_name, struct lyd_node **config); @@ -786,6 +809,9 @@ int nc_server_config_add_tls_ca_cert_truststore_ref(const struct ly_ctx *ctx, co * @param[in] endpt_name Identifier of an existing endpoint. * @param[in,out] config Modified configuration YANG data tree. * @return 0 on success, non-zero otherwise. + * + * @note The parent ca-certs container and possibly the client-authentication + * container are also deleted to maintain a valid YANG data tree. */ int nc_server_config_del_tls_ca_cert_truststore_ref(const char *endpt_name, struct lyd_node **config); @@ -842,6 +868,9 @@ int nc_server_config_add_tls_ctn(const struct ly_ctx *ctx, const char *endpt_nam * If 0, all of the cert-to-name entries on the given endpoint will be deleted. * @param[in,out] config Modified configuration YANG data tree. * @return 0 on success, non-zero otherwise. + * + * @note If the last cert-to-name entry is deleted, the configuration becomes + * invalid. Add a new cert-to-name entry to make it valid again. */ int nc_server_config_del_tls_ctn(const char *endpt_name, uint32_t id, struct lyd_node **config); @@ -896,6 +925,9 @@ int nc_server_config_add_ch_address_port(const struct ly_ctx *ctx, const char *c * If NULL, all of the Call Home clients will be deleted. * @param[in,out] config Modified configuration YANG data tree. * @return 0 on success, non-zero otherwise. + * + * @note If the last client is deleted, the parent call-home presence container + * is also deleted to maintain a valid YANG data tree. */ int nc_server_config_del_ch_client(const char *client_name, struct lyd_node **config); @@ -907,6 +939,9 @@ int nc_server_config_del_ch_client(const char *client_name, struct lyd_node **co * If NULL, all of the CH endpoints which belong to the given client will be deleted. * @param[in,out] config Modified configuration YANG data tree. * @return 0 on success, non-zero otherwise. + * + * @note If the last endpoint of the client is deleted, the configuration + * becomes invalid. Add a new endpoint to make it valid again. */ int nc_server_config_del_ch_endpt(const char *client_name, const char *endpt_name, struct lyd_node **config); @@ -1074,6 +1109,9 @@ int nc_server_config_add_ch_ssh_hostkey(const struct ly_ctx *ctx, const char *cl * If NULL, all of the hostkeys on the given endpoint will be deleted. * @param[in,out] config Modified configuration YANG data tree. * @return 0 on success, non-zero otherwise. + * + * @note If the last hostkey is deleted, the configuration becomes invalid. + * Add a new hostkey to make it valid again. */ int nc_server_config_del_ch_ssh_hostkey(const char *client_name, const char *endpt_name, const char *hostkey_name, struct lyd_node **config); @@ -1319,6 +1357,9 @@ int nc_server_config_add_ch_tls_server_cert(const struct ly_ctx *ctx, const char * @param[in] endpt_name Identifier of an existing Call Home endpoint that belongs to the given client. * @param[in,out] config Modified configuration YANG data tree. * @return 0 on success, non-zero otherwise. + * + * @note Deleting the server certificate makes the configuration invalid. + * Add a new server certificate to make it valid again. */ int nc_server_config_del_ch_tls_server_cert(const char *client_name, const char *endpt_name, struct lyd_node **config); @@ -1347,6 +1388,9 @@ int nc_server_config_add_ch_tls_keystore_ref(const struct ly_ctx *ctx, const cha * @param[in] endpt_name Identifier of an existing Call Home endpoint that belongs to the given client. * @param[in,out] config Modified configuration YANG data tree. * @return 0 on success, non-zero otherwise. + * + * @note Deleting the keystore reference makes the configuration invalid. + * Add a new keystore reference to make it valid again. */ int nc_server_config_del_ch_tls_keystore_ref(const char *client_name, const char *endpt_name, struct lyd_node **config); @@ -1378,6 +1422,10 @@ int nc_server_config_add_ch_tls_client_cert(const struct ly_ctx *ctx, const char * If NULL, all of the client certificates will be deleted. * @param[in,out] config Modified configuration YANG data tree. * @return 0 on success, non-zero otherwise. + * + * @note If the last certificate is deleted, the parent ee-certs container and + * possibly the client-authentication container are also deleted to maintain + * a valid YANG data tree. */ int nc_server_config_del_ch_tls_client_cert(const char *client_name, const char *endpt_name, const char *cert_name, struct lyd_node **config); @@ -1405,6 +1453,9 @@ int nc_server_config_add_ch_tls_client_cert_truststore_ref(const struct ly_ctx * * @param[in] endpt_name Identifier of an existing Call Home endpoint that belongs to the given client. * @param[in,out] config Modified configuration YANG data tree. * @return 0 on success, non-zero otherwise. + * + * @note The parent ee-certs container and possibly the client-authentication + * container are also deleted to maintain a valid YANG data tree. */ int nc_server_config_del_ch_tls_client_cert_truststore_ref(const char *client_name, const char *endpt_name, struct lyd_node **config); @@ -1436,6 +1487,10 @@ int nc_server_config_add_ch_tls_ca_cert(const struct ly_ctx *ctx, const char *cl * If NULL, all of the CA certificates will be deleted. * @param[in,out] config Modified configuration YANG data tree. * @return 0 on success, non-zero otherwise. + * + * @note If the last certificate is deleted, the parent ca-certs container and + * possibly the client-authentication container are also deleted to maintain + * a valid YANG data tree. */ int nc_server_config_del_ch_tls_ca_cert(const char *client_name, const char *endpt_name, const char *cert_name, struct lyd_node **config); @@ -1463,6 +1518,9 @@ int nc_server_config_add_ch_tls_ca_cert_truststore_ref(const struct ly_ctx *ctx, * @param[in] endpt_name Identifier of an existing Call Home endpoint that belongs to the given client. * @param[in,out] config Modified configuration YANG data tree. * @return 0 on success, non-zero otherwise. + * + * @note The parent ca-certs container and possibly the client-authentication + * container are also deleted to maintain a valid YANG data tree. */ int nc_server_config_del_ch_tls_ca_cert_truststore_ref(const char *client_name, const char *endpt_name, struct lyd_node **config); @@ -1496,6 +1554,9 @@ int nc_server_config_add_ch_tls_ctn(const struct ly_ctx *ctx, const char *client * If 0, all of the CTN entries will be deleted. * @param[in,out] config Modified configuration YANG data tree. * @return 0 on success, non-zero otherwise. + * + * @note If the last cert-to-name entry is deleted, the configuration becomes + * invalid. Add a new cert-to-name entry to make it valid again. */ int nc_server_config_del_ch_tls_ctn(const char *client_name, const char *endpt_name, uint32_t id, struct lyd_node **config); diff --git a/src/server_config_util.c b/src/server_config_util.c index 7ef6d89e..ca730c2d 100644 --- a/src/server_config_util.c +++ b/src/server_config_util.c @@ -223,13 +223,31 @@ nc_server_config_check_delete(struct lyd_node **tree, const char *path_fmt, ...) API int nc_server_config_del_endpt(const char *endpt_name, struct lyd_node **config) { + int rc = 0; + struct lyd_node *node = NULL; + NC_CHECK_ARG_RET(NULL, config, 1); if (endpt_name) { - return nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='%s']", endpt_name); + rc = nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='%s']", endpt_name); } else { - return nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint"); + rc = nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint"); + } + if (rc) { + goto cleanup; } + + lyd_find_path(*config, "/ietf-netconf-server:netconf-server/listen/endpoints", 0, &node); + if (node && !lyd_child(node)) { + /* no more endpoints, delete the listen container */ + VRB(NULL, "Last listen endpoint deleted, also removing the \"listen\" container to maintain YANG validity."); + if ((rc = nc_server_config_check_delete(config, "/ietf-netconf-server:netconf-server/listen"))) { + goto cleanup; + } + } + +cleanup: + return rc; } API int @@ -1092,13 +1110,31 @@ nc_server_config_add_ch_address_port(const struct ly_ctx *ctx, const char *clien API int nc_server_config_del_ch_client(const char *ch_client_name, struct lyd_node **config) { + int rc = 0; + struct lyd_node *node = NULL; + NC_CHECK_ARG_RET(NULL, config, 1); if (ch_client_name) { - return nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/call-home/netconf-client[name='%s']", ch_client_name); + rc = nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/call-home/netconf-client[name='%s']", ch_client_name); } else { - return nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/call-home/netconf-client"); + rc = nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/call-home/netconf-client"); + } + if (rc) { + goto cleanup; } + + lyd_find_path(*config, "/ietf-netconf-server:netconf-server/call-home", 0, &node); + if (node && !lyd_child(node)) { + /* no more clients, delete the call-home container */ + VRB(NULL, "Last call-home client deleted, also removing the \"call-home\" container to maintain YANG validity."); + if ((rc = nc_server_config_check_delete(config, "/ietf-netconf-server:netconf-server/call-home"))) { + goto cleanup; + } + } + +cleanup: + return rc; } API int diff --git a/src/server_config_util_tls.c b/src/server_config_util_tls.c index 4e33bb8e..5c727c65 100644 --- a/src/server_config_util_tls.c +++ b/src/server_config_util_tls.c @@ -267,6 +267,85 @@ nc_server_config_del_ch_tls_keystore_ref(const char *client_name, const char *en "central-keystore-reference", client_name, endpt_name); } +/** + * @brief After deleting a cert or truststore ref, clean up the parent cert bag + * (ee-certs/ca-certs) if empty, and then client-authentication if it has no + * cert bags left. This maintains YANG validation (must 'ca-certs or ee-certs'). + * + * @param[in,out] config Configuration YANG data tree. + * @param[in] client_auth_path Path to the client-authentication container. + * @param[in] cert_bag_name Either "ee-certs" or "ca-certs". + * @return 0 on success, non-zero otherwise. + */ +static int +nc_server_config_tls_cleanup_cert_bag(struct lyd_node **config, const char *client_auth_path, + const char *cert_bag_name) +{ + int rc = 0; + char *path = NULL; + struct lyd_node *node = NULL, *child; + int has_ca = 0, has_ee = 0, bag_has_content = 0; + + /* check if the cert bag (ee-certs/ca-certs) is effectively empty and delete it if so */ + if (asprintf(&path, "%s/%s", client_auth_path, cert_bag_name) == -1) { + ERRMEM; + path = NULL; + rc = 1; + goto cleanup; + } + + if ((rc = lyd_find_path(*config, path, 0, &node))) { + goto cleanup; + } + + /* check if the cert bag has any meaningful content, + * inline-definition with no certificate entries is not meaningful content */ + LY_LIST_FOR(lyd_child(node), child) { + if (!strcmp(LYD_NAME(child), "inline-definition")) { + if (lyd_child(child)) { + bag_has_content = 1; + } + } else { + /* central-truststore-reference or any other node */ + bag_has_content = 1; + } + } + + if (!bag_has_content) { + /* cert bag is empty, delete it */ + VRB(NULL, "Last entry of \"%s\" deleted, also removing the empty cert bag to maintain YANG validity.", + cert_bag_name); + if ((rc = nc_server_config_check_delete(config, "%s", path))) { + goto cleanup; + } + } + + /* check if client-authentication has no ca-certs or ee-certs */ + if ((rc = lyd_find_path(*config, client_auth_path, 0, &node))) { + goto cleanup; + } + + LY_LIST_FOR(lyd_child(node), child) { + if (!strcmp(LYD_NAME(child), "ca-certs")) { + has_ca = 1; + } else if (!strcmp(LYD_NAME(child), "ee-certs")) { + has_ee = 1; + } + } + + if (!has_ca && !has_ee) { + /* neither ca-certs nor ee-certs, delete client-authentication */ + VRB(NULL, "No more cert bags left, also removing \"client-authentication\" to maintain YANG validity."); + if ((rc = nc_server_config_check_delete(config, "%s", client_auth_path))) { + goto cleanup; + } + } + +cleanup: + free(path); + return rc; +} + static int _nc_server_config_add_tls_client_cert(const struct ly_ctx *ctx, const char *tree_path, const char *cert_path, struct lyd_node **config) @@ -326,17 +405,40 @@ nc_server_config_add_tls_client_cert(const struct ly_ctx *ctx, const char *endpt API int nc_server_config_del_tls_client_cert(const char *endpt_name, const char *cert_name, struct lyd_node **config) { + int rc = 0; + char *client_auth_path = NULL; + NC_CHECK_ARG_RET(NULL, endpt_name, config, 1); if (cert_name) { - return nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='%s']/tls/" + rc = nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='%s']/tls/" "tls-server-parameters/client-authentication/ee-certs/inline-definition/" "certificate[name='%s']", endpt_name, cert_name); } else { - return nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='%s']/tls/" + rc = nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='%s']/tls/" "tls-server-parameters/client-authentication/ee-certs/inline-definition/" "certificate", endpt_name); } + if (rc) { + goto cleanup; + } + + /* clean up ee-certs and possibly client-authentication to maintain YANG validation */ + if (asprintf(&client_auth_path, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='%s']/tls/" + "tls-server-parameters/client-authentication", endpt_name) == -1) { + ERRMEM; + client_auth_path = NULL; + rc = 1; + goto cleanup; + } + + if ((rc = nc_server_config_tls_cleanup_cert_bag(config, client_auth_path, "ee-certs"))) { + goto cleanup; + } + +cleanup: + free(client_auth_path); + return rc; } API int @@ -376,17 +478,40 @@ API int nc_server_config_del_ch_tls_client_cert(const char *client_name, const char *endpt_name, const char *cert_name, struct lyd_node **config) { + int rc = 0; + char *client_auth_path = NULL; + NC_CHECK_ARG_RET(NULL, client_name, endpt_name, config, 1); if (cert_name) { - return nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/call-home/netconf-client[name='%s']/" + rc = nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/call-home/netconf-client[name='%s']/" "endpoints/endpoint[name='%s']/tls/tls-server-parameters/client-authentication/ee-certs/" "inline-definition/certificate[name='%s']", client_name, endpt_name, cert_name); } else { - return nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/call-home/netconf-client[name='%s']/" + rc = nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/call-home/netconf-client[name='%s']/" "endpoints/endpoint[name='%s']/tls/tls-server-parameters/client-authentication/ee-certs/" "inline-definition/certificate", client_name, endpt_name); } + if (rc) { + goto cleanup; + } + + /* clean up ee-certs and possibly client-authentication to maintain YANG validation */ + if (asprintf(&client_auth_path, "/ietf-netconf-server:netconf-server/call-home/netconf-client[name='%s']/" + "endpoints/endpoint[name='%s']/tls/tls-server-parameters/client-authentication", client_name, endpt_name) == -1) { + ERRMEM; + client_auth_path = NULL; + rc = 1; + goto cleanup; + } + + if ((rc = nc_server_config_tls_cleanup_cert_bag(config, client_auth_path, "ee-certs"))) { + goto cleanup; + } + +cleanup: + free(client_auth_path); + return rc; } API int @@ -417,10 +542,32 @@ nc_server_config_add_tls_client_cert_truststore_ref(const struct ly_ctx *ctx, co API int nc_server_config_del_tls_client_cert_truststore_ref(const char *endpt_name, struct lyd_node **config) { + int rc = 0; + char *client_auth_path = NULL; + NC_CHECK_ARG_RET(NULL, endpt_name, config, 1); - return nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='%s']/tls/" - "tls-server-parameters/client-authentication/ee-certs/central-truststore-reference", endpt_name); + if ((rc = nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='%s']/tls/" + "tls-server-parameters/client-authentication/ee-certs/central-truststore-reference", endpt_name))) { + goto cleanup; + } + + /* clean up ee-certs and possibly client-authentication to maintain YANG validation */ + if (asprintf(&client_auth_path, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='%s']/tls/" + "tls-server-parameters/client-authentication", endpt_name) == -1) { + ERRMEM; + client_auth_path = NULL; + rc = 1; + goto cleanup; + } + + if ((rc = nc_server_config_tls_cleanup_cert_bag(config, client_auth_path, "ee-certs"))) { + goto cleanup; + } + +cleanup: + free(client_auth_path); + return rc; } API int @@ -454,11 +601,33 @@ API int nc_server_config_del_ch_tls_client_cert_truststore_ref(const char *client_name, const char *endpt_name, struct lyd_node **config) { + int rc = 0; + char *client_auth_path = NULL; + NC_CHECK_ARG_RET(NULL, client_name, endpt_name, config, 1); - return nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/call-home/" + if ((rc = nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/call-home/" "netconf-client[name='%s']/endpoints/endpoint[name='%s']/tls/tls-server-parameters/" - "client-authentication/ee-certs/central-truststore-reference", client_name, endpt_name); + "client-authentication/ee-certs/central-truststore-reference", client_name, endpt_name))) { + goto cleanup; + } + + /* clean up ee-certs and possibly client-authentication to maintain YANG validation */ + if (asprintf(&client_auth_path, "/ietf-netconf-server:netconf-server/call-home/netconf-client[name='%s']/" + "endpoints/endpoint[name='%s']/tls/tls-server-parameters/client-authentication", client_name, endpt_name) == -1) { + ERRMEM; + client_auth_path = NULL; + rc = 1; + goto cleanup; + } + + if ((rc = nc_server_config_tls_cleanup_cert_bag(config, client_auth_path, "ee-certs"))) { + goto cleanup; + } + +cleanup: + free(client_auth_path); + return rc; } API int @@ -495,17 +664,40 @@ nc_server_config_add_tls_ca_cert(const struct ly_ctx *ctx, const char *endpt_nam API int nc_server_config_del_tls_ca_cert(const char *endpt_name, const char *cert_name, struct lyd_node **config) { + int rc = 0; + char *client_auth_path = NULL; + NC_CHECK_ARG_RET(NULL, endpt_name, config, 1); if (cert_name) { - return nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='%s']/tls/" + rc = nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='%s']/tls/" "tls-server-parameters/client-authentication/ca-certs/inline-definition/" "certificate[name='%s']", endpt_name, cert_name); } else { - return nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='%s']/tls/" + rc = nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='%s']/tls/" "tls-server-parameters/client-authentication/ca-certs/inline-definition/" "certificate", endpt_name); } + if (rc) { + goto cleanup; + } + + /* clean up ca-certs and possibly client-authentication to maintain YANG validation */ + if (asprintf(&client_auth_path, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='%s']/tls/" + "tls-server-parameters/client-authentication", endpt_name) == -1) { + ERRMEM; + client_auth_path = NULL; + rc = 1; + goto cleanup; + } + + if ((rc = nc_server_config_tls_cleanup_cert_bag(config, client_auth_path, "ca-certs"))) { + goto cleanup; + } + +cleanup: + free(client_auth_path); + return rc; } API int @@ -545,17 +737,40 @@ API int nc_server_config_del_ch_tls_ca_cert(const char *client_name, const char *endpt_name, const char *cert_name, struct lyd_node **config) { + int rc = 0; + char *client_auth_path = NULL; + NC_CHECK_ARG_RET(NULL, client_name, endpt_name, config, 1); if (cert_name) { - return nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/call-home/netconf-client[name='%s']/" + rc = nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/call-home/netconf-client[name='%s']/" "endpoints/endpoint[name='%s']/tls/tls-server-parameters/client-authentication/ca-certs/" "inline-definition/certificate[name='%s']", client_name, endpt_name, cert_name); } else { - return nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/call-home/netconf-client[name='%s']/" + rc = nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/call-home/netconf-client[name='%s']/" "endpoints/endpoint[name='%s']/tls/tls-server-parameters/client-authentication/ca-certs/" "inline-definition/certificate", client_name, endpt_name); } + if (rc) { + goto cleanup; + } + + /* clean up ca-certs and possibly client-authentication to maintain YANG validation */ + if (asprintf(&client_auth_path, "/ietf-netconf-server:netconf-server/call-home/netconf-client[name='%s']/" + "endpoints/endpoint[name='%s']/tls/tls-server-parameters/client-authentication", client_name, endpt_name) == -1) { + ERRMEM; + client_auth_path = NULL; + rc = 1; + goto cleanup; + } + + if ((rc = nc_server_config_tls_cleanup_cert_bag(config, client_auth_path, "ca-certs"))) { + goto cleanup; + } + +cleanup: + free(client_auth_path); + return rc; } API int @@ -586,10 +801,32 @@ nc_server_config_add_tls_ca_cert_truststore_ref(const struct ly_ctx *ctx, const API int nc_server_config_del_tls_ca_cert_truststore_ref(const char *endpt_name, struct lyd_node **config) { + int rc = 0; + char *client_auth_path = NULL; + NC_CHECK_ARG_RET(NULL, endpt_name, config, 1); - return nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='%s']/tls/" - "tls-server-parameters/client-authentication/ca-certs/central-truststore-reference", endpt_name); + if ((rc = nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='%s']/tls/" + "tls-server-parameters/client-authentication/ca-certs/central-truststore-reference", endpt_name))) { + goto cleanup; + } + + /* clean up ca-certs and possibly client-authentication to maintain YANG validation */ + if (asprintf(&client_auth_path, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='%s']/tls/" + "tls-server-parameters/client-authentication", endpt_name) == -1) { + ERRMEM; + client_auth_path = NULL; + rc = 1; + goto cleanup; + } + + if ((rc = nc_server_config_tls_cleanup_cert_bag(config, client_auth_path, "ca-certs"))) { + goto cleanup; + } + +cleanup: + free(client_auth_path); + return rc; } API int @@ -623,11 +860,33 @@ API int nc_server_config_del_ch_tls_ca_cert_truststore_ref(const char *client_name, const char *endpt_name, struct lyd_node **config) { + int rc = 0; + char *client_auth_path = NULL; + NC_CHECK_ARG_RET(NULL, client_name, endpt_name, config, 1); - return nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/call-home/" + if ((rc = nc_server_config_delete(config, "/ietf-netconf-server:netconf-server/call-home/" "netconf-client[name='%s']/endpoints/endpoint[name='%s']/tls/tls-server-parameters/" - "client-authentication/ca-certs/central-truststore-reference", client_name, endpt_name); + "client-authentication/ca-certs/central-truststore-reference", client_name, endpt_name))) { + goto cleanup; + } + + /* clean up ca-certs and possibly client-authentication to maintain YANG validation */ + if (asprintf(&client_auth_path, "/ietf-netconf-server:netconf-server/call-home/netconf-client[name='%s']/" + "endpoints/endpoint[name='%s']/tls/tls-server-parameters/client-authentication", client_name, endpt_name) == -1) { + ERRMEM; + client_auth_path = NULL; + rc = 1; + goto cleanup; + } + + if ((rc = nc_server_config_tls_cleanup_cert_bag(config, client_auth_path, "ca-certs"))) { + goto cleanup; + } + +cleanup: + free(client_auth_path); + return rc; } static const char * diff --git a/tests/test_config.c b/tests/test_config.c index 9bf8389c..a62b7d10 100644 --- a/tests/test_config.c +++ b/tests/test_config.c @@ -795,6 +795,70 @@ test_config_all_nodes(void **state) lyd_free_all(tree); } +static void +test_config_cascade_delete(void **state) +{ + int ret; + struct lyd_node *tree = NULL, *node = NULL; + struct ln2_test_ctx *test_ctx = *state; + + /* + * Test the fix for GitHub issue #614: + * Deleting the last entry of a list inside a presence container must + * also delete the empty presence container to maintain YANG validity. + */ + + /* add a listen endpoint and a call-home client */ + ret = nc_server_config_add_address_port(test_ctx->ctx, "endpt", NC_TI_SSH, + "127.0.0.1", TEST_PORT, &tree); + assert_int_equal(ret, 0); + ret = nc_server_config_add_ssh_hostkey(test_ctx->ctx, "endpt", "hostkey1", + TESTS_DIR "/data/key_rsa", NULL, &tree); + assert_int_equal(ret, 0); + ret = nc_server_config_add_ssh_user_password(test_ctx->ctx, "endpt", "user1", "passwd", &tree); + assert_int_equal(ret, 0); + + ret = nc_server_config_add_ch_address_port(test_ctx->ctx, "ch1", "ch-endpt1", + NC_TI_SSH, "127.0.0.1", TEST_PORT_2_STR, &tree); + assert_int_equal(ret, 0); + ret = nc_server_config_add_ch_ssh_hostkey(test_ctx->ctx, "ch1", "ch-endpt1", + "hostkey1", TESTS_DIR "/data/key_rsa", NULL, &tree); + assert_int_equal(ret, 0); + ret = nc_server_config_add_ch_ssh_user_password(test_ctx->ctx, "ch1", "ch-endpt1", + "user1", "passwd", &tree); + assert_int_equal(ret, 0); + ret = nc_server_config_add_ch_persistent(test_ctx->ctx, "ch1", &tree); + assert_int_equal(ret, 0); + + /* must be valid */ + ret = lyd_validate_all(&tree, test_ctx->ctx, LYD_VALIDATE_PRESENT, NULL); + assert_int_equal(ret, 0); + + /* delete the last listen endpoint, the listen container should be deleted as well */ + ret = nc_server_config_del_endpt("endpt", &tree); + assert_int_equal(ret, 0); + ret = lyd_find_path(tree, "/ietf-netconf-server:netconf-server/listen", 0, &node); + + /* listen should have been deleted, lyd_find_path should find the parent netconf-server container */ + assert_int_equal(ret, LY_EINCOMPLETE); + assert_int_equal(strcmp(LYD_NAME(node), "netconf-server"), 0); + + /* delete the last call-home client, the call-home container should be deleted as well */ + ret = nc_server_config_del_ch_client("ch1", &tree); + assert_int_equal(ret, 0); + ret = lyd_find_path(tree, "/ietf-netconf-server:netconf-server/call-home", 0, &node); + + /* call-home should have been deleted, lyd_find_path should find the parent netconf-server container */ + assert_int_equal(ret, LY_EINCOMPLETE); + assert_int_equal(strcmp(LYD_NAME(node), "netconf-server"), 0); + + /* the configuration should stay valid */ + ret = lyd_validate_all(&tree, test_ctx->ctx, LYD_VALIDATE_PRESENT, NULL); + assert_int_equal(ret, 0); + + lyd_free_all(tree); +} + static void * unsupported_asymkey_client_thread(void *arg) { @@ -1036,6 +1100,7 @@ main(void) cmocka_unit_test_setup_teardown(test_preserve_conn, setup_f, ln2_glob_test_teardown), cmocka_unit_test_setup_teardown(test_transport_params_oper_get, setup_f, ln2_glob_test_teardown), cmocka_unit_test_setup_teardown(test_config_all_nodes, setup_f, ln2_glob_test_teardown), + cmocka_unit_test_setup_teardown(test_config_cascade_delete, setup_f, ln2_glob_test_teardown), cmocka_unit_test_setup_teardown(test_unusupported_asymkey_format, setup_f, ln2_glob_test_teardown), cmocka_unit_test_setup_teardown(test_invalid_diff, setup_f, ln2_glob_test_teardown), }; From aaf5f655e1dc16aa53c73fb9793106421ed9d0c6 Mon Sep 17 00:00:00 2001 From: Roman Janota Date: Fri, 31 Jul 2026 15:11:12 +0200 Subject: [PATCH 2/3] SOVERSION bump to version 5.4.14 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 68d03757..139b3330 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,7 +67,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 4) -set(LIBNETCONF2_MICRO_SOVERSION 13) +set(LIBNETCONF2_MICRO_SOVERSION 14) set(LIBNETCONF2_SOVERSION_FULL ${LIBNETCONF2_MAJOR_SOVERSION}.${LIBNETCONF2_MINOR_SOVERSION}.${LIBNETCONF2_MICRO_SOVERSION}) set(LIBNETCONF2_SOVERSION ${LIBNETCONF2_MAJOR_SOVERSION}) From a2c3d7778c8a780b5dd8676615fbd0641726ba75 Mon Sep 17 00:00:00 2001 From: Roman Janota Date: Fri, 31 Jul 2026 15:11:26 +0200 Subject: [PATCH 3/3] VERSION bump to version 4.4.15 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 139b3330..28dbb18c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,7 +59,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 4) -set(LIBNETCONF2_MICRO_VERSION 14) +set(LIBNETCONF2_MICRO_VERSION 15) set(LIBNETCONF2_VERSION ${LIBNETCONF2_MAJOR_VERSION}.${LIBNETCONF2_MINOR_VERSION}.${LIBNETCONF2_MICRO_VERSION}) # Version of the library