Skip to content

P2P-протокол

Файлы: backend/src/p2p/network.rs, pairing/libp2p_pairing.rs, sharing/libp2p_sharing.rs, sharing/types.rs, pairing/types.rs, sharing/crypto.rs

Два протокола поверх libp2p: pairing (однократный обмен identity) и sharing (постоянная синхронизация данных). Оба используют request-response поверх Noise+Yamux с mDNS-обнаружением.

Транспортный стек

plantuml Diagram
КомпонентОписание
TCPТранспорт Tokio, listen на /ip4/0.0.0.0/tcp/0 (случайный порт)
NoiseАутентифицированное шифрование с Ed25519 ключами устройства
YamuxМультиплексирование — несколько substreams в одном TCP-соединении
mDNSОбнаружение пиров в локальной сети
Request-ResponseJSON-сериализованные сообщения с ответом

Feature: test-loopback

При сборке с --features test-loopback все пиры подключаются через 127.0.0.1 вместо mDNS-обнаруженных IP-адресов.

На обычной dev-машине с рабочей сетью (Wi-Fi/Ethernet) эта фича не обязательна — бекенды на одном хосте подключаются друг к другу по реальному LAN IP. Фича полезна как страховка в средах, где реальный IP может быть недоступен (CI-раннеры, Docker, VM без LAN).

rust
// p2p/utils.rs
pub fn dial_addr_for(multiaddr: &Multiaddr) -> Option<Multiaddr> {
    #[cfg(feature = "test-loopback")]
    { /* replace IP with 127.0.0.1, keep port */ }

    #[cfg(not(feature = "test-loopback"))]
    { /* use original multiaddr */ }
}

Pairing Protocol

Protocol ID: /kontinuum/pairing/1.0.0

Два режима паринга: QR-based (legacy) и Bluetooth-style discovery с PIN. Ссылки: PairingManager, pairing service.

Типы сообщений

Legacy (QR)

ТипНазначениеКлючевые поля
PairingRequestMsgЗапрос info или pairingget_info, pairing_request?
PairingResponseMsgОтвет с info или результатомinfo?, pairing_response?
PairingRequestДанные паринг-запросаtoken, device_info
PairingResponseРезультат парингаapproved, identity?, error?

Discovery (PIN-based)

ТипНазначениеКлючевые поля
DiscoverymDNS объявлениеidentity_id, identity_display_name, device_name
InitiateЗапрос pairing с PINinitiator_device, initiator_identity, pin (6-digit), device_count
ConfirmПодтверждение/отказapproved, pin, confirmer_device?, confirmer_identity?, error?

Данные, передаваемые при паринге

rust
pub struct PairingDeviceInfo {
    pub name: String,            // "MacBook Pro"
    pub device_type: String,     // "laptop", "phone", "tablet", ...
    pub public_key: Vec<u8>,     // Ed25519 public key (32 bytes)
    pub peer_id: Option<String>, // libp2p PeerId
    pub os: Option<String>,
    pub storage: Option<StorageInfo>,
}

pub struct PairingIdentityData {
    pub id: String,
    pub display_name: String,
    pub public_key: Vec<u8>,              // Ed25519 public key
    pub signing_key: Option<Vec<u8>>,     // Ed25519 private key (если vault unlocked)
    pub devices: Vec<PairingDeviceInfo>,  // Все устройства identity
    pub spaces: Vec<SpaceInfo>,           // Пространства для синхронизации
}

Константы

КонстантаЗначениеОписание
Token TTL300 секВремя жизни QR-токена
Idle connection timeout300 секТаймаут неактивного pairing-соединения
mpsc channel capacity10Буфер команд к background service

QR-pairing flow

plantuml Diagram

PIN discovery flow

plantuml Diagram

Sharing Protocol

Protocol ID: /kontinuum/sharing/1.0.0

Постоянный сервис для синхронизации данных между устройствами. Ссылки: SharingManager, sharing service.

Типы сообщений

Подключение

ТипНазначениеКлючевые поля
AnnounceАнонс при подключенииidentity_id, device_id, identity_public_key, device_name, storage, ip
PingHeartbeattimestamp, storage, device_count
PongОтвет на pingtimestamp, storage, device_count
DeviceRevokedУведомление об отзывеold_identity_id, reason

Пространства

