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")]
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,
}
}
}
#[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());
}
}