Skip to content

Commit

Permalink
server/upsd.c and docs/config examples: handle "LISTEN *" determinist…
Browse files Browse the repository at this point in the history
…ically (for both IPv4 and IPv6 if we can) [#2012]

Signed-off-by: Jim Klimov <[email protected]>
  • Loading branch information
jimklimov committed Aug 5, 2023
1 parent 22b51fc commit 207539f
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 6 deletions.
6 changes: 6 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
6 changes: 6 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
12 changes: 12 additions & 0 deletions conf/upsd.conf.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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 * <port>` (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.
Expand All @@ -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 * <port>`
# directive.

# =======================================================================
# MAXCONN <connections>
Expand Down
6 changes: 6 additions & 0 deletions docs/config-notes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 * <port>` (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 <<NUT_Security,security chapter>> for
information on how to access and secure upsd clients connections.

Expand Down
14 changes: 12 additions & 2 deletions docs/man/upsd.conf.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 * <port>` (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
Expand All @@ -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 * <port>`
directive.

"MAXCONN 'connections'"::

Expand Down
6 changes: 6 additions & 0 deletions docs/security.txt
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,12 @@ compiled in).
LISTEN ::1
LISTEN 2001:0db8:1234:08d3:1319:8a2e:0370:7344

As a special case, `LISTEN * <port>` (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.

Expand Down
10 changes: 8 additions & 2 deletions scripts/augeas/nutupsdconf.aug.in
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
171 changes: 169 additions & 2 deletions server/upsd.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 * <port>` 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;
Expand All @@ -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 */
Expand Down

0 comments on commit 207539f

Please sign in to comment.