From 2f20fe03020e5c3bcfcb68b296cb48ce3e6afb48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Tue, 19 May 2026 09:35:40 -0300 Subject: [PATCH] connectd: set IPV6_V6ONLY=1 on IPv6 sockets for consistent dual-stack behaviour Systems with net.ipv6.bindv6only=0 (macOS, Fedora, Arch, vanilla kernels) create dual-stack sockets by default: binding '::' also covers '0.0.0.0', so the subsequent IPv4 wildcard bind fails with EADDRINUSE. Debian/Ubuntu ship bindv6only=1 so both binds succeed here, which is why this was never noticed on typical Linux CI. Explicitly set IPV6_V6ONLY=1 on AF_INET6 sockets before bind so both address families always get independent sockets regardless of the system sysctl. Also free the errstr allocation left behind when the IPv4 bind fails acceptably (IPv6 succeeded), fixing a memleak in connectd on those systems. test_ipv4_and_ipv6: accept IPv6-only binding in the single-socket case, which can still occur on IPv4-only hosts. Changelog-Fixed: connectd: on macOS and other systems with dual-stack IPv6 default, wildcard '--addr=:' now correctly binds both IPv4 and IPv6. --- connectd/connectd.c | 18 ++++++++++++++++++ tests/test_misc.py | 6 +++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/connectd/connectd.c b/connectd/connectd.c index 54ac08918db0..c16f05ff2f7e 100644 --- a/connectd/connectd.c +++ b/connectd/connectd.c @@ -1298,6 +1298,21 @@ static struct listen_fd *make_listen_fd(const tal_t *ctx, status_unusual("Failed setting socket reuse: %s", strerror(errno)); +#ifdef IPV6_V6ONLY + /* Most Linux distros (Debian, Ubuntu) ship net.ipv6.bindv6only=1 in + * sysctl, making IPv6 sockets IPv6-only by default, so a separate IPv4 + * wildcard socket can also bind. macOS, Fedora, Arch and vanilla + * kernels default to 0 (dual-stack): binding '::' also covers + * '0.0.0.0', and the subsequent IPv4 bind fails with EADDRINUSE. + * Explicitly set IPV6_V6ONLY=1 so both sockets always bind + * independently, regardless of the system sysctl. */ + if (domain == AF_INET6) { + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on))) + status_unusual("Failed setting IPV6_V6ONLY: %s", + strerror(errno)); + } +#endif + if (bind(fd, addr, len) != 0) { const char *es = strerror(errno); *errstr = tal_fmt(ctx, "Failed to bind socket for %s%s: %s", @@ -1532,6 +1547,9 @@ setup_listeners(const tal_t *ctx, } else if (!ipv6_ok) { /* Both failed, return now, errstr set. */ return NULL; + } else { + /* IPv4 failed, but IPv6 (dual-stack) succeeded: discard errstr. */ + *errstr = tal_free(*errstr); } continue; } diff --git a/tests/test_misc.py b/tests/test_misc.py index 4154ad6eba7f..860429744a31 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1782,10 +1782,10 @@ def test_ipv4_and_ipv6(node_factory): assert bind[1]['address'] == '0.0.0.0' assert int(bind[1]['port']) == port else: - # Assume we're IPv4 only... + # Either IPv4-only, or IPv6 dual-stack (covers IPv4 too, so no separate IPv4 socket) assert len(bind) == 1 - assert bind[0]['type'] == 'ipv4' - assert bind[0]['address'] == '0.0.0.0' + assert bind[0]['type'] in ('ipv4', 'ipv6') + assert bind[0]['address'] in ('0.0.0.0', '::') assert int(bind[0]['port']) == port