# Cherry Studio LAN Transfer Protocol Specification > Version: 1.0 > Last Updated: 2025-12 This document defines the LAN file transfer protocol between the Cherry Studio desktop client (Electron) and mobile client (Expo). --- ## Table of Contents 1. [Protocol Overview](#1-protocol-overview) 2. [Service Discovery (Bonjour/mDNS)](#2-service-discovery-bonjourmdns) 3. [TCP Connection and Handshake](#3-tcp-connection-and-handshake) 4. [Message Format Specification](#4-message-format-specification) 5. [File Transfer Protocol](#5-file-transfer-protocol) 6. [Heartbeat and Keep-alive](#6-heartbeat-and-keep-alive) 7. [Error Handling](#7-error-handling) 8. [Constants and Configuration](#8-constants-and-configuration) 9. [Complete Sequence Diagram](#9-complete-sequence-diagram) 10. [Mobile Implementation Guide](#10-mobile-implementation-guide) --- ## 1. Protocol Overview ### 1.1 Architecture Roles | Role | Platform | Responsibility | |------|----------|---------------| | **Client** | Electron Desktop | Scan services, initiate connections, send files | | **Server** | Expo Mobile | Publish services, accept connections, receive files | ### 1.2 Protocol Stack (v1) ``` ┌─────────────────────────────────────┐ │ Application Layer (File Transfer)│ ├─────────────────────────────────────┤ │ Message Layer (Control: JSON \n) │ │ (Data: Binary Frame)│ ├─────────────────────────────────────┤ │ Transport Layer (TCP) │ ├─────────────────────────────────────┤ │ Discovery Layer (Bonjour/mDNS) │ └─────────────────────────────────────┘ ``` ### 1.3 Communication Flow Overview ``` 1. Service Discovery → Mobile publishes mDNS service, Desktop scans and discovers 2. TCP Handshake → Establish connection, exchange device info (version=1) 3. File Transfer → Control messages use JSON, file_chunk uses binary frame chunked transfer 4. Keep-alive → ping/pong heartbeat ``` --- ## 2. Service Discovery (Bonjour/mDNS) ### 2.1 Service Type | Property | Value | |----------|-------| | Service Type | `cherrystudio` | | Protocol | `tcp` | | Full Service ID | `_cherrystudio._tcp` | ### 2.2 Service Publishing (Mobile) Mobile must publish the service via mDNS/Bonjour: ```typescript { name: "Cherry Studio Mobile", type: "cherrystudio", protocol: "tcp", port: 53317, txt: { version: "1", platform: "ios" // or "android" } } ``` ### 2.3 Service Discovery (Desktop) Desktop scans and resolves service information: ```typescript type LanTransferPeer = { id: string; name: string; host?: string; fqdn?: string; port?: number; type?: string; protocol?: 'tcp' | 'udp'; addresses: string[]; txt?: Record; updatedAt: number; } ``` ### 2.4 IP Address Selection Strategy When a service has multiple IP addresses, prefer IPv4: ```typescript const preferredAddress = addresses.find((addr) => isIPv4(addr)) || addresses[0] ``` --- ## 3. TCP Connection and Handshake ### 3.1 Connection Establishment 1. Client establishes TCP connection using the discovered `host:port` 2. Immediately sends a handshake message upon connection 3. Waits for server handshake acknowledgment ### 3.2 Handshake Messages (Protocol Version v1) #### Client → Server: `handshake` ```typescript type LanTransferHandshakeMessage = { type: 'handshake'; deviceName: string; version: string; // Protocol version, currently "1" platform?: string; // 'darwin' | 'win32' | 'linux' appVersion?: string; } ``` --- ## 4. Message Format Specification (Mixed Protocol) v1 uses a "control JSON + binary data frame" mixed protocol (streaming mode, no per-chunk ACK): - **Control messages** (handshake, heartbeat, file_start/ack, file_end, file_complete): UTF-8 JSON, `\n` delimited - **Data messages** (`file_chunk`): Binary frames using Magic + total length for framing, no Base64 ### 4.1 Control Message Encoding (JSON + `\n`) | Property | Specification | |----------|--------------| | Encoding | UTF-8 | | Serialization | JSON | | Message Delimiter | `\n` (0x0A) | ### 4.2 `file_chunk` Binary Frame Format To solve TCP packet splitting/merging and eliminate Base64 overhead, `file_chunk` uses binary frames with total length: ``` ┌──────────┬──────────┬────────┬───────────────┬──────────────┬────────────┬───────────┐ │ Magic │ TotalLen │ Type │ TransferId Len│ TransferId │ ChunkIdx │ Data │ │ 0x43 0x53│ (4B BE) │ 0x01 │ (2B BE) │ (UTF-8) │ (4B BE) │ (raw) │ └──────────┴──────────┴────────┴───────────────┴──────────────┴────────────┴───────────┘ ``` | Field | Size | Description | |-------|------|-------------| | Magic | 2B | Constant `0x43 0x53` ("CS"), distinguishes from JSON messages | | TotalLen | 4B | Big-endian, total frame length (excluding Magic/TotalLen) | | Type | 1B | `0x01` for `file_chunk` | | TransferId Len | 2B | Big-endian, transferId string length | | TransferId | nB | UTF-8 transferId (length from previous field) | | ChunkIdx | 4B | Big-endian, chunk index starting from 0 | | Data | mB | Raw file binary data (unencoded) | > Total frame length calculation: `TotalLen = 1 + 2 + transferIdLen + 4 + dataLen` ### 4.3 Message Parsing Strategy 1. Read socket data into buffer 2. If first two bytes are `0x43 0x53` → parse as binary frame 3. Else if first byte is `{` → parse as JSON + `\n` control message 4. Otherwise discard 1 byte and continue loop ### 4.4 Message Type Summary (v1) | Type | Direction | Encoding | Purpose | |------|-----------|----------|---------| | `handshake` | Client → Server | JSON+\n | Handshake request (version=1) | | `handshake_ack` | Server → Client | JSON+\n | Handshake response | | `ping` | Client → Server | JSON+\n | Heartbeat request | | `pong` | Server → Client | JSON+\n | Heartbeat response | | `file_start` | Client → Server | JSON+\n | Start file transfer | | `file_start_ack` | Server → Client | JSON+\n | File transfer acknowledgment | | `file_chunk` | Client → Server | Binary | File data chunk (no Base64, streaming, no per-chunk ACK) | | `file_end` | Client → Server | JSON+\n | File transfer end | | `file_complete` | Server → Client | JSON+\n | Transfer completion result | --- ## 5. File Transfer Protocol ### 5.1 Transfer Flow ``` Client (Sender) Server (Receiver) | | |──── 1. file_start ────────────────>| | | |<─── 2. file_start_ack ─────────────| | | |══════ Loop: send data chunks ══════| | | |──── 3. file_chunk [0] ────────────>| |──── 3. file_chunk [1] ────────────>| | ... repeat until all sent ... | | | |──── 5. file_end ──────────────────>| | | |<─── 6. file_complete ──────────────| ``` ### 5.2 Message Definitions #### 5.2.1 `file_start` ```typescript type LanTransferFileStartMessage = { type: 'file_start'; transferId: string; // UUID, unique transfer identifier fileName: string; fileSize: number; mimeType: string; checksum: string; // SHA-256 hash of entire file (hex) totalChunks: number; chunkSize: number; } ``` #### 5.2.2 `file_start_ack` ```typescript type LanTransferFileStartAckMessage = { type: 'file_start_ack'; transferId: string; accepted: boolean; message?: string; // Rejection reason } ``` #### 5.2.3 `file_chunk` — Binary Frame See section 4.2 for frame format. `Data` is raw file binary data. Integrity relies on `file_start.checksum` (full file SHA-256). #### 5.2.4 `file_end` ```typescript type LanTransferFileEndMessage = { type: 'file_end'; transferId: string; } ``` #### 5.2.5 `file_complete` ```typescript type LanTransferFileCompleteMessage = { type: 'file_complete'; transferId: string; success: boolean; filePath?: string; // Save path (on success) error?: string; // Error message (on failure) } ``` ### 5.3 Checksum ```typescript async function calculateFileChecksum(filePath: string): Promise { const hash = crypto.createHash('sha256') const stream = fs.createReadStream(filePath) for await (const chunk of stream) { hash.update(chunk) } return hash.digest('hex') } ``` ### 5.4 Chunk Size ```typescript const CHUNK_SIZE = 512 * 1024 // 512KB const totalChunks = Math.ceil(fileSize / CHUNK_SIZE) ``` --- ## 6. Heartbeat and Keep-alive ### 6.1 Messages - **`ping`** (Client → Server): `{ type: 'ping', payload?: string }` - **`pong`** (Server → Client): `{ type: 'pong', received: boolean, payload?: string }` ### 6.2 Strategy - Send `ping` immediately after successful handshake to verify connection - Optional: periodically send heartbeats to keep the connection alive --- ## 7. Error Handling ### 7.1 Timeout Configuration | Operation | Timeout | Description | |-----------|---------|-------------| | TCP Connection | 10s | Connection establishment timeout | | Handshake | 10s | Waiting for `handshake_ack` | | Transfer Complete | 60s | Waiting for `file_complete` | ### 7.2 Error Scenarios | Scenario | Client Handling | Server Handling | |----------|----------------|-----------------| | TCP connection failure | Notify UI, allow retry | - | | Handshake timeout | Disconnect, notify UI | Close socket | | Handshake rejected | Show rejection reason | - | | Chunk processing failure | Abort transfer, cleanup | Clean up temp files | | Unexpected disconnect | Cleanup state, notify UI | Clean up temp files | | Insufficient storage | - | Send `accepted: false` | --- ## 8. Constants and Configuration ```typescript export const LAN_TRANSFER_PROTOCOL_VERSION = '1' export const LAN_TRANSFER_SERVICE_TYPE = 'cherrystudio' export const LAN_TRANSFER_SERVICE_FULL_NAME = '_cherrystudio._tcp' export const LAN_TRANSFER_TCP_PORT = 53317 export const LAN_TRANSFER_CHUNK_SIZE = 512 * 1024 // 512KB export const LAN_TRANSFER_GLOBAL_TIMEOUT_MS = 10 * 60 * 1000 // 10 minutes export const LAN_TRANSFER_HANDSHAKE_TIMEOUT_MS = 10_000 export const LAN_TRANSFER_CHUNK_TIMEOUT_MS = 30_000 export const LAN_TRANSFER_COMPLETE_TIMEOUT_MS = 60_000 export const LAN_TRANSFER_ALLOWED_EXTENSIONS = ['.zip'] export const LAN_TRANSFER_ALLOWED_MIME_TYPES = ['application/zip', 'application/x-zip-compressed'] ``` --- ## 9. Complete Sequence Diagram ``` ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Renderer│ │ Main │ │ Mobile │ │ (UI) │ │ Process │ │ Server │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ ═══════ Service Discovery ═════════ │ │ startScan() │ │ │────────────────────────────────────>│ mDNS browse │ │ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─>│ │ │<─ ─ ─ service discovered ─ ─ ─ ─ ─ ─│ │<────── onServicesUpdated ───────────│ │ │ │ │ │ ═══════ Handshake ════════════════ │ │ connect(peer) │ │ │────────────────────────────────────>│──────── TCP Connect ───────────────>│ │ │──────── handshake ─────────────────>│ │ │<─────── handshake_ack ──────────────│ │ │──────── ping ──────────────────────>│ │ │<─────── pong ───────────────────────│ │<────── connect result ──────────────│ │ │ │ │ │ ═══════ File Transfer ════════════ │ │ sendFile(path) │ │ │────────────────────────────────────>│──────── file_start ────────────────>│ │ │<─────── file_start_ack ─────────────│ │ │──────── file_chunk[0] (binary) ────>│ │<────── progress event ──────────────│ │ │ │──────── file_chunk[1] (binary) ────>│ │<────── progress event ──────────────│ ... repeat ... │ │ │──────── file_end ──────────────────>│ │ │<─────── file_complete ──────────────│ │<────── complete event ──────────────│ │ ``` --- ## 10. Mobile Implementation Guide (v1) ### 10.1 Required Features 1. **mDNS Service Publishing**: Publish `_cherrystudio._tcp` service on TCP port `53317` 2. **TCP Server**: Listen on the specified port 3. **Message Parsing**: Control messages via UTF-8 + `\n` JSON; data messages via binary frames (Magic+TotalLen framing) 4. **Handshake Handling**: Validate `handshake`, send `handshake_ack`, respond to `ping` 5. **File Receiving (Streaming)**: Parse `file_start`, receive `file_chunk` binary frames (write to file + incremental hash), process `file_end`, send `file_complete` ### 10.2 Recommended Libraries **React Native / Expo:** - mDNS: `react-native-zeroconf` or `@homielab/react-native-bonjour` - TCP: `react-native-tcp-socket` - Crypto: `expo-crypto` or `react-native-quick-crypto` --- ## Appendix A: TypeScript Type Definitions Complete type definitions are located in `src/shared/types/lanTransfer.ts`. See the source code for the full interface definitions. ## Appendix B: Version History | Version | Date | Changes | |---------|------|---------| | 1.0 | 2025-12 | Initial release with binary frame format and streaming transfer |