AeroRsync - Native rsync Protocol in Pure Rust โ
AeroRsync is AeroFTP's implementation of the rsync wire protocol 31, written from scratch in Rust. It powers AeroFTP's delta sync path on SFTP without requiring an external rsync binary on the client - not on Linux, not on macOS, not on Windows. It is the engine behind the Delta Sync feature you see in AeroSync.
Status: Production since v3.6.1, the first cross-OS file manager to speak native rsync protocol 31 in pure Rust. No
rsync.exebundle. No WSL requirement. Verified byte-identical against a stockrsync --serverin CI.
AeroRsync does not replace rsync. It talks to rsync: on the far end there is still a standard rsync --server. What it removes is the dependency on the rsync binary on your machine.
Page last verified against main 4ab49265 on 25 September 2026. The July timings below are unchanged: they are the last loopback comparison, not a new run.
Why It Exists โ
Before AeroRsync, AeroFTP shelled out to the local rsync binary over an SSH channel. That worked on Linux and macOS, where rsync is universally installed, and it left two bad options on Windows:
- Bundle a GPL
rsync.exe- license complexity, larger download, manual updates every release. - Require WSL - a barrier most Windows users reject outright.
Either would have permanently split AeroFTP into a first-class Unix product and a second-class Windows one. We chose neither and wrote a native rsync client in Rust instead.
Feature Matrix vs rsync โ
๐ข present ยท ๐ก partial or conditional ยท ๐ด not present
Where a row is ๐ด for AeroRsync, the Why column says whether that is a deliberate boundary or unfinished work. Conflating those two is the fastest way to misread this table.
Protocol and wire โ
| Capability | rsync | AeroRsync | Why |
|---|---|---|---|
| Wire protocol 31 | ๐ข | ๐ข | Speaks bytes-on-wire to stock rsync --server |
| Protocols 27-30 | ๐ข | ๐ด | Deliberate: protocol-31-only. The native path refuses the peer before any data moves and sends the file as plain SFTP, with no delta. Delta needs mode classic, which runs the stock binary and exists on Unix only |
| Rolling Adler-32 signatures | ๐ข | ๐ข | Identical algorithm |
| Block strong hash: md5 | ๐ข | ๐ข | Live-verified |
| Block strong hash: md4 | ๐ข | ๐ข | Live-verified. Seeding mirrors rsync 3.2.7 checksum.c |
| Block strong hash: sha1 | ๐ก | ๐ข | AeroRsync implements it; rsync dropped sha1 after 3.2.7, so a 3.4.1 peer refuses it |
| Block strong hash: xxh64 / xxh3 / xxh128 | ๐ข | ๐ข | Live-verified, all three |
| Whole-file checksum trailer verify | ๐ข | ๐ข | Computed and verified in both directions |
| Literal compression: zstd | ๐ข | ๐ข | Tokens compatible with token.c::send_zstd_token. Only rsync 3.2.0 and later offer it |
| Literal compression: zlibx | ๐ข | ๐ข | Raw deflate, one session-wide stream, Z_SYNC_FLUSH per record. Pinned against captured rsync 3.1.3 bytes, which is the pre-zstd vintage that NAS firmware ships |
| Literal compression: lz4 | ๐ข | ๐ด | No codec here, and not advertised, which is the point: see the note below |
| Literal compression: zlib | ๐ข | ๐ด | Declined structurally, not for effort. zlib feeds matched block data through the compressor history on both ends (token.c::see_deflate_token), so decoding it needs inflateIncomp, a function rsync patches into the zlib it bundles and which exists in neither the system zlib nor any Rust crate. Upstream split zlibx out precisely for this reason |
| Negotiates only codecs it can drive | ๐ข | ๐ข | True for rsync by construction: it advertises the list it was compiled with. True for AeroRsync only since it stopped copying that list verbatim, which is the note below |
| Multiplexed I/O framing | ๐ข | ๐ข |
Transport and session โ
| Capability | rsync | AeroRsync | Why |
|---|---|---|---|
| SSH remote-shell transport | ๐ข | ๐ข | |
Daemon mode rsync:// | ๐ข | ๐ด | Deliberate: a different transport, not a missing piece of a wire-31 client |
| Local pipe / batch files | ๐ข | ๐ด | Deliberate |
| SSH key auth | ๐ข | ๐ข | |
| SSH password auth | ๐ข | ๐ข | With cross-leg host-key pinning |
| SSH agent auth | ๐ข | ๐ก | Unix via SSH_AUTH_SOCK; Windows Pageant is a follow-up |
| GSSAPI / keyboard-interactive | ๐ข | ๐ด | Deliberate |
| Mandatory host-key pinning | ๐ด | ๐ข | Part of the module flow, not left to known_hosts |
| One session for many files | ๐ข | ๐ข | AerorsyncBatch reuses one SSH session for N files |
| Recursive tree in one invocation | ๐ข | ๐ด | Unfinished work. AeroSync owns the tree today; tracked as capstone Y-RSC.7 |
Metadata preserved โ
| Capability | rsync | AeroRsync | Why |
|---|---|---|---|
| Modification time | ๐ข | ๐ข | |
| Permissions | ๐ข | ๐ข | Applied at atomic finalize |
| Symlinks | ๐ข | ๐ข | Unix, both directions. Never followed; target sanitised against a hostile server |
user.* extended attributes (-X) | ๐ข | ๐ข | Unix, single-file and batch. Windows off: no direct user.* analogue |
| Other xattr namespaces | ๐ข | ๐ด | Deliberate: user.* only |
POSIX ACL (-A) | ๐ข | ๐ข | Linux, single file, both directions, opt-in. Named user and an explicit mask are covered live. Directory default ACL waits on recursive entries. Windows and macOS stay off |
Owner / group (-o / -g) | ๐ข | ๐ด | Unfinished work. uid/gid already travel on the wire; nothing applies them. Needs a privileged receiver |
| Devices and special files | ๐ข | ๐ด | Unfinished work. Unix-only, privileged create |
Hardlinks (-H) | ๐ข | ๐ด | Structurally blocked until recursive scope: detecting that two paths share an inode needs the whole file list |
Transfer behaviour โ
| Capability | rsync | AeroRsync | Why |
|---|---|---|---|
| Single-file block delta | ๐ข | ๐ข | The core capability |
| Atomic destination write | ๐ข | ๐ข | .aerotmp + rename, with a kill-9 invariant |
| Streaming, RSS bounded | ๐ข | ๐ข | Both directions; no in-memory file cap |
| Sparse destination writes | ๐ข | ๐ข | Opt-in hole-punched writes; output reads back byte-identical |
--delete / --backup / --link-dest | ๐ข | ๐ด | Deliberate: deletion and retention belong to the sync layer, which has its own safety gates |
Filters --exclude / --include | ๐ข | ๐ด | Deliberate: AeroFTP filters one layer up with .aeroignore |
--inplace / --append / --partial-dir | ๐ข | ๐ด | Deliberate: --inplace would weaken the atomic-rename invariant |
--mkpath (create remote parents) | ๐ข | ๐ด | Not implemented. Earlier documentation claimed it was; that was wrong |
Deployment โ
| Capability | rsync | AeroRsync | Why |
|---|---|---|---|
| Works with no client binary installed | ๐ด | ๐ข | The reason the module exists |
| Windows without WSL / MSYS2 / Cygwin | ๐ด | ๐ข | The only delta path AeroFTP has on Windows |
| In-process, no fork+exec | ๐ด | ๐ข | Linked inside the app |
| Memory-safe implementation | ๐ด | ๐ข | Rust. 21 unsafe blocks, counted on main ba6ea3df0, all POSIX calls with no Rust equivalent and each with a SAFETY note: 10 xattr wrappers, 5 ACL fd calls, 3 in the transport (utimensat, user lookup, group lookup) and 3 in the streaming writer (fchmod on the still-open temp file before the ACL apply, and statfs with the read of the buffer it fills, which decides whether the temporary can be locked). Every wire byte is decoded in safe Rust. The July page said 13, which was the count before the ACL calls landed |
| Usable as a standalone library | ๐ข | ๐ด | The aerorsync crate on crates.io is a name reservation with no public API |
Performance vs stock rsync โ
Measured 28 July 2026 on an idle 24-core machine: both engines back-to-back, same 50 MB fixtures, same container, SSH over loopback.
How the comparison was made fair. The rsync side ran with -logDtprcz, the client flags that produce --server -logDtprcze.iLsfxCIvu - byte-for-byte the argument string AeroRsync sends. That was verified, not assumed: a wrapper script on the container logged the actual server command line for each candidate flag set.
The everyday -az was left out of the published numbers on purpose. With it the delta scenarios measure nothing: the two 50 MB fixtures share a size and an mtime, so rsync's quick check declares them identical and skips the transfer. The c (--checksum) in the strict set is what forces a real comparison.

