fips-core 0.4.2

Reusable FIPS mesh, endpoint, transport, and protocol library
Documentation
use super::{MAX_WEBRTC_CANDIDATE_LENGTH, TransportError, debug};
use crate::config::{
    MAX_WEBRTC_HOST_CANDIDATE_SOCKETS, MAX_WEBRTC_LOCAL_CANDIDATE_LINES,
    MAX_WEBRTC_LOCAL_CANDIDATE_ROUTES, MAX_WEBRTC_REMOTE_CANDIDATE_LINES,
    MAX_WEBRTC_REMOTE_CANDIDATE_ROUTES,
};
use ::webrtc::api::APIBuilder;
use ::webrtc::api::media_engine::MediaEngine;
use ::webrtc::api::setting_engine::SettingEngine;
use ::webrtc::ice::candidate::Candidate;
use ::webrtc::ice::candidate::candidate_base::unmarshal_candidate;
use ::webrtc::ice::mdns::MulticastDnsMode;
use ::webrtc::ice::network_type::NetworkType;
use if_addrs::IfOperStatus;
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::net::{IpAddr, Ipv4Addr};
use std::sync::Arc;

#[derive(Clone)]
pub(super) struct CandidateAddressPolicy {
    source: CandidateAddressSource,
    profile: CandidateNetworkProfile,
}

#[derive(Clone)]
enum CandidateAddressSource {
    System,
    #[cfg(test)]
    Snapshot(Vec<LocalCandidateAddress>),
    #[cfg(test)]
    Fixed(Vec<IpAddr>),
}

#[derive(Clone, Copy)]
enum CandidateNetworkProfile {
    Production,
    #[cfg(test)]
    LoopbackUdp4,
}

#[derive(Clone, Debug)]
struct LocalCandidateAddress {
    interface: String,
    index: Option<u32>,
    ip: IpAddr,
    is_p2p: bool,
    oper_status: IfOperStatus,
}

impl CandidateAddressPolicy {
    pub(super) fn system() -> Self {
        Self {
            source: CandidateAddressSource::System,
            profile: CandidateNetworkProfile::Production,
        }
    }

    #[cfg(test)]
    pub(super) fn loopback_udp4() -> Self {
        Self {
            source: CandidateAddressSource::Fixed(vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]),
            profile: CandidateNetworkProfile::LoopbackUdp4,
        }
    }

    #[cfg(test)]
    pub(super) fn from_test_snapshot(
        addresses: impl IntoIterator<Item = (String, IpAddr, bool, u32)>,
    ) -> Self {
        Self {
            source: CandidateAddressSource::Snapshot(
                addresses
                    .into_iter()
                    .map(|(interface, ip, is_p2p, index)| LocalCandidateAddress {
                        interface,
                        index: Some(index),
                        ip,
                        is_p2p,
                        oper_status: IfOperStatus::Up,
                    })
                    .collect(),
            ),
            profile: CandidateNetworkProfile::Production,
        }
    }

    #[cfg(test)]
    pub(super) fn from_test_status_snapshot(
        addresses: impl IntoIterator<Item = (String, IpAddr, bool, u32, IfOperStatus)>,
    ) -> Self {
        Self {
            source: CandidateAddressSource::Snapshot(
                addresses
                    .into_iter()
                    .map(
                        |(interface, ip, is_p2p, index, oper_status)| LocalCandidateAddress {
                            interface,
                            index: Some(index),
                            ip,
                            is_p2p,
                            oper_status,
                        },
                    )
                    .collect(),
            ),
            profile: CandidateNetworkProfile::Production,
        }
    }

    pub(super) fn build_api(&self) -> Result<Arc<::webrtc::api::API>, TransportError> {
        self.build_api_inner(None)
    }

    #[cfg(test)]
    pub(super) fn build_api_with_vnet(
        &self,
        vnet: Arc<::webrtc::util::vnet::net::Net>,
    ) -> Result<Arc<::webrtc::api::API>, TransportError> {
        self.build_api_inner(Some(vnet))
    }

    #[cfg(test)]
    pub(super) fn selected_ips_for_test(&self) -> Result<Vec<IpAddr>, TransportError> {
        self.selected_ips()
    }

    fn build_api_inner(
        &self,
        vnet: Option<Arc<::webrtc::util::vnet::net::Net>>,
    ) -> Result<Arc<::webrtc::api::API>, TransportError> {
        let selected = self.selected_ips()?;
        debug!(
            selected_candidate_addresses = selected.len(),
            max_candidate_addresses = MAX_WEBRTC_HOST_CANDIDATE_SOCKETS,
            "built immutable WebRTC candidate-address generation"
        );
        let allowed: HashSet<_> = selected.into_iter().collect();
        let mut media_engine = MediaEngine::default();
        media_engine
            .register_default_codecs()
            .map_err(|error| TransportError::StartFailed(error.to_string()))?;
        let mut setting_engine = SettingEngine::default();
        setting_engine.set_ice_multicast_dns_mode(MulticastDnsMode::Disabled);
        setting_engine.set_ip_filter(Box::new(move |ip| allowed.contains(&ip)));
        match self.profile {
            CandidateNetworkProfile::Production => {
                setting_engine.set_network_types(vec![NetworkType::Udp4, NetworkType::Udp6]);
            }
            #[cfg(test)]
            CandidateNetworkProfile::LoopbackUdp4 => {
                setting_engine.set_include_loopback_candidate(true);
                setting_engine.set_network_types(vec![NetworkType::Udp4]);
            }
        }
        if let Some(vnet) = vnet {
            setting_engine.set_vnet(Some(vnet));
        }
        Ok(Arc::new(
            APIBuilder::new()
                .with_media_engine(media_engine)
                .with_setting_engine(setting_engine)
                .build(),
        ))
    }

    fn selected_ips(&self) -> Result<Vec<IpAddr>, TransportError> {
        match &self.source {
            CandidateAddressSource::System => {
                let addresses = if_addrs::get_if_addrs()
                    .map_err(|error| TransportError::StartFailed(error.to_string()))?
                    .into_iter()
                    .map(|interface| LocalCandidateAddress {
                        ip: interface.ip(),
                        is_p2p: interface.is_p2p(),
                        oper_status: interface.oper_status,
                        interface: interface.name,
                        index: interface.index,
                    })
                    .collect();
                Ok(select_candidate_addresses(addresses))
            }
            #[cfg(test)]
            CandidateAddressSource::Snapshot(addresses) => {
                Ok(select_candidate_addresses(addresses.clone()))
            }
            #[cfg(test)]
            CandidateAddressSource::Fixed(addresses) => Ok(addresses.clone()),
        }
    }
}

fn select_candidate_addresses(addresses: Vec<LocalCandidateAddress>) -> Vec<IpAddr> {
    let mut per_interface_family: HashMap<(String, bool), LocalCandidateAddress> = HashMap::new();
    for candidate in addresses.into_iter().filter(is_usable_candidate) {
        let key = (candidate.interface.clone(), candidate.ip.is_ipv6());
        match per_interface_family.get_mut(&key) {
            Some(existing) if local_address_cmp(&candidate, existing) == Ordering::Less => {
                *existing = candidate;
            }
            None => {
                per_interface_family.insert(key, candidate);
            }
            _ => {}
        }
    }
    let mut representatives: Vec<_> = per_interface_family.into_values().collect();
    representatives.sort_by(local_address_cmp);

    let mut selected = Vec::with_capacity(MAX_WEBRTC_HOST_CANDIDATE_SOCKETS);
    let mut selected_ips = HashSet::new();
    take_matching(
        &representatives,
        &mut selected,
        &mut selected_ips,
        1,
        |item| !item.is_p2p && item.ip.is_ipv4(),
    );
    take_matching(
        &representatives,
        &mut selected,
        &mut selected_ips,
        1,
        |item| !item.is_p2p && item.ip.is_ipv6(),
    );
    take_matching(
        &representatives,
        &mut selected,
        &mut selected_ips,
        1,
        |item| item.is_p2p && item.ip.is_ipv4(),
    );
    take_matching(
        &representatives,
        &mut selected,
        &mut selected_ips,
        1,
        |item| item.is_p2p && item.ip.is_ipv6(),
    );
    let remaining = MAX_WEBRTC_HOST_CANDIDATE_SOCKETS.saturating_sub(selected.len());
    take_matching(
        &representatives,
        &mut selected,
        &mut selected_ips,
        remaining,
        |_| true,
    );
    debug_assert!(selected.len() <= MAX_WEBRTC_HOST_CANDIDATE_SOCKETS);
    selected
}

fn take_matching(
    candidates: &[LocalCandidateAddress],
    selected: &mut Vec<IpAddr>,
    selected_ips: &mut HashSet<IpAddr>,
    limit: usize,
    predicate: impl Fn(&LocalCandidateAddress) -> bool,
) {
    let mut taken = 0usize;
    for candidate in candidates {
        if selected.len() >= MAX_WEBRTC_HOST_CANDIDATE_SOCKETS || taken >= limit {
            return;
        }
        if predicate(candidate) && selected_ips.insert(candidate.ip) {
            selected.push(candidate.ip);
            taken += 1;
        }
    }
}

