Skip to content

Instantly share code, notes, and snippets.

@m1guelpf
Created March 25, 2026 13:46
Show Gist options
  • Select an option

  • Save m1guelpf/f807fea7d64b461cff455de71d76c9b2 to your computer and use it in GitHub Desktop.

Select an option

Save m1guelpf/f807fea7d64b461cff455de71d76c9b2 to your computer and use it in GitHub Desktop.
Reverse engineering of the Synology Drive sync protocol (Sproto)

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:

Layer Name Magic Used By Serialization
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):

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):

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)

ID Name Direction Purpose
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]

// 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:

ID Name Wire Type Size Description
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:

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
0 Null (empty)
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

Tag Byte Type Wire Encoding
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:

{
  "@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"):

→ {
    "_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:

Field Type Description
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: GetResumeDownloadTokenContinueResumeDownload loop.

5.5 Download Response Structure

The download response PObject contains metadata at the top level and file content nested under "file":

← {
    "@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:

{
  "_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:

{
  "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_ids 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:

{
  "_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:

{
  "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 == trueFileRemoveEvent (file deleted)
  • is_removed == falseFileAddEvent (file created or modified)

Renames and moves are indicated by a rename sub-object within the event.

Event PObject fields:

Field Type Description
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:

Case Condition Action
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)

→ {
    "_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)

→ { "_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:

→ {
    "_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:

→ {
    "_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:

→ {
    "_action": "list",
    "view_id": 1,
    "node_id": 12345,
    "list_dir_only": false,
    "merge_local": false
  }

Response (node_list array of Node objects):

← {
    "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:

Field Type Description
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:

→ {
    "_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")

→ {
    "_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:

→ { "_action": "query_node", "view_id": 1, "path": "/Documents" }
← { "node": { "node_id": 100, "sync_id": 50000, "name": "Documents", ... } }

9.6 SearchFile (_action = "search_file")

→ {
    "_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:

Field Type Description
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:

Field Type Default Description
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.
→ { "_action": "list_sync_to_device", "list_dir_only": false, "merge_local": false, "include_node_locking": true }
← { "node_list": [...], "cursor": "opaque_token_abc123" }
  1. Change detection (subsequent syncs, requires server build >= 10238): Send the stored cursor. Compare returned cursor — if identical, nothing changed.
→ { "_action": "list_sync_to_device", "cursor": "opaque_token_abc123" }
← { "cursor": "opaque_token_abc123" }   // unchanged — skip listing
← { "cursor": "opaque_token_def456" }   // changed — do a full listing

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:

{
  "error": {
    "code": 4097,
    "reason": "Invalid session"
  }
}

11.2 Server Error Code Categories

Range Category Client Error
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

Server Code Meaning
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.sqliteconnection_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:

{
  "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:

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment