# Budowa zdecentralizowanego komunikatora: integracja libp2p w Flutterze za pomocą Go
Współczesne komunikatory wymagają prywatności i decentralizacji. W tym artykule omówimy implementację komunikatora P2P z szyfrowaniem end-to-end opartym na libp2p. Rozwiązanie wykorzystuje Go do logiki sieciowej, Flutter do interfejsu użytkownika i FFI do interakcji między komponentami. Architektura eliminuje centralne serwery, zapewniając bezpieczeństwo nawet przy obejściu NAT i zapór sieciowych.
Podstawy architektury: Flutter, Go i FFI
Kluczowym zadaniem jest integracja libp2p z aplikacją mobilną bez kompromisów w wydajności. Flutter odpowiada za interfejs, a logika sieciowa jest wyodrębniona do binarnego pliku Go, kompilowanego w:
.dylibdla macOS.sodla Android- Statyczną bibliotekę
.adla iOS
Interakcja między Dart a Go odbywa się poprzez FFI (Foreign Function Interface) z wykorzystaniem funkcji kompatybilnych z C. Pozwala to uniknąć kosztów serializacji i zachować natywną prędkość wymiany danych. Ważne jest uwzględnienie specyfiki zarządzania pamięcią: dane stringowe przekazywane z Go do Dart wymagają ręcznego zwolnienia za pomocą FreeString.
Kompilacja biblioteki Go dla platform mobilnych
Proces kompilacji wymaga ustawień specyficznych dla platformy. Dla Android używany jest NDK:
CGO_ENABLED=1 GOOS=android GOARCH=arm64 \
CC=$ANDROID_NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android21-clang \
go build -buildmode=c-shared \
-o libp2p_network.so \
./main.go
Dla iOS:
CGO_ENABLED=1 GOOS=ios GOARCH=arm64 \
CC=$(xcrun --sdk iphoneos --find clang) \
CGO_CFLAGS="-isysroot $(xcrun --sdk iphoneos --show-sdk-path) -arch arm64 -miphoneos-version-min=12.0" \
CGO_LDFLAGS="-isysroot $(xcrun --sdk iphoneos --show-sdk-path) -arch arm64 -miphoneos-version-min=12.0" \
go build -buildmode=c-archive \
-o libp2p_network.a \
./main.go
Na Android biblioteka jest umieszczana w jniLibs/arm64-v8a/, na iOS linkowana poprzez Xcode. Dla symulatora wymagana jest kompilacja pod x86_64 i połączenie archiwów za pomocą lipo -create.
Most FFI: interakcja Go i Dart
Eksport funkcji z Go wymaga przestrzegania ścisłych reguł:
- Wszystkie funkcje oznaczane są komentarzem
//export - Obowiązkowa obecność pustej
func main() {} - Zarządzanie pamięcią:
C.CString()alokuje pamięć w stercie C, którą Dart musi zwolnić za pomocąFreeString
Przykład eksportu Go:
//export StartNode
func StartNode(storagePath *C.char) *C.char {
path := C.GoString(storagePath)
node, err := p2p.NewNode(path)
// ...
return C.CString(node.GetPeerID())
}
W Dart następuje załadowanie biblioteki i powiązanie funkcji:
_loadLibrary() {
if (Platform.isAndroid) {
_lib = DynamicLibrary.open('libp2p_network.so');
}
_startNode = _lib!.lookupFunction<
Pointer<Utf8> Function(Pointer<Utf8>),
Pointer<Utf8> Function(Pointer<Utf8>)
>('StartNode');
}
Wywołanie funkcji obejmuje konwersję stringów i ręczne zarządzanie pamięcią:
final pathPtr = dir.path.toNativeUtf8();
final resultPtr = _startNode(pathPtr);
final peerId = resultPtr.toDartString();
_freeString(resultPtr);
calloc.free(pathPtr);
Praca z libp2p: tworzenie węzła i wymiana wiadomości
Inicjalizacja węzła obejmuje konfigurację transportów i protokołów:
func NewNode(storagePath string) (*Node, error) {
h, err := libp2p.New(
libp2p.Identity(priv),
libp2p.ListenAddrStrings(
"/ip4/0.0.0.0/tcp/0",
"/ip4/0.0.0.0/udp/0/quic-v1",
),
libp2p.EnableNATService(),
libp2p.EnableRelay(),
libp2p.NATPortMap(),
)
// ...
}
Każde urządzenie otrzymuje unikalny PeerID — hash klucza publicznego Ed25519. Wysyłanie wiadomości jest realizowane poprzez strumieniową wymianę:
func (n *Node) SendMessage(peerIDStr, content, msgType, id string) error {
peerID, _ := peer.Decode(peerIDStr)
s, err := n.host.NewStream(ctx, peerID, "/messaging/1.0.0")
s.Write(data)
s.Close()
return nil
}
Wiadomości są dostarczane bezpośrednio przy połączeniu online lub poprzez węzeł serwerowy w trybie offline. Szyfrowane dane są przechowywane tymczasowo do czasu podłączenia odbiorcy.
Zapewnienie łączności w trudnych warunkach sieciowych
Dla połączeń głosowych zaimplementowano trójpoziomowy system transportu:
- Bezpośredni transport UDP (Pion ICE) — główny kanał z minimalnym opóźnieniem. Wymiana kandydatów ICE odbywa się poprzez wiadomości libp2p.
- DCUtR (Hole Punching) — mechanizm przebicia połączenia przez NAT za pomocą Relay.
- Fallback przez Relay — przekazywanie ruchu poprzez węzeł serwerowy przy braku możliwości bezpośredniego połączenia.
Kod obsługi strumienia audio demonstruje pracę z niestandardowym formatem pakietów:
func (n *Node) SendAudio(data []byte) error {
packet := make([]byte, 1+2+len(data))
packet[0] = 0xFE
binary.BigEndian.PutUint16(packet[1:], uint16(len(data)))
copy(packet[3:], data)
call.Stream.Write(packet)
return nil
}
Odbiór danych jest zorganizowany poprzez pętlę z rozpoznawaniem bajtów synchronizacyjnych i przetwarzaniem ramek Opus.
Szyfrowanie i bezpieczeństwo: Double Ratchet w akcji
Dla czatów prywatnych zaimplementowano protokół Double Ratchet z Perfect Forward Secrecy i Post-Compromise Security. Kluczowe cechy:
- Każda wiadomość jest szyfrowana unikalnym kluczem
- Okresowa rotacja kluczy
- Ochrona przed kompromitacją kluczy długoterminowych
System gwarantuje szyfrowanie end-to-end bez przekazywania kluczy przez węzły pośrednie. Wszystkie operacje kryptograficzne są wykonywane na urządzeniu użytkownika, co eliminuje wycieki przy przechowywaniu danych na węzłach retransmisyjnych.
Obsługa scenariuszy offline bez kompromisów w prywatności
Push-powiadomienia są używane wyłącznie jako wyzwalacze do obudzenia aplikacji. Ważne, że:
- W powiadomieniach push brak danych wiadomości
- Brak informacji o nadawcy
- Wyzwalacz zawiera tylko flagę służbową
Sekwencja pracy:
- Przylatuje ciche powiadomienie push
- System operacyjny na krótko aktywuje aplikację
- Węzeł Go podłącza się do sieci i pobiera zaszyfrowane pakiety
- Deszyfrowanie odbywa się lokalnie
- Tworzone jest lokalne powiadomienie dla użytkownika
To podejście zachowuje E2EE nawet przy użyciu scentralizowanych usług push-powiadomień.
Co ważne
- Most FFI wymaga ścisłego zarządzania pamięcią: wycieki pojawiają się przy pominięciu wywołania
FreeString - Circuit Relay v2 jest kluczowy dla obejścia NAT, ale zwiększa opóźnienie o 15-20%
- Double Ratchet zapewnia PFS, ale wymaga synchronizacji stanu między urządzeniami
- Wyzwalacze push nie naruszają prywatności, ponieważ nie zawierają użytecznej zawartości
- Kompilacja pod iOS wymaga ręcznego łączenia archiwów dla symulatora i urządzenia
— Editorial Team
Brak komentarzy.