diff --git a/.github/workflows/analyze-target.yml b/.github/workflows/analyze-target.yml index 618735849d..c8f657b3bc 100644 --- a/.github/workflows/analyze-target.yml +++ b/.github/workflows/analyze-target.yml @@ -1,7 +1,7 @@ name: "Analyze (target)" on: pull_request_target: - branches: [master] + branches: [master, failover] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73e7b34fbe..81c0c2ab75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,9 @@ name: "ci" on: push: - branches: [master] + branches: [master, failover] pull_request: - branches: [master] + branches: [master, failover] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml index deb6f0c0e1..ec45680913 100644 --- a/.github/workflows/coverity.yml +++ b/.github/workflows/coverity.yml @@ -10,6 +10,7 @@ on: pull_request_target: branches: - master + - failover types: - labeled workflow_dispatch: diff --git a/.github/workflows/static-code-analysis.yml b/.github/workflows/static-code-analysis.yml index 11c6b840c0..48facbc445 100644 --- a/.github/workflows/static-code-analysis.yml +++ b/.github/workflows/static-code-analysis.yml @@ -1,9 +1,9 @@ name: "Static code analysis" on: push: - branches: [master] + branches: [master, failover] pull_request: - branches: [master] + branches: [master, failover] schedule: # Everyday at midnight - cron: '0 0 * * *' diff --git a/Makefile.am b/Makefile.am index 05e0baba93..1f8e5102ca 100644 --- a/Makefile.am +++ b/Makefile.am @@ -301,6 +301,7 @@ if HAVE_CMOCKA test_sbus_message \ test_sbus_opath \ test_fo_srv \ + test_failover_server \ pam-srv-tests \ ssh-srv-tests \ test_ipa_subdom_util \ @@ -422,6 +423,10 @@ sssdlib_LTLIBRARIES += \ $(NULL) endif +sssdlib_LTLIBRARIES += \ + libsss_minimal.la \ + $(NULL) + ldblib_LTLIBRARIES = \ memberof.la @@ -648,6 +653,24 @@ SSSD_FAILOVER_OBJ = \ src/providers/fail_over_srv.c \ $(SSSD_RESOLV_OBJ) +# Make sure to build new failover code to test compilation even though it is +# not used anywhere yet. +SSSD_NEW_FAILOVER_OBJ = \ + src/providers/failover/failover.c \ + src/providers/failover/failover_callback.c \ + src/providers/failover/failover_refresh_candidates.c \ + src/providers/failover/failover_group.c \ + src/providers/failover/failover_server_resolve.c \ + src/providers/failover/failover_server.c \ + src/providers/failover/failover_srv.c \ + src/providers/failover/failover_transaction.c \ + src/providers/failover/failover_vtable_op.c \ + src/providers/failover/failover_vtable.c \ + src/providers/failover/ldap/failover_ldap_connect.c \ + src/providers/failover/ldap/failover_ldap_kinit.c \ + $(SSSD_RESOLV_OBJ) \ + $(NULL) + SSSD_LIBS = \ $(TALLOC_LIBS) \ $(TEVENT_LIBS) \ @@ -840,6 +863,15 @@ dist_noinst_HEADERS = \ src/providers/be_refresh.h \ src/providers/fail_over.h \ src/providers/fail_over_srv.h \ + src/providers/failover/failover.h \ + src/providers/failover/failover_group.h \ + src/providers/failover/failover_refresh_candidates.h \ + src/providers/failover/failover_server.h \ + src/providers/failover/failover_server_resolve.h \ + src/providers/failover/failover_srv.h \ + src/providers/failover/failover_transaction.h \ + src/providers/failover/failover_vtable.h \ + src/providers/failover/failover_vtable_op.h \ src/util/child_common.h \ src/util/child_bootstrap.h \ src/providers/simple/simple_access.h \ @@ -901,6 +933,7 @@ dist_noinst_HEADERS = \ src/providers/idp/idp_id.h \ src/providers/idp/idp_opts.h \ src/providers/idp/idp_private.h \ + src/providers/minimal/minimal_id.h \ src/tools/tools_util.h \ src/resolv/async_resolv.h \ src/tests/common.h \ @@ -3471,6 +3504,24 @@ test_fo_srv_LDADD = \ libsss_test_common.la \ $(NULL) +test_failover_server_SOURCES = \ + src/tests/cmocka/test_failover_server.c \ + src/providers/failover/failover_server.c \ + $(SSSD_RESOLV_TESTS_OBJ) \ + $(NULL) +test_failover_server_CFLAGS = \ + $(AM_CFLAGS) \ + $(CMOCKA_CFLAGS) \ + $(NULL) +test_failover_server_LDADD = \ + $(CARES_LIBS) \ + $(CMOCKA_LIBS) \ + $(POPT_LIBS) \ + $(SSSD_INTERNAL_LTLIBS) \ + $(TALLOC_LIBS) \ + libsss_test_common.la \ + $(NULL) + test_sdap_initgr_SOURCES = \ src/tests/cmocka/common_mock_sdap.c \ src/tests/cmocka/common_mock_sysdb_objects.c \ @@ -4678,6 +4729,28 @@ libsss_idp_la_LDFLAGS = \ -module \ $(NULL) +libsss_minimal_la_SOURCES = \ + src/providers/minimal/minimal_init.c \ + src/providers/minimal/minimal_id.c \ + src/providers/minimal/minimal_id_services.c \ + src/providers/minimal/minimal_ldap_auth.c \ + $(SSSD_NEW_FAILOVER_OBJ) \ + $(NULL) + +libsss_minimal_la_CFLAGS = \ + $(AM_CFLAGS) \ + $(NULL) + +libsss_minimal_la_LIBADD = \ + $(SSSD_INTERNAL_LTLIBS) \ + libsss_ldap_common.la \ + $(NULL) + +libsss_minimal_la_LDFLAGS = \ + -avoid-version \ + -module \ + $(NULL) + krb5_child_SOURCES = \ src/providers/krb5/krb5_child.c \ src/providers/krb5/krb5_child_share.c \ diff --git a/minimal-provider-notes.txt b/minimal-provider-notes.txt new file mode 100644 index 0000000000..45a5b7bf06 --- /dev/null +++ b/minimal-provider-notes.txt @@ -0,0 +1,88 @@ +# Minimal SSSD provider + +This is used as a proof of concept for the new failover implementation. It can +also be used to see what changes are required in order to switch to the new +code, however it really does only minimum amount of changes to get it working. +It would be very good to provide more thorough refactoring in the real +providers. + +The minimal provider supports: +- services lookup (getent services) +- user authentication + +## Populate LDAP + +``` +$ vim objects.ldif +dn: ou=users,dc=ldap,dc=test +objectClass: top +objectClass: organizationalUnit +ou: users + +# Password is Secret123 +dn: cn=user-1,ou=users,dc=ldap,dc=test +uid: user-1 +uidNumber: 10000 +homeDirectory: /home/user-1 +gidNumber: 100000 +cn: user-1 +objectClass: posixAccount +objectClass: top +userPassword:: e1BCS0RGMi1TSEE1MTJ9MTAwMDAwJEVZU2lqOFgxTTVFZUIrMXlHQzdvZkhwZzd + XZXpYRGJwJG0vTVUyMUIrTGNNb2tkRVcvUFJ6YWlhc21zdlNDeVJWdGxPU3c3c05YbHk2NUxBcUcz + ODJqQUJWUEp2N1ZnOUtRdXhEamVlbmxEV3V5Ylg5UFdKMW5nPT0= + +dn: ou=services,dc=ldap,dc=test +objectClass: top +objectClass: organizationalUnit +ou: services + +dn: cn=service0,ou=services,dc=ldap,dc=test +objectClass: ipService +cn: service0 +ipServiceProtocol: tcp +ipServicePort: 12345 + +$ ldapadd -D "cn=Directory Manager" -w Secret123 -H ldap://master.ldap.test -f objects.ldif -vv +``` + +## Verify LDAP contents + +``` +$ ldapsearch -D "cn=Directory Manager" -w Secret123 -H ldap://master.ldap.test -b dc=ldap,dc=test +``` + +## Configure SSSD for services lookup + +``` +[sssd] +domains = minimal + +[domain/minimal] +debug_level = 9 +id_provider = minimal + +$ getent services -s sss service0 +service0 12345/tcp +``` + +## Configure SSSD for user authentication + +Note: user lookup is done by id provider + +``` +[sssd] +services = nss, pam +domains = minimal + +[domain/minimal] +debug_level = 9 +id_provider = ldap +auth_provider = minimal +ldap_uri = _srv_ +dns_discovery_domain = ldap.test +ldap_tls_reqcert = never + +$ su user-1 +Password: Secret123 +``` diff --git a/src/providers/failover/failover.c b/src/providers/failover/failover.c new file mode 100644 index 0000000000..c42d9c81cb --- /dev/null +++ b/src/providers/failover/failover.c @@ -0,0 +1,214 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include + +#include "config.h" +#include "providers/failover/failover.h" +#include "providers/failover/failover_group.h" +#include "util/util.h" +#include "util/sss_ptr_list.h" + +static struct sss_failover_candidates_ctx * +sss_failover_candidates_init(TALLOC_CTX *mem_ctx, + unsigned int max_servers, + unsigned int min_refresh_time) +{ + struct sss_failover_candidates_ctx *ctx; + errno_t ret; + + ctx = talloc_zero(mem_ctx, struct sss_failover_candidates_ctx); + if (ctx == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + return NULL; + } + + ctx->refresh_req = NULL; + ctx->last_refresh_time = 0; + ctx->min_refresh_time = min_refresh_time; + + /* Setup list of candidate servers. */ + ctx->servers = talloc_zero_array(ctx, struct sss_failover_server *, + max_servers + 1); + if (ctx->servers == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + + ctx->notify_queue = tevent_queue_create(ctx, "candidates_notify_queue"); + if (ctx->notify_queue == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + + /* Stop the queue. It will be started when candidates are refreshed. */ + tevent_queue_stop(ctx->notify_queue); + + ret = EOK; + +done: + if (ret != EOK) { + talloc_free(ctx); + return NULL; + } + + return ctx; +} + +static int +sss_failover_destructor(struct sss_failover_ctx *fctx) +{ + return 0; +} + +struct sss_failover_ctx * +sss_failover_init(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + const char *name, + struct resolv_ctx *resolver_ctx, + enum restrict_family family_order) +{ + struct sss_failover_ctx *fctx; + errno_t ret; + + DEBUG(SSSDBG_TRACE_FUNC, "Creating failover context for service %s\n", + name); + + fctx = talloc_zero(mem_ctx, struct sss_failover_ctx); + if (fctx == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + return NULL; + } + + /* TODO init */ + fctx->ev = ev; + fctx->name = talloc_strdup(fctx, name); + fctx->resolver_ctx = resolver_ctx; + fctx->family_order = family_order; + fctx->kinit_ctx = NULL; + + if (fctx->name == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + + /* Configuration. TODO make it configurable. */ + fctx->opts.max_candidates = 5; + fctx->opts.min_refresh_time = 60; + fctx->opts.ping_timeout = 3; + fctx->opts.negative_dns_srv_ttl = 3600; + fctx->opts.min_candidates_lookup_time = 1; + + /* Setup server groups. We expect at least two groups: primary and backup */ + fctx->current_group = 0; + fctx->groups = talloc_zero_array(fctx, struct sss_failover_group *, 3); + if (fctx->groups == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + + /* Setup list of candidate servers. */ + fctx->candidates = sss_failover_candidates_init( + fctx, fctx->opts.max_candidates, fctx->opts.min_refresh_time); + if (fctx->candidates == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + + /* We are not connected to any server yet. */ + fctx->active_server = NULL; + + fctx->vtable = talloc_zero(fctx, struct sss_failover_vtable); + if (fctx->vtable == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + + fctx->vtable_op_queue = tevent_queue_create(fctx, "vtable_op_queue"); + if (fctx->vtable_op_queue == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + + talloc_set_destructor(fctx, sss_failover_destructor); + + ret = EOK; + +done: + if (ret != EOK) { + talloc_zfree(fctx); + } + + return fctx; +} + +void +sss_failover_set_active_server(struct sss_failover_ctx *fctx, + struct sss_failover_server *server) +{ + if (fctx->active_server != NULL) { + if (server == fctx->active_server) { + /* it is the same server, nothing to do */ + return; + } + + DEBUG(SSSDBG_TRACE_FUNC, "Releasing old active server %s\n", + fctx->active_server->name); + + talloc_unlink(fctx, fctx->active_server); + } + + DEBUG(SSSDBG_TRACE_FUNC, "Setting new active server %s\n", server->name); + fctx->active_server = talloc_reference(fctx, server); +} + +void +sss_failover_set_connection(struct sss_failover_ctx *fctx, void *connection) +{ + if (fctx->connection != NULL) { + if (connection == fctx->connection) { + /* it is the same connection, nothing to do */ + return; + } + + DEBUG(SSSDBG_TRACE_FUNC, "Releasing old connection %p\n", + fctx->connection); + + talloc_unlink(fctx, fctx->connection); + } + + DEBUG(SSSDBG_TRACE_FUNC, "Setting new connection %p\n", connection); + fctx->connection = talloc_steal(fctx, connection); +} + +void * +sss_failover_get_connection(TALLOC_CTX *mem_ctx, struct sss_failover_ctx *fctx) +{ + if (fctx->connection == NULL) { + return NULL; + } + + return talloc_reference(mem_ctx, fctx->connection); +} diff --git a/src/providers/failover/failover.h b/src/providers/failover/failover.h new file mode 100644 index 0000000000..91d34b2610 --- /dev/null +++ b/src/providers/failover/failover.h @@ -0,0 +1,151 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef _FAILOVER_H_ +#define _FAILOVER_H_ + +#include +#include + +#include "config.h" +#include "resolv/async_resolv.h" +#include "providers/failover/failover_server.h" +#include "providers/failover/failover_group.h" +#include "providers/failover/failover_vtable.h" +#include "util/util.h" + +struct sss_failover_candidates_ctx { + /* List of servers that were found as working. */ + struct sss_failover_server **servers; + + /* Active refresh request. NULL if there is no ongoing refresh. */ + struct tevent_req *refresh_req; + + /* This queue serves as a notification mechanism. It is started when + * candidates list were refreshed and is stopped when the list is being + * refreshed. + */ + struct tevent_queue *notify_queue; + + /* Last refresh time. */ + unsigned int last_refresh_time; + + /* Do not issue new refresh if now < last_refresh_time + min_refresh_time */ + unsigned int min_refresh_time; +}; + +struct sss_failover_options { + /* Maximum number of candidate servers. */ + unsigned int max_candidates; + + /* Minimum time that has to elapse before refreshing candidates again. */ + unsigned int min_refresh_time; + + /* Minimum amount of time that will wait for candidates servers to respond + to a ping. If any server is found within this time, we do not wait for other + servers to respond and return what we have. */ + unsigned int min_candidates_lookup_time; + + /* How long do we want to wait for a server ping to succeed. */ + unsigned int ping_timeout; + + /* TTL for missing DNS SRV records. */ + unsigned int negative_dns_srv_ttl; +}; + +struct sss_failover_ctx { + struct tevent_context *ev; + char *name; + struct resolv_ctx *resolver_ctx; + struct sss_failover_vtable *vtable; + enum restrict_family family_order; + + struct sss_failover_options opts; + + /* NULL-terminated list of failover server groups. The first group has the + * highest priority. */ + struct sss_failover_group **groups; + + /* Currently selected group that provided server candidates. */ + unsigned int current_group; + + /* Non-NULL if kinit is required to connect to the server. The context may + * be the same to make sure the same server is used for KDC and connection + * or different. */ + struct sss_failover_ctx *kinit_ctx; + + /* Candidate servers. */ + struct sss_failover_candidates_ctx *candidates; + + /* Currently active server. */ + struct sss_failover_server *active_server; + + /* Backend specific established connection. */ + void *connection; + + /* Queue of sss_vtable_op tevent requests. These requests are used to + * connect to the server and the queue serializes the requests to ensure + * that we establish only one connection that is then reused. */ + struct tevent_queue *vtable_op_queue; +}; + +/** + * @brief Initialize failover context. + * + * @param mem_ctx + * @param ev + * @param resolver_ctx + * @param family_order + * @return struct sss_failover_ctx* + */ +struct sss_failover_ctx * +sss_failover_init(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + const char *name, + struct resolv_ctx *resolver_ctx, + enum restrict_family family_order); + +/** + * @brief Set active server. + * + * This is a noop if @server and @fctx->active_server is identical. + */ +void +sss_failover_set_active_server(struct sss_failover_ctx *fctx, + struct sss_failover_server *server); + +/** + * @brief Set new connection, release old one. + * + * This is a noop if @connection and @fctx->connection is identical. + */ +void +sss_failover_set_connection(struct sss_failover_ctx *fctx, void *connection); + +/** + * @brief Get connection. + * + * The connection is talloc_reference to mem_ctx. + * + * @param mem_ctx + * @param fctx + * @return void* + */ +void * +sss_failover_get_connection(TALLOC_CTX *mem_ctx, struct sss_failover_ctx *fctx); + +#endif /* _FAILOVER_H_ */ diff --git a/src/providers/failover/failover_callback.c b/src/providers/failover/failover_callback.c new file mode 100644 index 0000000000..1d43351bbe --- /dev/null +++ b/src/providers/failover/failover_callback.c @@ -0,0 +1,22 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include + +#include "config.h" +#include "providers/failover/failover.h" +#include "util/util.h" diff --git a/src/providers/failover/failover_group.c b/src/providers/failover/failover_group.c new file mode 100644 index 0000000000..ef38044e86 --- /dev/null +++ b/src/providers/failover/failover_group.c @@ -0,0 +1,383 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include + +#include "config.h" +#include "providers/failover/failover_group.h" +#include "providers/failover/failover_server.h" +#include "providers/failover/failover_srv.h" +#include "providers/failover/failover.h" +#include "util/util.h" + +static errno_t +sss_failover_group_allocate_slot(struct sss_failover_ctx *fctx, + unsigned int *_slot) +{ + size_t count; + unsigned int slot; + + count = talloc_array_length(fctx->groups); + + for (slot = 0; fctx->groups[slot] != NULL && slot < count; slot++) { + /* Find the first NULL slot. slot < count is just for safety */ + } + + /* We need to allocate more items? */ + if (slot >= count - 1) { + fctx->groups = talloc_realloc(fctx, fctx->groups, + struct sss_failover_group *, count + 1); + if (fctx->groups == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + return ENOMEM; + } + + fctx->groups[count] = NULL; + fctx->groups[count - 1] = NULL; + slot = count - 1; + } + + *_slot = slot; + + return EOK; +} + +struct sss_failover_group * +sss_failover_group_new(struct sss_failover_ctx *fctx, + const char *name) +{ + struct sss_failover_group *group; + unsigned int slot; + errno_t ret; + + if (name == NULL || fctx == NULL || fctx->groups == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Invalid parameters!\n"); + return NULL; + } + + ret = sss_failover_group_allocate_slot(fctx, &slot); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to allocate slot [%d]: %s\n", ret, + sss_strerror(ret)); + return NULL; + } + + DEBUG(SSSDBG_TRACE_FUNC, "Creating failover group %s:%u\n", name, slot); + + group = talloc_zero(fctx->groups, struct sss_failover_group); + if (group == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + return NULL; + } + + group->name = talloc_strdup(group, name); + if (group->name == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + + group->slot = slot; + + group->configured_servers = talloc_zero_array(group, struct sss_failover_server *, 1); + if (group->configured_servers == NULL) { + ret = ENOMEM; + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + goto done; + } + + group->dns_discovery_enabled = false; + group->discovered_servers = talloc_zero_array(group, struct sss_failover_server *, 1); + if (group->discovered_servers == NULL) { + ret = ENOMEM; + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + goto done; + } + + group->servers = talloc_zero_array(group, struct sss_failover_server *, 1); + if (group->servers == NULL) { + ret = ENOMEM; + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + goto done; + } + + fctx->groups[slot] = group; + + ret = EOK; + +done: + if (ret != EOK) { + talloc_free(group); + return NULL; + } + + return group; +} + +errno_t +sss_failover_group_setup_dns_discovery(struct sss_failover_group *group) +{ + group->dns_discovery_enabled = true; + + return EOK; +} + +errno_t +sss_failover_group_add_server(struct sss_failover_group *group, + struct sss_failover_server *server) +{ + struct sss_failover_server **new_array; + size_t count; + + if (group == NULL || group->configured_servers == NULL || server == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Invalid arguments\n"); + return EINVAL; + } + + count = talloc_array_length(group->configured_servers); + + new_array = talloc_realloc(group, group->configured_servers, + struct sss_failover_server *, count + 1); + if (new_array == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + return ENOMEM; + } + + group->configured_servers = new_array; + group->configured_servers[count - 1] = talloc_steal(group->configured_servers, server); + group->configured_servers[count] = NULL; + + return EOK; +} + +struct sss_failover_group_resolve_state { + struct sss_failover_ctx *fctx; + struct sss_failover_group *group; +}; + +static void sss_failover_group_resolve_done(struct tevent_req *subreq); + +struct tevent_req * +sss_failover_group_resolve_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + struct sss_failover_group *group) +{ + struct sss_failover_group_resolve_state *state; + struct tevent_req *subreq; + struct tevent_req *req; + errno_t ret; + time_t now; + + DEBUG(SSSDBG_TRACE_FUNC, "Resolving server group %s:%d\n", group->name, + group->slot); + + req = tevent_req_create(mem_ctx, &state, + struct sss_failover_group_resolve_state); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "tevent_req_create() failed\n"); + return NULL; + } + + state->fctx = fctx; + state->group = group; + + now = time(NULL); + if (group->dns_discovery_enabled && group->dns_expiration_time < now) { + /* Refresh SRV records. */ + const char *domains[] = {"ldap.test", NULL}; + const char *protocol = "tcp"; + const char *service = "ldap"; + + // TODO handle protocol, service, domains and plugin + subreq = sss_failover_srv_resolve_send(state, ev, fctx, service, + protocol, domains); + if (subreq == NULL) { + ret = ENOMEM; + goto done; + } + + tevent_req_set_callback(subreq, sss_failover_group_resolve_done, req); + + ret = EAGAIN; + } else { + /* We have what we need. */ + ret = EOK; + } + +done: + if (ret == EOK) { + tevent_req_done(req); + tevent_req_post(req, ev); + } else if (ret != EAGAIN) { + tevent_req_error(req, ret); + tevent_req_post(req, ev); + } + + return req; +} + +static void sss_failover_group_resolve_done(struct tevent_req *subreq) +{ + struct sss_failover_group_resolve_state *state; + struct sss_failover_server **servers; + struct tevent_req *req; + uint32_t ttl; + errno_t ret; + + req = tevent_req_callback_data(subreq, struct tevent_req); + state = tevent_req_data(req, struct sss_failover_group_resolve_state); + + ret = sss_failover_srv_resolve_recv(state, subreq, &ttl, + &servers); + talloc_zfree(subreq); + if (ret == ENOENT) { + ttl = state->fctx->opts.negative_dns_srv_ttl; + servers = talloc_zero_array(state, struct sss_failover_server *, 1); + if (servers == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + ret = EOK; + } else if (ret != EOK) { + goto done; + } + + talloc_zfree(state->group->discovered_servers); + state->group->discovered_servers = talloc_steal(state->group, servers); + state->group->dns_expiration_time = time(NULL) + ttl; + +done: + if (ret != EOK) { + tevent_req_error(req, ret); + return; + } + + tevent_req_done(req); +} + +errno_t +sss_failover_group_resolve_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + struct sss_failover_server ***_servers) +{ + struct sss_failover_group_resolve_state *state; + struct sss_failover_server *current; + struct sss_failover_server **out; + size_t count_conf; + size_t count_dns; + size_t count; + int i, j, k; + bool found; + + state = tevent_req_data(req, struct sss_failover_group_resolve_state); + + TEVENT_REQ_RETURN_ON_ERROR(req); + + count_conf = talloc_array_length(state->group->configured_servers) - 1; + count_dns = talloc_array_length(state->group->discovered_servers) - 1; + count = count_conf + count_dns; + + DEBUG(SSSDBG_TRACE_FUNC, + "There are %zu configured servers inside group %d:%s:\n", + count_conf, state->group->slot, state->group->name); + + if (DEBUG_IS_SET(SSSDBG_TRACE_ALL)) { + for (i = 0; state->group->configured_servers[i] != NULL; i++) { + current = state->group->configured_servers[i]; + DEBUG(SSSDBG_TRACE_ALL, "- %s:%u\n", current->name, current->port); + } + } + + if (state->group->dns_discovery_enabled) { + DEBUG(SSSDBG_TRACE_FUNC, + "Discovered %zu servers from DNS inside group %d:%s:\n", + count_dns, state->group->slot, state->group->name); + + if (DEBUG_IS_SET(SSSDBG_TRACE_ALL)) { + for (i = 0; state->group->discovered_servers[i] != NULL; i++) { + current = state->group->discovered_servers[i]; + DEBUG(SSSDBG_TRACE_ALL, "- %s:%u\n", current->name, + current->port); + } + } + } + + out = talloc_zero_array(mem_ctx, struct sss_failover_server *, count + 1); + if (out == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + return ENOMEM; + } + + /* Add configured servers first. */ + for (i = 0; state->group->configured_servers[i] != NULL; i++) { + out[i] = talloc_reference(out, state->group->configured_servers[i]); + if (out[i] == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + talloc_free(out); + return ENOMEM; + } + } + + /* Now add discovered servers. But avoid adding duplicates. */ + for (j = 0; state->group->discovered_servers[j] != NULL; j++, i++) { + found = false; + current = state->group->discovered_servers[j]; + for (k = 0; out[k] != NULL; k++) { + if (sss_failover_server_equal(out[k], current)) { + found = true; + break; + } + } + + if (found) { + break; + } + + out[i] = talloc_reference(out, current); + if (out[i] == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + talloc_free(out); + return ENOMEM; + } + } + + + // TODO sort by priority and weight + + for (count = 0; out[count] != NULL; count++); + out = talloc_realloc(mem_ctx, out, struct sss_failover_server *, count + 1); + if (out == NULL) { + talloc_free(out); + return ENOMEM; + } + + if (DEBUG_IS_SET(SSSDBG_TRACE_ALL)) { + DEBUG(SSSDBG_TRACE_ALL, "Sorted server list without duplicates:\n"); + for (i = 0; out[i] != NULL; i++) { + DEBUG(SSSDBG_TRACE_ALL, "- %s:%u\n", out[i]->name, out[i]->port); + } + } + + DEBUG(SSSDBG_TRACE_FUNC, "Returning %zu servers from group %d:%s\n", count, + state->group->slot, state->group->name); + + *_servers = out; + + return EOK; +} diff --git a/src/providers/failover/failover_group.h b/src/providers/failover/failover_group.h new file mode 100644 index 0000000000..0527109d6e --- /dev/null +++ b/src/providers/failover/failover_group.h @@ -0,0 +1,116 @@ +;/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef _FAILOVER_GROUP_H_ +#define _FAILOVER_GROUP_H_ + +#include + +#include "config.h" +#include "util/util.h" +#include "providers/failover/failover_server.h" + +struct sss_failover_ctx; + +struct sss_failover_group { + struct sss_failover_ctx *fctx; + + /* Group name. */ + char *name; + + /* Priority. 0 = highest priority (primary servers). */ + unsigned int slot; + + /* DNS SRV plugin information */ + bool dns_discovery_enabled; + time_t dns_expiration_time; + void *dns_plugin_data; + void *dns_plugin; + + /* Configured or discovered servers. */ + struct sss_failover_server **configured_servers; + struct sss_failover_server **discovered_servers; + + /* Servers inside this group. Sorted by priority and weight. */ + struct sss_failover_server **servers; +}; + +/** + * @brief Create new server group @name. + * + * Add new static servers to it with @sss_failover_server_group_add_server. + * + * @param fctx + * @param name + * @return struct sss_failover_group* + */ +struct sss_failover_group * +sss_failover_group_new(struct sss_failover_ctx *fctx, + const char *name); + +/** + * @brief Enable DNS discovery within this group. + * + * @param group + * @return errno_t + */ +errno_t +sss_failover_group_setup_dns_discovery(struct sss_failover_group *group); + +/** + * @brief Add new server to the failover group. + * + * @param group + * @param server + * @return errno_t + */ +errno_t +sss_failover_group_add_server(struct sss_failover_group *group, + struct sss_failover_server *server); + +/** + * @brief Resolve servers within this group. + * + * It does not resolve servers to IP address, it resolves the DNS SRV record + * (if required) and combine SRV servers with those statically configured. + * + * @param mem_ctx + * @param ev + * @param fctx + * @param group + * @return struct tevent_req* + */ +struct tevent_req * +sss_failover_group_resolve_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + struct sss_failover_group *group); + +/** + * @brief Return list of servers within this group. + * + * @param mem_ctx + * @param req + * @param _servers + * @return errno_t + */ +errno_t +sss_failover_group_resolve_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + struct sss_failover_server ***_servers); + +#endif /* _FAILOVER_GROUP_H_ */ diff --git a/src/providers/failover/failover_refresh_candidates.c b/src/providers/failover/failover_refresh_candidates.c new file mode 100644 index 0000000000..d8a12bff96 --- /dev/null +++ b/src/providers/failover/failover_refresh_candidates.c @@ -0,0 +1,766 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include +#include +#include + +#include "config.h" +#include "util/util.h" +#include "providers/failover/failover.h" +#include "providers/failover/failover_group.h" +#include "providers/failover/failover_refresh_candidates.h" +#include "providers/failover/failover_server_resolve.h" +#include "util/sss_sockets.h" + +struct sss_failover_ping_state { + struct tevent_context *ev; + struct sss_failover_ctx *fctx; + struct sss_failover_server *server; + unsigned int timeout; + + struct timeval ping_start; +}; + +static void +sss_failover_ping_resolved(struct tevent_req *subreq); + +static void +sss_failover_ping_done(struct tevent_req *subreq); + +static struct tevent_req * +sss_failover_ping_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + struct sss_failover_server *server, + unsigned int timeout) +{ + struct sss_failover_ping_state *state; + struct tevent_req *subreq; + struct tevent_req *req; + errno_t ret; + + req = tevent_req_create(mem_ctx, &state, + struct sss_failover_ping_state); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "tevent_req_create() failed\n"); + return NULL; + } + + state->ev = ev; + state->fctx = fctx; + state->server = server; + state->timeout = timeout; + + subreq = sss_failover_server_resolve_send(state, ev, + state->fctx->resolver_ctx, + state->fctx->family_order, + state->server); + if (subreq == NULL) { + ret = ENOMEM; + goto done; + } + + tevent_req_set_callback(subreq, sss_failover_ping_resolved, req); + + ret = EOK; + +done: + if (ret != EOK) { + tevent_req_error(req, ret); + tevent_req_post(req, ev); + } + + return req; +} + +static void +sss_failover_ping_resolved(struct tevent_req *subreq) +{ + struct sss_failover_ping_state *state; + struct tevent_req *req; + errno_t ret; + + req = tevent_req_callback_data(subreq, struct tevent_req); + state = tevent_req_data(req, struct sss_failover_ping_state); + + ret = sss_failover_server_resolve_recv(subreq, NULL); + talloc_zfree(subreq); + if (ret != EOK) { + goto done; + } + + DEBUG(SSSDBG_TRACE_FUNC, "Pinging %s:%d (%s)\n", state->server->name, + state->server->port, state->server->addr->human); + + state->ping_start = tevent_timeval_current(); + + subreq = sssd_async_socket_init_send(state, state->ev, false, + state->server->addr->sockaddr, + state->server->addr->sockaddr_len, + state->timeout); + if (subreq == NULL) { + ret = ENOMEM; + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + goto done; + } + + tevent_req_set_callback(subreq, sss_failover_ping_done, req); + +done: + if (ret != EOK) { + tevent_req_error(req, ret); + return; + } +} + +static void sss_failover_ping_done(struct tevent_req *subreq) +{ + struct sss_failover_ping_state *state; + struct timeval ping_duration; + struct timeval ping_end; + struct tevent_req *req; + errno_t ret; + int fd; + + ping_end = tevent_timeval_current(); + req = tevent_req_callback_data(subreq, struct tevent_req); + state = tevent_req_data(req, struct sss_failover_ping_state); + + ret = sssd_async_socket_init_recv(subreq, &fd); + talloc_zfree(subreq); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, + "Server %s:%d is not reachable within %d seconds [%d]: %s\n", + state->server->name, state->server->port, state->timeout, ret, + sss_strerror(ret)); + goto done; + } + + close(fd); + + ping_duration = tevent_timeval_until(&state->ping_start, &ping_end); + DEBUG(SSSDBG_TRACE_FUNC, "Server %s:%d responded in %lds:%ldus\n", + state->server->name, state->server->port, ping_duration.tv_sec, + ping_duration.tv_usec); + + ret = EOK; + +done: + if (ret != EOK) { + tevent_req_error(req, ret); + return; + } + + tevent_req_done(req); +} + +static errno_t +sss_failover_ping_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + struct sss_failover_server **_server) +{ + struct sss_failover_ping_state *state; + + TEVENT_REQ_RETURN_ON_ERROR(req); + + state = tevent_req_data(req, struct sss_failover_ping_state); + *_server = talloc_reference(mem_ctx, state->server); + + return EOK; +} + +struct sss_failover_ping_parallel_state { + struct tevent_context *ev; + struct sss_failover_ctx *fctx; + struct sss_failover_server **servers; + unsigned int shortcut_time; + unsigned int max_servers; + + TALLOC_CTX *reqs_ctx; + struct tevent_timer *shortcut_te; + struct tevent_timer *batch_te; + unsigned int shortcut_attempts; + unsigned int active_requests; + unsigned int batch; + size_t next_server; + size_t count; + + struct sss_failover_server **candidates; + size_t candidates_index; +}; + +static void +sss_failover_ping_parallel_cleanup(struct tevent_req *req, + enum tevent_req_state req_state); + +static struct tevent_timer * +sss_failover_ping_parallel_shortcut_setup(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + unsigned int delay, + struct tevent_req *req); +static void +sss_failover_ping_parallel_shortcut(struct tevent_context *ev, + struct tevent_timer *te, + struct timeval tv, + void *data); + +static void +sss_failover_ping_parallel_batch(struct tevent_context *ev, + struct tevent_timer *te, + struct timeval tv, + void *data); + +static void +sss_failover_ping_parallel_done(struct tevent_req *subreq); + +static struct tevent_req * +sss_failover_ping_parallel_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + struct sss_failover_server **servers, + unsigned int max_servers, + unsigned int shortcut_time) +{ + struct sss_failover_ping_parallel_state *state; + struct timeval tv = {0, 0}; + struct tevent_req *req; + errno_t ret; + + req = tevent_req_create(mem_ctx, &state, + struct sss_failover_ping_parallel_state); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "tevent_req_create() failed\n"); + return NULL; + } + + state->ev = ev; + state->fctx = fctx; + state->servers = servers; + state->max_servers = max_servers; + state->shortcut_time = shortcut_time; + + state->batch = 1; + state->next_server = 0; + state->count = talloc_array_length(servers) - 1; + + state->reqs_ctx = talloc_new(state); + if (state->reqs_ctx == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + + state->candidates_index = 0; + state->candidates = talloc_zero_array(state, struct sss_failover_server *, + max_servers + 1); + if (state->candidates == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + + tevent_req_set_cleanup_fn(req, sss_failover_ping_parallel_cleanup); + + state->shortcut_attempts = 0; + state->shortcut_te = sss_failover_ping_parallel_shortcut_setup( + state, state->ev, state->shortcut_time, req); + + sss_failover_ping_parallel_batch(ev, NULL, tv, req); + + ret = EOK; + +done: + if (ret != EOK) { + tevent_req_error(req, ret); + tevent_req_post(req, ev); + } + + return req; +} + +static void +sss_failover_ping_parallel_cleanup(struct tevent_req *req, + enum tevent_req_state req_state) +{ + struct sss_failover_ping_parallel_state *state; + + state = tevent_req_data(req, struct sss_failover_ping_parallel_state); + + /* This request is done. Terminate any remaining timers and pings. */ + talloc_zfree(state->shortcut_te); + talloc_zfree(state->batch_te); + talloc_zfree(state->reqs_ctx); + state->active_requests = 0; +} + +static struct tevent_timer * +sss_failover_ping_parallel_shortcut_setup(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + unsigned int delay, + struct tevent_req *req) +{ + struct tevent_timer *te; + struct timeval tv; + + tv = tevent_timeval_current_ofs(delay, 0); + te = tevent_add_timer(ev, mem_ctx, tv, + sss_failover_ping_parallel_shortcut, req); + if (te == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Unable to schedule next shortcut!\n"); + } + + return te; +} + +static void +sss_failover_ping_parallel_shortcut(struct tevent_context *ev, + struct tevent_timer *te, + struct timeval tv, + void *data) +{ + struct sss_failover_ping_parallel_state *state; + struct tevent_req *req; + + req = talloc_get_type(data, struct tevent_req); + state = tevent_req_data(req, struct sss_failover_ping_parallel_state); + + state->shortcut_te = NULL; + state->shortcut_attempts++; + + /* There is at least one candidate server available. Return it. */ + if (state->candidates[0] != NULL) { + DEBUG(SSSDBG_TRACE_FUNC, + "Some candidates were already found in %d seconds, do not wait " + "for others\n", + state->shortcut_time * state->shortcut_attempts); + tevent_req_done(req); + return; + } + + state->shortcut_te = sss_failover_ping_parallel_shortcut_setup( + state, state->ev, state->shortcut_time, req); +} + +static void +sss_failover_ping_parallel_batch(struct tevent_context *ev, + struct tevent_timer *te, + struct timeval tv, + void *data) +{ + struct sss_failover_ping_parallel_state *state; + struct tevent_req *subreq; + struct tevent_req *req; + uint32_t delay; + size_t limit; + size_t i; + + req = talloc_get_type(data, struct tevent_req); + state = tevent_req_data(req, struct sss_failover_ping_parallel_state); + + state->batch_te = NULL; + + /* Issue three batches in total to avoid pinging too many servers if not + * necessary. We want to find @max_servers working servers. The first batch + * (@max_servers pings) is issued immediately and we will wait 400ms for it + * to finish. If we don't get a reply in time we issue next batch + * (@max_servers pings) and wait 200ms. If we still have no reply, we ping + * remaining servers. + */ + switch (state->batch) { + case 1: + case 2: + limit = MIN(state->count, state->max_servers + state->next_server); + delay = 400000 / state->batch; + break; + default: + limit = state->count; + delay = 0; + } + + DEBUG(SSSDBG_TRACE_FUNC, "Sending ping to servers from batch %d\n", + state->batch); + + for (i = state->next_server; i < limit; i++) { + DEBUG(SSSDBG_TRACE_ALL, "Batch %d: %s:%d\n", state->batch, + state->servers[i]->name, state->servers[i]->port); + } + + for (; state->next_server < limit; state->next_server++) { + subreq = sss_failover_ping_send(state->reqs_ctx, ev, state->fctx, + state->servers[state->next_server], + state->fctx->opts.ping_timeout); + if (subreq == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Unable to create new ping request\n"); + goto fail; + } + + state->active_requests++; + tevent_req_set_callback(subreq, sss_failover_ping_parallel_done, req); + } + + state->batch++; + if (delay > 0) { + tv = tevent_timeval_current_ofs(0, delay); + state->batch_te = tevent_add_timer(ev, state, tv, + sss_failover_ping_parallel_batch, req); + if (state->batch_te == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Unable to schedule next batch!\n"); + goto fail; + } + } + + return; + +fail: + if (state->active_requests == 0) { + tevent_req_error(req, ENOMEM); + if (state->batch == 1) { + tevent_req_post(req, ev); + } + } +} + +static void +sss_failover_ping_parallel_done(struct tevent_req *subreq) +{ + struct sss_failover_ping_parallel_state *state; + struct timeval tv = {0, 0}; + struct tevent_req *req; + errno_t ret; + + req = tevent_req_callback_data(subreq, struct tevent_req); + state = tevent_req_data(req, struct sss_failover_ping_parallel_state); + + ret = sss_failover_ping_recv(state->candidates, subreq, + &state->candidates[state->candidates_index]); + talloc_zfree(subreq); + state->active_requests--; + + if (ret == EOK) { + state->candidates_index++; + } + + /* Are we done? */ + if (state->candidates_index == state->max_servers) { + tevent_req_done(req); + return; + } + + if (state->active_requests == 0) { + /* There are still servers to try, don't wait for the timer. */ + if (state->next_server < state->count) { + talloc_zfree(state->batch_te); + sss_failover_ping_parallel_batch(state->ev, NULL, tv, req); + return; + } + + /* All servers were tried. */ + tevent_req_done(req); + return; + } + + /* Wait for another ping to finish. */ +} + +static errno_t +sss_failover_ping_parallel_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + size_t *_num_servers, + struct sss_failover_server ***_servers) +{ + struct sss_failover_ping_parallel_state *state; + + state = tevent_req_data(req, struct sss_failover_ping_parallel_state); + + TEVENT_REQ_RETURN_ON_ERROR(req); + + *_num_servers = state->candidates_index; + *_servers = talloc_steal(mem_ctx, state->candidates); + + return EOK; +} + +struct sss_failover_refresh_candidates_state { + struct tevent_context *ev; + struct sss_failover_ctx *fctx; + + unsigned int current_group; + struct sss_failover_group *group; + struct sss_failover_server **group_servers; +}; + +static errno_t +sss_failover_refresh_candidates_group_next(struct tevent_req *req); + +static void +sss_failover_refresh_candidates_group_resolved(struct tevent_req *subreq); + +static void +sss_failover_refresh_candidates_done(struct tevent_req *subreq); + +errno_t +sss_failover_refresh_candidates_recv(struct tevent_req *subreq); + +struct tevent_req * +sss_failover_refresh_candidates_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx) +{ + struct sss_failover_refresh_candidates_state *state; + struct tevent_req *req; + errno_t ret; + + req = tevent_req_create(mem_ctx, &state, + struct sss_failover_refresh_candidates_state); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "tevent_req_create() failed\n"); + return NULL; + } + + state->ev = ev; + state->fctx = fctx; + state->current_group = 0; + state->group = state->fctx->groups[0]; + + state->fctx->candidates->last_refresh_time = time(NULL); + state->fctx->candidates->refresh_req = req; + + DEBUG(SSSDBG_TRACE_FUNC, "Refreshing failover server candidates\n"); + + /* Stop the queue as we are refreshing the candidates list now. */ + DEBUG(SSSDBG_TRACE_FUNC, "Stopping candidates notification queue\n"); + tevent_queue_stop(fctx->candidates->notify_queue); + + ret = sss_failover_refresh_candidates_group_next(req); + + if (ret != EOK) { + tevent_req_error(req, ret); + tevent_req_post(req, ev); + } + + return req; +} + +static errno_t +sss_failover_refresh_candidates_group_next(struct tevent_req *req) +{ + struct sss_failover_refresh_candidates_state *state; + struct tevent_req *subreq; + + state = tevent_req_data(req, struct sss_failover_refresh_candidates_state); + state->group = state->fctx->groups[state->current_group]; + + if (state->group == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "No more groups to try\n"); + return ENOENT; + } + + DEBUG(SSSDBG_TRACE_FUNC, "Trying failover group: %s:%u\n", + state->group->name, state->group->slot); + + subreq = sss_failover_group_resolve_send(state, state->ev, state->fctx, + state->group); + if (subreq == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + return ENOMEM; + } + + tevent_req_set_callback(subreq, + sss_failover_refresh_candidates_group_resolved, + req); + + return EOK; +} + +static void +sss_failover_refresh_candidates_group_resolved(struct tevent_req *subreq) +{ + struct sss_failover_refresh_candidates_state *state; + struct tevent_req *req; + errno_t ret; + + req = tevent_req_callback_data(subreq, struct tevent_req); + state = tevent_req_data(req, struct sss_failover_refresh_candidates_state); + + talloc_zfree(state->group_servers); + ret = sss_failover_group_resolve_recv(state, subreq, &state->group_servers); + talloc_zfree(subreq); + if (ret != EOK) { + goto done; + } + + /* No servers found, try next group. */ + if (state->group_servers[0] == NULL) { + DEBUG(SSSDBG_TRACE_FUNC, "No servers found, trying next group\n"); + + state->current_group++; + ret = sss_failover_refresh_candidates_group_next(req); + if (ret != EOK) { + goto done; + } + + return; + } + + /* Servers found. Ping them in multiple batches. */ + subreq = sss_failover_ping_parallel_send(state, state->ev, state->fctx, + state->group_servers, + state->fctx->opts.max_candidates, + state->fctx->opts.min_candidates_lookup_time); + if (subreq == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + + tevent_req_set_callback(subreq, sss_failover_refresh_candidates_done, req); + +done: + if (ret != EOK) { + tevent_req_error(req, ret); + } +} + +static void +sss_failover_refresh_candidates_done(struct tevent_req *subreq) +{ + struct sss_failover_refresh_candidates_state *state; + struct sss_failover_server **candidates; + struct tevent_req *req; + size_t count; + errno_t ret; + int i; + + req = tevent_req_callback_data(subreq, struct tevent_req); + state = tevent_req_data(req, struct sss_failover_refresh_candidates_state); + + ret = sss_failover_ping_parallel_recv(state, subreq, &count, &candidates); + talloc_zfree(subreq); + if (ret != EOK) { + /* This is system error like ENOMEM. Not functional. */ + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to ping any server [%d]: %s\n", ret, + sss_strerror(ret)); + goto done; + } + + if (count == 0) { + DEBUG(SSSDBG_TRACE_FUNC, "No servers found, trying next group\n"); + + state->current_group++; + ret = sss_failover_refresh_candidates_group_next(req); + if (ret != EOK) { + goto done; + } + } + + DEBUG(SSSDBG_TRACE_FUNC, "Found %zu candidate servers in group %s:%u\n", + count, state->group->name, state->group->slot); + + if (DEBUG_IS_SET(SSSDBG_TRACE_ALL)) { + for (i = 0; candidates[i] != NULL; i++) { + DEBUG(SSSDBG_TRACE_ALL, "Found candidate server: %s:%u\n", + candidates[i]->name, candidates[i]->port); + } + } + + talloc_unlink(state->fctx->candidates, state->fctx->candidates->servers); + state->fctx->candidates->servers = talloc_steal(state->fctx->candidates, + candidates); + +done: + if (ret != EOK) { + tevent_req_error(req, ret); + return; + } + + tevent_req_done(req); +} + +errno_t +sss_failover_refresh_candidates_recv(struct tevent_req *req) +{ + struct sss_failover_refresh_candidates_state *state; + + state = tevent_req_data(req, struct sss_failover_refresh_candidates_state); + + state->fctx->candidates->last_refresh_time = time(NULL); + state->fctx->candidates->refresh_req = NULL; + + /* Notify listeners that refresh is finished. */ + DEBUG(SSSDBG_TRACE_FUNC, "Starting candidates notification queue\n"); + tevent_queue_start(state->fctx->candidates->notify_queue); + + TEVENT_REQ_RETURN_ON_ERROR(req); + + return EOK; +} + +bool +sss_failover_refresh_candidates_oob_can_run(struct sss_failover_ctx *fctx) +{ + time_t now; + + now = time(NULL); + + /* There is ongoing active request? */ + if (fctx->candidates->refresh_req != NULL) { + return false; + } + + /* Has enough time elapsed? */ + if (now <= fctx->candidates->last_refresh_time + + fctx->candidates->min_refresh_time) { + return false; + } + + return true; +} + +static void +sss_failover_refresh_candidates_oob_done(struct tevent_req *subreq); + +void +sss_failover_refresh_candidates_oob_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx) +{ + struct tevent_req *subreq; + + if (!sss_failover_refresh_candidates_oob_can_run(fctx)) { + DEBUG(SSSDBG_TRACE_FUNC, "Minimum refresh time has not elapsed yet or " + "there is an active refresh request.\n"); + return; + } + + subreq = sss_failover_refresh_candidates_send(mem_ctx, ev, fctx); + if (subreq == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + return; + } + + tevent_req_set_callback(subreq, sss_failover_refresh_candidates_oob_done, + NULL); +} + +static void +sss_failover_refresh_candidates_oob_done(struct tevent_req *subreq) +{ + sss_failover_refresh_candidates_recv(subreq); + talloc_free(subreq); +} diff --git a/src/providers/failover/failover_refresh_candidates.h b/src/providers/failover/failover_refresh_candidates.h new file mode 100644 index 0000000000..1bbdaa1abe --- /dev/null +++ b/src/providers/failover/failover_refresh_candidates.h @@ -0,0 +1,42 @@ +;/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef _FAILOVER_REFRESH_CANDIDATES_H_ +#define _FAILOVER_REFRESH_CANDIDATES_H_ + +#include + +#include "config.h" +#include "providers/failover/failover.h" + +struct tevent_req * +sss_failover_refresh_candidates_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx); + +errno_t +sss_failover_refresh_candidates_recv(struct tevent_req *req); + +bool +sss_failover_refresh_candidates_oob_can_run(struct sss_failover_ctx *fctx); + +void +sss_failover_refresh_candidates_oob_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx); + +#endif /* _FAILOVER_REFRESH_CANDIDATES_H_ */ diff --git a/src/providers/failover/failover_server.c b/src/providers/failover/failover_server.c new file mode 100644 index 0000000000..bc7a5e124c --- /dev/null +++ b/src/providers/failover/failover_server.c @@ -0,0 +1,353 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include +#include + +#include "config.h" +#include "providers/failover/failover_server.h" +#include "util/util.h" + +static struct sss_failover_server_address * +sss_failover_server_address_new(TALLOC_CTX *mem_ctx, + const uint16_t port, + const int family, + const time_t expire, + const uint8_t *addr_binary) +{ + struct sss_failover_server_address *out; + char buf[INET6_ADDRSTRLEN] = {0}; + const char *ntop_result; + struct sockaddr_in *in4; + struct sockaddr_in6 *in6; + errno_t ret; + + if (addr_binary == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Empty IP address!\n"); + return NULL; + } + + out = talloc_zero(mem_ctx, struct sss_failover_server_address); + if (out == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + + out->family = family; + out->expire = expire; + + switch (family) { + case AF_INET: + out->binary_len = sizeof(struct in_addr); + out->sockaddr_len = sizeof(struct sockaddr_in); + + in4 = talloc_zero(out, struct sockaddr_in); + if (in4 == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + + in4->sin_family = AF_INET; + in4->sin_port = (in_port_t)htons(port); + memcpy(&in4->sin_addr, addr_binary, out->binary_len); + out->sockaddr = (struct sockaddr *)in4; + break; + case AF_INET6: + out->binary_len = sizeof(struct in6_addr); + out->sockaddr_len = sizeof(struct sockaddr_in6); + + in6 = talloc_zero(out, struct sockaddr_in6); + if (in6 == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + + in6->sin6_family = AF_INET6; + in6->sin6_port = (in_port_t)htons(port); + memcpy(&in6->sin6_addr, addr_binary, out->binary_len); + out->sockaddr = (struct sockaddr *)in6; + break; + default: + DEBUG(SSSDBG_CRIT_FAILURE, "Unknown IP family: %d\n", out->family); + ret = EINVAL; + goto done; + } + + out->binary = talloc_memdup(out, addr_binary, out->binary_len); + if (out->binary == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + + ntop_result = inet_ntop(family, addr_binary, buf, INET6_ADDRSTRLEN); + if (ntop_result == NULL) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "Unable to convert IP address to string [%d]: %s\n", ret, + sss_strerror(ret)); + goto done; + } + + out->human = talloc_strdup(out, ntop_result); + if (out->human == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + + ret = EOK; + +done: + if (ret != EOK) { + talloc_free(out); + return NULL; + } + + return out; +} + +struct sss_failover_server * +sss_failover_server_new(TALLOC_CTX *mem_ctx, + const char *hostname, + const char *uri, + const uint16_t port, + const int priority, + const int weight) +{ + struct sss_failover_server *srv; + + if (hostname == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Hostname is not set!\n"); + return NULL; + } + + srv = talloc_zero(mem_ctx, struct sss_failover_server); + if (srv == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + return NULL; + } + + srv->name = talloc_strdup(srv, hostname); + if (srv->name == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + talloc_free(srv); + return NULL; + } + + srv->uri = talloc_strdup(srv, uri); + if (srv->uri == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + talloc_free(srv); + return NULL; + } + + srv->port = port; + srv->priority = priority; + srv->weight = weight; + + return srv; +} + +errno_t +sss_failover_server_set_address(struct sss_failover_server *srv, + int family, + int ttl, + const uint8_t *addr) +{ + struct sss_failover_server_address *new_addr; + time_t expire; + + if (family != AF_INET && family != AF_INET6) { + DEBUG(SSSDBG_CRIT_FAILURE, "Invalid family given: %d\n", family); + return EINVAL; + } + + if (addr == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Empty addr given\n"); + return EINVAL; + } + + expire = time(NULL) + ttl; + new_addr = sss_failover_server_address_new(srv, srv->port, family, expire, + addr); + if (new_addr == NULL) { + return ENOMEM; + } + + if (srv->addr != NULL) { + talloc_free(srv->addr); + } + + srv->addr = new_addr; + + DEBUG(SSSDBG_TRACE_FUNC, "Server %s resolved to %s, ttl %d\n", + srv->name, srv->addr->human, ttl); + + return EOK; +} + +struct sss_failover_server * +sss_failover_server_clone(TALLOC_CTX *mem_ctx, + const struct sss_failover_server *srv) +{ + struct sss_failover_server *out; + errno_t ret; + + if (srv == NULL || srv->name == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Empty failover server information!\n"); + return NULL; + } + + if (srv->addr != NULL + && (srv->addr->binary == NULL || srv->addr->human == NULL)) { + DEBUG(SSSDBG_CRIT_FAILURE, "Address is not complete!\n"); + return NULL; + } + + out = talloc_zero(mem_ctx, struct sss_failover_server); + if (out == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + return NULL; + } + + out->priority = srv->priority; + out->weight = srv->weight; + out->port = srv->port; + out->state = srv->state; + + out->name = talloc_strdup(out, srv->name); + if (out->name == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + + if (srv->uri != NULL) { + out->uri = talloc_strdup(out, srv->uri); + if (out->uri == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + } + + if (srv->addr == NULL) { + ret = EOK; + goto done; + } + + out->addr = sss_failover_server_address_new(out, srv->port, + srv->addr->family, + srv->addr->expire, + srv->addr->binary); + if (out->addr == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to create new server address!\n"); + ret = ENOMEM; + goto done; + } + + ret = EOK; + +done: + if (ret != EOK) { + talloc_free(out); + return NULL; + } + + return out; +} + +bool +sss_failover_server_maybe_working(struct sss_failover_server *srv) +{ + switch (srv->state) { + case SSS_FAILOVER_SERVER_STATE_OFFLINE: + case SSS_FAILOVER_SERVER_STATE_RESOLVER_ERROR: + return false; + case SSS_FAILOVER_SERVER_STATE_UNKNOWN: + case SSS_FAILOVER_SERVER_STATE_REACHABLE: + case SSS_FAILOVER_SERVER_STATE_WORKING: + return true; + default: + DEBUG(SSSDBG_CRIT_FAILURE, "Bug: unknown state [%d]!\n", srv->state); + return false; + } +} + +void +sss_failover_server_mark_unknown(struct sss_failover_server *srv) +{ + DEBUG(SSSDBG_TRACE_FUNC, + "Marking server [%s] as state unknown\n", srv->name); + srv->state = SSS_FAILOVER_SERVER_STATE_UNKNOWN; +} + +void +sss_failover_server_mark_reachable(struct sss_failover_server *srv) +{ + DEBUG(SSSDBG_TRACE_FUNC, + "Marking server [%s] as reachable\n", srv->name); + srv->state = SSS_FAILOVER_SERVER_STATE_REACHABLE; +} + +void +sss_failover_server_mark_working(struct sss_failover_server *srv) +{ + DEBUG(SSSDBG_TRACE_FUNC, + "Marking server [%s] as functional\n", srv->name); + srv->state = SSS_FAILOVER_SERVER_STATE_WORKING; +} + +void +sss_failover_server_mark_offline(struct sss_failover_server *srv) +{ + DEBUG(SSSDBG_TRACE_FUNC, "Marking server [%s] as offline\n", srv->name); + srv->state = SSS_FAILOVER_SERVER_STATE_OFFLINE; +} + +void +sss_failover_server_mark_resolver_error(struct sss_failover_server *srv) +{ + DEBUG(SSSDBG_TRACE_FUNC, + "Marking server [%s] as unable to resolve hostname\n", srv->name); + srv->state = SSS_FAILOVER_SERVER_STATE_RESOLVER_ERROR; +} + +bool +sss_failover_server_equal(const struct sss_failover_server *a, + const struct sss_failover_server *b) +{ + if (a->name == NULL || b->name == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Bug: server with no name?\n"); + return false; + } + + if (strcmp(a->name, b->name) != 0) { + return false; + } + + if (a->port != b->port) { + return false; + } + + return true; +} diff --git a/src/providers/failover/failover_server.h b/src/providers/failover/failover_server.h new file mode 100644 index 0000000000..98b9d4ddf7 --- /dev/null +++ b/src/providers/failover/failover_server.h @@ -0,0 +1,189 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef _FAILOVER_SERVER_H_ +#define _FAILOVER_SERVER_H_ + +#include + +#include "config.h" +#include "util/util.h" + +enum sss_failover_server_state { + /** + * @brief State of the server is unknown. + */ + SSS_FAILOVER_SERVER_STATE_UNKNOWN, + + /** + * @brief The server is responding but there is no active connection. + * + * E.g. ping succeeded, but full connection was not done. + */ + SSS_FAILOVER_SERVER_STATE_REACHABLE, + + /** + * @brief The server is fully functional. + */ + SSS_FAILOVER_SERVER_STATE_WORKING, + + /** + * @brief The server is currently offline. + */ + SSS_FAILOVER_SERVER_STATE_OFFLINE, + + /** + * @brief The server host name can not be resolved. + */ + SSS_FAILOVER_SERVER_STATE_RESOLVER_ERROR, +}; + +struct sss_failover_server_address { + /* AF_INET or AF_INET6 */ + int family; + + /* Human readable IP address. */ + char *human; + + /* IP address in binary format. */ + uint8_t *binary; + + /* Length of @binary */ + size_t binary_len; + + /* Generic sockaddr record. */ + struct sockaddr *sockaddr; + + /* @sockaddr length */ + socklen_t sockaddr_len; + + /* Time when the address will be expired and needs to be resolved again. */ + time_t expire; +}; + +struct sss_failover_server { + /* DNS hostname */ + char *name; + + /* Server URI */ + char *uri; + + /* Service port. */ + uint16_t port; + + /* DNS priority */ + int priority; + + /* DNS weight */ + int weight; + + /* Host IP address. */ + struct sss_failover_server_address *addr; + + /* Current state. */ + enum sss_failover_server_state state; + + /* Connection handle if state is CONNECTED. */ + void *connection; +}; + +/** + * @brief Create new failover server record. + * + * @return struct sss_failover_server * + */ +struct sss_failover_server * +sss_failover_server_new(TALLOC_CTX *mem_ctx, + const char *hostname, + const char *uri, + const uint16_t port, + const int priority, + const int weight); + +/** + * @brief Set resolved IP address of the server hostname. + * + * @param srv + * @param family + * @param ttl + * @param addr + * @return errno_t + */ +errno_t +sss_failover_server_set_address(struct sss_failover_server *srv, + int family, + int ttl, + const uint8_t *addr); + +/** + * @brief Clone failover server record. + * + * @param mem_ctx + * @param srv + * @return struct sss_failover_server * + */ +struct sss_failover_server * +sss_failover_server_clone(TALLOC_CTX *mem_ctx, + const struct sss_failover_server *srv); + + +/** + * @brief Return true if server state suggest that the server may work. + */ +bool +sss_failover_server_maybe_working(struct sss_failover_server *srv); + +/** + * @brief Mark server as state unknown + */ +void +sss_failover_server_mark_unknown(struct sss_failover_server *srv); + +/** + * @brief Mark server as reachable. + */ +void +sss_failover_server_mark_reachable(struct sss_failover_server *srv); + +/** + * @brief Mark server as fully functional and working. + */ +void +sss_failover_server_mark_working(struct sss_failover_server *srv); + +/** + * @brief Mark server as offline. + */ +void +sss_failover_server_mark_offline(struct sss_failover_server *srv); + +/** + * @brief Mark server as unable to resolve hostname. + */ +void +sss_failover_server_mark_resolver_error(struct sss_failover_server *srv); + +/** + * @brief Compare two servers and return true if they are equal. + * + * Note: this only compares name and port. + */ +bool +sss_failover_server_equal(const struct sss_failover_server *a, + const struct sss_failover_server *b); + +#endif /* _FAILOVER_SERVER_H_ */ diff --git a/src/providers/failover/failover_server_resolve.c b/src/providers/failover/failover_server_resolve.c new file mode 100644 index 0000000000..e3a8f998bc --- /dev/null +++ b/src/providers/failover/failover_server_resolve.c @@ -0,0 +1,178 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include +#include + +#include "config.h" +#include "providers/failover/failover_server.h" +#include "resolv/async_resolv.h" +#include "util/util.h" + +static bool +sss_failover_server_resolve_address_changed(struct sss_failover_server *server, + struct resolv_hostent *hostent) +{ + if (server->addr == NULL) { + /* this is the first resolution */ + return true; + } + + if (server->addr->family != hostent->family) { + /* new address has different family */ + return true; + } + + return memcmp(server->addr->binary, hostent->addr_list[0]->ipaddr, + server->addr->binary_len) == 0; +} + +struct sss_failover_server_resolve_state { + struct sss_failover_server *server; + bool changed; +}; + +static void +sss_failover_server_resolve_done(struct tevent_req *subreq); + +struct tevent_req * +sss_failover_server_resolve_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct resolv_ctx *resolv_ctx, + enum restrict_family family_order, + struct sss_failover_server *server) +{ + struct sss_failover_server_resolve_state *state; + struct tevent_req *subreq; + struct tevent_req *req; + errno_t ret; + time_t now; + + req = tevent_req_create(mem_ctx, &state, + struct sss_failover_server_resolve_state); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to create tevent request!\n"); + return NULL; + } + + state->changed = false; + state->server = talloc_reference(mem_ctx, server); + if (state->server == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + + + now = time(NULL); + if (state->server->addr != NULL && state->server->addr->expire > now) { + /* Address is still valid. */ + tevent_req_done(req); + tevent_req_post(req, ev); + return req; + } + + subreq = resolv_gethostbyname_send(state, ev, resolv_ctx, server->name, + family_order, default_host_dbs); + if (subreq == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to create tevent request!\n"); + ret = ENOMEM; + goto done; + } + + tevent_req_set_callback(subreq, sss_failover_server_resolve_done, req); + + ret = EOK; + +done: + if (ret != EOK) { + tevent_req_error(req, ret); + tevent_req_post(req, ev); + } + + return req; +} + +static void +sss_failover_server_resolve_done(struct tevent_req *subreq) +{ + struct sss_failover_server_resolve_state *state; + struct resolv_hostent *hostent; + struct tevent_req *req; + int resolv_status; + errno_t ret; + + req = tevent_req_callback_data(subreq, struct tevent_req); + state = tevent_req_data(req, struct sss_failover_server_resolve_state); + + ret = resolv_gethostbyname_recv(subreq, req, &resolv_status, NULL, + &hostent); + talloc_zfree(subreq); + if (ret != EOK) { + if (resolv_status == ARES_EFILE) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to resolve server '%s': %s [local hosts file]\n", + state->server->name, resolv_strerror(resolv_status)); + } else { + DEBUG(SSSDBG_CRIT_FAILURE, "Failed to resolve server '%s': %s\n", + state->server->name, resolv_strerror(resolv_status)); + } + + tevent_req_error(req, ret); + return; + } + + if (hostent->addr_list == NULL || hostent->addr_list[0] == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "No IP address found\n"); + tevent_req_error(req, ENOENT); + return; + } + + /* check if address has changed */ + state->changed = sss_failover_server_resolve_address_changed(state->server, + hostent); + + ret = sss_failover_server_set_address(state->server, hostent->family, + hostent->addr_list[0]->ttl, + hostent->addr_list[0]->ipaddr); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to set server address [%d]: %s\n", + ret, sss_strerror(ret)); + tevent_req_error(req, ret); + return; + } + + tevent_req_done(req); +} + +errno_t +sss_failover_server_resolve_recv(struct tevent_req *req, + bool *_changed) +{ + struct sss_failover_server_resolve_state *state; + + state = tevent_req_data(req, struct sss_failover_server_resolve_state); + + TEVENT_REQ_RETURN_ON_ERROR(req); + + if (_changed != NULL) { + *_changed = state->changed; + } + + return EOK; +} diff --git a/src/providers/failover/failover_server_resolve.h b/src/providers/failover/failover_server_resolve.h new file mode 100644 index 0000000000..54032915aa --- /dev/null +++ b/src/providers/failover/failover_server_resolve.h @@ -0,0 +1,64 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef _FAILOVER_SERVER_RESOLVE_H_ +#define _FAILOVER_SERVER_RESOLVE_H_ + +#include +#include + +#include "config.h" +#include "resolv/async_resolv.h" +#include "util/util.h" + +/** + * @brief Resolve server hostname into an IP address. + * + * When IP address is resolved, it calls @sss_failover_server_set_address to + * store the address in the @sss_failover_server record. Otherwise it keeps it + * intact. + * + * @param mem_ctx + * @param ev + * @param resolv_ctx + * @param family_order + * @param server + * @return struct tevent_req* + */ +struct tevent_req * +sss_failover_server_resolve_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct resolv_ctx *resolv_ctx, + enum restrict_family family_order, + struct sss_failover_server *server); + +/** + * @brief Receives the return code. + * + * If EOK, IP address has been stored inside the server record. @_changed is + * true if the IP address of the host has changed, false if it is still the + * same. + * + * @param req + * @param _changed + * @return errno_t + */ +errno_t +sss_failover_server_resolve_recv(struct tevent_req *req, + bool *_changed); + +#endif /* _FAILOVER_SERVER_RESOLVE_H_ */ diff --git a/src/providers/failover/failover_srv.c b/src/providers/failover/failover_srv.c new file mode 100644 index 0000000000..1f72ec574d --- /dev/null +++ b/src/providers/failover/failover_srv.c @@ -0,0 +1,178 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include +#include +#include + +#include "config.h" +#include "providers/failover/failover.h" +#include "providers/failover/failover_server.h" +#include "resolv/async_resolv.h" +#include "util/util.h" + +struct sss_failover_resolve_srv_state { + struct sss_failover_server **servers; + char *final_discovery_domain; + uint32_t ttl; +}; + +static void sss_failover_resolve_srv_done(struct tevent_req *subreq); + +struct tevent_req * +sss_failover_srv_resolve_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + const char *service, + const char *protocol, + const char * const * discovery_domains) +{ + struct sss_failover_resolve_srv_state *state; + struct tevent_req *subreq; + struct tevent_req *req; + const char **domains_dup; + size_t count; + size_t i; + errno_t ret; + + req = tevent_req_create(mem_ctx, &state, + struct sss_failover_resolve_srv_state); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "tevent_req_create() failed\n"); + return NULL; + } + + for (count = 0; discovery_domains[count] != NULL; count++); + domains_dup = talloc_zero_array(state, const char *, count + 1); + for (i = 0; discovery_domains[i] != NULL; i++) { + domains_dup[i] = talloc_strdup(domains_dup, discovery_domains[i]); + if (domains_dup[i] == NULL) { + ret = ENOMEM; + goto done; + } + } + + DEBUG(SSSDBG_TRACE_FUNC, "Discovering servers for %s/%s from DNS\n", + service, protocol); + + subreq = resolv_discover_srv_send(state, ev, fctx->resolver_ctx, service, + protocol, domains_dup); + if (subreq == NULL) { + ret = ENOMEM; + goto done; + } + + tevent_req_set_callback(subreq, sss_failover_resolve_srv_done, req); + + ret = EOK; + +done: + if (ret != EOK) { + tevent_req_error(req, ret); + tevent_req_post(req, ev); + } + + return req; +} + +static void sss_failover_resolve_srv_done(struct tevent_req *subreq) +{ + struct sss_failover_resolve_srv_state *state; + struct ares_srv_reply *reply_list; + struct ares_srv_reply *record; + struct tevent_req *req; + size_t num_servers; + errno_t ret; + int i; + + req = tevent_req_callback_data(subreq, struct tevent_req); + state = tevent_req_data(req, struct sss_failover_resolve_srv_state); + + ret = resolv_discover_srv_recv(state, subreq, &reply_list, &state->ttl, + &state->final_discovery_domain); + talloc_zfree(subreq); + if (ret != EOK) { + goto done; + } + + DEBUG(SSSDBG_TRACE_FUNC, "Got answer. Processing...\n"); + + /* sort and store the answer */ + ret = resolv_sort_srv_reply(&reply_list); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Could not sort the answers from DNS " + "[%d]: %s\n", ret, strerror(ret)); + goto done; + } + + num_servers = 0; + for (record = reply_list; record != NULL; record = record->next) { + num_servers++; + } + + DEBUG(SSSDBG_TRACE_FUNC, "Got %zu servers\n", num_servers); + + state->servers = talloc_zero_array(state, struct sss_failover_server *, + num_servers + 1); + if (state->servers == NULL) { + ret = ENOMEM; + goto done; + } + + for (record = reply_list, i = 0; + record != NULL; + record = record->next, i++) { + // TODO handle uri + state->servers[i] = sss_failover_server_new( + state->servers, record->host, "ldap://master.ldap.test", record->port, record->priority, + record->weight); + if (state->servers[i] == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + goto done; + } + } + + talloc_zfree(reply_list); + + ret = EOK; + +done: + if (ret != EOK) { + tevent_req_error(req, ret); + return; + } + + tevent_req_done(req); +} + +errno_t +sss_failover_srv_resolve_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + uint32_t *_ttl, + struct sss_failover_server ***_servers) +{ + struct sss_failover_resolve_srv_state *state; + + state = tevent_req_data(req, struct sss_failover_resolve_srv_state); + + TEVENT_REQ_RETURN_ON_ERROR(req); + + *_servers = talloc_steal(mem_ctx, state->servers); + + return EOK; +} diff --git a/src/providers/failover/failover_srv.h b/src/providers/failover/failover_srv.h new file mode 100644 index 0000000000..3b944a37aa --- /dev/null +++ b/src/providers/failover/failover_srv.h @@ -0,0 +1,66 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef _FAILOVER_SRV_H_ +#define _FAILOVER_SRV_H_ + +#include +#include + +#include "config.h" +#include "providers/failover/failover.h" +#include "providers/failover/failover_server.h" +#include "util/util.h" + +/** + * @brief Resolve DNS SRV record using selected discovery domains. + * + * If the first discovery domain yields no servers, we proceed with the next + * domain. + * + * @param mem_ctx + * @param ev + * @param fctx + * @param service + * @param protocol + * @param discovery_domains + * @return struct tevent_req* + */ +struct tevent_req * +sss_failover_srv_resolve_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + const char *service, + const char *protocol, + const char * const * discovery_domains); + +/** + * @brief Get TTL and discovered servers. + * + * @param mem_ctx + * @param req + * @param _ttl + * @param _servers + * @return errno_t + */ +errno_t +sss_failover_srv_resolve_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + uint32_t *_ttl, + struct sss_failover_server ***_servers); + +#endif /* _FAILOVER_SRV_H_ */ diff --git a/src/providers/failover/failover_transaction.c b/src/providers/failover/failover_transaction.c new file mode 100644 index 0000000000..e5f4a3bd74 --- /dev/null +++ b/src/providers/failover/failover_transaction.c @@ -0,0 +1,467 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include + +#include "config.h" +#include "providers/failover/failover_transaction.h" +#include "providers/failover/failover_vtable_op.h" +#include "util/util.h" + +errno_t +sss_failover_transaction_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + struct tevent_req *caller_req, + tevent_req_fn connected_callback) +{ + return sss_failover_transaction_ex_send(mem_ctx, ev, fctx, caller_req, + connected_callback, true, true, true, + SSS_FAILOVER_TRANSACTION_TLS_DEFAULT); +} + +struct sss_failover_transaction_connected_state { + struct sss_failover_ctx *fctx; +}; + +struct sss_failover_transaction_state { + struct tevent_context *ev; + struct sss_failover_ctx *fctx; + + bool reuse_connection; + bool authenticate_connection; + bool read_rootdse; + enum sss_failover_transaction_tls force_tls; + + /* Top level tevent request. Finished when this transaction is done. */ + struct tevent_req *caller_req; + void *caller_data; + size_t caller_data_size; + const char *caller_data_type; + + /* Connection request. Finished when we have a connection and + * connected_callback is fired. */ + struct tevent_req *connected_req; + tevent_req_fn connected_callback; + + /* Single transaction attempt. If successful, the main transaction request + * is finished. Otherwise, we try next server. */ + struct tevent_req *attempt_req; + + /* How many times was this transaction restarted. */ + unsigned int attempts; + + /* Connection information. */ + struct sss_failover_server *current_server; + time_t kinit_expiration_time; + void *connection; +}; + +static errno_t +sss_failover_transaction_restart(struct tevent_req *req); + +static errno_t +sss_failover_transaction_next(struct tevent_req *req); + +static errno_t +sss_failover_transaction_kinit(struct tevent_req *req); + +static void +sss_failover_transaction_kinit_done(struct tevent_req *subreq); + +static errno_t +sss_failover_transaction_connect(struct tevent_req *req); + +static void +sss_failover_transaction_connect_done(struct tevent_req *subreq); + +static void +sss_failover_transaction_attempt_done(struct tevent_req *attempt_req); + +static void +sss_failover_transaction_done(struct tevent_req *subreq); + +errno_t +sss_failover_transaction_ex_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + struct tevent_req *caller_req, + tevent_req_fn connected_callback, + bool reuse_connection, + bool authenticate_connection, + bool read_rootdse, + enum sss_failover_transaction_tls force_tls) +{ + struct sss_failover_transaction_state *state; + struct tevent_req *req; + errno_t ret; + + DEBUG(SSSDBG_TRACE_FUNC, + "Creating new failover transaction for service %s\n", fctx->name); + + req = tevent_req_create(mem_ctx, &state, struct sss_failover_transaction_state); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "tevent_req_create() failed\n"); + return ENOMEM; + } + + state->ev = ev; + state->fctx = fctx; + state->reuse_connection = reuse_connection; + state->authenticate_connection = authenticate_connection; + state->read_rootdse = read_rootdse; + + state->caller_req = caller_req; + state->caller_data = _tevent_req_data(caller_req); + state->caller_data_size = talloc_get_size(state->caller_data); + state->caller_data_type = talloc_get_name(state->caller_data); + state->connected_callback = connected_callback; + state->attempts = 0; + + tevent_req_set_callback(req, sss_failover_transaction_done, caller_req); + + ret = sss_failover_transaction_restart(req); + if (ret != EOK) { + /* We cannot get any working server. Just cancel this request. */ + talloc_free(req); + } + + return ret; +} + +static errno_t +sss_failover_transaction_restart(struct tevent_req *req) +{ + struct sss_failover_transaction_connected_state *connected_state; + struct sss_failover_transaction_state *state; + void *attempt_state; + errno_t ret; + + state = tevent_req_data(req, struct sss_failover_transaction_state); + state->attempts++; + + DEBUG(SSSDBG_TRACE_FUNC, "Transaction attempt %u\n", state->attempts); + + /* This request is what fires up the connected_callback - we have active + * connection to a server and the user can start querying it. */ + state->connected_req = tevent_req_create(state, + &connected_state, struct sss_failover_transaction_connected_state); + if (state->connected_req == NULL) { + ret = ENOMEM; + goto done; + } + connected_state->fctx = state->fctx; + + /* Create attempt req, this is used by the user as a replacement for + * caller_req. The user will seamlessly call + * tevent_req_done/error(attempt_req). */ + state->attempt_req = __tevent_req_create(state, &attempt_state, + state->caller_data_size, state->caller_data_type, + __func__, __location__); + if (state->attempt_req == NULL) { + ret = ENOMEM; + goto done; + } + + /* Switch the attempt_req state to caller_req state so it is used seamlessly + * by the user. This is quite a hack and the attempt_state must stay + * attached to attempt_req otherwise tevent_req_destructor will cause double + * free. We also cannot free req nor attempt_req to make sure all data is + * available to the caller_req recv function. This is not nice, but OK as + * there should not be many retry attempts and the memory is freed when + * caller_req is freed. */ + memcpy(attempt_state, state->caller_data, state->caller_data_size); + + tevent_req_set_callback(state->attempt_req, + sss_failover_transaction_attempt_done, req); + + tevent_req_set_callback(state->connected_req, state->connected_callback, + state->attempt_req); + + ret = sss_failover_transaction_next(req); + +done: + if (ret != EOK && state->attempts > 1) { + /* The failover transaction was restarted due to server error but we + * cannot retrieve any new server. Terminate the main request since we + * are already in an async loop. This in turn will finish the + * caller_req. */ + tevent_req_error(req, ret); + } + + return ret; +} + +static errno_t +sss_failover_transaction_next(struct tevent_req *req) +{ + struct sss_failover_transaction_state *state; + errno_t ret; + + state = tevent_req_data(req, struct sss_failover_transaction_state); + + /* Unlink current server to decrease refcount. */ + if (state->current_server != NULL) { + talloc_unlink(state, state->current_server); + state->current_server = NULL; + } + + DEBUG(SSSDBG_TRACE_FUNC, "Trying to find a working server\n"); + if (state->fctx->kinit_ctx != NULL && state->authenticate_connection) { + ret = sss_failover_transaction_kinit(req); + } else { + ret = sss_failover_transaction_connect(req); + } + + return ret; +} + +static errno_t +sss_failover_transaction_kinit(struct tevent_req *req) +{ + struct sss_failover_transaction_state *state; + struct tevent_req *subreq; + + state = tevent_req_data(req, struct sss_failover_transaction_state); + + DEBUG(SSSDBG_TRACE_FUNC, "Attempting to kinit\n"); + + subreq = sss_failover_vtable_op_kinit_send(state, state->ev, + state->fctx->kinit_ctx); + if (subreq == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory\n"); + return ENOMEM; + } + + tevent_req_set_callback(subreq, sss_failover_transaction_kinit_done, req); + return EOK; +} + +static void +sss_failover_transaction_kinit_done(struct tevent_req *subreq) +{ + struct sss_failover_transaction_state *state; + struct sss_failover_server *server; + struct tevent_req *req; + errno_t ret; + + req = tevent_req_callback_data(subreq, struct tevent_req); + state = tevent_req_data(req, struct sss_failover_transaction_state); + + ret = sss_failover_vtable_op_kinit_recv(state, subreq, &server, + &state->kinit_expiration_time); + talloc_zfree(subreq); + if (ret == ERR_NO_MORE_SERVERS) { + DEBUG(SSSDBG_OP_FAILURE, + "There are no more servers to try, cancelling operation\n"); + goto done; + } else if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, + "Error while attempting to kinit, cancelling operation [%d]: %s\n", + ret, sss_strerror(ret)); + goto done; + } + + DEBUG(SSSDBG_TRACE_FUNC, "kinit against KDC %s was successful\n", + server->name); + + /* We do not need this server anymore. */ + talloc_unlink(state, server); + + ret = sss_failover_transaction_connect(req); + +done: + if (ret != EOK) { + /* We cannot get TGT. Terminate main request. */ + tevent_req_error(req, ret); + return; + } +} + +static errno_t +sss_failover_transaction_connect(struct tevent_req *req) +{ + struct sss_failover_transaction_state *state; + struct tevent_req *subreq; + + state = tevent_req_data(req, struct sss_failover_transaction_state); + + DEBUG(SSSDBG_TRACE_FUNC, "Trying to establish connection\n"); + + subreq = sss_failover_vtable_op_connect_send(state, state->ev, state->fctx, + state->reuse_connection, + state->authenticate_connection, + state->read_rootdse, + state->force_tls, + state->kinit_expiration_time); + if (subreq == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory\n"); + return ENOMEM; + } + + tevent_req_set_callback(subreq, sss_failover_transaction_connect_done, req); + return EOK; +} + +static void +sss_failover_transaction_connect_done(struct tevent_req *subreq) +{ + struct sss_failover_transaction_state *state; + struct tevent_req *req; + void *connection; + errno_t ret; + + req = tevent_req_callback_data(subreq, struct tevent_req); + state = tevent_req_data(req, struct sss_failover_transaction_state); + + /* If successful, state->current_server is additional talloc_reference + * to an active, connected server. */ + ret = sss_failover_vtable_op_connect_recv(state, subreq, + &state->current_server, + &connection); + talloc_zfree(subreq); + if (ret == ERR_NO_MORE_SERVERS) { + DEBUG(SSSDBG_OP_FAILURE, + "There are no more servers to try, cancelling operation\n"); + goto done; + } else if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, + "Error while attempting to connect, cancelling operation [%d]: %s\n", + ret, sss_strerror(ret)); + goto done; + } + + DEBUG(SSSDBG_TRACE_FUNC, "Connected to %s, connection %p\n", + state->current_server->name, connection); + + sss_failover_set_active_server(state->fctx, state->current_server); + sss_failover_set_connection(state->fctx, connection); + + /* We are connected. Now continue with connected_callback. */ + tevent_req_done(state->connected_req); + +done: + if (ret != EOK) { + /* We cannot establish connection. Terminate main request. */ + tevent_req_error(req, ret); + return; + } +} + +/* Finish the main failover transaction request or try next server. */ +static void sss_failover_transaction_attempt_done(struct tevent_req *attempt_req) +{ + struct sss_failover_transaction_state *state; + struct tevent_req *req; + void *attempt_state; + enum tevent_req_state treq_state; + uint64_t treq_error; + + req = tevent_req_callback_data(attempt_req, struct tevent_req); + state = tevent_req_data(req, struct sss_failover_transaction_state); + attempt_state = _tevent_req_data(attempt_req); + + /* Copy the transaction_req state back to the caller_req state. We can not + * free the transaction state as there is no way to move possible new data + * to the caller state context. If the transaction is restarted we will + * allocate new transaction state, keeping this one hanging. It is OK as + * there is only finite number of possible restarts and eventually all the + * memory will be freed when the caller_req state is freed. */ + memcpy(state->caller_data, attempt_state, state->caller_data_size); + + if (tevent_req_is_error(attempt_req, &treq_state, &treq_error)) { + switch (treq_state) { + case TEVENT_REQ_USER_ERROR: + /* Try next server. */ + if (treq_error == ERR_SERVER_FAILURE) { + sss_failover_server_mark_offline(state->current_server); + sss_failover_transaction_restart(req); + return; + } + + tevent_req_error(req, treq_error); + return; + case TEVENT_REQ_TIMED_OUT: + tevent_req_error(req, ETIMEDOUT); + return; + case TEVENT_REQ_NO_MEMORY: + tevent_req_oom(req); + return; + default: + tevent_req_error(req, ERR_INTERNAL); + return; + } + } + + tevent_req_done(req); +} + +/* The failover transaction is done. Finish the caller request. */ +static void sss_failover_transaction_done(struct tevent_req *req) +{ + struct tevent_req *caller_req; + enum tevent_req_state req_state; + uint64_t req_error; + + caller_req = tevent_req_callback_data(req, struct tevent_req); + + /* Terminate the caller req. */ + if (tevent_req_is_error(req, &req_state, &req_error)) { + switch (req_state) { + case TEVENT_REQ_USER_ERROR: + DEBUG(SSSDBG_TRACE_FUNC, + "Failover transaction end up with error " + "[%" PRIu64 "]: %s\n", req_error, sss_strerror(req_error)); + tevent_req_error(caller_req, req_error); + return; + case TEVENT_REQ_TIMED_OUT: + DEBUG(SSSDBG_TRACE_FUNC, "Failover transaction timed out\n"); + tevent_req_error(caller_req, ETIMEDOUT); + return; + case TEVENT_REQ_NO_MEMORY: + tevent_req_oom(caller_req); + return; + default: + DEBUG(SSSDBG_TRACE_FUNC, "Bug: Unexpected state %d\n", req_state); + tevent_req_error(caller_req, ERR_INTERNAL); + return; + } + } + + DEBUG(SSSDBG_TRACE_FUNC, "Failover transaction was successful\n"); + tevent_req_done(caller_req); +} + +/* Return connection. This is only called if we have a successful connection. */ +void * +_sss_failover_transaction_connected_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req) +{ + struct sss_failover_transaction_connected_state *state; + void *connection; + + state = tevent_req_data(req, + struct sss_failover_transaction_connected_state); + + connection = sss_failover_get_connection(mem_ctx, state->fctx); + if (connection == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Bug: connection should not be NULL!\n"); + } + + return connection; +} diff --git a/src/providers/failover/failover_transaction.h b/src/providers/failover/failover_transaction.h new file mode 100644 index 0000000000..7d242c8400 --- /dev/null +++ b/src/providers/failover/failover_transaction.h @@ -0,0 +1,93 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +/** + * The failover transaction code is responsible for choosing and connecting to a + * server and retrying the whole operation if the server stops working in the + * middle of the request. + * + * The operation is wrapped by @sss_failover_transaction_send and it should make + * sure to fetch all required data from the server before writing them to the + * sysdb. If the operation fails due to the server failure, the operation tevent + * request must fail with ERR_SERVER_FAILURE to indicate the failure to the + * failover transaction code. In this case, the failover mechanism marks the + * server as offline, picks the next available server and restarts the whole + * operation. Neither the caller nor the operation has to deal with any failover + * mechanics. + * + * The result of the operation can be received by + * @sss_failover_transaction_recv. + */ + +#ifndef _FAILOVER_TRANSACTION_H_ +#define _FAILOVER_TRANSACTION_H_ + +#include +#include + +#include "config.h" +#include "resolv/async_resolv.h" +#include "util/util.h" +#include "util/typeof.h" + +struct sss_failover_ctx; + +enum sss_failover_transaction_tls { + SSS_FAILOVER_TRANSACTION_TLS_DEFAULT, + SSS_FAILOVER_TRANSACTION_TLS_ON, + SSS_FAILOVER_TRANSACTION_TLS_OFF +}; + +errno_t +sss_failover_transaction_ex_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + struct tevent_req *caller_req, + tevent_req_fn connected_callback, + bool reuse_connection, + bool authenticate_connection, + bool read_rootdse, + enum sss_failover_transaction_tls force_tls); + +errno_t +sss_failover_transaction_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + struct tevent_req *caller_req, + tevent_req_fn connected_callback); + +/** + * @brief Submit a failover transaction. + * + * The failover code will pick a working server and submit a working connection + * to the underlying @req_send tevent request, passing @input_data along. + * + * If the receive @req_recv function returns ERR_SERVER_FAILURE, the transaction + * is repeated with another server as long as there is a server available. The + * transaction is cancelled if there are no more servers to try. + * + * The callback and data types are checked during compilation. + */ + +void * +_sss_failover_transaction_connected_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req); + +#define sss_failover_transaction_connected_recv(mem_ctx, req, type) \ + talloc_get_type_abort(_sss_failover_transaction_connected_recv((mem_ctx), (req)), type) + +#endif /* _FAILOVER_TRANSACTION_H_ */ diff --git a/src/providers/failover/failover_vtable.c b/src/providers/failover/failover_vtable.c new file mode 100644 index 0000000000..52c5067cb1 --- /dev/null +++ b/src/providers/failover/failover_vtable.c @@ -0,0 +1,46 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include + +#include "config.h" +#include "providers/failover/failover_vtable.h" +#include "providers/failover/failover.h" +#include "util/util.h" + +void +sss_failover_vtable_set_connect(struct sss_failover_ctx *fctx, + sss_failover_vtable_connect_send_t send_fn, + sss_failover_vtable_connect_recv_t recv_fn, + void *data) +{ + fctx->vtable->connect.send = send_fn; + fctx->vtable->connect.recv = recv_fn; + fctx->vtable->connect.data = data; +} + +void +sss_failover_vtable_set_kinit(struct sss_failover_ctx *fctx, + sss_failover_vtable_kinit_send_t send_fn, + sss_failover_vtable_kinit_recv_t recv_fn, + void *data) +{ + fctx->vtable->kinit.send = send_fn; + fctx->vtable->kinit.recv = recv_fn; + fctx->vtable->kinit.data = data; +} diff --git a/src/providers/failover/failover_vtable.h b/src/providers/failover/failover_vtable.h new file mode 100644 index 0000000000..cd7733e5be --- /dev/null +++ b/src/providers/failover/failover_vtable.h @@ -0,0 +1,97 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef _FAILOVER_VTABLE_H_ +#define _FAILOVER_VTABLE_H_ + +#include +#include + +#include "config.h" +#include "resolv/async_resolv.h" +#include "providers/failover/failover_server.h" +#include "util/util.h" + +struct sss_failover_ctx; +enum sss_failover_transaction_tls; + +struct sss_failover_vtable_kinit_output_data { + time_t expiration_time; +}; + +typedef struct tevent_req * +(*sss_failover_vtable_kinit_send_t)(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + struct sss_failover_server *server, + bool addr_changed, + void *pvt); + +typedef errno_t +(*sss_failover_vtable_kinit_recv_t)(TALLOC_CTX *mem_ctx, + struct tevent_req *, + time_t *_expiration_time); + +struct sss_failover_vtable_kinit { + sss_failover_vtable_kinit_send_t send; + sss_failover_vtable_kinit_recv_t recv; + void *data; +}; + +typedef struct tevent_req * +(*sss_failover_vtable_connect_send_t)(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + struct sss_failover_server *server, + bool addr_changed, + bool reuse_connection, + bool authenticate_connection, + bool read_rootdse, + enum sss_failover_transaction_tls force_tls, + time_t kinit_expiration_time, + void *pvt); + +typedef errno_t +(*sss_failover_vtable_connect_recv_t)(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + void **_connection); + + +struct sss_failover_vtable_connect { + sss_failover_vtable_connect_send_t send; + sss_failover_vtable_connect_recv_t recv; + void *data; +}; + +struct sss_failover_vtable { + struct sss_failover_vtable_kinit kinit; + struct sss_failover_vtable_connect connect; +}; + +void +sss_failover_vtable_set_connect(struct sss_failover_ctx *fctx, + sss_failover_vtable_connect_send_t send_fn, + sss_failover_vtable_connect_recv_t recv_fn, + void *data); + +void +sss_failover_vtable_set_kinit(struct sss_failover_ctx *fctx, + sss_failover_vtable_kinit_send_t send_fn, + sss_failover_vtable_kinit_recv_t recv_fn, + void *data); + +#endif /* _FAILOVER_VTABLE_H_ */ diff --git a/src/providers/failover/failover_vtable_op.c b/src/providers/failover/failover_vtable_op.c new file mode 100644 index 0000000000..735c9a6767 --- /dev/null +++ b/src/providers/failover/failover_vtable_op.c @@ -0,0 +1,596 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include + +#include "config.h" +#include "providers/failover/failover.h" +#include "providers/failover/failover_transaction.h" +#include "providers/failover/failover_server.h" +#include "providers/failover/failover_server_resolve.h" +#include "providers/failover/failover_refresh_candidates.h" +#include "providers/failover/failover_vtable_op.h" +#include "util/util.h" + +static struct sss_failover_server * +sss_failover_vtable_op_pick_server(TALLOC_CTX *mem_ctx, + struct sss_failover_ctx *fctx) +{ + struct sss_failover_server *server; + size_t index; + size_t start; + size_t count; + + /* Total count of elements. */ + count = talloc_array_length(fctx->candidates->servers) - 1; + + start = sss_rand() % count; + for (size_t i = 0; i < count; i++) { + index = (start + i) % count; + + server = fctx->candidates->servers[index]; + + /* This slot is empty. Continue. */ + if (server == NULL) { + continue; + } + + if (sss_failover_server_maybe_working(server)) { + return talloc_reference(mem_ctx, server); + } + } + + /* We iterated over all candidates and none is working. */ + return NULL; +} + +enum sss_failover_vtable_op { + /* Perform kinit against given KDC. */ + SSS_FAILOVER_VTABLE_OP_KINIT, + + /* Connect to the server. */ + SSS_FAILOVER_VTABLE_OP_CONNECT, +}; + +/** + * @brief Issue vtable operation against specific server. + * + * The operation should check the @server state and shortcut if possible (for + * example if the server is already connected and working). @addr_changed is + * true if the server hostname resolved to different address then what is stored + * (it was previously unresolved, or the DNS record has changed). The operation + * should take this information into consideration (e.g. reconnect to the server + * with new address). + * + * The server state can be unknown, reachable or working. The server address + * is guaranteed to be resolved. + */ +typedef struct tevent_req * +(*sss_failover_vtable_op_send_t)(TALLOC_CTX *mem_ctx, + struct sss_failover_ctx *fctx, + struct sss_failover_server *server, + bool addr_changed); + +/** + * @brief Receive operation result and point to its private data. + * + * The private data is then stored on the server structure by caller. + */ +typedef errno_t +(*sss_failover_vtable_op_recv_t)(TALLOC_CTX *mem_ctx, + struct tevent_req *, + void **_op_private_data); + +struct sss_failover_vtable_op_args { + union { + struct { + bool reuse_connection; + bool authenticate_connection; + bool read_rootdse; + enum sss_failover_transaction_tls force_tls; + time_t expiration_time; + } connect; + } input; + + union { + struct { + time_t expiration_time; + } kinit; + + struct { + void *connection; + } connect; + } output; +}; + +struct sss_failover_vtable_op_state { + struct tevent_context *ev; + struct sss_failover_ctx *fctx; + enum sss_failover_vtable_op operation; + struct sss_failover_vtable_op_args *args; + + struct sss_failover_server *current_server; + bool candidates_refreshed; +}; + +static void +sss_failover_vtable_op_trigger(struct tevent_req *req, + void *pvt); + +static errno_t +sss_failover_vtable_op_server_next(struct tevent_req *req); + +static errno_t +sss_failover_vtable_op_refresh_candidates(struct tevent_req *req); + +static void +sss_failover_vtable_op_refresh_candidates_done(struct tevent_req *subreq); + +static void +sss_failover_vtable_op_server_resolved(struct tevent_req *subreq); + +static struct tevent_req * +sss_failover_vtable_op_subreq_send(struct sss_failover_vtable_op_state *state, + bool addr_changed); + +static errno_t +sss_failover_vtable_op_subreq_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *subreq); + +static void +sss_failover_vtable_op_done(struct tevent_req *subreq); + +static struct tevent_req * +sss_failover_vtable_op_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + enum sss_failover_vtable_op operation, + struct sss_failover_vtable_op_args *args) +{ + struct sss_failover_vtable_op_state *state; + struct tevent_req *req; + errno_t ret; + bool bret; + + req = tevent_req_create(mem_ctx, &state, + struct sss_failover_vtable_op_state); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "tevent_req_create() failed\n"); + + /* Free args to simplify logic in the caller. */ + talloc_free(args); + return NULL; + } + + state->ev = ev; + state->fctx = fctx; + state->operation = operation; + state->args = talloc_steal(state, args); + + switch (state->operation) { + case SSS_FAILOVER_VTABLE_OP_KINIT: + case SSS_FAILOVER_VTABLE_OP_CONNECT: + /* Correct operation. */ + break; + default: + DEBUG(SSSDBG_CRIT_FAILURE, "Invalid operation: [%d]\n", state->operation); + ret = EINVAL; + goto done; + } + + /* Queuing the requests ensures that there is only one request that does + * actual server selection and resolution. All subsequent requests will just + * shortcut and pick the last selected server, if it is still working. */ + bret = tevent_queue_add(fctx->vtable_op_queue, fctx->ev, req, + sss_failover_vtable_op_trigger, NULL); + if (!bret) { + DEBUG(SSSDBG_OP_FAILURE, "Unable to add request to tevent queue\n"); + ret = ENOMEM; + goto done; + } + + ret = EOK; + +done: + if (ret != EOK) { + tevent_req_error(req, ret); + tevent_req_post(req, ev); + } + + return req; +} + +static void +sss_failover_vtable_op_trigger(struct tevent_req *req, + void *pvt) +{ + errno_t ret; + + ret = sss_failover_vtable_op_server_next(req); + if (ret != EOK) { + tevent_req_error(req, ret); + } +} + +static errno_t +sss_failover_vtable_op_server_next(struct tevent_req *req) +{ + struct sss_failover_vtable_op_state *state; + struct tevent_req *subreq; + + state = tevent_req_data(req, struct sss_failover_vtable_op_state); + + if (state->current_server == NULL) { + /* Select first server to try.*/ + if (state->fctx->active_server != NULL + && sss_failover_server_maybe_working(state->fctx->active_server)) { + /* Try active server first. */ + state->current_server = state->fctx->active_server; + DEBUG(SSSDBG_TRACE_FUNC, "Trying current active server: %s\n", + state->current_server->name); + } else { + /* Pick a first server from candidates. */ + state->current_server = sss_failover_vtable_op_pick_server(state, state->fctx); + if (state->current_server == NULL) { + /* No candidates are available, schedule a refresh. */ + return sss_failover_vtable_op_refresh_candidates(req); + } + + DEBUG(SSSDBG_TRACE_FUNC, "Trying candidate server: %s\n", + state->current_server->name); + } + } else { + /* We already tried this server and it is not working. Submit an out of + * band request of server candidates and try the next available + * server. */ + + DEBUG(SSSDBG_TRACE_FUNC, "Server %s does not work\n", + state->current_server->name); + + DEBUG(SSSDBG_TRACE_FUNC, "Issuing out of band refresh of candidates\n"); + + if (sss_failover_refresh_candidates_oob_can_run(state->fctx)) { + sss_failover_refresh_candidates_oob_send(state->fctx, state->ev, + state->fctx); + } + + state->current_server = sss_failover_vtable_op_pick_server(state, state->fctx); + if (state->current_server == NULL) { + /* No candidates are available. Wait for new ones. */ + return sss_failover_vtable_op_refresh_candidates(req); + } + + DEBUG(SSSDBG_TRACE_FUNC, "Trying next candidate server: %s\n", + state->current_server->name); + } + + /* TODO shortcut if already connected */ + + /* First resolve the hostname. */ + DEBUG(SSSDBG_TRACE_FUNC, "Resolving hostname of %s\n", + state->current_server->name); + + subreq = sss_failover_server_resolve_send(state, state->ev, + state->fctx->resolver_ctx, + state->fctx->family_order, + state->current_server); + if (subreq == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + return ENOMEM; + } + + tevent_req_set_callback(subreq, sss_failover_vtable_op_server_resolved, + req); + + return EOK; +} + +static errno_t +sss_failover_vtable_op_refresh_candidates(struct tevent_req *req) +{ + struct sss_failover_vtable_op_state *state; + struct tevent_queue *queue; + struct tevent_req *subreq; + + state = tevent_req_data(req, struct sss_failover_vtable_op_state); + queue = state->fctx->candidates->notify_queue; + + if (state->candidates_refreshed) { + /* We already refreshed the candidates. */ + DEBUG(SSSDBG_TRACE_FUNC, "Refresh did not find any working server\n"); + return ERR_NO_MORE_SERVERS; + } + + DEBUG(SSSDBG_TRACE_FUNC, + "No more candidate servers are available, wait for a refresh\n"); + + state->candidates_refreshed = true; + + /* Issue refresh request if there is none. */ + if (sss_failover_refresh_candidates_oob_can_run(state->fctx)) { + sss_failover_refresh_candidates_oob_send(state->fctx, state->ev, + state->fctx); + } + + /* Register for notification. */ + subreq = tevent_queue_wait_send(state, state->ev, queue); + if (subreq == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + return ENOMEM; + } + + tevent_req_set_callback(subreq, + sss_failover_vtable_op_refresh_candidates_done, + req); + + return EOK; +} + +static void +sss_failover_vtable_op_refresh_candidates_done(struct tevent_req *subreq) +{ + struct tevent_req *req; + errno_t ret; + + req = tevent_req_callback_data(subreq, struct tevent_req); + + ret = sss_failover_vtable_op_server_next(req); + if (ret != EOK) { + tevent_req_error(req, ret); + } +} + +static void +sss_failover_vtable_op_server_resolved(struct tevent_req *subreq) +{ + struct sss_failover_vtable_op_state *state; + struct tevent_req *req; + bool addr_changed; + errno_t ret; + + req = tevent_req_callback_data(subreq, struct tevent_req); + state = tevent_req_data(req, struct sss_failover_vtable_op_state); + + ret = sss_failover_server_resolve_recv(subreq, &addr_changed); + talloc_zfree(subreq); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, + "Unable to resolve server hostname %s [%d]: %s\n", + state->current_server->name, ret, sss_strerror(ret)); + sss_failover_server_mark_resolver_error(state->current_server); + ret = sss_failover_vtable_op_server_next(req); + goto done; + } + + /* Trigger the operation. */ + DEBUG(SSSDBG_TRACE_FUNC, "Name resolved, starting vtable operation\n"); + + subreq = sss_failover_vtable_op_subreq_send(state, addr_changed); + if (subreq == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + + tevent_req_set_callback(subreq, sss_failover_vtable_op_done, req); + +done: + if (ret != EOK) { + tevent_req_error(req, ret); + return; + } +} + +static struct tevent_req * +sss_failover_vtable_op_subreq_send(struct sss_failover_vtable_op_state *state, + bool addr_changed) +{ + switch (state->operation) { + case SSS_FAILOVER_VTABLE_OP_KINIT: + return state->fctx->vtable->kinit.send( + state, state->ev, state->fctx, state->current_server, addr_changed, + state->fctx->vtable->kinit.data); + case SSS_FAILOVER_VTABLE_OP_CONNECT: + return state->fctx->vtable->connect.send( + state, state->ev, state->fctx, state->current_server, addr_changed, + state->args->input.connect.reuse_connection, + state->args->input.connect.authenticate_connection, + state->args->input.connect.read_rootdse, + state->args->input.connect.force_tls, + state->args->input.connect.expiration_time, + state->fctx->vtable->connect.data); + } + + DEBUG(SSSDBG_CRIT_FAILURE, "Bug: Unknown operation\n"); + return NULL; +} + +static errno_t +sss_failover_vtable_op_subreq_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *subreq) +{ + struct sss_failover_vtable_op_state *state; + struct tevent_req *req; + + req = tevent_req_callback_data(subreq, struct tevent_req); + state = tevent_req_data(req, struct sss_failover_vtable_op_state); + + switch (state->operation) { + case SSS_FAILOVER_VTABLE_OP_KINIT: + return state->fctx->vtable->kinit.recv(state, subreq, + &state->args->output.kinit.expiration_time); + case SSS_FAILOVER_VTABLE_OP_CONNECT: + return state->fctx->vtable->connect.recv(state, subreq, + &state->args->output.connect.connection); + } + + DEBUG(SSSDBG_CRIT_FAILURE, "Bug: Unknown operation\n"); + return ENOTSUP; +} + +static void sss_failover_vtable_op_done(struct tevent_req *subreq) +{ + struct sss_failover_vtable_op_state *state; + struct tevent_req *req; + errno_t ret; + + req = tevent_req_callback_data(subreq, struct tevent_req); + state = tevent_req_data(req, struct sss_failover_vtable_op_state); + + ret = sss_failover_vtable_op_subreq_recv(state, subreq); + talloc_zfree(subreq); + + switch (ret) { + case EOK: + /* The operation was successful. */ + sss_failover_server_mark_working(state->current_server); + + /* Remember this server. */ + talloc_unlink(state->fctx, state->fctx->active_server); + state->fctx->active_server = talloc_reference(state->fctx, + state->current_server); + break; + case ENOMEM: + /* There is no reason to retry if we our out of memory. */ + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + goto done; + default: + /* Server is not working. */ + sss_failover_server_mark_offline(state->current_server); + ret = sss_failover_vtable_op_server_next(req); + if (ret == EOK) { + return; + } + } + +done: + if (ret != EOK) { + tevent_req_error(req, ret); + return; + } + + tevent_req_done(req); +} + +static errno_t +sss_failover_vtable_op_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + struct sss_failover_server **_server, + struct sss_failover_vtable_op_args **_args) +{ + struct sss_failover_vtable_op_state *state = NULL; + state = tevent_req_data(req, struct sss_failover_vtable_op_state); + + TEVENT_REQ_RETURN_ON_ERROR(req); + + if (_server != NULL) { + *_server = talloc_reference(mem_ctx, state->current_server); + } + + if (_args != NULL) { + *_args = talloc_steal(mem_ctx, state->args); + } + + return EOK; +} + +struct tevent_req * +sss_failover_vtable_op_kinit_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx) +{ + struct sss_failover_vtable_op_args *args; + + args = talloc_zero(NULL, struct sss_failover_vtable_op_args); + if (args == NULL) { + return NULL; + } + + return sss_failover_vtable_op_send(mem_ctx, ev, fctx, + SSS_FAILOVER_VTABLE_OP_KINIT, args); +} + +errno_t +sss_failover_vtable_op_kinit_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + struct sss_failover_server **_server, + time_t *_expiration_time) +{ + struct sss_failover_vtable_op_args *args; + errno_t ret; + + ret = sss_failover_vtable_op_recv(mem_ctx, req, _server, &args); + if (ret != EOK) { + return ret; + } + + if (_expiration_time != NULL) { + *_expiration_time = args->output.kinit.expiration_time; + } + + talloc_free(args); + return EOK; +} + +struct tevent_req * +sss_failover_vtable_op_connect_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + bool reuse_connection, + bool authenticate_connection, + bool read_rootdse, + enum sss_failover_transaction_tls force_tls, + time_t kinit_expiration_time) +{ + struct sss_failover_vtable_op_args *args; + + args = talloc_zero(NULL, struct sss_failover_vtable_op_args); + if (args == NULL) { + return NULL; + } + + args->input.connect.reuse_connection = reuse_connection; + args->input.connect.authenticate_connection = authenticate_connection; + args->input.connect.read_rootdse = read_rootdse; + args->input.connect.force_tls = force_tls; + args->input.connect.expiration_time = kinit_expiration_time; + return sss_failover_vtable_op_send(mem_ctx, ev, fctx, + SSS_FAILOVER_VTABLE_OP_CONNECT, args); +} + +errno_t +sss_failover_vtable_op_connect_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + struct sss_failover_server **_server, + void **_connection) +{ + struct sss_failover_vtable_op_args *args; + errno_t ret; + + ret = sss_failover_vtable_op_recv(mem_ctx, req, _server, &args); + if (ret != EOK) { + return ret; + } + + if (_connection != NULL) { + *_connection = talloc_steal(mem_ctx, args->output.connect.connection); + } + + talloc_free(args); + return EOK; +} diff --git a/src/providers/failover/failover_vtable_op.h b/src/providers/failover/failover_vtable_op.h new file mode 100644 index 0000000000..d244e62a72 --- /dev/null +++ b/src/providers/failover/failover_vtable_op.h @@ -0,0 +1,131 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef _FAILOVER_VTABLE_OP_H_ +#define _FAILOVER_VTABLE_OP_H_ + +#include +#include + +#include "config.h" +#include "providers/failover/failover.h" +#include "providers/failover/failover_server.h" +#include "util/util.h" + +/** + * @defgroup Failover vtable operations. + * + * The purpose of sss_failover_vtable_op_* requests is to find a working server + * on which the operation succeeds. + * + * - If there is already working and active server, use it. + * - Otherwise find first available server, resolve its hostname and use it. + * - If the operation succeeds, mark the server as working and store operation + * data. + * - If the operation fails, mark the server as not working and try next server. + * + * Note that this request does not decide if the operation should be started or + * not (e.g. if the server is already connected or not). To simplify the logic, + * this is the responsibility of the operation it self (e.g. check if the server + * is already connected in the @send_fn and then shortcut, otherwise try to + * establish connection). + * + * The requests are serialized in @fctx->vtable_op_queue to ensure that we + * always talk to a single server at the same time. + * + * @{ + */ + +/** + * @brief Select a KDC and attempt to kinit with the host credentials. + * + * @param mem_ctx + * @param ev + * @param fctx + * @return struct tevent_req * + */ +struct tevent_req * +sss_failover_vtable_op_kinit_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx); + +/** + * @brief Receive result of the operation. + * + * If @_server is not NULL and EOK is returned, it contains the server that was + * successfully used to finish the operation. The server reference count is + * increased and linked to @mem_ctx. + * + * @param mem_ctx + * @param req + * @param _server + * @param _expiration_time Host TGT expiration time. + * @return errno_t + */ +errno_t +sss_failover_vtable_op_kinit_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + struct sss_failover_server **_server, + time_t *_expiration_time); + +/** + * @brief Select a server and attempt to establish a working connection. + * + * @param mem_ctx + * @param ev + * @param fctx + * @param reuse_connection + * @param authenticate_connection + * @param read_rootdse + * @param force_tls + * @param kinit_expiration_time + * @return struct tevent_req * + */ +struct tevent_req * +sss_failover_vtable_op_connect_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + bool reuse_connection, + bool authenticate_connection, + bool read_rootdse, + enum sss_failover_transaction_tls force_tls, + time_t kinit_expiration_time); + +/** + * @brief Receive result of the operation. + * + * If @_server is not NULL and EOK is returned, it contains the server that was + * successfully used to finish the operation. The server reference count is + * increased and linked to @mem_ctx. + * + * @param mem_ctx + * @param req + * @param _server + * @param _connection Established connection data. + * @return errno_t + */ +errno_t +sss_failover_vtable_op_connect_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + struct sss_failover_server **_server, + void **_connection); + +/** + * @} + */ + +#endif /* _FAILOVER_VTABLE_OP_H_ */ diff --git a/src/providers/failover/ldap/failover_ldap.h b/src/providers/failover/ldap/failover_ldap.h new file mode 100644 index 0000000000..ba90f6eeac --- /dev/null +++ b/src/providers/failover/ldap/failover_ldap.h @@ -0,0 +1,66 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef _FAILOVER_LDAP_H_ +#define _FAILOVER_LDAP_H_ + +#include +#include + +#include "config.h" +#include "resolv/async_resolv.h" +#include "providers/failover/failover_server.h" +#include "util/util.h" + +struct sss_failover_ldap_connection { + struct sdap_server_opts *srv_opts; + struct sdap_handle *sh; + char *uri; +}; + +struct tevent_req * +sss_failover_ldap_kinit_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + struct sss_failover_server *server, + bool addr_changed, + void *pvt); + +errno_t +sss_failover_ldap_kinit_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + time_t *_expiration_time); + +struct tevent_req * +sss_failover_ldap_connect_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + struct sss_failover_server *server, + bool addr_changed, + bool reuse_connection, + bool authenticate_connection, + bool read_rootdse, + enum sss_failover_transaction_tls force_tls, + time_t kinit_expiration_time, + void *pvt); + +errno_t +sss_failover_ldap_connect_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + void **_connection); + +#endif /* _FAILOVER_LDAP_H_ */ diff --git a/src/providers/failover/ldap/failover_ldap_connect.c b/src/providers/failover/ldap/failover_ldap_connect.c new file mode 100644 index 0000000000..6799c68b3a --- /dev/null +++ b/src/providers/failover/ldap/failover_ldap_connect.c @@ -0,0 +1,158 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include +#include + +#include "config.h" +#include "providers/failover/failover.h" +#include "providers/failover/failover_transaction.h" +#include "providers/failover/failover_server.h" +#include "providers/failover/ldap/failover_ldap.h" +#include "providers/ldap/sdap_async_private.h" +#include "util/util.h" + +struct sss_failover_ldap_connect_state { + struct sss_failover_ldap_connection *connection; +}; + +static void sss_failover_ldap_connect_done(struct tevent_req *subreq); + +struct tevent_req * +sss_failover_ldap_connect_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + struct sss_failover_server *server, + bool addr_changed, + bool reuse_connection, + bool authenticate_connection, + bool read_rootdse, + enum sss_failover_transaction_tls force_tls, + time_t kinit_expiration_time, + void *pvt) +{ + struct sss_failover_ldap_connect_state *state; + struct sdap_options *opts; + struct tevent_req *subreq; + struct tevent_req *req; + enum connect_tls tls; + errno_t ret; + + /* TODO handle active connection */ + + opts = talloc_get_type_abort(pvt, struct sdap_options); + + req = tevent_req_create(mem_ctx, &state, + struct sss_failover_ldap_connect_state); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "tevent_req_create() failed\n"); + return NULL; + } + + state->connection = talloc_zero(state, struct sss_failover_ldap_connection); + if (state->connection == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory\n"); + ret = ENOMEM; + goto done; + } + + state->connection->uri = talloc_strdup(state->connection, server->uri); + if (state->connection->uri == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory\n"); + ret = ENOMEM; + goto done; + } + + switch (force_tls) { + case SSS_FAILOVER_TRANSACTION_TLS_DEFAULT: + tls = CON_TLS_DFL; + break; + case SSS_FAILOVER_TRANSACTION_TLS_ON: + tls = CON_TLS_ON; + break; + case SSS_FAILOVER_TRANSACTION_TLS_OFF: + tls = CON_TLS_OFF; + break; + } + + subreq = sdap_cli_connect_send(state, ev, opts, server->uri, + server->addr->sockaddr, + server->addr->sockaddr_len, !read_rootdse, + tls, !authenticate_connection, + kinit_expiration_time); + if (subreq == NULL) { + ret = ENOMEM; + goto done; + } + + tevent_req_set_callback(subreq, sss_failover_ldap_connect_done, req); + + return req; + +done: + tevent_req_error(req, ret); + tevent_req_post(req, ev); + + return req; +} + +static void +sss_failover_ldap_connect_done(struct tevent_req *subreq) +{ + struct sss_failover_ldap_connect_state *state; + struct tevent_req *req; + errno_t ret; + + req = tevent_req_callback_data(subreq, struct tevent_req); + state = tevent_req_data(req, struct sss_failover_ldap_connect_state); + + ret = sdap_cli_connect_recv(subreq, state, &state->connection->sh, + &state->connection->srv_opts); + talloc_zfree(subreq); + if (ret != EOK) { + goto done; + } + + talloc_steal(state->connection, state->connection->sh); + talloc_steal(state->connection, state->connection->srv_opts); + +done: + if (ret != EOK) { + tevent_req_error(req, ret); + return; + } + + tevent_req_done(req); +} + +errno_t +sss_failover_ldap_connect_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + void **_connection) +{ + struct sss_failover_ldap_connect_state *state; + state = tevent_req_data(req, struct sss_failover_ldap_connect_state); + + TEVENT_REQ_RETURN_ON_ERROR(req); + + if (_connection != NULL) { + *_connection = (void*)talloc_steal(mem_ctx, state->connection); + } + + return EOK; +} diff --git a/src/providers/failover/ldap/failover_ldap_kinit.c b/src/providers/failover/ldap/failover_ldap_kinit.c new file mode 100644 index 0000000000..3d7d4bbfc0 --- /dev/null +++ b/src/providers/failover/ldap/failover_ldap_kinit.c @@ -0,0 +1,199 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include +#include + +#include "config.h" +#include "providers/failover/failover.h" +#include "providers/failover/failover_server.h" +#include "providers/failover/failover_server_resolve.h" +#include "providers/failover/failover_vtable_op.h" +#include "providers/ldap/sdap_async_private.h" +#include "providers/ldap/ldap_common.h" +#include "util/util.h" + +static void +sss_failover_ldap_kinit_options(struct sdap_options *opts, + const char **_keytab, + const char **_realm, + const char **_principal, + bool *_canonicalize, + int *_lifetime, + int *_timeout) +{ + *_keytab = dp_opt_get_string(opts->basic, SDAP_KRB5_KEYTAB); + *_realm = sdap_gssapi_realm(opts->basic); + *_principal = dp_opt_get_string(opts->basic, SDAP_SASL_AUTHID); + *_canonicalize = dp_opt_get_bool(opts->basic, SDAP_KRB5_CANONICALIZE); + *_lifetime = dp_opt_get_int(opts->basic, SDAP_KRB5_TICKET_LIFETIME); + *_timeout = dp_opt_get_int(opts->basic, SDAP_OPT_TIMEOUT); +} + +struct sss_failover_ldap_kinit_state { + time_t expiration_time; +}; + +static void sss_failover_ldap_kinit_done(struct tevent_req *subreq); + +struct tevent_req * +sss_failover_ldap_kinit_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + struct sss_failover_server *server, + bool addr_changed, + void *pvt) +{ + struct sss_failover_ldap_kinit_state *state; + struct sdap_options *opts; + struct tevent_req *subreq; + struct tevent_req *req; + const char *keytab; + const char *principal; + const char *realm; + bool canonicalize; + int timeout; + int lifetime; + errno_t ret; + + req = tevent_req_create(mem_ctx, &state, + struct sss_failover_ldap_kinit_state); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "tevent_req_create() failed\n"); + return NULL; + } + + opts = talloc_get_type_abort(pvt, struct sdap_options); + + sss_failover_ldap_kinit_options(opts, &keytab, &realm, &principal, + &canonicalize, &lifetime, &timeout); + + ret = setenv("KRB5_CANONICALIZE", canonicalize ? "true" : "false", 1); + if (ret != 0) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to set KRB5_CANONICALIZE to %s\n", + canonicalize ? "true" : "false"); + ret = errno; + goto done; + } + + DEBUG(SSSDBG_TRACE_FUNC, "Attempting kinit (%s, %s, %s, %d, %s)\n", + keytab != NULL ? keytab : "default", principal, realm, lifetime, + server->name); + + /* TODO write kdcinfo */ + + subreq = sdap_get_tgt_send(state, ev, realm, principal, keytab, lifetime, + timeout); + if (subreq == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Out of memory!\n"); + ret = ENOMEM; + goto done; + } + + tevent_req_set_callback(subreq, sss_failover_ldap_kinit_done, req); + + ret = EOK; + +done: + if (ret != EOK) { + tevent_req_error(req, ret); + tevent_req_post(req, ev); + } + + return req; +} + +static void +sss_failover_ldap_kinit_done(struct tevent_req *subreq) +{ + struct sss_failover_ldap_kinit_state *state; + struct tevent_req *req; + krb5_error_code kerr; + char *ccname; + int result; + errno_t ret; + + req = tevent_req_callback_data(subreq, struct tevent_req); + state = tevent_req_data(req, struct sss_failover_ldap_kinit_state); + + ret = sdap_get_tgt_recv(subreq, state, &result, &kerr, &ccname, + &state->expiration_time); + talloc_zfree(subreq); + if (ret != EOK) { + goto done; + } + + /* ret is request error, result is child error, kerr is kerberos error */ + switch (ret) { + case EOK: + if (result == EOK) { + /* TGT acquired. */ + ret = setenv("KRB5CCNAME", ccname, 1); + if (ret != 0) { + ret = errno; + DEBUG(SSSDBG_OP_FAILURE, + "Unable to set env. variable KRB5CCNAME!\n"); + goto done; + } + ret = EOK; + goto done; + } else if (kerr == KRB5_KDC_UNREACH) { + ret = ERR_SERVER_FAILURE; + goto done; + } else if (result == EFAULT || result == EIO || result == EPERM) { + ret = ERR_AUTH_FAILED; + goto done; + } else { + ret = ERR_AUTH_FAILED; + goto done; + } + break; + case ETIMEDOUT: + /* The child did not responds. Try another KDC. */ + ret = ERR_SERVER_FAILURE; + goto done; + default: + /* Child did not execute correctly. Terminate. */ + goto done; + } + +done: + if (ret != EOK) { + tevent_req_error(req, ret); + return; + } + + tevent_req_done(req); +} + +errno_t +sss_failover_ldap_kinit_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + time_t *_expiration_time) +{ + struct sss_failover_ldap_kinit_state *state = NULL; + state = tevent_req_data(req, struct sss_failover_ldap_kinit_state); + + TEVENT_REQ_RETURN_ON_ERROR(req); + + if (_expiration_time != NULL) { + *_expiration_time = state->expiration_time; + } + + return EOK; +} diff --git a/src/providers/failover/readme.md b/src/providers/failover/readme.md new file mode 100644 index 0000000000..5bef0d3a39 --- /dev/null +++ b/src/providers/failover/readme.md @@ -0,0 +1,185 @@ +# SSSD Failover High-Level Documentation + +This document provides high-level view on the implementation of the failover +mechanism. The code abstracts automatic server selection, connection management +a retry logic from the backend code. The backend should not touch failover +internals. The main entry port for an operation that needs to contact a remote +server is `sss_failover_transaction_send()`. + +## Backend API + +### Failover Context + +* [sss_failover.c]() +* [sss_failover.h]() + +Previously, we had one failover context per backend and the context then +contained "services" (LDAP, AD, AD_GC, ...). Now there is a single failover +context for each required service or domain. This shifts the logic a bit from +pattern "resolve_service(fctx, AD)" to "connect_to(fctx_ad)". + +* `sss_failover_init()` - Initialize new failover context + +### Server and Group Management + +* [sss_failover_group.c]() +* [sss_failover_group.h]() + +Servers are organized into prioritized groups (e.g., primary, backup). Each +group is created when the backend starts - the backend will add the hard-coded +servers and enabled DNS discovery when required. + +When the failover tries to find a working server it tries to find servers +withing each group in order (group 0 has the highest priority). If no servers +are found within the group it tries the next group. + +- `sss_failover_group_new()` - Create a new server group +- `sss_failover_group_add_server()` - Add static servers to group +- `sss_failover_group_setup_dns_discovery()` - Enable DNS SRV discovery for group + +### Failover Transaction + +* [sss_failover_transaction.c]() +* [sss_failover_transaction.h]() + +The failover transaction hides the complicated logic of retrying an operation +the server fails in the middle of the operation. This replaces `sdap_id_op` code +and logic that was used previously, by hiding the logic inside a tevent request +wrapper. + +#### Usage Pattern + +```c +struct my_operation_state { + struct sss_failover_ldap_connection *conn; +}; + +static void my_operation_done(struct tevent_req *subreq); + +struct tevent_req *my_operation_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx) +{ + struct my_operation_state *state; + struct tevent_req *subreq; + struct tevent_req *req; + errno_t ret; + + req = tevent_req_create(mem_ctx, &state, + struct my_operation_state); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "tevent_req_create() failed\n"); + return NULL; + } + + /* ...setup state... */ + + ret = sss_failover_transaction_send(state, ev, fctx, req, + my_operation_done); + if (ret != EOK) { + goto done; + } + + return req; + +done: + tevent_req_error(req, ret); + tevent_req_post(req, ev); + + return req; +} + +static void my_operation_done(struct tevent_req *subreq) +{ + struct my_operation_state *state; + struct tevent_req *req; + errno_t ret; + + req = tevent_req_callback_data(subreq, struct tevent_req); + state = tevent_req_data(req, struct my_operation_state); + + state->conn = sss_failover_transaction_connected_recv(state, subreq, + struct sss_failover_ldap_connection); + talloc_zfree(subreq); + + if (state->conn == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Bug: No connection?\n"); + tevent_req_error(req, EINVAL); + return; + } + + /* Do what needs to be done and then call tevent_req_done(req) or + * tevent_req_error(req, ret) */ + +done: + if (ret != EOK) { + tevent_req_error(req, ret); + return; + } + + tevent_req_done(req); +} + +errno_t my_operation_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req) +{ + TEVENT_REQ_RETURN_ON_ERROR(req); + + return EOK; +} +``` + +- The operation **must** return `ERR_SERVER_FAILURE` if the failure is + server-related +- The failover code will then mark the server offline and retry with the next + server +- Fetch all data from the server **before** writing to sysdb to ensure atomicity + on retry + +### Errors + +* `ERR_SERVER_FAILURE` - Returning this error withing a failover transaction + will retry the transaction with another server + +* `ERR_NO_MORE_SERVERS` - This is returned from the transaction if there are no + more servers to try + +## Internals + +### Virtual Table + +* [sss_failover_vtable.c]() +* [sss_failover_vtable.h]() + +Provides setters and getters of providers custom function to connect, kinit, ... + +### Virtual Table Operations + +* [sss_failover_vtable_op.c]() +* [sss_failover_vtable_op.h]() + +This code is responsible for establishing server connection and kinit. It wraps the call to the given vtable function with server selection and resolution mechanism. + +- **`sss_failover_vtable_op_kinit_send/recv()`** - Selects a KDC and obtains host credentials +- **`sss_failover_vtable_op_connect_send/recv()`** - Selects a server and establishes connection + +These operations: +- Select servers from the candidate pool +- Resolve hostnames to IP addresses +- Call backend-specific vtable functions (kinit/connect) +- Mark servers as working/offline based on results +- Serialize through `vtable_op_queue` to ensure single active connection + +### Server Candidates + +* [sss_failover_refresh_candidates.c]() +* [sss_failover_refresh_candidates.h]() + +Instead of trying to connect to a server one by one, the new failover +implementation maintains a list of "candidate servers". The list is refreshed +periodically or when needed by pining servers from a server group in parallel +batches so it can quickly find the working servers, significantly reducing +operation time. + +The list of candidates is stored inside the failover context. Only one refresh +is triggered at the same time. diff --git a/src/providers/minimal/minimal.h b/src/providers/minimal/minimal.h new file mode 100644 index 0000000000..0f9a15adf7 --- /dev/null +++ b/src/providers/minimal/minimal.h @@ -0,0 +1,42 @@ +/* + SSSD + + minimal Identity Backend Module + + Authors: + Justin Stephenson + + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + + +#ifndef _MINIMAL_H_ +#define _MINIMAL_H_ + +#include "config.h" +#include + +#include "providers/ldap/ldap_common.h" +#include "providers/failover/failover.h" + +struct minimal_init_ctx { + struct sdap_options *options; + struct sdap_id_ctx *id_ctx; + struct sdap_auth_ctx *auth_ctx; + struct sss_failover_ctx *fctx; +}; + +#endif diff --git a/src/providers/minimal/minimal_id.c b/src/providers/minimal/minimal_id.c new file mode 100644 index 0000000000..6e080965f9 --- /dev/null +++ b/src/providers/minimal/minimal_id.c @@ -0,0 +1,247 @@ +/* + SSSD + + minimal Identity Backend Module + + Authors: + Justin Stephenson + + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include + +#include "util/util.h" +#include "providers/minimal/minimal.h" +#include "providers/minimal/minimal_id.h" +#include "providers/minimal/minimal_id_services.h" +#include "providers/failover/failover_transaction.h" + +struct minimal_handle_acct_req_state { + struct dp_id_data *ar; + const char *err; + int dp_error; + int minimal_ret; + int sdap_ret; +}; + +static void minimal_handle_acct_req_done(struct tevent_req *subreq); + +static struct tevent_req * +minimal_handle_acct_req_send(TALLOC_CTX *mem_ctx, + struct be_ctx *be_ctx, + struct dp_id_data *ar, + struct sss_failover_ctx *fctx, + struct sdap_id_ctx *id_ctx, + struct sdap_domain *sdom, + bool noexist_delete) +{ + struct tevent_req *req; + struct tevent_req *subreq; + struct minimal_handle_acct_req_state *state; + errno_t ret; + + + req = tevent_req_create(mem_ctx, &state, + struct minimal_handle_acct_req_state); + if (req == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "tevent_req_create() failed.\n"); + return NULL; + } + state->ar = ar; + + if (ar == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Missing input.\n"); + ret = EINVAL; + goto done; + } + + switch (ar->entry_type & BE_REQ_TYPE_MASK) { + case BE_REQ_SERVICES: + DEBUG(SSSDBG_TRACE_FUNC, "Executing BE_REQ_SERVICES request\n"); + + subreq = minimal_services_get_send(state, be_ctx->ev, fctx, id_ctx, + sdom, ar->filter_value, + ar->extra_value, ar->filter_type, + noexist_delete); + break; + default: /*fail*/ + ret = EINVAL; + state->err = "Invalid request type"; + DEBUG(SSSDBG_OP_FAILURE, + "Unexpected request type: 0x%X [%s:%s] in %s\n", + ar->entry_type, ar->filter_value, + ar->extra_value?ar->extra_value:"-", + ar->domain); + goto done; + } + + if (!subreq) { + ret = ENOMEM; + goto done; + } + + tevent_req_set_callback(subreq, minimal_handle_acct_req_done, req); + return req; + +done: + if (ret == EOK) { + tevent_req_done(req); + } else { + tevent_req_error(req, ret); + } + + tevent_req_post(req, be_ctx->ev); + return req; +} + +static void minimal_handle_acct_req_done(struct tevent_req *subreq) +{ + struct tevent_req *req = tevent_req_callback_data(subreq, struct tevent_req); + struct minimal_handle_acct_req_state *state; + errno_t ret; + const char *err = "Invalid request type"; + + state = tevent_req_data(req, struct minimal_handle_acct_req_state); + + switch (state->ar->entry_type & BE_REQ_TYPE_MASK) { + case BE_REQ_SERVICES: + err = "Service lookup failed"; + ret = minimal_services_get_recv(subreq); + break; + default: /* fail */ + ret = EINVAL; + break; + } + talloc_zfree(subreq); + + state->minimal_ret = ret; + if (ret != EOK) { + state->err = err; + tevent_req_error(req, ret); + return; + } + + state->err = "Success"; + tevent_req_done(req); +} + +static errno_t +minimal_handle_acct_req_recv(struct tevent_req *req, + int *_dp_error, const char **_err, + int *minimal_ret) +{ + struct minimal_handle_acct_req_state *state; + + state = tevent_req_data(req, struct minimal_handle_acct_req_state); + + if (_dp_error) { + *_dp_error = DP_ERR_OK; + } + + if (_err) { + *_err = state->err; + } + + if (minimal_ret) { + *minimal_ret = state->minimal_ret; + } + + TEVENT_REQ_RETURN_ON_ERROR(req); + return EOK; +} + +struct minimal_account_info_handler_state { + struct dp_reply_std reply; +}; + +static void minimal_account_info_handler_done(struct tevent_req *subreq); + +struct tevent_req * +minimal_account_info_handler_send(TALLOC_CTX *mem_ctx, + struct minimal_init_ctx *init_ctx, + struct dp_id_data *data, + struct dp_req_params *params) +{ + struct minimal_account_info_handler_state *state; + struct tevent_req *subreq = NULL; + struct tevent_req *req; + errno_t ret; + + req = tevent_req_create(mem_ctx, &state, + struct minimal_account_info_handler_state); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "tevent_req_create() failed\n"); + return NULL; + } + + subreq = minimal_handle_acct_req_send(state, params->be_ctx, data, + init_ctx->fctx, + init_ctx->id_ctx, + init_ctx->id_ctx->opts->sdom, + true); + if (subreq == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "minimal_handle_acct_req_send() failed.\n"); + ret = ENOMEM; + goto immediately; + } + + tevent_req_set_callback(subreq, minimal_account_info_handler_done, req); + + return req; + +immediately: + dp_reply_std_set(&state->reply, DP_ERR_DECIDE, ret, NULL); + + tevent_req_done(req); + tevent_req_post(req, params->ev); + + return req; +} + +static void minimal_account_info_handler_done(struct tevent_req *subreq) +{ + struct minimal_account_info_handler_state *state; + struct tevent_req *req; + const char *error_msg = NULL; + int dp_error = DP_ERR_FATAL; + errno_t ret; + + req = tevent_req_callback_data(subreq, struct tevent_req); + state = tevent_req_data(req, struct minimal_account_info_handler_state); + + ret = minimal_handle_acct_req_recv(subreq, &dp_error, &error_msg, NULL); + talloc_zfree(subreq); + + dp_reply_std_set(&state->reply, dp_error, ret, error_msg); + tevent_req_done(req); +} + +errno_t +minimal_account_info_handler_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + struct dp_reply_std *data) +{ + struct minimal_account_info_handler_state *state = NULL; + + state = tevent_req_data(req, struct minimal_account_info_handler_state); + + TEVENT_REQ_RETURN_ON_ERROR(req); + + *data = state->reply; + + return EOK; +} diff --git a/src/providers/minimal/minimal_id.h b/src/providers/minimal/minimal_id.h new file mode 100644 index 0000000000..d7f9e79a24 --- /dev/null +++ b/src/providers/minimal/minimal_id.h @@ -0,0 +1,51 @@ +/* + SSSD + + minimal Identity Backend Module + + Authors: + Justin Stephenson + + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + + +#ifndef _MINIMAL_ID_H_ +#define _MINIMAL_ID_H_ + +#include "config.h" +#include + +#include "providers/backend.h" +#include "providers/ldap/ldap_common.h" +#include "util/util.h" + +struct minimal_id_ctx { + struct be_ctx *be_ctx; + struct minimal_init_ctx *init_ctx; + struct dp_option *minimal_options; +}; + +struct tevent_req * +minimal_account_info_handler_send(TALLOC_CTX *mem_ctx, + struct minimal_init_ctx *init_ctx, + struct dp_id_data *data, + struct dp_req_params *params); + +errno_t minimal_account_info_handler_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + struct dp_reply_std *data); +#endif diff --git a/src/providers/minimal/minimal_id_services.c b/src/providers/minimal/minimal_id_services.c new file mode 100644 index 0000000000..66f2d70d9f --- /dev/null +++ b/src/providers/minimal/minimal_id_services.c @@ -0,0 +1,276 @@ +/* + SSSD + + Authors: + Stephen Gallagher + + Copyright (C) 2012 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + + +#include + +#include "util/util.h" +#include "util/strtonum.h" +#include "db/sysdb.h" +#include "db/sysdb_services.h" +#include "providers/ldap/ldap_common.h" +#include "providers/ldap/sdap_async.h" +#include "providers/minimal/minimal_id_services.h" +#include "providers/failover/ldap/failover_ldap.h" +#include "providers/failover/failover_transaction.h" + +struct minimal_services_get_state { + struct tevent_context *ev; + struct sdap_id_ctx *id_ctx; + struct sdap_domain *sdom; + struct sysdb_ctx *sysdb; + struct sss_domain_info *domain; + struct sss_failover_ldap_connection *conn; + + const char *name; + const char *protocol; + + char *filter; + const char **attrs; + + int filter_type; + + bool noexist_delete; + bool test; +}; + +static void +minimal_services_get_connect_done(struct tevent_req *subreq); +static void +minimal_services_get_done(struct tevent_req *subreq); + +struct tevent_req * +minimal_services_get_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + struct sdap_id_ctx *id_ctx, + struct sdap_domain *sdom, + const char *name, + const char *protocol, + int filter_type, + bool noexist_delete) +{ + errno_t ret; + struct tevent_req *req; + struct minimal_services_get_state *state; + const char *attr_name; + char *clean_name; + char *clean_protocol = NULL; + + req = tevent_req_create(mem_ctx, &state, struct minimal_services_get_state); + if (!req) return NULL; + + state->ev = ev; + state->id_ctx = id_ctx; + state->sdom = sdom; + state->domain = sdom->dom; + state->sysdb = sdom->dom->sysdb; + state->name = name; + state->protocol = protocol; + state->filter_type = filter_type; + state->noexist_delete = noexist_delete; + state->test = true; + + switch(filter_type) { + case BE_FILTER_NAME: + attr_name = id_ctx->opts->service_map[SDAP_AT_SERVICE_NAME].name; + break; + case BE_FILTER_IDNUM: + attr_name = id_ctx->opts->service_map[SDAP_AT_SERVICE_PORT].name; + break; + default: + ret = EINVAL; + goto error; + } + + ret = sss_filter_sanitize(state, name, &clean_name); + if (ret != EOK) goto error; + + if (protocol != NULL) { + ret = sss_filter_sanitize(state, protocol, &clean_protocol); + if (ret != EOK) goto error; + } + + if (clean_protocol) { + state->filter = talloc_asprintf( + state, "(&(%s=%s)(%s=%s)(objectclass=%s))", + attr_name, clean_name, + id_ctx->opts->service_map[SDAP_AT_SERVICE_PROTOCOL].name, + clean_protocol, + id_ctx->opts->service_map[SDAP_OC_SERVICE].name); + } else { + state->filter = + talloc_asprintf(state, "(&(%s=%s)(objectclass=%s))", + attr_name, clean_name, + id_ctx->opts->service_map[SDAP_OC_SERVICE].name); + } + talloc_zfree(clean_name); + talloc_zfree(clean_protocol); + if (!state->filter) { + DEBUG(SSSDBG_MINOR_FAILURE, + "Failed to build the base filter\n"); + ret = ENOMEM; + goto error; + } + DEBUG(SSSDBG_TRACE_LIBS, + "Preparing to search for services with filter [%s]\n", + state->filter); + + ret = build_attrs_from_map(state, id_ctx->opts->service_map, + SDAP_OPTS_SERVICES, NULL, + &state->attrs, NULL); + if (ret != EOK) goto error; + + ret = sss_failover_transaction_send(state, ev, fctx, req, + minimal_services_get_connect_done); + if (ret != EOK) goto error; + + return req; + +error: + tevent_req_error(req, ret); + tevent_req_post(req, ev); + return req; +} + +static void +minimal_services_get_connect_done(struct tevent_req *subreq) +{ + struct tevent_req *req = + tevent_req_callback_data(subreq, struct tevent_req); + struct minimal_services_get_state *state = + tevent_req_data(req, struct minimal_services_get_state); + + state->conn = sss_failover_transaction_connected_recv(state, subreq, + struct sss_failover_ldap_connection); + talloc_zfree(subreq); + + if (state->conn == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Bug: No connection?\n"); + tevent_req_error(req, EINVAL); + return; + } + + if (state->test) { + state->test = false; + tevent_req_error(req, ERR_SERVER_FAILURE); + return; + } + + subreq = sdap_get_services_send(state, state->ev, + state->domain, state->sysdb, + state->id_ctx->opts, + state->sdom->service_search_bases, + state->conn->sh, + state->attrs, state->filter, + dp_opt_get_int(state->id_ctx->opts->basic, + SDAP_SEARCH_TIMEOUT), + false); + if (!subreq) { + tevent_req_error(req, ENOMEM); + return; + } + tevent_req_set_callback(subreq, minimal_services_get_done, req); +} + +static void +minimal_services_get_done(struct tevent_req *subreq) +{ + errno_t ret; + uint16_t port; + char *endptr; + struct tevent_req *req = + tevent_req_callback_data(subreq, struct tevent_req); + struct minimal_services_get_state *state = + tevent_req_data(req, struct minimal_services_get_state); + + ret = sdap_get_services_recv(NULL, subreq, NULL); + talloc_zfree(subreq); + + /* Check whether we need to try again with another + * failover server. + */ + // TODO handle how to yield ERR_SERVER_FAILED + // ret = sdap_id_op_done(state->op, ret, &dp_error); + // if (dp_error == DP_ERR_OK && ret != EOK) { + // /* retry */ + // ret = minimal_services_get_retry(req); + // if (ret != EOK) { + // tevent_req_error(req, ret); + // return; + // } + + // /* Return to the mainloop to retry */ + // return; + // } + // state->sdap_ret = ret; + + // /* An error occurred. */ + // if (ret && ret != ENOENT) { + // state->dp_error = dp_error; + // tevent_req_error(req, ret); + // return; + //} + + if (ret == ENOENT && state->noexist_delete == true) { + /* Ensure that this entry is removed from the sysdb */ + switch(state->filter_type) { + case BE_FILTER_NAME: + ret = sysdb_svc_delete(state->domain, state->name, + 0, state->protocol); + if (ret != EOK) { + tevent_req_error(req, ret); + return; + } + break; + + case BE_FILTER_IDNUM: + port = strtouint16(state->name, &endptr, 10); + if (errno || *endptr || (state->name == endptr)) { + tevent_req_error(req, (errno ? errno : EINVAL)); + return; + } + + ret = sysdb_svc_delete(state->domain, NULL, port, + state->protocol); + if (ret != EOK) { + tevent_req_error(req, ret); + return; + } + break; + + default: + tevent_req_error(req, EINVAL); + return; + } + } + + tevent_req_done(req); +} + +errno_t +minimal_services_get_recv(struct tevent_req *req) +{ + TEVENT_REQ_RETURN_ON_ERROR(req); + + return EOK; +} diff --git a/src/providers/minimal/minimal_id_services.h b/src/providers/minimal/minimal_id_services.h new file mode 100644 index 0000000000..6f82b2e3ac --- /dev/null +++ b/src/providers/minimal/minimal_id_services.h @@ -0,0 +1,52 @@ +/* + SSSD + + minimal Identity Backend Module + + Authors: + Justin Stephenson + + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + + +#ifndef _MINIMAL_ID_SERVICES_H_ +#define _MINIMAL_ID_SERVICES_H_ + +#include "config.h" +#include +#include + +#include "providers/backend.h" +#include "providers/ldap/ldap_common.h" +#include "util/util.h" +#include "providers/failover/failover.h" + +struct tevent_req * +minimal_services_get_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + struct sdap_id_ctx *id_ctx, + struct sdap_domain *sdom, + const char *name, + const char *protocol, + int filter_type, + bool noexist_delete); + +errno_t +minimal_services_get_recv(struct tevent_req *req); + +#endif diff --git a/src/providers/minimal/minimal_init.c b/src/providers/minimal/minimal_init.c new file mode 100644 index 0000000000..d6f126fc46 --- /dev/null +++ b/src/providers/minimal/minimal_init.c @@ -0,0 +1,349 @@ +/* + SSSD + + minimal Provider Initialization functions + + Authors: + Justin Stephenson + + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "src/providers/data_provider.h" +#include "providers/ldap/ldap_common.h" +#include "providers/ldap/ldap_opts.h" +#include "providers/ldap/sdap_async_private.h" +#include "providers/ldap/sdap_access.h" +#include "providers/ldap/ldap_resolver_enum.h" +#include "providers/fail_over_srv.h" +#include "providers/be_refresh.h" + +#include "src/providers/minimal/minimal.h" +#include "src/providers/minimal/minimal_id.h" +#include "src/providers/minimal/minimal_ldap_auth.h" +#include "src/providers/failover/failover.h" +#include "src/providers/failover/failover_vtable.h" +#include "src/providers/failover/ldap/failover_ldap.h" + +/* Copied from ldap_init.c with no changes */ +static errno_t get_sdap_service(TALLOC_CTX *mem_ctx, + struct be_ctx *be_ctx, + struct sdap_options *opts, + struct sdap_service **_sdap_service) +{ + errno_t ret; + const char *urls; + const char *backup_urls; + const char *dns_service_name; + struct sdap_service *sdap_service; + + urls = dp_opt_get_string(opts->basic, SDAP_URI); + backup_urls = dp_opt_get_string(opts->basic, SDAP_BACKUP_URI); + dns_service_name = dp_opt_get_string(opts->basic, SDAP_DNS_SERVICE_NAME); + if (dns_service_name != NULL) { + DEBUG(SSSDBG_CONF_SETTINGS, + "Service name for discovery set to %s\n", dns_service_name); + } + + ret = sdap_service_init(mem_ctx, be_ctx, "LDAP", + dns_service_name, + urls, + backup_urls, + &sdap_service); + if (ret != EOK) { + return ret; + } + + *_sdap_service = sdap_service; + return EOK; +} + +/* Copied from ldap_init.c with some changes + * removing calls to + * - sdap_gssapi_init() + * - sdap_idmap_init() + * - confdb_certmap_to_sysdb() + * - sdap_init_certmap() */ +static errno_t ldap_init_misc(struct be_ctx *be_ctx, + struct sdap_options *options, + struct sdap_id_ctx *id_ctx) +{ + errno_t ret; + + setup_ldap_debug(options->basic); + + ret = setup_tls_config(options->basic); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to get TLS options [%d]: %s\n", + ret, sss_strerror(ret)); + return ret; + } + + ret = ldap_id_setup_tasks(id_ctx); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to setup background tasks " + "[%d]: %s\n", ret, sss_strerror(ret)); + return ret; + } + + /* Setup SRV lookup plugin */ + ret = be_fo_set_dns_srv_lookup_plugin(be_ctx, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to set SRV lookup plugin " + "[%d]: %s\n", ret, sss_strerror(ret)); + return ret; + } + + /* Setup periodical refresh of expired records */ + ret = sdap_refresh_init(be_ctx, id_ctx); + if (ret != EOK && ret != EEXIST) { + DEBUG(SSSDBG_MINOR_FAILURE, "Periodical refresh will not work " + "[%d]: %s\n", ret, sss_strerror(ret)); + } + + return EOK; +} + +/* Copied from ldap_init.c */ +static errno_t minimal_init_auth_ctx(TALLOC_CTX *mem_ctx, + struct be_ctx *be_ctx, + struct sdap_id_ctx *id_ctx, + struct sdap_options *options, + struct sdap_auth_ctx **_auth_ctx) +{ + struct sdap_auth_ctx *auth_ctx; + + auth_ctx = talloc(mem_ctx, struct sdap_auth_ctx); + if (auth_ctx == NULL) { + return ENOMEM; + } + + auth_ctx->be = be_ctx; + auth_ctx->opts = options; + auth_ctx->service = id_ctx->conn->service; + auth_ctx->chpass_service = NULL; + + *_auth_ctx = auth_ctx; + + return EOK; +} + +static struct sss_failover_ctx * +sssm_minimal_init_failover(TALLOC_CTX *mem_ctx, + struct be_ctx *be_ctx, + struct sdap_options *opts) +{ + struct sss_failover_ctx *fctx; + struct sss_failover_group *group; + struct sss_failover_server *server; + errno_t ret; + + /* Setup new failover. */ + fctx = sss_failover_init(mem_ctx, be_ctx->ev, "LDAP", + be_ctx->be_res->resolv, + be_ctx->be_res->family_order); + if (fctx == NULL) { + return NULL; + } + + /* Add primary servers */ + group = sss_failover_group_new(fctx, "primary"); + if (group == NULL) { + ret = ENOMEM; + goto done; + } + + ret = sss_failover_group_setup_dns_discovery(group); + if (ret != EOK) { + goto done; + } + + server = sss_failover_server_new(fctx, "fake_1.ldap.test", + "ldap://fake_1.ldap.test", 389, 1, 1); + if (server == NULL) { + ret = ENOMEM; + goto done; + } + + ret = sss_failover_group_add_server(group, server); + if (ret != EOK) { + goto done; + } + + server = sss_failover_server_new(fctx, "fake_2.ldap.test", + "ldap://fake_2.ldap.test", 389, 1, 1); + if (server == NULL) { + ret = ENOMEM; + goto done; + } + + ret = sss_failover_group_add_server(group, server); + if (ret != EOK) { + goto done; + } + + server = sss_failover_server_new(fctx, "master.ldap.test", + "ldap://master.ldap.test", 389, 1, 1); + if (server == NULL) { + ret = ENOMEM; + goto done; + } + + ret = sss_failover_group_add_server(group, server); + if (ret != EOK) { + goto done; + } + + sss_failover_vtable_set_connect(fctx, + sss_failover_ldap_connect_send, + sss_failover_ldap_connect_recv, + opts); + + ret = EOK; + +done: + if (ret != EOK) { + talloc_free(fctx); + return NULL; + } + + return fctx; +} + +errno_t sssm_minimal_init(TALLOC_CTX *mem_ctx, + struct be_ctx *be_ctx, + struct data_provider *provider, + const char *module_name, + void **_module_data) +{ + struct sdap_service *sdap_service; + struct minimal_init_ctx *init_ctx; + errno_t ret; + + init_ctx = talloc_zero(mem_ctx, struct minimal_init_ctx); + if (init_ctx == NULL) { + return ENOMEM; + } + + /* Always initialize options since it is needed everywhere. */ + ret = ldap_get_options(init_ctx, be_ctx->domain, be_ctx->cdb, + be_ctx->conf_path, be_ctx->provider, + &init_ctx->options); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to initialize LDAP options " + "[%d]: %s\n", ret, sss_strerror(ret)); + goto done; + } + + /* Always initialize id_ctx since it is needed everywhere. */ + ret = get_sdap_service(init_ctx, be_ctx, init_ctx->options, &sdap_service); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to initialize failover service " + "[%d]: %s\n", ret, sss_strerror(ret)); + goto done; + } + + init_ctx->id_ctx = sdap_id_ctx_new(init_ctx, be_ctx, sdap_service); + if (init_ctx->id_ctx == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to initialize LDAP ID context\n"); + ret = ENOMEM; + goto done; + } + + init_ctx->id_ctx->opts = init_ctx->options; + + /* Setup miscellaneous things. */ + ret = ldap_init_misc(be_ctx, init_ctx->options, init_ctx->id_ctx); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to init LDAP module " + "[%d]: %s\n", ret, sss_strerror(ret)); + goto done; + } + + /* Initialize auth_ctx only if DPT_AUTH target is enabled. */ + if (dp_target_enabled(provider, module_name, DPT_AUTH)) { + ret = minimal_init_auth_ctx(init_ctx, be_ctx, init_ctx->id_ctx, + init_ctx->options, &init_ctx->auth_ctx); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to create auth context " + "[%d]: %s\n", ret, sss_strerror(ret)); + return ret; + } + } + + /* Setup new failover. */ + init_ctx->fctx = sssm_minimal_init_failover(init_ctx, be_ctx, init_ctx->id_ctx->opts); + if (init_ctx->fctx == NULL) { + DEBUG(SSSDBG_FATAL_FAILURE, "Unable to init new failover\n"); + ret = ENOMEM; + goto done; + } + + *_module_data = init_ctx; + + ret = EOK; + +done: + if (ret != EOK) { + talloc_free(init_ctx); + } + + return ret; +} + +errno_t sssm_minimal_id_init(TALLOC_CTX *mem_ctx, + struct be_ctx *be_ctx, + void *module_data, + struct dp_method *dp_methods) +{ + struct minimal_init_ctx *init_ctx; + errno_t ret; + + init_ctx = talloc_get_type(module_data, struct minimal_init_ctx); + + dp_set_method(dp_methods, DPM_ACCOUNT_HANDLER, + minimal_account_info_handler_send, minimal_account_info_handler_recv, init_ctx, + struct minimal_init_ctx, struct dp_id_data, struct dp_reply_std); + + /* LDAP provider check online handler */ + dp_set_method(dp_methods, DPM_CHECK_ONLINE, + sdap_online_check_handler_send, sdap_online_check_handler_recv, init_ctx->id_ctx, + struct sdap_id_ctx, void, struct dp_reply_std); + + dp_set_method(dp_methods, DPM_ACCT_DOMAIN_HANDLER, + default_account_domain_send, default_account_domain_recv, NULL, + void, struct dp_get_acct_domain_data, struct dp_reply_std); + + ret = EOK; + + return ret; +} + +errno_t sssm_minimal_auth_init(TALLOC_CTX *mem_ctx, + struct be_ctx *be_ctx, + void *module_data, + struct dp_method *dp_methods) +{ + struct minimal_init_ctx *init_ctx; + + init_ctx = talloc_get_type(module_data, struct minimal_init_ctx); + + dp_set_method(dp_methods, DPM_AUTH_HANDLER, + minimal_sdap_pam_auth_handler_send, minimal_sdap_pam_auth_handler_recv, init_ctx, + struct minimal_init_ctx, struct pam_data, struct pam_data *); + + return EOK; +} diff --git a/src/providers/minimal/minimal_ldap_auth.c b/src/providers/minimal/minimal_ldap_auth.c new file mode 100644 index 0000000000..3a6e47c706 --- /dev/null +++ b/src/providers/minimal/minimal_ldap_auth.c @@ -0,0 +1,884 @@ +/* + SSSD + + LDAP Backend Module + + Authors: + Sumit Bose + + Copyright (C) 2008 Red Hat + Copyright (C) 2010, rhafer@suse.de, Novell Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include + +#include "util/util.h" +#include "util/user_info_msg.h" +#include "db/sysdb.h" +#include "providers/ldap/ldap_common.h" +#include "providers/ldap/sdap_async.h" +#include "providers/ldap/sdap_async_private.h" +#include "providers/ldap/ldap_auth.h" +#include "providers/minimal/minimal.h" +#include "providers/failover/failover_transaction.h" +#include "providers/failover/ldap/failover_ldap.h" + +static errno_t +find_password_expiration_attributes(TALLOC_CTX *mem_ctx, + const struct ldb_message *msg, + enum sdap_access_type access_type, + struct dp_option *opts, + enum pwexpire *pwd_exp_type, + void **data) +{ + const char *mark; + const char *val; + struct spwd *spwd; + const char *pwd_policy; + int ret; + + *pwd_exp_type = PWEXPIRE_NONE; + *data = NULL; + + switch (access_type) { + case SDAP_TYPE_IPA: + /* MIT-Kerberos is the only option for IPA */ + pwd_policy = PWD_POL_OPT_MIT; + break; + case SDAP_TYPE_LDAP: + pwd_policy = dp_opt_get_string(opts, SDAP_PWD_POLICY); + if (pwd_policy == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing password policy.\n"); + return EINVAL; + } + break; + default: + DEBUG(SSSDBG_CRIT_FAILURE,"Unknown access_type [%i].\n", access_type); + return EINVAL; + } + + if (strcasecmp(pwd_policy, PWD_POL_OPT_NONE) == 0) { + DEBUG(SSSDBG_TRACE_ALL, "No password policy requested.\n"); + return EOK; + } else if (strcasecmp(pwd_policy, PWD_POL_OPT_MIT) == 0) { + mark = ldb_msg_find_attr_as_string(msg, SYSDB_KRBPW_LASTCHANGE, NULL); + if (mark != NULL) { + DEBUG(SSSDBG_TRACE_ALL, + "Found Kerberos password expiration attributes.\n"); + val = ldb_msg_find_attr_as_string(msg, SYSDB_KRBPW_EXPIRATION, + NULL); + if (val != NULL) { + *data = talloc_strdup(mem_ctx, val); + if (*data == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strdup failed.\n"); + return ENOMEM; + } + *pwd_exp_type = PWEXPIRE_KERBEROS; + + return EOK; + } + } else { + DEBUG(SSSDBG_CRIT_FAILURE, + "No Kerberos password expiration attributes found, " + "but MIT Kerberos password policy was requested. " + "Access will be denied.\n"); + return EACCES; + } + } else if (strcasecmp(pwd_policy, PWD_POL_OPT_SHADOW) == 0) { + mark = ldb_msg_find_attr_as_string(msg, SYSDB_SHADOWPW_LASTCHANGE, NULL); + if (mark != NULL) { + DEBUG(SSSDBG_TRACE_ALL, + "Found shadow password expiration attributes.\n"); + spwd = talloc_zero(mem_ctx, struct spwd); + if (spwd == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc failed.\n"); + return ENOMEM; + } + + val = ldb_msg_find_attr_as_string(msg, SYSDB_SHADOWPW_LASTCHANGE, NULL); + ret = string_to_shadowpw_days(val, &spwd->sp_lstchg); + if (ret != EOK) goto shadow_fail; + + val = ldb_msg_find_attr_as_string(msg, SYSDB_SHADOWPW_MIN, NULL); + ret = string_to_shadowpw_days(val, &spwd->sp_min); + if (ret != EOK) goto shadow_fail; + + val = ldb_msg_find_attr_as_string(msg, SYSDB_SHADOWPW_MAX, NULL); + ret = string_to_shadowpw_days(val, &spwd->sp_max); + if (ret != EOK) goto shadow_fail; + + val = ldb_msg_find_attr_as_string(msg, SYSDB_SHADOWPW_WARNING, NULL); + ret = string_to_shadowpw_days(val, &spwd->sp_warn); + if (ret != EOK) goto shadow_fail; + + val = ldb_msg_find_attr_as_string(msg, SYSDB_SHADOWPW_INACTIVE, NULL); + ret = string_to_shadowpw_days(val, &spwd->sp_inact); + if (ret != EOK) goto shadow_fail; + + val = ldb_msg_find_attr_as_string(msg, SYSDB_SHADOWPW_EXPIRE, NULL); + ret = string_to_shadowpw_days(val, &spwd->sp_expire); + if (ret != EOK) goto shadow_fail; + + *data = spwd; + *pwd_exp_type = PWEXPIRE_SHADOW; + + return EOK; + } else { + DEBUG(SSSDBG_CRIT_FAILURE, "No shadow password attributes found, " + "but shadow password policy was requested. " + "Access will be denied.\n"); + return EACCES; + } + } + + DEBUG(SSSDBG_TRACE_ALL, "No password expiration attributes found.\n"); + return EOK; + +shadow_fail: + talloc_free(spwd); + return ret; +} + +/* ==Get-User-DN========================================================== */ +struct get_user_dn_state { + char *username; + + char *orig_dn; +}; + +static void get_user_dn_done(struct tevent_req *subreq); + +static struct tevent_req *get_user_dn_send(TALLOC_CTX *memctx, + struct tevent_context *ev, + struct sss_domain_info *domain, + struct sdap_handle *sh, + struct sdap_options *opts, + const char *username) +{ + struct tevent_req *req; + struct tevent_req *subreq; + struct get_user_dn_state *state; + char *clean_name; + char *filter; + const char **attrs; + errno_t ret; + + req = tevent_req_create(memctx, &state, struct get_user_dn_state); + if (!req) return NULL; + + ret = sss_parse_internal_fqname(state, username, + &state->username, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Cannot parse %s\n", username); + goto done; + } + + ret = sss_filter_sanitize(state, state->username, &clean_name); + if (ret != EOK) { + goto done; + } + + filter = talloc_asprintf(state, "(&(%s=%s)(objectclass=%s))", + opts->user_map[SDAP_AT_USER_NAME].name, + clean_name, + opts->user_map[SDAP_OC_USER].name); + talloc_zfree(clean_name); + if (filter == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to build the base filter\n"); + ret = ENOMEM; + goto done; + } + + /* We're mostly interested in the DN anyway */ + attrs = talloc_array(state, const char *, 3); + if (attrs == NULL) { + ret = ENOMEM; + goto done; + } + attrs[0] = "objectclass"; + attrs[1] = opts->user_map[SDAP_AT_USER_NAME].name; + attrs[2] = NULL; + + subreq = sdap_search_user_send(state, ev, domain, opts, + opts->sdom->user_search_bases, + sh, attrs, filter, + dp_opt_get_int(opts->basic, + SDAP_SEARCH_TIMEOUT), + SDAP_LOOKUP_SINGLE); + if (!subreq) { + ret = ENOMEM; + goto done; + } + tevent_req_set_callback(subreq, get_user_dn_done, req); + return req; + +done: + if (ret == EOK) { + tevent_req_done(req); + } else { + tevent_req_error(req, ret); + } + tevent_req_post(req, ev); + return req; +} + +static void get_user_dn_done(struct tevent_req *subreq) +{ + errno_t ret; + struct tevent_req *req = tevent_req_callback_data(subreq, + struct tevent_req); + struct get_user_dn_state *state = tevent_req_data(req, + struct get_user_dn_state); + struct ldb_message_element *el; + struct sysdb_attrs **users; + size_t count; + + ret = sdap_search_user_recv(state, subreq, NULL, &users, &count); + talloc_zfree(subreq); + if (ret && ret != ENOENT) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to retrieve users\n"); + tevent_req_error(req, ret); + return; + } + + if (count == 0) { + DEBUG(SSSDBG_OP_FAILURE, "No such user\n"); + tevent_req_error(req, ENOMEM); + return; + } else if (count > 1) { + DEBUG(SSSDBG_OP_FAILURE, "Multiple users matched\n"); + tevent_req_error(req, EIO); + return; + } + + /* exactly one user. Get the originalDN */ + ret = sysdb_attrs_get_el_ext(users[0], SYSDB_ORIG_DN, false, &el); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, + "originalDN is not available for [%s].\n", state->username); + tevent_req_error(req, ret); + return; + } + + state->orig_dn = talloc_strdup(state, (const char *) el->values[0].data); + if (state->orig_dn == NULL) { + tevent_req_error(req, ENOMEM); + return; + } + + DEBUG(SSSDBG_TRACE_INTERNAL, "Found originalDN [%s] for [%s]\n", + state->orig_dn, state->username); + tevent_req_done(req); +} + +static int get_user_dn_recv(TALLOC_CTX *mem_ctx, struct tevent_req *req, + char **orig_dn) +{ + struct get_user_dn_state *state = tevent_req_data(req, + struct get_user_dn_state); + + if (orig_dn) { + *orig_dn = talloc_move(mem_ctx, &state->orig_dn); + } + + TEVENT_REQ_RETURN_ON_ERROR(req); + + return EOK; +} + +int get_user_dn(TALLOC_CTX *memctx, + struct sss_domain_info *domain, + enum sdap_access_type access_type, + struct sdap_options *opts, + const char *username, + char **user_dn, + enum pwexpire *user_pw_expire_type, + void **user_pw_expire_data) +{ + TALLOC_CTX *tmpctx; + enum pwexpire pw_expire_type = PWEXPIRE_NONE; + void *pw_expire_data; + struct ldb_result *res; + const char **attrs; + const char *dn = NULL; + int ret; + + tmpctx = talloc_new(memctx); + if (!tmpctx) { + return ENOMEM; + } + + attrs = talloc_array(tmpctx, const char *, 11); + if (!attrs) { + ret = ENOMEM; + goto done; + } + + attrs[0] = SYSDB_ORIG_DN; + attrs[1] = SYSDB_SHADOWPW_LASTCHANGE; + attrs[2] = SYSDB_SHADOWPW_MIN; + attrs[3] = SYSDB_SHADOWPW_MAX; + attrs[4] = SYSDB_SHADOWPW_WARNING; + attrs[5] = SYSDB_SHADOWPW_INACTIVE; + attrs[6] = SYSDB_SHADOWPW_EXPIRE; + attrs[7] = SYSDB_KRBPW_LASTCHANGE; + attrs[8] = SYSDB_KRBPW_EXPIRATION; + attrs[9] = SYSDB_PWD_ATTRIBUTE; + attrs[10] = NULL; + + ret = sysdb_get_user_attr(tmpctx, domain, username, attrs, &res); + if (ret) { + goto done; + } + + switch (res->count) { + case 0: + /* No such user entry? Look it up */ + ret = EAGAIN; + break; + + case 1: + dn = ldb_msg_find_attr_as_string(res->msgs[0], SYSDB_ORIG_DN, NULL); + if (dn == NULL) { + /* The user entry has no original DN. This is the case when the ID + * provider is not LDAP-based (proxy perhaps) */ + ret = EAGAIN; + break; + } + + dn = talloc_strdup(tmpctx, dn); + if (!dn) { + ret = ENOMEM; + break; + } + + ret = find_password_expiration_attributes(tmpctx, + res->msgs[0], + access_type, + opts->basic, + &pw_expire_type, + &pw_expire_data); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "find_password_expiration_attributes failed.\n"); + } + break; + + default: + DEBUG(SSSDBG_CRIT_FAILURE, + "User search by name (%s) returned > 1 results!\n", + username); + ret = EFAULT; + break; + } + +done: + if (ret == EOK) { + *user_dn = talloc_strdup(memctx, dn); + if (!*user_dn) { + ret = ENOMEM; + } + /* pw_expire_data may be NULL */ + *user_pw_expire_data = talloc_steal(memctx, pw_expire_data); + *user_pw_expire_type = pw_expire_type; + } + + talloc_zfree(tmpctx); + return ret; +} + +/* ==Authenticate-User==================================================== */ + +struct minimal_auth_state { + struct tevent_context *ev; + struct sss_failover_ctx *fctx; + struct sdap_auth_ctx *ctx; + const char *username; + struct sss_auth_token *authtok; + struct sdap_service *sdap_service; + + struct sss_failover_ldap_connection *conn; + + char *dn; + enum pwexpire pw_expire_type; + void *pw_expire_data; +}; + +static errno_t auth_connect_send(struct tevent_req *req); +static void auth_get_dn_done(struct tevent_req *subreq); +static void auth_do_bind(struct tevent_req *req); +static void auth_connect_done(struct tevent_req *subreq); +static void auth_bind_user_done(struct tevent_req *subreq); + +static struct tevent_req * +minimal_auth_send(TALLOC_CTX *memctx, + struct tevent_context *ev, + struct sss_failover_ctx *fctx, + struct sdap_auth_ctx *ctx, + const char *username, + struct sss_auth_token *authtok, + bool try_chpass_service) +{ + struct tevent_req *req; + struct minimal_auth_state *state; + errno_t ret; + + req = tevent_req_create(memctx, &state, struct minimal_auth_state); + if (!req) return NULL; + + /* The token must be a password token */ + if (sss_authtok_get_type(authtok) != SSS_AUTHTOK_TYPE_PASSWORD && + sss_authtok_get_type(authtok) != SSS_AUTHTOK_TYPE_PAM_STACKED) { + if (sss_authtok_get_type(authtok) == SSS_AUTHTOK_TYPE_SC_PIN + || sss_authtok_get_type(authtok) == SSS_AUTHTOK_TYPE_SC_KEYPAD) { + /* Tell frontend that we do not support Smartcard authentication */ + ret = ERR_SC_AUTH_NOT_SUPPORTED; + } else { + ret = ERR_AUTH_FAILED; + } + goto fail; + } + + state->ev = ev; + state->fctx = fctx; + state->ctx = ctx; + state->username = username; + state->authtok = authtok; + if (try_chpass_service && ctx->chpass_service != NULL && + ctx->chpass_service->name != NULL) { + state->sdap_service = ctx->chpass_service; + } else { + state->sdap_service = ctx->service; + } + + ret = get_user_dn(state, state->ctx->be->domain, SDAP_TYPE_LDAP, + state->ctx->opts, state->username, &state->dn, + &state->pw_expire_type, &state->pw_expire_data); + if (ret == EAGAIN) { + DEBUG(SSSDBG_TRACE_FUNC, + "Need to look up the DN of %s later\n", state->username); + } else if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, + "Cannot get user DN [%d]: %s\n", ret, sss_strerror(ret)); + goto fail; + } + + ret = auth_connect_send(req); + if (ret != EOK) { + goto fail; + } + + return req; + +fail: + tevent_req_error(req, ret); + tevent_req_post(req, ev); + return req; +} + +static errno_t auth_connect_send(struct tevent_req *req) +{ + struct minimal_auth_state *state = tevent_req_data(req, + struct minimal_auth_state); + bool use_tls; + bool skip_conn_auth = false; + const char *sasl_mech; + errno_t ret; + + /* Check for undocumented debugging feature to disable TLS + * for authentication. This should never be used in production + * for obvious reasons. + */ + use_tls = !dp_opt_get_bool(state->ctx->opts->basic, SDAP_DISABLE_AUTH_TLS); + if (!use_tls) { + sss_log(SSS_LOG_ALERT, "LDAP authentication being performed over " + "insecure connection. This should be done " + "for debugging purposes only."); + } + + if (state->dn != NULL) { + /* In case the user's DN is known, the connection will only be used + * to bind as the user to perform the authentication. In that case, + * we don't need to authenticate the connection, because we're not + * looking up any information using the connection. This might be + * needed e.g. in case both ID and AUTH providers are set to LDAP + * and the server is AD, because otherwise the connection would both + * do a startTLS and later bind using GSSAPI or GSS-SPNEGO which + * doesn't work well with AD. + */ + skip_conn_auth = true; + } + + if (skip_conn_auth == false) { + sasl_mech = dp_opt_get_string(state->ctx->opts->basic, + SDAP_SASL_MECH); + if (sasl_mech && sdap_sasl_mech_needs_kinit(sasl_mech)) { + /* Don't force TLS on if we're told to use GSSAPI or GSS-SPNEGO */ + use_tls = false; + } + } + + if (ldap_is_ldapi_url(state->sdap_service->uri)) { + /* Don't force TLS on if we're a unix domain socket */ + use_tls = false; + } + + ret = sss_failover_transaction_ex_send( + state, state->ev, state->fctx, req, auth_connect_done, false, + !skip_conn_auth, true, + use_tls ? SSS_FAILOVER_TRANSACTION_TLS_ON + : SSS_FAILOVER_TRANSACTION_TLS_OFF); + + return ret; +} + +static bool check_encryption_used(LDAP *ldap) +{ + ber_len_t sasl_ssf = 0; + int tls_inplace = 0; + int ret; + + ret = ldap_get_option(ldap, LDAP_OPT_X_SASL_SSF, &sasl_ssf); + if (ret != LDAP_OPT_SUCCESS) { + DEBUG(SSSDBG_TRACE_LIBS, "ldap_get_option failed to get sasl ssf, " + "assuming SASL is not used.\n"); + sasl_ssf = 0; + } + + tls_inplace = ldap_tls_inplace(ldap); + + DEBUG(SSSDBG_TRACE_ALL, + "Encryption used: SASL SSF [%lu] tls_inplace [%s].\n", sasl_ssf, + tls_inplace == 1 ? "TLS inplace" : "TLS NOT inplace"); + + if (sasl_ssf <= 1 && tls_inplace != 1) { + DEBUG(SSSDBG_CRIT_FAILURE, + "No encryption detected on LDAP connection.\n"); + sss_log(SSS_LOG_CRIT, "No encryption detected on LDAP connection.\n"); + return false; + } + + return true; +} + +static void auth_connect_done(struct tevent_req *subreq) +{ + struct tevent_req *req = tevent_req_callback_data(subreq, + struct tevent_req); + struct minimal_auth_state *state = tevent_req_data(req, + struct minimal_auth_state); + + state->conn = sss_failover_transaction_connected_recv(state, subreq, + struct sss_failover_ldap_connection); + talloc_zfree(subreq); + + if (state->conn == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Bug: No connection?\n"); + tevent_req_error(req, EINVAL); + return; + } + + if (!ldap_is_ldapi_url(state->sdap_service->uri) && + !check_encryption_used(state->conn->sh->ldap) && + !dp_opt_get_bool(state->ctx->opts->basic, SDAP_DISABLE_AUTH_TLS)) { + DEBUG(SSSDBG_CRIT_FAILURE, "Aborting the authentication request.\n"); + sss_log(SSS_LOG_CRIT, "Aborting the authentication request.\n"); + tevent_req_error(req, ERR_AUTH_FAILED); + return; + } + + if (state->dn == NULL) { + /* The cached user entry was missing the bind DN. Need to look + * it up based on user name in order to perform the bind */ + subreq = get_user_dn_send(req, state->ev, state->ctx->be->domain, + state->conn->sh, state->ctx->opts, state->username); + if (subreq == NULL) { + tevent_req_error(req, ENOMEM); + return; + } + tevent_req_set_callback(subreq, auth_get_dn_done, req); + return; + } + + /* All required user data was pre-cached during an identity lookup. + * We can proceed with the bind */ + auth_do_bind(req); + return; +} + +static void auth_get_dn_done(struct tevent_req *subreq) +{ + struct tevent_req *req = tevent_req_callback_data(subreq, + struct tevent_req); + struct minimal_auth_state *state = tevent_req_data(req, struct minimal_auth_state); + errno_t ret; + + ret = get_user_dn_recv(state, subreq, &state->dn); + talloc_zfree(subreq); + if (ret != EOK) { + tevent_req_error(req, ERR_ACCOUNT_UNKNOWN); + return; + } + + /* The DN was found with an LDAP lookup + * We can proceed with the bind */ + return auth_do_bind(req); +} + +static void auth_do_bind(struct tevent_req *req) +{ + struct minimal_auth_state *state = tevent_req_data(req, struct minimal_auth_state); + struct tevent_req *subreq; + bool use_ppolicy = dp_opt_get_bool(state->ctx->opts->basic, + SDAP_USE_PPOLICY); + int timeout = dp_opt_get_int(state->ctx->opts->basic, SDAP_OPT_TIMEOUT); + + subreq = sdap_auth_send(state, state->ev, state->conn->sh, + NULL, NULL, state->dn, + state->authtok, + timeout, use_ppolicy, + state->ctx->opts->pwmodify_mode); + if (!subreq) { + tevent_req_error(req, ENOMEM); + return; + } + + tevent_req_set_callback(subreq, auth_bind_user_done, req); +} + +static void auth_bind_user_done(struct tevent_req *subreq) +{ + struct tevent_req *req = tevent_req_callback_data(subreq, + struct tevent_req); + struct minimal_auth_state *state = tevent_req_data(req, + struct minimal_auth_state); + int ret; + struct sdap_ppolicy_data *ppolicy = NULL; + + ret = sdap_auth_recv(subreq, state, &ppolicy); + talloc_zfree(subreq); + if (ppolicy != NULL) { + DEBUG(SSSDBG_TRACE_ALL,"Found ppolicy data, " + "assuming LDAP password policies are active.\n"); + state->pw_expire_type = PWEXPIRE_LDAP_PASSWORD_POLICY; + state->pw_expire_data = ppolicy; + } + switch (ret) { + case EOK: + break; + case ETIMEDOUT: + case ERR_NETWORK_IO: + tevent_req_error(req, ERR_SERVER_FAILURE); + return; + default: + tevent_req_error(req, ret); + return; + } + + tevent_req_done(req); +} + +static errno_t +minimal_auth_recv(struct tevent_req *req, + TALLOC_CTX *memctx, + enum pwexpire *pw_expire_type, + void **pw_expire_data) +{ + struct minimal_auth_state *state = tevent_req_data(req, struct minimal_auth_state); + + if (pw_expire_data != NULL) { + *pw_expire_data = talloc_steal(memctx, state->pw_expire_data); + } + + *pw_expire_type = state->pw_expire_type; + + TEVENT_REQ_RETURN_ON_ERROR(req); + + return EOK; +} + +struct minimal_sdap_pam_auth_handler_state { + struct pam_data *pd; + struct be_ctx *be_ctx; + struct sdap_auth_ctx *auth_ctx; +}; + +static void minimal_sdap_pam_auth_handler_done(struct tevent_req *subreq); + +struct tevent_req * +minimal_sdap_pam_auth_handler_send(TALLOC_CTX *mem_ctx, + struct minimal_init_ctx *init_ctx, + struct pam_data *pd, + struct dp_req_params *params) +{ + struct sdap_auth_ctx *auth_ctx = init_ctx->auth_ctx; + struct minimal_sdap_pam_auth_handler_state *state; + struct tevent_req *subreq; + struct tevent_req *req; + + req = tevent_req_create(mem_ctx, &state, + struct minimal_sdap_pam_auth_handler_state); + if (req == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "tevent_req_create() failed\n"); + return NULL; + } + + state->pd = pd; + state->be_ctx = params->be_ctx; + state->auth_ctx = auth_ctx; + pd->pam_status = PAM_SYSTEM_ERR; + + switch (pd->cmd) { + case SSS_PAM_AUTHENTICATE: + subreq = minimal_auth_send(state, params->ev, init_ctx->fctx, auth_ctx, + pd->user, pd->authtok, false); + if (subreq == NULL) { + pd->pam_status = PAM_SYSTEM_ERR; + goto immediately; + } + + tevent_req_set_callback(subreq, minimal_sdap_pam_auth_handler_done, req); + break; + case SSS_PAM_CHAUTHTOK_PRELIM: + case SSS_PAM_CHAUTHTOK: + pd->pam_status = PAM_SYSTEM_ERR; + goto immediately; + + case SSS_PAM_ACCT_MGMT: + case SSS_PAM_SETCRED: + case SSS_PAM_OPEN_SESSION: + case SSS_PAM_CLOSE_SESSION: + pd->pam_status = PAM_SUCCESS; + goto immediately; + default: + pd->pam_status = PAM_MODULE_UNKNOWN; + goto immediately; + } + + return req; + +immediately: + /* TODO For backward compatibility we always return EOK to DP now. */ + tevent_req_done(req); + tevent_req_post(req, params->ev); + + return req; +} + +static void minimal_sdap_pam_auth_handler_done(struct tevent_req *subreq) +{ + struct minimal_sdap_pam_auth_handler_state *state; + struct tevent_req *req; + enum pwexpire pw_expire_type; + void *pw_expire_data; + const char *password; + errno_t ret; + + req = tevent_req_callback_data(subreq, struct tevent_req); + state = tevent_req_data(req, struct minimal_sdap_pam_auth_handler_state); + + ret = minimal_auth_recv(subreq, state, &pw_expire_type, &pw_expire_data); + talloc_free(subreq); + + if (ret == EOK) { + ret = check_pwexpire_policy(pw_expire_type, pw_expire_data, state->pd, + state->be_ctx->domain->pwd_expiration_warning, + state->auth_ctx->opts); + if (ret == EINVAL) { + /* Unknown password expiration type. */ + state->pd->pam_status = PAM_SYSTEM_ERR; + goto done; + } + } + + switch (ret) { + case EOK: + state->pd->pam_status = PAM_SUCCESS; + break; + case ERR_AUTH_DENIED: + state->pd->pam_status = PAM_PERM_DENIED; + break; + case ERR_AUTH_FAILED: + state->pd->pam_status = PAM_AUTH_ERR; + break; + case ETIMEDOUT: + case ERR_NETWORK_IO: + state->pd->pam_status = PAM_AUTHINFO_UNAVAIL; + be_mark_offline(state->be_ctx); + break; + case ERR_ACCOUNT_EXPIRED: + state->pd->pam_status = PAM_ACCT_EXPIRED; + break; + case ERR_PASSWORD_EXPIRED: + state->pd->pam_status = PAM_NEW_AUTHTOK_REQD; + break; + case ERR_ACCOUNT_LOCKED: + state->pd->account_locked = true; + state->pd->pam_status = PAM_PERM_DENIED; + break; + case ERR_SC_AUTH_NOT_SUPPORTED: + state->pd->pam_status = PAM_BAD_ITEM; + break; + default: + state->pd->pam_status = PAM_SYSTEM_ERR; + break; + } + + if (ret == EOK && state->be_ctx->domain->cache_credentials) { + ret = sss_authtok_get_password(state->pd->authtok, &password, NULL); + if (ret == EOK) { + ret = sysdb_cache_password(state->be_ctx->domain, state->pd->user, + password); + } + + /* password caching failures are not fatal errors */ + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Failed to cache password for %s\n", + state->pd->user); + } else { + DEBUG(SSSDBG_CONF_SETTINGS, "Password successfully cached for %s\n", + state->pd->user); + } + } + +done: + /* TODO For backward compatibility we always return EOK to DP now. */ + tevent_req_done(req); +} + +errno_t +minimal_sdap_pam_auth_handler_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + struct pam_data **_data) +{ + struct minimal_sdap_pam_auth_handler_state *state = NULL; + + state = tevent_req_data(req, struct minimal_sdap_pam_auth_handler_state); + + TEVENT_REQ_RETURN_ON_ERROR(req); + + *_data = talloc_steal(mem_ctx, state->pd); + + return EOK; +} diff --git a/src/providers/minimal/minimal_ldap_auth.h b/src/providers/minimal/minimal_ldap_auth.h new file mode 100644 index 0000000000..23b9015fed --- /dev/null +++ b/src/providers/minimal/minimal_ldap_auth.h @@ -0,0 +1,49 @@ +/* + SSSD + + minimal Identity Backend Module + + Authors: + Pavel Březina + + Copyright (C) 2026 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + + +#ifndef _MINIMAL_LDAP_AUTH_H_ +#define _MINIMAL_LDAP_AUTH_H_ + +#include "config.h" +#include +#include + +#include "providers/data_provider/dp.h" +#include "providers/ldap/ldap_common.h" +#include "util/sss_pam_data.h" +#include "providers/minimal/minimal.h" + +struct tevent_req * +minimal_sdap_pam_auth_handler_send(TALLOC_CTX *mem_ctx, + struct minimal_init_ctx *init_ctx, + struct pam_data *pd, + struct dp_req_params *params); + +errno_t +minimal_sdap_pam_auth_handler_recv(TALLOC_CTX *mem_ctx, + struct tevent_req *req, + struct pam_data **_data); + +#endif diff --git a/src/tests/cmocka/test_failover_server.c b/src/tests/cmocka/test_failover_server.c new file mode 100644 index 0000000000..842179bb46 --- /dev/null +++ b/src/tests/cmocka/test_failover_server.c @@ -0,0 +1,570 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tests/cmocka/common_mock.h" +#include "providers/failover/failover_server.h" +#include "resolv/async_resolv.h" + +static int setup(void **state) +{ + TALLOC_CTX *test_ctx = NULL; + + assert_true(leak_check_setup()); + test_ctx = talloc_new(global_talloc_context); + assert_non_null(test_ctx); + check_leaks_push(test_ctx); + + *state = test_ctx; + + return 0; +} + +static int teardown(void **state) +{ + TALLOC_CTX *test_ctx = (TALLOC_CTX*)*state; + assert_true(check_leaks_pop(test_ctx)); + talloc_free(test_ctx); + assert_true(leak_check_teardown()); + + return 0; +} + +static uint8_t *mock_ip4_addr(TALLOC_CTX *mem_ctx, const char *addr) +{ + struct in_addr net_addr; + uint8_t *buf; + int ret; + + if (addr == NULL) { + return NULL; + } + + ret = inet_pton(AF_INET, addr, &net_addr); + assert_int_equal(ret, 1); + buf = talloc_memdup(mem_ctx, &net_addr, sizeof(struct in_addr)); + assert_non_null(buf); + + return buf; +} + +static uint8_t *mock_ip6_addr(TALLOC_CTX *mem_ctx, const char *addr) +{ + struct in6_addr net_addr; + uint8_t *buf; + int ret; + + if (addr == NULL) { + return NULL; + } + + ret = inet_pton(AF_INET6, addr, &net_addr); + assert_int_equal(ret, 1); + buf = talloc_memdup(mem_ctx, &net_addr, sizeof(struct in6_addr)); + assert_non_null(buf); + + return buf; +} + +/* Test: Successfully create a failover server */ +static void test_sss_failover_server_new(void **state) +{ + TALLOC_CTX *test_ctx = (TALLOC_CTX*)*state; + struct sss_failover_server *srv; + + srv = sss_failover_server_new(test_ctx, "server.ipa.test", + "ldap://server.ipa.test", 389, 10, 100); + assert_non_null(srv); + assert_non_null(srv->name); + assert_string_equal(srv->name, "server.ipa.test"); + assert_non_null(srv->uri); + assert_string_equal(srv->uri, "ldap://server.ipa.test"); + assert_int_equal(srv->port, 389); + + assert_null(srv->addr); + assert_int_equal(srv->priority, 10); + assert_int_equal(srv->weight, 100); + + talloc_free(srv); +} + +/* Test: NULL hostname does not crash */ +static void test_sss_failover_server_new__null(void **state) +{ + TALLOC_CTX *test_ctx = (TALLOC_CTX*)*state; + struct sss_failover_server *srv; + + srv = sss_failover_server_new(test_ctx, NULL, "ldap://server.ipa.test", 389, + 10, 100); + assert_null(srv); +} + +/* Test: Successfully create a failover server with IPv4 address */ +static void test_sss_failover_server_set_address__ipv4(void **state) +{ + TALLOC_CTX *test_ctx = (TALLOC_CTX*)*state; + struct sss_failover_server *srv; + struct in_addr expected_addr; + struct sockaddr_in *sa_in; + const char *hostname = "server.ipa.test"; + const char *addr = "192.168.1.100"; + uint8_t *net_addr; + errno_t ret; + time_t now; + + net_addr = mock_ip4_addr(test_ctx, addr); + assert_non_null(net_addr); + + srv = sss_failover_server_new(test_ctx, hostname, "ldap://server.ipa.test", + 389, 10, 100); + assert_non_null(srv); + + now = time(NULL); + ret = sss_failover_server_set_address(srv, AF_INET, 10, net_addr); + assert_int_equal(ret, EOK); + + assert_non_null(srv->addr); + assert_int_equal(srv->addr->family, AF_INET); + assert_non_null(srv->addr->human); + assert_string_equal(srv->addr->human, addr); + assert_true(srv->addr->expire - now >= 10); + + ret = inet_pton(AF_INET, addr, &expected_addr); + assert_int_equal(ret, 1); + assert_non_null(srv->addr->binary); + assert_ptr_not_equal(srv->addr->binary, net_addr); + assert_memory_equal(srv->addr->binary, &expected_addr, + sizeof(struct in_addr)); + assert_int_equal(srv->addr->binary_len, sizeof(struct in_addr)); + + /* Verify sockaddr is properly set */ + assert_non_null(srv->addr->sockaddr); + assert_int_equal(srv->addr->sockaddr_len, sizeof(struct sockaddr_in)); + sa_in = (struct sockaddr_in *)srv->addr->sockaddr; + assert_int_equal(sa_in->sin_family, AF_INET); + assert_int_equal(ntohs(sa_in->sin_port), 389); + assert_memory_equal(&sa_in->sin_addr, &expected_addr, sizeof(struct in_addr)); + + talloc_free(net_addr); + talloc_free(srv); +} + +/* Test: Successfully create a failover server with IPv6 address */ +static void test_sss_failover_server_set_address__ipv6(void **state) +{ + TALLOC_CTX *test_ctx = (TALLOC_CTX*)*state; + struct sss_failover_server *srv; + struct in6_addr expected_addr; + struct sockaddr_in6 *sa_in6; + const char *hostname = "server.ipa.test"; + const char *addr = "2a00:102a:403a:c7a7:e05e:11e6:3189:3326"; + uint8_t *net_addr; + errno_t ret; + time_t now; + + net_addr = mock_ip6_addr(test_ctx, addr); + assert_non_null(net_addr); + + srv = sss_failover_server_new(test_ctx, hostname, "ldap://server.ipa.test", + 389, 10, 100); + assert_non_null(srv); + + now = time(NULL); + ret = sss_failover_server_set_address(srv, AF_INET6, 10, net_addr); + assert_int_equal(ret, EOK); + + assert_non_null(srv->addr); + assert_int_equal(srv->addr->family, AF_INET6); + assert_non_null(srv->addr->human); + assert_string_equal(srv->addr->human, addr); + assert_true(srv->addr->expire - now >= 10); + + ret = inet_pton(AF_INET6, addr, &expected_addr); + assert_int_equal(ret, 1); + assert_non_null(srv->addr->binary); + assert_ptr_not_equal(srv->addr->binary, net_addr); + assert_memory_equal(srv->addr->binary, &expected_addr, + sizeof(struct in6_addr)); + assert_int_equal(srv->addr->binary_len, sizeof(struct in6_addr)); + + /* Verify sockaddr is properly set */ + assert_non_null(srv->addr->sockaddr); + assert_int_equal(srv->addr->sockaddr_len, sizeof(struct sockaddr_in6)); + sa_in6 = (struct sockaddr_in6 *)srv->addr->sockaddr; + assert_int_equal(sa_in6->sin6_family, AF_INET6); + assert_int_equal(ntohs(sa_in6->sin6_port), 389); + assert_memory_equal(&sa_in6->sin6_addr, &expected_addr, sizeof(struct in6_addr)); + + talloc_free(net_addr); + talloc_free(srv); +} + +/* Test: Error out if invalid family is given */ +static void test_sss_failover_server_set_address__invalid_family(void **state) +{ + TALLOC_CTX *test_ctx = (TALLOC_CTX*)*state; + struct sss_failover_server *srv; + const char *hostname = "server.ipa.test"; + const char *addr = "192.168.1.100"; + uint8_t *net_addr; + errno_t ret; + + net_addr = mock_ip4_addr(test_ctx, addr); + assert_non_null(net_addr); + + srv = sss_failover_server_new(test_ctx, hostname, "ldap://server.ipa.test", + 389, 10, 100); + assert_non_null(srv); + + ret = sss_failover_server_set_address(srv, AF_UNIX, 10, net_addr); + assert_int_equal(ret, EINVAL); + + talloc_free(net_addr); + talloc_free(srv); +} + +/* Test: Error out if invalid address is given */ +static void test_sss_failover_server_set_address__null_addr(void **state) +{ + TALLOC_CTX *test_ctx = (TALLOC_CTX*)*state; + struct sss_failover_server *srv; + const char *hostname = "server.ipa.test"; + errno_t ret; + + srv = sss_failover_server_new(test_ctx, hostname, "ldap://server.ipa.test", + 389, 10, 100); + assert_non_null(srv); + + ret = sss_failover_server_set_address(srv, AF_INET, 10, NULL); + assert_int_equal(ret, EINVAL); + + talloc_free(srv); +} + +/* Test: Successfully clone a failover server with IPv4 address */ +static void test_sss_failover_server_clone__ipv4(void **state) +{ + TALLOC_CTX *test_ctx = (TALLOC_CTX*)*state; + struct sss_failover_server *srv; + struct sss_failover_server *clone; + const char *hostname = "server.ipa.test"; + const char *addr = "192.168.1.100"; + uint8_t *net_addr; + errno_t ret; + + net_addr = mock_ip4_addr(test_ctx, addr); + assert_non_null(net_addr); + + srv = sss_failover_server_new(test_ctx, hostname, "ldap://server.ipa.test", + 389, 10, 100); + assert_non_null(srv); + + ret = sss_failover_server_set_address(srv, AF_INET, 10, net_addr); + assert_int_equal(ret, EOK); + + clone = sss_failover_server_clone(test_ctx, srv); + assert_non_null(clone); + + /* Verify name is cloned */ + assert_non_null(clone->name); + assert_string_equal(clone->name, srv->name); + + /* Verify priority and weight are cloned */ + assert_int_equal(clone->priority, srv->priority); + assert_int_equal(clone->weight, srv->weight); + + /* Verify address is cloned */ + assert_non_null(clone->addr); + assert_int_equal(clone->addr->family, srv->addr->family); + assert_int_equal(clone->addr->expire, srv->addr->expire); + assert_non_null(clone->addr->human); + assert_string_equal(clone->addr->human, srv->addr->human); + assert_non_null(clone->addr->binary); + assert_memory_equal(clone->addr->binary, srv->addr->binary, + sizeof(struct in_addr)); + assert_int_equal(clone->addr->binary_len, srv->addr->binary_len); + assert_non_null(clone->addr->sockaddr); + assert_int_equal(clone->addr->sockaddr_len, srv->addr->sockaddr_len); + + /* Verify clone is independent (different memory) */ + assert_ptr_not_equal(clone, srv); + assert_ptr_not_equal(clone->name, srv->name); + assert_ptr_not_equal(clone->addr, srv->addr); + assert_ptr_not_equal(clone->addr->binary, srv->addr->binary); + assert_ptr_not_equal(clone->addr->human, srv->addr->human); + assert_ptr_not_equal(clone->addr->sockaddr, srv->addr->sockaddr); + + talloc_free(net_addr); + talloc_free(srv); + talloc_free(clone); +} + +/* Test: Successfully clone a failover server with IPv6 address */ +static void test_sss_failover_server_clone__ipv6(void **state) +{ + TALLOC_CTX *test_ctx = (TALLOC_CTX*)*state; + struct sss_failover_server *srv; + struct sss_failover_server *clone; + const char *hostname = "server1.ipa.test"; + const char *addr = "2a00:102a:403a:c7a7:e05e:11e6:3189:3326"; + uint8_t *net_addr; + errno_t ret; + + net_addr = mock_ip6_addr(test_ctx, addr); + assert_non_null(net_addr); + + srv = sss_failover_server_new(test_ctx, hostname, "ldap://server1.ipa.test", + 389, 10, 100); + assert_non_null(srv); + + ret = sss_failover_server_set_address(srv, AF_INET6, 10, net_addr); + assert_int_equal(ret, EOK); + + clone = sss_failover_server_clone(test_ctx, srv); + assert_non_null(clone); + + /* Verify name is cloned */ + assert_non_null(clone->name); + assert_string_equal(clone->name, srv->name); + + /* Verify priority and weight are cloned */ + assert_int_equal(clone->priority, srv->priority); + assert_int_equal(clone->weight, srv->weight); + + /* Verify address is cloned */ + assert_non_null(clone->addr); + assert_int_equal(clone->addr->family, srv->addr->family); + assert_int_equal(clone->addr->expire, srv->addr->expire); + assert_non_null(clone->addr->human); + assert_string_equal(clone->addr->human, srv->addr->human); + assert_non_null(clone->addr->binary); + assert_memory_equal(clone->addr->binary, srv->addr->binary, + sizeof(struct in6_addr)); + assert_int_equal(clone->addr->binary_len, srv->addr->binary_len); + assert_non_null(clone->addr->sockaddr); + assert_int_equal(clone->addr->sockaddr_len, srv->addr->sockaddr_len); + + /* Verify clone is independent (different memory) */ + assert_ptr_not_equal(clone, srv); + assert_ptr_not_equal(clone->name, srv->name); + assert_ptr_not_equal(clone->addr, srv->addr); + assert_ptr_not_equal(clone->addr->binary, srv->addr->binary); + assert_ptr_not_equal(clone->addr->human, srv->addr->human); + assert_ptr_not_equal(clone->addr->sockaddr, srv->addr->sockaddr); + + talloc_free(net_addr); + talloc_free(srv); + talloc_free(clone); +} + +/* Test: Successfully clone a failover server with empty address */ +static void test_sss_failover_server_clone__null_addr(void **state) +{ + TALLOC_CTX *test_ctx = (TALLOC_CTX*)*state; + struct sss_failover_server *srv; + struct sss_failover_server *clone; + const char *hostname = "server.ipa.test"; + + srv = sss_failover_server_new(test_ctx, hostname, "ldap://server.ipa.test", + 389, 10, 100); + assert_non_null(srv); + + clone = sss_failover_server_clone(test_ctx, srv); + assert_non_null(clone); + + /* Verify name is cloned */ + assert_non_null(clone->name); + assert_string_equal(clone->name, srv->name); + + /* Verify priority and weight are cloned */ + assert_int_equal(clone->priority, srv->priority); + assert_int_equal(clone->weight, srv->weight); + + /* Verify address is cloned */ + assert_null(clone->addr); + + /* Verify clone is independent (different memory) */ + assert_ptr_not_equal(clone, srv); + assert_ptr_not_equal(clone->name, srv->name); + + talloc_free(srv); + talloc_free(clone); +} + +/* Test: Fail when cloning NULL server */ +static void test_sss_failover_server_clone__null_server(void **state) +{ + TALLOC_CTX *test_ctx = (TALLOC_CTX*)*state; + struct sss_failover_server *clone; + + clone = sss_failover_server_clone(test_ctx, NULL); + assert_null(clone); +} + +/* Test: Server state transitions */ +static void test_sss_failover_server_state_management(void **state) +{ + TALLOC_CTX *test_ctx = (TALLOC_CTX*)*state; + struct sss_failover_server *srv; + + srv = sss_failover_server_new(test_ctx, "server.ipa.test", + "ldap://server.ipa.test", 389, 10, 100); + assert_non_null(srv); + + /* Initial state should be UNKNOWN */ + assert_int_equal(srv->state, SSS_FAILOVER_SERVER_STATE_UNKNOWN); + assert_true(sss_failover_server_maybe_working(srv)); + + /* Mark as reachable */ + sss_failover_server_mark_reachable(srv); + assert_int_equal(srv->state, SSS_FAILOVER_SERVER_STATE_REACHABLE); + assert_true(sss_failover_server_maybe_working(srv)); + + /* Mark as working */ + sss_failover_server_mark_working(srv); + assert_int_equal(srv->state, SSS_FAILOVER_SERVER_STATE_WORKING); + assert_true(sss_failover_server_maybe_working(srv)); + + /* Mark as offline */ + sss_failover_server_mark_offline(srv); + assert_int_equal(srv->state, SSS_FAILOVER_SERVER_STATE_OFFLINE); + assert_false(sss_failover_server_maybe_working(srv)); + + /* Mark as unknown again */ + sss_failover_server_mark_unknown(srv); + assert_int_equal(srv->state, SSS_FAILOVER_SERVER_STATE_UNKNOWN); + assert_true(sss_failover_server_maybe_working(srv)); + + /* Mark as resolver error */ + sss_failover_server_mark_resolver_error(srv); + assert_int_equal(srv->state, SSS_FAILOVER_SERVER_STATE_RESOLVER_ERROR); + assert_false(sss_failover_server_maybe_working(srv)); + + talloc_free(srv); +} + +/* Test: Compare two equal servers */ +static void test_sss_failover_server_equal__same(void **state) +{ + TALLOC_CTX *test_ctx = (TALLOC_CTX*)*state; + struct sss_failover_server *srv1; + struct sss_failover_server *srv2; + + srv1 = sss_failover_server_new(test_ctx, "server.ipa.test", + "ldap://server.ipa.test", 389, 10, 100); + assert_non_null(srv1); + + srv2 = sss_failover_server_new(test_ctx, "server.ipa.test", + "ldap://server.ipa.test", 389, 20, 200); + assert_non_null(srv2); + + /* Should be equal (only name and port matter) */ + assert_true(sss_failover_server_equal(srv1, srv2)); + + talloc_free(srv1); + talloc_free(srv2); +} + +/* Test: Compare two servers with different names */ +static void test_sss_failover_server_equal__different_name(void **state) +{ + TALLOC_CTX *test_ctx = (TALLOC_CTX*)*state; + struct sss_failover_server *srv1; + struct sss_failover_server *srv2; + + srv1 = sss_failover_server_new(test_ctx, "server1.ipa.test", + "ldap://server1.ipa.test", 389, 10, 100); + assert_non_null(srv1); + + srv2 = sss_failover_server_new(test_ctx, "server2.ipa.test", + "ldap://server2.ipa.test", 389, 10, 100); + assert_non_null(srv2); + + /* Should not be equal (different names) */ + assert_false(sss_failover_server_equal(srv1, srv2)); + + talloc_free(srv1); + talloc_free(srv2); +} + +/* Test: Compare two servers with different ports */ +static void +test_sss_failover_server_equal__different_port(void **state) +{ + TALLOC_CTX *test_ctx = (TALLOC_CTX *)*state; + struct sss_failover_server *srv1; + struct sss_failover_server *srv2; + + srv1 = sss_failover_server_new(test_ctx, "server.ipa.test", + "ldap://server.ipa.test", 389, 10, 100); + assert_non_null(srv1); + + srv2 = sss_failover_server_new(test_ctx, "server.ipa.test", + "ldaps://server.ipa.test", 636, 10, 100); + assert_non_null(srv2); + + /* Should not be equal (different ports) */ + assert_false(sss_failover_server_equal(srv1, srv2)); + + talloc_free(srv1); + talloc_free(srv2); +} + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(test_sss_failover_server_new, + setup, teardown), + cmocka_unit_test_setup_teardown(test_sss_failover_server_new__null, + setup, teardown), + cmocka_unit_test_setup_teardown(test_sss_failover_server_set_address__ipv4, + setup, teardown), + cmocka_unit_test_setup_teardown(test_sss_failover_server_set_address__ipv6, + setup, teardown), + cmocka_unit_test_setup_teardown(test_sss_failover_server_set_address__invalid_family, + setup, teardown), + cmocka_unit_test_setup_teardown(test_sss_failover_server_set_address__null_addr, + setup, teardown), + cmocka_unit_test_setup_teardown(test_sss_failover_server_clone__ipv4, + setup, teardown), + cmocka_unit_test_setup_teardown(test_sss_failover_server_clone__ipv6, + setup, teardown), + cmocka_unit_test_setup_teardown(test_sss_failover_server_clone__null_addr, + setup, teardown), + cmocka_unit_test_setup_teardown(test_sss_failover_server_clone__null_server, + setup, teardown), + cmocka_unit_test_setup_teardown(test_sss_failover_server_state_management, + setup, teardown), + cmocka_unit_test_setup_teardown(test_sss_failover_server_equal__same, + setup, teardown), + cmocka_unit_test_setup_teardown(test_sss_failover_server_equal__different_name, + setup, teardown), + cmocka_unit_test_setup_teardown(test_sss_failover_server_equal__different_port, + setup, teardown) + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/src/tests/dlopen-tests.c b/src/tests/dlopen-tests.c index ceaaedc5d5..878f8767fc 100644 --- a/src/tests/dlopen-tests.c +++ b/src/tests/dlopen-tests.c @@ -111,6 +111,8 @@ struct so { { "libsss_idp.so", { LIBPFX"libdlopen_test_providers.so", LIBPFX"libsss_idp.so", NULL } }, #endif /* BUILD_ID_PROVIDER_IDP */ + { "libsss_minimal.so", { LIBPFX"libdlopen_test_providers.so", + LIBPFX"libsss_minimal.so", NULL } }, #ifdef HAVE_PYTHON2_BINDINGS { "_py2hbac.so", { LIBPFX"_py2hbac.so", NULL } }, { "_py2sss.so", { LIBPFX"_py2sss.so", NULL } }, diff --git a/src/util/typeof.h b/src/util/typeof.h new file mode 100644 index 0000000000..89f499d560 --- /dev/null +++ b/src/util/typeof.h @@ -0,0 +1,50 @@ +/* + Copyright (C) 2025 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef _SSS_TYPEOF_H_ +#define _SSS_TYPEOF_H_ + +/** + * Provide a compile-time type safety for callbacks and handlers. + * + * We use GCC __typeof__ extension to achieve this. We retrieve the private + * data type and create the expected handler function type with it. If the + * method accepts parsed D-Bus arguments, they are appended with variadic + * parameters. We check that the handler type matches the expected type + * and return the sbus_handler structure value. + * + * We also use __attribute__((unused)) to suppress compiler warning about + * unused __fn. + * + * We do not perform this check on platforms where this extension is not + * available and just create a generic handler. This does not matter since + * we test compilation with GCC anyway. + */ +#if (__GNUC__ >= 3) + +#define SSS_CHECK_FUNCTION_TYPE(fn, return_type, ...) ({ \ + __attribute__((unused)) return_type (*__fn)(__VA_ARGS__) = (fn); \ +}) + +#define SSS_TYPEOF(data) __typeof__(data) + +#else +#define SSS_CHECK_FUNCTION_TYPE(handler, return_type, ...) +#define SSS_TYPEOF(data) (void*) +#endif + +#endif /* _SSS_TYPEOF_H_ */ diff --git a/src/util/util.h b/src/util/util.h index ccf0b30ab7..a7cb9fdad6 100644 --- a/src/util/util.h +++ b/src/util/util.h @@ -141,25 +141,39 @@ enum sssd_exit_status { #endif #endif -#define TEVENT_REQ_RETURN_ON_ERROR(req) do { \ - enum tevent_req_state TRROEstate; \ - uint64_t TRROEuint64; \ - errno_t TRROEerr; \ +#define TEVENT_REQ_ERROR_TO_ERRNO(req) ({ \ + enum tevent_req_state __TRETE_state; \ + uint64_t __TRETE_uint64; \ + errno_t __TRETE_err; \ + errno_t __TRETE_ret = EOK; \ \ - if (tevent_req_is_error(req, &TRROEstate, &TRROEuint64)) { \ - TRROEerr = (errno_t)TRROEuint64; \ - switch (TRROEstate) { \ + if (tevent_req_is_error(req, &__TRETE_state, &__TRETE_uint64)) { \ + __TRETE_err = (errno_t)__TRETE_uint64; \ + switch (__TRETE_state) { \ case TEVENT_REQ_USER_ERROR: \ - if (TRROEerr == 0) { \ - return ERR_INTERNAL; \ + if (__TRETE_err == 0) { \ + __TRETE_ret = ERR_INTERNAL; \ } \ - return TRROEerr; \ + __TRETE_ret = __TRETE_err; \ + break; \ case TEVENT_REQ_TIMED_OUT: \ - return ETIMEDOUT; \ + __TRETE_ret = ETIMEDOUT; \ + break; \ + case TEVENT_REQ_NO_MEMORY: \ + __TRETE_ret = ENOMEM; \ + break; \ default: \ - return ERR_INTERNAL; \ + __TRETE_ret = ERR_INTERNAL; \ } \ } \ + __TRETE_ret; \ +}) + +#define TEVENT_REQ_RETURN_ON_ERROR(req) do { \ + errno_t TRROEret = TEVENT_REQ_ERROR_TO_ERRNO(req); \ + if (TRROEret != EOK) { \ + return TRROEret; \ + } \ } while (0) #define OUT_OF_ID_RANGE(id, min, max) \ diff --git a/src/util/util_errors.c b/src/util/util_errors.c index 48badb914d..21eb43f878 100644 --- a/src/util/util_errors.c +++ b/src/util/util_errors.c @@ -157,6 +157,7 @@ struct err_string error_to_str[] = { { "Certificate authority file not found"}, /* ERR_CA_DB_NOT_FOUND */ { "Server failure"}, /* ERR_SERVER_FAILURE */ + { "No more servers to try"}, /* ERR_NO_MORE_SERVERS */ { "ERR_LAST" } /* ERR_LAST */ }; diff --git a/src/util/util_errors.h b/src/util/util_errors.h index 244ade6334..aa73626343 100644 --- a/src/util/util_errors.h +++ b/src/util/util_errors.h @@ -182,6 +182,7 @@ enum sssd_errors { ERR_CA_DB_NOT_FOUND, ERR_SERVER_FAILURE, + ERR_NO_MORE_SERVERS, ERR_LAST /* ALWAYS LAST */ };