Private, peer-to-peer file synchronization over your local network.
FerriSync is an open-source, LAN-first synchronization system that lets your devices synchronize folders directly — without cloud storage, accounts, or a central server.
🚧 Developer Preview
FerriSync is in early development and intended for developers and technical early adopters. APIs, protocol, configuration, and synchronization behavior may change.
Most file synchronization services rely on a cloud service as the middleman:
Device A → Cloud → Device B
FerriSync takes a different approach:
Device A ←────────→ Device B
LAN
Your devices discover and authenticate each other, then synchronize directly over the local network.
No cloud. No account. No central synchronization server.
| Feature | Status |
|---|---|
| LAN device discovery (mDNS) | ✅ |
| Device pairing (TOFU + consent) | ✅ |
| Encrypted transfers (TLS 1.3) | ✅ |
| Bidirectional folder sync | ✅ |
| Conflict handling (.bak) | ✅ |
| File watching | ✅ |
| CLI | ✅ |
| Interactive shell (REPL) | ✅ |
| Android client (Flutter) | 🚧 usable |
| Device rename across frontends | ✅ |
| Auto re-pair on startup | ✅ |
| Session history | ✅ |
| Packaged releases / installers | ❌ |
| Remote (non-LAN) sync | ❌ |
| iOS client | ❌ |
| Production-ready UX | 🚧 |
This table is a snapshot of reality, not marketing.
There are already excellent synchronization tools, including Syncthing. FerriSync explores a different approach to the synchronization experience, with a strong focus on LAN-first operation and simple device discovery and pairing:
- LAN-first operation — designed around the local network, not the internet
- Simple discovery and pairing — discover, tap, confirm
- A reusable core —
ferrisync-corebehind CLI, REPL, and mobile clients - Developer-friendly architecture — small crates, black-box test scripts
Honest shortcomings today: no packaged releases, no NAT traversal / remote sync, single-platform mobile client, evolving UX.
- LAN discovery
- Device pairing (TOFU + consent flow)
- Bidirectional sync with conflict handling
- Android prototype
- Packaged releases & installers
- Improved conflict-resolution UX
- Improved mobile UX
- Headless daemon with system tray
- Remote synchronization (QUIC transport)
- iOS client
- Desktop GUI
- Version vectors · block-level incremental sync
The fastest way to evaluate FerriSync is to run it on two machines connected to the same LAN.
FerriSync currently has no packaged releases — you need to build from source.
Requirements: Rust 1.80+ (rustup), optionally Flutter for the Android client.
git clone https://github.com/DavidNeurieder/FerriSync.git
cd FerriSync
cargo build --release# Interactive shell (REPL) — no argument needed
./target/release/ferrisync
Machine A:
serve ~/Photos # host a folder (default port 9847)
Machine B:
discover # list devices — shows each device's name
pair # pick device # — interactively, no address needed
sync ~/PhotosCopy --device "Desktop" # one-shot bidirectional sync by name
watch ~/PhotosCopy --device "Desktop" # ...or keep syncing on every change
Devices are shown and addressed by their display name — IP addresses and
internal ids never appear in the UI. On the same LAN, pair (no arguments)
scans and lets you pick a device by number, so you don't need its address. Once
paired, sync and watch use --device <name>.
Advanced networking: if discovery is blocked by your network, you can pair/sync by IP directly from the CLI:
ferrisync devices pair 192.168.1.42 [--port 9847], thensync ~/PhotosCopy --device "Desktop".
Build and install the Android client (see Flutter), then:
- Start
serveon the PC (or open the folder in the phone app) - In the app: discover → tap pair → approve the request on the PC
- Pick folders on either side and sync
ferrisync with no arguments is a persistent shell — servers, watches, and
session history live across commands:
serve ~/Documents [--port 7000] host a folder for pairing + sync
serves / unserve <id> list / stop background servers
pendings devices waiting for pairing approval
confirm <n> / deny <n> resolve held pairing requests
discover [seconds] scan the LAN for peers
pair <ip> [--port <p>] initiate pairing
sync [folder --device <dev>] [--wait secs]
watch <folder> --device <dev> sync on every change (background)
watches / unwatch <id>
devices / folders list devices (with presence) / folders (with health)
activity recent sessions + file changes
conflicts unresolved conflict backups
doctor on-device diagnostics
sessions recent sync sessions (both directions)
rename <name> change this device's network name
status paired devices + folders (presence + health)
The shell prints a folder-centric startup dashboard — this device, the "how is everything?" headline, then devices and folders with presence/health:
FerriSync · Mr Desktop
✓ Everything is synced
Devices
● Pixel 9 · connected
● Laptop · connected
Folders
✓ ~/Photos ↔ Pixel 9 — healthy, last sync: 2 minutes ago
✓ ~/Documents ↔ Laptop — healthy, last sync: 14 minutes ago
cargo run -p ferrisync -- status
cargo run -p ferrisync -- status --verbose # ids + absolute timestamps
cargo run -p ferrisync -- status --json # machine-readable health
cargo run -p ferrisync -- devices # paired devices + presence
cargo run -p ferrisync -- devices discover # scan the LAN
cargo run -p ferrisync -- devices pair # interactive discovery+pair
cargo run -p ferrisync -- devices pair 192.168.1.42 --port 9847
cargo run -p ferrisync -- devices rename "Pixel 9" "PixelX"
cargo run -p ferrisync -- folders add ~/Docs --device "Pixel 9"
cargo run -p ferrisync -- folders remove ~/Docs
cargo run -p ferrisync -- activity # recent sessions + changes
cargo run -p ferrisync -- conflicts # unresolved conflicts
cargo run -p ferrisync -- conflict-resolve notes.txt --keep this
cargo run -p ferrisync -- doctor # on-device diagnostics
cargo run -p ferrisync -- doctor --explain firewall
cargo run -p ferrisync -- sync ~/Documents --device "Pixel 9" --wait 30
cargo run -p ferrisync -- sync ~/Documents --device 192.168.1.42:9847 # by IP
cargo run -p ferrisync -- serve ~/Documents --auto-accept
cargo run -p ferrisync -- rename "Mr Desktop"Every command also accepts ferrisync --json <command> (or places --json
after the command) to emit parseable output instead of human text.
Running ferrisync (no subcommand) with stdin piped feeds commands to the
same shell and exits on EOF — handy for scripting.
All frontends share the same derived status language. A device presence is:
- connected — seen within the last 5 minutes
- recently seen — seen within the last 24 hours
- offline — silent for over a day (or never seen)
A configured folder gets a combined health:
- healthy — peer connected and the folder has synced before
- syncing — transferring right now
- waiting — peer is around but not connected, or never synced yet
- offline — peer fell off before the first sync
- conflict — has unresolved conflict backups
- error — the last sync attempt failed
- not configured — hosted locally, not yet attached to a remote
Folders that are conflict, error, or offline appear under ATTENTION in
status output.
Known devices connect instantly (TOFU). Unknown devices are held for approval:
- REPL:
[serve:...] PAIRING REQUESTappears inline; answery/n, or usependings+confirm <n>/deny <n> - CLI serve: prompts
Allow '<name>' to pair? [y/N];--auto-acceptskips asking
Warning:
--auto-accept(also triggered implicitly by runningservewithout a TTY) trusts any device that can reach this host and grants it read/write access to the folder. Only use it on trusted, private networks — never on the public internet or untrusted Wi-Fi.
Device pairing is the single approval gate. Once a device is trusted, its folder-pair requests are auto-approved from that point on — no second per-folder consent prompt. The paired folder records the remote folder's path, so the frontend can show exactly which folder on the peer it is shared with.
Denied devices stay rejected until that server restarts.
When the host restarts, previously paired devices reconnect automatically. If you'd rather re-establish pairing eagerly (instead of waiting for mDNS discovery), enable the auto re-pair setting — on startup the device dials each known peer at its last-seen address and re-sends the pairing handshake.
Every frontend shares one persisted name (fallback: OS hostname). Renaming on any side updates advertisements and pairing immediately — running folder servers restart under the new name automatically.
┌─────────────────┐
│ ferrisync-core │
│ engine · crypto│
│ protocol · io │
└────────┬────────┘
┌───────────────┼───────────────┐
│ │ │
REPL CLI Flutter
(interactive) (commands) (flutter_rust_bridge)
One reusable Rust core and one ferrisync binary; every interface is a thin
presentation layer over the same synchronization engine.
- Discovery — mDNS/DNS-SD advertisement and browsing on the LAN
- Authentication — trust on first use: certificates are generated locally, fingerprints pinned at pairing; unknown peers require operator consent
- Transport — TLS 1.3 over TCP (default port 9847)
- Integrity & comparison — BLAKE3 hashes detect content differences;
conflicts currently use newer-mtime-wins with hash-based tie-breaking,
preserving the overwritten side as
.bak - Synchronization — disk-index based comparison; missing files always pull, divergent files transfer exactly once
FerriSync is designed to protect transfers from network eavesdropping and unauthorized peers: TLS 1.3 on every connection, TOFU certificate pinning at pairing, and explicit operator consent for unknown devices.
It currently assumes the local device and operating system are trusted. In particular, FerriSync does not yet protect against:
- a paired device that is itself compromised
- malicious file content received from a trusted peer (no malware scanning)
- other software running on the same machine reading synced data
FerriSync is currently for:
- Rust developers and open-source contributors
- Homelab / self-hosting enthusiasts
- Privacy-focused developers
- People interested in peer-to-peer networking and sync systems
FerriSync is not yet for:
- Anyone expecting Dropbox-level polish
- Production-critical data
- People who don't want to build early-stage software from source
We're especially interested in developers who can test FerriSync across multiple physical devices and provide feedback on the synchronization model, the pairing flow, and failure cases.
cargo testSystem tests over real network sockets live in scripts/:
| Script | What it exercises |
|---|---|
test_vbox_repl_pairing.sh |
REPL↔REPL over VirtualBox host-only NIC: consent pop-ups both directions, bidirectional sync |
test_lan_pairing_flow.sh |
Host REPL ↔ Android emulator over LAN/TAP (no adb forwards) |
test_linux_flutter_sync.sh |
Host REPL ↔ Linux Flutter app: device pairing, folder pairing, bidirectional Linux↔Linux sync |
test_linux_flutter_auto_repair.sh |
Auto re-pair at startup: app re-pairs a known device against a running host (port 19898) |
test_linux_flutter_repl_folder_pair.sh |
Linux Flutter app ↔ REPL/CLI owner: device pairing + folder pairing with remote folder display (port 19897) |
test_linux_flutter_two_folder_pick.sh |
Two-folder pick: a peer advertising multiple published folders returns both (port 19899) |
test_android_cli_sync.sh |
Android CLI ↔ host: pairing and sync over the emulator |
test_android_flutter_sync.sh |
Android app ↔ host: pairing and sync over the emulator |
With a default-deny incoming policy, every device→host connection (pairing, sync, serving) is silently dropped. Host-initiated connections work regardless, so failures show up as the peer hanging in its retry loop while nothing arrives.
The tests use two virtual networks; allow them explicitly:
# VirtualBox REPL<->REPL tests (host-only adapter vboxnet0, 192.168.56.0/24)
sudo ufw allow in on vboxnet0 comment 'ferrisync vbox tests'
# Android emulator TAP tests (tap0, 192.168.179.0/24)
sudo ufw allow in on tap0 comment 'ferrisync emu tests'Equivalent subnet form: sudo ufw allow from 192.168.56.0/24. To tighten
further, restrict to the sync port range instead of whole interfaces:
sudo ufw allow in on vboxnet0 proto tcp to any port 19880:20000mDNS needs no extra rule — UFW's default ruleset already permits UDP 5353.
Inspect or undo:
sudo ufw status numbered
sudo ufw delete <n>cd ferrisync-flutter
flutter pub get
flutter_rust_bridge_codegen generate
flutter runSee ferrisync-flutter/ for details.
make test-android-flutter builds an x86_64 emulator APK that will crash on an
arm64 phone (MissingLibraryException: libflutter.so). For real devices:
make install-phone # universal APK (x86_64 + arm64) + adb install
# or just build:
make build-android-apk-universalData directory: ~/.local/share/ferrisync/ (Linux)
metadata.db— SQLite database (devices, folders, file index, session history)identity.crt/identity.key— persisted TLS identity (TOFU pairing)device.name— user-chosen device name (optional; hostname fallback)- File chunks capped at 64 KB
FerriSync is early and feedback is welcome.
Good places to start:
- Try the developer preview between two real devices
- Report bugs and rough edges
- Test synchronization across platforms
- Improve documentation
- Work on the CLI/REPL or the Flutter client
- Improve the synchronization core
Issues and discussions are welcome.
AGPL-3.0