18 KiB
FastSync
A high-performance file synchronization system with SSH and TCP transport, TLS encryption, streaming zstd compression, multithreaded transfer, incremental sync, metadata preservation, and rsync-compatible CLI flags.
Technical Overview
- Dual transport: custom TCP client-server or SSH subprocess (rsync-style
user@host:/path) - TLS encryption: OpenSSL-based TLS 1.2+ for encrypted TCP connections with optional CA verification
- Chunked file transfer: files grouped into configurable-size chunks (default ~10 MB)
- Streaming zstd compression (levels 1–22) using
ZSTD_compressStream2 - Multithreading: producer-consumer pipeline with thread-safe queues (scanner → loader → sender)
- Incremental sync: skip files unchanged since last transfer (compares size + mtime)
- Batch incremental: send incremental checks in batched groups for reduced round-trips
- Metadata preservation:
mode,uid,gid,mtimerestored on disk when enabled sendfile()zero-copy on TCP (~2× faster on loopback)- SSH ControlMaster for connection reuse across repeated invocations
- Bandwidth limiting: token-bucket throttling (
--bwlimit) --delete: receiver removes files not present in sender manifest--exclude/--include: glob-pattern filename filtering- Path traversal protection:
..sequences in file paths are rejected automatically - Connection limits: server enforces maximum concurrent connections (default 100)
- Keep-alive: periodic
STATUS_KEEPALIVEmessages detect stalled connections - Abort handling:
SIGINTsendsSTATUS_ABORTfor clean server-side teardown - Atomic writes: received files are written to a temporary name then atomically renamed
- Backup mode:
--backuppreserves overwritten files with optional--backup-dir - Log file:
--log-fileredirects log output to a file instead of stderr - Transfer statistics:
--statsprints summary of transferred bytes, files, and timing
System Architecture
Client
- Recursively scans source directories (BFS), supports exclude and include patterns
- Groups files into chunks (configurable size)
- Streaming zstd compression with configurable level
- Chunk serialization (compact binary format) or per-file transfer
- Incremental transfer: sends file metadata to server, skips unchanged files
- Batch incremental: groups incremental checks to minimize round-trips
- Manifests all sent paths when
--deleteis active - Sends via TCP
sendfile()or SSH pipe - Optional progress display with throughput
- Bandwidth limiting via token-bucket algorithm
- Configurable I/O and connection timeouts (
--timeout,--contimeout) - Backup overwritten files (
--backup) with optional directory (--backup-dir) - Transfer statistics summary (
--stats) - Maximum directory depth control (
--max-depth) - Log file output (
--log-file) - Exclude patterns from file (
--exclude-from)
Server
- TCP mode: listens on configurable port (default 8080); SSH mode: runs via
--stdio - TLS mode: wraps TCP connections with OpenSSL with optional CA verification
- Receives and reassembles files
- Decompresses (streaming zstd), deserializes, restores metadata
- Handles incremental checks: compares size + mtime against destination files
- Handles batch incremental checks for reduced round-trips
- Processes
STATUS_MANIFESTfor--delete: walks destination tree, removes extras - Per-connection concurrency via
fork()with configurable connection limit (default 100) - Thread pool for parallel processing
- Atomic writes: files written to
.tmppath then atomically renamed on success - Abort handling: cleanly shuts down on
STATUS_ABORTfrom client - Path traversal protection: rejects file paths containing
..
Protocol Details
Status Codes
| Code | Meaning |
|---|---|
STATUS_OK |
Operation successful |
STATUS_ERROR |
Error occurred |
STATUS_FINISHED |
Transfer complete |
STATUS_NEXT |
Ready for next file (per-file mode) |
STATUS_CHUNK |
Following data is a serialized chunk |
STATUS_MANIFEST |
Following data is a file manifest (for --delete) |
STATUS_CHECK |
Incremental check: client sends file path + size + mtime, server responds with OK (skip) or NEXT (send) |
STATUS_CHECK_BATCH |
Batch incremental check: multiple file checks sent in one message |
STATUS_KEEPALIVE |
Keep-alive heartbeat to detect stalled connections |
STATUS_ABORT |
Abort signal: client interrupts, server cleans up and exits |
STATUS_DELTA_SIGNATURE |
Delta sync: following data is a file signature (rsync-style rolling hash) |
STATUS_DELTA_DATA |
Delta sync: following data is a delta patch for a file |
Wire Format — Metadata
When use_metadata is enabled (-M), each file entry carries a 4-byte present flag followed by five fields (mode, uid, gid, mtime_sec, mtime_nsec). When disabled globally, no metadata bytes are sent — zero wire overhead.
Transfer Flow
Config → (STATUS_NEXT | STATUS_CHUNK | STATUS_CHECK | STATUS_CHECK_BATCH)* → [STATUS_MANIFEST] → STATUS_FINISHED → STATUS_OK
Keep-alive (STATUS_KEEPALIVE) may be sent at any point during the transfer. The receiver resets its inactivity timer on receipt. If no data arrives within the receive timeout, the connection is aborted.
Abort (STATUS_ABORT) may be sent at any point. On receipt the server cleans up temporary files and exits the child process.
Protocol Version
1.3.0 — server and client must match. Mismatch results in STATUS_ERROR.
Command-Line Arguments
Client
| Argument | Description |
|---|---|
| Positional | <source> <dest> — automatic SSH detection if dest contains : |
-c [level] |
Compression with optional level (1–22, default 5) |
-z [level] |
Alias for -c |
-a, --archive |
Archive mode: enables -c -m -M (no -s) |
-m |
Multithreading mode |
-s |
Chunk serialization (batch all files per chunk) |
-f, --sendfile |
Sendfile zero-copy. Incompatible with -c / -s. TCP only. |
-M, --preserve |
Preserve file metadata (mode, uid, gid, mtime) |
-n, --dry-run |
Scan and print what would be transferred |
-p <port> |
SSH port (default: 22) |
-v, --verbose |
Enable debug logging |
--progress |
Show real-time transfer speed |
--delete |
Delete files on receiver not present in source |
--exclude <pattern> |
Exclude files matching glob pattern (repeatable) |
--exclude-from <file> |
Read exclude patterns from a file (one per line) |
--include <pattern> |
Only transfer files matching glob pattern (repeatable, whitelist) |
--max-size <n> |
Skip files larger than n bytes |
--min-size <n> |
Skip files smaller than n bytes |
--incremental |
Skip files unchanged since last transfer (size + mtime). Auto-enables --preserve. Incompatible with -s. |
--bwlimit <KB/s> |
Bandwidth limit in kilobytes per second |
--chunk-size <n> |
Chunk size in bytes (default: 10485760) |
--timeout <sec> |
I/O timeout in seconds (default: 30) |
--contimeout <sec> |
Connection timeout in seconds (default: 10) |
--backup |
Backup existing destination files before overwriting |
--backup-dir <dir> |
Target directory for backups (requires --backup) |
--stats |
Print transfer statistics at end (bytes, files, timing) |
--max-depth <n> |
Maximum directory depth to recurse (0 = unlimited, default: 0) |
--log-file <path> |
Write log messages to file instead of stderr |
--source-dir <path> |
Source directory (overrides FASTSYNC_SOURCE_DIR) |
--dest-dir <path> |
Server destination directory (overrides FASTSYNC_DEST_DIR) |
--save-to-disk |
Write received files to disk |
--server-host <ip> |
Server IP address (default: 127.0.0.1) |
--server-port <n> |
Server port (default: 8080) |
--tls |
Enable TLS encryption |
--cert <path> |
TLS certificate file (PEM) |
--key <path> |
TLS private key file (PEM) |
--ca <path> |
TLS CA certificate file for verification (PEM) |
Server
| Argument | Description |
|---|---|
--stdio |
Run in stdio mode (for SSH transport; single connection then exits) |
-p <port> |
TCP listen port (default: 8080, range: 1–65535) |
--tls |
Enable TLS encryption |
--cert <path> |
TLS certificate file (PEM) |
--key <path> |
TLS private key file (PEM) |
--ca <path> |
TLS CA certificate file for verification (PEM) |
-v, --verbose |
Enable debug logging |
--help |
Show help |
Environment Variables
| Variable | Default | Description |
|---|---|---|
FASTSYNC_SOURCE_DIR |
— | Source directory fallback |
FASTSYNC_DEST_DIR |
— | Destination directory fallback |
FASTSYNC_SAVE_TO_DISK |
false |
Disk persistence fallback |
FASTSYNC_SSH_PORT |
22 |
Default SSH port |
FASTSYNC_SERVER_HOST |
127.0.0.1 |
Default server host |
FASTSYNC_SERVER_PORT |
8080 |
Default server port |
FASTSYNC_TLS_CERT |
— | Default TLS certificate path |
FASTSYNC_TLS_KEY |
— | Default TLS private key path |
FASTSYNC_TLS_CA |
— | Default TLS CA certificate path |
Implementation Details
Data Structures
- Chunk — collection of files (~10 MB total by default)
- File — path, content (
Data), optionalFileMetadatapointer - FileMetadata —
mode,uid,gid,mtime_sec,mtime_nsec - Config — runtime parameters (transported over wire, TLS settings excluded). Includes
timeout,contimeout,backup,backup_dir,stats, andmax_depth. - Queue — thread-safe bounded queue with condition variables
- DirectoryScanner — recursive BFS traversal with exclude and include pattern support, max-depth enforcement
Key Algorithms
- File scanning — BFS directory traversal; entries matched against exclude and include patterns, max-depth enforced
- Chunking — files accumulated until
chunk_sizethreshold, then flushed - Compression — streaming zstd via
ZSTD_compressStream2/ZSTD_decompressStream - Network protocol — status-code-driven exchange with metadata packing, keep-alive, and abort support
- Incremental check — client sends
STATUS_CHECK+ path + size + mtime; server compares against destination. Can be batched viaSTATUS_CHECK_BATCHfor reduced round-trips. - Bandwidth limiting — token-bucket algorithm with
nanosleepthrottling on 64 KB write chunks - Metadata restoration —
chmod(),chown(),utimensat()on the receiving side --delete— sender tracks all sent paths; receiver walks destination tree and removes unlisted files/directories- SSH transport —
socketpair()+fork()+execvp("ssh", ...)withControlMasterand port support - TLS transport — OpenSSL
SSL_CTXwith TLS 1.2 minimum, optional CA verification, transparentSSL_read/SSL_writeviaio_set_ssl() - Path traversal protection —
has_path_traversal()rejects any file path containing..components, preventing directory escape attacks - Connection limiting — server tracks active connections and rejects new ones beyond
max_connections(default 100) - Keep-alive — idle connections receive periodic
STATUS_KEEPALIVEto detect half-open TCP connections - Abort handling —
SIGINTsets an abort flag; the next protocol operation sendsSTATUS_ABORTfor clean server cleanup - Atomic writes — files are written to a
.tmpsuffix then atomically renamed viarename(), preventing partial files - Backup — before overwriting, existing files are moved to
--backup-dir(or same directory with~suffix) preserving the original
Security Features
Path Traversal Protection
All received file paths are validated by has_path_traversal() before any disk operation. Any path containing .. components is rejected with STATUS_ERROR, preventing directory escape attacks.
TLS Certificate Verification
When --ca is provided, the server performs mutual TLS verification (SSL_VERIFY_PEER with depth 4). Without --ca, TLS is still encrypted but peer certificates are not verified.
Connection Limits
The server enforces a maximum of 100 concurrent connections (configurable via max_connections in Server). When the limit is reached, new connections are immediately rejected and closed.
Abort Handling
If the client receives SIGINT (Ctrl+C) during a transfer, it sends STATUS_ABORT to the server. The server then cleans up temporary files and exits the child process, preventing incomplete files from remaining on disk.
Atomic Writes
Received files are written to a temporary path (suffixed with .tmp) and then atomically renamed to the final filename via rename(). This prevents partial or corrupted files from appearing at the destination if the transfer is interrupted.
Build Requirements
- C11 compiler
- CMake >= 3.22
- zstd library
- OpenSSL (development headers and libraries)
- pthreads
- SSH client (for SSH transport mode only)
Installing Dependencies
Ubuntu/Debian:
sudo apt install cmake build-essential libzstd-dev libssl-dev openssh-client
Nix:
nix-shell # provides zstd, openssl, cmake, gcc
Building
cmake -B build -S . && cmake --build build -j$(nproc)
Running
Server (TCP mode)
./build/server
Server with TLS
./build/server --tls --cert server.pem --key server-key.pem
Server via SSH
Place the fastsync-server binary in the remote $PATH. The client runs ssh user@host fastsync-server --stdio automatically when an SSH-style destination is given.
Client — SSH (rsync-style)
./build/client /path/to/send user@host:/path/to/receive
Client — TCP
./build/client --source-dir /path/to/send --dest-dir /path/to/receive --save-to-disk
Client — TCP with TLS
./build/client --tls --cert client.pem --key client-key.pem --ca ca.pem \
--source-dir /path/to/send --dest-dir /path/to/receive --save-to-disk
Common Options
# Archive mode (compression + multithreading + metadata)
./build/client -a /path/to/send user@host:/path
# Dry run
./build/client -n /path/to/send /path/to/receive
# With progress and custom chunk size
./build/client --progress --chunk-size 2097152 /src user@host:/dst
# Exclude temporary files + delete extras on receiver
./build/client --exclude "*.tmp" --exclude "*.o" --delete /src user@host:/dst
# Incremental sync (skip unchanged files)
./build/client --incremental /src user@host:/dst
# Bandwidth limit to 1 MB/s
./build/client --bwlimit 1024 /src user@host:/dst
# With timeouts and stats
./build/client --timeout 60 --contimeout 15 --stats /src user@host:/dst
# Backup overwritten files to a directory
./build/client --backup --backup-dir /backups /src user@host:/dst
# Exclude patterns from file, limit depth
./build/client --exclude-from ignore.txt --max-depth 3 /src user@host:/dst
# Log to file
./build/client --log-file /tmp/fastsync.log /src user@host:/dst
# All features
./build/client -a --progress --chunk-size 5242880 --exclude "*.log" --delete /src /dst
Testing
# Unit tests (18 suites — array_list, chunk, compression, config, data, delta, file, glob,
# metadata, property, protocol, queue, robustness, scanner,
# shared_utils, stress, transport_tcp, transport_ssh, transport_tls)
./build/tests
# Integration + benchmark suite
python3 test.py
The benchmark prints throughput metrics, best configuration, and speedup vs rsync.
Performance Considerations
- Chunk size (~10 MB default) balances memory and transfer efficiency
- Compression level trades CPU for bandwidth
sendfile()bypasses userspace — ~2× faster on localhost for large files- Multithreading scales with core count and uses memory-based pipeline sizing
- Metadata transfer adds negligible overhead (~24 bytes per file when enabled)
- SSH socketpair buffer set to 1 MB for improved pipe throughput
- SSH ControlMaster reuses connections across repeated invocations
- Incremental sync eliminates redundant transfers entirely
- Batch incremental reduces round-trips by grouping multiple checks into one message
- Bandwidth limiting uses token-bucket with nanosleep for accurate throttling
- Atomic writes add a single
rename()per file — negligible overhead - Path traversal check is O(n) in path length with negligible cost
Benchmark Results
25 MB of mixed file sizes over localhost with disk I/O throttled (reads ≤ 15 MB/s, writes ≤ 10 MB/s) and network emulation via tc netem. Each test was run 3×; the median is reported below.
LAN (1000 Mbit, 20 ms ±1 ms, 0.1% loss)
| Configuration | Time | vs rsync (archive) | vs rsync (compress) |
|---|---|---|---|
Best: -m -c |
0.20 s | 11.2× faster | 3.6× faster |
Compression (-c) |
0.31 s | 7.3× faster | 2.3× faster |
| Standard | 1.27 s | 1.8× faster | — |
| rsync (archive) | 2.27 s | — | — |
| rsync (archive + compress) | 0.72 s | — | — |
WAN (100 Mbit, 50 ms ±10 ms, 1% loss)
| Configuration | Time | vs rsync (archive) | vs rsync (compress) |
|---|---|---|---|
Best: -m -c |
0.39 s | 44.8× faster | 3.8× faster |
Compression (-c) |
0.64 s | 27.3× faster | 2.3× faster |
| Standard | 7.12 s | 2.4× faster | — |
| rsync (archive) | 17.44 s | — | — |
| rsync (archive + compress) | 1.47 s | — | — |
Compression reduces the data on the wire enough that the transfer becomes latency-bound rather than bandwidth-bound. On WAN, the best configuration runs 10.8× faster than the theoretical limit for uncompressed data, since zstd shrinks the 25 MB payload to a fraction of its original size over the wire.