openrtc 2.3.0

OpenRTC: a Rust-first P2P runtime for device discovery, signaling, and iroh/QUIC networking.
Documentation
//! Generation-fenced control frames for packet-carrier preparation.
//!
//! These frames travel only over an already-admitted OpenRTC control stream.
//! They negotiate route-local carrier objects; they do not admit peers,
//! authorize product traffic, or install a physical replacement.

use crate::{
    iroh_carrier::CarrierFrameExpectation,
    iroh_carrier_kind::{EXPERIMENTAL_MOQ_TRANSPORT_ID, EXPERIMENTAL_WEBRTC_TRANSPORT_ID},
};

pub const CARRIER_BOOTSTRAP_PROTOCOL_VERSION: u8 = 1;
pub const CARRIER_BOOTSTRAP_FRAME_TYPE: &str = "#openrtc-iroh-carrier";

#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CarrierBootstrapKind {
    Ble,
    WebRtc,
    MoqDraft14,
}

impl CarrierBootstrapKind {
    pub fn transport_id(self) -> u64 {
        match self {
            Self::Ble => 0x42_4c_45,
            Self::WebRtc => EXPERIMENTAL_WEBRTC_TRANSPORT_ID,
            Self::MoqDraft14 => EXPERIMENTAL_MOQ_TRANSPORT_ID,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CarrierBootstrapAction {
    Request,
    Ready,
    Failed,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
/// Process-local fence issued with a carrier request. The peer echoes every
/// field; only the issuing process compares the echo with its lifecycle state.
pub struct CarrierGenerationFence {
    pub transport_stable_id: u64,
    pub transport_generation: u64,
    pub route_generation: u64,
}

#[cfg(not(target_arch = "wasm32"))]
impl From<crate::client::NativePeerDataGeneration> for CarrierGenerationFence {
    fn from(value: crate::client::NativePeerDataGeneration) -> Self {
        Self {
            transport_stable_id: value.transport_stable_id,
            transport_generation: value.transport_generation,
            route_generation: value.route_generation,
        }
    }
}

/// One carrier bootstrap attempt.
///
/// `failure_code` is intentionally typed and bounded. Adapter errors, relay
/// tokens, SDP, candidates, and other potentially sensitive diagnostics do not
/// belong in this envelope.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CarrierBootstrapFrame {
    #[serde(rename = "type")]
    pub frame_type: String,
    pub protocol_version: u8,
    pub carrier: CarrierBootstrapKind,
    pub action: CarrierBootstrapAction,
    pub upgrade_id: String,
    pub carrier_session_id: String,
    pub attempt: u8,
    pub base: CarrierGenerationFence,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub failure_code: Option<String>,
}

impl CarrierBootstrapFrame {
    pub fn request(
        carrier: CarrierBootstrapKind,
        base: CarrierGenerationFence,
        attempt: u8,
    ) -> Result<Self, CarrierBootstrapError> {
        let mut upgrade_id = [0u8; 16];
        let mut carrier_session_id = [0u8; 16];
        getrandom::getrandom(&mut upgrade_id).map_err(|_| CarrierBootstrapError::Entropy)?;
        getrandom::getrandom(&mut carrier_session_id)
            .map_err(|_| CarrierBootstrapError::Entropy)?;
        Ok(Self::request_with_ids(
            carrier,
            base,
            attempt,
            upgrade_id,
            carrier_session_id,
        ))
    }

    pub fn request_with_ids(
        carrier: CarrierBootstrapKind,
        base: CarrierGenerationFence,
        attempt: u8,
        upgrade_id: [u8; 16],
        carrier_session_id: [u8; 16],
    ) -> Self {
        Self {
            frame_type: CARRIER_BOOTSTRAP_FRAME_TYPE.to_string(),
            protocol_version: CARRIER_BOOTSTRAP_PROTOCOL_VERSION,
            carrier,
            action: CarrierBootstrapAction::Request,
            upgrade_id: hex::encode(upgrade_id),
            carrier_session_id: hex::encode(carrier_session_id),
            attempt,
            base,
            failure_code: None,
        }
    }

    pub fn ready_from(request: &Self) -> Result<Self, CarrierBootstrapError> {
        request.validate()?;
        if request.action != CarrierBootstrapAction::Request {
            return Err(CarrierBootstrapError::InvalidTransition);
        }
        let mut ready = request.clone();
        ready.action = CarrierBootstrapAction::Ready;
        Ok(ready)
    }

    pub fn failed_from(
        request: &Self,
        failure_code: impl Into<String>,
    ) -> Result<Self, CarrierBootstrapError> {
        request.validate()?;
        let failure_code = failure_code.into();
        if request.action != CarrierBootstrapAction::Request
            || failure_code.is_empty()
            || failure_code.len() > 96
            || !failure_code
                .bytes()
                .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
        {
            return Err(CarrierBootstrapError::InvalidFailureCode);
        }
        let mut failed = request.clone();
        failed.action = CarrierBootstrapAction::Failed;
        failed.failure_code = Some(failure_code);
        Ok(failed)
    }

    pub fn validate(&self) -> Result<(), CarrierBootstrapError> {
        if self.frame_type != CARRIER_BOOTSTRAP_FRAME_TYPE {
            return Err(CarrierBootstrapError::WrongFrameType);
        }
        if self.protocol_version != CARRIER_BOOTSTRAP_PROTOCOL_VERSION {
            return Err(CarrierBootstrapError::UnsupportedVersion(
                self.protocol_version,
            ));
        }
        if self.attempt == 0 {
            return Err(CarrierBootstrapError::InvalidAttempt);
        }
        decode_id(&self.upgrade_id).ok_or(CarrierBootstrapError::InvalidUpgradeId)?;
        decode_id(&self.carrier_session_id)
            .ok_or(CarrierBootstrapError::InvalidCarrierSessionId)?;
        match self.action {
            CarrierBootstrapAction::Request | CarrierBootstrapAction::Ready
                if self.failure_code.is_some() =>
            {
                Err(CarrierBootstrapError::UnexpectedFailureCode)
            }
            CarrierBootstrapAction::Failed if self.failure_code.is_none() => {
                Err(CarrierBootstrapError::InvalidFailureCode)
            }
            _ => Ok(()),
        }
    }

    pub fn is_current(
        &self,
        carrier: CarrierBootstrapKind,
        base: CarrierGenerationFence,
        attempt: u8,
        upgrade_id: &str,
    ) -> bool {
        self.validate().is_ok()
            && self.carrier == carrier
            && self.base == base
            && self.attempt == attempt
            && self.upgrade_id == upgrade_id
    }

    pub fn is_response_to(&self, request: &Self) -> bool {
        self.validate().is_ok()
            && request.validate().is_ok()
            && request.action == CarrierBootstrapAction::Request
            && matches!(
                self.action,
                CarrierBootstrapAction::Ready | CarrierBootstrapAction::Failed
            )
            && self.carrier == request.carrier
            && self.upgrade_id == request.upgrade_id
            && self.carrier_session_id == request.carrier_session_id
            && self.attempt == request.attempt
            && self.base == request.base
    }

    pub fn frame_expectation(&self) -> Result<CarrierFrameExpectation, CarrierBootstrapError> {
        self.validate()?;
        let session = decode_id(&self.carrier_session_id)
            .ok_or(CarrierBootstrapError::InvalidCarrierSessionId)?;
        Ok(CarrierFrameExpectation {
            transport_id: self.carrier.transport_id(),
            carrier_session_id: session,
            transport_generation: self.base.transport_generation.saturating_add(1),
        })
    }

    pub fn from_json(value: &serde_json::Value) -> Option<Self> {
        if value.get("type").and_then(serde_json::Value::as_str)
            != Some(CARRIER_BOOTSTRAP_FRAME_TYPE)
        {
            return None;
        }
        serde_json::from_value::<Self>(value.clone())
            .ok()
            .filter(|frame| frame.validate().is_ok())
    }
}

fn decode_id(value: &str) -> Option<[u8; 16]> {
    if value.len() != 32
        || !value
            .bytes()
            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
    {
        return None;
    }
    let decoded = hex::decode(value).ok()?;
    decoded.try_into().ok()
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CarrierBootstrapError {
    Entropy,
    WrongFrameType,
    UnsupportedVersion(u8),
    InvalidAttempt,
    InvalidUpgradeId,
    InvalidCarrierSessionId,
    InvalidTransition,
    InvalidFailureCode,
    UnexpectedFailureCode,
}

impl std::fmt::Display for CarrierBootstrapError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Entropy => formatter.write_str("carrier bootstrap entropy unavailable"),
            Self::WrongFrameType => formatter.write_str("wrong carrier bootstrap frame type"),
            Self::UnsupportedVersion(version) => {
                write!(formatter, "unsupported carrier bootstrap version {version}")
            }
            Self::InvalidAttempt => {
                formatter.write_str("carrier bootstrap attempt must be non-zero")
            }
            Self::InvalidUpgradeId => formatter.write_str("invalid carrier upgrade id"),
            Self::InvalidCarrierSessionId => formatter.write_str("invalid carrier session id"),
            Self::InvalidTransition => formatter.write_str("invalid carrier bootstrap transition"),
            Self::InvalidFailureCode => formatter.write_str("invalid carrier failure code"),
            Self::UnexpectedFailureCode => {
                formatter.write_str("failure code is not allowed for this carrier action")
            }
        }
    }
}

impl std::error::Error for CarrierBootstrapError {}

#[cfg(test)]
mod tests {
    use super::*;