| Scenario | AeroRsync | stock rsync 3.2.7 | Bytes on the wire |
|---|---|---|---|
| Cold upload, 50 MB incompressible | 1.068 s | 1.367 s | 52 436 493 vs 52 440 795 |
| Delta upload, 640 ร 4 KiB regions changed | 2.000 s | 1.358 s | 3 748 322 vs 3 743 589 |
| Delta download, same change set | 1.310 s | 1.344 s | 43 481 sent vs 50 737 |
| Redundant upload, nothing to do | 0.461 s | 1.260 s | 49 B vs 82 B |
| 20 ร 256 KiB, one session per file | 4.981 s | 25.249 s | - |
20 ร 256 KiB, one recursive rsync call | no recursive scope | 1.308 s | - |

Three different results, and the reading matters more than the score:
- AeroRsync wins wherever a process would have to be spawned. Cold upload, no-op, and twenty small files transferred one at a time.
rsyncpays a freshsshplusrsyncfork per file - roughly 1.25 s each - while AeroRsync opens an in-process session. That is the 5.1ร on the small-file batch and the 2.7ร on the no-op. - AeroRsync loses the delta upload by about 30%, while putting the same work on the wire. 3 748 322 bytes against 3 743 589 is a 0.13% difference, so the protocol decisions agree; the gap is CPU in the Rust encode path against thirty years of tuned C.
- The real gap is the row that has no AeroRsync number. rsync does the whole 20-file tree in one invocation in 1.308 s - 3.8ร better than the per-file path. That is recursive scope, and
AerorsyncBatchis what closes it at the transport layer. This benchmark exercises the per-file path on purpose, so it does not show the batch improvement.
Reproducibility. Across three runs the wall-clock figures move by a few percent - AeroRsync's cold upload measured 1.005, 1.049 and 1.068 s - while the byte counts are identical every time, which is what you would expect from a deterministic protocol on a fixed payload.
A caveat that works against us, stated anyway. The delta fixtures are compressible - A_base.bin compresses 9.1:1, and its mutated regions are largely reconstructible from blocks elsewhere in the basis, so rsync reports only 119 800 bytes of literal data on the download. Real-world delta ratios on incompressible payloads will be worse. The comparison stays valid, because both engines see identical bytes. The cold-upload fixture is genuinely incompressible (1.0:1), so that row is a clean transport measurement.
Validation โ
AeroRsync is not theoretical. The wire format is pinned at several levels, all of which run in CI.
| Layer | What it proves | Count |
|---|---|---|
| Unit tests on the module | Encode/decode round-trips against frozen byte transcripts captured from rsync 3.2.7, plus a captured-wire oracle from rsync 3.1.3 for the deflate token path. Measured on main c713857e: 713 selected, 15 ignored, 698 executed. The workflow still fails the job below 677 | 698 |
| CI lane 3, live | End-to-end against a real rsync --server in Docker: byte-identical upload (sha256 match), streaming upload, symlinks both directions, user.* xattrs inline / out-of-band / binary-with-NUL / empty, the batch path over one session, a symlink proving it does not inherit its target attributes, and POSIX ACL named-user upload and download | 15 |
| Checksum matrix, live | The production upload and download transports driven once per negotiated algorithm: xxh128, xxh3, xxh64, md5, md4, sha1 | 8 |
| Product path, live | integration_delta_sync confirms the real product selects the native transport for a host-key-pinned SFTP profile | in CI |
| Cross-OS | cargo check and cargo test --lib on windows-2022 every push, plus a --no-default-features gate proving the classic-only fallback surface still compiles | in CI |
Which rsync, precisely. Three fixtures, three versions, and the distinction is worth stating because this page used to blur it:
| Fixture | Port | rsync | Base image | Serves |
|---|---|---|---|---|
aeroftp-rsync-real | 2224 | 3.2.7 | Debian 12 bookworm | CI lane 3 and the checksum matrix |
aeroftp-delta-sync-fixture | 2222 | 3.4.1 | Alpine 3.19 | the integration_delta_sync product-path job |
aeroftp-rsync-313-deflate | 2225 | 3.1.3 | SHA-pinned source build | the deflate oracle: both peers pinned to protocol 31 with no zstd, so zlibx is what the negotiation actually picks |
So "byte-identical" is verified against rsync 3.2.7. Version 3.4.1 is also exercised in CI, by the product-path job rather than by the byte-identical test. Version 3.1.3 is there for a different reason: it is old enough to predate zstd, which is the only way to exercise the deflate token path against a real peer rather than against our own assumptions about it.
Architecture โ
AeroRsync lives in src-tauri/src/aerorsync/. On main 4ab49265 the Rust sources in that directory are about 42 900 lines. Line counts move with every edit; the roles do not.
| Module | Lines on 4ab49265 | Role |
|---|---|---|
native_driver.rs | 11 175 | Session state machine: preamble exchange, file list, signatures, delta, summary |
real_wire.rs | 8 992 | Wire encode/decode: varint, varlong, preamble, file list, sum_head, sum_block, delta ops, summary frame, multiplex |
delta_transport_impl.rs | 5 213 | AerorsyncDeltaTransport and AerorsyncBatch, bridging the driver to the production DeltaTransport trait |
tests.rs | 3 091 | Unit tests against frozen rsync 3.2.7 transcripts, plus the captured rsync 3.1.3 deflate wire oracle |
engine_adapter.rs | 2 551 | Streaming signature and delta application |
ssh_transport.rs and russh_session_transport.rs | 2 667 | The two SSH legs, with pinned host-key fingerprints |
events.rs | 1 228 | Progress, warnings, completion |
acl_fs.rs, xattr_fs.rs, streaming_writer.rs, and the rest | ~7 980 | ACL and xattr read/apply, atomic writes, live lanes, types, planner, fallback policy |
The production entry point is SftpProvider::delta_transport(). It builds the native transport when the parent SFTP session has pinned a host key. On Unix, the stock rsync binary takes that place only when the native transport cannot be constructed, or when no host key was pinned. On Windows there is no stock binary, so those two cases are a plain SFTP transfer. A native session that has already started and then fails does not start the stock binary either: the file goes out as a plain SFTP transfer, with no delta, on every platform. Host-key mismatch and the other hard refusals are shown to the user and are not retried. A rsync 3.1.x peer negotiates no algorithm strings and is spoken natively (MD5, classic flag bytes, zlibx via --new-compress). A peer below protocol 31, including 27, 28, 29 and 30, is refused before any data moves and the file goes over that same plain SFTP transfer. A frame that is actually corrupt stays a hard error and is shown.
Configuration โ
The Cargo feature aerorsync is compiled by default, and the runtime toggle has been ON by default in Auto mode since v3.8.0 - the host-key algorithm negotiation asymmetry between the ssh2 leg and the russh leg was resolved in May 2026, and the default was flipped after that.
From the GUI: Settings โ Advanced โ Delta backend. From the CLI:
aeroftp-cli aerorsync mode get # auto | native | classic
aeroftp-cli aerorsync mode set nativeAuto attempts the native engine first. The stock rsync binary is the Unix stand-in for a native transport that was never built, not the recovery path after a native error. A file below the minimum size, an SSH identity file the profile requires but does not have on disk, or no remote rsync goes out as a plain SFTP transfer, with no notice. A host-key mismatch or a permission refusal stops and is shown. Mode native does not use the stock binary at all.
Limitations โ
Two lists, because a boundary and a backlog are not the same sentence.
Deliberate. One file per invocation: AeroRsync is a delta accelerator, and the tree belongs to AeroSync. Enumeration, deletion and retention stay there, with their own safety gates. Implementing --delete on the wire would install a second, weaker deletion authority under the one that was hardened. Filters are applied one layer up by .aeroignore. The destination write strategy belongs to the atomic writer. No rsync:// daemon mode. Protocol 31 only. AerorsyncBatch still reuses one SSH session for many files: that is session reuse, not a recursive file list.
Unfinished. Owner and group travel on some historical captures and are not applied. Applying them needs a privileged receiver, which rarely matches a desktop deployment. Device and special files are unimplemented. Hardlinks stay blocked until recursive scope exists: two paths that share an inode can only be seen from the whole file list. Directory default ACL waits on that same scope. Access ACL on a single Linux file already ships, in both directions.
Version-dependent. Upstream rsync dropped sha1 from the negotiated checksum list between 3.2.7 and 3.4.1. AeroRsync still implements it and it works against peers that still offer it, but a modern rsync will refuse it - that is the peer declining, not AeroRsync failing. The compression side cuts the other way: zstd arrived in 3.2.0, so anything older negotiates zlibx. That path is driven and pinned against a captured rsync 3.1.3 transcript, which is the only way an old peer actually selects it.
Why the advertised list is shorter than rsync's. The negotiated winner is the first name in your own advertised list that the peer also offers, computed the same way on both sides (compat.c::parse_negotiate_str). That makes the list a promise: every name in it is one the peer may hand you. Mirroring stock rsync's zstd lz4 zlibx zlib none verbatim was a real defect, because stock ranks lz4 above zlibx and AeroRsync has no lz4 codec at all: a peer built with lz4 but without zstd made the negotiation settle on a codec we could not drive, and the file left the native path while a working zlibx sat one position lower. AeroRsync now advertises zstd zlibx none and drives all three. The trailing none is what keeps the two lists from ever failing to intersect, so a peer offering only codecs we decline still gets a delta transfer, just an uncompressed one, instead of dropping off the native path. A test sweeps every subset of stock's list and fails if any of them can negotiate something undrivable.
Not yet a library. The aerorsync crate on crates.io is a reserved name at 0.0.x with no public API. Extraction depends on three gates: stock-rsync interop green end to end, the dependency direction inverted from AeroFTP to aerorsync, and a separate clean-room commit history. No date is attached to that.
Origin Story โ
AeroRsync started as Strada C, the third option at a fork in the road:
- Strada A, the wrapper: use the local
rsyncbinary. Worked on Unix, blocked Windows. - Strada B, rclone-style block hashing: write our own delta primitives in the
StorageProvidertrait. Wide compatibility, but it requires every provider to expose remote partial-block read/write, and most do not. - Strada C, this one: reimplement the rsync wire protocol natively in Rust. Narrow scope, SFTP only, but a real cross-OS delta path with no client-side dependency.
Strada C took about ten days of wire-protocol archaeology to converge on byte-identical behaviour. Three composed wire bugs - the algorithm-list separator, protocol min-negotiation, and the sender phase loop - had to be unpicked with wire-dump tooling before the first end-to-end live upload came back with a matching sha256.
The name is the sixth member of the Aero family, alongside AeroSync (sync UX), AeroVault (encryption), AeroPlayer (audio), AeroAgent (AI) and AeroTools (developer surface).
Related Pages โ
- Delta Sync - the user-facing UI built on top of AeroRsync
- AeroSync - the higher-level sync workflow
- SFTP - the underlying protocol
- MCP Overview -
aeroftp_sync_treeuses this engine when applicable - CLI Commands -
aeroftp-cli sync --watchactivates the delta path when eligible