ТипНазначениеКлючевые поля
PublishПубликация (plaintext)SpacePayload
ShareПриватный шаринг (encrypted)target_identity_id, ephemeral_public_key, nonce, encrypted_payload
AckПодтверждение приёмаspace_id, accepted

Файловый трансфер

ТипНазначениеКлючевые поля
FileRequestЗапрос файлаspace_id, file_hash
FileChunkChunk данныхspace_id, file_hash, offset, data, total_size, is_last

State sync

ТипНазначениеКлючевые поля
SyncDigestСравнение хешей состоянияstate_hash, device_count, space_count, cloud_count
SyncSnapshotПолный снимок для mergedevices, spaces, clouds, tombstones
SyncUpdateИнкрементальное обновлениеidentity_id, origin_device_id, update (8 вариантов)
DeviceSyncСинхронизация устройствidentity_id, devices[]

Удалённая навигация

ТипНазначениеКлючевые поля
DirectoryListRequestЗапрос содержимого директорииrequest_id, path
DirectoryListResponseОтветrequest_id, entries[], error?

SpacePayload

rust
pub struct SpacePayload {
    pub space_id: String,
    pub space_name: String,
    pub space_description: Option<String>,
    pub icon: Option<String>,
    pub color: Option<String>,
    pub owner_identity_id: String,
    pub owner_identity_name: String,
    pub files: Vec<SharedFileMetadata>,
    pub timestamp: u64,
}

StateUpdate варианты

rust
pub enum StateUpdate {
    DeviceAdded(DeviceSyncInfo),
    DeviceRemoved { device_id: String, timestamp: u64 },
    SpaceCreated(SpaceSyncEntry),
    SpaceUpdated(SpaceSyncEntry),
    SpaceDeleted { space_id: String, timestamp: u64 },
    CloudAdded(CloudSyncEntry),
    CloudUpdated(CloudSyncEntry),
    CloudDeleted { cloud_id: String, timestamp: u64 },
}

Константы

КонстантаЗначениеОписание
Chunk size32 KBРазмер чанка файлового трансфера
Heartbeat interval30 секИнтервал Ping/Pong
Idle connection timeout120 секТаймаут неактивного sharing-соединения
Stale peer timeout90 секPeer без Pong помечается как offline
mpsc channel capacity64Буфер SharingCommand
Tombstone TTL30 днейВремя жизни tombstone записей

Шифрование приватного шаринга

Файл: backend/src/stronghold.rs (примитивы seal_for_recipients / unseal_envelope)

SharingMessage::Share использует SealedEnvelope — hybrid encryption: один random content_key AES-256-GCM-шифрует payload, fan-out wrap'ится через X25519 на каждое recipient device-box-pubkey (DeviceBoxKey генерируется внутри Stronghold). Сложность по wire: O(recipients * 60B) + O(plaintext) вместо O(recipients * plaintext_len).

plantuml Diagram

Криптографические примитивы

ОперацияБиблиотекаОписание
DeviceBoxKeyiota_strongholdX25519 secret keypair, generated in-place, non-extractable
Ephemeral DHx25519_dalekPer-recipient ephemeral X25519 для key-wrap layer
KDFhkdf (SHA-256)info="kontinuum sharing key-wrap"
AEADaes-gcm (AES-256-GCM)12-байтный nonce, 16-байтный tag, оба слоя (wrap + content)
CSPRNGrand::OsRngRandom content_key, ephemeral keys, nonces

Legacy ECDH-on-Ed25519-signing-key (через ed25519_signing_to_x25519_secret) удалён в slice 16d: подписи-ключ и box-ключ — это разные крипто-роли, путать их нельзя.

State Sync

Файл: backend/src/sharing/state_sync.rs

CRDT-inspired синхронизация между устройствами одной identity.

Механизм

  1. При обнаружении пира с той же identity — отправляется SyncDigest (blake3 hash текущего состояния)
  2. Если digest отличается — запрашивается/отправляется SyncSnapshot (полное состояние)
  3. Merge: OR-Set семантика для коллекций, LWW-Register для полей (timestamp wins)
  4. Tombstones для пропагации удалений (TTL 30 дней)

State Hash

rust
fn compute_state_hash(devices, spaces, clouds, tombstones) -> String {
    // blake3 hasher
    // Sorted IDs → deterministic hash
    // "d:<device_id>" + "s:<space_id>" + "c:<cloud_id>" + "t:<type>:<id>"
    // → hex string
}