KEIBIDROP Architecture: From Raw TCP to Encrypted Virtual Filesystem

End-to-end encrypted, peer-to-peer file sharing with post-quantum cryptography.

18 min read

High-Level Architecture

Rust / Slint
Desktop GUI
SwiftUI
iOS / iPad
Jetpack Compose
Android
FFI (rustbridge) / gomobile bindings
Go Engine
pkg/logic/common · cmd/kd · mobile/mobile.go
Identity
4-tier keys
Encrypted envelope
Crypto
ML-KEM-1024
X25519 hybrid
Session
Dual-TCP handshake
Mutual verification
Discovery
mDNS / Bonjour
UDP multicast
SecureConn
AEAD encryption (AES-GCM / ChaCha20)
[4B len | 12B nonce | ct + 16B tag]
gRPC over SecureConn
Notify, BatchNotify, StreamFile
Heartbeat, Rekey
Direct P2P
IPv6 TCP
LAN or WAN
Relay
HTTP key-value store
Encrypted blobs, 10min TTL
Bridge
TCP proxy
io.Copy, direction-tagged
FUSE Virtual FS
macFUSE / FUSE3 / WinFSP
Syscall handlers, bitmap cache
No-FUSE Mode
AddFile / PullFile API
SyncTracker, mobile
Health Monitor
Heartbeat 5s, RTT tracking
Degraded / Disconnected states
Reconnect Manager
Exponential backoff
Relay refresh, bridge fallback

Identity Management

Hierarchical key management with platform-native secure storage. Master keys are 32 bytes sourced from the most secure available backend, with automatic fallback through four tiers.

Tier 1 iOS Keychain (SecItem) / Android Keystore
fallback
Tier 1a OS Keychain (macOS Keychain Services / libsecret / DPAPI)
fallback
Tier 1b File: ~/.config/keibidrop/.master.key (0600)
fallback
Tier 2 Passphrase: Argon2id(passphrase, salt) -> 32B key
Envelope Format

The encrypted identity file uses a fixed-format envelope:

magic4B
format1B
kdf1B
flags1B
params1B
salt16B
nonce12B
ciphertext + tagvar + 16B

Key derivation: MasterKey + Salt via HKDF-SHA512 produces a per-file key. Encryption uses ChaCha20-Poly1305.

Crypto Layer

Hybrid post-quantum key exchange combining X25519 (classical ECDH) with ML-KEM-768 (lattice-based KEM). Both must be broken to compromise the session key.

  Hybrid KEM Key Exchange
  ========================

  Seed1 = X25519Encapsulate(ownPriv, peerPub)    --> encSeed1
  Seed2 = MLKEMEncapsulate(peerMLKEMPub)         --> encSeed2

  SessionKey = HKDF-SHA512(seed1 || seed2, "keibidrop-session-v1")
             = 32 bytes (AES-256 / ChaCha20 key)
      
SecureConn Wire Format
  [4B big-endian length] [12B nonce] [ciphertext + 16B AEAD tag]
                          |
                          +-- 4B prefix + 8B counter
                              OUTBOUND: 0x4F555442 ("OUTB")
                              INBOUND:  0x494E4244 ("INBD")
          

Nonce prefixes prevent nonce reuse on bidirectional streams sharing the same key.

Cipher Negotiation
CipherSelection CriteriaPerformance
ChaCha20-Poly1305Universal (all platforms)Fast on ARM / no AES-NI
AES-256-GCMHardware AES detectedFast on x86 with AES-NI

Session & Handshake

