Retour à l'accueil

Intégration FFI Dart-Go pour libp2p dans Flutter — Guide Technique

L’article décrit l’implémentation d’un messager décentralisé avec chiffrement de bout en bout par l’intégration de libp2p (Go) dans une application Flutter. Interaction FFI, traversée NAT, sécurité Double Ratchet et gestion des scénarios hors ligne sont couverts en détail. La solution élimine les serveurs centraux et préserve la confidentialité des données.

Guide complet pour créer un messager décentralisé sur Flutter et Go
Advertisement 728x90

# Construire un messager décentralisé : Intégrer libp2p dans Flutter via Go

Les applications de messagerie d'aujourd'hui exigent confidentialité et décentralisation. Cet article détaille l'implémentation d'un messager P2P avec chiffrement de bout en bout basé sur libp2p. La solution utilise Go pour la logique réseau, Flutter pour l'interface utilisateur, et FFI pour l'interaction entre composants. L'architecture élimine les serveurs centraux, garantissant la sécurité même en contournant NAT et pare-feu.

Bases de l'architecture : Flutter, Go et FFI

Le défi principal consiste à intégrer libp2p dans une application mobile sans compromettre les performances. Flutter gère l'interface, tandis que la logique réseau est déplacée vers un binaire Go compilé en :

  • .dylib pour macOS
  • .so pour Android
  • Bibliothèque statique .a pour iOS

Dart et Go interagissent via FFI (Foreign Function Interface) en utilisant des fonctions compatibles C. Cela évite les surcoûts de sérialisation et maintient les vitesses d'échange de données natives. La gestion de la mémoire est cruciale : les données de chaînes passées de Go vers Dart nécessitent une libération manuelle via FreeString.

Google AdInline article slot

Compilation de la bibliothèque Go pour les plateformes mobiles

Le processus de compilation nécessite des paramètres spécifiques à chaque plateforme. Pour Android, utilisez 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

Pour 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

Sur Android, la bibliothèque est placée dans jniLibs/arm64-v8a/. Sur iOS, elle est liée via Xcode. Pour le simulateur, compilez pour x86_64 et fusionnez les archives avec lipo -create.

Google AdInline article slot

Pont FFI : Interaction entre Go et Dart

L'exportation de fonctions depuis Go nécessite des règles strictes :

  • Toutes les fonctions sont marquées avec //export
  • Inclure une fonction main vide func main() {}
  • Gestion de la mémoire : C.CString() alloue dans le tas C, que Dart doit libérer via FreeString

Exemple d'export Go :

//export StartNode
func StartNode(storagePath *C.char) *C.char {
    path := C.GoString(storagePath)
    node, err := p2p.NewNode(path)
    // ...
    return C.CString(node.GetPeerID())
}

En Dart, chargez la bibliothèque et liez les fonctions :

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');
}

L'appel de la fonction implique conversion de chaînes et gestion manuelle de la mémoire :

final pathPtr = dir.path.toNativeUtf8();
final resultPtr = _startNode(pathPtr);
final peerId = resultPtr.toDartString();
_freeString(resultPtr);
calloc.free(pathPtr);

Utilisation de libp2p : Création d'un nœud et échange de messages

L'initialisation du nœud configure les transports et protocoles :

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(),
    )
    // ...
}

Chaque appareil obtient un PeerID unique — un hachage de la clé publique Ed25519. L'envoi de messages utilise un échange basé sur des flux :

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
}

Les messages sont livrés directement en ligne ou via un nœud relais hors ligne. Les données chiffrées sont stockées temporairement jusqu'à ce que le destinataire se connecte.

Assurer la connectivité dans des conditions réseau difficiles

Pour les appels vocaux, un système de transport à trois niveaux est implémenté :

  • Transport UDP direct (Pion ICE) — canal principal avec latence minimale. Les candidats ICE sont échangés via des messages libp2p.
  • DCUtR (Hole Punching) — traversée NAT utilisant Relay.
  • Relais de secours — relais de trafic via un nœud serveur en cas d'échec de connexion directe.

Le code de gestion du flux audio montre un format de paquet personnalisé :

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
}

La réception de données utilise une boucle pour détecter les octets de synchronisation et traiter les trames Opus.

Chiffrement et sécurité : Double Ratchet en action

Pour les chats privés, le protocole Double Ratchet fournit une secret parfait de transmission en avant et une sécurité post-compromission. Fonctionnalités clés :

  • Chaque message utilise une clé unique
  • Rotation périodique des clés
  • Protection contre la compromission à long terme des clés

Le système assure un chiffrement de bout en bout sans que les clés passent par des nœuds intermédiaires. Toutes les opérations crypto s'exécutent sur l'appareil de l'utilisateur, évitant les fuites même sur les nœuds relais.

Gestion des scénarios hors ligne sans compromettre la confidentialité

Les notifications push servent uniquement de déclencheurs de réveil de l'application. Important :

  • Les messages push ne contiennent aucune donnée de message
  • Aucune info sur l'expéditeur
  • Le déclencheur ne contient qu'un drapeau de service

Flux de travail :

  • Notification push silencieuse arrive
  • Le système d'exploitation active brièvement l'application
  • Le nœud Go se connecte et récupère les paquets chiffrés
  • Le déchiffrement se fait localement
  • Une notification locale est générée pour l'utilisateur

Cela préserve le E2EE même avec des services push centralisés.

Points clés

  • Le pont FFI exige une gestion stricte de la mémoire : des fuites surviennent si FreeString est omis
  • Circuit Relay v2 est crucial pour la traversée NAT mais ajoute 15-20 % de latence
  • Double Ratchet fournit PFS mais nécessite une synchronisation d'état entre appareils
  • Les déclencheurs push ne compromettent pas la confidentialité car ils ne transportent aucune charge utile
  • Les builds iOS nécessitent une fusion manuelle d'archives pour simulateur et appareil

— Editorial Team

Advertisement 728x90

Lire ensuite