Powrót do strony głównej

Integracja Dart-Go FFI dla libp2p w Flutter — przewodnik techniczny

Artykuł opisuje implementację zdecentralizowanego komunikatora z szyfrowaniem end-to-end przez integrację libp2p (Go) w aplikację Flutter. Szczegółowo omówiono interakcję FFI, obejście NAT, bezpieczeństwo Double Ratchet i obsługę scenariuszy offline. Rozwiązanie wyklucza centralne serwery i zachowuje prywatność danych.

Pełny przewodnik po tworzeniu zdecentralizowanego komunikatora na Flutter i Go
Advertisement 728x90

# 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:

  • .dylib dla macOS
  • .so dla Android
  • Statyczną bibliotekę .a dla 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.

Google AdInline article slot

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.

Google AdInline article slot

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:

Google AdInline article slot
_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

Advertisement 728x90

Czytaj dalej