# Synology Drive Sync Protocol (Sproto) — Reverse Engineering Specification
> **Source**: Reverse-engineered from `cloud-drive-daemon` (ARM64, macOS, version 4.0 build 17889)
> **Binary path**: `~/Library/Application Support/SynologyDrive/SynologyDrive.app/Contents/MacOS/cloud-drive-daemon`
> **Source files referenced in debug strings**: `//Users/fct/synosrc/dog-builder-4.0/source/synosyncfolder/lib/protocol/proto-common.cpp`
---
## 1. Transport Layer
### 1.1 Connection
The protocol operates over **TCP port 6690** with optional TLS upgrade. The connection sequence is:
1. Client opens a raw TCP connection to `<server>:6690`
2. Client optionally negotiates TLS (see §3)
3. Client authenticates (see §4)
4. Client enters the request/response loop
### 1.2 Two Protocol Layers
The binary implements **two distinct serialization formats** that share the same TCP connection:
| **Legacy** | Section-based (SCMD) | `0x25521814` | Auth, events, keepalive, SSL upgrade, legacy sync | Tag-Value sections with SPROTO_ID tags |
| **Modern** | PObject-based (CloudStation) | `0x25521814` | File operations (download, upload, share, settings) | Recursive map/array/string/int serialization via PStream |
Both layers share the same 8-byte wire header (magic `0x25521814`) but differ in payload encoding. The legacy layer uses flat Tag-Value sections, while the modern layer uses PStream-serialized PObject trees. The `PROTO_VERSION` byte is `0x46` (70 decimal) for the current protocol version.
---
## 2. Legacy Protocol Layer (Section-Based)
### 2.1 Wire Header
Every message starts with an **8-byte header**:
```
Offset Size Type Field Description
0 4 uint32 magic Always 0x25521814 (big-endian: bytes 25 52 18 14)
4 1 uint8 version Protocol version (0x46 = 70 decimal)
5 1 uint8 command SCMD_xxx command type (see §2.2)
6 2 uint16 pkt_len Payload length, big-endian (often 0; actual length determined by payload)
```
**Reading** (`ProtoReadHeader`):
```c
int ProtoReadHeader(Channel& chan, uint16_t& pkt_len, uint8_t& version, uint8_t& cmd) {
chan.SetTimeout(10);
int32_t magic;
chan.ReadInt32(&magic);
chan.ReadUInt8(&version);
chan.ReadUInt8(&cmd);
chan.ReadUInt16(&pkt_len);
if (magic != 0x25521814) return -5; // invalid protocol
return 0;
}
```
**Writing** (`ProtoWriteHeader`):
```c
int ProtoWriteHeader(Channel& chan, uint16_t pkt_len, uint8_t cmd) {
chan.WriteInt32(0x25521814); // magic
chan.WriteUInt8(PROTO_VERSION); // version
chan.WriteUInt8(cmd); // command
chan.WriteUInt16(pkt_len); // payload length
return 0;
}
```
### 2.2 Command Types (SCMD)
| 0x00 | (Unknown) | — | Invalid/default |
| 0x01 | SCMD_REQUEST | C→S | Generic request wrapper |
| 0x02 | SCMD_KEEP_ALIVE | C↔S | Connection keepalive |
| 0x03 | SCMD_AUTH | C→S | Authentication (v1, legacy) |
| 0x04 | SCMD_NOTIFY | S→C | Server push notification |
| 0x05 | SCMD_LOGOUT | C→S | Disconnect/logout |
| 0x06 | SCMD_EVT_PULL | C→S | Pull sync events from server |
| 0x07 | SCMD_EVT_RECV | S→C | Receive sync events (v1) |
| 0x08 | SCMD_NEW_EVT_INFO | S→C | New event notification |
| 0x09 | SCMD_EVT_SEND | C→S | Send sync events to server |
| 0x0A | SCMD_USER_CTRL | C→S | User control command |
| 0x0B | SCMD_TEST_CONN | C→S | Connection test |
| 0x0C | SCMD_RESPONSE | S→C | Generic response |
| 0x0D | SCMD_USER_CHECK | C→S | Verify user credentials |
| 0x0E | SCMD_REQUEST_CONNECT | C→S | Request new connection |
| 0x0F | SCMD_EVT_REMOVE | C→S | Remove/acknowledge events |
| 0x10 | SCMD_GET_FILTER | C→S | Get file filter rules |
| 0x11 | SCMD_LIST_VIEW | C→S | List shared folders/views |
| 0x12 | SCMD_AUTH_2 | C→S | Authentication (v2, current) |
| 0x13 | SCMD_EVT_RECV_2 | S→C | Receive sync events (v2) |
| 0x14 | SCMD_REQUEST_SSL | C→S | Request SSL upgrade (v1) |
| 0x15 | SCMD_EVT_RECV_3 | S→C | Receive sync events (v3) |
| 0x16 | SCMD_REQUEST_SSL_2 | C→S | Request SSL upgrade (v2, current) |
| 0x17 | SCMD_PKG_VER_QUERY | C→S | Query server package version |
| 0x18 | SCMD_GET_FOLDER_LIST | C→S | Get folder listing / file download |
| 0x19 | (unnamed) | C→S | Server info query |
| 0x1a | (unnamed) | C↔S | Multi-result node listing |
| 0x1c | (unnamed) | C↔S | Version listing |
| 0x1d | (unnamed) | C→S | Share link operations |
### 2.3 Section Fields (SPROTO_ID)
After the header, the legacy protocol body consists of **TLV sections**. Each section is identified by a 1-byte SPROTO_ID tag. The tag determines the wire type:
**Fixed-size fields**: `[tag_byte][value_bytes]` (1, 2, 4, or 8 bytes, little-endian)
**Variable-length fields**: `[tag_byte][length_uint16][data_bytes]`
```c
// Writing a fixed-size section
int ProtoWriteSection(Channel& chan, uint8_t section_id, uint8_t value) {
SecAttr* attr = ProtoGetSecAttr(section_id);
if (!attr || attr->type != 1) return -5; // PROTO_ERR_INVALID
chan.WriteUInt8(section_id); // tag
chan.WriteUInt8(value); // value
return 0;
}
// Writing a variable-length section
int ProtoWriteSection(Channel& chan, uint8_t section_id, const string& value) {
SecAttr* attr = ProtoGetSecAttr(section_id);
if (!attr || attr->type != 3) return -5;
chan.WriteUInt8(section_id); // tag
chan.WriteUInt16(value.length()); // length prefix
chan.Write(value.data(), value.length()); // data
return 0;
}
```
Complete field table:
| 0x01 | RES | uint8 | 1 | Result/response code |
| 0x02 | TYPE | uint8 | 1 | Message/operation type |
| 0x03 | USER | vardata | var | Username string |
| 0x04 | PASSWD | vardata | var | Password string |
| 0x05 | CLIENT | vardata | var | Client identifier string |
| 0x06 | SESSION | vardata | var | Session token string |
| 0x07 | PATH | vardata | var | File path string |
| 0x08 | NOTIFY | uint8 | 1 | Notification flag |
| 0x09 | SID | uint64 | 8 | Session ID |
| 0x0A | PREFIX | vardata | var | Path prefix string |
| 0x0B | EVT_COUNT | uint64 | 8 | Event count |
| 0x0C | SEQID | uint32 | 4 | Sequence ID |
| 0x0D | EVTID | uint64 | 8 | Event ID |
| 0x0E | FILETYPE | uint8 | 1 | File type (file/dir/symlink) |
| 0x0F | FILESTATUS | uint8 | 1 | File sync status |
| 0x10 | FILEHASH | vardata | var | File content hash |
| 0x11 | FILETIME | uint32 | 4 | File modification time (unix timestamp) |
| 0x12 | FILESIZE | uint64 | 8 | File size in bytes |
| 0x13 | FILEUPDATER | vardata | var | Last updater username |
| 0x14 | FILEOP | uint8 | 1 | File operation type |
| 0x15 | DELTALEN | uint64 | 8 | Delta patch length |
| 0x16 | DELTADATA | vardata | var | Delta patch data |
| 0x17 | DELTACOUNT | uint32 | 4 | Number of delta blocks |
| 0x18 | DELTAID | uint32 | 4 | Delta block identifier |
| 0x19 | DELTATYPE | uint8 | 1 | Delta type (whole file vs patch) |
| 0x1A | USRENABLE | uint8 | 1 | User enable flag |
| 0x1B | SERIAL_NUMBER | vardata | var | Device serial number |
| 0x1C | PROTO_VERSION | uint8 | 1 | Protocol version |
| 0x1D | SERVER_ID | vardata | var | Server identifier (DS ID) |
| 0x1E | ALIVE | uint32 | 4 | Keep-alive interval (seconds) |
| 0x1F | DATA_BLOCK | uint32 | 4 | Data block size |
| 0x20 | SYNC_BLOCK | uint32 | 4 | Sync block size |
| 0x21 | MACHASH | vardata | var | Mac extended attribute hash |
| 0x22 | MACSIZE | uint64 | 8 | Mac extended attribute size |
| 0x23 | AUTH_TYPE | uint8 | 1 | Authentication type |
| 0x24 | VIEW_ID | uint64 | 8 | View/shared folder ID |
| 0x25 | VIEW_NAME | vardata | var | View/shared folder name |
| 0x26 | VIEW_COUNT | uint32 | 4 | Number of views |
| 0x27 | META_BLOCK | uint32 | 4 | Metadata block size |
| 0x28 | FILTER_VERSION | vardata | var | Filter rules version string |
| 0x29 | PROTO_VERSION_2 | vardata | var | Extended protocol version string |
| 0x2A | KEEP_ALIVE_OPTION | uint32 | 4 | Keep-alive configuration |
| 0x2B | KEEP_ALIVE_CONTROL | uint32 | 4 | Keep-alive control flags |
| 0x2C | ACK_OPTION | uint32 | 4 | Acknowledgement options |
| 0x2D | SSL_OPTION | uint32 | 4 | SSL negotiation options |
| 0x2E | SSL_RESPONSE_OPTION | uint32 | 4 | SSL response options |
| 0x2F | PKG_MAJOR_VERSION | uint16 | 2 | Server package major version |
| 0x30 | PKG_MINOR_VERSION | uint16 | 2 | Server package minor version |
| 0x31 | PKG_BUILDNUM | uint32 | 4 | Server package build number |
| 0x32 | FOLDER_COUNT | uint32 | 4 | Number of folders |
| 0x33 | JOB_TOKEN | vardata | var | Resumable job token |
| 0x34 | FILE_OFFSET | uint64 | 8 | File offset for resume |
| 0x35 | PATCH_TOKEN | vardata | var | Delta patch token |
| 0x36 | RESTORE_ID | vardata | var | Restore/device identifier |
---
## 3. Modern Protocol Layer (PObject-Based)
### 3.1 PObject Type System
`PObject` is a 24-byte tagged union supporting 7 types:
```c
struct PObject {
int32_t type_tag; // +0x00: discriminant (0-6)
uint64_t value; // +0x08: inline value or pointer to heap data
uint64_t extra; // +0x10: used by some container types
};
// Total: 24 bytes
```
| type_tag | Type | C++ Storage |
| 1 | Array | `std::vector<PObject>` |
| 2 | Map | `std::map<std::string, PObject>` (red-black tree, sorted keys) |
| 3 | Integer | `uint64_t` inline |
| 4 | String | `PObject::SimpleString` (custom string class) |
| 5 | Binary | `PObject::binary_type` {offset, length, path} |
| 6 | BinaryEx | `PObject::binary_ex_type` (binary + hash) |
All field access in the modern protocol uses **string keys** via `PObject::operator[]("key")`. There are no numeric field IDs in the modern layer — the "SPROTO_ID" concept only applies to the legacy section-based layer.
### 3.2 PStream Wire Format
`PStream` serializes PObjects using a **tag-length-value** binary encoding. All multi-byte integers are **big-endian** (network byte order).
#### Type Tags on the Wire
| `0x00` | Null | `[0x00][0x00]` (tag + zero-length indicator) |
| `0x01` | Integer | `[0x01][size_byte][value_bytes]` — variable-length big-endian |
| `0x10` | String | `[0x10][uint16_be length][utf8_bytes]` |
| `0x30` | Binary | `[0x30][uint64_be length][raw_file_bytes]` — streamed via Channel |
| `0x40` | End Marker | Terminates Map or Array |
| `0x41` | Array Start | `[0x41][elements...][0x40]` |
| `0x42` | Map Start | `[0x42][key-value pairs...][0x40]` |
| `0x43` | BinaryEx | `[0x43][map with "binary" + "send_hash" keys][0x40]` |
#### Integer Encoding (Variable-Length, Big-Endian)
Integers use the **minimum number of bytes** needed:
```
Value range Size byte Wire bytes
< 0x100 0x01 [0x01][0x01][1 byte BE]
< 0x10000 0x02 [0x01][0x02][2 bytes BE]
< 0x100000000 0x04 [0x01][0x04][4 bytes BE]
>= 0x100000000 0x08 [0x01][0x08][8 bytes BE]
```
#### String Encoding
```
[0x10][uint16_be length][utf8_bytes]
```
Note: String keys starting with `_` (underscore) have the leading underscore **stripped** during serialization — the length is decremented by 1 and the pointer advanced by 1. This is an internal convention for field name prefixing.
#### Map Encoding
```
[0x42] // map start tag
[0x10][key1_len][key1_str] // string key
[type_tag][value1...] // value (any type)
[0x10][key2_len][key2_str] // next key
[type_tag][value2...] // next value
...
[0x40] // end marker
```
#### Array Encoding
```
[0x41] // array start tag
[type_tag][element1...] // first element
[type_tag][element2...] // second element
...
[0x40] // end marker
```
#### Binary File Transfer (tag 0x30) — CRITICAL PATH
Binary data (file content) is transferred as a **single contiguous stream**:
```
[0x30] // binary tag
[uint64_be total_length] // file size
[raw_bytes...] // file data streamed via Channel
```
**Sending** (`PStream::Send(binary_type)`):
1. Opens the file at `binary_type.path`
2. Sends tag `0x30`
3. Sends file length as uint64 big-endian
4. Calls `Channel::SendFileData(fd, offset, length, progress_reporter)` — bulk transfer via the channel
**Receiving** (`PStream::Recv(binary_type)`):
1. Reads uint64 big-endian length
2. Creates destination file at the configured receive location
3. Calls `Channel::RecvFileData(fd, total_size, already_received, progress_reporter)` — bulk receive
4. Truncates file to exact received size
5. File data is NOT chunked at the PStream layer — the Channel handles buffering
#### Extended Binary (tag 0x43)
Wraps binary in a map with integrity hash:
```
[0x43] // binary_ex start
[0x10][0x06]["binary"] // key: "binary"
[0x30][length][data...] // value: binary_type (file data)
[0x10][0x09]["send_hash"] // key: "send_hash"
[0x10][hash_len][hash_bytes] // value: computed hash string
[0x40] // end marker
```
Maximum nesting depth: **256 levels** (enforced in `PStream::RecvDispatch`).
### 3.2 PObject Message Framing
The modern protocol also uses the same 8-byte wire header, but with different semantics:
- **Magic**: `0x25521814` (same)
- **Command**: Resolved from `_action` string via an internal action-to-SCMD map
- **pkt_len**: Always set to 0 (length is implicit in PObject serialization)
### 3.3 Protocol Type Byte
The CloudStation layer sends a header via `CloudStation::SendHeader(channel, 0x46, protocol_type)` where `protocol_type` determines the server-side handler:
| SCMD Byte | Meaning | Actions |
|-----------|---------|---------|
| 0x01 | Single-response RPC | ~90% of actions: `upload`, `get_file_info`, `search_file`, `list_team_folder`, `query_node`, `delete`, `copy`, `move`, `create`, `update`, labels, sharing, webhooks, etc. |
| 0x11 | User query | `query_user` |
| 0x12 | Authentication | `auth` |
| 0x18 | File transfer | `download`, `restore` (via `ProtoDownload`/`ProtoRestore`) |
| 0x19 | Server info | `query_server_info` |
| 0x1a | Multi-result listing | `list`, `list_v2`, `recent`, `list_shared_with_me`, `list_shared_with_others`, `list_ancestor_by_path` |
| 0x1c | Version listing | `list_version`, `list_file_version` |
| 0x1d | Share link | `share_link` |
Note: `ProtocolClient` has a `std::map<string, int>` designed to override the SCMD byte per action, but it is **never populated** — the map stays empty and all requests default to 0x01. The real SCMD differentiation is hardcoded at each `CloudStation::*` call site.
### 3.4 The `@proto` Envelope
Every modern protocol message carries a `@proto` metadata envelope:
```json
{
"@proto": {
"type": "header",
"date": 1711234567,
"version": { "major": 7, "minor": 0 },
"body-continue": false,
"x-forward": { // optional, for proxied connections
"<ip>": "192.168.1.1",
"port": 6690,
"proto": "tcp"
}
},
"_action": "download",
"_agent": {
"platform": "mac",
"type": "drive",
"device_uuid": "2e7ef840-...",
"restore_id": "df50e0fe...",
"version": { "major": 4, "minor": 0, "mini": 0, "build": 17889 }
},
"session": "04f7ffbd...",
"view_id": 1,
"root_node_id": 1,
... // action-specific fields
}
```
### 3.5 Multi-Part Messages
Large payloads are sent as multi-part messages using the `body-continue` flag:
1. **Header frame**: Contains `@proto.body-continue = true`, `_action`, `_agent`, etc.
2. **Body frame(s)**: Each contains `@proto.body-continue = true` (more coming) or `false` (last frame)
3. Arrays are sent element-by-element, each as a separate body frame
The receiver collects body frames into an array when multiple are present.
### 3.6 Keep-Alive Handling
Both during send and receive, the protocol transparently handles keep-alive messages:
- Keep-alive messages have `{"type": "keep_alive"}` at the top level
- They are silently consumed and the receiver loops to get the real response
- The `alive` field in real responses specifies the keep-alive interval in seconds (0 = close connection)
---
## 4. Connection Establishment
### 4.1 SSL Upgrade (Optional)
If SSL is required, the client first sends an `encrypt_channel` request:
```
→ Header: magic=0x25521814, cmd=SCMD_REQUEST_SSL_2 (0x16)
→ PObject: { "_action": "encrypt_channel", ... }
← PObject: { "error": null } or { "error": { "code": N, "reason": "..." } }
```
On success, the TCP connection is upgraded to TLS using OpenSSL. The client configures:
- `ssl_allow_untrust`: Whether to accept self-signed certs (from connection settings)
- SSL hostname verification
- SSL certificate signature pinning
### 4.2 Connection Handshake
After optional SSL upgrade:
```
→ Header: cmd=SCMD_REQUEST_CONNECT (0x0E)
→ PObject: {
"_action": "connect",
"_agent": { "platform": "mac", "type": "drive", "version": {...}, "device_uuid": "..." },
"session": "<session_token>",
"restore_id": "<device_restore_id>",
"client_version": "<version_string>",
"client_type": "drive" // or "drive_backup" for backup tasks
}
← PObject: {
"alive": 300, // keep-alive interval in seconds; 0 = connection refused
"server": {
"package_version": {
"build": 17889
}
}
}
```
The client validates that the server build number hasn't changed since last connection. If it has, the connection is rejected with error code -33.
**Client types**: `"drive"` (normal sync) or `"drive_backup"` (backup tasks, conn_type == 2).
### 4.3 Connection Pool
The client maintains a pool of channels (`Connection::pop`/`Connection::push`). Channels have expiration based on the `alive` interval. Expired channels are transparently reopened via `ReopenChannel`.
---
## 5. File Download Protocol
### 5.1 Download Request
File downloads use the `"download"` or `"batch_download"` action:
```
→ PObject: {
"_action": "download", // or "batch_download"
"session": "...",
"view_id": 1,
"root_node_id": 1,
"task_id": "...", // batch_download only
"dry_run": false, // batch_download: check without downloading
"is_preview": false, // batch_download: preview mode
... auth info ...
}
```
### 5.2 Download Request (DownloadRemoteHandler::PrepareDownloadRequest)
The sync engine builds download requests with `_action = "download"` (or `"resume_download"`):
```json
→ {
"_action": "download",
"session": "...",
"view_id": 1,
"sync_id": 50000,
"max_id": 50100,
"target_sync_id": 50001,
"path": "id:812195821838704701",
"c2_offload": false,
"force_current_version": false
}
```
**Critical: the `path` field is NOT a filesystem path.** For servers with build >= 12001 (0x2EE1), the path must be `"id:" + file_id` (the permanent file identifier string from `list`/`get_file_info` responses). For older servers, use the actual filesystem path.
When the sync watch path is not `"/"` (i.e., syncing a subfolder, not the view root), the code **erases `view_id` from the request** and uses the `"id:<file_id>"` path format. When syncing the root, the full filesystem path from the event context is used instead.
**Request fields:**
| `path` | string | `"id:<file_id>"` (build >= 12001) or filesystem path (older) |
| `sync_id` | uint64 | Known sync_id from local DB or event |
| `max_id` | uint64 | Max known sync ID |
| `target_sync_id` | uint64 | Specific file version to download |
| `c2_offload` | bool | Whether to use C2 cloud offload |
| `force_current_version` | bool | Force download of current version |
### 5.3 Download Wire Framing
**Request**: `[8-byte header]` then `[PStream-serialized PObject]`
The SCMD byte depends on the code path:
- `ProtocolClient::Request` (sync engine path): SCMD **0x01** (the action→SCMD map is empty, always defaults to SCMD_REQUEST)
- `ProtoDownload` (standalone low-level path): SCMD **0x18**
**Response: NO frame header.** The response is a raw PStream-serialized PObject. `ProtocolClient::RecvResponse` goes straight to `PStream::Recv`.
File content arrives **inline within the response PObject** as `binary_type` (tag 0x30) or `binary_ex_type` (tag 0x43) fields:
1. Tag byte `0x30` or `0x43`
2. `uint64_be` total file length
3. Raw file bytes streamed through the channel
4. For `binary_ex_type`: after file data, a `"send_hash"` field for integrity verification
The `ProtocolClient` sets a **receive location** (temp directory path) via `PStream::SetDefaultRecvLocation` so that `PStream::Recv` knows where to write binary data to disk during deserialization.
### 5.4 Download Flow (DownloadRemoteHandler)
1. `PrepareDownloadResponseFromEvent` — pre-populates response from cached event data
2. `PrepareDownloadResponseFromLocal` — adds locally-available data (for delta downloads)
3. `GetUnsatisfiedAttributes` — determines what still needs fetching from server
4. `PrepareDownloadRequest` — builds the request PObject (see §5.2)
5. `SimpleDownload` — sends via `ProtocolClient::Request`
6. If `sync_id` in response matches request → file is already up-to-date, skip
7. `ProcessDownloadFile` — writes file to disk, applies attributes
8. `CommitDatabaseDownload` — updates local DB
For files > 512KB (0x80000), **resumable download** is used: `GetResumeDownloadToken` → `ContinueResumeDownload` loop.
### 5.5 Download Response Structure
The download response PObject contains metadata at the top level and file content nested under `"file"`:
```json
← {
"@proto": { "body-continue": false, ... },
"alive": 300,
"sync_id": 50001,
"max_id": 50100,
"path": "/Documents/report.pdf",
"file_id": "812195821838704701",
"file_type": "file",
"parent_id": "812195821838704640",
"permanent_link": "...",
"exec_bit": { "exec_bit": 0 },
"unix_perm": { "uid": 1000, "gid": 100, "mode": 644 },
"mac_attribute": { "hash": "...", "size": 0 },
"synology_acl": { "acl": "...", "hash": "..." },
"share_priv": { "disabled": 0, "deny_list": "", "ro_list": "", "rw_list": "", "hash": "..." },
"mtime": { "mtime": 1711234567 },
"server": { "package_version": { "build": 17889 } },
"file": {
"data": <binary_type tag 0x30>, // THE ACTUAL FILE CONTENT
"hash": "d41d8cd98f00b204...", // content hash
"size": 375000, // file size
"refer": false, // true = server already has it (dedup)
"refer_local": false, // true = local copy is identical
"is_delta": false // true = data is a delta patch, not whole file
}
}
```
**The file content is at `response["file"]["data"]`** as a `binary_type` (PStream tag 0x30). During PStream deserialization, this binary field is streamed directly to disk at the configured receive location — the PObject stores a path to the temp file, not the bytes in memory.
If the response has **no `"file"` sub-object**, the server is returning only metadata (e.g., because the `path` field in the request didn't correctly identify a file). The `path` field must be `"id:<file_id>"` for server build >= 12001.
**Dedup cases** (no binary data transferred):
- `file.refer == true`: Server already has a copy with matching hash
- `file.refer_local == true`: Local copy matches, no download needed
### 5.6 Delta Downloads
When `file.is_delta == true`, the `file.data` binary contains an rsync-style delta patch (not the whole file). The client:
1. Reads `file.size` for the suggested block size via `FileReader::getSuggestedBlockSize()`
2. Uses `DeltaFileReader::setFile()` with the binary data path
3. Applies the delta against the local base file via `DeltaFileReader::readFile()`
### 5.7 Resumable Downloads
For files > 512KB, the client uses resume tokens:
1. `GetResumeDownloadToken` — gets a server-issued token
2. `ContinueResumeDownload` — sends chunks with the token
3. On failure, saves state for later continuation
### 5.5 Batch Download (Modern Path)
The `batch_download` action via `CloudStation::DownloadFile` returns an archive:
```
← PObject: {
"archive_info": {
"location": "/path/to/archive",
"archive_name": "filename.zip",
"archive_codepage": "utf-8"
}
}
```
---
## 6. File Upload Protocol
### 6.1 Upload Request Structure
```
→ PObject: {
"_action": "upload", // or "resume_upload" for resumable
"session": "...",
"view_id": 1,
"sync_id": 12345, // current sync cursor
"max_id": 12345, // max sync ID known
"path": "/relative/path", // parent-relative if parent has view_id
"conflict_policy": "compare_mtime",
"is_dir": false,
"file_type": "regular",
"file": {
"size": 12345, // file size (uint64)
"hash": "md5hex...", // content hash
"data": <binary_type>, // file content (sent as tag 0x30 binary)
"refer": false, // true = server already has content (dedup)
"is_delta": false, // true = sending rsync delta, not full file
"base_size": 0, // base file size for delta
"real_size": 12345, // actual file size (may differ if delta)
"signature": <binary_type> // rsync signature (if signature_offload)
},
"mtime": 1711234567,
"mac_attribute": {
"data": <binary_type> // macOS extended attributes
},
"exec_bit": 0,
"acl_attribute": "...",
"unix_permission": { "uid": N, "gid": N, "mode": N }
}
```
**Important**: Before sending, `file.data`, `file.signature`, and `mac_attribute.data` are **stripped from the metadata PObject** and sent as separate binary frames via `ProtocolClient::Request`. This means the metadata goes as a PObject map, and binary payloads follow as binary_type (tag 0x30) attachments.
### 6.2 Content Deduplication
The client computes the file hash before upload. If the hash matches what's in the local database (unchanged file), or if the server's dry-run response returns a `file_id`, the upload sets `file.refer = true` and **erases `file.data`** — no content transfer needed. The server already has a copy.
### 6.3 Delta Uploads (rsync-style)
For modified files where the server has a previous version:
1. Client creates a `synodrive::rsapi::SimpleFileReader`
2. Computes an rsync-style signature with `setSignature()` using a block size from `FileReader::getSuggestedBlockSize(fileSize)`
3. If a base version exists in the DB, computes a delta with `setDelta()`
4. Sets `file.is_delta = true` and `file.base_size` — only the delta is uploaded
5. Server can also request **signature offload** (`signature_offload` flag) or **hash offload** (`hash_offload` flag) in the dry-run response
### 6.4 Resumable Uploads
For large files, the protocol uses server-issued tokens:
1. **Get token**: Action `"resume_get_token"` → server returns `resume_token`
2. **Dry-run**: Pre-flight check for bandwidth/quota
3. **Upload chunks**: Each chunk sent via `SimpleUpload` with `_resume_token` field
4. **On pause/abort**: Resume state saved to disk (`TempFile::convert_permanent()`)
5. **Continue later**: `ContinueResumeUpload` resends with the saved token
### 6.5 Upload Pre-Flight (Dry-Run)
Before large uploads, the client sends an upload request with `bandwidth_check = true` and optionally `reserve_size = <file_size>`. The server validates quota/bandwidth constraints without transferring data.
### 6.6 C2 Cloud Offload
For Synology C2 cloud storage shares:
- `HandleUploadFileToC2` tries uploading to C2 first
- On failure, `FallBackToUploadFileToDriveServer` falls back to direct server upload
- Delta uploads are disabled for C2 shares
---
## 7. Event Synchronization
### 7.1 Long Polling (Primary Change Detection)
The daemon uses long-polling as its primary change detection mechanism via the `"long_poll"` action.
**Request:**
```json
{
"_action": "long_poll",
"timeout": 300,
"profile_digest": "abc123...",
"subscribe": [
{
"sync_id": 12345, // last known sync cursor
"path": "/watched/path",
"view_id": 1,
"node_id": 1,
"lock_id": 0,
"option_change_lock_id": 0,
"recursive": true
}
]
}
```
**Response:**
```json
{
"changes": [
{ "view_id": 1 }, // which views/shares changed
{ "view_id": 2 }
],
"profile_changed": false, // user profile needs refresh
"profile_digest": "def456...",
"notification": [...] // connection notifications for UI
}
```
**Long-poll lifecycle** (`LongPoller::DoTask`):
1. Worker 11 sends `long_poll` request — this **blocks** until server has changes or timeout
2. Server responds with a `changes` array listing which `view_id`s have new events
3. For each changed view, client calls `IssueSyncerPullEvent` to trigger sync workers
4. If `profile_changed` is true, refreshes user info via `QueryUserInfo`
5. On error, falls back to direct `pull_event` for all subscribed sessions
### 7.2 Event Pull (Actual Change Fetching)
After long-poll signals changes, the sync workers fetch actual events via `"pull_event"`:
**Request:**
```json
{
"_action": "pull_event",
"sync_id": 12345, // current cursor position
"path": "/watched/path",
"node_id": 1,
"recursive": true,
"file": { "offset": 0 },
"mtime": { "offset": 0 },
"mac_attribute": { "offset": 0 },
"exec_bit": { "offset": 0 }
}
```
**Response:**
```json
{
"event_list": [
{
"file_id": "...",
"path": "/changed/file.txt",
"file_op": 1, // operation type
"file_type": 0,
"file_size": 12345,
"file_hash": "...",
"mtime": 1711234567,
"sync_id": 12346
}
],
"next_sync_id": 12350,
"rescan_later": false,
"merge_mode": false,
"server": {
"package_version": { "build": 17889 }
}
}
```
For server version 3+, events reference files by `file_id` and use `convert_req2event_v80`. Events are pushed to `SyncerEventManager` or `EventManager::PushServerEvent` for processing by sync workers.
### 7.4 Event Structure (from `convert_req2event`)
Events are **not** tagged with an operation enum. Instead, the event type is determined by the `is_removed` field:
- `is_removed == true` → **FileRemoveEvent** (file deleted)
- `is_removed == false` → **FileAddEvent** (file created or modified)
Renames and moves are indicated by a `rename` sub-object within the event.
**Event PObject fields:**
| `is_removed` | bool | true = delete event, false = add/modify |
| `path` | string | File path |
| `file_id` | string | Permanent file identifier |
| `parent_id` | string | Parent node identifier |
| `permanent_link` | string | Permanent link |
| `sync_id` | uint64 | Sync cursor position for this event |
| `max_id` | uint64 | Max sync ID at time of event |
| `file.hash` | string | File content hash |
| `file.size` | uint64 | File size |
| `mtime.mtime` | uint32 | Modification timestamp |
| `mac_attribute.hash` | string | macOS extended attributes hash |
| `mac_attribute.size` | uint64 | macOS extended attributes size |
| `exec_bit.exec_bit` | uint32 | Unix executable bit |
| `unix_perm.uid` | uint32 | Unix UID |
| `unix_perm.gid` | uint32 | Unix GID |
| `unix_perm.mode` | uint32 | Unix file mode |
| `synology_acl.acl` | string | Synology ACL string |
| `synology_acl.hash` | string | ACL hash |
| `share_priv.disabled` | int32 | Share privilege disabled flag |
| `share_priv.deny_list` | string | Denied users list |
| `share_priv.ro_list` | string | Read-only users list |
| `share_priv.rw_list` | string | Read-write users list |
| `share_priv.hash` | string | Privilege hash |
| `rename.opt` | string | Rename operation: `"rename"` (name change) or `"move"` (path change) |
The file type is determined by `PObjectHelper::GetSupportedFileType(event)` which reads `file_type` string (`"file"`, `"dir"`, `"symlink"`) or falls back to `is_dir` boolean.
### 7.5 Three-Way Merge Sync
The daemon implements a three-way merge algorithm for conflict detection, comparing base state, local state, and server state:
| 1 | No change | Skip |
| 2, 4 | Local modified | Upload |
| 3 | Server modified | Download |
| 5 | Both modified | Conflict resolution |
| 6 | Server removed | Delete local |
| 7 | Server removed + Local modified | Conflict |
| 8 | Local removed | Delete remote |
| 9 | Local removed + Server modified | Conflict |
| 10 | Both removed | Clean up |
| 11, 12 | Both created | Conflict |
| 13 | Local created | Upload |
| 14 | Server created | Download |
| 15 | Server created + both advance | Download |
### 7.3 Worker Architecture
The daemon runs multiple workers managed by `WorkerManager` with barrier synchronization:
- **Workers 0-5**: Sync workers that process upload/download events
- **Worker 11**: Long-poll worker for server change detection
- Workers can be paused/resumed as a group (barrier-based) for connection changes
- Channel pool capacity: **5 concurrent channels**
- On connection change, all workers pause → connection reloads → workers resume
---
## 8. Authentication
### 8.1 AuthSession (`_action = "auth"`, protocol_type = 0x12)
```json
→ {
"_action": "auth",
"client": "SynologyDriveClient",
"client_type": "drive",
"client_version": "4.0.0-17889",
"dry_run": false,
"renew_session": "",
"username": "admin",
"password": "secret",
"otp": ""
}
← {
"session": "04f7ffbd0c793a3a...",
"server_id": "df50e0fe4e6e521c..."
}
```
**Auth modes** (set by `AppendAuthInfo` at `0x1004462e0`, selected based on available credentials):
**A. Session reuse** (not first auth):
- `session`: existing session token string
- `username`: (optional, sent if not anonymous)
**B. RSA key-based** (normal operation after device pairing):
- `username`: username string
- `pem.key_fingerprint`: RSA public key fingerprint
- `pem.salt`: current `time()` as string
- `pem.signature`: RSA signature of base64(username + time_string) signed with private key
- `otp`: (optional, for 2FA)
**C. Username/password** (first-time login):
- `username`: username string
- `password`: password string (cleartext, but over SSL)
- `otp`: (optional, for 2FA)
**D. Share token**: `sharing_token` field
**E. Sudo**: `sudo` field (user string or uid)
Note: There is **no legacy section-based auth** (SPROTO_ID_USER/SPROTO_ID_PASSWD). The `ProtoWriteSection` functions exist as dead code but are never called. All auth goes through PObject/PStream.
**Session bootstrap flow**: The GUI collects credentials → calls `CloudStation::AuthSession` which opens a dedicated TCP connection with SCMD 0x12 → sends credentials as PObject fields → receives `session` token and `server_id` → stores in `ConnectionInfo` → persisted to `sys.sqlite`. Subsequent connections use the session token via `ProtocolClient::Connect`.
### 8.2 QueryServer (`_action = "query_server_info"`, protocol_type = 0x19)
```json
→ { "_action": "query_server_info", "get_all": true }
← {
"database_serial": "...",
"database_restore_id": "df50e0fe...",
"server_id": "...",
"package_version": { "major": 4, "minor": 0, "build": 17889 },
"dsm": { "major": 7, "minor": 2, "build": 64570, "fix": 0, "unique": "..." },
"server_alias": "...",
"host_name": "..."
}
```
The `database_restore_id` is needed as the `restore_id` for subsequent requests.
---
## 9. Directory Listing & File Tree Enumeration
### 9.1 ListTeamFolder (`_action = "list_team_folder"`)
Lists all available shares/views:
```json
→ {
"_action": "list_team_folder",
"offset": 0,
"limit": 0,
"sort_by": "name",
"sort_direction": "asc"
}
← {
"view_list": [
{
"view_id": 1,
"file_id": "812195821838704641",
"name": "home",
"capabilities": {
"can_preview": true, "can_read": true, "can_write": true,
"can_delete": true, "can_rename": true, "can_comment": true,
"can_share": true, "can_encrypt": false, "can_organize": true,
"can_download": true, "can_sync": true
},
"enable_versioning": true,
"keep_versions": 32,
"disable_download": false
}
]
}
```
Each `view_id` identifies a share/folder. The personal "My Drive" corresponds to the `"home"` view.
### 9.2 ListNode (`_action = "list"`, protocol_type = 0x1a)
Lists directory contents. Three overloads:
**By path:**
```json
→ {
"_action": "list",
"view_id": 1, // set via SetViewId
"path": "/",
"list_dir_only": false,
"merge_local": true,
"search_criteria": {
"limit": 1000,
"offset": 0,
"sort_by": "name",
"sort_direction": "asc"
}
}
```
**`merge_local`**: When `true` (used by simple UI listings), the server merges locally-pending changes into the response for a unified view. When `false` (used by sync engine, search/filter queries, and three-way merge), the server returns only canonical server-side state. Rule of thumb: `true` for browsing, `false` for sync.
**Pagination**: The server returns one PObject per request containing `node_list` (up to `limit` entries) and `total_count`. Two pagination modes exist:
- **Offset-based** (`use_cursor = false`): Client sends `search_criteria.offset`, increments by page size each call. Terminates when `offset + len(node_list) >= total_count`.
- **Cursor-based** (`use_cursor = true`, default): Client sends `search_criteria.cursor` (server-opaque token from previous response). Version-gated — requires server build >= 10238 (`VersionInfo::IsSupportCursor()`). Offset and cursor are mutually exclusive — only one is sent per request.
Default page size: **50** (`SearchNodeFilter.limit` default).
Note: The sync engine does NOT paginate — `ThreeWayMergeHelper::GetServerFileList` uses `list_sync_to_device` (see §9.10) which returns all files in a directory in one response. Directory recursion is event-driven (one event per subdirectory, breadth-first). Only the UI/API layer uses offset/cursor pagination.
**By node_id:**
```json
→ {
"_action": "list",
"view_id": 1,
"node_id": 12345,
"list_dir_only": false,
"merge_local": false
}
```
**Response (`node_list` array of Node objects):**
```json
← {
"node_list": [
{
"node_id": 100,
"sync_id": 50000,
"name": "Documents",
"file_size": 0,
"mtime": 1711234567,
"hash": "",
"file_id": "812195821838704700",
"file_type": "dir",
"is_removed": false,
"privilege": "read-write"
},
{
"node_id": 101,
"sync_id": 50001,
"name": "photo.jpg",
"file_size": 2048576,
"mtime": 1711234500,
"hash": "d41d8cd98f00b204...",
"file_id": "812195821838704701",
"file_type": "file",
"is_removed": false
}
],
"total_count": 2
}
```
**Node struct fields:**
| `node_id` | uint64 | Unique node identifier |
| `sync_id` | uint64 | Sync cursor position |
| `file_size` | uint64 | File size (0 for dirs) |
| `mtime` | uint32 | Modification time (unix) |
| `name` | string | File/folder name |
| `hash` | string | Content hash |
| `file_id` | string | Permanent file identifier |
| `is_removed` | bool | In trash |
| `file_type` | string | `"file"`, `"dir"`, or `"symlink"` |
| `privilege` | string | `"read-only"`, `"denied"`, or full access |
### 9.3 ListFile v2 (`_action = "list_v2"`, protocol_type = 0x1a)
Richer listing with full FileInfo:
```json
→ {
"_action": "list_v2",
"view_id": 1,
"path": "/Documents",
"list_dir_only": false,
"search_criteria": {
"limit": 1000,
"offset": 0,
"sort_by": "name",
"sort_direction": "asc"
},
"extra": ["capabilities", "labels", "node_locking"]
}
```
Response `node_list` contains full FileInfo objects (70+ fields including capabilities, sharing, labels, locking state, timestamps, etc.).
### 9.4 GetFileInfo (`_action = "get_file_info"`)
```json
→ {
"_action": "get_file_info",
"view_id": 1,
"path": "/Documents/report.pdf",
"extra": ["capabilities"]
}
← { "node": { /* full FileInfo object */ } }
```
Optional fields: `case_insensitive_path` (bool), `log_action` (string), `update_access_time` (bool).
### 9.5 QueryNode (`_action = "query_node"`)
Lightweight single-node query:
```json
→ { "_action": "query_node", "view_id": 1, "path": "/Documents" }
← { "node": { "node_id": 100, "sync_id": 50000, "name": "Documents", ... } }
```
### 9.6 SearchFile (`_action = "search_file"`)
```json
→ {
"_action": "search_file",
"search_criteria": {
"keyword": "report",
"file_type": "file",
"limit": 50,
"offset": 0
}
}
← {
"search_list": [ /* FileInfo objects */ ],
"total_count": 15,
"search_time": 42
}
```
### 9.7 FileInfo Struct (Full)
Returned by `list_v2`, `get_file_info`, `search_file`, `recent`:
| `file_id` | string | Permanent file identifier |
| `path` | string | Full path |
| `display_path` | string | Display path |
| `name` | string | File name |
| `parent_id` | uint64 | Parent node ID |
| `size` | uint64 | File size |
| `created_time` | uint64 | Creation timestamp |
| `access_time` | uint64 | Last access timestamp |
| `modified_time` | uint64 | Modification timestamp |
| `change_time` | uint64 | Metadata change timestamp |
| `recycled_time` | uint64 | Trash timestamp |
| `sync_id` | uint64 | Sync cursor position |
| `max_id` | uint64 | Max known sync ID |
| `hash` | string | Content hash |
| `owner` | string | File owner |
| `permanent_link` | string | Permanent link |
| `content_type` | string | MIME type |
| `removed` | bool | In trash |
| `encrypted` | bool | Encrypted |
| `starred` | bool | Starred |
| `shared` | bool | Shared |
| `adv_shared` | bool | Advanced sharing enabled |
| `transient` | bool | Transient/temporary |
| `disable_download` | bool | Download disabled |
| `capabilities.*` | bool | `can_preview`, `can_read`, `can_write`, `can_delete`, `can_rename`, `can_comment`, `can_share`, `can_encrypt`, `can_organize`, `can_download`, `can_sync`, `can_lock`, `can_auto_lock` |
| `labels` | array | `[{label_id, color}, ...]` |
| `shared_with` | array | `[{permission_id, type, nickname, display_name, role, inherited}, ...]` |
| `node_locking` | object | `{is_locked, is_auto_lock, lock_id, lock_user, lock_time}` |
| `adv_shared_info` | object | `{has_password, due_date}` |
| `revisions` | array | Version history |
| `properties` | object | Custom properties |
| `app_properties` | object | App-specific properties |
### 9.8 SearchNodeFilter
Available filter criteria for `list` and `list_v2`:
| `limit` | uint64 | 50 | Max results per page |
| `offset` | uint64 | 0 | Pagination offset |
| `cursor` | uint64 | 0 | Cursor-based pagination |
| `sort_by` | string | — | Sort field |
| `sort_direction` | string | — | `"asc"` or `"desc"` |
| `file_type` | string | — | `"file"`, `"dir"`, `"folder"` |
| `keyword` | string | — | Search keyword |
| `label_id` | string | — | Filter by label |
| `starred` | bool | false | Only starred items |
| `list_removed` | bool | false | Include trashed items |
| `include_transient` | bool | false | Include transient items |
| `extensions` | array | — | File extension filter |
| `version_ctime_upper_bound` | uint64 | — | Created before |
| `version_ctime_lower_bound` | uint64 | — | Created after |
| `version_mtime_upper_bound` | uint64 | — | Modified before |
| `version_mtime_lower_bound` | uint64 | — | Modified after |
| `version_file_size_upper_bound` | uint64 | — | Smaller than |
| `version_file_size_lower_bound` | uint64 | — | Larger than |
### 9.9 `list_sync_to_device` (`_action = "list_sync_to_device"`, SCMD 0x1a)
A specialized listing action used by the sync engine. Unlike `list`/`list_v2`, it returns **all** children in a single response with no pagination, and supports a cursor for efficient change detection.
**Used by**: `ThreeWayMergeHelper::GetServerFileList` (for team folders at root) and `ListEventHandler::GetSyncToDeviceListCursor`.
**Branch logic in `GetServerFileList`**:
- Team folder at root path (`/`): uses `"list_sync_to_device"`
- Flag at `this+0x71b` set: uses `"query_node"` instead
- Default: uses `"list"`
**Two modes:**
1. **Full listing** (initial sync / no cursor): Omit the `cursor` field. Server returns all children.
```json
→ { "_action": "list_sync_to_device", "list_dir_only": false, "merge_local": false, "include_node_locking": true }
← { "node_list": [...], "cursor": "opaque_token_abc123" }
```
2. **Change detection** (subsequent syncs, requires server build >= 10238): Send the stored cursor. Compare returned cursor — if identical, nothing changed.
```json
→ { "_action": "list_sync_to_device", "cursor": "opaque_token_abc123" }
← { "cursor": "opaque_token_abc123" } ← { "cursor": "opaque_token_def456" } ```
The field name is `"cursor"` (string type). For initial bulk download, simply omit it or send empty string.
### 9.10 Complete File Tree Enumeration Sequence
```
1. Auth: _action="auth" → session + server_id
2. Server info: _action="query_server_info" → database_restore_id
3. List shares: _action="list_team_folder" → view_list[].view_id
4. For each view: _action="list" path="/" → node_list[]
5. For each dir: _action="list" path="/subdir" → recurse
6. For each file: _action="get_file_info" (if full metadata needed)
```
For sync clients, prefer `list_sync_to_device` (no pagination, cursor change detection) over `list` (paginated, no change detection).
---
## 10. Other Protocol Actions
### 10.1 File Management Actions (all use protocol_type = 0x01)
| Action String | SCMD | Purpose |
|---------------|------|---------|
| `download` | SCMD_REQUEST | Download a file |
| `batch_download` | SCMD_REQUEST | Download multiple files as archive |
| `upload` | SCMD_REQUEST | Upload a file |
| `upload_from_dsm` | SCMD_REQUEST | Upload from DSM path |
| `batch_remove` | SCMD_REQUEST | Delete files |
| `batch_move` | SCMD_REQUEST | Move files |
| `batch_copy` | SCMD_REQUEST | Copy files |
| `batch_restore` | SCMD_REQUEST | Restore files from trash |
| `restore` | SCMD_REQUEST | Restore a file version |
| `force_current_version` | SCMD_REQUEST | Force version |
### 10.2 Metadata Actions
| Action String | Purpose |
|---------------|---------|
| `get_file_info` | Get file metadata |
| `update_file_info` | Update file metadata |
| `get_node_info` | Get node info by ID |
| `query_node` | Query node details |
| `get_file_id` | Get file ID from path |
| `list_version` | List file versions |
| `get_thumbnail` | Get file thumbnail |
| `get_photo_metadata` | Get photo EXIF data |
### 10.3 Sharing Actions
| Action String | Purpose |
|---------------|---------|
| `share_link` | Get/set share link |
| `get_link` | Get sharing link |
| `get_advance_sharing` | Get advanced sharing settings |
| `create_advance_sharing` | Create advanced sharing |
| `update_advance_sharing` | Update advanced sharing |
| `delete_advance_sharing` | Remove advanced sharing |
| `list_sharing` | List all shares |
| `get_sharing_permission` | Get sharing permissions |
| `update_sharing_permission` | Update sharing permissions |
| `list_shared_with_me` | List files shared with me |
| `list_shared_with_others` | List files shared by me |
### 10.4 Server Management Actions
| Action String | Purpose |
|---------------|---------|
| `connect` | Connection handshake |
| `encrypt_channel` | SSL upgrade |
| `query_user_info` | Query user information |
| `query_server_info` | Query server information |
| `list_settings` | List server settings |
| `update_settings` | Update settings |
| `list_member_profile` | List team member profiles |
| `get_portal_link` | Get web portal URL |
| `pull_event` | Pull sync events |
| `get_event_count` | Get pending event count |
| `get_max_sync_id` | Get current sync watermark |
### 10.5 Node Locking
| Action String | Purpose |
|---------------|---------|
| `edit_locking_on_node` | Lock/unlock a file |
| `list_node_locking` | List locked files |
| `pull_node_locking_event` | Pull lock change events |
| `request_unlock` | Request file unlock from another user |
| `sync_node_locking` | Sync lock state |
### 10.6 Labels / Stars
| Action String | Purpose |
|---------------|---------|
| `create_label` | Create a label |
| `edit_label_on_node` | Apply label to file |
| `list_label` | List all labels |
| `list_labelled` | List files with label |
| `edit_star_on_node` | Star/unstar a file |
| `list_starred` | List starred files |
---
## 11. Error Handling
### 11.1 Error Response Format
Errors in the modern protocol layer are returned as:
```json
{
"error": {
"code": 4097,
"reason": "Invalid session"
}
}
```
### 11.2 Server Error Code Categories
| 0x1000-0x1FFF | Authentication | -6 |
| 0x2000-0x2FFF | Permission | -6 |
| 0x3000-0x3FFF | Filesystem/path | -18 |
| 0x4000-0x4FFF | Session | -3 |
| 0x5000-0x5FFF | Resource limits | -10 |
| 0x6000-0x6FFF | General | -3 |
| 0x7000-0x7FFF | Feature | -3 |
| 0x8000-0x8FFF | Feature (alt) | -1 |
| 0xD000-0xDFFF | Drive-specific | -1 |
### 11.3 Specific Error Codes
| 0x1002 | Invalid password |
| 0x1003 | User disabled |
| 0x1007 | Password expired |
| 0x100D | OTP required |
| 0x100E | OTP invalid |
| 0x2002 | Access denied |
| 0x3002 | Invalid view ID |
| 0x3003 | Invalid path in view |
| 0x3004 | Name conflict |
| 0x3005 | Path too long |
| 0x3008 | Disk full |
| 0x4001 | Invalid session |
| 0x4002 | Session expired |
| 0x4003 | Session wiped |
| 0x4004 | Version too low |
| 0x7001 | Feature unavailable |
| 0x8001 | Feature unavailable (alt) |
| 0xD001 | Drive-specific error |
| 0xD002 | Drive-specific error (alt) |
---
## 12. Implementation Guide
### 12.1 Minimal Client Sequence
To implement a basic download client:
1. **TCP Connect** to `<server>:6690`
2. **SSL Upgrade**: Send `SCMD_REQUEST_SSL_2` with `encrypt_channel` action → upgrade to TLS
3. **Connect**: Send `SCMD_REQUEST_CONNECT` with `connect` action, session, client info → receive `alive` interval
4. **Download**: Send `SCMD_REQUEST` with `download` action, path, view_id → receive file data
### 12.2 Session Management
- The `session` token is obtained during initial authentication (via the UI/web interface)
- It's stored in `sys.sqlite` → `connection_table.session`
- The `restore_id` identifies the device and is also in `connection_table`
- Sessions persist across restarts
### 12.3 Key Implementation Considerations
1. **Endianness**: All integers are **big-endian** (network byte order) on the wire. Verified by decompiling `Channel::WriteInt<uint32_t>` (at `0x1004ef0d8`) which uses `buf[i] = value >> (8 * (3 - i))` (MSB first), and `DecomposeInt<uint32_t>` (at `0x1004e6ef8`) in PStream which does the same. No `htonl`/`htons` — manual shift-and-mask serialization. The magic `0x25521814` appears on the wire as bytes `[0x25, 0x52, 0x18, 0x14]`.
2. **String encoding**: UTF-8 throughout. PStream strings with `_` prefix have the underscore stripped on wire.
3. **Connection pooling**: The server expects persistent connections with keep-alive
4. **Channel capacity**: The client maintains up to 5 channels in its pool
5. **Worker parallelism**: Up to 6 concurrent sync operations
6. **File Provider pipeline depth**: Apple limits to 3 concurrent downloads (configurable in Info.plist `NSExtensionFileProviderDownloadPipelineDepth`)
7. **Timeout**: Read header timeout is 10 seconds; general timeout is 60 seconds
### 12.4 Client Identification
The `_agent` block must include:
```json
{
"platform": "mac", // or "windows", "linux"
"type": "drive", // "drive" or "drive_backup"
"device_uuid": "<uuid>", // unique device identifier
"restore_id": "<server_ds_id>",
"version": {
"major": 4, "minor": 0, "mini": 0, "build": 17889
}
}
```
---
## 13. Version Compatibility
The protocol version is `0x46` (70 decimal), meaning **major 7, minor 0**. The `ProtoCheck` function validates version compatibility:
```c
int ProtoCheck(int version) {
int major = (version - version % 10) / 10;
if (major > 7) return 3; // too new, incompatible
if (version < 70) return 2; // too old
if (version == 70) return 0; // exact match
return 1; // compatible, newer minor
}
```
This means:
- Version 70 (7.0): exact match, fully compatible
- Versions 71-79 (7.1-7.9): compatible, client newer
- Versions < 70: too old, rejected
- Versions >= 80: too new, rejected (different major)
---
## Appendix A: Source File References
Debug strings in the binary reveal the original source structure:
```
synosyncfolder/lib/protocol/proto-common.cpp — Section encoding/decoding
synosyncfolder/lib/protocol/proto-client.cpp — ProtocolClient (send/recv/connect)
synosyncfolder/lib/protocol/proto-ui.cpp — UI-facing CloudStation protocol
synosyncfolder/lib/stream/stream.cpp — PStream serialization
synosyncfolder/lib/channel/channel.cpp — TCP channel management
synosyncfolder/lib/connection/connection.cpp — Connection pool
synosyncfolder/lib/connection/conn-finder.cpp — Connection discovery (SmartDNS/QuickConnect)
synosyncfolder/daemon/daemon-impl.cpp — Main daemon implementation
synosyncfolder/daemon/worker_mgr.cpp — Worker manager
synosyncfolder/handler/upload-local-handler.cpp — Upload handler
synosyncfolder/handler/download-remote-handler.cpp — Download handler
synosyncfolder/handler/handler-helper.cpp — Handler utilities
```
## Appendix B: Connection Discovery
The client supports multiple connection methods, tried in order:
1. **Direct connection**: Connect to `server_ip:6690` directly
2. **SmartDNS LAN**: Resolve hostname via local DNS, try IPv4 and IPv6
3. **QuickConnect**: Use Synology's QuickConnect relay service to find the server
4. **Relay tunnel**: Fall back to Synology relay servers for NAT traversal
5. **Punch daemon**: UDP hole-punching via `punchd` (localhost port, configurable)
The connection finder (`conn-finder.cpp`) tries these in stages and uses the first successful one ("early-stopping" on success).