    fn generation() -> CarrierGenerationFence {
        CarrierGenerationFence {
            transport_stable_id: 41,
            transport_generation: 7,
            route_generation: 3,
        }
    }

    fn request(carrier: CarrierBootstrapKind, attempt: u8) -> CarrierBootstrapFrame {
        CarrierBootstrapFrame::request_with_ids(
            carrier,
            generation(),
            attempt,
            [attempt; 16],
            [attempt.saturating_add(1); 16],
        )
    }

    #[test]
    fn request_ready_round_trip_retains_complete_fence() {
        let request = request(CarrierBootstrapKind::WebRtc, 2);
        let ready = CarrierBootstrapFrame::ready_from(&request).unwrap();
        let value = serde_json::to_value(&ready).unwrap();
        let decoded = CarrierBootstrapFrame::from_json(&value).unwrap();

        assert!(decoded.is_current(
            CarrierBootstrapKind::WebRtc,
            generation(),
            2,
            &request.upgrade_id
        ));
        assert_eq!(
            decoded.frame_expectation().unwrap().transport_generation,
            generation().transport_generation + 1
        );
        assert!(decoded.is_response_to(&request));
    }

    #[test]
    fn response_must_match_session_and_complete_generation_fence() {
        let request = request(CarrierBootstrapKind::WebRtc, 2);
        let mut ready = CarrierBootstrapFrame::ready_from(&request).unwrap();
        ready.carrier_session_id = "00".repeat(16);
        assert!(!ready.is_response_to(&request));

        let mut ready = CarrierBootstrapFrame::ready_from(&request).unwrap();
        ready.base.route_generation += 1;
        assert!(!ready.is_response_to(&request));

        let failed = CarrierBootstrapFrame::failed_from(&request, "carrier-timeout").unwrap();
        assert!(failed.is_response_to(&request));
    }

    #[test]
    fn stale_generation_and_attempt_are_not_current() {
        let request = request(CarrierBootstrapKind::MoqDraft14, 1);
        let mut replacement = generation();
        replacement.transport_generation += 1;

        assert!(!request.is_current(
            CarrierBootstrapKind::MoqDraft14,
            replacement,
            1,
            &request.upgrade_id
        ));
        assert!(!request.is_current(
            CarrierBootstrapKind::MoqDraft14,
            generation(),
            2,
            &request.upgrade_id
        ));
    }

    #[test]
    fn failures_are_typed_and_do_not_accept_diagnostics() {
        let request = request(CarrierBootstrapKind::WebRtc, 1);
        assert!(CarrierBootstrapFrame::failed_from(&request, "relay-unavailable").is_ok());
        assert!(CarrierBootstrapFrame::failed_from(&request, "token=secret value").is_err());
    }

    #[test]
    fn malformed_bootstrap_json_is_not_claimed() {
        let request = request(CarrierBootstrapKind::WebRtc, 1);
        let mut value = serde_json::to_value(request).unwrap();
        value["carrierSessionId"] = serde_json::Value::String("not-an-id".to_string());
        assert!(CarrierBootstrapFrame::from_json(&value).is_none());
    }
}