From: Andreas Schneider Date: Wed, 20 Nov 2024 17:17:29 +0000 (+0100) Subject: Add support for systemd socket activation X-Git-Tag: krb5-1.22-beta1~29 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=refs%2Fpull%2F1400%2Fhead;p=thirdparty%2Fkrb5.git Add support for systemd socket activation If LISTEN_PID and LISTEN_FDS are set in the environment, expect listener sockets to be present at fds starting at 3. If any match a configured listener address and port, use it instead of creating a new socket. [ghudson@mit.edu: combined two commits; changed sa_compare() to sa_equal(); restructured changes to fetch listener sockets into a list once and to match on socket type as well as address; added tests] ticket: 9157 (new) --- diff --git a/.gitignore b/.gitignore index a7a217a6f3..ae6780c37b 100644 --- a/.gitignore +++ b/.gitignore @@ -258,6 +258,7 @@ local.properties /src/kdc/rtest /src/kdc/t_ndr /src/kdc/t_replay +/src/kdc/t_sockact /src/lib/k5sprt32.def diff --git a/doc/admin/admin_commands/kadmind.rst b/doc/admin/admin_commands/kadmind.rst index 7e1482635d..bc66890def 100644 --- a/doc/admin/admin_commands/kadmind.rst +++ b/doc/admin/admin_commands/kadmind.rst @@ -121,6 +121,14 @@ ENVIRONMENT See :ref:`kerberos(7)` for a description of Kerberos environment variables. +As of release 1.22, kadmind supports systemd socket activation via the +LISTEN_PID and LISTEN_FDS environment variables. Sockets provided by +the caller must correspond to configured listener addresses (via the +**kadmind_listen** or **kpasswd_listen** variables or equivalents) or +they will be ignored. Any configured listener addresses that do not +correspond to caller-provided sockets will be ignored if socket +activation is used. + SEE ALSO -------- diff --git a/doc/admin/admin_commands/krb5kdc.rst b/doc/admin/admin_commands/krb5kdc.rst index 631a0de84e..97fbe5ed7d 100644 --- a/doc/admin/admin_commands/krb5kdc.rst +++ b/doc/admin/admin_commands/krb5kdc.rst @@ -106,6 +106,13 @@ ENVIRONMENT See :ref:`kerberos(7)` for a description of Kerberos environment variables. +As of release 1.22, krb5kdc supports systemd socket activation via the +LISTEN_PID and LISTEN_FDS environment variables. Sockets provided by +the caller must correspond to configured listener addresses (via the +**kdc_listen** variable or equivalent) or they will be ignored. Any +configured listener addresses that do not correspond to +caller-provided sockets will be ignored if socket activation is used. + SEE ALSO -------- diff --git a/src/include/socket-utils.h b/src/include/socket-utils.h index 177662c874..02c10ec02b 100644 --- a/src/include/socket-utils.h +++ b/src/include/socket-utils.h @@ -43,6 +43,8 @@ #ifndef SOCKET_UTILS_H #define SOCKET_UTILS_H +#include + /* Some useful stuff cross-platform for manipulating socket addresses. We assume at least ipv4 sockaddr_in support. The sockaddr_storage stuff comes from the ipv6 socket api enhancements; socklen_t is @@ -159,4 +161,37 @@ sa_socklen(const struct sockaddr *sa) abort(); } +/* Return true if a and b are the same address (and port if applicable). */ +static inline bool +sa_equal(const struct sockaddr *a, const struct sockaddr *b) +{ + if (a == NULL || b == NULL || a->sa_family != b->sa_family) + return false; + + if (a->sa_family == AF_INET) { + const struct sockaddr_in *x = sa2sin(a); + const struct sockaddr_in *y = sa2sin(b); + + if (x->sin_port != y->sin_port) + return false; + return memcmp(&x->sin_addr, &y->sin_addr, sizeof(x->sin_addr)) == 0; + } else if (a->sa_family == AF_INET6) { + const struct sockaddr_in6 *x = sa2sin6(a); + const struct sockaddr_in6 *y = sa2sin6(b); + + if (x->sin6_port != y->sin6_port) + return false; + return memcmp(&x->sin6_addr, &y->sin6_addr, sizeof(x->sin6_addr)) == 0; +#ifndef _WIN32 + } else if (a->sa_family == AF_UNIX) { + const struct sockaddr_un *x = sa2sun(a); + const struct sockaddr_un *y = sa2sun(b); + + return strcmp(x->sun_path, y->sun_path) == 0; +#endif + } + + return false; +} + #endif /* SOCKET_UTILS_H */ diff --git a/src/kdc/Makefile.in b/src/kdc/Makefile.in index 7199b3472d..bf4d9b5808 100644 --- a/src/kdc/Makefile.in +++ b/src/kdc/Makefile.in @@ -31,7 +31,8 @@ SRCS= \ EXTRADEPSRCS= \ $(srcdir)/t_ndr.c \ - $(srcdir)/t_replay.c + $(srcdir)/t_replay.c \ + $(srcdir)/t_sockact.c OBJS= \ authind.o \ @@ -78,16 +79,21 @@ T_REPLAY_OBJS=t_replay.o t_replay: $(T_REPLAY_OBJS) replay.o $(KRB5_BASE_DEPLIBS) $(CC_LINK) -o $@ $(T_REPLAY_OBJS) $(CMOCKA_LIBS) $(KRB5_BASE_LIBS) +t_sockact: t_sockact.o $(KRB5_BASE_DEPLIBS) + $(CC_LINK) -o $@ t_sockact.o $(KRB5_BASE_LIBS) + check-cmocka: t_replay $(RUN_TEST) ./t_replay > /dev/null -check-pytests: +check-pytests: t_sockact $(RUNPYTEST) $(srcdir)/t_workers.py $(PYTESTFLAGS) $(RUNPYTEST) $(srcdir)/t_emptytgt.py $(PYTESTFLAGS) $(RUNPYTEST) $(srcdir)/t_bigreply.py $(PYTESTFLAGS) + $(RUNPYTEST) $(srcdir)/t_sockact.py $(PYTESTFLAGS) install: $(INSTALL_PROGRAM) krb5kdc ${DESTDIR}$(SERVER_BINDIR)/krb5kdc clean: $(RM) krb5kdc rtest.o rtest t_replay.o t_replay t_ndr.o t_ndr + $(RM) t_sockact.o t_sockact diff --git a/src/kdc/deps b/src/kdc/deps index 2d54fa93df..86be4c475f 100644 --- a/src/kdc/deps +++ b/src/kdc/deps @@ -427,3 +427,13 @@ $(OUTPRE)t_replay.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \ $(top_srcdir)/include/port-sockets.h $(top_srcdir)/include/socket-utils.h \ extern.h kdc_util.h realm_data.h replay.c reqstate.h \ t_replay.c +$(OUTPRE)t_sockact.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \ + $(BUILDTOP)/include/krb5/krb5.h $(BUILDTOP)/include/osconf.h \ + $(BUILDTOP)/include/profile.h $(COM_ERR_DEPS) $(top_srcdir)/include/k5-buf.h \ + $(top_srcdir)/include/k5-err.h $(top_srcdir)/include/k5-gmt_mktime.h \ + $(top_srcdir)/include/k5-int-pkinit.h $(top_srcdir)/include/k5-int.h \ + $(top_srcdir)/include/k5-platform.h $(top_srcdir)/include/k5-plugin.h \ + $(top_srcdir)/include/k5-thread.h $(top_srcdir)/include/k5-trace.h \ + $(top_srcdir)/include/krb5.h $(top_srcdir)/include/krb5/authdata_plugin.h \ + $(top_srcdir)/include/krb5/plugin.h $(top_srcdir)/include/port-sockets.h \ + $(top_srcdir)/include/socket-utils.h t_sockact.c diff --git a/src/kdc/t_sockact.c b/src/kdc/t_sockact.c new file mode 100644 index 0000000000..cb4a4bc7a9 --- /dev/null +++ b/src/kdc/t_sockact.c @@ -0,0 +1,121 @@ +/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* kdc/t_sockact.c - socket activation test harness */ +/* + * Copyright (C) 2025 by the Massachusetts Institute of Technology. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Usage: t_sockact address... -- program args... + * + * This program simulates systemd socket activation by creating one or more + * listener sockets at the specified addresses, setting LISTEN_FDS and + * LISTEN_PID in the environment, and executing the specified command. (The + * real systemd would not execute the program until there is input on one of + * the listener sockets, but we do not need to simulate that, and executing the + * command immediately allow easier integration with k5test.py.) + */ + +#include "k5-int.h" +#include "socket-utils.h" + +static int max_fd; + +static void +create_socket(const struct sockaddr *addr) +{ + int fd, one = 1; + + fd = socket(addr->sa_family, SOCK_STREAM, 0); + if (fd < 0) + abort(); + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) != 0) + abort(); +#ifdef SO_REUSEPORT + if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)) != 0) + abort(); +#endif +#if defined(IPV6_V6ONLY) + if (addr->sa_family == AF_INET6) { + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &one, sizeof(one)) != 0) + abort(); + } +#endif + if (bind(fd, addr, sa_socklen(addr)) != 0) + abort(); + if (listen(fd, 5) != 0) + abort(); + max_fd = fd; +} + +int +main(int argc, char **argv) +{ + const char *addrstr; + struct sockaddr_storage ss = { 0 }; + struct addrinfo hints = { 0 }, *ai_list = NULL, *ai = NULL; + char *host, nbuf[128]; + int i, port; + + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "--") == 0) + break; + addrstr = argv[i]; + if (*addrstr == '/') { + ss.ss_family = AF_UNIX; + strlcpy(ss2sun(&ss)->sun_path, addrstr, + sizeof(ss2sun(&ss)->sun_path)); + create_socket(ss2sa(&ss)); + } else { + if (k5_parse_host_string(addrstr, 0, &host, &port) != 0 || !port) + abort(); + hints.ai_socktype = SOCK_STREAM; + hints.ai_family = AF_UNSPEC; + hints.ai_flags = AI_PASSIVE; +#ifdef AI_NUMERICSERV + hints.ai_flags |= AI_NUMERICSERV; +#endif + (void)snprintf(nbuf, sizeof(nbuf), "%d", port); + if (getaddrinfo(host, nbuf, &hints, &ai_list) != 0) + abort(); + for (ai = ai_list; ai != NULL; ai = ai->ai_next) + create_socket(ai->ai_addr); + freeaddrinfo(ai_list); + free(host); + } + } + argv += i + 1; + + (void)snprintf(nbuf, sizeof(nbuf), "%d", max_fd - 2); + setenv("LISTEN_FDS", nbuf, 1); + (void)snprintf(nbuf, sizeof(nbuf), "%lu", (unsigned long)getpid()); + setenv("LISTEN_PID", nbuf, 1); + execv(argv[0], argv); + abort(); + return 1; +} diff --git a/src/kdc/t_sockact.py b/src/kdc/t_sockact.py new file mode 100644 index 0000000000..2a4aeb9edf --- /dev/null +++ b/src/kdc/t_sockact.py @@ -0,0 +1,43 @@ +from k5test import * + +if not which('systemd-socket-activate'): + skip_rest('socket activation tests', 'systemd-socket-activate not found') + +# Configure listeners for two UNIX domain sockets and two ports. +kdc_conf = {'realms': {'$realm': { + 'kdc_listen': '$testdir/sock1 $testdir/sock2', + 'kdc_tcp_listen': '$port7 $port8'}}} +realm = K5Realm(kdc_conf=kdc_conf, start_kdc=False) + +# Create socket activation fds for just one of the UNIX domain sockets +# and one of the ports. +realm.start_server(['./t_sockact', os.path.join(realm.testdir, 'sock1'), + str(realm.portbase + 8), '--', krb5kdc, '-n'], + 'starting...') + +mark('UNIX socket 1') +cconf1 = {'realms': {'$realm': {'kdc': '$testdir/sock1'}}} +env1 = realm.special_env('sock1', False, krb5_conf=cconf1) +realm.kinit(realm.user_princ, password('user'), env=env1) + +mark('port8') +cconf2 = {'realms': {'$realm': {'kdc': '$hostname:$port8'}}} +env2 = realm.special_env('sock1', False, krb5_conf=cconf2) +realm.kinit(realm.user_princ, password('user'), env=env2) + +# Test that configured listener addresses are ignored if they don't +# match caller-provided sockets. + +mark('UNIX socket 2') +cconf3 = {'realms': {'$realm': {'kdc': '$testdir/sock2'}}} +env3 = realm.special_env('sock2', False, krb5_conf=cconf3) +realm.kinit(realm.user_princ, password('user'), env=env3, expected_code=1, + expected_msg='Cannot contact any KDC') + +mark('port7') +cconf4 = {'realms': {'$realm': {'kdc': '$hostname:$port7'}}} +env4 = realm.special_env('sock1', False, krb5_conf=cconf4) +realm.kinit(realm.user_princ, password('user'), env=env3, expected_code=1, + expected_msg='Cannot contact any KDC') + +success('systemd socket activation tests') diff --git a/src/lib/apputils/net-server.c b/src/lib/apputils/net-server.c index 6fa8a97e03..c8c83606d6 100644 --- a/src/lib/apputils/net-server.c +++ b/src/lib/apputils/net-server.c @@ -65,6 +65,19 @@ #include "udppktinfo.h" +/* List of systemd socket activation addresses and socket types. */ +struct sockact_list { + size_t nsockets; + struct { + struct sockaddr_storage addr; + int type; + } *fds; +}; + +/* When systemd socket activation is used, caller-provided sockets begin at + * file descriptor 3. */ +const int SOCKACT_START = 3; + /* XXX */ #define KDC5_NONET (-1779992062L) @@ -694,6 +707,82 @@ static const enum conn_type bind_conn_types[] = [UNX] = CONN_UNIXSOCK_LISTENER }; +/* If any systemd socket activation fds are indicated by the environment, set + * them close-on-exec and put their addresses and socket types into *list. */ +static void +init_sockact_list(struct sockact_list *list) +{ + const char *v; + char *end; + long lpid; + int fd; + size_t nfds, i; + socklen_t slen; + + list->nsockets = 0; + list->fds = NULL; + + /* Check if LISTEN_FDS is meant for this process. */ + v = getenv("LISTEN_PID"); + if (v == NULL) + return; + lpid = strtol(v, &end, 10); + if (end == NULL || end == v || *end != '\0' || lpid != getpid()) + return; + + /* Get the number of activated sockets. */ + v = getenv("LISTEN_FDS"); + if (v == NULL) + return; + nfds = strtoul(v, &end, 10); + if (end == NULL || end == v || *end != '\0') + return; + if (nfds == 0 || nfds > (size_t)INT_MAX - SOCKACT_START) + return; + + list->fds = calloc(nfds, sizeof(*list->fds)); + if (list->fds == NULL) + return; + + for (i = 0; i < nfds; i++) { + fd = i + SOCKACT_START; + set_cloexec_fd(fd); + slen = sizeof(list->fds[i].addr); + (void)getsockname(fd, ss2sa(&list->fds[i].addr), &slen); + slen = sizeof(list->fds[i].type); + (void)getsockopt(fd, SOL_SOCKET, SO_TYPE, &list->fds[i].type, &slen); + } + + list->nsockets = nfds; +} + +/* Release any storage used by *list. */ +static void +fini_sockact_list(struct sockact_list *list) +{ + free(list->fds); + list->fds = NULL; + list->nsockets = 0; +} + +/* If sa matches an address in *list, return the associated file descriptor and + * clear the address from *list. Otherwise return -1. */ +static int +find_sockact(struct sockact_list *list, const struct sockaddr *sa, int type) +{ + size_t i; + + for (i = 0; i < list->nsockets; i++) { + if (list->fds[i].type == type && + sa_equal(ss2sa(&list->fds[i].addr), sa)) { + list->fds[i].type = -1; + memset(&list->fds[i].addr, 0, sizeof(list->fds[i].addr)); + return i + SOCKACT_START; + } + } + return -1; +} + /* * Set up a listening socket. * @@ -708,8 +797,9 @@ static const enum conn_type bind_conn_types[] = */ static krb5_error_code setup_socket(struct bind_address *ba, struct sockaddr *sock_address, - void *handle, const char *prog, verto_ctx *ctx, - int listen_backlog, verto_callback vcb, enum conn_type ctype) + struct sockact_list *sockacts, void *handle, const char *prog, + verto_ctx *ctx, int listen_backlog, verto_callback vcb, + enum conn_type ctype) { krb5_error_code ret; struct connection *conn; @@ -722,20 +812,31 @@ setup_socket(struct bind_address *ba, struct sockaddr *sock_address, krb5_klog_syslog(LOG_DEBUG, _("Setting up %s socket for address %s"), bind_type_names[ba->type], addrbuf); - /* Create the socket. */ - ret = create_server_socket(sock_address, bind_socktypes[ba->type], prog, - &sock); - if (ret) - goto cleanup; + if (sockacts->nsockets > 0) { + /* Look for a systemd socket activation fd matching sock_address. */ + sock = find_sockact(sockacts, sock_address, bind_socktypes[ba->type]); + if (sock == -1) { + /* Ignore configured addresses that don't match any caller-provided + * sockets. */ + ret = 0; + goto cleanup; + } + } else { + /* We're not using socket activation; create the socket. */ + ret = create_server_socket(sock_address, bind_socktypes[ba->type], + prog, &sock); + if (ret) + goto cleanup; - /* Listen for backlogged connections on stream sockets. (For RPC sockets - * this will be done by svc_register().) */ - if ((ba->type == TCP || ba->type == UNX) && - listen(sock, listen_backlog) != 0) { - ret = errno; - com_err(prog, errno, _("Cannot listen on %s server socket on %s"), - bind_type_names[ba->type], addrbuf); - goto cleanup; + /* Listen for backlogged connections on stream sockets. (For RPC + * sockets this will be done by svc_register().) */ + if ((ba->type == TCP || ba->type == UNX) && + listen(sock, listen_backlog) != 0) { + ret = errno; + com_err(prog, errno, _("Cannot listen on %s server socket on %s"), + bind_type_names[ba->type], addrbuf); + goto cleanup; + } } /* Set non-blocking I/O for non-RPC listener sockets. */ @@ -837,6 +938,7 @@ setup_addresses(verto_ctx *ctx, void *handle, const char *prog, struct bind_address addr; struct sockaddr_un sun; struct addrinfo hints, *ai_list = NULL, *ai = NULL; + struct sockact_list sockacts = { 0 }; verto_callback vcb; char addrbuf[128]; @@ -855,6 +957,8 @@ setup_addresses(verto_ctx *ctx, void *handle, const char *prog, hints.ai_flags |= AI_NUMERICSERV; #endif + init_sockact_list(&sockacts); + /* Add all the requested addresses. */ for (i = 0; i < bind_addresses.n; i++) { addr = bind_addresses.data[i]; @@ -870,8 +974,9 @@ setup_addresses(verto_ctx *ctx, void *handle, const char *prog, addr.address); goto cleanup; } - ret = setup_socket(&addr, (struct sockaddr *)&sun, handle, prog, - ctx, listen_backlog, verto_callbacks[addr.type], + ret = setup_socket(&addr, (struct sockaddr *)&sun, &sockacts, + handle, prog, ctx, listen_backlog, + verto_callbacks[addr.type], bind_conn_types[addr.type]); if (ret) { krb5_klog_syslog(LOG_ERR, @@ -911,8 +1016,8 @@ setup_addresses(verto_ctx *ctx, void *handle, const char *prog, /* Set the real port number. */ sa_setport(ai->ai_addr, addr.port); - ret = setup_socket(&addr, ai->ai_addr, handle, prog, ctx, - listen_backlog, verto_callbacks[addr.type], + ret = setup_socket(&addr, ai->ai_addr, &sockacts, handle, prog, + ctx, listen_backlog, verto_callbacks[addr.type], bind_conn_types[addr.type]); if (ret) { k5_print_addr(ai->ai_addr, addrbuf, sizeof(addrbuf)); @@ -937,6 +1042,7 @@ setup_addresses(verto_ctx *ctx, void *handle, const char *prog, cleanup: if (ai_list != NULL) freeaddrinfo(ai_list); + fini_sockact_list(&sockacts); return ret; }