Skip to content

Разработка P2P функциональности

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

Kontinuum использует libp2p для P2P-коммуникации:

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

Подробная документация протоколов и типов сообщений: Internals: P2P Protocol.

Два протокола

Pairing (/kontinuum/pairing/1.0.0)

Однократный обмен identity между устройствами. Два режима:

  • QR-based (legacy): host отображает QR → joiner сканирует → обмен identity
  • PIN-based discovery: mDNS обнаружение → 6-значный PIN → подтверждение → обмен identity

При pairing'е устройство с меньшим числом девайсов принимает identity другого (ADR-5).

Подробнее: PairingManager, Pairing service, P2P: Pairing Protocol.

Sharing (/kontinuum/sharing/1.0.0)

Постоянный сервис для синхронизации данных:

  • Publish — broadcast пространства всем пирам (plaintext)
  • Share — приватное расшаривание (ECDH + ChaCha20Poly1305)
  • File sync — передача файлов чанками по 32 KB с blake3 верификацией
  • State sync — CRDT-inspired синхронизация устройств/пространств/облаков
  • Remote directory — листинг директорий на удалённом устройстве

Подробнее: SharingManager, Sharing service, P2P: Sharing Protocol.

Добавление нового типа P2P сообщений

1. Определи тип сообщения

rust
// backend/src/sharing/types.rs (или pairing/types.rs)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SharingMessage {
    // ... существующие типы ...
    MyNewMessage {
        field1: String,
        field2: Vec<u8>,
    },
}

2. Добавь обработку в background service

rust
// backend/src/sharing/libp2p_sharing.rs
match message {
    // ... существующие обработчики ...
    SharingMessage::MyNewMessage { field1, field2 } => {
        handle_my_new_message(field1, field2, &managers).await?;
    }
}

3. Добавь команду для отправки

rust
// backend/src/sharing/manager.rs
pub enum SharingCommand {
    // ... существующие команды ...
    MyNewAction { field1: String },
}

impl SharingManager {
    pub fn send_my_new_message(&self, field1: &str) -> Result<()> {
        let tx = self.command_tx.as_ref().ok_or("Sharing not running")?;
        tx.send(SharingCommand::MyNewAction { field1: field1.to_string() })
            .map_err(|e| anyhow::anyhow!("Send failed: {e}"))
    }
}

4. Создай сервисную функцию

rust
// backend/src/services/sharing.rs
#[api(POST, "/api/sharing/my-action")]
pub async fn my_action_svc(
    field1: String,
    sharing_manager: &Arc<Mutex<SharingManager>>,
) -> Result<(), String> {
    let mgr = sharing_manager.lock().map_err(|e| format!("{e}"))?;
    mgr.send_my_new_message(&field1).map_err(|e| e.to_string())
}

5. Зарегистрируй и протестируй

Тестирование P2P локально

Feature test-loopback

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

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

Когда test-loopback полезен — как страховка в средах, где реальный IP может быть недоступен:

  • CI-раннеры с нестандартной сетью
  • Docker-контейнеры (bridge networking)
  • VM без LAN-интерфейса
  • Строгий firewall, блокирующий input на LAN-интерфейсе
rust
// p2p/utils.rs — при test-loopback IP заменяется на 127.0.0.1
pub fn dial_addr_for(multiaddr: &Multiaddr) -> Option<Multiaddr> {
    #[cfg(feature = "test-loopback")]
    { /* replace IP with 127.0.0.1, keep port */ }
}

Интеграционные тесты

Самый надёжный способ тестировать P2P:

bash
# Все P2P сценарии
cd infra/testing
npx tsx src/runner.ts --no-build --no-observability --scenario pairing --verbose
npx tsx src/runner.ts --no-build --no-observability --scenario sharing --verbose

# Ручная инспекция: запустить бекенды без тестов
npx tsx src/runner.ts --no-build --no-observability --no-tests --verbose
# Затем использовать curl для ручного тестирования API

Паттерн: bidirectional pairing

submitPairingRequest() блокируется до получения ответа от host'а. Submit и approve нужно запускать конкурентно:

typescript
const [submitResponse, approval] = await Promise.all([
  joiner.submitPairingRequest(target, 'Device-joiner', 'desktop'),
  sleep(1000).then(() => host.approvePairing(target.token)),
]);

Отладка mDNS discovery

Устройства не обнаруживаются

  1. Проверь сеть: на обычной dev-машине с Wi-Fi/Ethernet P2P работает без test-loopback. Если сеть нестандартная (CI, Docker, VM), добавь фичу:

    bash
    cargo build --features headless,test-loopback --bin kontinuum-headless
  2. Проверь firewall: mDNS использует UDP multicast на порту 5353

    bash
    # Linux
    sudo ufw allow 5353/udp
    # Или временно
    sudo iptables -A INPUT -p udp --dport 5353 -j ACCEPT
  3. Проверь сеть: устройства должны быть в одной подсети

    bash
    # Проверить mDNS пакеты
    sudo tcpdump -i any port 5353
  4. Проверь health бекендов:

    bash
    curl http://localhost:8081/api/health
    curl http://localhost:8082/api/health

Android mDNS

На Android mDNS работает через NSD (Network Service Discovery). Необходимые permissions:

  • android.permission.CHANGE_WIFI_MULTICAST_STATE
  • android.permission.ACCESS_WIFI_STATE

Подробнее: Mobile: Android.

Ссылки