Разработка P2P функциональности
Транспортный стек
Kontinuum использует libp2p для P2P-коммуникации:
| Компонент | Описание |
|---|---|
| TCP | Транспорт Tokio, listen на /ip4/0.0.0.0/tcp/0 (случайный порт) |
| Noise | Аутентифицированное шифрование с Ed25519 ключами устройства |
| Yamux | Мультиплексирование — несколько substreams в одном TCP-соединении |
| mDNS | Обнаружение пиров в локальной сети |
| Request-Response | JSON-сериализованные сообщения с ответом |
Подробная документация протоколов и типов сообщений: 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. Определи тип сообщения
// 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
// backend/src/sharing/libp2p_sharing.rs
match message {
// ... существующие обработчики ...
SharingMessage::MyNewMessage { field1, field2 } => {
handle_my_new_message(field1, field2, &managers).await?;
}
}3. Добавь команду для отправки
// 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. Создай сервисную функцию
// 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. Зарегистрируй и протестируй
- Добавь маршрут в
router.rsиhandlers.rs - Добавь маршрут в
command-routes.ts - Напиши интеграционный тест (см. Тестирование: написание нового сценария)
Тестирование 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-интерфейсе
// 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:
# Все 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 нужно запускать конкурентно:
const [submitResponse, approval] = await Promise.all([
joiner.submitPairingRequest(target, 'Device-joiner', 'desktop'),
sleep(1000).then(() => host.approvePairing(target.token)),
]);Отладка mDNS discovery
Устройства не обнаруживаются
Проверь сеть: на обычной dev-машине с Wi-Fi/Ethernet P2P работает без
test-loopback. Если сеть нестандартная (CI, Docker, VM), добавь фичу:bashcargo build --features headless,test-loopback --bin kontinuum-headlessПроверь firewall: mDNS использует UDP multicast на порту 5353
bash# Linux sudo ufw allow 5353/udp # Или временно sudo iptables -A INPUT -p udp --dport 5353 -j ACCEPTПроверь сеть: устройства должны быть в одной подсети
bash# Проверить mDNS пакеты sudo tcpdump -i any port 5353Проверь health бекендов:
bashcurl 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_STATEandroid.permission.ACCESS_WIFI_STATE
Подробнее: Mobile: Android.
Ссылки
- Internals: P2P Protocol — полная документация протоколов, типы сообщений, sequence diagrams
- PairingManager — методы, state, background tasks
- SharingManager — методы, file sync, state sync
- Pairing service — API endpoints
- Sharing service — API endpoints