diff options
| author | Lena <lena@omega> | 2026-01-01 00:00:00 +0000 |
|---|---|---|
| committer | Lena <lena@omega> | 2026-01-01 00:00:00 +0000 |
| commit | 7e04941bccb2683f8a6e3ee38a99c50129234dd1 (patch) | |
| tree | 471227fa437291e7a6b499e3de6c106c54eaf311 /README | |
| download | rsend-7e04941bccb2683f8a6e3ee38a99c50129234dd1.tar.gz | |
rsend: push phone folders to a home SSH host over rsync
A small Android app for one-way folder backup, a KISS alternative to
Syncthing. It bundles rsync (built from pinned source via the NDK) and
a pure-Go SSH transport, both shipped in the APK as lib*.so and run from
the native library directory.
rsend pins the host key, stores the ed25519 identity Keystore-encrypted,
pushes each folder additively or as a mirror, and runs on demand or on a
WiFi-only schedule. The build is self-contained and reproducible: make
setup provisions the toolchain, make builds rsync, rsh, and the APK.
Diffstat (limited to 'README')
| -rw-r--r-- | README | 122 |
1 files changed, 122 insertions, 0 deletions
@@ -0,0 +1,122 @@ +rsend +===== +Push configured phone folders to a home SSH host using real rsync. + +rsend backs up folders such as DCIM, WhatsApp media, and call recordings to a +remote host over SSH. One-way push only, on WiFi by default, periodic or +manual. Small, boring, self-contained: real rsync and a pure-Go SSH transport, +both built from source and shipped inside the APK. No foreign prebuilt +binaries. + + +What it does +------------ +- Pushes one or more local folders to remote paths over rsync-over-SSH. +- Incremental: only changed files move; large videos resume (rsync --partial). +- Per-folder deletion policy: additive backup (default) or mirror (--delete). +- Runs on unmetered WiFi, periodically (WorkManager) or on demand. +- ed25519 key auth with strict, pinned host-key verification. + + +Layout +------ +- rsh/ pure-Go SSH transport used as rsync's remote shell (-e). +- rsync/ build script that compiles pinned rsync from source via the NDK. +- app/ Android app (Kotlin, classic Views); bundles both as lib*.so. +- ci/ CI-agnostic build and test scripts; the Makefile drives them. +- metadata/ reproducibility and F-Droid build notes. +- versions pinned toolchain and source versions; the single source of truth. + + +Build +----- +The repository is self-contained: clone it and the build fetches and pins +everything else from source. The only host prerequisites are a POSIX shell, +curl, python3, tar, and git. The toolchain (JDK, Go, Android SDK and NDK, +Gradle) and the rsync source are downloaded and version-pinned by the build +(see versions for every pin). + + make setup # provision the toolchain into $HOME/toolchains + . "$HOME/toolchains/env.sh" # put it on PATH (do this in each shell) + make # rsync (NDK) -> rsh (Go) -> APK + make test # Go tests plus the host-side end-to-end push + make verify-repro # build twice, diff the artifacts + +The APK lands under app/build/outputs/apk/. It is unsigned by default; signing +config is local and gitignored. Continuous integration runs the same scripts: +ci/setup-toolchain.sh then ci/build.sh and ci/test.sh. + + +Run +--- +1. Install the APK and grant all-files access and notifications. +2. Generate a key in the app and add the shown public key to the home host's + ~/.ssh/authorized_keys. +3. Set the remote host, user, and port; run Test connection and accept the + pinned host-key fingerprint. +4. Add folders, choose each folder's deletion policy, set the schedule. +5. Tap Sync now, or wait for the periodic WiFi sync. + +For scheduled backup to run reliably, tap Battery and allow rsend to ignore +battery optimization. Phones with aggressive power management (Samsung, Xiaomi, +and others) otherwise delay or skip background jobs; see dontkillmyapp.com. + + +Server +------ +Any host with sshd and rsync works; rsend pushes over rsync-over-SSH, not SFTP. +The account is confined to "rsync into one folder and nothing else" by a forced +rrsync command, not by its shell. sshd runs that command through the account's +login shell, so the shell must be real: /bin/sh works, while /bin/false or +/sbin/nologin would break rsync. The confinement comes from key-only auth, the +forced command, and rrsync's write-only root. As root on the server: + + useradd -m -d /srv/backup -s /bin/sh rsendbackup + passwd -l rsendbackup + mkdir -p /srv/backup/phone && chown -R rsendbackup:rsendbackup /srv/backup + +This assumes UsePAM yes in sshd_config (the default on most distros; set it if +your build has it off). With UsePAM yes a password-locked account still accepts +key logins. With UsePAM no, sshd refuses any locked account even for keys +("account is locked"); there, skip passwd -l, set PasswordAuthentication no, and +leave the account with a non-locked password field. + +Put the app's public key in /srv/backup/.ssh/authorized_keys as one restricted +line that forces rrsync, write-only, into that directory: + + command="/usr/bin/rrsync -wo /srv/backup/phone",restrict ssh-ed25519 AAAA... rsend + +To apply the same limit account-wide, so it holds even if another key is added +later and not only on this key, force the command in sshd_config and reload +sshd: + + Match User rsendbackup + ForceCommand /usr/bin/rrsync -wo /srv/backup/phone + PasswordAuthentication no + PermitTTY no + AllowTcpForwarding no + AllowAgentForwarding no + AllowStreamLocalForwarding no + X11Forwarding no + +The account can then do nothing but receive rsync into that folder: every +session, with any key, is forced through rrsync, which rejects anything but a +plain rsync transfer into its root. Remote paths are relative to that root, so +set a folder's remote path to DCIM, not an absolute path. rrsync may instead +live at /usr/share/rsync/scripts/rrsync; check command -v rrsync. Verify the +host key from Test connection against +ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub on the server. + + +Debug +----- +- The in-app log viewer tails the plain-text rsync log. +- Reproduce a sync from a shell using the same rsync invocation rsend logs at + the top of each run. +- rsh transport: RSH_KEY, RSH_KNOWN_HOSTS, and RSH_PORT select the key, + known_hosts file, and port. Run rsh by hand to isolate SSH from rsync. + + +License +------- +GPLv3. Bundling rsync makes the whole app GPLv3; see LICENSE. |