홈으로 돌아가기

Flutter에서 libp2p를 위한 Dart-Go FFI 통합 — 기술 가이드

이 기사는 libp2p (Go)를 Flutter 애플리케이션에 통합하여 종단간 암호화를 사용한 분산형 메신저 구현을 설명합니다. FFI 상호작용, NAT 순회, Double Ratchet 보안, 오프라인 시나리오 처리가 상세히 다뤄집니다. 이 솔루션은 중앙 서버를 제거하고 데이터 프라이버시를 보존합니다.

Flutter와 Go로 분산형 메신저 만드는 완전 가이드
Advertisement 728x90

분산형 메신저 구축: Go를 통해 Flutter에 libp2p 통합하기

요즘 메신저 앱은 프라이버시와 분산화를 요구합니다. 이 글에서는 libp2p를 기반으로 한 종단 간 암호화가 적용된 P2P 메신저 구현 과정을 자세히 분석합니다. 이 솔루션은 네트워킹 로직에 Go를, UI에 Flutter를, 컴포넌트 간 상호작용에 FFI를 사용합니다. 이 아키텍처는 중앙 서버를 제거하여 NAT와 방화벽을 우회하더라도 보안을 보장합니다.

아키텍처 기본: Flutter, Go, 그리고 FFI

주요 도전 과제는 성능 저하 없이 모바일 앱에 libp2p를 통합하는 것입니다. Flutter가 인터페이스를 담당하고, 네트워킹 로직은 다음으로 컴파일된 Go 바이너리로 이동합니다:

  • macOS용 .dylib
  • Android용 .so
  • iOS용 정적 라이브러리 .a

Dart와 Go는 C 호환 함수를 사용한 FFI(Foreign Function Interface)를 통해 상호작용합니다. 이를 통해 직렬화 오버헤드를 피하고 네이티브 데이터 교환 속도를 유지합니다. 메모리 관리가 핵심입니다: Go에서 Dart로 전달된 문자열 데이터는 FreeString을 통해 수동으로 해제해야 합니다.

Google AdInline article slot

모바일 플랫폼용 Go 라이브러리 빌드

컴파일 과정은 플랫폼별 설정이 필요합니다. Android의 경우 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

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

Android에서는 라이브러리를 jniLibs/arm64-v8a/에 배치합니다. iOS에서는 Xcode를 통해 링크합니다. 시뮬레이터용으로는 x86_64으로 빌드한 후 lipo -create를 사용해 아카이브를 병합합니다.

Google AdInline article slot

FFI 브리지: Go와 Dart 상호작용

Go에서 함수를 내보내기 위해 엄격한 규칙을 따라야 합니다:

  • 모든 함수에 //export 표시
  • func main() {} 포함
  • 메모리 관리: C.CString()은 C 힙에 할당되므로 Dart에서 FreeString으로 해제해야 함

Go 내보내기 예시:

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

Dart에서는 라이브러리를 로드하고 함수를 바인딩합니다:

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

함수 호출 시 문자열 변환과 수동 메모리 관리가 필요합니다:

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

libp2p 사용: 노드 생성과 메시지 교환

노드 초기화는 전송 계층과 프로토콜을 설정합니다:

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

각 기기는 공개 Ed25519 키의 해시인 고유한 PeerID를 가집니다. 메시지 전송은 스트림 기반 교환을 사용합니다:

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
}

메시지는 온라인일 때 직접 전달되거나 오프라인일 때 릴레이 노드를 통해 전달됩니다. 암호화된 데이터는 수신자가 연결될 때까지 임시 저장됩니다.

어려운 네트워크 환경에서의 연결성 보장

음성 통화의 경우 3단계 전송 시스템을 구현합니다:

  • 직접 UDP 전송 (Pion ICE)—최소 지연의 기본 채널. ICE 후보는 libp2p 메시지를 통해 교환됩니다.
  • DCUtR (Hole Punching)—릴레이를 사용한 NAT 순회.
  • 릴레이 대체—직접 연결 실패 시 서버 노드를 통한 트래픽 릴레이.

오디오 스트림 처리 코드는 사용자 정의 패킷 형식을 보여줍니다:

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
}

데이터 수신은 동기화 바이트를 감지하고 Opus 프레임을 처리하는 루프를 사용합니다.

암호화와 보안: Double Ratchet 작동

개인 채팅의 경우 Double Ratchet 프로토콜이 완벽한 순방향 비밀성(PFS)과 후손환 보안을 제공합니다. 주요 특징:

  • 각 메시지에 고유한 키 사용
  • 주기적인 키 로테이션
  • 장기 키 유출에 대한 보호

이 시스템은 중간 노드를 거치지 않고 키가 전달되지 않도록 종단 간 암호화를 보장합니다. 모든 암호 작업은 사용자 기기에서 실행되어 릴레이 노드에서도 유출을 방지합니다.

프라이버시를 손상시키지 않고 오프라인 시나리오 처리

푸시 알림은 앱 깨우기 트리거로만 사용됩니다. 중요한 점:

  • 푸시 메시지에 메시지 데이터 없음
  • 발신자 정보 없음
  • 트리거에 서비스 플래그만 포함

작업 흐름:

  • 무음 푸시 알림 도착
  • OS가 앱을 잠시 활성화
  • Go 노드가 연결되어 암호화된 패킷 가져옴
  • 로컬에서 복호화
  • 사용자에게 로컬 알림 생성

이로써 중앙화된 푸시 서비스를 사용하더라도 E2EE를 유지합니다.

주요 포인트

  • FFI 브리지는 엄격한 메모리 관리를 요구: FreeString을 생략하면 메모리 누수 발생
  • Circuit Relay v2는 NAT 순회에 필수적이지만 15-20% 지연 추가
  • Double Ratchet은 PFS를 제공하지만 기기 간 상태 동기화 필요
  • 푸시 트리거는 페이로드가 없어 프라이버시 손상 없음
  • iOS 빌드는 시뮬레이터와 디바이스용 수동 아카이브 병합 필요

— Editorial Team

Advertisement 728x90

다음 읽기