Description:
Captures everything needed to rebuild this router on a fresh flash: hardware/version identification, the APK package list and repository config, full UCI export, the standard sysupgrade backup, and the service enable/disable state from /etc/rc.d. It then diffs the overlay against what sysupgrade actually captured, drops anything already owned by an installed package (reproducible via apk add) or matching known secrets, and bundles the remaining orphaned files the backup would have silently left behind. Output is a single timestamped tarball in /tmp, with the archive path and package/modified/orphaned counts printed to stdout.
Good place to store is in ls -la /usr/bin/clone-router.sh
A few practical notes before you run it "for real":
- It writes to
/tmpon the router, which is tmpfs — the resulting tarball won't survive a reboot, so pull it off the router (scpwon't work since there's no sftp-server; usessh root@192.168.1.1 "cat /tmp/clone-*.tar.gz" > clone-backup.tar.gzlike I did for testing) if you want it to persist. - The tarball still contains
uci-export.txtand the sysupgradeconfig.tar.gz, both of which have your wifi/PPPoE passwords in plaintext — treat it accordingly once it leaves the router (don't drop it somewhere world-readable). - Each run leaves a new timestamped tarball behind; nothing auto-deletes old ones, so clean up
/tmp/clone-*.tar.gzon the router periodically if you run this more than once. - Architecture (Intel/x86) itself doesn't matter
Picking the newest clone-.tar.gz by mtime*:
REMOTE=$(ssh root@192.168.1.1 'ls -t /tmp/clone-*.tar.gz | head -1') && scp -O "root@192.168.1.1:$REMOTE" .
Grab the latest from the router:
ssh root@192.168.1.1 "rm -f $REMOTE"
#!/bin/sh
SELF=/usr/bin/clone-router.sh
D=/tmp/clone-$(date +%Y%m%d-%H%M)
mkdir -p "$D" || exit 1
ubus call system board > "$D/board.json" 2>/dev/null
cat /etc/openwrt_release > "$D/openwrt_release.txt" 2>/dev/null
uname -a > "$D/uname.txt"
mount > "$D/mounts.txt"
df -h > "$D/df.txt"
cp /etc/apk/repositories "$D/" 2>/dev/null
cp -r /etc/apk/repositories.d "$D/repositories.d" 2>/dev/null
sed 's/[>=<].*//' /etc/apk/world | grep -vE '^(kernel|base-files|libc)$' | sort > "$D/packages.txt"
tr '\n' ' ' < "$D/packages.txt" > "$D/packages-oneline.txt"
ls -l /etc/rc.d/ > "$D/rc.d-state.txt" 2>/dev/null
uci export > "$D/uci-export.txt"
sysupgrade -b "$D/config.tar.gz" 2>/dev/null
if [ -s "$D/config.tar.gz" ]; then
tar -tzf "$D/config.tar.gz" | sed -e 's|^\./||' -e 's|/$||' -e 's|^|/|' | sort > "$D/in-backup.txt"
else
echo "SYSUPGRADE BACKUP FAILED" > "$D/in-backup.txt"
fi
if [ -d /overlay/upper ]; then
find /overlay/upper \( -type f -o -type l \) 2>/dev/null | sed 's|^/overlay/upper||' | sort > "$D/modified.txt"
else
find /etc /root /www /srv /opt /usr/bin /usr/sbin /usr/lib /usr/share \( -type f -o -type l \) -newer /rom/etc/banner 2>/dev/null | sort > "$D/modified.txt"
fi
grep -Fxv -f "$D/in-backup.txt" "$D/modified.txt" \
| grep -vE '^/(tmp|var|dev|proc|sys)' \
| grep -Fxv "$SELF" > "$D/orphan-candidates.txt"
: > "$D/MISSING-from-backup.txt"
while IFS= read -r p; do
case "$p" in
/etc/shadow-|/etc/passwd-|/etc/urandom.seed) continue ;;
esac
if { [ -e "$p" ] || [ -L "$p" ]; } && ! apk info -W "$p" >/dev/null 2>&1; then
echo "$p" >> "$D/MISSING-from-backup.txt"
fi
done < "$D/orphan-candidates.txt"
rm -f "$D/orphan-candidates.txt"
if [ -s "$D/MISSING-from-backup.txt" ]; then
sed 's|^/||' "$D/MISSING-from-backup.txt" > /tmp/.rel-list.$$
tar -czf "$D/missing-files.tar.gz" -C / -T /tmp/.rel-list.$$ 2>/dev/null
rm -f /tmp/.rel-list.$$
fi
N_PKG=$(wc -l < "$D/packages.txt")
N_MOD=$(wc -l < "$D/modified.txt")
N_ORPH=$(wc -l < "$D/MISSING-from-backup.txt")
tar -czf "$D.tar.gz" -C /tmp "$(basename "$D")"
rm -rf "$D"
echo "archive: $D.tar.gz"
echo "packages: $N_PKG"
echo "modified: $N_MOD"
echo "orphaned: $N_ORPH"