connectd: set IPV6_V6ONLY=1 on IPv6 sockets for consistent dual-stack behaviour
What changed, and why it matters
This commit fixes a network setup bug in Core Lightning's connection daemon. On some operating systems (macOS, Fedora, Arch, and default Linux kernels), an IPv6 'listen on all addresses' socket was also covering IPv4, which prevented a separate IPv4 socket from starting and could leak a small amount of memory when the IPv4 bind failed. The patch forces IPv6 sockets to be IPv6-only before binding, so IPv4 and IPv6 listeners are always created independently, and it frees an unused error message to stop the memory leak. It is a reliability/availability fix, not a direct remote exploit.
Treat as a routine bug fix. Apply the patch to ensure consistent dual-stack listener behavior across platforms and to eliminate the connectd memory leak on affected systems. No emergency response is indicated.
Security signals we found
Denial-of-service availability fix: on affected systems, the node could not listen on IPv4 when configured for wildcard IPv4+IPv6
Memory leak fix in connectd error path
No evidence of malicious intent in the diff
Evidence from the diff
In connectd/connectd.c, make_listen_fd() now sets IPV6_V6ONLY=1 on AF_INET6 sockets before bind. This prevents dual-stack wildcard binding (:: also covering 0.0.0.0) on systems where net.ipv6.bindv6only=0, which caused the subsequent IPv4 wildcard bind to fail with EADDRINUSE. setup_listeners() now frees the errstr allocation when IPv4 fails acceptably because IPv6 succeeded, fixing a memory leak in connectd on those systems. The test test_ipv4_and_ipv6 is updated to accept either an IPv4-only or IPv6-only single-socket result.
Changed components
connectd/connectd.ctests/test_misc.pyInspect captured patch +21 / −3
diff --git a/connectd/connectd.c b/connectd/connectd.c
index 61759f7a..cb95ef9b 100644
--- a/connectd/connectd.c
+++ b/connectd/connectd.c
@@ -1301,6 +1301,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",
@@ -1535,6 +1550,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 91af3d7d..b3370453 100644
--- a/tests/test_misc.py
+++ b/tests/test_misc.py
@@ -1842,10 +1842,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
Why this scored 25/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.