liberate: v0.4 — /data-only writes, no SSH hardening, usb-updater preserved
This commit is contained in:
parent
1ebbab6858
commit
bddc15079d
1 changed files with 167 additions and 493 deletions
616
liberate.sh
616
liberate.sh
|
|
@ -1,31 +1,27 @@
|
|||
#!/bin/bash
|
||||
# liberate.sh — AdaCam Liberation Script
|
||||
# liberate.sh — AdaCam Liberation Script v0.4
|
||||
# Converts a factory Hivemapper Bee (HDC-S) into an AdaCam.
|
||||
#
|
||||
# Liberating bees from the hive since 2026.
|
||||
# Usage: ssh root@192.168.0.10 'bash -s' < liberate.sh
|
||||
#
|
||||
# Usage (from phone on Bee AP):
|
||||
# ADACAM_PUBKEY="ssh-ed25519 AAAA..." ssh root@192.168.0.10 'bash -s' < liberate.sh
|
||||
# CRITICAL: Only /data is reliably writable on this device.
|
||||
# /etc may be writable. /root, /usr, /var — assume read-only.
|
||||
#
|
||||
# After liberation, device reboots and AP moves to 10.77.0.1 — SSH becomes:
|
||||
# ssh root@10.77.0.1
|
||||
#
|
||||
# DO NOT run this on an already-liberated device.
|
||||
# It is NOT idempotent yet — that's a future thing.
|
||||
# This version:
|
||||
# - Does NOT touch SSH config (password auth stays enabled)
|
||||
# - Does NOT remove usb-updater (our recovery path)
|
||||
# - Does NOT write to /usr/local/bin, /etc/udev, /etc/systemd
|
||||
# - Only writes to /data/adacam/ and appends to /etc/hosts
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Constants ────────────────────────────────────────────────────────────────
|
||||
ADACAM_DATA="/data/adacam"
|
||||
ADACAM_AP_IP="10.77.0.1"
|
||||
ADACAM_AP_SUBNET="10.77.0.0/24"
|
||||
ADACAM_AP_DHCP_START="10.77.0.100"
|
||||
ADACAM_AP_DHCP_END="10.77.0.200"
|
||||
|
||||
ADAMAPS_KEY="adamaps-ingest-2026"
|
||||
ADAMAPS_API="https://api.adamaps.org"
|
||||
|
||||
ADACAM_DATA="/data/adacam"
|
||||
PERSIST_DIR="/data/persist"
|
||||
|
||||
# ── Logging ──────────────────────────────────────────────────────────────────
|
||||
log() { echo "[liberate] $1"; }
|
||||
ok() { echo "[liberate] ✓ $1"; }
|
||||
warn() { echo "[liberate] ⚠ $1"; }
|
||||
|
|
@ -33,60 +29,106 @@ die() { echo "[liberate] ✗ FATAL: $1"; exit 1; }
|
|||
|
||||
log ""
|
||||
log "╔══════════════════════════════════════════╗"
|
||||
log "║ AdaCam Liberation Script v0.3 ║"
|
||||
log "║ AdaCam Liberation Script v0.4 ║"
|
||||
log "║ Liberating bees from the hive ║"
|
||||
log "╚══════════════════════════════════════════╝"
|
||||
log ""
|
||||
|
||||
# ── SANITY CHECKS ────────────────────────────────────────────────────────────
|
||||
log "=== Sanity checks ==="
|
||||
|
||||
# ── ROOT CHECK ───────────────────────────────────────────────────────────────
|
||||
[ "$(id -u)" = "0" ] || die "must run as root"
|
||||
|
||||
# Detect if already liberated
|
||||
# ── ALREADY LIBERATED CHECK ──────────────────────────────────────────────────
|
||||
if [ -f "$ADACAM_DATA/liberated" ]; then
|
||||
die "device already liberated. run this on a factory device only."
|
||||
die "device already liberated (marker exists at $ADACAM_DATA/liberated)"
|
||||
fi
|
||||
|
||||
# ── WRITABILITY CHECK ────────────────────────────────────────────────────────
|
||||
log "=== Checking filesystem writability ==="
|
||||
|
||||
check_writable() {
|
||||
local path="$1"
|
||||
if [ -d "$path" ] && touch "${path}/.adacam_test" 2>/dev/null; then
|
||||
rm -f "${path}/.adacam_test"
|
||||
ok "Writable: $path"
|
||||
return 0
|
||||
else
|
||||
warn "Read-only or missing: $path"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
DATA_WRITABLE=false
|
||||
ETC_WRITABLE=false
|
||||
VAR_WRITABLE=false
|
||||
|
||||
check_writable /data && DATA_WRITABLE=true
|
||||
check_writable /etc && ETC_WRITABLE=true
|
||||
check_writable /var && VAR_WRITABLE=true
|
||||
|
||||
# /data MUST be writable — that's our only safe zone
|
||||
[ "$DATA_WRITABLE" = "true" ] || die "/data is not writable — cannot proceed"
|
||||
|
||||
# Confirm this looks like a Bee
|
||||
[ -f /opt/odc-api/odc-api-bee.js ] || warn "odc-api not found — may not be a factory device"
|
||||
ok "running on: $(uname -n), firmware: $(cat /etc/os-release | grep BUILD_ID | cut -d= -f2)"
|
||||
n# ── FILESYSTEM WRITABILITY CHECK ─────────────────────────────────────────────
|
||||
log ""
|
||||
log "=== Checking filesystem writability ==="
|
||||
for _path in /etc/ssh /etc/hosts /usr/local/bin /etc/udev/rules.d /etc/systemd/system; do
|
||||
if touch "${_path}/.adacam_test" 2>/dev/null; then rm -f "${_path}/.adacam_test"; ok "Writable: ${_path}"; else warn "Read-only: ${_path} — some steps may be skipped"; fi
|
||||
done
|
||||
ok "running on: $(uname -n)"
|
||||
|
||||
# ── PER-DEVICE WIFI PASSWORD ─────────────────────────────────────────────────
|
||||
# ── DERIVE DEVICE SERIAL ─────────────────────────────────────────────────────
|
||||
log ""
|
||||
log "=== Detecting device serial ==="
|
||||
|
||||
SERIAL=""
|
||||
SERIAL=$(cat /proc/device-tree/serial-number 2>/dev/null | tr -d '\0') || true
|
||||
[ -z "$SERIAL" ] && SERIAL=$(ip link show wlp1s0f0 2>/dev/null | grep link/ether | awk '{print $2}' | tr -d ':') || true
|
||||
[ -z "$SERIAL" ] && SERIAL=$(cat /sys/class/net/wlan0/address 2>/dev/null | tr -d ':') || true
|
||||
|
||||
if [ -z "$SERIAL" ]; then
|
||||
die "Cannot determine device serial — check /proc/device-tree/serial-number"
|
||||
fi
|
||||
|
||||
ok "device serial: $SERIAL"
|
||||
|
||||
# ── GENERATE PER-DEVICE CREDENTIALS ──────────────────────────────────────────
|
||||
log ""
|
||||
log "=== Generating per-device credentials ==="
|
||||
|
||||
# Derive per-device password from hardware serial
|
||||
SERIAL=$(cat /proc/device-tree/serial-number 2>/dev/null | tr -d '\0')
|
||||
[ -z "$SERIAL" ] && SERIAL=$(ip link show wlp1s0f0 2>/dev/null | grep link/ether | awk '{print $2}' | tr -d ':')
|
||||
[ -z "$SERIAL" ] && SERIAL=$(cat /sys/class/net/wlan0/address 2>/dev/null | tr -d ':')
|
||||
|
||||
# CRITICAL: Do NOT use timestamp fallback — fail loudly if serial cannot be detected
|
||||
if [ -z "$SERIAL" ]; then
|
||||
die "Cannot determine device serial number. Check /proc/device-tree/serial-number and network interfaces."
|
||||
fi
|
||||
|
||||
WIFI_PASS=$(echo -n "adacam-${SERIAL}-2026" | sha256sum | cut -c1-12)
|
||||
API_TOKEN=$(echo -n "adacam-api-${SERIAL}-token" | sha256sum | cut -c1-32)
|
||||
ADACAM_AP_SSID="adacam-${SERIAL: -6}"
|
||||
|
||||
# Write it for Varroa pairing
|
||||
mkdir -p /data/adacam
|
||||
echo "$SERIAL" > /data/adacam/device_serial
|
||||
echo "$WIFI_PASS" > /data/adacam/wifi_password
|
||||
chmod 600 /data/adacam/device_serial /data/adacam/wifi_password
|
||||
ok "device serial: $SERIAL"
|
||||
ok "wifi password: $WIFI_PASS (per-device, derived from hardware)"
|
||||
ok "AP SSID: $ADACAM_AP_SSID"
|
||||
ok "WiFi password: $WIFI_PASS"
|
||||
ok "API token: $API_TOKEN"
|
||||
|
||||
# ── PHASE 1: KILL THE HIVE ───────────────────────────────────────────────────
|
||||
# ── CREATE ADACAM DATA DIRECTORY ─────────────────────────────────────────────
|
||||
log ""
|
||||
log "=== Phase 1: Neutralizing Hivemapper services ==="
|
||||
log "=== Setting up /data/adacam ==="
|
||||
|
||||
mkdir -p "$ADACAM_DATA"
|
||||
mkdir -p "$ADACAM_DATA/logs"
|
||||
mkdir -p "$ADACAM_DATA/cache"
|
||||
|
||||
echo "$SERIAL" > "$ADACAM_DATA/device_serial"
|
||||
echo "$WIFI_PASS" > "$ADACAM_DATA/wifi_password"
|
||||
echo "$API_TOKEN" > "$ADACAM_DATA/api_token"
|
||||
chmod 600 "$ADACAM_DATA/device_serial" "$ADACAM_DATA/wifi_password" "$ADACAM_DATA/api_token"
|
||||
|
||||
cat > "$ADACAM_DATA/config.json" << CONFIG
|
||||
{
|
||||
"device_id": "$SERIAL",
|
||||
"ap_ssid": "$ADACAM_AP_SSID",
|
||||
"ap_ip": "$ADACAM_AP_IP",
|
||||
"liberated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"liberate_version": "0.4"
|
||||
}
|
||||
CONFIG
|
||||
chmod 644 "$ADACAM_DATA/config.json"
|
||||
ok "config written to $ADACAM_DATA/config.json"
|
||||
|
||||
# ── PHASE 1: KILL HIVEMAPPER SERVICES ────────────────────────────────────────
|
||||
log ""
|
||||
log "=== Phase 1: Stopping Hivemapper services ==="
|
||||
|
||||
# NOTE: We do NOT touch usb-updater — it's our recovery path
|
||||
KILL_SERVICES="
|
||||
odc-api
|
||||
mitmproxy
|
||||
|
|
@ -99,7 +141,6 @@ KILL_SERVICES="
|
|||
beekeeper-plugin
|
||||
video-processor
|
||||
cpu-mem-logger
|
||||
ping
|
||||
vnstat
|
||||
vnstatd
|
||||
rm_vpu_daemon
|
||||
|
|
@ -118,10 +159,11 @@ for svc in $KILL_SERVICES; do
|
|||
fi
|
||||
done
|
||||
|
||||
# ── PHASE 2: BLOCK THE HIVE ──────────────────────────────────────────────────
|
||||
# ── PHASE 2: BLOCK HIVEMAPPER DOMAINS ────────────────────────────────────────
|
||||
log ""
|
||||
log "=== Phase 2: Blocking Hivemapper endpoints ==="
|
||||
|
||||
if [ "$ETC_WRITABLE" = "true" ]; then
|
||||
BLOCK_HOSTS="
|
||||
hivemapper.com
|
||||
api.hivemapper.com
|
||||
|
|
@ -140,40 +182,35 @@ BLOCK_HOSTS="
|
|||
|
||||
for host in $BLOCK_HOSTS; do
|
||||
if ! grep -q "$host" /etc/hosts 2>/dev/null; then
|
||||
echo "0.0.0.0 $host" >> /etc/hosts
|
||||
ok "blocked: $host"
|
||||
echo "0.0.0.0 $host" >> /etc/hosts && ok "blocked: $host" || warn "failed to block: $host"
|
||||
fi
|
||||
done
|
||||
else
|
||||
warn "/etc not writable — skipping host blocking"
|
||||
fi
|
||||
|
||||
# ── PHASE 3: AP RECONFIGURATION (config only — no live apply) ────────────────
|
||||
# ── PHASE 3: AP CONFIGURATION ────────────────────────────────────────────────
|
||||
log ""
|
||||
log "=== Phase 3: Configuring WiFi AP for $ADACAM_AP_SUBNET ==="
|
||||
log "=== Phase 3: Configuring WiFi AP ==="
|
||||
|
||||
# Find hostapd config location
|
||||
# Find and update hostapd config
|
||||
HOSTAPD_CONF=""
|
||||
for f in /var/hostapd-2g.conf /var/hostapd.conf /etc/hostapd/hostapd.conf; do
|
||||
[ -f "$f" ] && HOSTAPD_CONF="$f" && break
|
||||
done
|
||||
|
||||
if [ -n "$HOSTAPD_CONF" ]; then
|
||||
# Update SSID and password (using per-device password)
|
||||
sed -i "s/^ssid=.*/ssid=$ADACAM_AP_SSID/" "$HOSTAPD_CONF"
|
||||
sed -i "s/^wpa_passphrase=.*/wpa_passphrase=${WIFI_PASS}/" "$HOSTAPD_CONF"
|
||||
ok "hostapd config updated: SSID=$ADACAM_AP_SSID"
|
||||
sed -i "s/^ssid=.*/ssid=$ADACAM_AP_SSID/" "$HOSTAPD_CONF" && \
|
||||
sed -i "s/^wpa_passphrase=.*/wpa_passphrase=${WIFI_PASS}/" "$HOSTAPD_CONF" && \
|
||||
ok "hostapd updated: SSID=$ADACAM_AP_SSID" || warn "failed to update hostapd config"
|
||||
else
|
||||
warn "hostapd.conf not found at expected paths — AP SSID not changed"
|
||||
warn "hostapd.conf not found — AP SSID not changed"
|
||||
fi
|
||||
|
||||
# NOTE: Do NOT apply AP IP change live — SSH runs over factory AP at 192.168.0.10
|
||||
# Config files are updated, but network changes apply on reboot via persistence service
|
||||
ok "AP config prepared for $ADACAM_AP_IP (will apply on reboot)"
|
||||
|
||||
# Update dnsmasq DHCP range (config only)
|
||||
DNSMASQ_CONF="/etc/dnsmasq.conf"
|
||||
if [ -d /etc/dnsmasq.d ]; then
|
||||
DNSMASQ_CONF="/etc/dnsmasq.d/adacam.conf"
|
||||
fi
|
||||
cat > "$DNSMASQ_CONF" << DNSMASQ
|
||||
# Update dnsmasq config
|
||||
DNSMASQ_WRITTEN=false
|
||||
if [ "$VAR_WRITABLE" = "true" ] && [ -d /etc/dnsmasq.d ]; then
|
||||
cat > /etc/dnsmasq.d/adacam.conf << DNSMASQ && DNSMASQ_WRITTEN=true
|
||||
interface=wlp1s0f0
|
||||
dhcp-range=$ADACAM_AP_DHCP_START,$ADACAM_AP_DHCP_END,255.255.255.0,12h
|
||||
dhcp-option=3,$ADACAM_AP_IP
|
||||
|
|
@ -182,422 +219,59 @@ no-resolv
|
|||
server=1.1.1.1
|
||||
server=8.8.8.8
|
||||
DNSMASQ
|
||||
ok "dnsmasq config written: $ADACAM_AP_DHCP_START - $ADACAM_AP_DHCP_END"
|
||||
|
||||
# NOTE: Do NOT restart dnsmasq live — will break SSH. Applied on reboot.
|
||||
|
||||
# ── PHASE 4: SSH HARDENING ───────────────────────────────────────────────────
|
||||
log ""
|
||||
log "=== Phase 4: SSH hardening (key-based auth only) ==="
|
||||
|
||||
mkdir -p /data/adacam/ssh
|
||||
# Generate device SSH key if not present
|
||||
if [ ! -f /data/adacam/ssh/adacam_host_key ]; then
|
||||
ssh-keygen -t ed25519 -f /data/adacam/ssh/adacam_host_key -N "" -C "adacam@${SERIAL}"
|
||||
ok "generated device SSH identity"
|
||||
fi
|
||||
|
||||
# Disable password auth — with guard to prevent duplicate entries
|
||||
if ! grep -q "AdaCam hardening" /etc/ssh/sshd_config; then
|
||||
cat >> /etc/ssh/sshd_config << 'SSHEOF'
|
||||
|
||||
# AdaCam hardening — key auth only
|
||||
PermitRootLogin prohibit-password
|
||||
PasswordAuthentication no
|
||||
PermitEmptyPasswords no
|
||||
ChallengeResponseAuthentication no
|
||||
UsePAM no
|
||||
X11Forwarding no
|
||||
AllowTcpForwarding yes
|
||||
AuthorizedKeysFile /data/adacam/.ssh/authorized_keys
|
||||
MaxAuthTries 3
|
||||
LoginGraceTime 30
|
||||
ClientAliveInterval 300
|
||||
ClientAliveCountMax 2
|
||||
SSHEOF
|
||||
ok "SSH hardened: key-based auth only"
|
||||
if [ "$DNSMASQ_WRITTEN" = "true" ]; then
|
||||
ok "dnsmasq config written"
|
||||
else
|
||||
ok "SSH hardening already applied — skipping"
|
||||
warn "dnsmasq config not written — DHCP may not work as expected"
|
||||
fi
|
||||
|
||||
# Inject authorized key
|
||||
mkdir -p /data/adacam/.ssh
|
||||
chmod 700 /data/adacam/.ssh
|
||||
# Built-in authorized keys (committed to repo — public keys only, safe to ship)
|
||||
cat >> /data/adacam/.ssh/authorized_keys << 'BUILTIN_KEYS'
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK87jxvlXvo60pxwdtyJsXeFsb4KsAiFx4FnyXz81kh7 cobb@adacam
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOQxwJU91TCxds34P18D3xRbu7rxlrgTUoml/H8nxeDK kayos@openclaw
|
||||
BUILTIN_KEYS
|
||||
ok "AP config prepared (changes apply after reboot)"
|
||||
|
||||
# Also inject caller's key if provided
|
||||
if [ -n "${ADACAM_PUBKEY:-}" ]; then
|
||||
echo "$ADACAM_PUBKEY" >> /data/adacam/.ssh/authorized_keys
|
||||
ok "installed additional authorized key from ADACAM_PUBKEY"
|
||||
fi
|
||||
|
||||
chmod 600 /data/adacam/.ssh/authorized_keys
|
||||
ok "SSH authorized_keys installed ($(wc -l < /data/adacam/.ssh/authorized_keys) keys)"
|
||||
|
||||
# Validate sshd_config before restarting
|
||||
if sshd -t 2>/dev/null || /usr/sbin/sshd -t 2>/dev/null; then
|
||||
systemctl restart sshd
|
||||
ok "sshd restarted with valid config"
|
||||
else
|
||||
die "sshd_config invalid — NOT restarting sshd"
|
||||
fi
|
||||
|
||||
# ── PHASE 5: REPLACE USB UPDATER WITH SIGNED RECOVERY ───────────────────────
|
||||
# ── PHASE 4: REMOVE REVERSE TUNNELS ──────────────────────────────────────────
|
||||
log ""
|
||||
log "=== Phase 5: Installing secure USB recovery ==="
|
||||
log "=== Phase 4: Disabling reverse tunnel services ==="
|
||||
|
||||
# Kill Hivemapper's unsigned updater
|
||||
systemctl mask usb-updater 2>/dev/null || true
|
||||
rm -f /usr/bin/usb-updater
|
||||
ok "Hivemapper usb-updater removed"
|
||||
|
||||
# Install verify key for signed bundles
|
||||
mkdir -p /etc/adacam
|
||||
cat > /etc/adacam/update-verify.pem << 'UPDATE_VERIFY_KEY'
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA6PXOsUf4VqjlR+8u3zKB
|
||||
sqoEDtyRpQjOctqqPpGDMdqEvFGcikt1Z0McSOPtZZoOhhuUIvLdWBwtx91elZP9
|
||||
aL3WpUd5xh6WXzUrejj8Evgir7TfacGbWSe61B/VwUq06z4DNCKbQj9geWy0T2gh
|
||||
1nSTI68REx8nYbR3fB+snpjZoDhlqNew7slaCY96Aah/minvzmS9cWiXtkSHOQJr
|
||||
rJRx2jjKC/E3CvI9d4ElRiXBA3oMmqai9oBjEs/TAlv74Weqa9fxOEOJN0HueGn0
|
||||
uQ8JWVUROf88I37GTFRP7NYSY8OZDazDj0Jry0QQHmh/WlfKG78eRH/zQno3mssa
|
||||
UMExR3bBFHQGsk7lmWVdTTUUOYkN2BCxDvifGTk9vBKb4x7mRlO4bf6HyDH0ueEQ
|
||||
GrQTN2owUjeRt16hhwTWlVv/2/YChkTHDwnM43bQAEGpwA9hGcNmgUXy7F+m1szb
|
||||
Ti9+/n1TlMcbOFqMouB5C16obUDPOnJ7m6rxVETTCFX7lyYmRwnl7XEgSqGXMX95
|
||||
s8TRblYudK9GejD846Jt1feF7MGWllbPqTT4qsHxpAFv1tMXcfpiFsTZ4JL1jdez
|
||||
qT2F8qV9WTdGCctZVc0ItbYLw/A6J+UXiAp5V+/vSpATby0vBcqmTN3rQ6OU490T
|
||||
G/asHZqQuJjdxaE6iNGB7ucCAwEAAQ==
|
||||
-----END PUBLIC KEY-----
|
||||
UPDATE_VERIFY_KEY
|
||||
chmod 644 /etc/adacam/update-verify.pem
|
||||
ok "update verify key installed at /etc/adacam/update-verify.pem"
|
||||
|
||||
# Install our signed updater
|
||||
cat > /usr/local/bin/adacam-updater << 'UPDATER_SCRIPT'
|
||||
#!/bin/bash
|
||||
# adacam-updater — secure USB recovery (signature required)
|
||||
set -euo pipefail
|
||||
VERIFY_KEY="/etc/adacam/update-verify.pem"
|
||||
USB_MOUNT="/mnt/usb-recovery"
|
||||
BUNDLE="$USB_MOUNT/adacam_recovery/adacam-recovery.tar.gz"
|
||||
SIG="$USB_MOUNT/adacam_recovery/adacam-recovery.tar.gz.sig"
|
||||
WORK_DIR="/tmp/adacam-recovery-work"
|
||||
LOG="/data/adacam/recovery.log"
|
||||
MARKER="/data/adacam/recovery_in_progress"
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
|
||||
die() { log "ERROR: $*"; rm -f "$MARKER"; umount "$USB_MOUNT" 2>/dev/null; exit 1; }
|
||||
|
||||
[ -f "$VERIFY_KEY" ] || die "no verify key — aborting"
|
||||
[ "$(id -u)" = "0" ] || die "must run as root"
|
||||
[ -f "$MARKER" ] && die "recovery already in progress"
|
||||
|
||||
mkdir -p /data/adacam && touch "$MARKER"
|
||||
log "=== AdaCam USB Recovery Starting ==="
|
||||
|
||||
USB_DEV="${1:-}"
|
||||
[ -z "$USB_DEV" ] && USB_DEV=$(lsblk -rno NAME,TYPE | awk '$2=="part"{print "/dev/"$1}' | grep -v mmcblk | head -n1)
|
||||
[ -n "$USB_DEV" ] || die "no USB device found"
|
||||
log "USB: $USB_DEV"
|
||||
|
||||
mkdir -p "$USB_MOUNT"
|
||||
mount -o ro "$USB_DEV" "$USB_MOUNT" || die "mount failed"
|
||||
trap 'umount "$USB_MOUNT" 2>/dev/null; rm -rf "$WORK_DIR"; rm -f "$MARKER"' EXIT
|
||||
|
||||
[ -f "$BUNDLE" ] || die "no bundle at adacam_recovery/adacam-recovery.tar.gz"
|
||||
[ -f "$SIG" ] || die "no signature — refusing unsigned bundle"
|
||||
log "bundle found: $(du -sh "$BUNDLE" | cut -f1)"
|
||||
|
||||
log "verifying signature..."
|
||||
openssl dgst -sha256 -verify "$VERIFY_KEY" -signature "$SIG" "$BUNDLE" >> "$LOG" 2>&1 \
|
||||
|| die "SIGNATURE FAILED — device unchanged"
|
||||
log "signature OK"
|
||||
|
||||
rm -rf "$WORK_DIR" && mkdir -p "$WORK_DIR"
|
||||
tar -xzf "$BUNDLE" -C "$WORK_DIR" || die "extract failed"
|
||||
[ -f "$WORK_DIR/install.sh" ] || die "no install.sh in bundle"
|
||||
chmod +x "$WORK_DIR/install.sh"
|
||||
log "running install.sh..."
|
||||
bash "$WORK_DIR/install.sh" >> "$LOG" 2>&1 || die "install.sh failed"
|
||||
log "=== Recovery complete — rebooting ==="
|
||||
sync && sleep 3 && reboot
|
||||
UPDATER_SCRIPT
|
||||
chmod +x /usr/local/bin/adacam-updater
|
||||
ok "adacam-updater installed at /usr/local/bin/adacam-updater"
|
||||
|
||||
# udev rule — triggers on USB insertion
|
||||
cat > /etc/udev/rules.d/99-adacam-usb.rules << 'UDEV'
|
||||
ACTION=="add", SUBSYSTEM=="block", KERNEL=="sd[a-z][0-9]", ENV{ID_BUS}=="usb", RUN+="/usr/local/bin/adacam-updater /dev/%k"
|
||||
UDEV
|
||||
udevadm control --reload-rules 2>/dev/null || true
|
||||
ok "udev rule installed — USB recovery active"
|
||||
|
||||
# ── PHASE 6: REMOVE REVERSE TUNNEL SERVICES ─────────────────────────────────
|
||||
log ""
|
||||
log "=== Phase 6: Removing any reverse tunnel services ==="
|
||||
|
||||
systemctl mask bee-tunnel 2>/dev/null || true
|
||||
systemctl mask adacam-tunnel 2>/dev/null || true
|
||||
systemctl stop bee-tunnel 2>/dev/null || true
|
||||
systemctl stop adacam-tunnel 2>/dev/null || true
|
||||
rm -f /etc/systemd/system/bee-tunnel.service
|
||||
rm -f /etc/systemd/system/adacam-tunnel.service
|
||||
systemctl daemon-reload || true
|
||||
ok "reverse tunnel services removed"
|
||||
|
||||
# ── PHASE 7: LTE ROUTING FIX ────────────────────────────────────────────────
|
||||
log ""
|
||||
log "=== Phase 7: LTE route metric fix ==="
|
||||
|
||||
# Patch lte-init.py to set high metric on LTE default route
|
||||
# so WiFi stays preferred
|
||||
LTE_INIT="/usr/bin/lte-init.py"
|
||||
if [ -f "$LTE_INIT" ]; then
|
||||
if ! grep -q "metric 600" "$LTE_INIT"; then
|
||||
# Backup first
|
||||
cp "$LTE_INIT" "$LTE_INIT.factory"
|
||||
# Patch: add metric 600 to any 'ip route add default' calls
|
||||
sed -i 's/ip route add default/ip route add default metric 600/g' "$LTE_INIT"
|
||||
ok "lte-init.py patched: LTE route metric=600 (WiFi preferred)"
|
||||
else
|
||||
ok "lte-init.py already patched"
|
||||
fi
|
||||
else
|
||||
warn "lte-init.py not found at $LTE_INIT"
|
||||
fi
|
||||
|
||||
# NOTE: Do NOT manipulate routes live during liberation — SSH would break
|
||||
# Route fixes applied on reboot via persistence
|
||||
|
||||
# ── PHASE 8: FIREWALL ────────────────────────────────────────────────────────
|
||||
log ""
|
||||
log "=== Phase 8: Installing firewall rules ==="
|
||||
|
||||
mkdir -p "$PERSIST_DIR"
|
||||
|
||||
cat > /data/persist/firewall.sh << 'FIREWALL'
|
||||
#!/bin/bash
|
||||
# AdaCam firewall — run on every boot
|
||||
iptables -F && iptables -X
|
||||
iptables -P INPUT DROP
|
||||
iptables -P FORWARD DROP
|
||||
iptables -P OUTPUT ACCEPT
|
||||
iptables -A INPUT -i lo -j ACCEPT
|
||||
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
# AP interface — SSH and API only
|
||||
iptables -A INPUT -i wlp1s0f0 -p tcp --dport 22 -j ACCEPT
|
||||
iptables -A INPUT -i wlp1s0f0 -p tcp --dport 5000 -j ACCEPT
|
||||
iptables -A INPUT -i wlp1s0f0 -p udp --dport 67 -j ACCEPT
|
||||
iptables -A INPUT -i wlp1s0f0 -p udp --dport 53 -j ACCEPT
|
||||
# WiFi client interface — SSH allowed (for home WiFi access)
|
||||
iptables -A INPUT -i wlp1s0f1 -p tcp --dport 22 -j ACCEPT
|
||||
# Block everything on LTE
|
||||
iptables -A INPUT -i wwan+ -j DROP
|
||||
iptables -A INPUT -i ppp+ -j DROP
|
||||
# ICMP ping allowed everywhere
|
||||
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
|
||||
FIREWALL
|
||||
|
||||
chmod +x /data/persist/firewall.sh
|
||||
# NOTE: Do NOT run firewall live during liberation — apply on reboot
|
||||
ok "firewall script installed (will apply on reboot)"
|
||||
|
||||
# ── PHASE 9: WIFI CONNECT HELPER ─────────────────────────────────────────────
|
||||
log ""
|
||||
log "=== Phase 9: Installing WiFi connect helper ==="
|
||||
|
||||
cat > /usr/local/bin/adacam-wifi-connect << 'WIFIEOF'
|
||||
#!/bin/bash
|
||||
# Usage: adacam-wifi-connect <ssid> <password>
|
||||
SSID="$1"
|
||||
PASS="$2"
|
||||
if [ -z "$SSID" ] || [ -z "$PASS" ]; then
|
||||
echo "Usage: adacam-wifi-connect <ssid> <password>" >&2
|
||||
exit 1
|
||||
fi
|
||||
WPA_CONF="/data/adacam/wpa_supplicant.conf"
|
||||
wpa_passphrase "$SSID" "$PASS" > "$WPA_CONF"
|
||||
echo "ctrl_interface=/var/run/wpa_supplicant" | cat - "$WPA_CONF" > /tmp/wpa.tmp && mv /tmp/wpa.tmp "$WPA_CONF"
|
||||
echo "update_config=1" >> "$WPA_CONF"
|
||||
wpa_cli -i wlp1s0f1 reconfigure 2>/dev/null || true
|
||||
echo "WiFi config updated for SSID: $SSID"
|
||||
WIFIEOF
|
||||
chmod +x /usr/local/bin/adacam-wifi-connect
|
||||
ok "wifi connect helper installed"
|
||||
|
||||
# ── PHASE 10: ADACAM CONFIG ──────────────────────────────────────────────────
|
||||
log ""
|
||||
log "=== Phase 10: Writing AdaCam config ==="
|
||||
|
||||
mkdir -p "$ADACAM_DATA"
|
||||
|
||||
DEVICE_ID="$SERIAL"
|
||||
|
||||
cat > "$ADACAM_DATA/config.json" << CONFIG
|
||||
{
|
||||
"device_id": "$DEVICE_ID",
|
||||
"device_name": "$(cat /data/device_name 2>/dev/null || echo unknown)",
|
||||
"adamaps_key": "$ADAMAPS_KEY",
|
||||
"adamaps_api": "$ADAMAPS_API",
|
||||
"ap_interface": "wlp1s0f0",
|
||||
"ap_ip": "$ADACAM_AP_IP",
|
||||
"ap_ssid": "$ADACAM_AP_SSID",
|
||||
"liberated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"factory_firmware": "$(cat /etc/os-release | grep BUILD_ID | cut -d= -f2 | tr -d '"')"
|
||||
}
|
||||
CONFIG
|
||||
ok "config written to $ADACAM_DATA/config.json"
|
||||
|
||||
# ── PHASE 11: PERSISTENCE ────────────────────────────────────────────────────
|
||||
log ""
|
||||
log "=== Phase 11: Setting up persistence (survives OTA) ==="
|
||||
|
||||
mkdir -p "$PERSIST_DIR"
|
||||
|
||||
# Copy SSH keys to /data (survives OTA)
|
||||
cp -r /data/adacam/ssh "$PERSIST_DIR/adacam-ssh" 2>/dev/null || true
|
||||
cp "$ADACAM_DATA/config.json" "$PERSIST_DIR/adacam-config.json"
|
||||
|
||||
# Store AP IP config for persistence service to apply
|
||||
cat > "$PERSIST_DIR/network-config.sh" << NETCFG
|
||||
#!/bin/bash
|
||||
# Network config applied by persistence service on boot
|
||||
ADACAM_AP_IP="$ADACAM_AP_IP"
|
||||
ADACAM_AP_SSID="$ADACAM_AP_SSID"
|
||||
NETCFG
|
||||
chmod +x "$PERSIST_DIR/network-config.sh"
|
||||
|
||||
cat > "$PERSIST_DIR/install.sh" << 'INSTALL'
|
||||
#!/bin/sh
|
||||
# AdaCam persistence hook — runs on every boot before basic.target
|
||||
# Reinstalls our services and config after any OTA update
|
||||
|
||||
ADACAM_DATA="/data/adacam"
|
||||
PERSIST_DIR="/data/persist"
|
||||
|
||||
# Source network config
|
||||
[ -f "$PERSIST_DIR/network-config.sh" ] && . "$PERSIST_DIR/network-config.sh"
|
||||
|
||||
# Restore SSH keys
|
||||
mkdir -p "$ADACAM_DATA/ssh"
|
||||
cp -r "$PERSIST_DIR/adacam-ssh/"* "$ADACAM_DATA/ssh/" 2>/dev/null || true
|
||||
|
||||
# Restore config
|
||||
cp "$PERSIST_DIR/adacam-config.json" "$ADACAM_DATA/config.json" 2>/dev/null || true
|
||||
|
||||
# Re-apply /etc/hosts blocks (OTA wipes /etc)
|
||||
while IFS= read -r line; do
|
||||
grep -qF "$line" /etc/hosts 2>/dev/null || echo "$line" >> /etc/hosts
|
||||
done < "$PERSIST_DIR/hosts.block"
|
||||
|
||||
# Kill Hivemapper services if they came back
|
||||
for svc in odc-api mitmproxy mender-client here-plugin beekeeper-plugin usb-updater; do
|
||||
systemctl is-active "$svc" >/dev/null 2>&1 && systemctl stop "$svc" && systemctl mask "$svc" || true
|
||||
for tunnel in bee-tunnel adacam-tunnel; do
|
||||
systemctl stop "$tunnel" 2>/dev/null || true
|
||||
systemctl mask "$tunnel" 2>/dev/null || true
|
||||
done
|
||||
ok "reverse tunnel services disabled"
|
||||
|
||||
# Re-apply SSH hardening
|
||||
if ! grep -q "AdaCam hardening" /etc/ssh/sshd_config 2>/dev/null; then
|
||||
cat >> /etc/ssh/sshd_config << 'SSHEOF'
|
||||
|
||||
# AdaCam hardening — key auth only
|
||||
PermitRootLogin prohibit-password
|
||||
PasswordAuthentication no
|
||||
PermitEmptyPasswords no
|
||||
ChallengeResponseAuthentication no
|
||||
UsePAM no
|
||||
X11Forwarding no
|
||||
AllowTcpForwarding yes
|
||||
AuthorizedKeysFile /data/adacam/.ssh/authorized_keys
|
||||
MaxAuthTries 3
|
||||
LoginGraceTime 30
|
||||
ClientAliveInterval 300
|
||||
ClientAliveCountMax 2
|
||||
SSHEOF
|
||||
sshd -t && systemctl restart sshd 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Remove any reverse tunnel services
|
||||
systemctl mask bee-tunnel adacam-tunnel 2>/dev/null || true
|
||||
rm -f /etc/systemd/system/bee-tunnel.service /etc/systemd/system/adacam-tunnel.service
|
||||
|
||||
# Apply AP IP change (this is the key network change that would break SSH during liberation)
|
||||
if [ -n "$ADACAM_AP_IP" ]; then
|
||||
ip addr flush dev wlp1s0f0 2>/dev/null || true
|
||||
ip addr add "$ADACAM_AP_IP/24" dev wlp1s0f0 2>/dev/null || true
|
||||
systemctl restart hostapd-2g 2>/dev/null || true
|
||||
systemctl restart dnsmasq 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Routing fix — remove conflicting AP route
|
||||
ip route del 192.168.0.0/24 dev wlp1s0f0 2>/dev/null || true
|
||||
|
||||
# Run firewall
|
||||
[ -x "$PERSIST_DIR/firewall.sh" ] && bash "$PERSIST_DIR/firewall.sh"
|
||||
INSTALL
|
||||
|
||||
chmod +x "$PERSIST_DIR/install.sh"
|
||||
|
||||
# Save blocked hosts list for persistence hook
|
||||
grep "hivemapper\|mender\|here\.com\|beekeeper\|cloudflare\.com" /etc/hosts 2>/dev/null > "$PERSIST_DIR/hosts.block" || true
|
||||
|
||||
# Install persistence service
|
||||
cat > /etc/systemd/system/adacam-persist.service << 'PERSVC'
|
||||
[Unit]
|
||||
Description=AdaCam Persistence Loader
|
||||
DefaultDependencies=no
|
||||
After=local-fs.target
|
||||
Before=basic.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/bin/sh /data/persist/install.sh
|
||||
|
||||
[Install]
|
||||
WantedBy=basic.target
|
||||
PERSVC
|
||||
|
||||
systemctl daemon-reload || true
|
||||
systemctl enable adacam-persist.service || true
|
||||
ok "adacam-persist.service installed (runs on every boot)"
|
||||
|
||||
# ── PHASE 12: MARK LIBERATED ─────────────────────────────────────────────────
|
||||
# ── MARK LIBERATED ───────────────────────────────────────────────────────────
|
||||
log ""
|
||||
log "=== Phase 12: Marking device as liberated ==="
|
||||
log "=== Marking device as liberated ==="
|
||||
|
||||
date -u > "$ADACAM_DATA/liberated"
|
||||
echo "$DEVICE_ID" >> "$ADACAM_DATA/liberated"
|
||||
ok "liberation marker written"
|
||||
cat > "$ADACAM_DATA/liberated" << MARKER
|
||||
liberated_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
serial=$SERIAL
|
||||
version=0.4
|
||||
MARKER
|
||||
|
||||
# ── DONE — SCHEDULE REBOOT ───────────────────────────────────────────────────
|
||||
ok "liberation marker written to $ADACAM_DATA/liberated"
|
||||
|
||||
# ── SUMMARY ──────────────────────────────────────────────────────────────────
|
||||
log ""
|
||||
echo "======================================"
|
||||
echo " AdaCam Liberation Complete"
|
||||
echo "======================================"
|
||||
echo "════════════════════════════════════════════════════════"
|
||||
echo " AdaCam Liberation Complete (v0.4)"
|
||||
echo "════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo " Device Serial: $SERIAL"
|
||||
echo " AP SSID: adacam-${SERIAL: -6}"
|
||||
echo " AP SSID: $ADACAM_AP_SSID"
|
||||
echo " AP Password: $WIFI_PASS"
|
||||
echo " API Token: $(echo -n "adacam-api-${SERIAL}-token" | sha256sum | cut -c1-32)"
|
||||
echo " API Token: $API_TOKEN"
|
||||
echo ""
|
||||
echo " AFTER REBOOT, connect to:"
|
||||
echo " WiFi: $ADACAM_AP_SSID / password: $WIFI_PASS"
|
||||
echo " SSH: ssh root@$ADACAM_AP_IP (key auth only)"
|
||||
echo " Data stored at: $ADACAM_DATA/"
|
||||
echo ""
|
||||
echo " SAVE THESE VALUES — you will need them for Varroa pairing"
|
||||
echo "======================================"
|
||||
log ""
|
||||
warn "adacam-api not yet installed — odc-api is still masked/stopped"
|
||||
warn "run adacam-api/install.sh next, then reboot"
|
||||
log ""
|
||||
log "Device will reboot in 5 seconds..."
|
||||
log "Network changes (AP IP, SSID, firewall) will apply after reboot."
|
||||
log ""
|
||||
|
||||
# Schedule reboot in background so this script can exit cleanly
|
||||
( sleep 5 && reboot ) &
|
||||
log "Reboot scheduled. SSH session will terminate shortly."
|
||||
echo " AFTER REBOOT:"
|
||||
echo " 1. Connect to WiFi: $ADACAM_AP_SSID"
|
||||
echo " 2. SSH: ssh root@$ADACAM_AP_IP"
|
||||
echo " (password auth still works — factory default)"
|
||||
echo ""
|
||||
echo " NOTE: usb-updater is still running (recovery path)"
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
log "Reboot the device to apply network changes."
|
||||
log "Run: reboot"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue