Back to Home

Dart-Go FFI Integration for libp2p in Flutter — Technical Guide

The article describes the implementation of a decentralized messenger with end-to-end encryption through the integration of libp2p (Go) into a Flutter application. FFI interaction, NAT traversal, Double Ratchet security, and offline scenario handling are covered in detail. The solution eliminates central servers and preserves data privacy.

Complete Guide to Creating a Decentralized Messenger on Flutter and Go
Advertisement 728x90

# Building a Decentralized Messenger: Integrating libp2p into Flutter via Go

Today's messaging apps demand privacy and decentralization. This article breaks down the implementation of a P2P messenger with end-to-end encryption based on libp2p. The solution uses Go for networking logic, Flutter for the UI, and FFI for component interaction. The architecture eliminates central servers, ensuring security even when bypassing NAT and firewalls.

Architecture Basics: Flutter, Go, and FFI

The key challenge is integrating libp2p into a mobile app without compromising performance. Flutter handles the interface, while the networking logic is moved to a Go binary compiled into:

  • .dylib for macOS
  • .so for Android
  • Static library .a for iOS

Dart and Go interact via FFI (Foreign Function Interface) using C-compatible functions. This avoids serialization overhead and maintains native data exchange speeds. Memory management is crucial: string data passed from Go to Dart requires manual freeing via FreeString.

Google AdInline article slot

Building the Go Library for Mobile Platforms

The compilation process requires platform-specific settings. For Android, use 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

For 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

On Android, the library goes in jniLibs/arm64-v8a/. On iOS, it's linked via Xcode. For the simulator, build for x86_64 and merge archives using lipo -create.

Google AdInline article slot

FFI Bridge: Go and Dart Interaction

Exporting functions from Go requires strict rules:

  • All functions are marked with //export
  • Include an empty func main() {}
  • Memory management: C.CString() allocates in the C heap, which Dart must free via FreeString

Go export example:

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

In Dart, load the library and bind functions:

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

Calling the function involves string conversion and manual memory management:

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

Working with libp2p: Creating a Node and Exchanging Messages

Node initialization sets up transports and protocols:

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

Each device gets a unique PeerID—a hash of the public Ed25519 key. Message sending uses stream-based exchange:

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
}

Messages are delivered directly when online or via a relay node when offline. Encrypted data is stored temporarily until the recipient connects.

Ensuring Connectivity in Challenging Network Conditions

For voice calls, a three-tier transport system is implemented:

  • Direct UDP Transport (Pion ICE)—primary channel with minimal latency. ICE candidates are exchanged via libp2p messages.
  • DCUtR (Hole Punching)—NAT traversal using Relay.
  • Relay Fallback—traffic relay through a server node when direct connection fails.

Audio stream handling code shows custom packet format:

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
}

Receiving data uses a loop to detect sync bytes and process Opus frames.

Encryption and Security: Double Ratchet in Action

For private chats, the Double Ratchet protocol provides Perfect Forward Secrecy and Post-Compromise Security. Key features:

  • Each message uses a unique key
  • Periodic key rotation
  • Protection against long-term key compromise

The system ensures end-to-end encryption without keys passing through intermediate nodes. All crypto operations run on the user's device, preventing leaks even on relay nodes.

Handling Offline Scenarios Without Compromising Privacy

Push notifications serve only as app wake-up triggers. Importantly:

  • Push messages contain no message data
  • No sender info
  • Trigger holds only a service flag

Workflow:

  • Silent push notification arrives
  • OS briefly activates the app
  • Go node connects and fetches encrypted packets
  • Decryption happens locally
  • Local notification is generated for the user

This preserves E2EE even with centralized push services.

Key Points

  • FFI bridge demands strict memory management: leaks occur if FreeString is skipped
  • Circuit Relay v2 is critical for NAT traversal but adds 15-20% latency
  • Double Ratchet provides PFS but requires state sync across devices
  • Push triggers don't compromise privacy as they carry no payload
  • iOS builds need manual archive merging for simulator and device

— Editorial Team

Advertisement 728x90

Read Next