docs/core/networking.mdx
Spacedrive connects devices directly using Iroh, a peer-to-peer networking library built on QUIC. This enables secure communication between your devices without relying on cloud servers.
The networking system manages all device-to-device communication through a single service that handles connections, protocols, and state management.
NetworkingService coordinates all networking operations. It manages the Iroh endpoint, tracks device states, and routes messages to protocol handlers.
pub struct NetworkingService {
endpoint: Endpoint, // Iroh's QUIC endpoint
device_registry: DeviceRegistry, // Tracks all known devices
protocol_registry: ProtocolRegistry, // Routes messages
identity: NetworkIdentity, // Cryptographic identity
}
NetworkIdentity manages your device's cryptographic identity using Ed25519 keys. This identity persists across sessions and proves your device's authenticity to others.
pub struct NetworkIdentity {
node_id: NodeId, // Derived from public key
signing_key: SigningKey, // Ed25519 private key
verifying_key: VerifyingKey, // Ed25519 public key
}
DeviceRegistry maintains the state of all discovered and paired devices. It provides a single source of truth for device relationships.
pub enum DeviceState {
Discovered { node_addr: NodeAddr },
Pairing { session_id: Uuid },
Paired { session_keys: SessionKeys },
Connected { connection: Connection },
Disconnected { reason: DisconnectReason },
}
Iroh provides the underlying transport using QUIC, which offers:
The networking module uses ALPN (Application-Layer Protocol Negotiation) to route connections to specific protocol handlers.
// Protocol registration
registry.register("pairing/1.0", PairingProtocol::new());
registry.register("sync/1.0", SyncProtocol::new());
registry.register("transfer/1.0", TransferProtocol::new());
// Connection routing based on ALPN
match alpn {
"pairing/1.0" => pairing_handler.handle(connection),
"sync/1.0" => sync_handler.handle(connection),
_ => Err(UnknownProtocol)
}
Devices find each other through multiple mechanisms:
Iroh automatically discovers devices on your local network using mDNS. When a device starts, it broadcasts its presence and listens for others.
// Automatic local discovery
endpoint.discovery().add_discovery(Box::new(
DnsDiscovery::builder().build()
));
You can connect to devices using their NodeAddr, which includes their NodeId and network addresses.
// Connect to a specific device
let node_addr = NodeAddr {
node_id: NodeId::from_str("...")?,
relay_url: Some("https://relay.iroh.network"),
direct_addresses: vec!["192.168.1.100:11204".parse()?],
};
endpoint.connect(node_addr, "sync/1.0").await?;
Pairing establishes trust between devices using cryptographic signatures and user-friendly codes.
The initiator generates a pairing code that the joiner enters to establish trust.
<Steps> <Step title="Generate Pairing Code"> The initiator creates a BIP39 mnemonic code: ```rust // Initiator generates code let code = PairingCode::generate(); // "brave-lion-sunset" ``` </Step> <Step title="Exchange Device Info"> Both devices exchange their information and public keys: ```rust pub struct DeviceInfo { pub device_id: Uuid, pub device_name: String, pub device_type: DeviceType, pub public_key: VerifyingKey, } ``` </Step> <Step title="Challenge-Response"> The initiator challenges the joiner to prove they have the code: ```rust // Initiator sends challenge let challenge = Challenge::random();// Joiner signs challenge let signature = identity.sign(&challenge);
// Initiator verifies signature identity.verify(&challenge, &signature)?;
</Step>
<Step title="Establish Session">
Both devices derive session keys for future communication:
```rust
// Derive shared secret using ECDH
let shared_secret = ecdh(my_private, their_public);
// Derive session keys
let keys = SessionKeys::from_shared_secret(shared_secret);
The pairing protocol prevents several attacks:
Paired devices communicate using an encrypted messaging protocol.
pub enum NetworkMessage {
// Library discovery
LibraryAnnounce { libraries: Vec<LibraryInfo> },
LibraryRequest { library_id: Uuid },
// Sync coordination
SyncRequest { library_id: Uuid, after: HLC },
SyncResponse { entries: Vec<SyncEntry> },
// File operations
FileRequest { entry_id: Uuid },
FileResponse { chunks: Vec<Chunk> },
// Diagnostics
Ping { timestamp: SystemTime },
Pong { timestamp: SystemTime },
}
Messages are serialized as JSON and encrypted using session keys:
// Send a message
async fn send_message(
connection: &mut Connection,
message: NetworkMessage,
keys: &SessionKeys,
) -> Result<()> {
// Serialize to JSON
let json = serde_json::to_vec(&message)?;
// Encrypt with session key
let encrypted = keys.encrypt(&json)?;
// Send over QUIC stream
let mut stream = connection.open_uni().await?;
stream.write_all(&encrypted).await?;
stream.finish().await?;
Ok(())
}
QUIC provides reliable delivery, but the application layer adds:
The file transfer protocol enables secure, resumable file sharing between devices.
while let Some(chunk) = file.read_chunk(CHUNK_SIZE).await? { let encrypted = session_keys.encrypt(&chunk)?; stream.write_all(&encrypted).await?; }
</Step>
<Step title="Verify Transfer">
Both devices verify the transfer using checksums:
```rust
let checksum = blake3::hash(&file_data);
if checksum != expected_checksum {
return Err(TransferError::ChecksumMismatch);
}
The event loop handles all incoming connections and routes them appropriately.
pub struct NetworkingEventLoop {
endpoint: Endpoint,
registry: ProtocolRegistry,
commands: mpsc::Receiver<NetworkCommand>,
}
impl NetworkingEventLoop {
pub async fn run(mut self) -> Result<()> {
loop {
select! {
// Handle incoming connections
Some(connection) = self.endpoint.accept() => {
let alpn = connection.alpn();
let handler = self.registry.get(alpn)?;
tokio::spawn(handler.handle(connection));
}
// Process commands
Some(cmd) = self.commands.recv() => {
self.handle_command(cmd).await?;
}
}
}
}
}
Connections transition through several states:
Connections are kept alive using periodic pings:
// Ping every 30 seconds of inactivity
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(30);
async fn keep_alive_loop(connection: Connection) {
let mut interval = tokio::time::interval(KEEP_ALIVE_INTERVAL);
loop {
interval.tick().await;
if let Err(_) = send_ping(&connection).await {
// Connection lost
break;
}
}
}
Iroh handles NAT traversal automatically using several techniques:
First, Iroh attempts a direct connection using known addresses:
// Try direct addresses first
for addr in &node_addr.direct_addresses {
if let Ok(conn) = endpoint.connect_direct(addr).await {
return Ok(conn);
}
}
If direct connection fails, Iroh uses STUN to discover public addresses:
// STUN automatically handled by Iroh
// Discovers public IP and port mapping
let public_addr = endpoint.my_addr().await?;
When both devices are behind symmetric NATs, Iroh falls back to relay servers:
// Relay connection automatic in Iroh
// Uses relay_url from NodeAddr
let conn = endpoint.connect(node_addr, alpn).await?;
// This may be relayed if direct connection impossible
The networking stack provides multiple encryption layers:
pub struct KeyHierarchy {
// Long-term identity
device_key: SigningKey,
// Per-pairing session keys
session_keys: HashMap<DeviceId, SessionKeys>,
// Per-transfer ephemeral keys
transfer_keys: HashMap<TransferId, TransferKeys>,
}
// In Core initialization
let networking = NetworkingService::new(
data_dir.clone(),
device_id,
)?;
// Start the event loop
let event_loop = networking.spawn_event_loop();
tokio::spawn(event_loop.run());
// Generate pairing code (initiator)
let code = core.networking()
.start_pairing_as_initiator()
.await?;
println!("Share this code: {}", code);
// Join pairing (joiner)
core.networking()
.join_pairing(&code)
.await?;
// Get paired device
let device = core.networking()
.get_device(device_id)?;
// Send sync request
let message = NetworkMessage::SyncRequest {
library_id,
after: last_sync_hlc,
};
device.send_message(message).await?;
// Share a file
let transfer = core.share_with_device(
entry_id,
device_id,
TransferOptions {
compress: true,
encrypt: true,
},
).await?;
// Monitor progress
while let Some(progress) = transfer.progress().await {
println!("Transfer: {}%", progress.percentage);
}
Typical performance on local network:
If devices can't connect:
# Check if port is open
nc -zv 192.168.1.100 11204
# Monitor Iroh logs
RUST_LOG=iroh=debug cargo run
# Test connectivity
iroh doctor connect <node-id>
Problem: "Connection refused"
Problem: "Connection timeout"
Problem: "Pairing failed"
// Get connection info
let info = endpoint.connection_info(node_id).await?;
println!("RTT: {:?}", info.rtt);
println!("Congestion: {:?}", info.congestion_window);
// List connections
for conn in endpoint.connections() {
println!("Connected to: {}", conn.remote_node_id());
}
// Force relay connection
let mut node_addr = node_addr.clone();
node_addr.direct_addresses.clear(); // Force relay
New protocols are registered during initialization:
impl Protocol for CustomProtocol {
fn alpn(&self) -> &[u8] {
b"custom/1.0"
}
async fn handle(
&self,
connection: Connection,
) -> Result<()> {
// Handle incoming connection
}
}
// Register during startup
networking.register_protocol(Box::new(CustomProtocol::new()));
The networking module uses a typed error system:
pub enum NetworkError {
// Connection errors
ConnectionFailed(node_id: NodeId),
ConnectionTimeout(duration: Duration),
// Protocol errors
UnknownProtocol(alpn: String),
ProtocolViolation(reason: String),
// Security errors
InvalidSignature,
DecryptionFailed,
// Device errors
DeviceNotPaired(device_id: Uuid),
DeviceOffline(device_id: Uuid),
}
Device relationships and session keys are encrypted and persisted via the KeyManager:
// Paired device data stored per-device in KeyManager
pub struct PersistedPairedDevice {
device_info: DeviceInfo,
session_keys: SessionKeys,
paired_at: DateTime<Utc>,
trust_level: TrustLevel,
relay_url: Option<String>,
}
// Storage:
// - Device key: OS keychain or encrypted fallback file
// - Paired devices: KeyManager's encrypted KV store (secrets.redb)
// - Network identity: Derived from device key
Enhanced Discovery
Advanced Protocols
Infrastructure
Performance
The networking module is designed for extensibility:
// Custom protocol implementation
pub trait NetworkProtocol: Send + Sync {
fn alpn(&self) -> &[u8];
fn handle(&self, conn: Connection) -> BoxFuture<Result<()>>;
}
// Custom discovery mechanism
pub trait Discovery: Send + Sync {
fn discover(&self) -> BoxStream<NodeAddr>;
}
// Custom relay selection
pub trait RelaySelector: Send + Sync {
fn select(&self, relays: &[Url]) -> Url;
}