Zero-trust authenticated key exchange. Both peers verify each other's fingerprint (exchanged out-of-band) and derive a shared session key using the hybrid KEM.

  Full duplex: TWO TCP connections, each with its own handshake.
  Both peers verify each other's fingerprints.

  CONNECTION 1: Alice outbound -> Bob inbound

  Alice                                              Bob
  -----                                              ---
  TCP SYN ----------------------------------------->  Accept
                                                      |
  Send handshake:                                     |
  [4B len | JSON] --------------------------------->  |
    {fingerprint: Bob's expected FP,                  |
     public_keys: {x25519: Alice_pub, mlkem: Alice_pub},
     enc_seeds: {x25519: encSeed1, mlkem: encSeed2},  |
     outboundPort, supportedCiphers[], persistent}    |
                                                      v
                                              Verify Alice's fingerprint
                                              (constant-time compare vs registered)
                                              X25519Decapsulate(encSeed1) -> seed1
                                              MLKEMDecapsulate(encSeed2) -> seed2
                                              DeriveKey(seed1+seed2) -> SEK_inbound
                                              Negotiate cipher (AES-GCM or ChaCha20)
  SecureConn wraps Connection 1 (Alice writes, Bob reads)

  CONNECTION 2: Bob outbound -> Alice inbound

  Alice                                              Bob
  -----                                              ---
  Accept  <-----------------------------------------  TCP SYN
  |
  |                                                   Send handshake:
  |  <--------------------------------------------- [4B len | JSON]
  |                                                    {fingerprint: Alice's expected FP,
  |                                                     public_keys: {x25519: Bob_pub, mlkem: Bob_pub},
  |                                                     enc_seeds: {x25519: encSeed3, mlkem: encSeed4},
  v                                                     outboundPort, supportedCiphers[], persistent}
  Verify Bob's fingerprint
  (constant-time compare vs registered)
  X25519Decapsulate(encSeed3) -> seed3
  MLKEMDecapsulate(encSeed4) -> seed4
  DeriveKey(seed3+seed4) -> SEK_outbound
  Negotiate cipher

  SecureConn wraps Connection 2 (Bob writes, Alice reads)

  RESULT: Two independent encrypted channels

  Alice -> Bob: SecureConn(conn1, SEK_inbound,  nonce_prefix=OUTB)
  Bob -> Alice: SecureConn(conn2, SEK_outbound, nonce_prefix=INBD)

  Each direction has:
    - Independent symmetric key (from different KEM seeds)
    - Independent nonce counter (prefix prevents collision)
    - Independent rekey schedule (1GB or 1M messages)

  gRPC multiplexes on top of these two SecureConn streams.
      
PeerHandshakeMessage
{
  "fingerprint": "base64(86 chars)",
  "public_keys": {
    "x25519": "base64(32B)",
    "mlkem":  "base64(1184B)"
  },
  "enc_seeds": {
    "x25519": "base64(32B)",
    "mlkem":  "base64(1088B)"
  },
  "outboundPort": 26100,
  "supportedCiphers": ["chacha20-poly1305", "aes-256-gcm"],
  "persistent": true
}
          
Re-keying

Forward secrecy via periodic key rotation. Triggered at 1GB transferred or 1M messages (whichever first). New KEM seeds are exchanged mid-session without connection teardown.

Relay System

Privacy-preserving signaling server. The relay stores opaque encrypted blobs keyed by irreversible tokens. It cannot correlate users, reverse tokens, or decrypt connection metadata.

  Fingerprint (86 chars)
       |
       +-- ExtractRoomPassword (first 32 bytes)
       |        |
       |        +-- HKDF "keibidrop-relay-lookup-v1"   --> Lookup Token
       |        |
       |        +-- HKDF "keibidrop-relay-encrypt-v1"  --> Encryption Key
       |
  What the relay sees:
    POST /register  { token: "abc...", blob: "encrypted..." }
    GET  /fetch     { token: "abc..." } --> "encrypted..."

  What the relay CANNOT do:
    - Reverse token back to fingerprint
    - Decrypt the blob (contains IP + port + pubkeys)
    - Correlate which users are communicating
      
