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-обнаружением.
Транспортный стек
| Компонент | Описание |
|---|---|
| TCP | Транспорт Tokio, listen на /ip4/0.0.0.0/tcp/0 (случайный порт) |
| Noise | Аутентифицированное шифрование с Ed25519 ключами устройства |
| Yamux | Мультиплексирование — несколько substreams в одном TCP-соединении |
| mDNS | Обнаружение пиров в локальной сети |
| Request-Response | JSON-сериализованные сообщения с ответом |
Feature: test-loopback
При сборке с --features test-loopback все пиры подключаются через 127.0.0.1 вместо mDNS-обнаруженных IP-адресов.
На обычной dev-машине с рабочей сетью (Wi-Fi/Ethernet) эта фича не обязательна — бекенды на одном хосте подключаются друг к другу по реальному LAN IP. Фича полезна как страховка в средах, где реальный IP может быть недоступен (CI-раннеры, Docker, VM без LAN).
// 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 или pairing | get_info, pairing_request? |
PairingResponseMsg | Ответ с info или результатом | info?, pairing_response? |
PairingRequest | Данные паринг-запроса | token, device_info |
PairingResponse | Результат паринга | approved, identity?, error? |
Discovery (PIN-based)
| Тип | Назначение | Ключевые поля |
|---|---|---|
Discovery | mDNS объявление | identity_id, identity_display_name, device_name |
Initiate | Запрос pairing с PIN | initiator_device, initiator_identity, pin (6-digit), device_count |
Confirm | Подтверждение/отказ | approved, pin, confirmer_device?, confirmer_identity?, error? |
Данные, передаваемые при паринге
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 TTL | 300 сек | Время жизни QR-токена |
| Idle connection timeout | 300 сек | Таймаут неактивного pairing-соединения |
| mpsc channel capacity | 10 | Буфер команд к background service |
QR-pairing flow
PIN discovery flow
Sharing Protocol
Protocol ID: /kontinuum/sharing/1.0.0
Постоянный сервис для синхронизации данных между устройствами. Ссылки: SharingManager, sharing service.
Типы сообщений
Подключение
| Тип | Назначение | Ключевые поля |
|---|---|---|
Announce | Анонс при подключении | identity_id, device_id, identity_public_key, device_name, storage, ip |
Ping | Heartbeat | timestamp, storage, device_count |
Pong | Ответ на ping | timestamp, 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 |
FileChunk | Chunk данных | space_id, file_hash, offset, data, total_size, is_last |
State sync
| Тип | Назначение | Ключевые поля |
|---|---|---|
SyncDigest | Сравнение хешей состояния | state_hash, device_count, space_count, cloud_count |
SyncSnapshot | Полный снимок для merge | devices, 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
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 варианты
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 size | 32 KB | Размер чанка файлового трансфера |
| Heartbeat interval | 30 сек | Интервал Ping/Pong |
| Idle connection timeout | 120 сек | Таймаут неактивного sharing-соединения |
| Stale peer timeout | 90 сек | Peer без Pong помечается как offline |
| mpsc channel capacity | 64 | Буфер SharingCommand |
| Tombstone TTL | 30 дней | Время жизни 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).
Криптографические примитивы
| Операция | Библиотека | Описание |
|---|---|---|
| DeviceBoxKey | iota_stronghold | X25519 secret keypair, generated in-place, non-extractable |
| Ephemeral DH | x25519_dalek | Per-recipient ephemeral X25519 для key-wrap layer |
| KDF | hkdf (SHA-256) | info="kontinuum sharing key-wrap" |
| AEAD | aes-gcm (AES-256-GCM) | 12-байтный nonce, 16-байтный tag, оба слоя (wrap + content) |
| CSPRNG | rand::OsRng | Random 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.
Механизм
- При обнаружении пира с той же identity — отправляется
SyncDigest(blake3 hash текущего состояния) - Если digest отличается — запрашивается/отправляется
SyncSnapshot(полное состояние) - Merge: OR-Set семантика для коллекций, LWW-Register для полей (timestamp wins)
- Tombstones для пропагации удалений (TTL 30 дней)
State Hash
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
}