feat(cloner): Add SecuBox Station Cloner/Deployer

Host-side orchestrator (secubox-clone-station.sh):
- Dual USB serial control with MOKATOOL integration
- ASU API firmware building for clone images
- TFTP serving with auto-generated U-Boot commands
- Full workflow: detect → pull → flash → verify

On-device CLI (secubox-cloner):
- Build ext4 images for same device type
- TFTP server management via dnsmasq
- Clone token generation with auto-approve
- Integration with master-link mesh onboarding

First-boot provisioning (50-secubox-clone-provision):
- Partition resize to full disk (parted + resize2fs)
- Master discovery via mDNS/network scan
- Automatic mesh join with pre-approved tokens

Master-link enhancements:
- ml_clone_token_generate() for 24h auto-approve tokens
- ml_token_is_auto_approve() for token type detection
- Auto-approve logic in join request handler

SecuBox CLI additions:
- secubox clone (build, serve, token, status, list, export)
- secubox master-link (status, peers, token, join, approve)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-08 06:52:59 +01:00
parent 320277b87f
commit 8015d790e0
7 changed files with 2275 additions and 17 deletions

View File

@ -944,3 +944,49 @@ _Last updated: 2026-02-07_
- Fixed port conflict: console (8515), yijing360 (8521)
- Deployed yijing-360.zip with generator.py
- Emancipated: yijing360.gk2.secubox.in with SSL
26. **HAProxy Multi-Certificate SNI Fix (2026-02-07)**
- Fixed multi-domain SSL certificate handling using `crt-list` instead of directory mode
- Added `generate_certs_list()` function in haproxyctl to create certs.list from .pem files
- Updated `haproxy-sync-certs` to regenerate certs.list after syncing ACME certs
- HTTPS frontend now uses `crt-list /opt/haproxy/certs/certs.list` for reliable SNI matching
- Each certificate's SANs and CN are extracted to create explicit domain-to-cert mappings
- Fallback to directory mode if certs.list doesn't exist (backwards compatible)
27. **HAProxy Backend IP Fix (2026-02-07)**
- Fixed localhost (127.0.0.1) usage in HAProxy backends - must use 192.168.255.1 (host bridge IP)
- HAProxy runs in LXC container, cannot reach host services via 127.0.0.1
- Added auto-conversion in RPCD handler: 127.0.0.1/localhost → 192.168.255.1
- Fixed CLI tools: secubox-exposure, jellyfinctl, jitsctl, simplexctl, secubox-subdomain
- Fixed Fabricator Streamlit Services page backend creation
- Fixed HAProxy config templates for jitsi
28. **Station Cloner/Deployer Implementation (2026-02-08)**
- Created `secubox-tools/secubox-clone-station.sh` — host-side cloning orchestrator for dual USB serial.
- Commands: detect, pull, flash, verify, clone (full workflow), console, uboot, env-backup
- Integrates with MOKATOOL (`mochabin_tool.py`) for serial console automation
- Uses ASU API (firmware-selector.openwrt.org) for building clone images
- TFTP serving for network boot with auto-generated U-Boot commands
- Created `secubox-core/root/usr/sbin/secubox-cloner` — on-device clone manager CLI.
- Commands: build, serve, token, status, list, export
- Builds ext4 images for same device type (required for partition resize)
- Generates clone provision scripts for TFTP download
- Integrates with master-link for mesh join tokens
- Created `secubox-core/root/etc/uci-defaults/50-secubox-clone-provision` — first-boot provisioning.
- Step 1: Resize root partition to full disk (parted + resize2fs)
- Step 2: Discover master via mDNS or network scan
- Step 3: Configure as mesh peer (master-link UCI)
- Step 4: Join mesh with token or request approval
- Enhanced `secubox-master-link`:
- Added `ml_clone_token_generate()` for auto-approve clone tokens (24h TTL)
- Added `ml_token_is_auto_approve()` for token type detection
- Updated `ml_join_request()` to auto-approve clone tokens
- New CLI commands: clone-token, register-token
- Updated `secubox` CLI:
- Added `secubox clone` command group (build, serve, token, status, list, export)
- Added `secubox master-link` command group (status, peers, token, clone-token, join, approve, pending)
- **Clone workflow**:
1. Master: `secubox clone build && secubox clone serve --start`
2. Host: `./secubox-clone-station.sh clone` (detects, pulls, flashes target)
3. Target boots, resizes root, auto-joins mesh with pre-approved token
- Part of v0.19 mesh deployment automation.

View File

@ -51,7 +51,15 @@ _Last updated: 2026-02-07_
- Gossip-based exposure config sync via secubox-p2p
- Created `luci-app-vortex-dns` dashboard
### Just Completed (2026-02-06/07)
### Just Completed (2026-02-06/08)
- **Station Cloner/Deployer** — DONE (2026-02-08)
- Host-side `secubox-clone-station.sh` with MOKATOOL integration for dual USB serial control
- On-device `secubox-cloner` CLI for build/serve/token/export
- First-boot provisioning script with partition resize and mesh join
- Master-link clone tokens with auto-approve for seamless onboarding
- Added `secubox clone` and `secubox master-link` CLI command groups
- Full workflow: build image on master → TFTP serve → flash target → auto-join mesh
- **HAProxy "End of Internet" Default Page** — DONE (2026-02-07)
- Cyberpunk fallback page for unknown/unmatched domains

View File

