|
12 | 12 | #include <sys/socket.h> |
13 | 13 | #include <unistd.h> |
14 | 14 |
|
15 | | -/* IGMPv3 Protocol Definitions */ |
| 15 | +/* IGMPv2/IGMPv3 Protocol Definitions */ |
| 16 | +#define IGMP_V2_MEMBERSHIP_REPORT 0x16 |
16 | 17 | #define IGMP_V3_MEMBERSHIP_REPORT 0x22 |
17 | 18 | #define IGMPV3_MODE_IS_INCLUDE 1 |
18 | 19 | #define IGMPV3_MODE_IS_EXCLUDE 2 |
|
21 | 22 | #define IGMPV3_ALLOW_NEW_SOURCES 5 |
22 | 23 | #define IGMPV3_BLOCK_OLD_SOURCES 6 |
23 | 24 |
|
| 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 | + |
24 | 33 | /* IGMPv3 Membership Report structures */ |
25 | 34 | struct igmpv3_grec { |
26 | 35 | uint8_t grec_type; |
@@ -59,6 +68,53 @@ static uint16_t calculate_checksum(const void *data, size_t len) { |
59 | 68 | return ~sum; |
60 | 69 | } |
61 | 70 |
|
| 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 | + |
62 | 118 | void bind_to_upstream_interface(int sock, const char *ifname) { |
63 | 119 | if (ifname && ifname[0] != '\0') { |
64 | 120 | struct ifreq ifr; |
@@ -312,202 +368,130 @@ int join_mcast_group(service_t *service) { |
312 | 368 | return sock; |
313 | 369 | } |
314 | 370 |
|
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 | | - */ |
351 | 371 | int rejoin_mcast_group(service_t *service) { |
352 | 372 | 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; |
361 | 373 | struct sockaddr_in *mcast_addr; |
362 | 374 | 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; |
363 | 382 | uint32_t group_addr; |
364 | 383 | uint16_t nsrcs = 0; |
365 | 384 | int is_ssm = 0; |
| 385 | + int result = -1; |
| 386 | + int sent_v2 = 0; |
| 387 | + int sent_v3 = 0; |
366 | 388 |
|
367 | | - /* Only support IPv4 for now */ |
368 | 389 | 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"); |
370 | 391 | return -1; |
371 | 392 | } |
372 | 393 |
|
373 | 394 | mcast_addr = (struct sockaddr_in *)service->addr->ai_addr; |
374 | 395 | group_addr = mcast_addr->sin_addr.s_addr; |
375 | 396 |
|
376 | | - /* Check if this is Source-Specific Multicast (SSM) */ |
377 | 397 | if (service->msrc != NULL && strcmp(service->msrc, "") != 0 && |
378 | 398 | service->msrc_addr != NULL) { |
379 | 399 | 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"); |
382 | 401 | return -1; |
383 | 402 | } |
384 | 403 | source_addr = (struct sockaddr_in *)service->msrc_addr->ai_addr; |
385 | 404 | is_ssm = 1; |
386 | 405 | nsrcs = 1; |
387 | 406 | } |
388 | 407 |
|
389 | | - /* Create raw socket for IGMP */ |
390 | | - raw_sock = socket(AF_INET, SOCK_RAW, IPPROTO_IGMP); |
| 408 | + raw_sock = create_igmp_raw_socket(); |
391 | 409 | 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); |
402 | 410 | return -1; |
403 | 411 | } |
404 | 412 |
|
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)); |
408 | 418 |
|
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"); |
424 | 422 |
|
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); |
433 | 433 | } |
| 434 | + } else { |
| 435 | + logger(LOG_DEBUG, "Skipping IGMPv2 report for SSM subscription"); |
434 | 436 | } |
435 | 437 |
|
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); |
445 | 442 |
|
446 | | - /* Group Record */ |
447 | | - grec = (struct igmpv3_grec *)(packet + sizeof(struct igmpv3_report)); |
| 443 | + grec = (struct igmpv3_grec *)(packet_v3 + sizeof(struct igmpv3_report)); |
448 | 444 |
|
449 | 445 | if (is_ssm) { |
450 | | - /* Source-Specific Multicast: MODE_IS_INCLUDE with source list */ |
451 | 446 | grec->grec_type = IGMPV3_MODE_IS_INCLUDE; |
452 | | - grec->grec_auxwords = 0; |
453 | 447 | grec->grec_nsrcs = htons(nsrcs); |
454 | 448 | grec->grec_mca = group_addr; |
455 | | - |
456 | | - /* Add source address to the source list */ |
457 | 449 | uint32_t *src_list = |
458 | 450 | (uint32_t *)((uint8_t *)grec + sizeof(struct igmpv3_grec)); |
459 | 451 | 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); |
463 | 454 | } 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); |
474 | 458 | } |
475 | 459 |
|
476 | | - /* Calculate checksum */ |
477 | | - report->csum = calculate_checksum(packet, packet_len); |
| 460 | + report_v3->csum = calculate_checksum(packet_v3, packet_len_v3); |
478 | 461 |
|
479 | | - /* Destination: 224.0.0.22 (IGMPv3 reports destination) */ |
480 | 462 | memset(&dest, 0, sizeof(dest)); |
481 | 463 | dest.sin_family = AF_INET; |
482 | 464 | dest.sin_addr.s_addr = inet_addr("224.0.0.22"); |
483 | 465 |
|
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) { |
489 | 468 | 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 | + } |
492 | 486 | } |
493 | 487 |
|
494 | 488 | close(raw_sock); |
495 | 489 |
|
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; |
506 | 492 | } 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"); |
510 | 494 | } |
511 | 495 |
|
512 | | - return 0; |
| 496 | + return result; |
513 | 497 | } |
0 commit comments