Skip to content

Commit 1e49a01

Browse files
committed
feat: implement IGMPv2 and IGMPv3 report sending for multicast rejoining
Enhance the rejoin_mcast_group function to send both IGMPv2 and IGMPv3 reports via raw sockets. This ensures that upstream devices refresh their membership state even when the kernel suppresses joins due to existing memberships. Additionally, update the multicast.h documentation to reflect these changes.
1 parent 50a7239 commit 1e49a01

File tree

2 files changed

+128
-144
lines changed

2 files changed

+128
-144
lines changed

src/multicast.c

Lines changed: 124 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
#include <sys/socket.h>
1313
#include <unistd.h>
1414

15-
/* IGMPv3 Protocol Definitions */
15+
/* IGMPv2/IGMPv3 Protocol Definitions */
16+
#define IGMP_V2_MEMBERSHIP_REPORT 0x16
1617
#define IGMP_V3_MEMBERSHIP_REPORT 0x22
1718
#define IGMPV3_MODE_IS_INCLUDE 1
1819
#define IGMPV3_MODE_IS_EXCLUDE 2
@@ -21,6 +22,14 @@
2122
#define IGMPV3_ALLOW_NEW_SOURCES 5
2223
#define IGMPV3_BLOCK_OLD_SOURCES 6
2324