@ -0,0 +1,306 @@
#!/bin/sh
#
# SecuBox Clone Auto-Provisioning
# Runs on first boot of cloned devices to:
# 1. Resize root partition to full disk
# 2. Configure as mesh peer
# 3. Join mesh with token or request approval
#
# Environment variables (set by clone builder):
# SECUBOX_MASTER - Master IP address
# SECUBOX_CLONE_TOKEN - Pre-approved join token
# SECUBOX_AUTO_JOIN - Auto-join mesh (1=yes, 0=request only)
#
# Check if we're a clone (marker file or env)
CLONE_MARKER="/etc/secubox/.clone-provision"
PROVISION_LOG="/var/log/secubox-clone-provision.log"
# Default values (can be overridden by embedded config)
MASTER_IP="${SECUBOX_MASTER:-}"
CLONE_TOKEN="${SECUBOX_CLONE_TOKEN:-}"
AUTO_JOIN="${SECUBOX_AUTO_JOIN:-1}"
log() {
local msg="[$(date +%T)] $*"
echo "$msg" >> "$PROVISION_LOG"
logger -t secubox-clone "$*"
echo "$msg"
}
# Try to load embedded clone config
load_clone_config() {
# Check for config in /etc/secubox/clone.conf
if [ -f /etc/secubox/clone.conf ]; then
. /etc/secubox/clone.conf
log "Loaded clone config from /etc/secubox/clone.conf"
return 0
fi
# Check for TFTP-downloaded config
if [ -f /tmp/clone-config.sh ]; then
. /tmp/clone-config.sh
log "Loaded clone config from /tmp/clone-config.sh"
return 0
fi
return 1
}
# Resize root partition to full disk
resize_root() {
log "Checking root partition resize..."
ROOT_DEV=$(awk '$2=="/" {print $1}' /proc/mounts)
if [ -z "$ROOT_DEV" ]; then
log "Cannot determine root device"
return 1
fi
# Determine disk and partition number
# Handle both /dev/mmcblk0p2 and /dev/sda2 formats
if echo "$ROOT_DEV" | grep -q "mmcblk\|nvme"; then
# MMC/NVMe style: /dev/mmcblk0p2 -> /dev/mmcblk0 + 2
DISK=$(echo "$ROOT_DEV" | sed 's/p[0-9]*$//')
PART_NUM=$(echo "$ROOT_DEV" | grep -o 'p[0-9]*$' | tr -d 'p')
else
# SCSI style: /dev/sda2 -> /dev/sda + 2
DISK=$(echo "$ROOT_DEV" | sed 's/[0-9]*$//')
PART_NUM=$(echo "$ROOT_DEV" | grep -o '[0-9]*$')
fi
if [ -z "$DISK" ] || [ -z "$PART_NUM" ]; then
log "Cannot parse disk/partition from $ROOT_DEV"
return 1
fi
log "Root device: $ROOT_DEV (disk: $DISK, partition: $PART_NUM)"
# Get current and potential sizes
local current_size=$(df -m / | tail -1 | awk '{print $2}')
log "Current root size: ${current_size}MB"
# Check if parted is available
if ! command -v parted >/dev/null 2>&1; then
log "parted not available, skipping resize"
return 1
fi
# Get disk total size
local disk_size=$(parted -s "$DISK" unit MB print 2>/dev/null | \
grep "^Disk $DISK" | sed 's/.*: //' | tr -d 'MB')
if [ -n "$disk_size" ]; then
log "Disk size: ${disk_size}MB"
# Only resize if there's significant space to gain (>10%)
local potential_gain=$((disk_size - current_size))
if [ "$potential_gain" -gt "$((current_size / 10))" ]; then
log "Resizing partition $PART_NUM to 100%..."
# Resize partition
if parted -s "$DISK" resizepart "$PART_NUM" 100% 2>/dev/null; then
log "Partition resized"
# Resize filesystem
if resize2fs "$ROOT_DEV" 2>/dev/null; then
sync
local new_size=$(df -m / | tail -1 | awk '{print $2}')
log "Filesystem resized: ${current_size}MB -> ${new_size}MB"
else
log "resize2fs failed (may need reboot)"
fi
else
log "parted resize failed"
fi
else
log "Partition already at optimal size"
fi
fi
return 0
}
# Discover master via mDNS or network scan
discover_master() {
# If master already set, use it
if [ -n "$MASTER_IP" ]; then
log "Master IP from config: $MASTER_IP"
echo "$MASTER_IP"
return 0
fi
log "Discovering master..."
# Try mDNS if avahi is available
if command -v avahi-browse >/dev/null 2>&1; then
local mdns_master=$(avahi-browse -rt _secubox._tcp 2>/dev/null | \
grep -oP '\d+\.\d+\.\d+\.\d+' | head -1)
if [ -n "$mdns_master" ]; then
log "Found master via mDNS: $mdns_master"
echo "$mdns_master"
return 0
fi
fi
# Try common gateway (often the master)
local gateway=$(ip route | grep default | awk '{print $3}' | head -1)
if [ -n "$gateway" ]; then
# Check if it responds to SecuBox P2P port
if nc -z -w2 "$gateway" 7331 2>/dev/null; then
log "Found master at gateway: $gateway"
echo "$gateway"
return 0
fi
fi
# Scan common SecuBox subnets
for subnet in 192.168.255 192.168.1 192.168.0; do
if nc -z -w1 "${subnet}.1" 7331 2>/dev/null; then
log "Found master via scan: ${subnet}.1"
echo "${subnet}.1"
return 0
fi
done
log "No master discovered"
return 1
}
# Configure as mesh peer
configure_peer() {
local master="$1"
log "Configuring as mesh peer (upstream: $master)..."
# Set master-link configuration
uci -q batch <<-EOF
set master-link.main=master-link
set master-link.main.enabled='1'
set master-link.main.role='peer'
set master-link.main.upstream='$master'
set master-link.main.auto_approve='0'
commit master-link
EOF
# Mark as clone
mkdir -p /etc/secubox
echo "$master" > /etc/secubox/.clone-master
date -Iseconds > /etc/secubox/.clone-provisioned
log "Peer configuration saved"
}
# Join mesh network
join_mesh() {
local master="$1"
local token="$2"
if [ -n "$token" ]; then
log "Joining mesh with pre-approved token..."
# Use master-link join with token
if [ -x /usr/lib/secubox/master-link.sh ]; then
if /usr/lib/secubox/master-link.sh join "$master" "$token" 2>/dev/null; then
log "Joined mesh successfully with token"
return 0
else
log "Token join failed, falling back to request"
fi
fi
fi
# Request join (requires manual approval on master)
if [ "$AUTO_JOIN" = "1" ]; then
log "Requesting mesh join (needs approval on master)..."
if [ -x /usr/lib/secubox/master-link.sh ]; then
if /usr/lib/secubox/master-link.sh request_join "$master" 2>/dev/null; then
log "Join request sent to master"
return 0
fi
fi
else
log "Auto-join disabled, manual mesh setup required"
fi
return 1
}
# Start SecuBox services
start_services() {
log "Starting SecuBox services..."
# Enable and start core service
if [ -x /etc/init.d/secubox-core ]; then
/etc/init.d/secubox-core enable
/etc/init.d/secubox-core start
fi
# Enable master-link for mesh
if [ -x /etc/init.d/secubox-master-link ]; then
/etc/init.d/secubox-master-link enable
/etc/init.d/secubox-master-link start
fi
log "Services started"
}
# Main provisioning flow
main() {
log "═══════════════════════════════════════════"
log "SecuBox Clone Provisioning Starting"
log "═══════════════════════════════════════════"
# Load any embedded config
load_clone_config
# Step 1: Resize root partition
log "Step 1/4: Resize root partition"
resize_root || log "Resize skipped or failed"
# Step 2: Discover master
log "Step 2/4: Discover master"
MASTER_IP=$(discover_master)
if [ -z "$MASTER_IP" ]; then
log "No master found - standalone mode"
log "Manual mesh setup required: secubox master-link join <master-ip>"
fi
# Step 3: Configure as peer
if [ -n "$MASTER_IP" ]; then
log "Step 3/4: Configure as mesh peer"
configure_peer "$MASTER_IP"
# Step 4: Join mesh
log "Step 4/4: Join mesh"
join_mesh "$MASTER_IP" "$CLONE_TOKEN"
else
log "Step 3/4: Skipped (no master)"
log "Step 4/4: Skipped (no master)"
fi
# Start services
start_services
# Summary
log "═══════════════════════════════════════════"
log "Clone Provisioning Complete"
log "───────────────────────────────────────────"
log "Root size: $(df -h / | tail -1 | awk '{print $2}')"
log "Master: ${MASTER_IP:-none}"
log "Mesh status: $(uci -q get master-link.main.role 2>/dev/null || echo 'unconfigured')"
log "═══════════════════════════════════════════"
# Mark provisioning complete (prevents re-run)
touch "$CLONE_MARKER"
}
# Run if not already provisioned
if [ ! -f "$CLONE_MARKER" ]; then
main
else
logger -t secubox-clone "Clone already provisioned, skipping"
fi
exit 0

View File

@ -23,27 +23,28 @@ Modular OpenWrt Security Appliance Framework
${BOLD}Usage:${NC} secubox <command> [subcommand] [options]
${BOLD}Commands:${NC}
${GREEN}app${NC} Manage modules and AppStore
${GREEN}feed${NC} Manage catalog feed sources
${GREEN}profile${NC} Manage profiles and templates
${GREEN}skill${NC} Discover and manage skills
${GREEN}feedback${NC} Report issues and find resolutions
${GREEN}device${NC} Device information and management
${GREEN}net${NC} Network management
${GREEN}diag${NC} Diagnostics and health checks
${GREEN}landing${NC} Generate landing pages from vhosts
${GREEN}ai${NC} AI copilot (optional)
${GREEN}app${NC} Manage modules and AppStore
${GREEN}feed${NC} Manage catalog feed sources
${GREEN}profile${NC} Manage profiles and templates
${GREEN}skill${NC} Discover and manage skills
${GREEN}feedback${NC} Report issues and find resolutions
${GREEN}device${NC} Device information and management
${GREEN}net${NC} Network management
${GREEN}diag${NC} Diagnostics and health checks
${GREEN}landing${NC} Generate landing pages from vhosts
${GREEN}clone${NC} Station cloning and deployment
${GREEN}master-link${NC} Mesh network management
${GREEN}ai${NC} AI copilot (optional)
${BOLD}Examples:${NC}
secubox app list
secubox app install wireguard-vpn
secubox feed list
secubox feed add my-feed http://example.com/catalog.json --type unpublished
secubox profile export --name "My Setup" --include-feeds
secubox skill list
secubox feedback report luci-app-example --type bug --summary "Crash on load"
secubox profile export --name "My Setup"
secubox diag health
secubox device status
secubox clone build && secubox clone serve --start
secubox master-link status
Run ${BOLD}secubox <command> help${NC} for command-specific help.
EOF
@ -464,6 +465,105 @@ EOF
esac
}
# Clone commands
cmd_clone() {
case "$1" in
build)
shift
/usr/sbin/secubox-cloner build "$@"
;;
serve)
shift
/usr/sbin/secubox-cloner serve "$@"
;;
token)
shift
/usr/sbin/secubox-cloner token "$@"
;;
status)
/usr/sbin/secubox-cloner status
;;
list)
/usr/sbin/secubox-cloner list
;;
export)
shift
/usr/sbin/secubox-cloner export "$@"
;;
help|*)
cat <<EOF
${BOLD}secubox clone${NC} - Station cloning and deployment
${BOLD}Usage:${NC}
secubox clone build [--resize SIZE] Build clone image for current device
secubox clone serve [--start|--stop] Manage TFTP clone server
secubox clone token [--auto-approve] Generate clone join token
secubox clone status Show cloner status
secubox clone list List pending/joined clones
secubox clone export [FILE] Export clone image
${BOLD}Clone Workflow:${NC}
1. ${GREEN}secubox clone build${NC} Build clone image for same device type
2. ${GREEN}secubox clone serve --start${NC} Start TFTP server
3. Boot target from TFTP (see U-Boot commands)
4. ${GREEN}secubox clone list${NC} Verify clone joined mesh
${BOLD}Examples:${NC}
secubox clone build
secubox clone token --auto-approve
secubox clone serve --start
EOF
;;
esac
}
# Master-link shortcut commands
cmd_master_link() {
case "$1" in
status)
/usr/lib/secubox/master-link.sh status
;;
peers)
/usr/lib/secubox/master-link.sh peers
;;
token)
/usr/lib/secubox/master-link.sh token-generate
;;
clone-token)
/usr/lib/secubox/master-link.sh clone-token
;;
join)
shift
/usr/lib/secubox/master-link.sh join "$@"
;;
approve)
/usr/lib/secubox/master-link.sh join-approve "$2"
;;
pending)
ls -la /var/lib/secubox-master-link/requests/ 2>/dev/null || echo "No pending requests"
;;
help|*)
cat <<EOF
${BOLD}secubox master-link${NC} - Mesh network management
${BOLD}Usage:${NC}
secubox master-link status Show mesh status
secubox master-link peers List mesh peers
secubox master-link token Generate join token
secubox master-link clone-token Generate auto-approve clone token
secubox master-link join <ip> Join a mesh (as peer)
secubox master-link approve <fp> Approve pending join request
secubox master-link pending List pending join requests
${BOLD}Examples:${NC}
secubox master-link status
secubox master-link token
secubox master-link approve abc123
EOF
;;
esac
}
# Main command router
case "$1" in
app)
@ -506,6 +606,14 @@ case "$1" in
shift
cmd_ai "$@"
;;
clone|cloner)
shift
cmd_clone "$@"
;;
master-link|mesh)
shift
cmd_master_link "$@"
;;
-v|--version|version)
echo "SecuBox v${SECUBOX_VERSION}"
;;

View File

@ -0,0 +1,730 @@
#!/bin/sh
#
# secubox-cloner - On-device clone image builder and server
#
# Builds and serves SecuBox clone images for same-device-type deployment.
# Cloned devices auto-resize root and join mesh as peers.
#
# Usage:
# secubox-cloner build [--resize SIZE] Build clone image for current device
# secubox-cloner serve [--start|--stop] Manage TFTP clone server
# secubox-cloner token [--auto-approve] Generate clone join token
# secubox-cloner status Show cloner status
# secubox-cloner list List pending/joined clones
# secubox-cloner export [FILE] Export clone image to file
#
VERSION="1.0.0"
# Directories
CLONE_DIR="/srv/secubox/clone"
TFTP_ROOT="/srv/tftp"
TOKENS_DIR="/var/run/secubox/clone-tokens"
STATE_FILE="/var/run/secubox/cloner.state"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
log() { echo -e "${GREEN}[CLONER]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
# ============================================================================
# Device Detection
# ============================================================================
detect_device() {
local board_name=""
# Try /tmp/sysinfo/board_name first (most reliable)
if [ -f /tmp/sysinfo/board_name ]; then
board_name=$(cat /tmp/sysinfo/board_name)
fi
# Fallback to uci
if [ -z "$board_name" ]; then
board_name=$(uci -q get system.@system[0].hostname 2>/dev/null || echo "")
fi
# Map board name to device type
case "$board_name" in
*mochabin*|*MOCHAbin*|globalscale,mochabin)
echo "mochabin"
;;
*espressobin*ultra*|globalscale,espressobin-ultra)
echo "espressobin-ultra"
;;
*espressobin*|globalscale,espressobin*)
echo "espressobin-v7"
;;
*x86*|*generic*)
echo "x86-64"
;;
*)
# Try to detect from kernel
if grep -q "Marvell" /proc/cpuinfo 2>/dev/null; then
if grep -q "Cortex-A72" /proc/cpuinfo 2>/dev/null; then
echo "mochabin"
else
echo "espressobin-v7"
fi
elif uname -m | grep -q "x86_64"; then
echo "x86-64"
else
echo "unknown"
fi
;;
esac
}
get_lan_ip() {
uci -q get network.lan.ipaddr 2>/dev/null || echo "192.168.255.1"
}
get_device_id() {
uci -q get secubox.main.device_id 2>/dev/null || \
(cat /sys/class/net/eth0/address 2>/dev/null | tr -d ':' | sha256sum | cut -d' ' -f1 | cut -c1-16)
}
# ============================================================================
# Image Building
# ============================================================================
build_image() {
local resize_target="${1:-}"
log "Building clone image..."
# Detect device type
local device_type=$(detect_device)
if [ "$device_type" = "unknown" ]; then
error "Could not detect device type"
return 1
fi
log "Device type: $device_type"
# Create directories
mkdir -p "$CLONE_DIR"
mkdir -p "$TFTP_ROOT"
# Method 1: Export current firmware (if sysupgrade backup works)
# This creates a perfect clone of the running system
local image_name="secubox-clone-${device_type}.img"
local image_path="$CLONE_DIR/$image_name"
# Check if we have a pre-built image we can use
local existing_image=$(ls -t "$CLONE_DIR"/*.img.gz 2>/dev/null | head -1)
if [ -n "$existing_image" ]; then
log "Using existing image: $existing_image"
image_path="$existing_image"
else
# Try to build via ASU API if curl is available
if command -v curl >/dev/null 2>&1; then
log "Building image via ASU API..."
build_via_asu "$device_type" "$image_path" || {
warn "ASU build failed, trying local export..."
build_local_export "$image_path"
}
else
build_local_export "$image_path"
fi
fi
# Inject clone provisioning script
inject_clone_provision "$image_path"
# Optionally resize image
if [ -n "$resize_target" ]; then
resize_image "$image_path" "$resize_target"
fi
# Copy to TFTP root
log "Copying to TFTP root..."
if [ -f "$image_path" ]; then
local tftp_image="$TFTP_ROOT/secubox-clone.img"
if echo "$image_path" | grep -q "\.gz$"; then
gunzip -c "$image_path" > "$tftp_image"
else
cp "$image_path" "$tftp_image"
fi
log "Clone image ready: $tftp_image"
log "Size: $(ls -lh "$tftp_image" | awk '{print $5}')"
fi
# Save state
cat > "$STATE_FILE" <<EOF
DEVICE_TYPE="$device_type"
IMAGE_PATH="$image_path"
TFTP_IMAGE="$TFTP_ROOT/secubox-clone.img"
BUILD_TIME="$(date -Iseconds)"
LAN_IP="$(get_lan_ip)"
EOF
return 0
}
build_via_asu() {
local device_type="$1"
local output="$2"
# Map device type to ASU profile
local target=""
local profile=""
case "$device_type" in
mochabin)
target="mvebu/cortexa72"
profile="globalscale_mochabin"
;;
espressobin-v7)
target="mvebu/cortexa53"
profile="globalscale_espressobin"
;;
espressobin-ultra)
target="mvebu/cortexa53"
profile="globalscale_espressobin-ultra"
;;
x86-64)
target="x86/64"
profile="generic"
;;
*)
error "Unknown device type: $device_type"
return 1
;;
esac
# Get installed packages (for cloning same config)
local packages=$(opkg list-installed 2>/dev/null | awk '{print $1}' | grep -v "^kernel" | tr '\n' ' ')
# Minimal clone packages (core system + SecuBox essentials)
local clone_packages="luci luci-ssl dnsmasq-full curl wget-ssl ca-certificates"
clone_packages="$clone_packages wireguard-tools luci-proto-wireguard"
clone_packages="$clone_packages haproxy docker lxc lxc-attach"
clone_packages="$clone_packages block-mount e2fsprogs parted"
# Build request JSON
local request_json="/tmp/asu-request.json"
cat > "$request_json" <<EOF
{
"profile": "$profile",
"target": "$target",
"version": "24.10.5",
"packages": "$clone_packages",
"rootfs_size_mb": 1024,
"client": "secubox-cloner/1.0"
}
EOF
log "Submitting ASU build request..."
local response
response=$(curl -s -w "\n%{http_code}" \
-H "Content-Type: application/json" \
-d "@$request_json" \
"https://sysupgrade.openwrt.org/api/v1/build" 2>/dev/null)
local http_code=$(echo "$response" | tail -1)
local body=$(echo "$response" | sed '$d')
if [ "$http_code" = "200" ] || [ "$http_code" = "202" ]; then
# Extract request hash
local request_hash=$(echo "$body" | jsonfilter -e '@.request_hash' 2>/dev/null)
if [ -n "$request_hash" ]; then
log "Build queued: $request_hash"
log "This may take several minutes..."
# Poll for completion
local max_wait=600
local elapsed=0
while [ $elapsed -lt $max_wait ]; do
sleep 10
elapsed=$((elapsed + 10))
response=$(curl -s "https://sysupgrade.openwrt.org/api/v1/build/$request_hash" 2>/dev/null)
local images=$(echo "$response" | jsonfilter -e '@.images[*]' 2>/dev/null | wc -l)
if [ "$images" -gt 0 ]; then
# Find ext4-sdcard image
local image_name=$(echo "$response" | jsonfilter -e '@.images[*].name' 2>/dev/null | grep -E "ext4.*sdcard" | head -1)
if [ -z "$image_name" ]; then
image_name=$(echo "$response" | jsonfilter -e '@.images[0].name' 2>/dev/null)
fi
if [ -n "$image_name" ]; then
log "Downloading: $image_name"
curl -# -o "$output" \
"https://sysupgrade.openwrt.org/store/$request_hash/$image_name" || {
error "Download failed"
return 1
}
log "Image downloaded: $output"
return 0
fi
fi
printf "\r Waiting... %ds " "$elapsed" >&2
done
error "Build timed out"
return 1
fi
fi
error "ASU request failed: HTTP $http_code"
return 1
}
build_local_export() {
local output="$1"
log "Creating local system export..."
# Use sysupgrade backup as base
local backup="/tmp/secubox-backup.tar.gz"
sysupgrade -b "$backup" 2>/dev/null
if [ -f "$backup" ]; then
log "System backup created: $(ls -lh "$backup" | awk '{print $5}')"
# For now, just note that we need a pre-built base image
warn "Full image build requires base firmware image"
warn "Use ASU API or provide pre-built image in $CLONE_DIR"
return 1
fi
return 1
}
resize_image() {
local image="$1"
local target_size="$2"
log "Resizing image to $target_size..."
# Check for resize tools
if ! command -v parted >/dev/null 2>&1; then
warn "parted not available, skipping resize"
return 1
fi
# This would use the resize-openwrt-image.sh approach
# For on-device use, we'll defer resize to first boot
log "Image will auto-resize to full disk at first boot"
return 0
}
inject_clone_provision() {
local image="$1"
local master_ip=$(get_lan_ip)
log "Generating clone provision script..."
# Create provision script for TFTP download at first boot
local script="$TFTP_ROOT/clone-provision.sh"
cat > "$script" <<PROVISION
#!/bin/sh
# SecuBox Clone Provision Script
# Downloaded and executed at first boot by cloned device
MASTER_IP="$master_ip"
CLONE_TOKEN="${CLONE_TOKEN:-}"
log() { logger -t secubox-clone "\$*"; echo "\$*"; }
log "SecuBox clone provisioning starting..."
log "Master: \$MASTER_IP"
# Step 1: Resize root partition to full disk
log "Resizing root partition..."
ROOT_DEV=\$(awk '\$2=="/" {print \$1}' /proc/mounts)
if [ -n "\$ROOT_DEV" ]; then
DISK=\$(echo "\$ROOT_DEV" | sed 's/p\\?[0-9]*\$//')
[ "\$DISK" = "\$ROOT_DEV" ] && DISK=\$(echo "\$ROOT_DEV" | sed 's/[0-9]*\$//')
PART_NUM=\$(echo "\$ROOT_DEV" | grep -o '[0-9]*\$')
if command -v parted >/dev/null 2>&1; then
parted -s "\$DISK" resizepart "\$PART_NUM" 100% 2>/dev/null || true
resize2fs "\$ROOT_DEV" 2>/dev/null || true
sync
log "Root resized: \$(df -h / | tail -1 | awk '{print \$2}')"
else
log "parted not available, skipping resize"
fi
fi
# Step 2: Configure as mesh peer
log "Configuring as mesh peer..."
uci set master-link.main=master-link
uci set master-link.main.role='peer'
uci set master-link.main.upstream="\$MASTER_IP"
uci commit master-link
# Step 3: Join mesh
if [ -n "\$CLONE_TOKEN" ]; then
log "Joining mesh with pre-approved token..."
/usr/lib/secubox/master-link.sh join "\$MASTER_IP" "\$CLONE_TOKEN" 2>/dev/null || {
log "Token join failed, requesting approval..."
/usr/lib/secubox/master-link.sh request_join "\$MASTER_IP" 2>/dev/null || true
}
else
log "No token provided, requesting mesh join (needs approval)..."
/usr/lib/secubox/master-link.sh request_join "\$MASTER_IP" 2>/dev/null || true
fi
# Step 4: Start SecuBox services
log "Starting SecuBox services..."
/etc/init.d/secubox-core enable 2>/dev/null
/etc/init.d/secubox-core start 2>/dev/null
log "Clone provisioning complete"
PROVISION
chmod +x "$script"
log "Clone provision script: $script"
}
# ============================================================================
# TFTP Server
# ============================================================================
serve_start() {
log "Starting clone TFTP server..."
# Use existing TFTP recovery infrastructure
if [ -x /usr/sbin/secubox-tftp-recovery ]; then
/usr/sbin/secubox-tftp-recovery start
else
# Manual dnsmasq TFTP setup
mkdir -p "$TFTP_ROOT"
uci -q set dhcp.@dnsmasq[0].enable_tftp='1'
uci -q set dhcp.@dnsmasq[0].tftp_root="$TFTP_ROOT"
uci commit dhcp
/etc/init.d/dnsmasq restart
fi
# Generate U-Boot script for clone
generate_uboot_script
local lan_ip=$(get_lan_ip)
log "TFTP server running"
log "Clone image: $TFTP_ROOT/secubox-clone.img"
log "Server IP: $lan_ip:69"
echo ""
echo "To flash a target device, use these U-Boot commands:"
echo " setenv serverip $lan_ip"
echo " dhcp"
echo " tftpboot 0x20000000 secubox-clone.img"
echo " mmc dev 0"
echo ' mmc write 0x20000000 0 ${filesize}'
echo " reset"
}
serve_stop() {
log "Stopping clone TFTP server..."
if [ -x /usr/sbin/secubox-tftp-recovery ]; then
/usr/sbin/secubox-tftp-recovery stop
else
uci -q set dhcp.@dnsmasq[0].enable_tftp='0'
uci commit dhcp
/etc/init.d/dnsmasq restart
fi
log "TFTP server stopped"
}
generate_uboot_script() {
local lan_ip=$(get_lan_ip)
local script_dir="$TFTP_ROOT"
# Generate human-readable U-Boot commands
cat > "$script_dir/clone-uboot.txt" <<UBOOT
# SecuBox Clone Boot Commands
# Run these in U-Boot to flash the clone image
# 1. Set network
setenv serverip $lan_ip
setenv ipaddr 192.168.1.100
# 2. Get IP via DHCP (optional)
dhcp
# 3. Download clone image
tftpboot 0x20000000 secubox-clone.img
# 4. Write to eMMC/SD
mmc dev 0
mmc write 0x20000000 0 \${filesize}
# 5. Boot cloned system
reset
UBOOT
log "U-Boot commands: $script_dir/clone-uboot.txt"
}
# ============================================================================
# Token Management
# ============================================================================
generate_token() {
local auto_approve="${1:-0}"
local ttl="${2:-86400}" # 24 hours default
mkdir -p "$TOKENS_DIR"
# Generate random token
local token=$(head -c 32 /dev/urandom | sha256sum | cut -d' ' -f1)
# Save token metadata
local token_file="$TOKENS_DIR/${token}.json"
cat > "$token_file" <<EOF
{
"token": "$token",
"created": "$(date -Iseconds)",
"expires": "$(date -d "+${ttl} seconds" -Iseconds 2>/dev/null || date -Iseconds)",
"ttl": $ttl,
"auto_approve": $([ "$auto_approve" = "1" ] && echo "true" || echo "false"),
"type": "clone",
"used": false
}
EOF
# Also register with master-link if available
if [ -x /usr/lib/secubox/master-link.sh ]; then
/usr/lib/secubox/master-link.sh register-token "$token" "$ttl" "clone" >/dev/null 2>&1 || true
fi
# Export for use in provision script
export CLONE_TOKEN="$token"
echo "$token"
}
# ============================================================================
# Status & Listing
# ============================================================================
show_status() {
echo ""
echo -e "${BOLD}SecuBox Cloner v$VERSION${NC}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Device info
local device_type=$(detect_device)
local device_id=$(get_device_id)
local lan_ip=$(get_lan_ip)
echo "Device: $device_type"
echo "Device ID: ${device_id:0:16}..."
echo "LAN IP: $lan_ip"
echo ""
# Build state
echo "Clone Images:"
if [ -d "$CLONE_DIR" ]; then
for img in "$CLONE_DIR"/*.img "$CLONE_DIR"/*.img.gz 2>/dev/null; do
[ -f "$img" ] || continue
local size=$(ls -lh "$img" 2>/dev/null | awk '{print $5}')
echo " - $(basename "$img") ($size)"
done
fi
if [ -f "$TFTP_ROOT/secubox-clone.img" ]; then
local size=$(ls -lh "$TFTP_ROOT/secubox-clone.img" | awk '{print $5}')
echo " - TFTP: secubox-clone.img ($size)"
fi
echo ""
# TFTP status
local tftp_enabled=$(uci -q get dhcp.@dnsmasq[0].enable_tftp)
if [ "$tftp_enabled" = "1" ]; then
echo -e "TFTP: ${GREEN}Running${NC} ($lan_ip:69)"
else
echo -e "TFTP: ${YELLOW}Stopped${NC}"
fi
echo ""
# Pending tokens
echo "Clone Tokens:"
local token_count=0
if [ -d "$TOKENS_DIR" ]; then
for tf in "$TOKENS_DIR"/*.json 2>/dev/null; do
[ -f "$tf" ] || continue
local token=$(jsonfilter -i "$tf" -e '@.token' 2>/dev/null | cut -c1-16)
local used=$(jsonfilter -i "$tf" -e '@.used' 2>/dev/null)
local auto=$(jsonfilter -i "$tf" -e '@.auto_approve' 2>/dev/null)
if [ "$used" = "true" ]; then
echo " - ${token}... (used)"
elif [ "$auto" = "true" ]; then
echo " - ${token}... (auto-approve)"
else
echo " - ${token}... (pending)"
fi
token_count=$((token_count + 1))
done
fi
[ $token_count -eq 0 ] && echo " (none)"
echo ""
# Mesh peers (clones)
echo "Joined Clones:"
if [ -x /usr/lib/secubox/master-link.sh ]; then
/usr/lib/secubox/master-link.sh peers 2>/dev/null | head -5 || echo " (none)"
else
echo " (master-link not available)"
fi
echo ""
}
list_clones() {
echo ""
echo -e "${BOLD}Cloned Devices${NC}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# List from master-link
if [ -x /usr/lib/secubox/master-link.sh ]; then
echo ""
echo "Active Peers:"
/usr/lib/secubox/master-link.sh peers 2>/dev/null || echo " (none)"
echo ""
echo "Pending Approvals:"
/usr/lib/secubox/master-link.sh pending 2>/dev/null || echo " (none)"
else
echo "master-link not available"
fi
echo ""
}
export_image() {
local output="${1:-/tmp/secubox-clone-$(date +%Y%m%d).img.gz}"
if [ -f "$TFTP_ROOT/secubox-clone.img" ]; then
log "Exporting clone image..."
gzip -c "$TFTP_ROOT/secubox-clone.img" > "$output"
log "Exported: $output"
log "Size: $(ls -lh "$output" | awk '{print $5}')"
elif [ -d "$CLONE_DIR" ]; then
local latest=$(ls -t "$CLONE_DIR"/*.img* 2>/dev/null | head -1)
if [ -n "$latest" ]; then
log "Copying: $latest"
cp "$latest" "$output"
log "Exported: $output"
else
error "No clone image available. Run: secubox-cloner build"
return 1
fi
else
error "No clone image available. Run: secubox-cloner build"
return 1
fi
}
# ============================================================================
# Usage
# ============================================================================
usage() {
cat <<'EOF'
SecuBox Cloner - On-device clone image builder and server
Usage: secubox-cloner <command> [options]
Commands:
build [--resize SIZE] Build clone image for current device type
serve [--start|--stop] Manage TFTP clone server
token [--auto-approve] Generate clone join token (24h TTL)
status Show cloner status (images, tokens, clones)
list List pending and joined clones
export [FILE] Export clone image to file
Options:
--resize SIZE Pre-resize image (e.g., 16G for eMMC)
--auto-approve Token auto-approves clone join (no manual step)
Examples:
# Build and serve clone image
secubox-cloner build
secubox-cloner serve --start
# Generate auto-approve token
secubox-cloner token --auto-approve
# Check status
secubox-cloner status
# Export for USB transfer
secubox-cloner export /mnt/usb/clone.img.gz
Clone Workflow:
1. Master: secubox-cloner build && secubox-cloner serve --start
2. Target: Boot via TFTP (U-Boot commands shown after serve)
3. Target auto-resizes root and joins mesh
4. Master: secubox-cloner list (shows new clone)
EOF
}
# ============================================================================
# Main
# ============================================================================
case "${1:-}" in
build)
shift
resize=""
while [ $# -gt 0 ]; do
case "$1" in
--resize) resize="$2"; shift 2 ;;
*) shift ;;
esac
done
build_image "$resize"
;;
serve)
case "${2:-start}" in
--start|start) serve_start ;;
--stop|stop) serve_stop ;;
*) serve_start ;;
esac
;;
token)
auto_approve=0
[ "$2" = "--auto-approve" ] && auto_approve=1
generate_token "$auto_approve"
;;
status)
show_status
;;
list)
list_clones
;;
export)
shift
export_image "$@"
;;
help|--help|-h|"")
usage
;;
*)
error "Unknown command: $1"
echo ""
usage >&2
exit 1
;;
esac

View File

@ -194,6 +194,89 @@ ml_token_cleanup() {
echo "{\"cleaned\":$cleaned}"
}
# Generate clone-specific token with auto-approve
# Used by secubox-cloner for station cloning
ml_clone_token_generate() {
ml_init
local ttl="${1:-86400}" # 24 hours default for clones
local token_type="${2:-clone}"
local now=$(date +%s)
local expires=$((now + ttl))
local rand=$(head -c 32 /dev/urandom 2>/dev/null | sha256sum | cut -d' ' -f1)
[ -z "$rand" ] && rand=$(date +%s%N | sha256sum | cut -d' ' -f1)
# HMAC token using master key
local key_data=$(cat "$KEYFILE" 2>/dev/null)
local token=$(echo "${key_data}:clone:${rand}:${now}" | sha256sum | cut -d' ' -f1)
local token_hash=$(echo "$token" | sha256sum | cut -d' ' -f1)
# Store token in UCI with auto_approve flag
local section_id="clone_$(echo "$token_hash" | cut -c1-8)"
uci -q batch <<-EOF
set master-link.${section_id}=token
set master-link.${section_id}.hash='${token_hash}'
set master-link.${section_id}.created='${now}'
set master-link.${section_id}.expires='${expires}'
set master-link.${section_id}.peer_fp=''
set master-link.${section_id}.status='active'
set master-link.${section_id}.type='${token_type}'
set master-link.${section_id}.auto_approve='1'
EOF
uci commit master-link
# Store full token locally for validation
echo "$token" > "$ML_TOKENS_DIR/${token_hash}"
# Record in blockchain
local fp=$(factory_fingerprint 2>/dev/null)
chain_add_block "clone_token_generated" \
"{\"token_hash\":\"$token_hash\",\"type\":\"$token_type\",\"expires\":$expires,\"created_by\":\"$fp\"}" \
"$(echo "clone_token:${token_hash}:${now}" | sha256sum | cut -d' ' -f1)" >/dev/null 2>&1
# Build join URL
local my_addr=$(uci -q get network.lan.ipaddr)
[ -z "$my_addr" ] && my_addr=$(ip -4 addr show br-lan 2>/dev/null | grep -oP 'inet \K[0-9.]+' | head -1)
logger -t master-link "Clone token generated: ${token_hash} (auto-approve, expires: $(date -d @$expires -Iseconds 2>/dev/null || echo $expires))"
cat <<-EOF
{
"token": "$token",
"token_hash": "$token_hash",
"type": "$token_type",
"auto_approve": true,
"expires": $expires,
"ttl": $ttl,
"master_ip": "$my_addr",
"url": "http://${my_addr}:${MESH_PORT}/master-link/?token=${token}&type=clone"
}
EOF
}
# Check if token is a clone token with auto-approve
ml_token_is_auto_approve() {
local token="$1"
local token_hash=$(echo "$token" | sha256sum | cut -d' ' -f1)
# Find token in UCI
local sections=$(uci -q show master-link 2>/dev/null | grep "=token$" | sed "s/master-link\.\(.*\)=token/\1/")
for sec in $sections; do
local hash=$(uci -q get "master-link.${sec}.hash")
if [ "$hash" = "$token_hash" ]; then
local auto=$(uci -q get "master-link.${sec}.auto_approve")
if [ "$auto" = "1" ]; then
echo "true"
return 0
fi
break
fi
done
echo "false"
return 1
}
# ============================================================================
# Join Protocol
# ============================================================================
@ -236,9 +319,12 @@ ml_join_request() {
logger -t master-link "Join request from $peer_hostname ($peer_fp) at $peer_addr"
# Check auto-approve
# Check auto-approve: either global setting or token-specific (clone tokens)
local auto_approve=$(uci -q get master-link.main.auto_approve)
if [ "$auto_approve" = "1" ]; then
local token_auto=$(ml_token_is_auto_approve "$token")
if [ "$auto_approve" = "1" ] || [ "$token_auto" = "true" ]; then
logger -t master-link "Auto-approving join for $peer_fp (global=$auto_approve, token=$token_auto)"
ml_join_approve "$peer_fp"
return $?
fi
@ -871,6 +957,37 @@ case "${1:-}" in
token-cleanup)
ml_token_cleanup
;;
clone-token|generate-clone-token)
# Generate clone-specific auto-approve token
# Usage: master-link.sh clone-token [ttl]
ml_clone_token_generate "${2:-86400}" "clone"
;;
register-token)
# Register external token (for secubox-cloner integration)
# Usage: master-link.sh register-token <token> <ttl> <type>
local ext_token="$2"
local ext_ttl="${3:-86400}"
local ext_type="${4:-clone}"
local token_hash=$(echo "$ext_token" | sha256sum | cut -d' ' -f1)
local now=$(date +%s)
local expires=$((now + ext_ttl))
local section_id="ext_$(echo "$token_hash" | cut -c1-8)"
uci -q batch <<-EOF
set master-link.${section_id}=token
set master-link.${section_id}.hash='${token_hash}'
set master-link.${section_id}.created='${now}'
set master-link.${section_id}.expires='${expires}'
set master-link.${section_id}.peer_fp=''
set master-link.${section_id}.status='active'
set master-link.${section_id}.type='${ext_type}'
set master-link.${section_id}.auto_approve='1'
EOF
uci commit master-link
mkdir -p "$ML_TOKENS_DIR"
echo "$ext_token" > "$ML_TOKENS_DIR/${token_hash}"
logger -t master-link "External token registered: ${token_hash} (type=$ext_type, expires=$(date -d @$expires -Iseconds 2>/dev/null || echo $expires))"
echo "{\"registered\":true,\"token_hash\":\"$token_hash\",\"expires\":$expires}"
;;
join-request)
ml_join_request "$2" "$3" "$4" "$5"
;;

View File

@ -0,0 +1,943 @@
#!/bin/bash
#
# secubox-clone-station.sh - Host-side SecuBox Station Cloner
#
# Orchestrates cloning of SecuBox devices via dual USB serial:
# - Master device: Extract config, build clone image, generate join token
# - Target device: Enter U-Boot, flash image, auto-join mesh
#
# Dependencies:
# - MOKATOOL (mochabin_tool.py) for serial console automation
# - secubox-image.sh for ASU API firmware building
# - TFTP server (dnsmasq or tftpd-hpa)
#
# Usage:
# ./secubox-clone-station.sh detect # Detect serial devices
# ./secubox-clone-station.sh pull [--master DEV] # Pull image from master
# ./secubox-clone-station.sh flash [--target DEV] # Flash image to target
# ./secubox-clone-station.sh clone # Full workflow
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SECUBOX_ROOT="$(dirname "$SCRIPT_DIR")"
# MOKATOOL location
MOKATOOL_DIR="${MOKATOOL_DIR:-/home/reepost/DEVEL/MOKATOOL}"
MOKATOOL="$MOKATOOL_DIR/mochabin_tool.py"
# Clone station directories
CLONE_DIR="$SCRIPT_DIR/clone-station"
CLONE_IMAGES="$CLONE_DIR/images"
CLONE_LOGS="$CLONE_DIR/logs"
TFTP_ROOT="${TFTP_ROOT:-/srv/tftp}"
# Defaults
BAUDRATE=115200
DEFAULT_MASTER=""
DEFAULT_TARGET=""
MASTER_DEV=""
TARGET_DEV=""
OPENWRT_VERSION="24.10.5"
CLONE_TOKEN=""
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
log_step() { echo -e "${CYAN}[STEP]${NC} $*"; }
# =============================================================================
# Helpers
# =============================================================================
check_deps() {
local missing=0
# Check MOKATOOL
if [[ ! -x "$MOKATOOL" ]]; then
log_error "MOKATOOL not found: $MOKATOOL"
log_info "Set MOKATOOL_DIR environment variable or install from ~/DEVEL/MOKATOOL"
missing=1
fi
# Check Python dependencies
if ! python3 -c "import serial, pexpect, rich, typer" 2>/dev/null; then
log_warn "Missing Python deps. Install: pip install 'typer[all]' pyserial pexpect rich pyyaml"
fi
# Check TFTP directory
if [[ ! -d "$TFTP_ROOT" ]]; then
log_warn "TFTP root not found: $TFTP_ROOT"
log_info "Create with: sudo mkdir -p $TFTP_ROOT && sudo chmod 777 $TFTP_ROOT"
fi
return $missing
}
mokatool() {
python3 "$MOKATOOL" "$@"
}
# Create directory structure
init_dirs() {
mkdir -p "$CLONE_IMAGES"
mkdir -p "$CLONE_LOGS"
mkdir -p "$TFTP_ROOT" 2>/dev/null || true
}
# Get timestamp for logging
get_tag() {
date +%Y%m%d-%H%M%S
}
# =============================================================================
# Device Detection
# =============================================================================
detect_devices() {
log_step "Detecting USB serial devices..."
local found_master=""
local found_target=""
# Use MOKATOOL to list ports
mokatool list-ports 2>/dev/null || true
echo ""
# Scan each USB serial device
for dev in /dev/ttyUSB* /dev/ttyACM*; do
[[ -c "$dev" ]] || continue
log_info "Probing $dev..."
# Try to detect device type by sending CR and checking response
local response=""
response=$(timeout 3 python3 -c "
import serial
import time
try:
ser = serial.Serial('$dev', $BAUDRATE, timeout=1)
time.sleep(0.2)
ser.write(b'\\r\\n')
time.sleep(0.5)
data = ser.read(1000).decode('utf-8', errors='ignore')
print(data)
ser.close()
except Exception as e:
print(f'ERROR: {e}')
" 2>/dev/null || echo "")
if echo "$response" | grep -qiE "(SecuBox|OpenWrt|root@|BusyBox)"; then
log_info "${GREEN}MASTER${NC}: $dev (SecuBox/OpenWrt running)"
found_master="$dev"
MASTER_DEV="$dev"
elif echo "$response" | grep -qiE "(U-Boot|=>|Marvell|Hit any key)"; then
log_info "${YELLOW}TARGET${NC}: $dev (U-Boot prompt detected)"
found_target="$dev"
TARGET_DEV="$dev"
elif echo "$response" | grep -qiE "login:"; then
log_info "${GREEN}MASTER${NC}: $dev (Linux login prompt)"
found_master="$dev"
MASTER_DEV="$dev"
else
log_info " → Unknown/no response"
fi
done
echo ""
echo -e "${BOLD}Detection Summary:${NC}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [[ -n "$MASTER_DEV" ]]; then
echo -e " Master: ${GREEN}$MASTER_DEV${NC}"
else
echo -e " Master: ${RED}Not found${NC}"
fi
if [[ -n "$TARGET_DEV" ]]; then
echo -e " Target: ${GREEN}$TARGET_DEV${NC}"
else
echo -e " Target: ${YELLOW}Not found (or not at U-Boot)${NC}"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Save to state file
cat > "$CLONE_DIR/.detected" <<EOF
MASTER_DEV="$MASTER_DEV"
TARGET_DEV="$TARGET_DEV"
DETECTED_AT="$(date -Iseconds)"
EOF
}
load_detected() {
if [[ -f "$CLONE_DIR/.detected" ]]; then
source "$CLONE_DIR/.detected"
fi
}
# =============================================================================
# Master Operations
# =============================================================================
get_master_info() {
local master="${1:-$MASTER_DEV}"
[[ -z "$master" ]] && { log_error "No master device"; return 1; }
log_step "Getting master device info..."
# Use Python to send commands and get output
python3 -c "
import serial
import time
import sys
dev = '$master'
try:
ser = serial.Serial(dev, $BAUDRATE, timeout=2)
time.sleep(0.2)
# Get board info
ser.write(b'cat /tmp/sysinfo/board_name 2>/dev/null || echo unknown\\n')
time.sleep(0.3)
board = ser.read(500).decode('utf-8', errors='ignore').strip().split('\\n')[-1]
# Get SecuBox version
ser.write(b'secubox --version 2>/dev/null || echo unknown\\n')
time.sleep(0.3)
version = ser.read(500).decode('utf-8', errors='ignore').strip().split('\\n')[-1]
# Get device ID
ser.write(b'uci -q get secubox.main.device_id 2>/dev/null || echo unknown\\n')
time.sleep(0.3)
device_id = ser.read(500).decode('utf-8', errors='ignore').strip().split('\\n')[-1]
# Get LAN IP
ser.write(b'uci -q get network.lan.ipaddr 2>/dev/null || echo 192.168.1.1\\n')
time.sleep(0.3)
lan_ip = ser.read(500).decode('utf-8', errors='ignore').strip().split('\\n')[-1]
ser.close()
print(f'Board: {board}')
print(f'SecuBox: {version}')
print(f'Device ID: {device_id}')
print(f'LAN IP: {lan_ip}')
except Exception as e:
print(f'Error: {e}', file=sys.stderr)
sys.exit(1)
" 2>&1
}
generate_clone_token() {
local master="${1:-$MASTER_DEV}"
[[ -z "$master" ]] && { log_error "No master device"; return 1; }
log_step "Generating clone token on master..."
# Send command to generate token
CLONE_TOKEN=$(python3 -c "
import serial
import time
import sys
dev = '$master'
try:
ser = serial.Serial(dev, $BAUDRATE, timeout=5)
time.sleep(0.2)
# Generate auto-approve clone token (valid 24h)
ser.write(b'secubox-cloner token --auto-approve 2>/dev/null || /usr/lib/secubox/master-link.sh generate-token 86400 clone\\n')
time.sleep(1)
output = ser.read(2000).decode('utf-8', errors='ignore')
# Extract token (should be a hex string)
for line in output.strip().split('\\n'):
line = line.strip()
if len(line) == 64 and all(c in '0123456789abcdef' for c in line):
print(line)
break
ser.close()
except Exception as e:
print(f'Error: {e}', file=sys.stderr)
sys.exit(1)
" 2>&1)
if [[ -n "$CLONE_TOKEN" && ${#CLONE_TOKEN} -eq 64 ]]; then
log_info "Clone token: ${CLONE_TOKEN:0:16}...${CLONE_TOKEN: -8}"
echo "$CLONE_TOKEN" > "$CLONE_DIR/.clone_token"
return 0
else
log_warn "Could not generate clone token on master"
log_info "Token will need to be generated manually or clone will request approval"
return 1
fi
}
# =============================================================================
# Image Building
# =============================================================================
build_clone_image() {
local device="${1:-mochabin}"
log_step "Building clone image for $device via ASU API..."
# Use existing secubox-image.sh
local image_script="$SCRIPT_DIR/secubox-image.sh"
if [[ ! -x "$image_script" ]]; then
log_error "secubox-image.sh not found: $image_script"
return 1
fi
# Build with ext4 (needed for resize)
mkdir -p "$CLONE_IMAGES"
local image_file
image_file=$("$image_script" --output "$CLONE_IMAGES" build "$device") || {
log_error "Image build failed"
return 1
}
log_info "Clone image built: $image_file"
echo "$image_file"
}
inject_clone_config() {
local image="$1"
local master_ip="$2"
local token="${3:-}"
log_step "Injecting clone configuration into image..."
# This would require mounting the image and modifying it
# For now, we'll create a companion script that gets downloaded at first boot
local clone_script="$CLONE_DIR/clone-provision.sh"
cat > "$clone_script" <<CLONESCRIPT
#!/bin/sh
# SecuBox Clone Auto-Provision Script
# Downloaded and executed at first boot
MASTER_IP="$master_ip"
CLONE_TOKEN="$token"
log() { logger -t secubox-clone "\$*"; echo "\$*"; }
log "SecuBox clone provisioning starting..."
log "Master: \$MASTER_IP"
# Step 1: Resize root partition
log "Resizing root partition..."
ROOT_DEV=\$(awk '\$2=="/" {print \$1}' /proc/mounts)
if [ -n "\$ROOT_DEV" ]; then
DISK=\$(echo "\$ROOT_DEV" | sed 's/p\\?[0-9]*\$//')
[ "\$DISK" = "\$ROOT_DEV" ] && DISK=\$(echo "\$ROOT_DEV" | sed 's/[0-9]*\$//')
PART_NUM=\$(echo "\$ROOT_DEV" | grep -o '[0-9]*\$')
if command -v parted >/dev/null 2>&1; then
parted -s "\$DISK" resizepart "\$PART_NUM" 100% 2>/dev/null || true
resize2fs "\$ROOT_DEV" 2>/dev/null || true
log "Root resized: \$(df -h / | tail -1 | awk '{print \$2}')"
fi
fi
# Step 2: Configure as mesh slave
log "Configuring as mesh peer..."
uci set master-link.main=master-link
uci set master-link.main.role='peer'
uci set master-link.main.upstream="\$MASTER_IP"
uci commit master-link
# Step 3: Join mesh
if [ -n "\$CLONE_TOKEN" ]; then
log "Joining mesh with pre-approved token..."
/usr/lib/secubox/master-link.sh join "\$MASTER_IP" "\$CLONE_TOKEN" 2>/dev/null || {
log "Join with token failed, requesting approval..."
/usr/lib/secubox/master-link.sh request_join "\$MASTER_IP" 2>/dev/null || true
}
else
log "Requesting mesh join (manual approval required)..."
/usr/lib/secubox/master-link.sh request_join "\$MASTER_IP" 2>/dev/null || true
fi
log "Clone provisioning complete"
CLONESCRIPT
chmod +x "$clone_script"
log_info "Clone provision script: $clone_script"
# Copy to TFTP for first-boot download
if [[ -d "$TFTP_ROOT" ]]; then
cp "$clone_script" "$TFTP_ROOT/clone-provision.sh"
log_info "Script available via TFTP: clone-provision.sh"
fi
}
# =============================================================================
# Pull from Master
# =============================================================================
cmd_pull() {
local master="${MASTER_DEV:-}"
# Parse args
while [[ $# -gt 0 ]]; do
case "$1" in
--master) master="$2"; shift 2 ;;
*) shift ;;
esac
done
load_detected
[[ -z "$master" ]] && master="$MASTER_DEV"
[[ -z "$master" ]] && { log_error "No master device. Run: $0 detect"; return 1; }
init_dirs
local tag=$(get_tag)
echo -e "${BOLD}SecuBox Clone Station - Pull from Master${NC}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Master: $master"
echo "Tag: $tag"
echo ""
# Get master info
get_master_info "$master"
echo ""
# Detect device type from master
local board_name
board_name=$(python3 -c "
import serial
import time
ser = serial.Serial('$master', $BAUDRATE, timeout=2)
time.sleep(0.2)
ser.write(b'cat /tmp/sysinfo/board_name 2>/dev/null\\n')
time.sleep(0.3)
output = ser.read(500).decode('utf-8', errors='ignore')
for line in output.strip().split('\\n'):
if 'mochabin' in line.lower() or 'espressobin' in line.lower() or 'x86' in line.lower():
print(line.strip())
break
ser.close()
" 2>/dev/null || echo "")
local device_type="mochabin" # default
case "$board_name" in
*mochabin*|*MOCHAbin*) device_type="mochabin" ;;
*espressobin*ultra*) device_type="espressobin-ultra" ;;
*espressobin*) device_type="espressobin-v7" ;;
*x86*|*generic*) device_type="x86-64" ;;
esac
log_info "Detected device type: $device_type"
# Get master LAN IP
local master_ip
master_ip=$(python3 -c "
import serial
import time
ser = serial.Serial('$master', $BAUDRATE, timeout=2)
time.sleep(0.2)
ser.write(b'uci -q get network.lan.ipaddr\\n')
time.sleep(0.3)
output = ser.read(500).decode('utf-8', errors='ignore')
for line in output.strip().split('\\n'):
if '.' in line and not line.startswith('uci'):
print(line.strip())
break
ser.close()
" 2>/dev/null || echo "192.168.255.1")
log_info "Master LAN IP: $master_ip"
# Generate clone token
generate_clone_token "$master" || true
# Build clone image
log_step "Building clone image for $device_type..."
local image_file
image_file=$(build_clone_image "$device_type") || {
log_error "Failed to build clone image"
return 1
}
# Inject clone config
local token=""
[[ -f "$CLONE_DIR/.clone_token" ]] && token=$(cat "$CLONE_DIR/.clone_token")
inject_clone_config "$image_file" "$master_ip" "$token"
# Copy image to TFTP
if [[ -d "$TFTP_ROOT" ]]; then
log_step "Copying image to TFTP root..."
# Decompress if gzipped
local tftp_image="$TFTP_ROOT/secubox-clone.img"
if [[ "$image_file" == *.gz ]]; then
gunzip -c "$image_file" > "$tftp_image"
else
cp "$image_file" "$tftp_image"
fi
log_info "TFTP image ready: $tftp_image"
log_info "Size: $(du -h "$tftp_image" | awk '{print $1}')"
fi
# Generate U-Boot commands for target
local host_ip
host_ip=$(ip route get 8.8.8.8 2>/dev/null | grep -oP 'src \K\S+' | head -1)
[[ -z "$host_ip" ]] && host_ip=$(hostname -I | awk '{print $1}')
echo ""
echo -e "${BOLD}Clone Image Ready${NC}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Image: $image_file"
echo "TFTP: $TFTP_ROOT/secubox-clone.img"
echo ""
echo -e "${BOLD}To flash target in U-Boot:${NC}"
echo " setenv serverip $host_ip"
echo " setenv ipaddr 192.168.1.100"
echo " dhcp"
echo " tftpboot 0x20000000 secubox-clone.img"
echo " mmc dev 0"
echo ' mmc write 0x20000000 0 ${filesize}'
echo " reset"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Save state
cat > "$CLONE_DIR/.pull_state" <<EOF
IMAGE_FILE="$image_file"
TFTP_IMAGE="$TFTP_ROOT/secubox-clone.img"
MASTER_IP="$master_ip"
CLONE_TOKEN="$token"
HOST_IP="$host_ip"
DEVICE_TYPE="$device_type"
PULL_TAG="$tag"
EOF
}
# =============================================================================
# Flash Target
# =============================================================================
cmd_flash() {
local target="${TARGET_DEV:-}"
local image=""
# Parse args
while [[ $# -gt 0 ]]; do
case "$1" in
--target) target="$2"; shift 2 ;;
--image) image="$2"; shift 2 ;;
*) shift ;;
esac
done
load_detected
[[ -z "$target" ]] && target="$TARGET_DEV"
[[ -z "$target" ]] && { log_error "No target device. Run: $0 detect"; return 1; }
# Load pull state
if [[ -f "$CLONE_DIR/.pull_state" ]]; then
source "$CLONE_DIR/.pull_state"
[[ -z "$image" ]] && image="$TFTP_IMAGE"
fi
[[ -z "$image" || ! -f "$image" ]] && {
log_error "No clone image. Run: $0 pull first"
return 1
}
# Get host IP
local host_ip="${HOST_IP:-}"
[[ -z "$host_ip" ]] && host_ip=$(ip route get 8.8.8.8 2>/dev/null | grep -oP 'src \K\S+' | head -1)
[[ -z "$host_ip" ]] && host_ip=$(hostname -I | awk '{print $1}')
local tag=$(get_tag)
echo -e "${BOLD}SecuBox Clone Station - Flash Target${NC}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Target: $target"
echo "Image: $image"
echo "Host: $host_ip"
echo ""
log_step "Entering U-Boot on target..."
# Use MOKATOOL to break into U-Boot and flash
mokatool break --port "$target" --baud "$BAUDRATE" || {
log_warn "Could not auto-break into U-Boot"
log_info "Ensure target is at U-Boot prompt or reset it"
}
log_step "Sending flash commands via MOKATOOL..."
# Create a temporary macro file for flashing
local macro_file="$CLONE_DIR/flash-clone.yaml"
cat > "$macro_file" <<MACRO
macros:
flash-clone:
desc: "Flash SecuBox clone image via TFTP"
steps:
- send: "setenv serverip $host_ip"
expect: "(=>|U-Boot>)"
- send: "setenv ipaddr 192.168.1.100"
expect: "(=>|U-Boot>)"
- send: "dhcp"
expect: "(=>|U-Boot>)"
timeout: 30
- send: "tftpboot 0x20000000 secubox-clone.img"
expect: "(=>|U-Boot>)"
timeout: 120
- send: "mmc dev 0"
expect: "(=>|U-Boot>)"
- send: 'mmc write 0x20000000 0 \${filesize}'
expect: "(=>|U-Boot>)"
timeout: 300
- send: "echo Flash complete, resetting..."
expect: "(=>|U-Boot>)"
- pause: 2
- send: "reset"
MACRO
mokatool macro --file "$macro_file" --name flash-clone --port "$target" \
--baud "$BAUDRATE" --no-break-first 2>&1 | tee "$CLONE_LOGS/flash-$tag.log" || {
log_warn "Flash may have failed or timed out"
log_info "Check logs: $CLONE_LOGS/flash-$tag.log"
}
echo ""
log_info "Flash commands sent. Target should be rebooting..."
log_info "Monitor progress: mokatool console --port $target"
# Save state
cat > "$CLONE_DIR/.flash_state" <<EOF
TARGET_DEV="$target"
FLASH_TAG="$tag"
FLASH_TIME="$(date -Iseconds)"
EOF
}
# =============================================================================
# Verify Clone
# =============================================================================
cmd_verify() {
load_detected
local master="${MASTER_DEV:-}"
local target="${TARGET_DEV:-}"
# Parse args
while [[ $# -gt 0 ]]; do
case "$1" in
--master) master="$2"; shift 2 ;;
--target) target="$2"; shift 2 ;;
*) shift ;;
esac
done
echo -e "${BOLD}SecuBox Clone Station - Verify${NC}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [[ -n "$target" && -c "$target" ]]; then
log_step "Checking target status..."
# Wait for Linux boot
log_info "Waiting for target to boot (up to 120s)..."
local i=0
while [ $i -lt 120 ]; do
local response
response=$(timeout 3 python3 -c "
import serial
import time
try:
ser = serial.Serial('$target', $BAUDRATE, timeout=1)
time.sleep(0.2)
ser.write(b'\\n')
time.sleep(0.5)
print(ser.read(500).decode('utf-8', errors='ignore'))
ser.close()
except: pass
" 2>/dev/null || echo "")
if echo "$response" | grep -qE "(root@|login:)"; then
log_info "Target booted!"
break
fi
sleep 2
i=$((i + 2))
printf "\r Waiting... %ds " "$i"
done
echo ""
# Check mesh status on target
log_step "Checking target mesh status..."
python3 -c "
import serial
import time
ser = serial.Serial('$target', $BAUDRATE, timeout=2)
time.sleep(0.2)
ser.write(b'secubox master-link status 2>/dev/null || echo \"master-link not ready\"\\n')
time.sleep(1)
print(ser.read(1000).decode('utf-8', errors='ignore'))
ser.close()
" 2>/dev/null || echo "Could not check target"
fi
if [[ -n "$master" && -c "$master" ]]; then
log_step "Checking master for new peers..."
python3 -c "
import serial
import time
ser = serial.Serial('$master', $BAUDRATE, timeout=2)
time.sleep(0.2)
ser.write(b'secubox master-link peers 2>/dev/null || echo \"No peers command\"\\n')
time.sleep(1)
print(ser.read(2000).decode('utf-8', errors='ignore'))
ser.close()
" 2>/dev/null || echo "Could not check master"
fi
}
# =============================================================================
# Full Clone Workflow
# =============================================================================
cmd_clone() {
local master=""
local target=""
# Parse args
while [[ $# -gt 0 ]]; do
case "$1" in
--master) master="$2"; shift 2 ;;
--target) target="$2"; shift 2 ;;
*) shift ;;
esac
done
init_dirs
echo -e "${BOLD}SecuBox Clone Station - Full Workflow${NC}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Step 1: Detect devices
if [[ -z "$master" || -z "$target" ]]; then
log_step "Step 1/4: Detecting devices..."
detect_devices
load_detected
[[ -z "$master" ]] && master="$MASTER_DEV"
[[ -z "$target" ]] && target="$TARGET_DEV"
else
MASTER_DEV="$master"
TARGET_DEV="$target"
fi
[[ -z "$master" ]] && { log_error "No master device found"; return 1; }
[[ -z "$target" ]] && { log_error "No target device found"; return 1; }
echo ""
log_info "Master: $master"
log_info "Target: $target"
echo ""
# Step 2: Pull from master
log_step "Step 2/4: Pulling image from master..."
cmd_pull --master "$master"
echo ""
read -p "Press Enter to flash target, or Ctrl+C to cancel..." _
# Step 3: Flash target
log_step "Step 3/4: Flashing target..."
cmd_flash --target "$target"
echo ""
log_info "Waiting 60s for target to boot..."
sleep 60
# Step 4: Verify
log_step "Step 4/4: Verifying clone..."
cmd_verify --master "$master" --target "$target"
echo ""
echo -e "${BOLD}Clone Complete!${NC}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Master: $master"
echo "Target: $target (now a mesh peer)"
echo ""
echo "Next steps:"
echo " - Monitor target: mokatool console --port $target"
echo " - Check mesh: ssh root@\$(target_ip) secubox master-link status"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
}
# =============================================================================
# MOKATOOL Helpers
# =============================================================================
cmd_console() {
local device="${1:-}"
load_detected
[[ -z "$device" ]] && device="${MASTER_DEV:-${TARGET_DEV:-}}"
[[ -z "$device" ]] && { log_error "No device specified"; return 1; }
log_info "Connecting to $device..."
mokatool console --port "$device" --baud "$BAUDRATE"
}
cmd_uboot() {
local device="${1:-}"
load_detected
[[ -z "$device" ]] && device="${TARGET_DEV:-}"
[[ -z "$device" ]] && { log_error "No device specified"; return 1; }
log_info "Breaking into U-Boot on $device..."
mokatool break --port "$device" --baud "$BAUDRATE"
}
cmd_env_backup() {
local device="${1:-}"
local output="${2:-$CLONE_DIR/uboot-env-$(get_tag).txt}"
load_detected
[[ -z "$device" ]] && device="${TARGET_DEV:-${MASTER_DEV:-}}"
[[ -z "$device" ]] && { log_error "No device specified"; return 1; }
log_step "Backing up U-Boot environment from $device..."
# Break into U-Boot and dump env
mokatool break --port "$device" --baud "$BAUDRATE" 2>/dev/null || true
python3 -c "
import serial
import time
ser = serial.Serial('$device', $BAUDRATE, timeout=3)
time.sleep(0.2)
ser.write(b'printenv\\n')
time.sleep(2)
output = ser.read(10000).decode('utf-8', errors='ignore')
# Filter to actual env vars
for line in output.split('\\n'):
if '=' in line and not line.startswith(' ') and not line.startswith('printenv'):
print(line.strip())
ser.close()
" > "$output" 2>/dev/null
log_info "U-Boot environment saved: $output"
}
# =============================================================================
# Usage
# =============================================================================
usage() {
cat <<'EOF'
SecuBox Clone Station - Host-side Device Cloner
Usage: secubox-clone-station.sh <command> [options]
Commands:
detect Detect USB serial devices (master/target)
pull [--master DEV] Pull clone image from master device
flash [--target DEV] Flash clone image to target via U-Boot
verify Verify clone joined mesh
clone Full workflow: detect → pull → flash → verify
console [DEV] Connect to serial console (via MOKATOOL)
uboot [DEV] Break into U-Boot prompt
env-backup [DEV] [FILE] Backup U-Boot environment
Options:
--master DEV Master device (e.g., /dev/ttyUSB0)
--target DEV Target device (e.g., /dev/ttyUSB1)
--image FILE Clone image file (for flash)
Environment:
MOKATOOL_DIR Path to MOKATOOL (default: ~/DEVEL/MOKATOOL)
TFTP_ROOT TFTP root directory (default: /srv/tftp)
Examples:
# Auto-detect devices
./secubox-clone-station.sh detect
# Full clone workflow
./secubox-clone-station.sh clone
# Manual workflow
./secubox-clone-station.sh pull --master /dev/ttyUSB0
./secubox-clone-station.sh flash --target /dev/ttyUSB1
# Interactive console
./secubox-clone-station.sh console /dev/ttyUSB0
Requirements:
- MOKATOOL (mochabin_tool.py) with pyserial, pexpect, rich, typer
- TFTP server configured and running
- Both devices connected via USB serial
EOF
}
# =============================================================================
# Main
# =============================================================================
check_deps || exit 1
init_dirs
case "${1:-}" in
detect)
detect_devices
;;
pull)
shift
cmd_pull "$@"
;;
flash)
shift
cmd_flash "$@"
;;
verify)
shift
cmd_verify "$@"
;;
clone)
shift
cmd_clone "$@"
;;
console)
shift
cmd_console "$@"
;;
uboot)
shift
cmd_uboot "$@"
;;
env-backup|env-dump)
shift
cmd_env_backup "$@"
;;
help|--help|-h|"")
usage
;;
*)
log_error "Unknown command: $1"
echo ""
usage >&2
exit 1
;;
esac