fix(collab): report relay join failure stages

This commit is contained in:
Kayshen-X 2026-08-02 19:36:55 +08:00
parent 7f8340f2a0
commit 15ff2bea43
3 changed files with 267 additions and 93 deletions

View file

@ -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

View file

@ -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(&region.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(&region.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

View 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(&region.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(&region.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) }"
);
}
}