Skip to main content
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.

Architecture

The networking system manages all device-to-device communication through a single service that handles connections, protocols, and state management.

Core Components

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 },
}

Network Transport

Iroh provides the underlying transport using QUIC, which offers:
  • Built-in encryption using TLS 1.3
  • Multiplexed streams over a single connection
  • Reliable delivery with automatic retransmission
  • NAT traversal with 90%+ success rate
  • Relay fallback when direct connections fail

Protocol System

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)
}

Device Discovery

Devices find each other through multiple mechanisms:

Local Network Discovery

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()
));

Manual Connection

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?;
Direct addresses work on local networks. The relay URL enables connections across the internet when direct connections fail.

Device Pairing

Pairing establishes trust between devices using cryptographic signatures and user-friendly codes.

Pairing Flow

The initiator generates a pairing code that the joiner enters to establish trust.
1

Generate Pairing Code

The initiator creates a BIP39 mnemonic code:
// Initiator generates code
let code = PairingCode::generate(); // "brave-lion-sunset"
2

Exchange Device Info

Both devices exchange their information and public keys:
pub struct DeviceInfo {
    pub device_id: Uuid,
    pub device_name: String,
    pub device_type: DeviceType,
    pub public_key: VerifyingKey,
}
3

Challenge-Response

The initiator challenges the joiner to prove they have the code:
// Initiator sends challenge
let challenge = Challenge::random();

// Joiner signs challenge
let signature = identity.sign(&challenge);

// Initiator verifies signature
identity.verify(&challenge, &signature)?;
4

Establish Session

Both devices derive session keys for future communication:
// Derive shared secret using ECDH
let shared_secret = ecdh(my_private, their_public);

// Derive session keys
let keys = SessionKeys::from_shared_secret(shared_secret);

Pairing Security

The pairing protocol prevents several attacks:
  • Man-in-the-middle: Public key exchange with out-of-band verification
  • Replay attacks: Fresh challenges for each pairing attempt
  • Brute force: Rate limiting on pairing attempts
  • Eavesdropping: All communication encrypted after initial handshake

Message Protocol

Paired devices communicate using an encrypted messaging protocol.

Message Types

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 },
}

Message Flow

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(())
}

Reliability

QUIC provides reliable delivery, but the application layer adds:
  • Message acknowledgments for critical operations
  • Automatic retries with exponential backoff
  • Connection health monitoring with periodic pings
  • Graceful reconnection after network changes

File Transfer

The file transfer protocol enables secure, resumable file sharing between devices.

Transfer Process

1

Request File

Device A requests a file by its entry ID:
let request = FileRequest {
    entry_id: Uuid::parse_str("...")?,
    resume_from: Some(1048576), // Resume from 1MB
};
2

Stream Chunks

Device B streams the file in encrypted chunks:
// 256KB chunks
const CHUNK_SIZE: usize = 262144;

while let Some(chunk) = file.read_chunk(CHUNK_SIZE).await? {
    let encrypted = session_keys.encrypt(&chunk)?;
    stream.write_all(&encrypted).await?;
}
3

Verify Transfer

Both devices verify the transfer using checksums:
let checksum = blake3::hash(&file_data);
if checksum != expected_checksum {
    return Err(TransferError::ChecksumMismatch);
}

Transfer Features

  • Resumable transfers: Continue from where you left off
  • Progress tracking: Real-time updates on transfer status
  • Bandwidth throttling: Respect network limits
  • Parallel transfers: Multiple files simultaneously
  • Compression: Optional gzip compression for text files

Connection Management

The event loop handles all incoming connections and routes them appropriately.

Event Loop

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?;
                }
            }
        }
    }
}

Connection States

Connections transition through several states:
  1. Connecting: Initial QUIC handshake
  2. Connected: Active connection, can send/receive
  3. Idle: No recent activity, may be closed
  4. Closing: Graceful shutdown in progress
  5. Closed: Connection terminated

Keep-Alive

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;
        }
    }
}

NAT Traversal

Iroh handles NAT traversal automatically using several techniques:

Direct Connection

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);
    }
}

STUN

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?;

Relay Fallback

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
Relay servers don’t decrypt your data. They only forward encrypted packets between devices.

Security

Encryption Layers

The networking stack provides multiple encryption layers:
  1. Transport encryption: QUIC’s built-in TLS 1.3
  2. Application encryption: Additional encryption using session keys
  3. File encryption: Per-file encryption keys for transfers

Key Management

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>,
}

Trust Model

  • Device identity: Ed25519 signatures prove device authenticity
  • Pairing verification: Out-of-band code exchange prevents MITM
  • Forward secrecy: New keys for each session and transfer
  • No central authority: Direct device-to-device trust

API Usage

Initialize Networking

// 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());

Pair Devices

// 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?;

Send Messages

// 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?;

Transfer Files

// 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);
}

Performance

Benchmarks

Typical performance on local network:
  • Connection setup: 10-50ms
  • Message latency: 1-5ms
  • File transfer: 100MB/s+ (gigabit network)
  • Memory usage: ~10MB per connection

Optimization Strategies

  1. Connection pooling: Reuse connections for multiple operations
  2. Stream multiplexing: Multiple logical streams over one connection
  3. Adaptive chunking: Adjust chunk size based on network conditions
  4. Compression: Enable for text-heavy workloads

Troubleshooting

Connection Issues

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>

Common Problems

Problem: “Connection refused”
  • Check firewall allows UDP port 11204
  • Verify both devices are running
  • Ensure correct NodeId
Problem: “Connection timeout”
  • Check network allows UDP traffic
  • Try relay connection instead of direct
  • Verify NAT type using STUN
Problem: “Pairing failed”
  • Ensure pairing code is entered correctly
  • Check code hasn’t expired (5 minute timeout)
  • Verify clocks are roughly synchronized

Debug Commands

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

Implementation Details

Protocol Registration

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()));

Error Handling

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),
}

State Persistence

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

Future Development

Planned Features

Enhanced Discovery
  • DHT-based global discovery
  • Bluetooth device discovery
  • QR code pairing
Advanced Protocols
  • Video streaming protocol
  • Real-time collaboration
  • Distributed compute
Infrastructure
  • Custom relay servers
  • Relay server selection
  • Bandwidth quotas
Performance
  • Protocol buffer serialization
  • Native stream handling
  • Zero-copy transfers

Extension Points

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;
}
  • Devices - Device identity and pairing
  • Sync - Data synchronization over network
  • Security - Encryption and trust model