# Budování decentralizovaného messengeru: integrace libp2p do Flutteru přes Go
Současné messengery vyžadují soukromí a decentralizaci. V tomto článku rozebereme implementaci P2P messengeru s end-to-end šifrováním na bázi libp2p. Řešení využívá Go pro síťovou logiku, Flutter pro UI a FFI pro interakci mezi komponenty. Architektura vylučuje centrální servery a zajišťuje bezpečnost i při obcházení NAT a firewallů.
Základy architektury: Flutter, Go a FFI
Klíčovým úkolem je integrace libp2p do mobilní aplikace bez kompromisů v výkonu. Flutter se stará o uživatelské rozhraní, zatímco síťová logika je vynechána do Go binárníku, který se kompiluje do:
.dylibpro macOS.sopro Android- Statické knihovny
.apro iOS
Komunikace mezi Dart a Go probíhá přes FFI (Foreign Function Interface) s využitím C-kompatibilních funkcí. To umožňuje vyhnout se nákladům na serializaci a zachovat nativní rychlost výměny dat. Je důležité zohlednit specifika správy paměti: řetězcová data přenášená z Go do Dart vyžaduje ruční uvolnění přes FreeString.
Sestavení Go knihovny pro mobilní platformy
Proces kompilace vyžaduje platformově specifická nastavení. Pro Android se používá 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
Pro 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 Androidu se knihovna umístí do jniLibs/arm64-v8a/, na iOS se linkuje přes Xcode. Pro simulátor je potřeba sestavení pro x86_64 a sloučení archivů přes lipo -create.
FFI most: komunikace mezi Go a Dart
Export funkcí z Go vyžaduje dodržení přísných pravidel:
- Všechny funkce jsou označeny komentářem
//export - Nutná je prázdná funkce
func main() {} - Správa paměti:
C.CString()alokuje paměť v C heapu, kterou musí Dart uvolnit přesFreeString
Příklad exportu z Go:
//export StartNode
func StartNode(storagePath *C.char) *C.char {
path := C.GoString(storagePath)
node, err := p2p.NewNode(path)
// ...
return C.CString(node.GetPeerID())
}
V Dart probíhá načtení knihovny a vazba funkcí:
_loadLibrary() {
if (Platform.isAndroid) {
_lib = DynamicLibrary.open('libp2p_network.so');
}
_startNode = _lib!.lookupFunction<
Pointer<Utf8> Function(Pointer<Utf8>),
Pointer<Utf8> Function(Pointer<Utf8>)
>('StartNode');
}
Volání funkce zahrnuje převod řetězců a ruční správu paměti:
final pathPtr = dir.path.toNativeUtf8();
final resultPtr = _startNode(pathPtr);
final peerId = resultPtr.toDartString();
_freeString(resultPtr);
calloc.free(pathPtr);
Práce s libp2p: vytvoření uzlu a výměna zpráv
Inicializace uzlu zahrnuje nastavení transportů a protokolů:
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ždé zařízení získává unikátní PeerID – hash veřejného Ed25519 klíče. Odesílání zpráv je realizováno přes streamovou výměnu:
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
}
Zprávy se doručují přímo při online spojení nebo přes reléovou uzlu v offline režimu. Zašifrovaná data se dočasně ukládají, dokud se připojí příjemce.
Zajistění spojení v náročných síťových podmínkách
Pro hlasové volání je implementována třístupňová transportní vrstva:
- Přímý UDP transport (Pion ICE) – primární kanál s minimální latencí. Výměna ICE kandidátů probíhá přes libp2p zprávy.
- DCUtR (Hole Punching) – mechanismus propíchnutí spojení přes NAT pomocí Relay.
- Fallback přes Relay – přenos provozu přes reléovou uzlu, pokud přímé spojení není možné.
Kód pro zpracování audioprůtoku ukazuje práci s vlastním packetovým formátem:
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
}
Příjem dat je organizován v cyklu s rozpoznáním synchronizačních bajtů a zpracováním Opus frameů.
Šifrování a bezpečnost: Double Ratchet v praxi
Pro soukromé chaty je implementován protokol Double Ratchet s Perfect Forward Secrecy a Post-Compromise Security. Klíčové vlastnosti:
- Každá zpráva je šifrována unikátním klíčem
- Periodická rotace klíčů
- Ochrana před kompromitací dlouhodobých klíčů
Systém zajišťuje end-to-end šifrování bez předávání klíčů přes mezilehlé uzly. Všechny kryptografické operace probíhají na zařízení uživatele, což vylučuje úniky při ukládání dat na reléových uzlech.
Zpracování offline scénářů bez kompromisu soukromí
Push notifikace slouží výhradně jako spouštěče pro probuzení aplikace. Důležité je, že:
- Push zprávy neobsahují data zpráv
- Neobsahují informace o odesílateli
- Spouštěč obsahuje pouze služebný příznak
Postup práce:
- Přijde silent push notifikace
- OS krátce aktivuje aplikaci
- Go uzel se připojí k síti a stáhne zašifrované pakety
- Dešifrování probíhá lokálně
- Vytvoří se lokální notifikace pro uživatele
Tento přístup zachovává E2EE i při použití centralizovaných push služeb.
Co je důležité
- FFI most vyžaduje striktní správu paměti: úniky vznikají při vynechání volání
FreeString - Circuit Relay v2 je klíčový pro obcházení NAT, ale zvyšuje latenci o 15–20 %
- Double Ratchet zajišťuje PFS, ale vyžaduje synchronizaci stavu mezi zařízeními
- Push spouštěče neporušují soukromí, protože neobsahují užitečné data
- Sestavení pro iOS vyžaduje ruční sloučení archivů pro simulátor a zařízení
— Editorial Team
Zatím žádné komentáře.