Connection Flow
  1. Alice registers:
     POST /register { Authorization: Bearer <lookup_token> }
     Body: Encrypt(IP + port + pubkeys, encryption_key)

  2. Bob fetches (using Alice's fingerprint):
     GET /fetch { Authorization: Bearer <lookup_token> }
     Response: encrypted blob

  3. Bob decrypts: IP + port + pubkeys
     Bob connects directly to Alice via TCP

  TTL: 10 minutes, in-memory only (no persistence)
          

Source Files

utils.go asymmetric.go

Bridge Relay

TCP relay for peers behind symmetric NATs or firewalls. The bridge pairs connections by token and performs blind byte forwarding without access to the encrypted stream content.

  Creator                    Bridge                     Joiner
  =======                    ======                     ======

  dial(token+"pair1") ------>  queue["pair1"]
                                    |
                               <----+---- dial(token+"pair1")
                             PAIRED: io.Copy(creator, joiner)

  dial(token+"pair2") ------>  queue["pair2"]
                                    |
                               <----+---- dial(token+"pair2")
                             PAIRED: io.Copy(creator, joiner)

  Token = SHA256(sorted(fp1, fp2) + direction)
  Direction tagging prevents self-pairing.
      
Token Generation

Bridge tokens are deterministic: both peers independently compute the same token from their sorted fingerprints and a direction tag. This enables rendezvous without prior coordination.

  token = SHA256(sort(fingerprint_a, fingerprint_b) + "pair1")
  // "pair1" = inbound connection direction
  // "pair2" = outbound connection direction
          

FUSE Filesystem

Virtual filesystem that presents peer files as local directories. Applications interact via standard POSIX syscalls; KEIBIDROP transparently fetches file data over gRPC on demand.

  Application (e.g., git clone, vim, Finder)
       | syscall: open(), read(), write(), rename()
  +----v---------+
  |    Kernel    |
  |   VFS/FUSE  |
  +----+---------+
       | FUSE protocol (via /dev/fuse)
  +----v--------------+
  | KeibiDrop FUSE    |
  | fuse_directory.go |
  | Getattr/Read/     |
  | Write/Rename      |
  +----+--------------+
       | gRPC: StreamFile, Notify
  +----v---------+
  |     Peer     |
  +--------------+
      
Key Data Structures
StructureTypePurpose
AllDirMapmap[string]*DirPath to directory node
AllFileMapmap[string]*FilePath to file metadata + data
RemoteFilesmap[string]*FileFiles available from peer
OpenFileHandlersmap[uint64]*HandleEntryOpen file handle state

Path convention: FUSE uses /filename (leading slash), no-FUSE uses filename (bare).

Rename Flow (Git Atomic Write)
  1. git writes:    .git/objects/pack/tmp_pack_XXXX.lock
  2. FUSE Write:    data buffered, Notify ADD_FILE to peer
  3. git renames:   tmp_pack_XXXX.lock -> pack-XXXX.idx
  4. FUSE Rename:   update maps, Notify RENAME_FILE to peer
  5. Peer:          os.Rename on disk, update RemoteFiles map
  6. Size check:    if final size != cached size, re-download
          
Platform Support
PlatformFUSE BackendMount Options
macOSmacFUSE (osxfuse)noappledouble, defer_permissions, iosize=524288
LinuxFUSE3 (libfuse3)allow_other, default_permissions
WindowsWinFSP--VolumePrefix=\\keibidrop
iOS/AndroidN/A (no-FUSE only)-

FUSE Deep Dive: From Minimal FS to Production

Evolution: Minimal FUSE to Production-Grade
LEVEL 1: Minimal FUSE (works but breaks everything)
  Getattr -> return hardcoded size/mode
  Read    -> fetch entire file from peer, return bytes
  Write   -> write to local, notify peer
  Problems: mmap crashes (git), stale cache (sync), no resume, OOM on large files

LEVEL 2: Per-file direct_io + on-demand reads
  OpenEx  -> set direct_io=true (bypass kernel page cache for freshness)
            EXCEPT .git/ paths (git needs mmap for pack files)
  Read    -> on-demand: fetch only requested byte range via gRPC
  Problems: slow random reads, no caching, 612 concurrent prefetches crash

LEVEL 3: Bitmap-tracked cache + prefetch semaphore
  Read    -> check ChunkBitmap: cached? -> local read
            not cached? -> gRPC StreamFile -> write to local -> mark bitmap
  Prefetch semaphore: max 8 concurrent StreamFile streams
  Problems: rename races with git, fcopyfile on macOS, ENOENT caching

LEVEL 4: Production (current)
  + Debounced notifications (200ms per-path, batched to 64)
  + Rename retargets pending notifications
  + ENOENT cache disabled (negative_vncache removed)
  + Post-release writes buffered (macOS fcopyfile)
  + Fsync after Release handled (git's fsync race)
  + Mode bits preserved cross-peer
  + Monotonic handle IDs (fd reuse prevention)
  + Panic recovery -> EIO (never crash the mount)
          
What We Debounce and Why
NOTIFICATION DEBOUNCE (200ms per path)

  Problem: git clone creates 600+ files in seconds.
           Each file triggers: ADD_FILE notification -> gRPC -> peer.
           600 concurrent gRPC calls -> OOM, socket exhaustion.

  Solution: per-path debounce timer (200ms)

  Write file.txt t=0ms   -> start 200ms timer for "file.txt"
  Write file.txt t=50ms  -> reset timer (file still changing)
  Write file.txt t=100ms -> reset timer
  [200ms passes]         -> send ONE ADD_FILE notification with final size

  If RENAME arrives for a pending file:
    Cancel pending ADD_FILE for old path
    Retarget to new path
    Restart 200ms timer

BATCH NOTIFICATIONS (up to 64 per RPC)

  Problem: even after debounce, hundreds of individual RPCs are slow.
  Solution: BatchNotify RPC packs up to 64 notifications per call.
            Sequence number ensures ordering.

REMOVE DEBOUNCE (1000ms)

  Problem: git's atomic write pattern: write tmp -> rename tmp -> target.
           The rename triggers REMOVE_FILE for the old path.
           But the REMOVE arrives before the ADD for the new path.
           Peer deletes the file, then re-adds it -> flicker, data loss.

  Solution: buffer REMOVE for 1000ms. If ADD arrives for same path
            within that window, cancel the REMOVE.

PREFETCH SEMAPHORE (max 8 concurrent)

  Problem: git clone resolving deltas opens 600 pack files simultaneously.
           Each open triggers a prefetch (full file download).
           600 concurrent StreamFile streams -> server OOM.

  Solution: buffered channel semaphore (cap 8).
            9th prefetch blocks until one finishes.
          
Connection to First File Operation (Full Flow)
FROM LAUNCH TO FIRST FILE READ (Desktop FUSE mode):

  1. App Launch
     KD_Initialize -> NewKeibiDrop -> InitSession (ephemeral keys)
     EnablePersistentIdentity -> LoadOrCreate identity
     UI shows fingerprint

  2. Fingerprint Exchange
     User copies fingerprint -> sends via Signal/Telegram
     Peer pastes -> AddPeerFingerprint (stored in session)

  3. Connect
     Connect() -> fingerprint tiebreak -> creator/joiner
     Creator: registerRoomToRelay -> listen for peer (or bridge)
     Joiner:  getRoomFromRelay -> dial peer (or bridge)

  4. Handshake
     Outbound: send PeerHandshakeMessage (pubkeys, enc_seeds, cipher list)
     Inbound:  receive, verify fingerprint, decapsulate seeds, derive SEK
     Wrap both TCP connections in SecureConn

  5. gRPC Setup
     Start gRPC server on inbound SecureConn
     Connect gRPC client to outbound SecureConn
     No TLS (encryption handled by SecureConn layer below)

  6. FUSE Mount
     FS.Mount(mountPoint) -> cgofuse FileSystemHost
     Root Dir created (AllDirMap, AllFileMap = empty)
     FS.Root set -> KDSvc.FS = FS (gRPC service can access filesystem)
     Mount blocks (FUSE event loop) until Unmount

  7. Peer Sends File List
     Peer's AddFile/batch -> Notify(ADD_FILE) RPC arrives
     service.go processes: creates File in RemoteFiles map
     Sets: Name, RelativePath, Size, Mode bits

  8. User Opens Finder / ls
     Finder calls: readdir("/mnt/keibidrop/")
     FUSE Readdir handler: iterate AllFileMap + AllDirMap
     Returns directory listing with file names and sizes

  9. User Opens a File
     open("/mnt/keibidrop/report.pdf")
     FUSE Open -> allocate HandleEntry (monotonic ID)
     -> check shouldUseDirectIo (true for non-.git files)
     -> set handle.DirectIo = true

  10. First Read
      read(fd, buf, 65536, offset=0)
      FUSE Read -> check bitmap: chunk 0 cached? NO
      -> OpenStreamProvider() -> gRPC StreamFile(path)
      -> peer reads file from disk -> sends 512KB chunks
      -> we write to local cache file -> mark bitmap
      -> copy requested bytes to FUSE read buffer
      -> return to application

  11. Subsequent Reads
      bitmap says chunk cached -> local disk read -> no network
      Sub-millisecond response time
          
Cross-Platform FUSE Differences
macOS (macFUSE via cgofuse)
  Mount options: noappledouble, defer_permissions, iosize=524288
  Quirks:
    - fcopyfile: writes can arrive AFTER Release (post-close writes)
    - com.apple.quarantine xattr: return ENODATA or apps won't open files
    - negative_vncache: REMOVED (caches ENOENT, breaks async file arrival)
    - .DS_Store: filtered from file list
    - /var -> /private/var symlink: check both in waitForUnmount
  Stale mount cleanup: diskutil unmount force

Linux (FUSE3 via cgofuse)
  Mount options: allow_other, default_permissions
  Quirks:
    - user_allow_other required in /etc/fuse.conf
    - FUSE3 vs FUSE2: different header paths
    - inotify works normally (unlike macOS FSEvents)
  Stale mount cleanup: fusermount -u

Windows (WinFSP via cgofuse)
  Mount options: --VolumePrefix=\\keibidrop
  Quirks:
    - Drive letter mount: "K:." normalized to "K:"
    - No /dev/fuse: uses minifilter driver
    - Different permission model (no Unix mode bits)
    - Antivirus may scan every file read (performance impact)
  Stale mount cleanup: N/A (WinFSP handles it)

Mobile (no FUSE)
  iOS: no kernel module access, uses no-FUSE SyncTracker
  Android: no FUSE (Android's FUSE is for MediaStore, not user apps)
  Both use: AddFile/PullFile API via gomobile bindings
          
SyscallmacOS behaviorLinux behaviorWindows behavior
RenameAtomic within volumeAtomic within volumeMay fail across volumes
mmapWorks if direct_io offWorks if direct_io offWinFSP emulates
xattrquarantine, FinderInfouser.*, security.*N/A (NTFS streams)
fsyncMay race with ReleaseOrdered per POSIXFlushFileBuffers
FUSE Rename: The Full Syscall-to-gRPC Flow (Linux)
Linux kernel rename syscall -> FUSE -> gRPC -> peer

  1. Application
     os.Rename("/mnt/kd/.git/objects/pack/tmp.lock", "/mnt/kd/.git/objects/pack/pack-abc.idx")

  2. Kernel VFS
     sys_renameat2(AT_FDCWD, old_path, AT_FDCWD, new_path, 0)
     -> VFS resolves paths through FUSE superblock
     -> FUSE_RENAME request queued to /dev/fuse

  3. cgofuse dispatch
     Go runtime receives FUSE_RENAME from kernel
     Dispatches to Dir.Rename(oldpath, newpath) handler
     This runs on a FUSE worker goroutine (pool of ~12)

  4. KeibiDrop Rename handler (fuse_directory.go)
     func (d *Dir) Rename(oldpath string, newpath string) int {
       // Validate: no dot-dot, no crossing mount boundary
       checkPath(oldpath) -> ENAMETOOLONG check
       checkPath(newpath) -> ENAMETOOLONG check

       // Lock maps (prevent concurrent Getattr from reading stale data)
       d.AfmLock.Lock()

       // Rename on local disk FIRST (before map updates)
       os.Rename(d.LocalDownloadFolder+oldpath, d.LocalDownloadFolder+newpath)

       // Update in-memory maps
       if file, ok := d.AllFileMap[oldpath]; ok {
         delete(d.AllFileMap, oldpath)
         file.Name = newName
         file.RelativePath = newpath
         d.AllFileMap[newpath] = file
       }

       // Update RemoteFiles map (for incoming notifications)
       d.RemoteFilesLock.Lock()
       if rf, ok := d.RemoteFiles[oldpath]; ok {
         delete(d.RemoteFiles, oldpath)
         d.RemoteFiles[newpath] = rf
       }
       d.RemoteFilesLock.Unlock()

       d.AfmLock.Unlock()

       // Notify peer via gRPC (debounced)
       d.OnLocalChange(types.FileEvent{
         Type:    RENAME_FILE,
         OldPath: oldpath,
         NewPath: newpath,
         Attr:    stat,  // includes final size for size-mismatch detection
       })

       refreshDirStat(parentDir) // update ctime/mtime under lock
       return 0  // success
     }

  5. Notification pipeline
     OnLocalChange -> debounce timer (200ms)
     -> if RENAME, retarget any pending ADD_FILE for oldpath
     -> after debounce: enqueue to notifyCh (bounded channel, cap 64)
     -> notification worker drains channel:
         BatchNotify RPC -> peer's gRPC server

  6. Peer receives (service.go)
     BatchNotify handler processes RENAME_FILE:
     -> os.Rename(localCache+oldpath, localCache+newpath) on disk
     -> update SyncTracker/RemoteFiles maps
     -> if Attr.Size != cached file size: re-download (EditRemoteFile)

  7. Peer's FUSE reflects change
     Next Getattr/Readdir on peer returns updated name
     Application sees renamed file instantly
          
Same Rename Flow on macOS (differences highlighted)
macOS-specific differences in rename path:

  1. Kernel uses renamex_np() (macOS extension) instead of renameat2
     -> cgofuse translates to the same Rename callback

  2. Finder rename triggers ADDITIONAL operations:
     -> Getattr on new path (check if exists)
     -> exchangedata() syscall (macOS atomic swap, optional)
     -> setxattr com.apple.FinderInfo (Finder metadata)
     -> We return ENODATA for FinderInfo -> Finder continues normally

  3. fcopyfile race (macOS-only):
     cp uses fcopyfile() internally which can WRITE after close()
     -> Our Write handler checks: if handle == nil (already Released)
       -> buffer the post-release write
       -> flush on next Open of same path

  4. .DS_Store creation:
     Finder creates .DS_Store in every directory it visits
     -> Filtered from notifications (isInternalFile check)
     -> Never synced to peer

  5. Same notification pipeline as Linux after step 4
          

No-FUSE Mode

Explicit file sharing via AddFile/PullFile API. Used on mobile platforms (iOS/Android) and as fallback when FUSE is unavailable. Supports resumable downloads via chunk bitmaps.

  SENDER                                    RECEIVER
  ======                                    ========

  User picks file
       |
  AddFile(path)
       |
  SyncTracker.LocalFiles[name] = File
       |
  Notify(ADD_FILE) --------------------------> peer sees file in list
                                                    |
                                               PullFile(name)
                                                    |
                                          StreamFile RPC <-- server pushes chunks
                                                    |
                                          bitmap tracks 512KB chunks
                                                    |
                                          .kdbitmap file for resumption
                                                    |
                                          complete -> SyncTracker.LocalFiles
      
Chunk Bitmap

Files are divided into 512KB chunks. A ChunkBitmap tracks which chunks have been received. On interruption, the .kdbitmap file persists progress, enabling resumable transfers.

  ChunkBitmap {
    TotalChunks int
    Received    []bool   // per-chunk completion
    File        string   // .kdbitmap path on disk
  }

  Chunk size: 512KB (524288 bytes)
  StreamFile: server pushes all chunks sequentially (no per-chunk RTT)
          

Source Files

logic.go download.go

gRPC Service

Application-layer protocol running over SecureConn. All RPCs are encrypted end-to-end. The gRPC server runs inside the peer process, serving file operations and health signals.

RPCDirectionPurpose
NotifyunaryFile change event (ADD/REMOVE/RENAME/EDIT)
BatchNotifyunaryBatched notifications (up to 64 per call)
StreamFileserver streamPush-based file download (all chunks)
Readbidi streamPull-based parallel reads (random access)
HeartbeatunaryHealth monitoring (5s interval)
RekeyunaryForward secrecy key rotation
NotifyType Enum
TypeValueTrigger
ADD_FILE0New file shared
EDIT_FILE1File content modified
REMOVE_FILE2File deleted/unshared
RENAME_FILE3File renamed (carries old + new path)
ADD_DIR4Directory created
EDIT_DIR5Directory metadata changed
REMOVE_DIR6Directory removed
DISCONNECT7Graceful disconnect signal

Transport Stack: TCP to SecureConn to gRPC

How a raw TCP connection becomes an encrypted gRPC channel, and how data flows through it.

APPLICATION LAYER
+-------------------------------------------------------------------+
|  gRPC Service (keibidrop.proto)                                    |
|                                                                   |
|  Phase 1: Metadata Sync                 Phase 2: File Transfer     |
|  +-------------------------+              +------------------------+|
|  | Notify(ADD_FILE)        |              | StreamFile(path)       ||
|  | Notify(RENAME_FILE)     |              |  -> server pushes      ||
|  | Notify(REMOVE_FILE)     |              |    all chunks          ||
|  | BatchNotify(64 max)     |              |  -> bitmap tracks      ||
|  | Heartbeat(seq, ts)      |              |    progress            ||
|  | Rekey(new seeds)        |              |                        ||
|  +-------------------------+              | Read(bidi stream)      ||
|  Unary RPCs, small payloads               |  -> client requests    ||
|  Peer learns file list instantly          |    specific chunks     ||
|                                           |  -> parallel workers   ||
|                                           +------------------------+|
|                                           Streaming RPCs, large    |
|                                           payloads (512KB chunks)  |
+------------------------------+------------------------------------+
                               | protobuf serialization
ENCRYPTION LAYER
+------------------------------+------------------------------------+
|  SecureConn (pkg/session/secureconn.go)                             |
|                                                                   |
|  Write path:  plaintext -> AEAD.Seal(nonce, pt, nil)              |
|               -> [4B length | 12B nonce | ciphertext + 16B tag]   |
|                                                                   |
|  Read path:   [4B length] -> read(len) -> AEAD.Open -> plaintext  |
|                                                                   |
|  Nonce:  [4B direction prefix | 8B monotonic counter]             |
|          OUTB = 0x4F555442    INBD = 0x494E4244                   |
|                                                                   |
|  Cipher: AES-256-GCM (if hardware) or ChaCha20-Poly1305          |
|  Key:    SEK derived from hybrid KEM (X25519 + ML-KEM-1024)       |
|  Rekey:  every 1GB or 1M messages (forward secrecy)               |
+------------------------------+------------------------------------+
                               | encrypted bytes
TRANSPORT LAYER
+------------------------------+------------------------------------+
|  Raw TCP Connection                                                 |
|                                                                   |
|  Direct P2P:  tcp6 [local_ipv6]:26001 -> [peer_ipv6]:26003       |
|  Bridge:      tcp  [local] -> bridge.keibisoft.com:26600          |
|               (bridge does io.Copy, sees only encrypted bytes)    |
|  LAN:         tcp  192.168.x.x:26531 -> 192.168.x.x:26001       |
|                                                                   |
|  Two connections per session:                                     |
|    Conn 1: outbound direction (Alice -> Bob)                      |
|    Conn 2: inbound direction  (Bob -> Alice)                      |
|  Each wrapped independently in SecureConn with separate keys      |
+-------------------------------------------------------------------+
      

FUSE Syscall to Network Flow

User opens video file in VLC:

  VLC calls open("/mnt/keibidrop/video.mp4")
       |
  +----v------------------+
  | Kernel VFS -> FUSE    |  syscall dispatch
  +----+------------------+
       |
  +----v--------------------------------------------------------+
  | KeibiDrop FUSE handler: Open()                                |
  |  -> allocate handle (monotonic ID, prevents fd reuse)        |
  |  -> check if file is in AllFileMap                           |
  |  -> if remote: set direct_io (unless .git/ path)             |
  |  -> return handle ID to kernel                               |
  +----+--------------------------------------------------------+
       |
  VLC calls read(fd, buf, 65536, offset=0)
       |
  +----v--------------------------------------------------------+
  | KeibiDrop FUSE handler: Read()                                |
  |  -> check local cache (bitmap: chunk downloaded?)            |
  |  -> if cached: read from local disk, return                  |
  |  -> if not cached:                                           |
  |       |                                                      |
  |  +----v------------------------------------------+           |
  |  | gRPC: StreamFile(path, offset, size)           |           |
  |  |  -> peer opens file on their disk             |           |
  |  |  -> pushes 512KB chunks via gRPC stream       |           |
  |  |  -> each chunk: SecureConn encrypts           |           |
  |  |  -> TCP sends to us                           |           |
  |  |  -> we decrypt, write to local cache          |           |
  |  |  -> update bitmap (mark chunks done)          |           |
  |  |  -> return data to FUSE read buffer           |           |
  |  +-----------------------------------------------+           |
  +--------------------------------------------------------------+
       |
  VLC receives data, decodes video frame, displays

Subsequent reads: bitmap says cached -> local disk read -> no network
      

No-FUSE: Mobile File Transfer Flow

User taps "Save All" on iPhone:

  SaveAllFiles()
       |
  +----v--------------------------------------------------------+
  | For each file (sequential):                                  |
  |  1. Mark file as downloading in UI                           |
  |  2. SaveFile(name) -> ExportFile -> PullFile                 |
  |       |                                                      |
  |  +----v------------------------------------------+           |
  |  | PullFile(remoteName, localPath)                |           |
  |  |  -> Truncate local file to expected size      |           |
  |  |  -> Create ChunkBitmap (512KB granularity)    |           |
  |  |  -> HealthMonitor.TransferStarted()           |           |
  |  |  -> pullParallelRead():                       |           |
  |  |     4 worker goroutines                       |           |
  |  |     each: gRPC StreamFile -> decrypt          |           |
  |  |           -> pwrite at chunk offset           |           |
  |  |           -> bitmap.Set(chunkIndex)           |           |
  |  |  -> HealthMonitor.TransferEnded()             |           |
  |  |  -> delete .kdbitmap (download complete)      |           |
  |  +-----------------------------------------------+           |
  |  3. Update UI: downloading=false, saved=true                 |
  +--------------------------------------------------------------+

If interrupted (app killed, network drop):
  -> .kdbitmap preserved on disk
  -> next PullFile detects partial file + bitmap
  -> resumes from last completed chunk
      

Discovery

Dual-stack local network discovery combining custom UDP multicast beacons with standard mDNS/Bonjour service advertisement. Peers are discovered within seconds on the same subnet.

  +---------------------------+       +------------------------+
  |   UDP Multicast Beacon    |       |    mDNS / Bonjour      |
  |   224.0.0.167:26999       |       |    224.0.0.251:5353    |
  |   JSON: {name, port}     |       |    _keibidrop._tcp     |
  |   Every 3 seconds         |       |    PTR + SRV + A       |
  +---------------------------+       +------------------------+
           |                                    |
           | all platforms                      | macOS: CGo DNS-SD (native)
           |                                    | others: pure Go multicast
           |                                    | iOS: Swift NetService
           |                                    |        (advertise only)
           +----------------+-------------------+
                            |
                     Peer discovered:
                     name + IP + port
      
mDNS Implementation Details

The mDNS implementation handles both browsing (finding peers) and responding (advertising self). On macOS, browsing uses native DNS-SD via CGo for reliability with .local resolution. Other platforms use pure-Go multicast DNS.

  Service name: _keibidrop._tcp.local.

  Response records:
    PTR: _keibidrop._tcp.local. -> instance._keibidrop._tcp.local.
    SRV: instance._keibidrop._tcp.local. -> hostname:port
    A:   hostname.local. -> IPv4 address
    TXT: version, fingerprint-prefix (for UI display)
          

Health & Reconnection

Continuous connection monitoring with automatic recovery. The health monitor detects degradation via RTT measurement, and the reconnect manager handles exponential backoff recovery.

Healthy
Degraded
Disconnected
Reconnecting
Connected
  [Healthy] ---RTT > 500ms---> [Degraded] ---5 failures---> [Disconnected]
     ^                              |                              |
     |                              |                              v
     +------RTT < 500ms------------+                     [Reconnecting]
                                                          exponential backoff:
                                                          1s, 2s, 4s, 8s, 16s, 30s
                                                                   |
                                                              success
                                                                   |
                                                                   v
                                                            [Connected]
      
Configuration
ParameterValueDescription
HeartbeatInterval5sTime between heartbeat pings
HeartbeatTimeout5sMax wait for heartbeat response
MaxFailures5Consecutive failures before disconnect
DegradedThreshold500msRTT above this = degraded
Backoff sequence1, 2, 4, 8, 16, 30sExponential with 30s cap

Source Files

health.go reconnect.go

Platform Matrix

KEIBIDROP runs on five platforms with platform-native integrations. The Go engine is shared; platform-specific code handles secure storage, filesystem access, discovery, and UI rendering.

Component macOS Linux Windows iOS Android
Keychain Keychain Services libsecret (D-Bus) DPAPI Credential Manager iOS Keychain (SecItem) EncryptedSharedPreferences
FUSE macFUSE FUSE3 WinFSP N/A (no-FUSE) N/A (no-FUSE)
mDNS Browse CGo DNS-SD Pure Go multicast Pure Go multicast Pure Go multicast Pure Go multicast
mDNS Advertise Go mDNS responder Go mDNS responder Go mDNS responder Swift NetService Go mDNS responder
UI Rust / Slint Rust / Slint Rust / Slint SwiftUI Jetpack Compose
Build make build-rust make build-rust make build-rust make build-ios make build-android
Binary keibidrop keibidrop keibidrop.exe KeibiDrop.xcframework keibidrop.aar

UI Layers

Three separate UI frontends share one Go engine. Desktop uses Rust/Slint with C FFI. Mobile uses gomobile to generate platform-native bindings (xcframework for iOS, AAR for Android).

  +----------------------------------------------------+
  |                  UI Frontends                       |
  +-------------+----------------+---------------------+
  | Rust/Slint  |    SwiftUI     |   Jetpack Compose   |
  | (desktop)   |    (iOS)       |   (Android)         |
  +------+------+-------+--------+-----------+---------+
         |              |                     |
  +------+------+ +-----+------+  +----------+--------+
  | rustbridge  | |  gomobile  |  |      gomobile      |
  | (C FFI)     | | (xcframe)  |  |      (AAR)         |
  +------+------+ +-----+------+  +----------+--------+
         |              |                     |
  +------+--------------+---------------------+--------+
  |                    Go Engine                        |
  |               pkg/logic/common/                    |
  +----------------------------------------------------+
      
FFI Boundary (rustbridge)

The rustbridge package exports 80+ functions via CGo for the Rust/Slint desktop UI. Key exports include:

  // Identity
  KD_LoadOrCreateIdentity() -> error
  KD_GetFingerprint() -> string
  KD_AddContact(name, fingerprint) -> error

  // Connection
  KD_Connect(fingerprint) -> error
  KD_Disconnect() -> error
  KD_GetConnectionStatus() -> string

  // Files
  KD_AddFile(path) -> error
  KD_PullFile(name) -> error
  KD_ListFiles() -> JSON

  // Events
  KD_PollEvent() -> string  // non-blocking
  KD_SetupEventCallbacks() -> error
  KD_GetLastError() -> string
          

CLI

The kd command-line interface communicates with a background daemon over a Unix socket. Designed for scripting and AI agent integration with JSON input/output.

  User terminal
       |
  +----v------+         +---------------+
  |  kd CLI   |--JSON-->|  kd daemon    |
  |  (client) |<--JSON--|  (Go engine)  |
  +-----------+  Unix   +---------------+
              socket
            /tmp/kd.sock
      
Commands
CommandDescription
kd startStart daemon (background engine process)
kd stopStop daemon gracefully
kd show fingerprintPrint own fingerprint
kd show statusConnection state + peer info
kd show configCurrent configuration
kd registerRegister on relay for discovery
kd connect <fp>Connect to peer by fingerprint
kd add-file <path>Share a file with connected peer
kd pull-file <name>Download a file from peer
kd listList shared files (local + remote)

Source Files

cmd/kd/main.go

18 min read | KEIBIDROP Series | Technical Deep Dive | Post-Quantum gRPC | Relay Privacy | Forward Secrecy | Git Clone Between Peers | Agent CLI