#![allow(dead_code)]
#![allow(clippy::too_many_arguments)]
#[derive(Debug, Clone)]
pub struct MulticastGroup {
pub address: String,
pub port: u16,
pub source: Option<String>,
pub ttl: u8,
}
impl MulticastGroup {
#[must_use]
pub fn new(address: impl Into<String>, port: u16) -> Self {
Self {
address: address.into(),
port,
source: None,
ttl: 1,
}
}
#[must_use]
pub fn with_source(mut self, source: impl Into<String>) -> Self {
self.source = Some(source.into());
self
}
#[must_use]
pub const fn with_ttl(mut self, ttl: u8) -> Self {
self.ttl = ttl;
self
}
#[must_use]
pub fn is_valid_address(addr: &str) -> bool {
let octets: Vec<&str> = addr.split('.').collect();
if octets.len() != 4 {
return false;
}
if let Ok(first) = octets[0].parse::<u8>() {
return first >= 224 && first <= 239;
}
false
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IgmpVersion {
V1,
V2,
V3,
}
#[derive(Debug, Clone)]
pub struct MulticastJoinRequest {
pub group: MulticastGroup,
pub interface: String,
pub igmp_version: IgmpVersion,
}
impl MulticastJoinRequest {
#[must_use]
pub fn new(
group: MulticastGroup,
interface: impl Into<String>,
igmp_version: IgmpVersion,
) -> Self {
Self {
group,
interface: interface.into(),
igmp_version,
}
}
}
#[derive(Debug, Clone)]
pub struct SsmChannel {
pub source_ip: String,
pub group_ip: String,
pub port: u16,
}
impl SsmChannel {
#[must_use]
pub fn new(source_ip: impl Into<String>, group_ip: impl Into<String>, port: u16) -> Self {
Self {
source_ip: source_ip.into(),
group_ip: group_ip.into(),
port,
}
}
#[must_use]
pub fn is_valid(&self) -> bool {
MulticastGroup::is_valid_address(&self.group_ip)
}
}
#[derive(Debug, Clone, Default)]
pub struct MulticastStats {
pub packets_received: u64,
pub bytes_received: u64,
pub join_count: u32,
pub last_packet_ms: u64,
}
#[derive(Debug, Default)]
pub struct MulticastManager {
active: Vec<(MulticastGroup, MulticastStats)>,
}
impl MulticastManager {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn join(&mut self, group: MulticastGroup) -> bool {
if !MulticastGroup::is_valid_address(&group.address) {
return false;
}
if let Some((_g, stats)) = self
.active
.iter_mut()
.find(|(g, _)| g.address == group.address && g.port == group.port)
{
stats.join_count += 1;
return true;
}
let mut stats = MulticastStats::default();
stats.join_count = 1;
self.active.push((group, stats));
true
}
pub fn leave(&mut self, group: &MulticastGroup) {
self.active
.retain(|(g, _)| !(g.address == group.address && g.port == group.port));
}
#[must_use]
pub fn active_groups(&self) -> Vec<&MulticastGroup> {
self.active.iter().map(|(g, _)| g).collect()
}
#[must_use]
pub fn stats_for(&self, address: &str, port: u16) -> Option<&MulticastStats> {
self.active
.iter()
.find(|(g, _)| g.address == address && g.port == port)
.map(|(_, s)| s)
}
pub fn record_packet(&mut self, address: &str, port: u16, bytes: u64, now_ms: u64) {
if let Some((_g, stats)) = self
.active
.iter_mut()
.find(|(g, _)| g.address == address && g.port == port)
{
stats.packets_received += 1;
stats.bytes_received += bytes;
stats.last_packet_ms = now_ms;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_valid_address_valid() {
assert!(MulticastGroup::is_valid_address("224.0.0.1"));
assert!(MulticastGroup::is_valid_address("239.255.255.255"));
assert!(MulticastGroup::is_valid_address("232.1.2.3"));
}
#[test]
fn test_is_valid_address_invalid() {
assert!(!MulticastGroup::is_valid_address("192.168.1.1"));
assert!(!MulticastGroup::is_valid_address("10.0.0.1"));
assert!(!MulticastGroup::is_valid_address("255.255.255.255"));
assert!(!MulticastGroup::is_valid_address("0.0.0.0"));
assert!(!MulticastGroup::is_valid_address("not.an.ip"));
assert!(!MulticastGroup::is_valid_address("223.255.255.255")); }
#[test]
fn test_multicast_group_new() {
let g = MulticastGroup::new("239.255.0.1", 5004);
assert_eq!(g.address, "239.255.0.1");
assert_eq!(g.port, 5004);
assert_eq!(g.ttl, 1);
assert!(g.source.is_none());
}
#[test]
fn test_multicast_group_with_source() {
let g = MulticastGroup::new("239.255.0.1", 5004)
.with_source("10.0.0.1")
.with_ttl(8);
assert_eq!(g.source, Some("10.0.0.1".to_string()));
assert_eq!(g.ttl, 8);
}
#[test]
fn test_igmp_version_variants() {
assert_ne!(IgmpVersion::V1, IgmpVersion::V2);
assert_ne!(IgmpVersion::V2, IgmpVersion::V3);
assert_eq!(IgmpVersion::V3, IgmpVersion::V3);
}
#[test]
fn test_multicast_join_request() {
let group = MulticastGroup::new("239.1.0.1", 1234);
let req = MulticastJoinRequest::new(group, "eth0", IgmpVersion::V3);
assert_eq!(req.interface, "eth0");
assert_eq!(req.igmp_version, IgmpVersion::V3);
}
#[test]
fn test_ssm_channel_valid() {
let ch = SsmChannel::new("10.0.0.1", "232.1.2.3", 5004);
assert!(ch.is_valid());
}
#[test]
fn test_ssm_channel_invalid_group() {
let ch = SsmChannel::new("10.0.0.1", "192.168.1.1", 5004);
assert!(!ch.is_valid());
}
#[test]
fn test_multicast_manager_join_leave() {
let mut mgr = MulticastManager::new();
let g = MulticastGroup::new("239.1.0.1", 5004);
assert!(mgr.join(g.clone()));
assert_eq!(mgr.active_groups().len(), 1);
mgr.leave(&g);
assert_eq!(mgr.active_groups().len(), 0);
}
#[test]
fn test_multicast_manager_reject_invalid() {
let mut mgr = MulticastManager::new();
let bad = MulticastGroup::new("10.0.0.1", 5004);
assert!(!mgr.join(bad));
assert_eq!(mgr.active_groups().len(), 0);
}
#[test]
fn test_multicast_manager_double_join() {
let mut mgr = MulticastManager::new();
let g = MulticastGroup::new("239.1.0.1", 5004);
mgr.join(g.clone());
mgr.join(g.clone());
assert_eq!(mgr.active_groups().len(), 1);
let stats = mgr
.stats_for("239.1.0.1", 5004)
.expect("should succeed in test");
assert_eq!(stats.join_count, 2);
}
#[test]
fn test_multicast_stats_record_packet() {
let mut mgr = MulticastManager::new();
let g = MulticastGroup::new("239.1.0.1", 5004);
mgr.join(g);
mgr.record_packet("239.1.0.1", 5004, 1316, 1_000);
mgr.record_packet("239.1.0.1", 5004, 1316, 2_000);
let stats = mgr
.stats_for("239.1.0.1", 5004)
.expect("should succeed in test");
assert_eq!(stats.packets_received, 2);
assert_eq!(stats.bytes_received, 2632);
assert_eq!(stats.last_packet_ms, 2_000);
}
}