···2020use n0_future::StreamExt;
2121pub use packet::PacketError;
2222use packet::PostcardPacket;
2323+use rand::Rng;
2324use thiserror::Error;
2425use tokio::sync::Mutex;
2526use tokio::task::AbortHandle;
···31323233#[derive(Debug, Error)]
3334pub enum PairingError {
3535+ #[error("Pairing manager not initialized")]
3636+ NotInitialized,
3437 #[error("Unknown remote endpoint")]
3538 UnknownRemote,
3639 #[error("Endpoint pairing already requested")]
···189192 result
190193 }
191194195195+ /// Request guest pairing with a remote peer using a PIN.
196196+ /// The remote must have guest mode enabled with a matching PIN.
197197+ pub async fn request_guest_pairing(
198198+ &self,
199199+ id: &EndpointId,
200200+ pin: &str,
201201+ ) -> Result<PairingResult, PairingError> {
202202+ info!("Guest pairing with {id}");
203203+ let Some(ref inner) = self.inner else {
204204+ return Err(PairingError::NotInitialized);
205205+ };
206206+207207+ let addr = {
208208+ let state = inner.state.lock().await;
209209+ let Some(remote) = state.by_id(id) else {
210210+ return Err(PairingError::UnknownRemote);
211211+ };
212212+ remote.addr()
213213+ };
214214+215215+ let connection = inner.router.endpoint().connect(addr, PAIRING_ALPN).await?;
216216+ let (mut sender, mut receiver) = connection.open_bi().await?;
217217+218218+ PostcardPacket::send(PairingCommand::GuestRequest(pin.to_string()), &mut sender).await?;
219219+220220+ let command: PairingCommand = PostcardPacket::recv(&mut receiver).await?;
221221+ let result = match command {
222222+ PairingCommand::Accept => PairingResult::Accepted,
223223+ PairingCommand::Reject => PairingResult::Rejected,
224224+ _ => {
225225+ error!("Unexpected response to guest pairing: {command:?}");
226226+ return Err(PairingError::InvalidState);
227227+ },
228228+ };
229229+230230+ PostcardPacket::send(PairingCommand::Ack, &mut sender).await?;
231231+ sender.finish()?;
232232+ let _ = sender.stopped().await;
233233+234234+ let mut state = inner.state.lock().await;
235235+ match &result {
236236+ PairingResult::Accepted => {
237237+ state.notify(PeerEvent::PairingAccepted(*id));
238238+ state.set_status(id, EndpointStatus::PairedConnected);
239239+ },
240240+ PairingResult::Rejected => {
241241+ state.notify(PeerEvent::PairingRejected(*id));
242242+ },
243243+ }
244244+245245+ Ok(result)
246246+ }
247247+192248 /// Perform the pairing handshake over the wire. Separated from request_pairing
193249 /// so that cleanup always runs regardless of where an error occurs.
194250 async fn do_pairing_handshake(
···428484 };
429485430486 inner.state.lock().await.remove_endpoint(id);
487487+ }
488488+489489+ // --- Guest mode ---
490490+491491+ /// Enable guest mode with a random 6-digit PIN.
492492+ /// Returns the PIN to display to the user.
493493+ pub async fn enable_guest_mode(&self, duration_minutes: u32) -> Result<String, PairingError> {
494494+ let Some(ref inner) = self.inner else {
495495+ return Err(PairingError::NotInitialized);
496496+ };
497497+498498+ let pin: String = {
499499+ let mut rng = rand::rng();
500500+ (0..6)
501501+ .map(|_| char::from(b'0' + rng.random_range(0..10u8)))
502502+ .collect()
503503+ };
504504+ let duration_secs = (duration_minutes as u64) * 60;
505505+506506+ inner
507507+ .state
508508+ .lock()
509509+ .await
510510+ .enable_guest_mode(pin.clone(), duration_secs);
511511+512512+ info!("[P2P] Guest mode enabled, PIN: {}", pin);
513513+ Ok(pin)
514514+ }
515515+516516+ /// Disable guest mode and remove all guest peers.
517517+ pub async fn disable_guest_mode(&self) -> Result<(), PairingError> {
518518+ let Some(ref inner) = self.inner else {
519519+ return Err(PairingError::NotInitialized);
520520+ };
521521+522522+ let mut state = inner.state.lock().await;
523523+ let removed = state.disable_guest_mode();
524524+ for peer_id in &removed {
525525+ state.notify(PeerEvent::GuestExpired(*peer_id));
526526+ }
527527+528528+ info!(
529529+ "[P2P] Guest mode disabled, removed {} guests",
530530+ removed.len()
531531+ );
532532+ Ok(())
431533 }
432534}
+6
crates/beaver_p2p/src/message_protocol.rs
···3131 remote_id
3232 );
33333434+ // Track this connection so it can be closed when the peer is removed.
3535+ self.state
3636+ .lock()
3737+ .await
3838+ .track_incoming_connection(remote_id, connection.clone());
3939+3440 // Accept uni streams — each incoming message arrives on its own stream.
3541 while let Ok(mut receiver) = connection.accept_uni().await {
3642 match BasePacket::recv(&mut receiver).await {
+11-12
crates/beaver_p2p/src/pairing_hook.rs
···3838 String::from_utf8_lossy(conn.alpn())
3939 );
40404141- // For message protocol, only allow PairedConnected endpoints.
4242- if conn.alpn() == MESSAGE_ALPN &&
4343- !self
4444- .state
4545- .lock()
4646- .await
4747- .has(&conn.remote_id(), EndpointStatus::PairedConnected)
4848- {
4949- return AfterHandshakeOutcome::Reject {
5050- error_code: 401u32.into(),
5151- reason: b"not paired".into(),
5252- };
4141+ // For message protocol, only allow paired or guest-connected endpoints.
4242+ if conn.alpn() == MESSAGE_ALPN {
4343+ let state = self.state.lock().await;
4444+ let allowed = state.has(&conn.remote_id(), EndpointStatus::PairedConnected) ||
4545+ state.has(&conn.remote_id(), EndpointStatus::GuestConnected);
4646+ if !allowed {
4747+ return AfterHandshakeOutcome::Reject {
4848+ error_code: 401u32.into(),
4949+ reason: b"not paired".into(),
5050+ };
5151+ }
5352 }
54535554 AfterHandshakeOutcome::Accept
+47-1
crates/beaver_p2p/src/pairing_protocol.rs
···6868 .await
6969 .expect("Failed to read");
70707171- // Step 1. Receive a request, store the tokio channel sender and ack receiver in the state.
7271 match command {
7372 PairingCommand::Request => {
7473 let state = self.state.lock().await;
···7776 .map(|ep| ep.name().to_owned())
7877 .unwrap_or_default();
7978 state.notify(PeerEvent::PairingRequest(remote_id, name));
7979+ },
8080+ PairingCommand::GuestRequest(pin) => {
8181+ let mut state = self.state.lock().await;
8282+ if state.validate_guest_pin(&pin) {
8383+ state.set_status(&remote_id, EndpointStatus::GuestConnected);
8484+ state.add_guest_peer(&remote_id);
8585+ drop(state);
8686+8787+ PostcardPacket::send(PairingCommand::Accept, &mut sender)
8888+ .await
8989+ .expect("Failed to send guest accept");
9090+9191+ match PostcardPacket::recv(&mut receiver).await {
9292+ Ok(PairingCommand::Ack) => {
9393+ self.state
9494+ .lock()
9595+ .await
9696+ .notify(PeerEvent::GuestAccepted(remote_id));
9797+ info!("[P2P] Guest paired: {remote_id}");
9898+ },
9999+ Ok(cmd) => {
100100+ error!("Unexpected command after guest accept: {cmd:?}");
101101+ self.state
102102+ .lock()
103103+ .await
104104+ .notify(PeerEvent::PairingFailed(remote_id));
105105+ },
106106+ Err(e) => {
107107+ error!("Failed to read guest Ack: {e}");
108108+ self.state
109109+ .lock()
110110+ .await
111111+ .notify(PeerEvent::PairingFailed(remote_id));
112112+ },
113113+ }
114114+ return Ok(());
115115+ } else {
116116+ info!("[P2P] Guest pairing rejected: invalid PIN from {remote_id}");
117117+ PostcardPacket::send(PairingCommand::Reject, &mut sender)
118118+ .await
119119+ .expect("Failed to send guest reject");
120120+ self.state
121121+ .lock()
122122+ .await
123123+ .notify(PeerEvent::PairingRejected(remote_id));
124124+ return Ok(());
125125+ }
80126 },
81127 _ => {
82128 error!("Unexpected command: {command:?}");
···209209 /// Mark a new document as active
210210 ActivateDocument,
211211 /// Set the document state for a pipeline (used by screenshot / reftests)
212212-@@ -754,6 +887,116 @@
212212+@@ -754,6 +887,122 @@
213213 /// aggregate lock count and notify the provider only when the count transitions from N to 0.
214214 /// <https://w3c.github.io/screen-wake-lock/#dfn-release-wake-lock>
215215 ReleaseWakeLock(WakeLockType),
···275275+ PairingRejectPairing(String, GenericCallback<Result<(), String>>),
276276+ /// Remove a paired peer (unpair and forget).
277277+ PairingRemovePeer(String, GenericCallback<Result<(), String>>),
278278++ /// Enable guest pairing mode with a duration in minutes. Returns the PIN.
279279++ PairingEnableGuestMode(u32, GenericCallback<Result<String, String>>),
280280++ /// Disable guest pairing mode and remove all guests.
281281++ PairingDisableGuestMode(GenericCallback<Result<(), String>>),
282282++ /// Request guest pairing with a PIN.
283283++ PairingRequestGuestPairing(String, String, GenericCallback<Result<bool, String>>),
278284+ /// Create a peer stream: create a virtual remote port entangled with a local port,
279285+ /// and send the offer to a remote peer.
280286+ /// Args: peer_id, local_port_id, remote_port_id, target_url, callback.