From 207539ffe6bdfba0b1107257e11c84ef9f2e0ff6 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 5 Aug 2023 22:04:40 +0200 Subject: [PATCH] server/upsd.c and docs/config examples: handle "LISTEN *" deterministically (for both IPv4 and IPv6 if we can) [#2012] Signed-off-by: Jim Klimov --- NEWS | 6 ++ UPGRADING | 6 ++ conf/upsd.conf.sample | 12 +++ docs/config-notes.txt | 6 ++ docs/man/upsd.conf.txt | 14 ++- docs/security.txt | 6 ++ scripts/augeas/nutupsdconf.aug.in | 10 +- server/upsd.c | 171 +++++++++++++++++++++++++++++- 8 files changed, 225 insertions(+), 6 deletions(-) diff --git a/NEWS b/NEWS index 5b21a56099..1525607348 100644 --- a/NEWS +++ b/NEWS @@ -287,6 +287,12 @@ as part of https://github.com/networkupstools/nut/issues/1410 solution. would warn if a hostname resolves to several addresses (and would only listen on the first hit, as before in such cases) [#2012] + - A definitive behavior for `LISTEN *` directives became specified, to try + handling both IPv4 and IPv6 "any" address (subject to `upsd` CLI options + to only choose one, and to OS abilities); this use-case may be practically + implemented as a single IPv6 socket on systems with enabled "dual-stack" + support or as two separate listening sockets [#2012] + - sstate (server state, e.g. upsd) should now "PING" drivers also if they last reported themselves as "stale" (and might later crash) so their connections would be terminated if really no longer active [#1626] diff --git a/UPGRADING b/UPGRADING index 03471a0660..7a92aae3d4 100644 --- a/UPGRADING +++ b/UPGRADING @@ -79,6 +79,12 @@ Changes from 2.8.0 to 2.8.1 resolves to several addresses (and would only listen on the first hit, as it did before in such cases) [#2012] +- A definitive behavior for `LISTEN *` directives became specified, to try + handling both IPv4 and IPv6 "any" address (subject to `upsd` CLI options + to only choose one, and to OS abilities); this use-case may be practically + implemented as a single IPv6 socket on systems with enabled "dual-stack" + support or as two separate listening sockets [#2012] + - Added support for `make sockdebug` for easier developer access to the tool; also if `configure --with-dev` is in effect, it would now be installed to the configured `libexec` location. A man page was also added. [#1936] diff --git a/conf/upsd.conf.sample b/conf/upsd.conf.sample index 8c8f95a634..29ed500127 100644 --- a/conf/upsd.conf.sample +++ b/conf/upsd.conf.sample @@ -64,6 +64,12 @@ # Note that it is not true for Windows platforms. You shouldn't use IPv6 in # your configuration files unless you have IPv6 installed. # +# As a special case, `LISTEN * ` (with an asterisk) would try +# to listen on "ANY" IP address for both IPv4 (0.0.0.0) and IPv6 (::0), +# subject to `upsd` command-line arguments or OS/kernel configuration. +# If the system supports "dual-stack" mode (IPv4-mapped IPv6) per RFC-3493 +# then there would be one listening socket for both address types. +# # One or more LISTEN statements give the IP address (or name that # resolves to such an address) for upsd to listen on, optionally with # a port number. @@ -74,6 +80,12 @@ # # This will only be read at startup of upsd. If you make changes here, # you'll need to restart upsd, as reload will have no effect. +# +# Please note that older NUT releases could have been using the "dual-stack" +# mode, if provided by the system. Current versions (since NUT v2.8.1 release) +# try to restrict their listening sockets to only support IPv6 addresses and +# so avoid IPv4-mapped mode, except when handling the special `LISTEN * ` +# directive. # ======================================================================= # MAXCONN diff --git a/docs/config-notes.txt b/docs/config-notes.txt index 5f9f7a236d..ec24e1515f 100644 --- a/docs/config-notes.txt +++ b/docs/config-notes.txt @@ -295,6 +295,12 @@ want `upsd` to listen on for connections, optionally with a port number. LISTEN 127.0.0.1 3493 LISTEN ::1 3493 +As a special case, `LISTEN * ` (with an asterisk) would try +to listen on "ANY" IP address for both IPv4 (`0.0.0.0`) and IPv6 (`::0`), +subject to `upsd` command-line arguments or OS/kernel configuration. +If the system supports "dual-stack" mode (IPv4-mapped IPv6) per RFC-3493 +then there would be one listening socket for both address types. + NOTE: Refer to the NUT user manual <> for information on how to access and secure upsd clients connections. diff --git a/docs/man/upsd.conf.txt b/docs/man/upsd.conf.txt index 8287e54a56..57618b9b0b 100644 --- a/docs/man/upsd.conf.txt +++ b/docs/man/upsd.conf.txt @@ -70,8 +70,12 @@ Multiple LISTEN addresses may be specified. The default is to bind to compiled in). + To listen on all available interfaces, you may also use '0.0.0.0' for IPv4 and -and '::' for IPv6. - +and '::' for IPv6. As a special case, `LISTEN * ` (with an asterisk) +would try to listen on both IPv4 (`0.0.0.0`) and IPv6 (`::0`) wild-card IP +addresses, subject to `upsd` command-line arguments or OS/kernel configuration. +If the system supports "dual-stack" mode (IPv4-mapped IPv6) per RFC-3493 +then there would be one listening socket for both address types. ++ LISTEN 127.0.0.1 LISTEN 192.168.50.1 LISTEN myhostname.mydomain @@ -80,6 +84,12 @@ and '::' for IPv6. + This parameter will only be read at startup. You'll need to restart (rather than reload) upsd to apply any changes made here. ++ +Please note that older NUT releases could have been using the "dual-stack" +mode, if provided by the system. Current versions (since NUT v2.8.1 release) +try to restrict their listening sockets to only support IPv6 addresses and +so avoid IPv4-mapped mode, except when handling the special `LISTEN * ` +directive. "MAXCONN 'connections'":: diff --git a/docs/security.txt b/docs/security.txt index 51a50c5fcc..7d69aa0bf3 100644 --- a/docs/security.txt +++ b/docs/security.txt @@ -234,6 +234,12 @@ compiled in). LISTEN ::1 LISTEN 2001:0db8:1234:08d3:1319:8a2e:0370:7344 +As a special case, `LISTEN * ` (with an asterisk) would try +to listen on "ANY" IP address for both IPv4 (`0.0.0.0`) and IPv6 (`::0`), +subject to `upsd` command-line arguments or OS/kernel configuration. +If the system supports "dual-stack" mode (IPv4-mapped IPv6) per RFC-3493 +then there would be one listening socket for both address types. + This parameter will only be read at startup. You'll need to restart (rather than reload) `upsd` to apply any changes made here. diff --git a/scripts/augeas/nutupsdconf.aug.in b/scripts/augeas/nutupsdconf.aug.in index 35bf896f09..dff4fef435 100644 --- a/scripts/augeas/nutupsdconf.aug.in +++ b/scripts/augeas/nutupsdconf.aug.in @@ -55,8 +55,14 @@ let upsd_certfile = [ opt_spc . key "CERTFILE" . sep_spc . store path . eol ] * ALLOW_NO_DEVICE Boolean * STATEPATH path * LISTEN interface port - * Multiple LISTEN addresses may be specified. The default is to bind to 0.0.0.0 if no LISTEN addresses are specified. - * LISTEN 127.0.0.1 LISTEN 192.168.50.1 LISTEN ::1 LISTEN 2001:0db8:1234:08d3:1319:8a2e:0370:7344 + * Multiple lines each with one LISTEN address (or host name) and an optional + * port may be specified. The default is to bind to IPv4 and IPv6 "localhost" + * addresses (subject to CLI options `-4` or `-6` constraining IP version or + * OS kernel/configuration support), if no LISTEN addresses are specified. + * LISTEN 127.0.0.1 + * LISTEN 192.168.50.1 + * LISTEN ::1 + * LISTEN 2001:0db8:1234:08d3:1319:8a2e:0370:7344 * *************************************************************************) let upsd_other = upsd_maxage | upsd_trackingdelay | upsd_allow_no_device | upsd_statepath | upsd_listen_list | upsd_maxconn | upsd_certfile diff --git a/server/upsd.c b/server/upsd.c index 23ec4df4e8..b550af1be8 100644 --- a/server/upsd.c +++ b/server/upsd.c @@ -267,6 +267,8 @@ static void stype_free(stype_t *server) /* create a listening socket for tcp connections */ static void setuptcp(stype_t *server) { + /* Well, currently it is more a request than requirement... */ + static int require_IPV6_V6ONLY = 1; #ifdef WIN32 WSADATA WSAdata; WSAStartup(2,&WSAdata); @@ -275,8 +277,167 @@ static void setuptcp(stype_t *server) struct addrinfo hints, *res, *ai; int v = 0, one = 1; + if (VALID_FD_SOCK(server->sock_fd)) { + /* Alredy bound, e.g. thanks to 'LISTEN *' handling and injection + * into the list we lop over */ + return; + } + upsdebugx(3, "setuptcp: try to bind to %s port %s", server->addr, server->port); + /* Special handling for `LISTEN * ` directive with literal asterisk: + * on systems with RFC-3493 (no relation!) support for "IPv4-mapped addresses" + * it suffices to LISTEN on "::" (aka "::0" or "0:0:0:0:0:0:0:0") and also + * get an IPv4 any-address listener. More so, they would conflict and + * listening on one such socket precludes listening on the other. On other + * systems (or with disabled mapping so IPv6 means IPv6 only) we need both. + * So we jump through some hoops: + * * Try to get IPv4 any-address, just to know if it is available right now; + * * Free it and try to get IPv6 any-address, and try to get again that + * IPv4 any-address (IFF it was available before but is not available now - + * not a problem). + * * Remember the entries used, to release later. + * For more details see https://github.com/networkupstools/nut/issues/2012 + */ + if (!strcmp(server->addr, "*")) { + stype_t *serverAnyV4 = NULL, *serverAnyV6 = NULL; + int canhaveAnyV4 = 0; + int canhaveAnyV6 = 0; + + /* For this use-case, we allow IPv6 to handle IPv4 if it can */ + int old_require_IPV6_V6ONLY = require_IPV6_V6ONLY; + require_IPV6_V6ONLY = (opt_af == AF_INET6); + + /* Note: default opt_af==AF_UNSPEC so not constrained to only one protocol */ + if (opt_af != AF_INET6) { + /* Not constrained to IPv6 */ + upsdebugx(1, "%s: handling 'LISTEN * %s' with IPv4 any-address support", + __func__, server->port); + serverAnyV4 = xcalloc(1, sizeof(*serverAnyV4)); + serverAnyV4->addr = xstrdup("0.0.0.0"); + serverAnyV4->port = xstrdup(server->port); + serverAnyV4->sock_fd = ERROR_FD_SOCK; + serverAnyV4->next = NULL; + } + + if (opt_af != AF_INET) { + /* Not constrained to IPv4 */ + upsdebugx(1, "%s: handling 'LISTEN * %s' with IPv6 any-address support", + __func__, server->port); + serverAnyV6 = xcalloc(1, sizeof(*serverAnyV6)); + serverAnyV6->addr = xstrdup("::0"); + serverAnyV6->port = xstrdup(server->port); + serverAnyV6->sock_fd = ERROR_FD_SOCK; + serverAnyV6->next = NULL; + } + + if (serverAnyV4) { + /* First pass to just check if we CAN have this listener now */ + setuptcp(serverAnyV4); + if (serverAnyV6) { + if (VALID_FD_SOCK(serverAnyV4->sock_fd)) { + upsdebugx(3, + "%s: Could bind to %s:%s trying to handle a 'LISTEN *' directive" + "; will release it for now to try IPv6", + __func__, serverAnyV4->addr, serverAnyV4->port); + canhaveAnyV4 = 1; + close(serverAnyV4->sock_fd); + serverAnyV4->sock_fd = ERROR_FD_SOCK; + /* Let the system know about the change: */ + /* usleep(100); */ + } else { + upsdebugx(3, + "%s: Could not bind to %s:%s trying to handle a 'LISTEN *' directive", + __func__, serverAnyV4->addr, serverAnyV4->port); + } + } /* else: just keep it, all done */ + } + + if (serverAnyV6) { + setuptcp(serverAnyV6); + if (VALID_FD_SOCK(serverAnyV6->sock_fd)) { + canhaveAnyV6 = 1; + } else { + upsdebugx(3, + "%s: Could not bind to %s:%s trying to handle a 'LISTEN *' directive", + __func__, serverAnyV6->addr, serverAnyV6->port); + } + + if (serverAnyV4 && canhaveAnyV4) { + /* Second pass to get this listener if we can (no IPv4-mapped IPv6 + * support was in force on this platform or its configuration) */ + upsdebugx(3, "%s: try taking IPv4 'ANY' again " + "(if dual-stack IPv6 'ANY' did not grab it)", __func__); + setuptcp(serverAnyV4); + if (INVALID_FD_SOCK(serverAnyV4->sock_fd)) { + upsdebugx(3, + "%s: Could not bind to IPv4 %s:%s after trying to bind to IPv6: " + "assuming dual-stack support on this system", + __func__, serverAnyV4->addr, serverAnyV4->port); + canhaveAnyV4 = 0; + } + } + } + + if (!canhaveAnyV4 && !canhaveAnyV6) { + fatalx(EXIT_FAILURE, + "Handling of 'LISTEN * %s' directive failed to bind to 'ANY' address", + server->port); + } + + /* Finalize our findings and reset to normal operation + * Note that at least one of these addresses is usable + * and we keep it (and replace original "server" entry + * keeping its place in the list). + */ + free(server->addr); + free(server->port); + if (canhaveAnyV4) { + upsdebugx(3, "%s: remembering IPv4 'ANY' instead of 'LISTEN *'", __func__); + server->addr = serverAnyV4->addr; + server->port = serverAnyV4->port; + server->sock_fd = serverAnyV4->sock_fd; + /* ...and keep whatever server->next there was */ + + /* Free the ghost, all needed info was relocated */ + free(serverAnyV4); + } else { + if (serverAnyV4) { + /* Free any contents there were too */ + stype_free(serverAnyV4); + } + } + serverAnyV4 = NULL; + + if (canhaveAnyV6) { + if (canhaveAnyV4) { + /* "server" already populated by excerpts from V4, attach to it */ + upsdebugx(3, "%s: also remembering IPv6 'ANY' instead of 'LISTEN *'", __func__); + serverAnyV6->next = server->next; + server->next = serverAnyV6; + } else { + /* Only retain V6 info */ + upsdebugx(3, "%s: remembering IPv6 'ANY' instead of 'LISTEN *'", __func__); + server->addr = serverAnyV6->addr; + server->port = serverAnyV6->port; + server->sock_fd = serverAnyV6->sock_fd; + /* ...and keep whatever server->next there was */ + + /* Free the ghost, all needed info was relocated */ + free(serverAnyV6); + } + } else { + if (serverAnyV6) { + /* Free any contents there were too */ + stype_free(serverAnyV6); + } + } + serverAnyV6 = NULL; + + require_IPV6_V6ONLY = old_require_IPV6_V6ONLY; + return; + } + memset(&hints, 0, sizeof(hints)); hints.ai_flags = AI_PASSIVE; hints.ai_family = opt_af; @@ -303,11 +464,17 @@ static void setuptcp(stype_t *server) fatal_with_errno(EXIT_FAILURE, "setuptcp: setsockopt"); } - /* Ordinarily we request that IPv6 listeners handle only IPv6. + /* Ordinarily we request that IPv6 listeners handle only IPv6 + * (except when we handle `LISTEN *` as detailed above). + * Note we specifically try to ensure this when CLI requires + * IPv6-only behavior (even if we want "any" addr for `LISTEN *`). * TOTHINK: Does any platform need `#ifdef IPV6_V6ONLY` given * that we apparently already have AF_INET6 OS support everywhere? + * TOTHINK: Do we want to setsockopt() to explicitly allow dual-stack + * (perhaps counteracting OS default or customized configuration) + * when handling `LISTEN *` use-cases? */ - if (ai->ai_family == AF_INET6) { + if (ai->ai_family == AF_INET6 && (require_IPV6_V6ONLY || (opt_af == AF_INET6))) { if (setsockopt(sock_fd, IPPROTO_IPV6, IPV6_V6ONLY, (void *)&one, sizeof(one)) != 0) { upsdebug_with_errno(3, "setuptcp: setsockopt IPV6_V6ONLY"); /* ack, ignore */