25+
/* IGMPv2 Membership Report structure */
26+
struct igmpv2_report {
27+
uint8_t type;
28+
uint8_t max_resp_time;
29+
uint16_t csum;
30+
uint32_t group_addr;
31+
} __attribute__((packed));
32+
2433
/* IGMPv3 Membership Report structures */
2534
struct igmpv3_grec {
2635
uint8_t grec_type;
@@ -59,6 +68,53 @@ static uint16_t calculate_checksum(const void *data, size_t len) {
5968
return ~sum;
6069
}
6170

71+
static int create_igmp_raw_socket(void) {
72+
int raw_sock;
73+
const char *upstream_if = get_upstream_interface_for_multicast();
74+
75+
raw_sock = socket(AF_INET, SOCK_RAW, IPPROTO_IGMP);
76+
if (raw_sock < 0) {
77+
logger(LOG_ERROR, "Failed to create raw IGMP socket: %s (need root?)",
78+
strerror(errno));
79+
return -1;
80+
}
81+
82+
if (connection_set_nonblocking(raw_sock) < 0) {
83+
logger(LOG_ERROR, "Failed to set raw IGMP socket non-blocking: %s",
84+
strerror(errno));
85+
close(raw_sock);
86+
return -1;
87+
}
88+
89+
bind_to_upstream_interface(raw_sock, upstream_if);
90+
91+
int hdrincl = 0;
92+
if (setsockopt(raw_sock, IPPROTO_IP, IP_HDRINCL, &hdrincl, sizeof(hdrincl)) <
93+
0) {
94+
logger(LOG_WARN, "Failed to set IP_HDRINCL: %s", strerror(errno));
95+
}
96+
97+
int router_alert = 1;
98+
if (setsockopt(raw_sock, IPPROTO_IP, IP_ROUTER_ALERT, &router_alert,
99+
sizeof(router_alert)) < 0) {
100+
logger(LOG_ERROR, "Failed to set IP_ROUTER_ALERT: %s", strerror(errno));
101+
close(raw_sock);
102+
return -1;
103+
}
104+
105+
if (upstream_if && upstream_if[0] != '\0') {
106+
struct ip_mreqn mreq;
107+
memset(&mreq, 0, sizeof(mreq));
108+
mreq.imr_ifindex = if_nametoindex(upstream_if);
109+
if (setsockopt(raw_sock, IPPROTO_IP, IP_MULTICAST_IF, &mreq, sizeof(mreq)) <
110+
0) {
111+
logger(LOG_WARN, "Failed to set IP_MULTICAST_IF: %s", strerror(errno));
112+
}
113+
}
114+
115+
return raw_sock;
116+
}
117+
62118
void bind_to_upstream_interface(int sock, const char *ifname) {
63119
if (ifname && ifname[0] != '\0') {
64120
struct ifreq ifr;
@@ -312,202 +368,130 @@ int join_mcast_group(service_t *service) {
312368
return sock;
313369
}
314370

315-
/*
316-
* Rejoin multicast group by sending IGMPv3 Membership Report via raw socket
317-
*
318-
* Background:
319-
* In our multi-process architecture, each client connection maintains its own
320-
* multicast socket. When multiple clients are playing the same multicast
321-
* source, the kernel maintains a reference count for that multicast group
322-
* membership.
323-
*
324-
* Problem:
325-
* The traditional leave/join approach (MCAST_LEAVE_GROUP + MCAST_JOIN_GROUP)
326-
* will NOT trigger an IGMP Report in this scenario because:
327-
* 1. When we call MCAST_LEAVE_GROUP, the kernel decrements the reference count
328-
* 2. If other sockets still have membership (refcount > 0), no IGMP Leave is
329-
* sent
330-
* 3. When we call MCAST_JOIN_GROUP again, the kernel sees the group already
331-
* exists
332-
* 4. No IGMP Report is sent because the interface is still a member of the
333-
* group
334-
*
335-
* Solution:
336-
* We manually construct and send an IGMPv3 Membership Report packet using a
337-
* raw socket. This bypasses the kernel's membership tracking and ensures that
338-
* the upstream router receives periodic membership reports, preventing it from
339-
* timing out and stopping the multicast stream delivery.
340-
*
341-
* This is particularly important when:
342-
* - The upstream router requires periodic IGMP Reports (typical timeout: 3
343-
* minutes)
344-
* - We want to ensure we become the "Reporter" for the group
345-
* - Multiple worker processes are serving the same multicast stream
346-
*
347-
* @param service Service structure containing multicast group and optional
348-
* source
349-
* @return 0 on success, -1 on failure
350-
*/
351371
int rejoin_mcast_group(service_t *service) {
352372
int raw_sock;
353-
struct sockaddr_in dest;
354-
struct igmpv3_report *report;
355-
struct igmpv3_grec *grec;
356-
uint8_t packet[sizeof(struct igmpv3_report) + sizeof(struct igmpv3_grec) +
357-
sizeof(uint32_t)];
358-
size_t packet_len;
359-
int r;
360-
const char *upstream_if;
361373
struct sockaddr_in *mcast_addr;
362374
struct sockaddr_in *source_addr = NULL;
375+
struct sockaddr_in dest;
376+
struct igmpv2_report report_v2;
377+
struct igmpv3_report *report_v3;
378+
struct igmpv3_grec *grec;
379+
uint8_t packet_v3[sizeof(struct igmpv3_report) + sizeof(struct igmpv3_grec) +
380+
sizeof(uint32_t)];
381+
size_t packet_len_v3 = 0;
363382
uint32_t group_addr;
364383
uint16_t nsrcs = 0;
365384
int is_ssm = 0;
385+
int result = -1;
386+
int sent_v2 = 0;
387+
int sent_v3 = 0;
366388

367-
/* Only support IPv4 for now */
368389
if (service->addr->ai_family != AF_INET) {
369-
logger(LOG_ERROR, "IGMPv3 raw socket rejoin only supports IPv4");
390+
logger(LOG_ERROR, "IGMP raw socket rejoin only supports IPv4");
370391
return -1;
371392
}
372393

373394
mcast_addr = (struct sockaddr_in *)service->addr->ai_addr;
374395
group_addr = mcast_addr->sin_addr.s_addr;
375396

376-
/* Check if this is Source-Specific Multicast (SSM) */
377397
if (service->msrc != NULL && strcmp(service->msrc, "") != 0 &&
378398
service->msrc_addr != NULL) {
379399
if (service->msrc_addr->ai_family != AF_INET) {
380-
logger(LOG_ERROR,
381-
"IGMPv3 raw socket rejoin: source address must be IPv4");
400+
logger(LOG_ERROR, "IGMP raw socket rejoin: source address must be IPv4");
382401
return -1;
383402
}
384403
source_addr = (struct sockaddr_in *)service->msrc_addr->ai_addr;
385404
is_ssm = 1;
386405
nsrcs = 1;
387406
}
388407

389-
/* Create raw socket for IGMP */
390-
raw_sock = socket(AF_INET, SOCK_RAW, IPPROTO_IGMP);
408+
raw_sock = create_igmp_raw_socket();
391409
if (raw_sock < 0) {
392-
logger(LOG_ERROR, "Failed to create raw IGMP socket: %s (need root?)",
393-
strerror(errno));
394-
return -1;
395-
}
396-
397-
/* Set socket to non-blocking mode to avoid blocking on sendto */
398-
if (connection_set_nonblocking(raw_sock) < 0) {
399-
logger(LOG_ERROR, "Failed to set raw IGMP socket non-blocking: %s",
400-
strerror(errno));
401-
close(raw_sock);
402410
return -1;
403411
}
404412

405-
/* Bind to upstream interface if specified */
406-
upstream_if = get_upstream_interface_for_multicast();
407-
bind_to_upstream_interface(raw_sock, upstream_if);
413+
if (!is_ssm) {
414+
memset(&report_v2, 0, sizeof(report_v2));
415+
report_v2.type = IGMP_V2_MEMBERSHIP_REPORT;
416+
report_v2.group_addr = group_addr;
417+
report_v2.csum = calculate_checksum(&report_v2, sizeof(report_v2));
408418

409-
/* Set IP_HDRINCL to 0 - kernel will add IP header */
410-
int hdrincl = 0;
411-
if (setsockopt(raw_sock, IPPROTO_IP, IP_HDRINCL, &hdrincl, sizeof(hdrincl)) <
412-
0) {
413-
logger(LOG_WARN, "Failed to set IP_HDRINCL: %s", strerror(errno));
414-
}
415-
416-
/* Set Router Alert option - required for IGMP packets (RFC 3376) */
417-
int router_alert = 1;
418-
if (setsockopt(raw_sock, IPPROTO_IP, IP_ROUTER_ALERT, &router_alert,
419-
sizeof(router_alert)) < 0) {
420-
logger(LOG_ERROR, "Failed to set IP_ROUTER_ALERT: %s", strerror(errno));
421-
close(raw_sock);
422-
return -1;
423-
}
419+
memset(&dest, 0, sizeof(dest));
420+
dest.sin_family = AF_INET;
421+
dest.sin_addr.s_addr = inet_addr("224.0.0.2");
424422

425-
/* Set IP_MULTICAST_IF to send from correct interface */
426-
if (upstream_if && upstream_if[0] != '\0') {
427-
struct ip_mreqn mreq;
428-
memset(&mreq, 0, sizeof(mreq));
429-
mreq.imr_ifindex = if_nametoindex(upstream_if);
430-
if (setsockopt(raw_sock, IPPROTO_IP, IP_MULTICAST_IF, &mreq, sizeof(mreq)) <
431-
0) {
432-
logger(LOG_WARN, "Failed to set IP_MULTICAST_IF: %s", strerror(errno));
423+
if (sendto(raw_sock, &report_v2, sizeof(report_v2), 0,
424+
(struct sockaddr *)&dest, sizeof(dest)) < 0) {
425+
logger(LOG_ERROR, "Failed to send IGMPv2 Report: %s", strerror(errno));
426+
} else {
427+
sent_v2 = 1;
428+
char group_str[INET_ADDRSTRLEN];
429+
inet_ntop(AF_INET, &mcast_addr->sin_addr, group_str, sizeof(group_str));
430+
logger(LOG_DEBUG,
431+
"Multicast: Sent IGMPv2 Report for ASM group %s via raw socket",
432+
group_str);
433433
}
434+
} else {
435+
logger(LOG_DEBUG, "Skipping IGMPv2 report for SSM subscription");
434436
}
435437

436-
/* Construct IGMPv3 Membership Report packet */
437-
memset(packet, 0, sizeof(packet));
438-
439-
report = (struct igmpv3_report *)packet;
440-
report->type = IGMP_V3_MEMBERSHIP_REPORT;
441-
report->resv1 = 0;
442-
report->csum = 0; /* Will calculate later */
443-
report->resv2 = 0;
444-
report->ngrec = htons(1); /* One group record */
438+
memset(packet_v3, 0, sizeof(packet_v3));
439+
report_v3 = (struct igmpv3_report *)packet_v3;
440+
report_v3->type = IGMP_V3_MEMBERSHIP_REPORT;
441+
report_v3->ngrec = htons(1);
445442

446-
/* Group Record */
447-
grec = (struct igmpv3_grec *)(packet + sizeof(struct igmpv3_report));
443+
grec = (struct igmpv3_grec *)(packet_v3 + sizeof(struct igmpv3_report));
448444

449445
if (is_ssm) {
450-
/* Source-Specific Multicast: MODE_IS_INCLUDE with source list */
451446
grec->grec_type = IGMPV3_MODE_IS_INCLUDE;
452-
grec->grec_auxwords = 0;
453447
grec->grec_nsrcs = htons(nsrcs);
454448
grec->grec_mca = group_addr;
455-
456-
/* Add source address to the source list */
457449
uint32_t *src_list =
458450
(uint32_t *)((uint8_t *)grec + sizeof(struct igmpv3_grec));
459451
src_list[0] = source_addr->sin_addr.s_addr;
460-
461-
packet_len = sizeof(struct igmpv3_report) + sizeof(struct igmpv3_grec) +
462-
sizeof(uint32_t);
452+
packet_len_v3 = sizeof(struct igmpv3_report) + sizeof(struct igmpv3_grec) +
453+
sizeof(uint32_t);
463454
} else {
464-
/* Any-Source Multicast (ASM): MODE_IS_EXCLUDE with empty source list */
465-
grec->grec_type = IGMPV3_MODE_IS_EXCLUDE; /* Exclude mode = join group,
466-
receive all sources */
467-
grec->grec_auxwords = 0;
468-
grec->grec_nsrcs =
469-
htons(0); /* No source list in exclude mode = receive from any source */
470-
grec->grec_mca = group_addr; /* Multicast group address (already in network
471-
byte order) */
472-
473-
packet_len = sizeof(struct igmpv3_report) + sizeof(struct igmpv3_grec);
455+
grec->grec_type = IGMPV3_MODE_IS_EXCLUDE;
456+
grec->grec_mca = group_addr;
457+
packet_len_v3 = sizeof(struct igmpv3_report) + sizeof(struct igmpv3_grec);
474458
}
475459

476-
/* Calculate checksum */
477-
report->csum = calculate_checksum(packet, packet_len);
460+
report_v3->csum = calculate_checksum(packet_v3, packet_len_v3);
478461

479-
/* Destination: 224.0.0.22 (IGMPv3 reports destination) */
480462
memset(&dest, 0, sizeof(dest));
481463
dest.sin_family = AF_INET;
482464
dest.sin_addr.s_addr = inet_addr("224.0.0.22");
483465

484-
/* Send the IGMPv3 Report */
485-
r = sendto(raw_sock, packet, packet_len, 0, (struct sockaddr *)&dest,
486-
sizeof(dest));
487-
488-
if (r < 0) {
466+
if (sendto(raw_sock, packet_v3, packet_len_v3, 0, (struct sockaddr *)&dest,
467+
sizeof(dest)) < 0) {
489468
logger(LOG_ERROR, "Failed to send IGMPv3 Report: %s", strerror(errno));
490-
close(raw_sock);
491-
return -1;
469+
} else {
470+
sent_v3 = 1;
471+
char group_str[INET_ADDRSTRLEN];
472+
inet_ntop(AF_INET, &mcast_addr->sin_addr, group_str, sizeof(group_str));
473+
if (is_ssm) {
474+
char source_str[INET_ADDRSTRLEN];
475+
inet_ntop(AF_INET, &source_addr->sin_addr, source_str,
476+
sizeof(source_str));
477+
logger(LOG_DEBUG,
478+
"Multicast: Sent IGMPv3 Report for SSM group %s source %s via raw "
479+
"socket",
480+
group_str, source_str);
481+
} else {
482+
logger(LOG_DEBUG,
483+
"Multicast: Sent IGMPv3 Report for ASM group %s via raw socket",
484+
group_str);
485+
}
492486
}
493487

494488
close(raw_sock);
495489

496-
char group_str[INET_ADDRSTRLEN];
497-
inet_ntop(AF_INET, &mcast_addr->sin_addr, group_str, sizeof(group_str));
498-
499-
if (is_ssm) {
500-
char source_str[INET_ADDRSTRLEN];
501-
inet_ntop(AF_INET, &source_addr->sin_addr, source_str, sizeof(source_str));
502-
logger(LOG_DEBUG,
503-
"Multicast: Sent IGMPv3 Report for SSM group %s source %s via raw "
504-
"socket",
505-
group_str, source_str);
490+
if (sent_v2 || sent_v3) {
491+
result = 0;
506492
} else {
507-
logger(LOG_DEBUG,
508-
"Multicast: Sent IGMPv3 Report for ASM group %s via raw socket",
509-
group_str);
493+
logger(LOG_ERROR, "Multicast: Failed to send IGMPv2 and IGMPv3 reports");
510494
}
511495

512-
return 0;
496+
return result;
513497
}

src/multicast.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,12 @@ uint32_t get_local_ip_for_fcc(void);
5656
int join_mcast_group(service_t *service);
5757

5858
/**
59-
* Rejoin a multicast group on an existing socket
60-
* This performs MCAST_LEAVE_GROUP followed by MCAST_JOIN_GROUP to force
61-
* the kernel to send a new IGMP Report message, refreshing membership.
59+
* Rejoin a multicast group by sending both IGMPv2 and IGMPv3 reports via raw
60+
* sockets. Ensures upstream devices refresh membership state even when the
61+
* kernel suppresses joins due to existing memberships.
6262
*
6363
* @param service Service structure containing multicast address info
64-
* @return 0 on success, -1 on failure
64+
* @return 0 on success, -1 if both IGMP versions fail
6565
*/
6666
int rejoin_mcast_group(service_t *service);
6767

0 commit comments

Comments
 (0)