Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions src/reflector/raw_socket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
27 changes: 27 additions & 0 deletions tests/raw_socket_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <cstdint>
#include <arpa/inet.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <format>
#include <fstream>
Expand Down Expand Up @@ -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.
Expand Down