diff --git a/NEWS b/NEWS index 3334786ef7..fe8dff5db1 100644 --- a/NEWS +++ b/NEWS @@ -62,7 +62,7 @@ as part of https://github.com/networkupstools/nut/issues/1410 solution. too low. As issue #1455 and PR #1495 found, in two cases the called commands did "meaningfully" modify data -- so without debug logs the program misbehaved. A known regression for `upscode2` driver; might - be or not be a problem with `upsd` driver in NUT v2.8.0 release, + be or not be a problem with `upsd` server in NUT v2.8.0 release, fixed for NUT v2.8.1. * A table in `cyberpower-mib` (for `snmp-ups` driver) sources was arranged in NUT v2.8.0 release in a way that precluded the driver @@ -278,6 +278,24 @@ as part of https://github.com/networkupstools/nut/issues/1410 solution. * added support for `NUT_QUIET_INIT_SSL` environment variable to hide the infamous "Init SSL without certificate database" warning [#1662] + - The `upsd.conf` listing of `LISTEN` addresses was previously inverted + (the last listed address was applied first), which was counter-intuitive + and fixed for this release [#2012] + + - The `upsd` configured to listen on IPv6 addresses should handle only + IPv6 (and not IPv4-mappings) to avoid surprises and insecurity; it + will now warn if a host name resolves to several addresses (and will 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). When both address families are + enabled, the `upsd` data server will first try to open an IPv6 socket + asking for disabled IPv4-mapped IPv6 address support (if the OS honors + that), and then an IPv4 socket (which may fail if the IPv6 socket already + covers it anyway); in other words, you can end up with one or 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 10b87c0968..d29edea18b 100644 --- a/UPGRADING +++ b/UPGRADING @@ -65,6 +65,30 @@ Changes from 2.8.0 to 2.8.1 the packaging recipes may use NUT source-code facilities and package just symlinks as relevant for each distro separately [#1462, #1504] +- The `upsd.conf` listing of `LISTEN` addresses was previously inverted + (the last listed address was applied first), which was counter-intuitive + and fixed for this release. If user configurations somehow relied on this + order (e.g. to prioritize IPv6 vs IPv4 listeners), configuration changes + may be needed. [#2012] + +- The `upsd` configured to listen on IPv6 addresses should handle only + IPv6 (and not IPv4-mappings like it might have done before) to avoid + surprises and insecurity -- if user configurations somehow relied on + this dual support, configuration changes may be needed to specify both + desired IP addresses. Note that the daemon logs will now warn if a + host name resolves to several addresses (and will 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 and required + IPv4-mapped IPv6 address support, or as two separate listening sockets - + logged messages to this effect (e.g. inability to listen on IPv4 after + opening IPv6) are expected on some platforms. End-users may also want to + reconfigure their `upsd.conf` files to remove some now-redundant `LISTEN` + lines. [#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..277e58417f 100644 --- a/conf/upsd.conf.sample +++ b/conf/upsd.conf.sample @@ -64,6 +64,13 @@ # 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) will try +# to listen on "ANY" IP address for both IPv6 (::0) and IPv4 (0.0.0.0), +# subject to `upsd` command-line arguments, or system configuration. +# Note that if the system supports IPv4-mapped IPv6 addressing per RFC-3493, +# and does not allow to disable this mode, then there may be one listening +# socket to handle both address families. +# # 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 +81,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 IPv4-mapped +# IPv6 addressing (sometimes also known as "dual-stack") mode, if provided +# by the system. Current versions (since NUT v2.8.1 release) explicitly try +# to restrict their listening sockets to only support one address family on +# each socket, and so avoid IPv4-mapped mode where possible. # ======================================================================= # MAXCONN diff --git a/configure.ac b/configure.ac index f32de9e8b0..b438855634 100644 --- a/configure.ac +++ b/configure.ac @@ -1120,7 +1120,7 @@ NUT_TYPE_SOCKLEN_T NUT_CHECK_SOCKETLIB NUT_FUNC_GETNAMEINFO_ARGTYPES -AC_CACHE_CHECK([for inet_ntop()], +AC_CACHE_CHECK([for inet_ntop() with IPv4 and IPv6 support], [ac_cv_func_inet_ntop], [AC_LANG_PUSH([C]) dnl e.g. add "-lws2_32" for mingw builds @@ -1148,7 +1148,8 @@ AC_CACHE_CHECK([for inet_ntop()], ]], [[/* const char* inet_ntop(int af, const void* src, char* dst, size_t cnt); */ char buf[128]; -printf("%s", inet_ntop(AF_INET, "1.2.3.4", buf, 10)) +printf("%s", inet_ntop(AF_INET, "1.2.3.4", buf, 10)); +printf("%s", inet_ntop(AF_INET6, "::1", buf, 10)) /* autoconf adds ";return 0;" */ ]])], [ac_cv_func_inet_ntop=yes], [ac_cv_func_inet_ntop=no] diff --git a/docs/config-notes.txt b/docs/config-notes.txt index 5f9f7a236d..8ed3269fd9 100644 --- a/docs/config-notes.txt +++ b/docs/config-notes.txt @@ -295,6 +295,13 @@ 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) will try to +listen on "ANY" IP address for both and IPv6 (`::0`) and IPv4 (`0.0.0.0`), +subject to `upsd` command-line arguments, or system configuration or support. +Note that if the system supports IPv4-mapped IPv6 addressing per RFC-3493, +and does not allow to disable this mode, then there may be one listening +socket to handle both address families. + 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..af8582a700 100644 --- a/docs/man/upsd.conf.txt +++ b/docs/man/upsd.conf.txt @@ -66,12 +66,18 @@ compiled into the code. This overrides any value you may have set with upsd will listen on port 3493 for this interface. + Multiple LISTEN addresses may be specified. The default is to bind to -127.0.0.1 if no LISTEN addresses are specified (and ::1 if IPv6 support is -compiled in). +`127.0.0.1` if no LISTEN addresses are specified (and also `::1` if IPv6 +support is compiled in). ++ +To listen on all available interfaces and configured IP addresses of your +system, you may also use `::` for IPv6 and `0.0.0.0` for IPv4, respectively. +As a special case, a single `LISTEN * ` directive (with an asterisk) will +try to listen on both IPv6 (`::0`) and IPv4 (`0.0.0.0`) wild-card IP addresses, +subject to `upsd` command-line arguments or system configuration. +Note that if the system supports IPv4-mapped IPv6 addressing per RFC-3493, +and does not allow to disable this mode, then there may be one listening +socket to handle both address families. + -To listen on all available interfaces, you may also use '0.0.0.0' for IPv4 and -and '::' for IPv6. - LISTEN 127.0.0.1 LISTEN 192.168.50.1 LISTEN myhostname.mydomain @@ -80,6 +86,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 IPv4-mapped +IPv6 addressing (sometimes also known as "dual-stack") mode, if provided +by the system. Current versions (since NUT v2.8.1 release) explicitly try +to restrict their listening sockets to only support one address family on +each socket, and so avoid IPv4-mapped mode where possible. "MAXCONN 'connections'":: diff --git a/docs/security.txt b/docs/security.txt index 51a50c5fcc..a6594885fd 100644 --- a/docs/security.txt +++ b/docs/security.txt @@ -234,6 +234,13 @@ compiled in). LISTEN ::1 LISTEN 2001:0db8:1234:08d3:1319:8a2e:0370:7344 +As a special case, `LISTEN * ` (with an asterisk) will try to +listen on "ANY" IP address for both IPv6 (`::0`) and IPv4 (`0.0.0.0`), +subject to `upsd` command-line arguments, or system configuration or support. +Note that if the system supports IPv4-mapped IPv6 addressing per RFC-3493, +and does not allow to disable this mode, then there may be one listening +socket to handle both address families. + 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..1592048387 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 system configuration or 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 8c66661b62..7123d625d6 100644 --- a/server/upsd.c +++ b/server/upsd.c @@ -237,13 +237,33 @@ void listen_add(const char *addr, const char *port) server->addr = xstrdup(addr); server->port = xstrdup(port); server->sock_fd = ERROR_FD_SOCK; - server->next = firstaddr; + server->next = NULL; - firstaddr = server; + if (firstaddr) { + stype_t *tmp; + for (tmp = firstaddr; tmp->next; tmp = tmp->next); + tmp->next = server; + } else { + firstaddr = server; + } upsdebugx(3, "listen_add: added %s:%s", server->addr, server->port); } +/* Close the connection if needed and free the allocated memory. + * WARNING: it is up to the caller to rewrite the "next" pointer + * in whoever points to this server instance (if needed)! */ +static void stype_free(stype_t *server) +{ + if (VALID_FD_SOCK(server->sock_fd)) { + close(server->sock_fd); + } + + free(server->addr); + free(server->port); + free(server); +} + /* create a listening socket for tcp connections */ static void setuptcp(stype_t *server) { @@ -255,8 +275,152 @@ 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 loop over */ + upsdebugx(6, "setuptcp: SKIP bind to %s port %s: entry already initialized", + server->addr, server->port); + return; + } + upsdebugx(3, "setuptcp: try to bind to %s port %s", server->addr, server->port); + /* Special handling note for `LISTEN * ` directive with the + * literal asterisk on systems with RFC-3493 (no relation!) support + * for "IPv4-mapped addresses": it is possible (and technically + * suffices) to LISTEN on "::" (aka "::0" or "0:0:0:0:0:0:0:0") and + * also get an IPv4 any-address listener automatically. 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 really means "IPv6 only") we need both sockets. + * NUT asks the system for "IPv6 only" mode when listening on any + * sort of IPv6 addresses; it is however up to the system to implement + * that ability and comply with our request. + * Here we jump through some hoops: + * * Try to get IPv6 any-address (unless constrained by CLI to IPv4); + * * Try to get IPv4 any-address (unless constrained by CLI to IPv6), + * log information for the sysadmin that it might conflict with the + * IPv6 listener (IFF we have just opened one); + * * Remember the one or two linked-list entries used, to release later. + */ + if (!strcmp(server->addr, "*")) { + stype_t *serverAnyV4 = NULL, *serverAnyV6 = NULL; + int canhaveAnyV4 = 0, canhaveAnyV6 = 0; + + /* 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 (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) { + /* Try to get this listener if we can (no IPv4-mapped + * IPv6 support was in force on this platform or its + * configuration in some way that setsockopt(IPV6_V6ONLY) + * failed to cancel). + */ + upsdebugx(3, "%s: try taking IPv4 'ANY'%s", + __func__, + canhaveAnyV6 ? " (if dual-stack IPv6 'ANY' did not grab it)" : ""); + setuptcp(serverAnyV4); + if (VALID_FD_SOCK(serverAnyV4->sock_fd)) { + canhaveAnyV4 = 1; + } else { + upsdebugx(3, + "%s: Could not bind to IPv4 %s:%s%s", + __func__, serverAnyV4->addr, serverAnyV4->port, + canhaveAnyV6 ? (" after trying to bind to IPv6: " + "assuming dual-stack support on this " + "system could not be disabled") : ""); + } + } + + 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; + + return; + } + memset(&hints, 0, sizeof(hints)); hints.ai_flags = AI_PASSIVE; hints.ai_family = opt_af; @@ -283,6 +447,18 @@ static void setuptcp(stype_t *server) fatal_with_errno(EXIT_FAILURE, "setuptcp: setsockopt"); } + /* Ordinarily we request that IPv6 listeners handle only IPv6 + * and not IPv4 mapped addresses - if the OS would honour that. + * TOTHINK: Does any platform need `#ifdef IPV6_V6ONLY` given + * that we apparently already have AF_INET6 OS support everywhere? + */ + if (ai->ai_family == 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 */ + } + } + if (bind(sock_fd, ai->ai_addr, ai->ai_addrlen) < 0) { upsdebug_with_errno(3, "setuptcp: bind"); close(sock_fd); @@ -306,6 +482,20 @@ static void setuptcp(stype_t *server) continue; } + if (ai->ai_next) { + char ipaddrbuf[SMALLBUF]; + const char *ipaddr; + snprintf(ipaddrbuf, sizeof(ipaddrbuf), " as "); + ipaddr = inet_ntop(ai->ai_family, ai->ai_addr, + ipaddrbuf + strlen(ipaddrbuf), + sizeof(ipaddrbuf)); + upslogx(LOG_WARNING, + "setuptcp: bound to %s%s but there seem to be " + "further (ignored) addresses resolved for this name", + server->addr, + ipaddr == NULL ? "" : ipaddrbuf); + } + server->sock_fd = sock_fd; break; } @@ -688,13 +878,16 @@ void server_load(void) { stype_t *server; - /* default behaviour if no LISTEN addres has been specified */ + /* default behaviour if no LISTEN address has been specified */ if (!firstaddr) { + /* Note: default opt_af==AF_UNSPEC so not constrained to only one protocol */ if (opt_af != AF_INET) { + upsdebugx(1, "%s: No LISTEN configuration provided, will try IPv6 localhost", __func__); listen_add("::1", string_const(PORT)); } if (opt_af != AF_INET6) { + upsdebugx(1, "%s: No LISTEN configuration provided, will try IPv4 localhost", __func__); listen_add("127.0.0.1", string_const(PORT)); } } @@ -716,14 +909,7 @@ void server_free(void) /* cleanup server fds */ for (server = firstaddr; server; server = snext) { snext = server->next; - - if (VALID_FD_SOCK(server->sock_fd)) { - close(server->sock_fd); - } - - free(server->addr); - free(server->port); - free(server); + stype_free(server); } firstaddr = NULL;