Skip to content

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.exe bundle. No WSL requirement. Verified byte-identical against a stock rsync --server in 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:

  1. Bundle a GPL rsync.exe - license complexity, larger download, manual updates every release.
  2. 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 โ€‹

CapabilityrsyncAeroRsyncWhy
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 โ€‹

CapabilityrsyncAeroRsyncWhy
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 โ€‹

CapabilityrsyncAeroRsyncWhy
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 โ€‹

CapabilityrsyncAeroRsyncWhy
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 โ€‹

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

AeroRsync and stock rsync on 50 MB fixtures

ScenarioAeroRsyncstock rsync 3.2.7Bytes on the wire
Cold upload, 50 MB incompressible1.068 s1.367 s52 436 493 vs 52 440 795
Delta upload, 640 ร— 4 KiB regions changed2.000 s1.358 s3 748 322 vs 3 743 589
Delta download, same change set1.310 s1.344 s43 481 sent vs 50 737
Redundant upload, nothing to do0.461 s1.260 s49 B vs 82 B
20 ร— 256 KiB, one session per file4.981 s25.249 s-
20 ร— 256 KiB, one recursive rsync callno recursive scope1.308 s-

Twenty files of 256 KiB, per file against one recursive rsync call

Three different results, and the reading matters more than the score:

  1. AeroRsync wins wherever a process would have to be spawned. Cold upload, no-op, and twenty small files transferred one at a time. rsync pays a fresh ssh plus rsync fork 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.
  2. 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.
  3. 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 AerorsyncBatch is 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.

LayerWhat it provesCount
Unit tests on the moduleEncode/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 677698
CI lane 3, liveEnd-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 download15
Checksum matrix, liveThe production upload and download transports driven once per negotiated algorithm: xxh128, xxh3, xxh64, md5, md4, sha18
Product path, liveintegration_delta_sync confirms the real product selects the native transport for a host-key-pinned SFTP profilein CI
Cross-OScargo check and cargo test --lib on windows-2022 every push, plus a --no-default-features gate proving the classic-only fallback surface still compilesin CI

Which rsync, precisely. Three fixtures, three versions, and the distinction is worth stating because this page used to blur it:

FixturePortrsyncBase imageServes
aeroftp-rsync-real22243.2.7Debian 12 bookwormCI lane 3 and the checksum matrix
aeroftp-delta-sync-fixture22223.4.1Alpine 3.19the integration_delta_sync product-path job
aeroftp-rsync-313-deflate22253.1.3SHA-pinned source buildthe 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.

ModuleLines on 4ab49265Role
native_driver.rs11 175Session state machine: preamble exchange, file list, signatures, delta, summary
real_wire.rs8 992Wire encode/decode: varint, varlong, preamble, file list, sum_head, sum_block, delta ops, summary frame, multiplex
delta_transport_impl.rs5 213AerorsyncDeltaTransport and AerorsyncBatch, bridging the driver to the production DeltaTransport trait
tests.rs3 091Unit tests against frozen rsync 3.2.7 transcripts, plus the captured rsync 3.1.3 deflate wire oracle
engine_adapter.rs2 551Streaming signature and delta application
ssh_transport.rs and russh_session_transport.rs2 667The two SSH legs, with pinned host-key fingerprints
events.rs1 228Progress, warnings, completion
acl_fs.rs, xattr_fs.rs, streaming_writer.rs, and the rest~7 980ACL 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:

bash
aeroftp-cli aerorsync mode get          # auto | native | classic
aeroftp-cli aerorsync mode set native

Auto 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 rsync binary. Worked on Unix, blocked Windows.
  • Strada B, rclone-style block hashing: write our own delta primitives in the StorageProvider trait. 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).

  • 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_tree uses this engine when applicable
  • CLI Commands - aeroftp-cli sync --watch activates the delta path when eligible

aeroftp.app - Released under the GPL-3.0 License. AeroFTP Reviews