use crate::config::{OutgoingInterface, PublicationAddressFamily};
use crate::error::MctxError;
use std::net::IpAddr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum RawValidationMode {
#[default]
StrictMulticastDestination,
AllowAnyDestination,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RawPublicationConfig {
pub family: Option<PublicationAddressFamily>,
pub outgoing_interface: Option<OutgoingInterface>,
pub bind_addr: Option<IpAddr>,
pub ttl: Option<u8>,
pub loopback: Option<bool>,
pub validation_mode: RawValidationMode,
}
impl Default for RawPublicationConfig {
fn default() -> Self {
Self::new()
}
}
impl RawPublicationConfig {
pub fn new() -> Self {
Self {
family: None,
outgoing_interface: None,
bind_addr: None,
ttl: None,
loopback: None,
validation_mode: RawValidationMode::StrictMulticastDestination,
}
}
pub fn ipv4() -> Self {
Self::new().with_family(PublicationAddressFamily::Ipv4)
}
pub fn ipv6() -> Self {
Self::new().with_family(PublicationAddressFamily::Ipv6)
}
pub fn validate(&self) -> Result<(), MctxError> {
if let Some(bind_addr) = self.bind_addr {
if bind_addr.is_multicast() || bind_addr.is_unspecified() {
return Err(MctxError::InvalidRawBindAddress);
}
if let Some(family) = self.family
&& !family_matches_ip(family, bind_addr)
{
return Err(MctxError::RawBindAddressFamilyMismatch);
}
}
if let Some(outgoing_interface) = self.outgoing_interface {
match outgoing_interface {
OutgoingInterface::Ipv4Addr(interface) => {
if interface.is_multicast() || interface.is_unspecified() {
return Err(MctxError::InvalidInterfaceAddress);
}
if matches!(self.family, Some(PublicationAddressFamily::Ipv6)) {
return Err(MctxError::OutgoingInterfaceFamilyMismatch);
}
}
OutgoingInterface::Ipv6Addr(interface) => {
if interface.is_multicast() || interface.is_unspecified() {
return Err(MctxError::InvalidInterfaceAddress);
}
if matches!(self.family, Some(PublicationAddressFamily::Ipv4)) {
return Err(MctxError::OutgoingInterfaceFamilyMismatch);
}
}
OutgoingInterface::Ipv6Index(index) => {
if index == 0 {
return Err(MctxError::InvalidIpv6InterfaceIndex);
}
if matches!(self.family, Some(PublicationAddressFamily::Ipv4)) {
return Err(MctxError::OutgoingInterfaceFamilyMismatch);
}
}
}
}
Ok(())
}
pub fn with_family(mut self, family: PublicationAddressFamily) -> Self {
self.family = Some(family);
self
}
pub fn with_outgoing_interface(
mut self,
outgoing_interface: impl Into<OutgoingInterface>,
) -> Self {
self.outgoing_interface = Some(outgoing_interface.into());
self
}
pub fn with_interface(self, interface: std::net::Ipv4Addr) -> Self {
self.with_outgoing_interface(interface)
}
pub fn with_ipv6_interface_index(mut self, interface_index: u32) -> Self {
self.outgoing_interface = Some(OutgoingInterface::Ipv6Index(interface_index));
self
}
pub fn with_bind_addr(mut self, bind_addr: impl Into<IpAddr>) -> Self {
self.bind_addr = Some(bind_addr.into());
self
}
pub fn with_ttl(mut self, ttl: u8) -> Self {
self.ttl = Some(ttl);
self
}
pub fn with_loopback(mut self, loopback: bool) -> Self {
self.loopback = Some(loopback);
self
}
pub fn with_validation_mode(mut self, validation_mode: RawValidationMode) -> Self {
self.validation_mode = validation_mode;
self
}
}
fn family_matches_ip(family: PublicationAddressFamily, ip: IpAddr) -> bool {
matches!(
(family, ip),
(PublicationAddressFamily::Ipv4, IpAddr::V4(_))
| (PublicationAddressFamily::Ipv6, IpAddr::V6(_))
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{Ipv4Addr, Ipv6Addr};
#[test]
fn valid_ipv4_raw_config_passes_validation() {
let cfg = RawPublicationConfig::ipv4()
.with_bind_addr(Ipv4Addr::new(192, 168, 1, 20))
.with_outgoing_interface(Ipv4Addr::new(192, 168, 1, 20))
.with_ttl(8);
assert!(cfg.validate().is_ok());
}
#[test]
fn valid_ipv6_raw_config_passes_validation() {
let cfg = RawPublicationConfig::ipv6()
.with_bind_addr("2001:db8::10".parse::<Ipv6Addr>().unwrap())
.with_ipv6_interface_index(7)
.with_validation_mode(RawValidationMode::AllowAnyDestination);
assert!(cfg.validate().is_ok());
}
#[test]
fn raw_bind_address_must_be_unicast() {
let cfg = RawPublicationConfig::new().with_bind_addr(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
assert!(matches!(
cfg.validate(),
Err(MctxError::InvalidRawBindAddress)
));
}
#[test]
fn raw_bind_address_family_must_match_config_family() {
let cfg = RawPublicationConfig::ipv6().with_bind_addr(Ipv4Addr::new(10, 0, 0, 1));
assert!(matches!(
cfg.validate(),
Err(MctxError::RawBindAddressFamilyMismatch)
));
}
#[test]
fn ipv4_raw_config_rejects_ipv6_interface_index() {
let cfg = RawPublicationConfig::ipv4().with_ipv6_interface_index(7);
assert!(matches!(
cfg.validate(),
Err(MctxError::OutgoingInterfaceFamilyMismatch)
));
}
}