From 6578dc47a7d1f7946528e9540d9cfa2e9a5d88d2 Mon Sep 17 00:00:00 2001 From: Lena Date: Sun, 16 Aug 2026 00:00:00 +0000 Subject: app: release 0.2.0 Document the multi-remote release, publish matching store metadata, and advance the version once from the released 0.1.3 base. --- README | 195 +++++++++++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 154 insertions(+), 41 deletions(-) (limited to 'README') diff --git a/README b/README index 2f321c9..f9722c9 100644 --- a/README +++ b/README @@ -1,21 +1,44 @@ rsend ===== -Push configured phone folders to a home SSH host using real rsync. +Push configured phone folders to home SSH hosts 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 an unmetered network 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. +rsend backs up folders such as DCIM, WhatsApp media, and call recordings to +one or more remote hosts over SSH. One-way push only, on an unmetered network +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 +downloaded application .so files are repackaged. 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 an unmetered network, periodically (WorkManager) or on demand. -- ed25519 key auth with strict, pinned host-key verification. +- Pushes one or more local folders to one or more SSH hosts over rsync, + each folder mapped to a named remote. +- Incremental: only changed files move; large videos resume. Partial transfers + are staged in a .rsend-partial directory, so an interrupted sync never + replaces a complete file on the server with a truncated one. +- Per-folder deletion policy: additive backup (default) or mirror + (--delete-after). A mirror follows rsync exactly: an empty readable source + empties its remote destination. A missing, non-directory, root, or unreadable + source fails before rsync starts. Deletions happen only after transfers, so + an interrupted run never removes the server's copy of a file whose + replacement has not arrived. Every item rsync removes there is named in the + log, and a mirror that ends with files unaccounted for is reported as failed. +- Rejects duplicate and ancestor remote destinations on the same configured + endpoint. The check is lexical, case-insensitive, and does not allow absolute + and relative paths to be mixed for one endpoint. Root, bare home, dot, and + parent components are rejected. Separate folder mappings use separate trees. +- Regular files and directories only. A source path that is itself a symlink is + refused. Symlinks, devices and sockets inside it are skipped and named in the + log ("skipping non-regular file"), which is what media backup wants; rsync's + -l would copy them if you ever need it. +- Native executables and APK packaging are aligned for Android devices with + 4 KB or 16 KB memory pages. +- Runs on an unmetered network, periodically (WorkManager) or on demand. A + manual sync obeys the same unmetered setting, and says so and stops rather + than queueing itself until the phone next sees wifi. +- Ed25519 client-key auth with strict, pinned host-key verification. rsend + accepts modern Ed25519, ECDSA, and RSA-SHA2 host keys, pins the key type with + the key, and warns loudly if a pinned host presents a different one. Layout @@ -25,26 +48,34 @@ Layout - 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. +- THIRD_PARTY complete shipped-component inventory and license notices. +- APACHE-2.0 license terms for the shipped Apache-licensed components. - versions pinned toolchain and source versions; the single source of truth. Build ----- The repository is self-contained: clone it and the build fetches everything -else. The host prerequisites are a POSIX shell, coreutils, make, curl, python3, -tar, and git. The toolchain archives and rsync source are version- and -checksum-pinned by the build (see versions for every pin). +else. Toolchain setup supports x86_64 Linux with glibc or musl and selects a +separately checksummed Temurin JDK for each. The host prerequisites are a POSIX +shell, coreutils, make, curl, python3, tar, git, a C compiler for the Go race +detector, and the commands checked by ci/test.sh. Alpine also needs bash for +the NDK compiler launchers and gcompat for the NDK host binaries. The CI file +gives exact Debian and Alpine package lists. Toolchain archives, the SDK +platform revision, and rsync source are version- and checksum-pinned by the +build; see versions. 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, real-rsync integration, and Kotlin tests - make verify-repro # build twice, compare the unsigned APKs + make test # tests, race detector, lint, vulnerability scan + make verify-repro # clean commit, two paths, byte-identical APKs -`make test` also requires host rsync. The APK lands under +`make test` also requires host rsync and lets govulncheck query the public Go +vulnerability database. No source code is uploaded. The APK lands under app/build/outputs/apk/. It is unsigned by default and cannot be installed until -signed. Continuous integration runs the same scripts: ci/setup-toolchain.sh, -ci/build.sh, and ci/test.sh. +signed. .gitlab-ci.yml runs exactly those scripts and holds no build logic of +its own: ci/setup-toolchain.sh, ci/test.sh, and ci/build.sh. For a locally installable APK, create a keystore and a gitignored keystore.properties before building: @@ -63,32 +94,51 @@ app/build/outputs/apk/release/app-release.apk. Run --- -1. Install the APK and grant all-files access and notifications. +1. Install the APK and grant all-files access and notifications. All-files + access is required: a sync refuses to start without it because configured + storage may otherwise be unreadable. 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. + ~/.ssh/authorized_keys. Import accepts unencrypted Ed25519 private keys only. +3. Add a remote (name, host, user, port); run Test connection and accept the + pinned host-key fingerprint. The dialog shows the key type next to the + fingerprint; verify it against the matching key file on the server. Nothing + is saved until you accept a key, so a failed test cannot disturb a remote + that already works. Repeat for every host you push to. +4. Add folders, pick each folder's remote and deletion policy, set the + schedule. Enabling mirror shows its exact deletion semantics before saving. 5. Tap Sync now, or wait for the periodic sync. Scheduled intervals have a 15-minute minimum imposed by WorkManager. 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. +battery optimization. Phones with aggressive power management otherwise delay +or skip background jobs. Android 16 also counts long WorkManager foreground +runs against the app's job quota; a very large first backup may need several +runs and resumes from .rsend-partial each time. Server ------ -Any host with sshd and rsync works, old rsyncs included (stock macOS); rsend -pushes over rsync-over-SSH, not SFTP. rsync creates the final component of a -folder's remote path on its own; create deeper missing parents once with -mkdir -p on the server. - -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: +Any rsync-compatible SSH service works, including ordinary shell accounts and +older rsync releases such as stock macOS. rsend pushes over rsync-over-SSH, not +SFTP. Remote paths may be absolute, relative, or home-relative, but mappings on +one endpoint must use either absolute or relative paths consistently. A path +must name a directory below root or home and cannot contain dot or parent +components. rsync creates the final component on its own; create deeper missing +parent directories once with mkdir -p on the server. A remote path cannot begin +with a colon: rsync interprets host::path as its unencrypted daemon protocol +instead of SSH. + +Protocol compatibility is not a security promise. This release bundles rsync +3.5.0, but it cannot update the rsync receiver or rrsync wrapper on the server. +Install upstream 3.5.0 or a vendor package carrying the August 2026 security +fixes on the server before relying on rrsync confinement. + +The recommended deployment confines the account to "rsync into one folder and +nothing else" with a forced rrsync command. This is not a protocol requirement. +sshd runs the forced command through the account's login shell, so the shell +must be real: /bin/sh works, while /bin/false or /sbin/nologin breaks 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 @@ -124,17 +174,80 @@ 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. +ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub on the server. The Test +connection dialog prints the key type it pinned; if it says something other +than ssh-ed25519, the server has no ed25519 host key and you should compare +against that type's .pub file instead. + +Interrupted transfers leave a .rsend-partial directory inside the destination +folder. rsync reuses it to resume and excludes it from the transfer, so it is +never deleted by a mirror. Abandoned partials from a sync that never resumed +can be removed by hand. + + +Upgrading +--------- +A configuration written by 0.1.x is read once and rewritten in the current +shape on first launch. The single remote becomes the sole entry, named after +its host; every safe folder points at it; schedule intervals below 15 minutes +are raised to WorkManager's minimum; and the pin 0.1.x kept in its own +known_hosts file is carried over. A pin that no longer describes that host and +port is dropped rather than failing the upgrade, which costs one Test +connection instead of the whole configuration. The exact old JSON is preserved +as config.json.0.1. A folder or endpoint that violates current safety rules is +omitted, preserved in that file, and reported in the app instead of causing the +rest of the configuration to be discarded. + +This is a deliberate, time-limited exception to the project's rule against +compatibility shims, kept because 0.1.3 is the last published release and is +therefore what every upgrading install has. It will be removed in 0.4.0: a +0.1.x configuration not opened by then is read as unparsable, preserved +alongside as config.json.broken, and the app starts empty. Debug ----- -- The in-app log viewer displays the plain-text rsync log; tap Refresh to reload. -- The log records the rsync arguments and complete process output for each run. +- The in-app log viewer displays the plain-text rsync log; tap Refresh to + reload. +- The log records the rsync arguments, the errors, and every file a mirror + deleted on the server ("del. "). Transfers are counted rather than + listed one line each: on a first sync of a large library the filenames would + push everything worth reading out of the capped log. The log is capped at + 512 KB and rolls over to sync.log.1. The viewer reads at most the newest + 64 KB across both files so layout cost stays bounded. Control characters are + escaped before writing so remote output cannot forge terminal log lines. +- If config.json cannot be read or parsed, rsend atomically renames it to a + unique config.json.broken file, starts from an empty configuration, and shows + the preserved filename in the app. It never deletes unreadable config bytes. - 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. + known_hosts file, and port; RSH_KEY_DATA passes the key itself, which is what + the app uses so the plaintext key never reaches the filesystem. Run rsh by + hand to isolate SSH from rsync. +- During transport, rsh offers only the host-key type already pinned for that + remote. Test connection prefers the pinned type so an untouched server + reproduces the same key, but can show a newly rotated type for explicit + approval. Re-run Test connection after deliberately rotating a host key. +- An IPv6 host goes in the host field as a bare literal (2001:db8::1); rsend + adds the brackets where rsh and rsync each need them. A bracketed literal is + accepted too. +- "foreground service refused" in the log means the sync ran as a plain + background job, which the system may stop early. Grant Battery > + unrestricted; rsync resumes from .rsend-partial on the next run either way. +- A host that drops packets instead of refusing them, a firewall closed to the + phone's current network for instance, stalls the connect for the full budget. + That budget is 10 seconds; RSH_CONNECT_TIMEOUT overrides it in whole seconds. + The log line reads "rsh: unreachable:", and the remaining folders bound to + that remote are skipped for the rest of the run rather than each paying the + timeout again. License ------- -GPLv3. Bundling rsync makes the whole app GPLv3; see LICENSE. +Copyright (C) 2026 Lena. GPLv3; see LICENSE. + +Bundling rsync makes the whole app GPLv3. THIRD_PARTY records every native and +JVM component shipped in the APK, its license, and its corresponding source. +Native source and tool archives are pinned and checksummed; JVM dependencies +use exact Maven versions. Test and build-only dependencies are listed +separately because they are not part of the APK. LICENSE and THIRD_PARTY are +packaged as plain APK assets, along with APACHE-2.0. -- cgit v1.2.3