1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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.
|