mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-02-20 07:37:26 -05:00
Migrate pairing to pkarr-based remote discovery
- Remove relay-URL based pairing flow and rely on pkarr/DNS for remote discovery - Extend pairing code with optional node_id and adopt version 2 QR format - Generate pairing codes using node_id and drop relay_url in non-QR flows - Enable combined mDNS, pkarr, and DNS discovery in the endpoint - Cache relay_url in persisted device info for reconnection optimization - Update protocol handlers to propagate node_id and pkarr-based flow - Adjust tests and logging to reflect the new cross-network pairing
This commit is contained in:
@@ -24,15 +24,9 @@ pub enum PairCmd {
|
||||
Join {
|
||||
/// Pairing code (12 words or JSON). If not provided, enters interactive mode.
|
||||
code: Option<String>,
|
||||
/// Relay URL for internet pairing (optional)
|
||||
#[arg(long)]
|
||||
relay_url: Option<String>,
|
||||
/// Node ID for internet pairing (optional, required if relay_url is provided)
|
||||
/// Node ID for remote pairing via pkarr (optional - enables relay path)
|
||||
#[arg(long)]
|
||||
node_id: Option<String>,
|
||||
/// Session ID for internet pairing (optional, required if relay_url is provided)
|
||||
#[arg(long)]
|
||||
session_id: Option<String>,
|
||||
},
|
||||
/// Show pairing sessions
|
||||
Status,
|
||||
@@ -52,28 +46,21 @@ impl PairCmd {
|
||||
|
||||
pub fn to_join_input(&self) -> Option<PairJoinInput> {
|
||||
match self {
|
||||
Self::Join { code, relay_url, node_id, session_id } => {
|
||||
Self::Join { code, node_id } => {
|
||||
// Code is required for non-interactive mode
|
||||
let code = code.as_ref()?.clone();
|
||||
|
||||
// If relay URL is provided, construct QR JSON format
|
||||
let code = if let Some(relay_url) = relay_url {
|
||||
// Validate node_id and session_id are also provided
|
||||
let node_id = node_id.as_ref().expect("--node-id is required when --relay-url is provided");
|
||||
let session_id = session_id.as_ref().expect("--session-id is required when --relay-url is provided");
|
||||
|
||||
// First try to parse as JSON (in case they passed the full QR JSON)
|
||||
// If node_id provided via CLI, wrap in QR JSON format for remote pairing
|
||||
let code = if let Some(node_id) = node_id {
|
||||
if code.trim().starts_with('{') {
|
||||
// Already JSON, just use it
|
||||
code
|
||||
} else {
|
||||
// Plain words - construct QR JSON format
|
||||
// Plain words + node_id - construct QR JSON format (v2)
|
||||
serde_json::json!({
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
"words": code,
|
||||
"node_id": node_id,
|
||||
"relay_url": relay_url,
|
||||
"session_id": session_id
|
||||
}).to_string()
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -100,7 +100,7 @@ pub async fn run(ctx: &Context, cmd: NetworkCmd) -> Result<()> {
|
||||
println!("Expires at: {}", o.expires_at);
|
||||
});
|
||||
}
|
||||
PairCmd::Join { ref code, ref relay_url, ref node_id, ref session_id } => {
|
||||
PairCmd::Join { ref code, ref node_id } => {
|
||||
// Check if we should run interactive mode
|
||||
let input = if let Some(input) = pc.to_join_input() {
|
||||
// Non-interactive: code and possibly flags were provided
|
||||
|
||||
@@ -9,7 +9,9 @@ use crate::service::network::{
|
||||
utils::{logging::NetworkLogger, NetworkIdentity},
|
||||
NetworkingError, Result,
|
||||
};
|
||||
use iroh::discovery::{mdns::MdnsDiscovery, Discovery};
|
||||
use iroh::discovery::{
|
||||
dns::DnsDiscovery, mdns::MdnsDiscovery, pkarr::PkarrPublisher, Discovery,
|
||||
};
|
||||
use iroh::endpoint::Connection;
|
||||
use iroh::{Endpoint, NodeAddr, NodeId, RelayMode, RelayUrl, Watcher};
|
||||
use std::sync::Arc;
|
||||
@@ -175,17 +177,17 @@ impl NetworkingService {
|
||||
// Create Iroh endpoint with discovery and relay configuration
|
||||
let secret_key = self.identity.to_iroh_secret_key()?;
|
||||
|
||||
// Create discovery service - using mDNS discovery
|
||||
let discovery = MdnsDiscovery::builder();
|
||||
|
||||
self.logger
|
||||
.info(&format!(
|
||||
"Created MdnsDiscovery builder for node {}",
|
||||
"Creating endpoint with mDNS + pkarr discovery for node {}",
|
||||
self.node_id
|
||||
))
|
||||
.await;
|
||||
|
||||
// Create endpoint with discovery
|
||||
// Create endpoint with combined discovery:
|
||||
// - mDNS for local network discovery
|
||||
// - PkarrPublisher to publish our address to dns.iroh.link (enables remote discovery)
|
||||
// - DnsDiscovery to resolve other nodes from dns.iroh.link
|
||||
let endpoint = Endpoint::builder()
|
||||
.secret_key(secret_key)
|
||||
.alpns(vec![
|
||||
@@ -195,7 +197,9 @@ impl NetworkingService {
|
||||
SYNC_ALPN.to_vec(),
|
||||
])
|
||||
.relay_mode(iroh::RelayMode::Default)
|
||||
.add_discovery(discovery)
|
||||
.add_discovery(MdnsDiscovery::builder())
|
||||
.add_discovery(PkarrPublisher::n0_dns())
|
||||
.add_discovery(DnsDiscovery::n0_dns())
|
||||
.bind_addr_v4(std::net::SocketAddrV4::new(
|
||||
std::net::Ipv4Addr::UNSPECIFIED,
|
||||
0,
|
||||
@@ -214,7 +218,7 @@ impl NetworkingService {
|
||||
self.endpoint = Some(endpoint.clone());
|
||||
|
||||
self.logger
|
||||
.info("Endpoint bound successfully with mDNS discovery enabled")
|
||||
.info("Endpoint bound successfully with mDNS + pkarr discovery enabled")
|
||||
.await;
|
||||
|
||||
// Create and start event loop
|
||||
@@ -1098,23 +1102,20 @@ impl NetworkingService {
|
||||
))
|
||||
}
|
||||
|
||||
/// Try to discover the initiator via relay (works across networks)
|
||||
/// Try to discover the initiator via pkarr/DNS (works across networks)
|
||||
/// Pkarr discovery automatically resolves node_id to relay_url and direct addresses
|
||||
async fn try_relay_discovery(
|
||||
&self,
|
||||
pairing_code: &crate::service::network::protocol::pairing::PairingCode,
|
||||
) -> Result<()> {
|
||||
// Get the NodeId from the pairing code (should always be present in new implementation)
|
||||
// Get the NodeId from the pairing code
|
||||
let node_id = pairing_code.node_id().ok_or_else(|| {
|
||||
NetworkingError::ConnectionFailed(
|
||||
"Pairing code missing NodeId - this indicates a bug in the new implementation"
|
||||
"Pairing code missing NodeId - cannot use pkarr discovery for remote pairing"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let relay_url = pairing_code
|
||||
.relay_url()
|
||||
.map(|url| url.parse::<iroh::RelayUrl>());
|
||||
|
||||
let endpoint = self
|
||||
.endpoint
|
||||
.as_ref()
|
||||
@@ -1124,69 +1125,39 @@ impl NetworkingService {
|
||||
|
||||
self.logger
|
||||
.info(&format!(
|
||||
"[Relay] Attempting to connect to initiator {} via relay",
|
||||
"[Pkarr] Attempting to discover and connect to initiator {} via pkarr/DNS",
|
||||
node_id.fmt_short()
|
||||
))
|
||||
.await;
|
||||
|
||||
// Build NodeAddr with relay information
|
||||
let relay_url_parsed = if let Some(relay_url) = relay_url {
|
||||
match relay_url {
|
||||
Ok(url) => {
|
||||
self.logger
|
||||
.debug(&format!("[Relay] Using relay URL: {}", url))
|
||||
.await;
|
||||
Some(url)
|
||||
}
|
||||
Err(e) => {
|
||||
self.logger
|
||||
.warn(&format!("[Relay] Failed to parse relay URL: {}", e))
|
||||
.await;
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.logger
|
||||
.warn("[Relay] No relay URL in pairing code, using default relay")
|
||||
.await;
|
||||
None
|
||||
};
|
||||
// Just provide the node_id - pkarr discovery will automatically:
|
||||
// 1. Query dns.iroh.link/pkarr for the node's published address info
|
||||
// 2. Get the relay_url and any direct addresses
|
||||
// 3. Try to connect via the best available path
|
||||
let node_addr = NodeAddr::new(node_id);
|
||||
|
||||
let node_addr = iroh::NodeAddr::from_parts(
|
||||
node_id,
|
||||
relay_url_parsed,
|
||||
vec![], // No direct addresses initially, will use relay
|
||||
);
|
||||
|
||||
// Try to connect via relay
|
||||
let timeout = tokio::time::Duration::from_secs(10); // Longer timeout for relay
|
||||
match tokio::time::timeout(timeout, endpoint.connect(node_addr.clone(), PAIRING_ALPN)).await
|
||||
{
|
||||
// Try to connect - pkarr discovery runs in the background
|
||||
let timeout = tokio::time::Duration::from_secs(15);
|
||||
match tokio::time::timeout(timeout, endpoint.connect(node_addr, PAIRING_ALPN)).await {
|
||||
Ok(Ok(conn)) => {
|
||||
self.logger
|
||||
.info("[Relay] Successfully connected to initiator via relay!")
|
||||
.info("[Pkarr] Successfully connected to initiator!")
|
||||
.await;
|
||||
|
||||
// Track the connection so it stays alive for the pairing protocol (with PAIRING_ALPN)
|
||||
// Track the connection for the pairing protocol
|
||||
{
|
||||
let mut connections = self.active_connections.write().await;
|
||||
connections.insert((node_id, PAIRING_ALPN.to_vec()), conn);
|
||||
self.logger
|
||||
.info(&format!(
|
||||
"[Relay] Tracked relay pairing connection to {}",
|
||||
node_id.fmt_short()
|
||||
))
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(Err(e)) => Err(NetworkingError::ConnectionFailed(format!(
|
||||
"Failed to connect via relay: {}",
|
||||
"Failed to connect via pkarr discovery: {}",
|
||||
e
|
||||
))),
|
||||
Err(_timeout) => Err(NetworkingError::ConnectionFailed(
|
||||
"Relay connection timeout".to_string(),
|
||||
"Pkarr discovery connection timeout".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -1215,29 +1186,15 @@ impl NetworkingService {
|
||||
"Invalid pairing handler type".to_string(),
|
||||
))?;
|
||||
|
||||
// Get our node information for relay discovery
|
||||
// Get our node ID for inclusion in QR code (enables pkarr lookup for remote pairing)
|
||||
let initiator_node_id = self.node_id();
|
||||
|
||||
// Get our relay URL from the endpoint (wait for relay to connect)
|
||||
let relay_url = if let Some(endpoint) = &self.endpoint {
|
||||
// Wait for relay to initialize (this is critical!)
|
||||
let relay = endpoint.home_relay().initialized().await;
|
||||
Some(relay.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Generate pairing code with relay information for cross-network pairing
|
||||
let random_seed = uuid::Uuid::new_v4();
|
||||
// Generate pairing code with node_id for remote discovery via pkarr
|
||||
// Note: relay_url is no longer included - joiner discovers it via pkarr/DNS
|
||||
let pairing_code =
|
||||
crate::service::network::protocol::pairing::PairingCode::from_session_id_with_relay_info(
|
||||
random_seed,
|
||||
initiator_node_id,
|
||||
relay_url,
|
||||
);
|
||||
crate::service::network::protocol::pairing::PairingCode::generate()?
|
||||
.with_node_id(initiator_node_id);
|
||||
|
||||
// The session_id derived from the pairing code
|
||||
// This ensures both initiator and joiner derive the same session_id from the BIP39 words
|
||||
let session_id = pairing_code.session_id();
|
||||
|
||||
// Start pairing session with the derived session_id
|
||||
@@ -1245,154 +1202,44 @@ impl NetworkingService {
|
||||
.start_pairing_session_with_id(session_id, pairing_code.clone())
|
||||
.await?;
|
||||
|
||||
// Get our node address first (needed for device registry)
|
||||
let mut node_addr = self.get_node_addr()?;
|
||||
|
||||
// If we don't have any direct addresses yet, wait a bit for them to be discovered
|
||||
if let Some(addr) = &node_addr {
|
||||
if addr.direct_addresses().count() == 0 {
|
||||
self.logger
|
||||
.info("No direct addresses discovered yet, waiting for endpoint to discover addresses...")
|
||||
.await;
|
||||
|
||||
// Wait up to 5 seconds for addresses to be discovered
|
||||
let mut attempts = 0;
|
||||
const MAX_ATTEMPTS: u32 = 10;
|
||||
const WAIT_TIME_MS: u64 = 500;
|
||||
|
||||
while attempts < MAX_ATTEMPTS {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(WAIT_TIME_MS)).await;
|
||||
node_addr = self.get_node_addr()?;
|
||||
|
||||
if let Some(addr) = &node_addr {
|
||||
if addr.direct_addresses().count() > 0 {
|
||||
self.logger
|
||||
.info(&format!(
|
||||
"Discovered {} direct addresses",
|
||||
addr.direct_addresses().count()
|
||||
))
|
||||
.await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
attempts += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if node_addr
|
||||
.as_ref()
|
||||
.map_or(true, |addr| addr.direct_addresses().count() == 0)
|
||||
{
|
||||
self.logger
|
||||
.warn("No direct addresses discovered after waiting, proceeding with relay-only address")
|
||||
.await;
|
||||
}
|
||||
|
||||
self.logger
|
||||
.info(&format!("Node address: {:?}", node_addr))
|
||||
.await;
|
||||
self.logger
|
||||
.info(&format!(
|
||||
"Direct addresses: {:?}",
|
||||
node_addr
|
||||
.as_ref()
|
||||
.map(|addr| addr.direct_addresses().collect::<Vec<_>>())
|
||||
.unwrap_or_default()
|
||||
))
|
||||
.await;
|
||||
self.logger
|
||||
.info(&format!(
|
||||
"Relay URL: {:?}",
|
||||
node_addr.as_ref().and_then(|addr| addr.relay_url())
|
||||
))
|
||||
.await;
|
||||
|
||||
// Register in device registry with the node address
|
||||
// Register in device registry
|
||||
let initiator_device_id = self.device_id();
|
||||
let initiator_node_id = self.node_id();
|
||||
let node_addr = self
|
||||
.get_node_addr()?
|
||||
.unwrap_or(NodeAddr::new(initiator_node_id));
|
||||
let device_registry = self.device_registry();
|
||||
{
|
||||
let mut registry = device_registry.write().await;
|
||||
// Use node_addr or create an empty one if not available
|
||||
let addr_for_registry = node_addr
|
||||
.clone()
|
||||
.unwrap_or(NodeAddr::new(initiator_node_id));
|
||||
registry.start_pairing(
|
||||
initiator_device_id,
|
||||
initiator_node_id,
|
||||
session_id,
|
||||
addr_for_registry,
|
||||
node_addr,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Publish pairing session via mDNS using user_data field
|
||||
// The joiner will filter discovered nodes by this session_id
|
||||
if let Some(endpoint) = &self.endpoint {
|
||||
let user_data =
|
||||
iroh::node_info::UserData::try_from(session_id.to_string()).map_err(|e| {
|
||||
NetworkingError::Protocol(format!("Failed to create user data: {}", e))
|
||||
})?;
|
||||
let endpoint = self.endpoint.as_ref().ok_or(NetworkingError::Protocol(
|
||||
"Networking not started".to_string(),
|
||||
))?;
|
||||
|
||||
self.logger
|
||||
.debug(&format!(
|
||||
"Setting user_data for discovery: {}",
|
||||
user_data.as_ref()
|
||||
))
|
||||
.await;
|
||||
let user_data =
|
||||
iroh::node_info::UserData::try_from(session_id.to_string()).map_err(|e| {
|
||||
NetworkingError::Protocol(format!("Failed to create user data: {}", e))
|
||||
})?;
|
||||
|
||||
// Get current user_data before setting to verify the change
|
||||
let current_node_data = endpoint.node_addr().get();
|
||||
self.logger
|
||||
.debug(&format!(
|
||||
"Current node user_data before set: {:?}",
|
||||
current_node_data.as_ref().and_then(|addr| {
|
||||
// NodeAddr doesn't expose user_data, so we can't check it here
|
||||
Some("(NodeAddr doesn't expose user_data)")
|
||||
})
|
||||
))
|
||||
.await;
|
||||
endpoint.set_user_data_for_discovery(Some(user_data));
|
||||
|
||||
endpoint.set_user_data_for_discovery(Some(user_data.clone()));
|
||||
self.logger
|
||||
.info(&format!(
|
||||
"Broadcasting pairing session {} via mDNS + pkarr",
|
||||
session_id
|
||||
))
|
||||
.await;
|
||||
|
||||
self.logger
|
||||
.debug(&format!(
|
||||
"Called endpoint.set_user_data_for_discovery with: {}",
|
||||
user_data.as_ref()
|
||||
))
|
||||
.await;
|
||||
|
||||
self.logger
|
||||
.info(&format!(
|
||||
"Broadcasting pairing session {} via mDNS (set_user_data_for_discovery called)",
|
||||
session_id
|
||||
))
|
||||
.await;
|
||||
|
||||
// Wait for mDNS re-advertisement to propagate
|
||||
// When user_data changes, the endpoint triggers a re-publish to discovery services
|
||||
// This delay ensures the updated broadcast (with session_id) is sent before joiners start listening
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
|
||||
self.logger
|
||||
.debug("mDNS re-advertisement delay completed, session_id should now be broadcast")
|
||||
.await;
|
||||
|
||||
// Log current node address to verify what's being broadcast
|
||||
if let Some(node_addr) = self.get_node_addr().ok().flatten() {
|
||||
self.logger
|
||||
.debug(&format!(
|
||||
"Broadcasting with {} direct addresses",
|
||||
node_addr.direct_addresses().count()
|
||||
))
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
return Err(NetworkingError::Protocol(
|
||||
"Networking not started".to_string(),
|
||||
));
|
||||
}
|
||||
// Wait for discovery re-advertisement to propagate
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
|
||||
let expires_in = 300; // 5 minutes
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ pub struct PersistedPairedDevice {
|
||||
pub last_connected_at: Option<DateTime<Utc>>,
|
||||
pub connection_attempts: u32,
|
||||
pub trust_level: TrustLevel,
|
||||
/// Cached relay URL for reconnection optimization (discovered via pkarr or connection)
|
||||
#[serde(default)]
|
||||
pub relay_url: Option<String>,
|
||||
}
|
||||
|
||||
/// Encrypted device data for disk storage
|
||||
@@ -259,6 +262,7 @@ impl DevicePersistence {
|
||||
device_id: Uuid,
|
||||
device_info: DeviceInfo,
|
||||
session_keys: SessionKeys,
|
||||
relay_url: Option<String>,
|
||||
) -> Result<()> {
|
||||
let mut devices = self.load_paired_devices().await?;
|
||||
|
||||
@@ -269,6 +273,7 @@ impl DevicePersistence {
|
||||
last_connected_at: None,
|
||||
connection_attempts: 0,
|
||||
trust_level: TrustLevel::Trusted,
|
||||
relay_url,
|
||||
};
|
||||
|
||||
devices.insert(device_id, paired_device);
|
||||
@@ -436,7 +441,7 @@ mod tests {
|
||||
|
||||
// Add paired device
|
||||
persistence
|
||||
.add_paired_device(device_id, device_info.clone(), session_keys.clone())
|
||||
.add_paired_device(device_id, device_info.clone(), session_keys.clone(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -460,7 +465,7 @@ mod tests {
|
||||
let session_keys = SessionKeys::from_shared_secret(vec![1, 2, 3, 4]);
|
||||
|
||||
persistence
|
||||
.add_paired_device(device_id, device_info, session_keys)
|
||||
.add_paired_device(device_id, device_info, session_keys, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -478,7 +483,7 @@ mod tests {
|
||||
let session_keys = SessionKeys::from_shared_secret(vec![1, 2, 3, 4]);
|
||||
|
||||
persistence
|
||||
.add_paired_device(device_id, device_info, session_keys)
|
||||
.add_paired_device(device_id, device_info, session_keys, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -503,7 +508,7 @@ mod tests {
|
||||
|
||||
// Add device (this will encrypt and save)
|
||||
persistence
|
||||
.add_paired_device(device_id, device_info.clone(), session_keys.clone())
|
||||
.add_paired_device(device_id, device_info.clone(), session_keys.clone(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -531,7 +536,7 @@ mod tests {
|
||||
|
||||
// Add device
|
||||
persistence
|
||||
.add_paired_device(device_id, device_info, session_keys)
|
||||
.add_paired_device(device_id, device_info, session_keys, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -161,6 +161,7 @@ impl DeviceRegistry {
|
||||
device_id: Uuid,
|
||||
info: DeviceInfo,
|
||||
session_keys: SessionKeys,
|
||||
relay_url: Option<String>,
|
||||
) -> Result<()> {
|
||||
// Parse node ID from network fingerprint
|
||||
let node_id = info
|
||||
@@ -208,10 +209,10 @@ impl DeviceRegistry {
|
||||
.await;
|
||||
}
|
||||
|
||||
// Persist the paired device for future reconnection
|
||||
// Persist the paired device for future reconnection (with relay_url for optimization)
|
||||
if let Err(e) = self
|
||||
.persistence
|
||||
.add_paired_device(device_id, info.clone(), session_keys.clone())
|
||||
.add_paired_device(device_id, info.clone(), session_keys.clone(), relay_url)
|
||||
.await
|
||||
{
|
||||
self.logger
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::service::network::{
|
||||
device::{DeviceInfo, SessionKeys},
|
||||
NetworkingError, Result,
|
||||
};
|
||||
use iroh::NodeId;
|
||||
use iroh::{NodeId, Watcher};
|
||||
use uuid::Uuid;
|
||||
|
||||
impl PairingProtocolHandler {
|
||||
@@ -229,11 +229,18 @@ impl PairingProtocolHandler {
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Get relay URL from endpoint for caching (enables reconnection via relay)
|
||||
let relay_url = self
|
||||
.endpoint
|
||||
.as_ref()
|
||||
.and_then(|ep| ep.home_relay().get().into_iter().next())
|
||||
.map(|r| r.to_string());
|
||||
|
||||
// Complete pairing in device registry
|
||||
{
|
||||
let mut registry = self.device_registry.write().await;
|
||||
registry
|
||||
.complete_pairing(actual_device_id, device_info.clone(), session_keys)
|
||||
.complete_pairing(actual_device_id, device_info.clone(), session_keys, relay_url)
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::service::network::{
|
||||
device::{DeviceInfo, SessionKeys},
|
||||
NetworkingError, Result,
|
||||
};
|
||||
use iroh::NodeId;
|
||||
use iroh::{NodeId, Watcher};
|
||||
use uuid::Uuid;
|
||||
|
||||
impl PairingProtocolHandler {
|
||||
@@ -180,11 +180,18 @@ impl PairingProtocolHandler {
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Get relay URL from endpoint for caching (enables reconnection via relay)
|
||||
let relay_url = self
|
||||
.endpoint
|
||||
.as_ref()
|
||||
.and_then(|ep| ep.home_relay().get().into_iter().next())
|
||||
.map(|r| r.to_string());
|
||||
|
||||
// Complete pairing in device registry
|
||||
{
|
||||
let mut registry = self.device_registry.write().await;
|
||||
registry
|
||||
.complete_pairing(device_id, initiator_device_info.clone(), session_keys)
|
||||
.complete_pairing(device_id, initiator_device_info.clone(), session_keys, relay_url)
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
||||
@@ -198,8 +198,8 @@ impl PairingProtocolHandler {
|
||||
/// Start a new pairing session as initiator
|
||||
/// Returns the session ID which should be advertised via DHT by the caller
|
||||
pub async fn start_pairing_session(&self) -> Result<Uuid> {
|
||||
let session_id = Uuid::new_v4();
|
||||
let pairing_code = PairingCode::from_session_id(session_id);
|
||||
let pairing_code = PairingCode::generate()?;
|
||||
let session_id = pairing_code.session_id();
|
||||
self.start_pairing_session_with_id(session_id, pairing_code)
|
||||
.await?;
|
||||
Ok(session_id)
|
||||
|
||||
@@ -24,11 +24,8 @@ pub struct PairingCode {
|
||||
/// Expiration timestamp
|
||||
expires_at: DateTime<Utc>,
|
||||
|
||||
/// Initiator's NodeId for relay discovery (optional for backward compatibility)
|
||||
/// Initiator's NodeId for remote discovery via pkarr (optional - enables relay path)
|
||||
node_id: Option<NodeId>,
|
||||
|
||||
/// Initiator's home relay URL for cross-network pairing (optional for backward compatibility)
|
||||
relay_url: Option<String>,
|
||||
}
|
||||
|
||||
impl PairingCode {
|
||||
@@ -51,44 +48,13 @@ impl PairingCode {
|
||||
session_id,
|
||||
expires_at: Utc::now() + chrono::Duration::minutes(5),
|
||||
node_id: None,
|
||||
relay_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate a pairing code from a session ID
|
||||
pub fn from_session_id(session_id: Uuid) -> Self {
|
||||
// Use the session ID as the BIP39 entropy source (16 bytes)
|
||||
let entropy = session_id.as_bytes();
|
||||
|
||||
// Expand entropy to full 32-byte secret deterministically
|
||||
// This matches the logic in decode_from_bip39_words to ensure round-trip compatibility
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
hasher.update(b"spacedrive-pairing-entropy-extension-v1");
|
||||
hasher.update(entropy);
|
||||
let derived_bytes = hasher.finalize();
|
||||
|
||||
let mut secret = [0u8; 32];
|
||||
secret[..16].copy_from_slice(entropy);
|
||||
secret[16..].copy_from_slice(&derived_bytes.as_bytes()[..16]);
|
||||
|
||||
// Generate BIP39 words from the entropy (first 16 bytes only, as per BIP39 standard)
|
||||
let words = Self::encode_to_bip39_words(&secret).unwrap_or_else(|_| {
|
||||
// Fallback to empty words if BIP39 fails
|
||||
[const { String::new() }; 12]
|
||||
});
|
||||
|
||||
// Derive session ID from the secret to ensure consistency with parsing
|
||||
// both encode and decode paths must derive the same session_id
|
||||
let derived_session_id = Self::derive_session_id(&secret);
|
||||
|
||||
Self {
|
||||
secret,
|
||||
words,
|
||||
session_id: derived_session_id,
|
||||
expires_at: Utc::now() + chrono::Duration::minutes(5),
|
||||
node_id: None,
|
||||
relay_url: None,
|
||||
}
|
||||
/// Add node_id for remote pairing via pkarr discovery
|
||||
pub fn with_node_id(mut self, node_id: NodeId) -> Self {
|
||||
self.node_id = Some(node_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Parse a pairing code from a BIP39 mnemonic string (for local pairing)
|
||||
@@ -124,6 +90,7 @@ impl PairingCode {
|
||||
}
|
||||
|
||||
/// Parse a pairing code from QR code JSON (for remote pairing)
|
||||
/// Version 2 format: {version, words, node_id} - session_id is derived from words
|
||||
pub fn from_qr_json(json: &str) -> crate::service::network::Result<Self> {
|
||||
let data: serde_json::Value = serde_json::from_str(json).map_err(|e| {
|
||||
crate::service::network::NetworkingError::Protocol(format!(
|
||||
@@ -132,89 +99,28 @@ impl PairingCode {
|
||||
))
|
||||
})?;
|
||||
|
||||
// Extract session_id
|
||||
let session_id = data
|
||||
.get("session_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| {
|
||||
crate::service::network::NetworkingError::Protocol(
|
||||
"Missing session_id in QR code".to_string(),
|
||||
)
|
||||
})?
|
||||
.parse::<uuid::Uuid>()
|
||||
.map_err(|e| {
|
||||
crate::service::network::NetworkingError::Protocol(format!(
|
||||
"Invalid session_id in QR code: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
// Extract node_id
|
||||
let node_id = data
|
||||
.get("node_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.parse::<NodeId>())
|
||||
.transpose()
|
||||
.map_err(|e| {
|
||||
crate::service::network::NetworkingError::Protocol(format!(
|
||||
"Invalid node_id in QR code: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
// Extract relay_url
|
||||
let relay_url = data
|
||||
.get("relay_url")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
// Extract words (BIP39 mnemonic)
|
||||
// Extract words (BIP39 mnemonic) - required
|
||||
let words_str = data.get("words").and_then(|v| v.as_str()).ok_or_else(|| {
|
||||
crate::service::network::NetworkingError::Protocol(
|
||||
"Missing words in QR code".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Parse the BIP39 words to get the secret
|
||||
let words: Vec<String> = words_str
|
||||
.split_whitespace()
|
||||
.map(|s| s.to_lowercase())
|
||||
.collect();
|
||||
// Parse words to get the base pairing code (session_id is derived from words)
|
||||
let mut code = Self::from_string(words_str)?;
|
||||
|
||||
if words.len() != 12 {
|
||||
return Err(crate::service::network::NetworkingError::Protocol(format!(
|
||||
"Invalid word count in QR code - expected 12 but got {}",
|
||||
words.len()
|
||||
)));
|
||||
// Extract node_id (optional - enables remote pairing via pkarr)
|
||||
if let Some(node_id_str) = data.get("node_id").and_then(|v| v.as_str()) {
|
||||
let node_id = node_id_str.parse::<NodeId>().map_err(|e| {
|
||||
crate::service::network::NetworkingError::Protocol(format!(
|
||||
"Invalid node_id in QR code: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
code.node_id = Some(node_id);
|
||||
}
|
||||
|
||||
let words_array: [String; 12] = words.try_into().map_err(|_| {
|
||||
crate::service::network::NetworkingError::Protocol(
|
||||
"Failed to convert words to array".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Decode BIP39 words to get secret
|
||||
let secret = Self::decode_from_bip39_words(&words_array)?;
|
||||
|
||||
// Use the session_id directly from the QR code (don't re-derive it)
|
||||
// This is critical for cross-network pairing where the initiator and joiner
|
||||
// must use the exact same session_id
|
||||
// node_id is required for QR code relay pairing
|
||||
let node_id = node_id.ok_or_else(|| {
|
||||
crate::service::network::NetworkingError::Protocol(
|
||||
"Missing node_id in QR code".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(PairingCode {
|
||||
secret,
|
||||
words: words_array,
|
||||
session_id, // Use the session_id from the QR code
|
||||
expires_at: Utc::now() + chrono::Duration::minutes(5),
|
||||
node_id: Some(node_id),
|
||||
relay_url,
|
||||
})
|
||||
Ok(code)
|
||||
}
|
||||
|
||||
/// Create pairing code from BIP39 words
|
||||
@@ -222,7 +128,7 @@ impl PairingCode {
|
||||
// Decode BIP39 words back to secret
|
||||
let secret = Self::decode_from_bip39_words(words)?;
|
||||
|
||||
// Derive session ID from the secret using the same method as from_session_id()
|
||||
// Derive session ID from the secret
|
||||
// This ensures both the initiator and joiner get the same session_id
|
||||
let session_id = Self::derive_session_id(&secret);
|
||||
|
||||
@@ -232,76 +138,9 @@ impl PairingCode {
|
||||
session_id,
|
||||
expires_at: Utc::now() + chrono::Duration::minutes(5),
|
||||
node_id: None,
|
||||
relay_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new pairing code with relay information for cross-network pairing
|
||||
pub fn generate_with_relay_info(
|
||||
node_id: NodeId,
|
||||
relay_url: Option<String>,
|
||||
) -> crate::service::network::Result<Self> {
|
||||
use rand::RngCore;
|
||||
|
||||
let mut secret = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut secret);
|
||||
|
||||
// Convert secret to 12 BIP39 words using proper mnemonic encoding
|
||||
let words = Self::encode_to_bip39_words(&secret)?;
|
||||
|
||||
// Derive session ID from secret
|
||||
let session_id = Self::derive_session_id(&secret);
|
||||
|
||||
Ok(PairingCode {
|
||||
secret,
|
||||
words,
|
||||
session_id,
|
||||
expires_at: Utc::now() + chrono::Duration::minutes(5),
|
||||
node_id: Some(node_id),
|
||||
relay_url,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a pairing code from session ID with relay information
|
||||
pub fn from_session_id_with_relay_info(
|
||||
session_id: Uuid,
|
||||
node_id: NodeId,
|
||||
relay_url: Option<String>,
|
||||
) -> Self {
|
||||
// Use the session ID as the BIP39 entropy source (16 bytes)
|
||||
let entropy = session_id.as_bytes();
|
||||
|
||||
// Expand entropy to full 32-byte secret deterministically
|
||||
// This matches the logic in decode_from_bip39_words to ensure round-trip compatibility
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
hasher.update(b"spacedrive-pairing-entropy-extension-v1");
|
||||
hasher.update(entropy);
|
||||
let derived_bytes = hasher.finalize();
|
||||
|
||||
let mut secret = [0u8; 32];
|
||||
secret[..16].copy_from_slice(entropy);
|
||||
secret[16..].copy_from_slice(&derived_bytes.as_bytes()[..16]);
|
||||
|
||||
// Generate BIP39 words from the entropy (first 16 bytes only, as per BIP39 standard)
|
||||
let words = Self::encode_to_bip39_words(&secret).unwrap_or_else(|_| {
|
||||
// Fallback to empty words if BIP39 fails
|
||||
[const { String::new() }; 12]
|
||||
});
|
||||
|
||||
// Derive session ID from the secret to ensure consistency with parsing
|
||||
// both encode and decode paths must derive the same session_id
|
||||
let derived_session_id = Self::derive_session_id(&secret);
|
||||
|
||||
Self {
|
||||
secret,
|
||||
words,
|
||||
session_id: derived_session_id,
|
||||
expires_at: Utc::now() + chrono::Duration::minutes(5),
|
||||
node_id: Some(node_id),
|
||||
relay_url,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the session ID from this pairing code
|
||||
pub fn session_id(&self) -> Uuid {
|
||||
self.session_id
|
||||
@@ -312,29 +151,23 @@ impl PairingCode {
|
||||
&self.secret
|
||||
}
|
||||
|
||||
/// Get the initiator's NodeId for relay discovery
|
||||
/// Get the initiator's NodeId for pkarr discovery
|
||||
pub fn node_id(&self) -> Option<NodeId> {
|
||||
self.node_id
|
||||
}
|
||||
|
||||
/// Get the initiator's home relay URL for cross-network pairing
|
||||
pub fn relay_url(&self) -> Option<&str> {
|
||||
self.relay_url.as_deref()
|
||||
}
|
||||
|
||||
/// Convert to display string (for local pairing - BIP39 words only)
|
||||
pub fn to_string(&self) -> String {
|
||||
self.words.join(" ")
|
||||
}
|
||||
|
||||
/// Convert to QR code JSON (for remote pairing - includes all metadata)
|
||||
/// Convert to QR code JSON (for remote pairing)
|
||||
/// Version 2: {version, words, node_id} - session_id derived from words, relay discovered via pkarr
|
||||
pub fn to_qr_json(&self) -> String {
|
||||
serde_json::json!({
|
||||
"version": 1,
|
||||
"session_id": self.session_id,
|
||||
"version": 2,
|
||||
"words": self.to_string(),
|
||||
"node_id": self.node_id.map(|id| id.to_string()),
|
||||
"relay_url": self.relay_url
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user