fn is_usable_candidate(candidate: &LocalCandidateAddress) -> bool {
    if matches!(
        candidate.oper_status,
        IfOperStatus::Down | IfOperStatus::NotPresent | IfOperStatus::LowerLayerDown
    ) {
        return false;
    }
    match candidate.ip {
        IpAddr::V4(ip) => {
            !ip.is_unspecified()
                && !ip.is_loopback()
                && !ip.is_link_local()
                && !ip.is_multicast()
                && ip != Ipv4Addr::BROADCAST
        }
        IpAddr::V6(ip) => {
            !ip.is_unspecified()
                && !ip.is_loopback()
                && !ip.is_unicast_link_local()
                && !ip.is_multicast()
        }
    }
}

fn local_address_cmp(left: &LocalCandidateAddress, right: &LocalCandidateAddress) -> Ordering {
    oper_status_rank(&left.oper_status)
        .cmp(&oper_status_rank(&right.oper_status))
        .then_with(|| address_scope_rank(left.ip).cmp(&address_scope_rank(right.ip)))
        .then_with(|| {
            left.index
                .unwrap_or(u32::MAX)
                .cmp(&right.index.unwrap_or(u32::MAX))
        })
        .then_with(|| left.interface.cmp(&right.interface))
        .then_with(|| left.ip.cmp(&right.ip))
}

fn oper_status_rank(status: &IfOperStatus) -> u8 {
    match status {
        IfOperStatus::Up => 0,
        IfOperStatus::Unknown => 1,
        IfOperStatus::Dormant => 2,
        IfOperStatus::Testing => 3,
        IfOperStatus::Down | IfOperStatus::NotPresent | IfOperStatus::LowerLayerDown => 4,
    }
}

fn address_scope_rank(ip: IpAddr) -> u8 {
    match ip {
        IpAddr::V4(ip) if ip.is_private() => 1,
        IpAddr::V6(ip) if ip.segments()[0] & 0xfe00 == 0xfc00 => 1,
        _ => 0,
    }
}

#[cfg(test)]
pub(super) fn build_webrtc_api() -> Result<Arc<::webrtc::api::API>, TransportError> {
    CandidateAddressPolicy::system().build_api()
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) struct EmbeddedCandidateCount {
    pub(super) raw_lines: usize,
    pub(super) unique_routes: usize,
}

#[derive(Clone, Copy)]
pub(super) enum EmbeddedCandidateScope {
    Local,
    Remote,
}

impl EmbeddedCandidateScope {
    fn limits(self) -> (usize, usize) {
        match self {
            Self::Local => (
                MAX_WEBRTC_LOCAL_CANDIDATE_ROUTES,
                MAX_WEBRTC_LOCAL_CANDIDATE_LINES,
            ),
            Self::Remote => (
                MAX_WEBRTC_REMOTE_CANDIDATE_ROUTES,
                MAX_WEBRTC_REMOTE_CANDIDATE_LINES,
            ),
        }
    }
}

pub(super) fn validate_embedded_ice_candidates(
    sdp: &str,
    scope: EmbeddedCandidateScope,
) -> Result<EmbeddedCandidateCount, TransportError> {
    let (max_routes, max_lines) = scope.limits();
    let mut raw_lines = 0usize;
    let mut unique = HashSet::new();
    for line in sdp.lines() {
        let Some(raw_candidate) = line.trim_start().strip_prefix("a=candidate:") else {
            continue;
        };
        raw_lines += 1;
        if raw_lines > max_lines || raw_candidate.len() > MAX_WEBRTC_CANDIDATE_LENGTH {
            return Err(TransportError::InvalidAddress(
                "WebRTC embedded candidate lines exceed limits".into(),
            ));
        }
        let candidate = unmarshal_candidate(raw_candidate).map_err(|error| {
            TransportError::InvalidAddress(format!("invalid WebRTC ICE candidate: {error}"))
        })?;
        let related = candidate
            .related_address()
            .map(|address| format!("{}:{}", address.address.to_ascii_lowercase(), address.port))
            .unwrap_or_default();
        unique.insert(format!(
            "{}|{}|{}|{}|{}|{}",
            candidate.network_type(),
            candidate.candidate_type(),
            candidate.address().to_ascii_lowercase(),
            candidate.port(),
            candidate.tcp_type(),
            related,
        ));
        if unique.len() > max_routes {
            return Err(TransportError::InvalidAddress(format!(
                "WebRTC embedded SDP exceeds {max_routes} unique ICE routes"
            )));
        }
    }
    Ok(EmbeddedCandidateCount {
        raw_lines,
        unique_routes: unique.len(),
    })
}