diff --git a/src/reflector/raw_socket.cpp b/src/reflector/raw_socket.cpp index c35e7cd..0340047 100644 --- a/src/reflector/raw_socket.cpp +++ b/src/reflector/raw_socket.cpp @@ -525,14 +525,13 @@ bool RawSocket::RejoinGroups() noexcept { continue; } // A fresh fd rather than a re-join on the old one: where a recreated interface is handed - // back the number it had, the kernel still has the old fd down for that (group, index) and - // refuses the join as a duplicate — and a membership it keeps for a dead index still counts - // against the socket's join cap (Linux igmp_max_memberships, 20 by default, and not - // raisable on a locked-down router), so kept sockets would exhaust it after a handful of - // recreations. Closed before the reopen rather than after, so the descriptor it frees is - // the one the reopen takes: at the process fd limit that is the difference between - // recovering and staying deaf. Refcounts are untouched, so the memberships already handed - // to reflectors stay valid. + // back the number it had, the kernel refuses the duplicate (group, index) join — and a + // membership it keeps for a dead index still counts against igmp_max_memberships, so a + // kept fd stops joining after 20 recreations (measured; see the recreation-cap test). + // Closed before the reopen rather than after, so the descriptor it frees is the one the + // reopen takes: at the process fd limit that is the difference between recovering and + // staying deaf. Refcounts are untouched, so the memberships already handed to reflectors + // stay valid. auto& join_fd = join_fds_.Get(family); join_fd.Reset(); join_fd.Reset(socket(family == IpAddress::Family::V6 ? AF_INET6 : AF_INET, SOCK_DGRAM, 0)); diff --git a/tests/raw_socket_test.cpp b/tests/raw_socket_test.cpp index 332ca26..3a1ad5a 100644 --- a/tests/raw_socket_test.cpp +++ b/tests/raw_socket_test.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -1152,6 +1153,32 @@ TEST_F(RawSocketInterfacePairRequiresRootTest, RebindRestoresTheCaptureAfterRecr EXPECT_EQ(socket.Fd(), fd); // same fd throughout, so dispatcher registrations survive } +#if defined(__linux__) +// A kept join fd would exhaust the socket's membership cap: a destroyed interface's memberships are +// not scrubbed from a surviving socket, so every recreation's join adds another. Measured here at +// the default igmp_max_memberships of 20 -- the 21st join on a kept fd fails with ENOBUFS -- which +// is why the rebind reopens rather than re-joining in place. +TEST_F(RawSocketInterfacePairRequiresRootTest, SurvivesRepeatedRecreationsWithoutExhaustingTheJoinCap) { + Interface iface{pair.InjectInterface()}; + ASSERT_TRUE(iface.IsValid()); + RawSocket socket{iface}; + ASSERT_TRUE(socket.IsValid()); + const auto group = IpAddress::MdnsGroupV4(); + auto membership = socket.JoinMulticastGroup(group); + ASSERT_TRUE(membership.IsValid()); + + // Comfortably past the cap, so a rebind holding its join fd runs out partway through. + for (int i = 0; i < 25; ++i) { + ASSERT_TRUE(pair.Recreate()) << "recreation " << i; + ASSERT_NE(iface.Reidentify(), Interface::IdentityChange::Parked) << "recreation " << i; + ASSERT_TRUE(socket.Rebind()) << "recreation " << i; + ASSERT_TRUE(socket.GroupsJoined()) << "recreation " << i; + } + + EXPECT_TRUE(KernelHasMembership(pair.InjectInterface(), group)); +} +#endif + #if defined(__linux__) // The capture re-attaching is not enough: without the re-join the socket comes back attached and // permanently deaf on every group it had.