fix(collab): report relay join failure stages
This commit is contained in:
parent
7f8340f2a0
commit
15ff2bea43
|
|
@ -14,7 +14,9 @@ use op_collab_transport::{
|
|||
use subtle::ConstantTimeEq;
|
||||
|
||||
use super::super::auth::{production_verifier, unix_time_ms, LocalAdmission, LocalTicketRenewer};
|
||||
use super::super::relay::{relay_guest_target, GuestConnectionRoute, GuestRelayRuntime};
|
||||
use super::super::relay::{
|
||||
relay_guest_target, report_secure_transport_failure, GuestConnectionRoute, GuestRelayRuntime,
|
||||
};
|
||||
use super::super::types::{
|
||||
CollabRuntimeFailure, GuestNetworkCommand, NetworkEvent, TerminalNetworkEvent,
|
||||
};
|
||||
|
|
@ -275,7 +277,7 @@ fn run_inner(
|
|||
let overall_deadline = Instant::now()
|
||||
.checked_add(join_budget)
|
||||
.ok_or(CollabRuntimeFailure::Transport)?;
|
||||
let (prelude, mut connection) = connect_address_sequence_cancellable(
|
||||
let connected = connect_address_sequence_cancellable(
|
||||
&addresses,
|
||||
overall_deadline,
|
||||
key.as_ref(),
|
||||
|
|
@ -283,8 +285,18 @@ fn run_inner(
|
|||
expected_remote_static.as_ref(),
|
||||
config,
|
||||
cancellation,
|
||||
)
|
||||
.map_err(|error| relay_join_failure(&error, relay_join))?;
|
||||
);
|
||||
let (prelude, mut connection) = match connected {
|
||||
Ok(connected) => connected,
|
||||
Err(error) => {
|
||||
let failure = relay_join_failure(&error, relay_join);
|
||||
if let Some(relay) = relay_runtime.as_ref() {
|
||||
let (relay_phase, relay_failure) = relay.bridge_diagnostic();
|
||||
report_secure_transport_failure(failure, relay_phase, relay_failure);
|
||||
}
|
||||
return Err(failure);
|
||||
}
|
||||
};
|
||||
let remote_static = *connection.remote_static();
|
||||
let (hello, expected_issuer, expected_subject) = {
|
||||
let local = local
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ use super::relay_bootstrap::{
|
|||
};
|
||||
use super::types::{CollabRuntimeError, CollabRuntimeFailure};
|
||||
|
||||
mod guest_runtime;
|
||||
pub(in crate::collab_runtime) use guest_runtime::report_secure_transport_failure;
|
||||
|
||||
const RELAY_HOME_REGION_ENV: &str = "OPENPENCIL_COLLAB_RELAY_HOME_REGION";
|
||||
#[cfg(any(test, debug_assertions))]
|
||||
const RELAY_DEV_UNSIGNED_ENV: &str = "OPENPENCIL_COLLAB_RELAY_DEV_UNSIGNED";
|
||||
|
|
@ -324,95 +327,6 @@ pub(super) struct GuestRelayRuntime {
|
|||
bridge: RelayGuestBridge,
|
||||
}
|
||||
|
||||
impl GuestRelayRuntime {
|
||||
pub(super) fn start(
|
||||
request: &RelayGuestRequest,
|
||||
key: std::sync::Arc<DeviceStaticKey>,
|
||||
local: std::sync::Arc<std::sync::RwLock<LocalAdmission>>,
|
||||
) -> Result<Self, CollabRuntimeFailure> {
|
||||
// Resolve and verify the invite on the guest network worker. The UI
|
||||
// only parses enough of the bounded invite to select its claimed
|
||||
// region and render status.
|
||||
let bootstrap = request.provider.load()?;
|
||||
let (invite, home_region) = match &request.secret {
|
||||
RelayJoinSecret::Invite(invite) => (invite.as_ref().clone(), request.home_region),
|
||||
RelayJoinSecret::Pairing(code) => {
|
||||
let invite = claim_pairing_invite(
|
||||
&bootstrap,
|
||||
code,
|
||||
request.control_plane.as_ref(),
|
||||
&key,
|
||||
&local,
|
||||
)?;
|
||||
let claimed_region = invite.locator().claims().home_region();
|
||||
(invite, claimed_region)
|
||||
}
|
||||
};
|
||||
let region = bootstrap.region(home_region)?;
|
||||
let endpoint = region.relay_endpoint.clone();
|
||||
let development_unsigned = development_unsigned_allowed(&endpoint);
|
||||
let now = unix_time_ms().map_err(|_| CollabRuntimeFailure::RelayUnavailable)? / 1_000;
|
||||
let route = if development_unsigned {
|
||||
invite
|
||||
.verify(&AcceptAllDevelopmentLocator, now)
|
||||
.map_err(invite_verify_failure)?
|
||||
} else {
|
||||
invite
|
||||
.verify(®ion.locator_verifier, now)
|
||||
.map_err(invite_verify_failure)?
|
||||
};
|
||||
// No region cross-check here: `home_region` is itself derived from the
|
||||
// (canonically parsed) invite claims on both branches, so the verify
|
||||
// step above is the authoritative gate.
|
||||
let expected_discovery_id = route
|
||||
.locator()
|
||||
.claims()
|
||||
.expected_discovery_id()
|
||||
.as_str()
|
||||
.to_owned();
|
||||
let expected_remote_static = *route.locator().claims().owner_noise_static().as_bytes();
|
||||
let auth = LocalAdmission::relay_auth_extension(*key.public_key())
|
||||
.map_err(|error| error.failure)?;
|
||||
let handshake = RelayHandshake::new(route, auth);
|
||||
let authenticator = if development_unsigned {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
LocalAdmission::challenge_bound_relay_authenticator(
|
||||
local,
|
||||
key,
|
||||
Arc::clone(®ion.relay_x25519_keys),
|
||||
)
|
||||
.map_err(|error| error.failure)?,
|
||||
)
|
||||
};
|
||||
let bridge = op_host_services::chat_runtime::block_on_anywhere(async move {
|
||||
if development_unsigned {
|
||||
start_development_guest_bridge(endpoint, handshake).await
|
||||
} else {
|
||||
RelayGuestBridge::start(
|
||||
endpoint,
|
||||
handshake,
|
||||
authenticator.ok_or(CollabRuntimeFailure::RelayUnavailable)?,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| CollabRuntimeFailure::RelayUnavailable)
|
||||
}
|
||||
})?;
|
||||
let local_addr = bridge.local_addr();
|
||||
Ok(Self {
|
||||
local_addr,
|
||||
expected_discovery_id,
|
||||
expected_remote_static,
|
||||
bridge,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) const fn local_addr(&self) -> SocketAddr {
|
||||
self.local_addr
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for GuestRelayRuntime {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter
|
||||
|
|
|
|||
248
crates/op-host-desktop/src/collab_runtime/relay/guest_runtime.rs
Normal file
248
crates/op-host-desktop/src/collab_runtime/relay/guest_runtime.rs
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
use super::*;
|
||||
|
||||
/// Coarse, credential-free startup stages for diagnosing a public Relay join.
|
||||
///
|
||||
/// Never add request data, endpoint URLs, invite material, identities, keys, or
|
||||
/// raw transport errors to this enum. Its `Debug` output is emitted to the
|
||||
/// local terminal when startup fails.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum RelayGuestStartupStage {
|
||||
Bootstrap,
|
||||
PairingClaim,
|
||||
RegionSelection,
|
||||
Clock,
|
||||
InviteVerification,
|
||||
LocalAuthentication,
|
||||
RelayBridge,
|
||||
}
|
||||
|
||||
struct RelayGuestStageFailure {
|
||||
stage: RelayGuestStartupStage,
|
||||
failure: CollabRuntimeFailure,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RelayGuestStageFailure {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
formatter,
|
||||
"RelayGuestStageFailed {{ stage: {:?}, failure: {:?} }}",
|
||||
self.stage, self.failure
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn at_stage<T>(
|
||||
stage: RelayGuestStartupStage,
|
||||
result: Result<T, CollabRuntimeFailure>,
|
||||
) -> Result<T, CollabRuntimeFailure> {
|
||||
at_stage_with(stage, result, |diagnostic| {
|
||||
eprintln!("[collab] {diagnostic}");
|
||||
})
|
||||
}
|
||||
|
||||
fn at_stage_with<T>(
|
||||
stage: RelayGuestStartupStage,
|
||||
result: Result<T, CollabRuntimeFailure>,
|
||||
report: impl FnOnce(RelayGuestStageFailure),
|
||||
) -> Result<T, CollabRuntimeFailure> {
|
||||
match result {
|
||||
Ok(value) => Ok(value),
|
||||
Err(failure) => {
|
||||
report(RelayGuestStageFailure { stage, failure });
|
||||
Err(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RelayGuestTransportFailure {
|
||||
failure: CollabRuntimeFailure,
|
||||
relay_phase: op_collab_relay_client::RelayBridgePhase,
|
||||
relay_failure: Option<op_collab_relay_client::RelayFailureKind>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RelayGuestTransportFailure {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
formatter,
|
||||
"RelayGuestStageFailed {{ stage: SecureTransport, failure: {:?}, relay_phase: {:?}, \
|
||||
relay_failure: {:?} }}",
|
||||
self.failure, self.relay_phase, self.relay_failure
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::collab_runtime) fn report_secure_transport_failure(
|
||||
failure: CollabRuntimeFailure,
|
||||
relay_phase: op_collab_relay_client::RelayBridgePhase,
|
||||
relay_failure: Option<op_collab_relay_client::RelayFailureKind>,
|
||||
) {
|
||||
let diagnostic = RelayGuestTransportFailure {
|
||||
failure,
|
||||
relay_phase,
|
||||
relay_failure,
|
||||
};
|
||||
eprintln!("[collab] {diagnostic}");
|
||||
}
|
||||
|
||||
impl GuestRelayRuntime {
|
||||
pub(in crate::collab_runtime) fn start(
|
||||
request: &RelayGuestRequest,
|
||||
key: Arc<DeviceStaticKey>,
|
||||
local: Arc<std::sync::RwLock<LocalAdmission>>,
|
||||
) -> Result<Self, CollabRuntimeFailure> {
|
||||
// Resolve and verify the invite on the guest network worker. The UI
|
||||
// only parses enough of the bounded invite to select its claimed
|
||||
// region and render status.
|
||||
let bootstrap = at_stage(RelayGuestStartupStage::Bootstrap, request.provider.load())?;
|
||||
let (invite, home_region) = match &request.secret {
|
||||
RelayJoinSecret::Invite(invite) => (invite.as_ref().clone(), request.home_region),
|
||||
RelayJoinSecret::Pairing(code) => {
|
||||
let invite = at_stage(
|
||||
RelayGuestStartupStage::PairingClaim,
|
||||
claim_pairing_invite(
|
||||
&bootstrap,
|
||||
code,
|
||||
request.control_plane.as_ref(),
|
||||
&key,
|
||||
&local,
|
||||
),
|
||||
)?;
|
||||
let claimed_region = invite.locator().claims().home_region();
|
||||
(invite, claimed_region)
|
||||
}
|
||||
};
|
||||
let region = at_stage(
|
||||
RelayGuestStartupStage::RegionSelection,
|
||||
bootstrap.region(home_region),
|
||||
)?;
|
||||
let endpoint = region.relay_endpoint.clone();
|
||||
let development_unsigned = development_unsigned_allowed(&endpoint);
|
||||
let now = at_stage(
|
||||
RelayGuestStartupStage::Clock,
|
||||
unix_time_ms().map_err(|_| CollabRuntimeFailure::RelayUnavailable),
|
||||
)? / 1_000;
|
||||
let route = at_stage(RelayGuestStartupStage::InviteVerification, {
|
||||
if development_unsigned {
|
||||
invite
|
||||
.verify(&AcceptAllDevelopmentLocator, now)
|
||||
.map_err(invite_verify_failure)
|
||||
} else {
|
||||
invite
|
||||
.verify(®ion.locator_verifier, now)
|
||||
.map_err(invite_verify_failure)
|
||||
}
|
||||
})?;
|
||||
// No region cross-check here: `home_region` is itself derived from the
|
||||
// (canonically parsed) invite claims on both branches, so the verify
|
||||
// step above is the authoritative gate.
|
||||
let expected_discovery_id = route
|
||||
.locator()
|
||||
.claims()
|
||||
.expected_discovery_id()
|
||||
.as_str()
|
||||
.to_owned();
|
||||
let expected_remote_static = *route.locator().claims().owner_noise_static().as_bytes();
|
||||
let auth = at_stage(
|
||||
RelayGuestStartupStage::LocalAuthentication,
|
||||
LocalAdmission::relay_auth_extension(*key.public_key()).map_err(|error| error.failure),
|
||||
)?;
|
||||
let handshake = RelayHandshake::new(route, auth);
|
||||
let authenticator = if development_unsigned {
|
||||
None
|
||||
} else {
|
||||
Some(at_stage(
|
||||
RelayGuestStartupStage::LocalAuthentication,
|
||||
LocalAdmission::challenge_bound_relay_authenticator(
|
||||
local,
|
||||
key,
|
||||
Arc::clone(®ion.relay_x25519_keys),
|
||||
)
|
||||
.map_err(|error| error.failure),
|
||||
)?)
|
||||
};
|
||||
let bridge = at_stage(
|
||||
RelayGuestStartupStage::RelayBridge,
|
||||
op_host_services::chat_runtime::block_on_anywhere(async move {
|
||||
if development_unsigned {
|
||||
start_development_guest_bridge(endpoint, handshake).await
|
||||
} else {
|
||||
RelayGuestBridge::start(
|
||||
endpoint,
|
||||
handshake,
|
||||
authenticator.ok_or(CollabRuntimeFailure::RelayUnavailable)?,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| CollabRuntimeFailure::RelayUnavailable)
|
||||
}
|
||||
}),
|
||||
)?;
|
||||
let local_addr = bridge.local_addr();
|
||||
Ok(Self {
|
||||
local_addr,
|
||||
expected_discovery_id,
|
||||
expected_remote_static,
|
||||
bridge,
|
||||
})
|
||||
}
|
||||
|
||||
pub(in crate::collab_runtime) const fn local_addr(&self) -> SocketAddr {
|
||||
self.local_addr
|
||||
}
|
||||
|
||||
pub(in crate::collab_runtime) fn bridge_diagnostic(
|
||||
&self,
|
||||
) -> (
|
||||
op_collab_relay_client::RelayBridgePhase,
|
||||
Option<op_collab_relay_client::RelayFailureKind>,
|
||||
) {
|
||||
let status = self.bridge.status();
|
||||
(status.phase, status.last_error)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn stage_wrapper_emits_only_the_safe_failure_shape() {
|
||||
let mut emitted = None;
|
||||
let result = at_stage_with::<()>(
|
||||
RelayGuestStartupStage::PairingClaim,
|
||||
Err(CollabRuntimeFailure::RelayUnavailable),
|
||||
|diagnostic| emitted = Some(diagnostic.to_string()),
|
||||
);
|
||||
|
||||
assert_eq!(result, Err(CollabRuntimeFailure::RelayUnavailable));
|
||||
assert_eq!(
|
||||
emitted.as_deref(),
|
||||
Some("RelayGuestStageFailed { stage: PairingClaim, failure: RelayUnavailable }")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn successful_stage_emits_nothing() {
|
||||
let mut emitted = false;
|
||||
let result = at_stage_with(RelayGuestStartupStage::Bootstrap, Ok(7_u8), |_| {
|
||||
emitted = true
|
||||
});
|
||||
|
||||
assert_eq!(result, Ok(7));
|
||||
assert!(!emitted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secure_transport_diagnostic_contains_only_safe_enums() {
|
||||
let diagnostic = RelayGuestTransportFailure {
|
||||
failure: CollabRuntimeFailure::RelayUnavailable,
|
||||
relay_phase: op_collab_relay_client::RelayBridgePhase::Failed,
|
||||
relay_failure: Some(op_collab_relay_client::RelayFailureKind::ConnectTimeout),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
diagnostic.to_string(),
|
||||
"RelayGuestStageFailed { stage: SecureTransport, failure: RelayUnavailable, \
|
||||
relay_phase: Failed, relay_failure: Some(ConnectTimeout) }"
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue