mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-11-08 13:14:17 +08:00
1245 lines
53 KiB
Bash
Executable File
1245 lines
53 KiB
Bash
Executable File
#!/bin/sh
|
|
|
|
PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
|
export PATH
|
|
umask 077
|
|
|
|
HONK_ROOT=/etc/honk
|
|
HONK_CONFIG=$HONK_ROOT/config.dae
|
|
HONK_SYSTEM=$HONK_ROOT/system.dae
|
|
HONK_STATE=$HONK_ROOT/state/honk.db
|
|
HONK_CORE=/usr/bin/honk-core
|
|
IMPORT_PREVIEW_ROOT=/tmp/honk-v2-import
|
|
BACKUP=/tmp/honk-backup.tar.gz
|
|
RESTORE=/tmp/honk-maintenance/restore.tar.gz
|
|
LOCK_DIR=/run/honk.lock
|
|
TXN_DIR=/etc/.honk-maintenance
|
|
JOURNAL=$TXN_DIR/journal
|
|
MAX_COMPRESSED=33554432
|
|
MAX_EXPANDED=134217728
|
|
MAX_FILE=67108864
|
|
MAX_FILES=512
|
|
CAPABILITY=$TXN_DIR/capability
|
|
|
|
if [ "${HONK_MAINT_LIBRARY:-0}" != 1 ]; then
|
|
. /usr/share/honk/lock.sh || exit 1
|
|
. /usr/share/honk/lifecycle.sh || exit 1
|
|
. /usr/share/libubox/jshn.sh || exit 1
|
|
[ -r /lib/functions.sh ] && . /lib/functions.sh
|
|
fi
|
|
|
|
json_result() {
|
|
json_ok=0
|
|
[ "$1" = true ] && json_ok=1
|
|
json_init
|
|
json_add_boolean ok "$json_ok"
|
|
[ -n "$2" ] && json_add_string path "$2"
|
|
json_add_string message "$3"
|
|
json_dump
|
|
}
|
|
|
|
error() {
|
|
json_result false "" "$1"
|
|
return 1
|
|
}
|
|
|
|
managed_core_pid() {
|
|
managed_pid=$(ubus call service list '{"name":"honk"}' 2>/dev/null | jsonfilter -e '@.honk.instances.main.pid' 2>/dev/null)
|
|
case "$managed_pid" in ''|*[!0-9]*) return 1 ;; esac
|
|
[ -r "/proc/$managed_pid/cmdline" ] || return 1
|
|
managed_cmd=$(tr '\000' ' ' < "/proc/$managed_pid/cmdline" 2>/dev/null) || return 1
|
|
case "$managed_cmd" in
|
|
*/usr/bin/honk-core\ --config\ /etc/honk/config.dae\ --disable-timestamp*) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
case "$managed_cmd" in *--mock-ebpf*) return 1 ;; esac
|
|
[ "$(readlink -f "/proc/$managed_pid/exe" 2>/dev/null)" = "$HONK_CORE" ] || return 1
|
|
printf '%s\n' "$managed_pid"
|
|
}
|
|
|
|
foreign_core_running() {
|
|
managed_pid=$(managed_core_pid 2>/dev/null || true)
|
|
for core_pid in $(pidof honk-core 2>/dev/null); do
|
|
[ "$core_pid" = "$managed_pid" ] || return 0
|
|
done
|
|
return 1
|
|
}
|
|
|
|
proc_starttime() {
|
|
stat_line=$(cat "/proc/$1/stat" 2>/dev/null) || return 1
|
|
stat_tail=${stat_line##*) }
|
|
set -- $stat_tail
|
|
[ "$#" -ge 20 ] || return 1
|
|
stat_index=1
|
|
for stat_field do
|
|
if [ "$stat_index" = 20 ]; then
|
|
case "$stat_field" in ''|*[!0-9]*) return 1 ;; esac
|
|
printf '%s\n' "$stat_field"
|
|
return 0
|
|
fi
|
|
stat_index=$((stat_index + 1))
|
|
done
|
|
return 1
|
|
}
|
|
|
|
new_nonce() {
|
|
nonce_value=$(cat /proc/sys/kernel/random/uuid 2>/dev/null | tr -d '-') || return 1
|
|
case "$nonce_value" in [0-9a-f][0-9a-f]*) [ "${#nonce_value}" = 32 ] || return 1 ;; *) return 1 ;; esac
|
|
printf '%s\n' "$nonce_value"
|
|
}
|
|
|
|
issue_capability() {
|
|
cap_phase=$1
|
|
[ "$(id -u 2>/dev/null)" = 0 ] || return 1
|
|
case "$cap_phase:$journal_operation:$journal_phase" in
|
|
candidate_health_start:restore:new_installed|rollback_old_start:restore:rollback_old_start|rollback_old_start:reset:rollback_old_start) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
cap_nonce=$(new_nonce) || return 1
|
|
cap_owner=$$
|
|
cap_starttime=$(proc_starttime "$cap_owner") || return 1
|
|
cap_tmp=$TXN_DIR/capability.new.$cap_owner
|
|
{
|
|
printf 'format=1\nnonce=%s\ntxn_id=%s\nphase=%s\nowner_pid=%s\nowner_starttime=%s\n' \
|
|
"$cap_nonce" "$txn_id" "$cap_phase" "$cap_owner" "$cap_starttime"
|
|
} > "$cap_tmp" || return 1
|
|
chmod 0600 "$cap_tmp" || { rm -f "$cap_tmp"; return 1; }
|
|
mv -f "$cap_tmp" "$CAPABILITY" || { rm -f "$cap_tmp"; return 1; }
|
|
export HONK_MAINT_CAP=$cap_nonce
|
|
honk_lifecycle_start 30
|
|
cap_start_rc=$?
|
|
unset HONK_MAINT_CAP
|
|
return "$cap_start_rc"
|
|
}
|
|
|
|
stop_managed() {
|
|
old_pid=$(managed_core_pid 2>/dev/null || true)
|
|
if foreign_core_running; then return 2; fi
|
|
if [ -n "$old_pid" ]; then
|
|
honk_lifecycle_stop 30 || return 1
|
|
managed_core_pid >/dev/null 2>&1 && return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
restore_running() {
|
|
if [ "$old_running" != 1 ]; then service_stopped=0; return 0; fi
|
|
foreign_core_running && return 1
|
|
honk_lifecycle_start 30 || return 1
|
|
service_stopped=0
|
|
}
|
|
|
|
capture_boot_links() {
|
|
boot_start_path=/etc/rc.d/S95honk
|
|
boot_stop_path=/etc/rc.d/K10honk
|
|
old_start_link=-
|
|
old_stop_link=-
|
|
if [ -L "$boot_start_path" ]; then old_start_link=$(readlink "$boot_start_path") || return 1
|
|
elif [ -e "$boot_start_path" ]; then return 1; fi
|
|
if [ -L "$boot_stop_path" ]; then old_stop_link=$(readlink "$boot_stop_path") || return 1
|
|
elif [ -e "$boot_stop_path" ]; then return 1; fi
|
|
case "$old_start_link:$old_stop_link" in
|
|
-:-|../init.d/honk:-|-:../init.d/honk|../init.d/honk:../init.d/honk) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
restore_boot_links() {
|
|
for boot_link in /etc/rc.d/S95honk /etc/rc.d/K10honk; do
|
|
[ ! -e "$boot_link" ] || [ -L "$boot_link" ] || return 1
|
|
rm -f "$boot_link" || return 1
|
|
done
|
|
if [ "$old_start_link" != - ]; then ln -s "$old_start_link" /etc/rc.d/S95honk || return 1; fi
|
|
if [ "$old_stop_link" != - ]; then ln -s "$old_stop_link" /etc/rc.d/K10honk || return 1; fi
|
|
}
|
|
|
|
disable_boot_links() {
|
|
for boot_link in /etc/rc.d/S95honk /etc/rc.d/K10honk; do
|
|
[ ! -e "$boot_link" ] || [ -L "$boot_link" ] || return 1
|
|
rm -f "$boot_link" || return 1
|
|
done
|
|
}
|
|
|
|
journal_write() {
|
|
journal_next=$TXN_DIR/journal.new
|
|
{
|
|
printf 'format=1\noperation=%s\nphase=%s\ntxn_id=%s\nold_running=%s\nold_enabled=%s\nold_boot=%s\nold_initialized=%s\nold_start_link=%s\nold_stop_link=%s\n' \
|
|
"$journal_operation" "$1" "$txn_id" "$old_running" "$old_enabled" "$old_boot" "$old_initialized" "$old_start_link" "$old_stop_link"
|
|
} > "$journal_next" || return 1
|
|
chmod 0600 "$journal_next" || return 1
|
|
sync
|
|
mv -f "$journal_next" "$JOURNAL" || return 1
|
|
sync
|
|
journal_phase=$1
|
|
}
|
|
|
|
journal_create() {
|
|
journal_operation=$1
|
|
txn_id=$(new_nonce) || return 1
|
|
[ ! -e "$TXN_DIR" ] && [ ! -L "$TXN_DIR" ] || return 1
|
|
mkdir "$TXN_DIR" || return 1
|
|
chmod 0700 "$TXN_DIR" || return 1
|
|
cp "$temp_dir/old-uci" "$TXN_DIR/old-uci" || return 1
|
|
chmod 0600 "$TXN_DIR/old-uci" || return 1
|
|
old_uci_snapshot=$TXN_DIR/old-uci
|
|
journal_active=1
|
|
journal_write prepared
|
|
}
|
|
|
|
journal_value() {
|
|
sed -n "s/^$1=//p" "$JOURNAL" 2>/dev/null
|
|
}
|
|
|
|
recover_transaction() {
|
|
[ -e "$TXN_DIR" ] || { journal_active=0; return 0; }
|
|
[ -d "$TXN_DIR" ] && [ ! -L "$TXN_DIR" ] && [ -f "$JOURNAL" ] && [ ! -L "$JOURNAL" ] || return 1
|
|
journal_operation=$(journal_value operation)
|
|
journal_phase=$(journal_value phase)
|
|
journal_format=$(journal_value format)
|
|
txn_id=$(journal_value txn_id)
|
|
old_running=$(journal_value old_running)
|
|
old_enabled=$(journal_value old_enabled)
|
|
old_boot=$(journal_value old_boot)
|
|
old_initialized=$(journal_value old_initialized)
|
|
old_start_link=$(journal_value old_start_link)
|
|
old_stop_link=$(journal_value old_stop_link)
|
|
[ "$journal_format" = 1 ] || return 1
|
|
case "$txn_id" in [0-9a-f][0-9a-f]*) [ "${#txn_id}" = 32 ] || return 1 ;; *) return 1 ;; esac
|
|
case "$journal_operation:$journal_phase" in backup:prepared|backup:stopped|backup:committed|restore:prepared|restore:stopped|restore:old_moved|restore:new_installed|restore:uci_applied|restore:service_started|restore:rollback_old_start|restore:committed|reset:prepared|reset:stopped|reset:old_moved|reset:new_installed|reset:uci_applied|reset:rollback_old_start|reset:committed) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
case "$old_running:$old_enabled:$old_boot:$old_initialized" in
|
|
0:0:0:0|0:0:0:1|0:0:1:0|0:0:1:1|0:1:0:0|0:1:0:1|0:1:1:0|0:1:1:1|1:0:0:0|1:0:0:1|1:0:1:0|1:0:1:1|1:1:0:0|1:1:0:1|1:1:1:0|1:1:1:1) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
case "$old_start_link:$old_stop_link" in -:-|../init.d/honk:-|-:../init.d/honk|../init.d/honk:../init.d/honk) ;; *) return 1 ;; esac
|
|
old_uci_snapshot=$TXN_DIR/old-uci
|
|
[ -f "$old_uci_snapshot" ] && [ ! -L "$old_uci_snapshot" ] || return 1
|
|
if [ "$journal_phase" = committed ]; then
|
|
rm -rf "$TXN_DIR" || return 1
|
|
journal_active=0
|
|
swap_started=0
|
|
if [ "$journal_operation" = backup ] && [ "$old_running" = 1 ]; then
|
|
honk_lifecycle_start 30 || return 1
|
|
fi
|
|
service_stopped=0
|
|
return 0
|
|
fi
|
|
foreign_core_running && return 1
|
|
if [ "$journal_operation" = restore ] || [ "$journal_operation" = reset ]; then
|
|
if managed_core_pid >/dev/null 2>&1; then
|
|
honk_lifecycle_stop 30 || return 1
|
|
fi
|
|
if [ -d "$TXN_DIR/old-tree" ] && [ ! -L "$TXN_DIR/old-tree" ]; then
|
|
if [ -e "$HONK_ROOT" ]; then
|
|
[ ! -e "$TXN_DIR/failed-tree" ] || return 1
|
|
mv "$HONK_ROOT" "$TXN_DIR/failed-tree" || return 1
|
|
fi
|
|
mv "$TXN_DIR/old-tree" "$HONK_ROOT" || return 1
|
|
else
|
|
[ -d "$HONK_ROOT" ] && [ ! -L "$HONK_ROOT" ] || return 1
|
|
fi
|
|
fi
|
|
if ! uci import honk < "$old_uci_snapshot" || ! uci commit honk; then return 1; fi
|
|
restore_boot_links || return 1
|
|
if [ "$old_running" = 1 ]; then
|
|
if ! managed_core_pid >/dev/null 2>&1; then
|
|
if [ "$journal_operation" = restore ] || [ "$journal_operation" = reset ]; then
|
|
journal_write rollback_old_start || return 1
|
|
issue_capability rollback_old_start || return 1
|
|
else
|
|
# A backup never changes the live tree; clear its journal before normal startup.
|
|
rm -rf "$TXN_DIR" || return 1
|
|
journal_active=0
|
|
honk_lifecycle_start 30 || return 1
|
|
fi
|
|
fi
|
|
else
|
|
if managed_core_pid >/dev/null 2>&1; then honk_lifecycle_stop 30 || return 1; fi
|
|
fi
|
|
rm -rf "$TXN_DIR" || return 1
|
|
journal_active=0
|
|
service_stopped=0
|
|
swap_started=0
|
|
}
|
|
|
|
backup_abort() {
|
|
backup_message=$1
|
|
if [ -n "$mock_pid" ] && ! stop_mock; then
|
|
error "${backup_message}_mock_stop_failed_runtime_not_restarted"
|
|
return 1
|
|
fi
|
|
if [ "$journal_active" = 1 ]; then
|
|
recover_transaction || backup_message="${backup_message}_runtime_restore_failed"
|
|
elif ! restore_running; then backup_message="${backup_message}_runtime_restore_failed"; fi
|
|
error "$backup_message"
|
|
}
|
|
|
|
restore_abort() {
|
|
restore_message=$1
|
|
if [ -n "$mock_pid" ] && ! stop_mock; then
|
|
error "${restore_message}_mock_stop_failed"
|
|
return 1
|
|
fi
|
|
if [ "$journal_active" = 1 ]; then
|
|
recover_transaction || restore_message="${restore_message}_rollback_failed"
|
|
elif [ "$service_stopped" = 1 ] && ! restore_running; then
|
|
restore_message="${restore_message}_runtime_restore_failed"
|
|
fi
|
|
error "$restore_message"
|
|
}
|
|
|
|
stop_mock() {
|
|
[ -n "$mock_pid" ] || return 0
|
|
if [ ! -r "/proc/$mock_pid/cmdline" ]; then
|
|
stop_status=0
|
|
wait "$mock_pid" 2>/dev/null || stop_status=$?
|
|
mock_pid=
|
|
[ "$stop_status" = 0 ] || return 1
|
|
return 0
|
|
fi
|
|
mock_identity=$(tr '\000' ' ' < "/proc/$mock_pid/cmdline" 2>/dev/null) || return 1
|
|
case "$mock_identity" in *"$mock_config"*--mock-ebpf*) ;; *) return 1 ;; esac
|
|
[ "$(readlink -f "/proc/$mock_pid/exe" 2>/dev/null)" = "$HONK_CORE" ] || return 1
|
|
kill -TERM "$mock_pid" 2>/dev/null || return 1
|
|
stop_forced=0
|
|
stop_tries=0
|
|
while kill -0 "$mock_pid" 2>/dev/null && [ "$stop_tries" -lt 10 ]; do sleep 1; stop_tries=$((stop_tries + 1)); done
|
|
if kill -0 "$mock_pid" 2>/dev/null; then
|
|
kill -KILL "$mock_pid" 2>/dev/null || return 1
|
|
stop_forced=1
|
|
stop_tries=0
|
|
while kill -0 "$mock_pid" 2>/dev/null && [ "$stop_tries" -lt 5 ]; do sleep 1; stop_tries=$((stop_tries + 1)); done
|
|
fi
|
|
kill -0 "$mock_pid" 2>/dev/null && return 1
|
|
stop_status=0
|
|
wait "$mock_pid" 2>/dev/null || stop_status=$?
|
|
mock_pid=
|
|
[ "$stop_forced" = 0 ] && [ "$stop_status" = 0 ]
|
|
}
|
|
|
|
cleanup() {
|
|
cleanup_status=$?
|
|
cleanup_safe=1
|
|
if [ -n "$mock_pid" ] && ! stop_mock; then cleanup_safe=0; fi
|
|
if [ "$cleanup_safe" = 1 ]; then
|
|
[ ! -e "$CAPABILITY" ] || rm -f "$CAPABILITY"
|
|
if [ "$journal_active" = 1 ]; then
|
|
recover_transaction || { cleanup_safe=0; echo "honk maintenance: transaction recovery failed; preserve $TXN_DIR and $temp_dir" >&2; }
|
|
elif [ "$service_stopped" = 1 ]; then
|
|
restore_running || { cleanup_safe=0; echo "honk maintenance: service restart failed; preserving $temp_dir and lock" >&2; }
|
|
fi
|
|
fi
|
|
if [ "$cleanup_safe" = 1 ]; then
|
|
if [ -n "$temp_dir" ] && [ -d "$temp_dir" ]; then rm -rf "$temp_dir"; fi
|
|
if [ "$lock_owned" = 1 ]; then honk_lock_release >/dev/null 2>&1; fi
|
|
else
|
|
echo "honk maintenance: cleanup/rollback not confirmed; keeping lock, temporary data at $temp_dir, and any /etc/.honk.restore.* snapshot" >&2
|
|
fi
|
|
return "$cleanup_status"
|
|
}
|
|
|
|
trap cleanup 0
|
|
trap 'exit 1' HUP INT TERM
|
|
|
|
lock_owned=0
|
|
temp_dir=
|
|
mock_pid=
|
|
mock_config=
|
|
old_running=0
|
|
service_stopped=0
|
|
swap_started=0
|
|
old_tree_path=
|
|
old_uci_snapshot=
|
|
journal_active=0
|
|
journal_operation=
|
|
journal_phase=
|
|
old_start_link=-
|
|
old_stop_link=-
|
|
lock_error=operation_in_progress
|
|
|
|
acquire_lock() {
|
|
if [ -e /etc/.honk-update/journal ] || [ -L /etc/.honk-update/journal ]; then lock_error=update_recovery_required; return 1; fi
|
|
if [ -d "$LOCK_DIR" ]; then
|
|
if [ -e "$TXN_DIR" ]; then lock_error=recovery_required; else lock_error=operation_in_progress_or_stale_lock; fi
|
|
return 1
|
|
fi
|
|
honk_lock_acquire || { lock_error=operation_in_progress; return 1; }
|
|
lock_owned=1
|
|
if [ -e "$TXN_DIR" ]; then
|
|
journal_active=1
|
|
recover_transaction || { lock_error=interrupted_transaction_recovery_failed; return 2; }
|
|
fi
|
|
lock_error=
|
|
}
|
|
|
|
recover_action() {
|
|
if [ -d "$LOCK_DIR" ]; then
|
|
error recovery_required_clear_stale_lock_only_after_confirming_no_honk_core
|
|
return 1
|
|
fi
|
|
if ! honk_lock_acquire; then error operation_in_progress; return 1; fi
|
|
lock_owned=1
|
|
if [ -e "$TXN_DIR" ]; then journal_active=1; fi
|
|
recover_transaction || { error interrupted_transaction_recovery_failed; return 1; }
|
|
json_result true "" transaction_recovered
|
|
}
|
|
|
|
check_root_file() {
|
|
[ ! -L "$1" ] && [ -f "$1" ] || return 1
|
|
[ "$(id -u 2>/dev/null)" = 0 ] || return 1
|
|
[ "$(ls -ln "$1" 2>/dev/null | awk 'NR == 1 { print $3 }')" = 0 ] || return 1
|
|
chmod 0600 "$1" 2>/dev/null || return 1
|
|
[ ! -L "$1" ] && [ -f "$1" ] || return 1
|
|
file_mode=$(ls -ldn "$1" 2>/dev/null | awk 'NR == 1 { print $1 }')
|
|
[ "$file_mode" = '-rw-------' ]
|
|
}
|
|
|
|
safe_relpath() {
|
|
case "$1" in ''|/*|*..*|*//*|*/./*|./*|*/.) return 1 ;; esac
|
|
# 空白、控制字符、通配符、反斜杠会打乱 tar 成员解析与解包,一律拒绝;
|
|
# 中文等 UTF-8 名放行(路由上不一定有 od/hexdump,只能用 awk 的字符类判)。
|
|
printf '%s\n' "$1" | LC_ALL=C awk '
|
|
$0 ~ /[[:space:][:cntrl:]]/ { bad = 1 }
|
|
$0 ~ /\*/ || $0 ~ /\?/ || $0 ~ /\[/ || $0 ~ /\\/ { bad = 1 }
|
|
END { exit bad ? 1 : 0 }
|
|
'
|
|
}
|
|
|
|
size_of() {
|
|
wc -c < "$1" | tr -d ' '
|
|
}
|
|
|
|
sha_of() {
|
|
sha256sum "$1" 2>/dev/null | awk '{print $1}'
|
|
}
|
|
|
|
core_commit() {
|
|
jsonfilter -i /usr/share/honk/provenance.json -e '@.core.commit' 2>/dev/null
|
|
}
|
|
|
|
load_uci_values() {
|
|
uci_bin=${1:-uci}
|
|
uci_config=${2:-}
|
|
# Run uci with optional -c/-t flags without eval: keep args in "$@" style
|
|
# via a helper so later callers can't inject through string concatenation.
|
|
if [ -n "$uci_config" ]; then
|
|
uci_run() { "$uci_bin" -c "$uci_config" -t "$temp_dir/uci-tmp" "$@"; }
|
|
else
|
|
uci_run() { "$uci_bin" "$@"; }
|
|
fi
|
|
uci_show=$(uci_run -q show honk 2>/dev/null) || return 1
|
|
printf '%s\n' "$uci_show" | awk -F= '
|
|
/^honk\.main=/ { main++ ; next }
|
|
/^honk\.main\.(enabled|boot_enabled|initialized|config_mode|config_file|listen_port|lan_network|geoip_url|geosite_url|geo_auto|geo_auto_freq|clash_api_enabled|clash_api_port|clash_api_secret)=/ { next }
|
|
/^honk\.main\./ { bad=1; next }
|
|
/^honk\.[A-Za-z0-9_.-]+=/ { bad=1; next }
|
|
END { exit (bad || main != 1) }
|
|
' || return 1
|
|
get_uci() { uci_run -q get "honk.main.$1" 2>/dev/null; }
|
|
u_enabled=$(get_uci enabled) || return 1
|
|
u_boot=$(get_uci boot_enabled) || return 1
|
|
u_initialized=$(get_uci initialized) || return 1
|
|
u_config_mode=$(get_uci config_mode 2>/dev/null || printf managed)
|
|
u_config=$(get_uci config_file) || return 1
|
|
u_port=$(get_uci listen_port) || return 1
|
|
u_network=$(get_uci lan_network) || return 1
|
|
u_clash_enabled=$(get_uci clash_api_enabled 2>/dev/null || printf 0)
|
|
u_clash_port=$(get_uci clash_api_port 2>/dev/null || printf 9090)
|
|
u_clash_secret=$(get_uci clash_api_secret 2>/dev/null || true)
|
|
u_geo_auto=$(get_uci geo_auto 2>/dev/null || printf 0)
|
|
u_geo_auto_freq=$(get_uci geo_auto_freq 2>/dev/null || printf daily)
|
|
case "$u_enabled:$u_boot:$u_initialized" in 0:0:0|0:0:1|0:1:0|0:1:1|1:0:0|1:0:1|1:1:0|1:1:1) ;; *) return 1 ;; esac
|
|
case "$u_config_mode" in ''|managed|manual) ;; *) return 1 ;; esac
|
|
case "$u_geo_auto:$u_geo_auto_freq" in 0:daily|0:weekly|1:daily|1:weekly) ;; *) return 1 ;; esac
|
|
[ "$u_config" = /etc/honk/config.dae ] || return 1
|
|
case "$u_port" in ''|*[!0-9]*) return 1 ;; esac
|
|
[ "$u_port" -ge 1024 ] && [ "$u_port" -le 65535 ] || return 1
|
|
case "$u_network" in ''|*[!A-Za-z0-9_.-]*) return 1 ;; esac
|
|
case "$u_clash_enabled" in 0|1) ;; *) return 1 ;; esac
|
|
case "$u_clash_port" in ''|*[!0-9]*) return 1 ;; esac
|
|
[ "$u_clash_port" -ge 1024 ] && [ "$u_clash_port" -le 65535 ] || return 1
|
|
if [ "$u_clash_enabled" = 1 ]; then
|
|
[ "$u_initialized" = 1 ] && [ "$u_clash_port" != "$u_port" ] || return 1
|
|
case "$u_clash_secret" in ''|*[!A-Za-z0-9._-]*) return 1 ;; esac
|
|
[ "${#u_clash_secret}" -ge 6 ] && [ "${#u_clash_secret}" -le 128 ] || return 1
|
|
fi
|
|
}
|
|
|
|
check_sqlite() {
|
|
check_db=$1
|
|
[ ! -L "$check_db" ] && [ -f "$check_db" ] || return 1
|
|
command -v sqlite3 >/dev/null 2>&1 || return 1
|
|
check_result=$(sqlite3 "$check_db" 'PRAGMA quick_check;' 2>/dev/null) || return 1
|
|
[ "$check_result" = ok ]
|
|
}
|
|
|
|
checkpoint_sqlite() {
|
|
[ -e "$HONK_STATE" ] || return 0
|
|
check_sqlite "$HONK_STATE" || return 1
|
|
checkpoint_result=$(sqlite3 "$HONK_STATE" 'PRAGMA wal_checkpoint(TRUNCATE);' 2>/dev/null) || return 1
|
|
case "$checkpoint_result" in
|
|
0\|[0-9]*\|[0-9]*) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
checkpoint_busy=${checkpoint_result%%|*}
|
|
checkpoint_rest=${checkpoint_result#*|}
|
|
checkpoint_log=${checkpoint_rest%%|*}
|
|
checkpoint_done=${checkpoint_rest#*|}
|
|
case "$checkpoint_log:$checkpoint_done" in *[!0-9:]*|:*|*:) return 1 ;; esac
|
|
[ "$checkpoint_busy" = 0 ] && [ "$checkpoint_log" = "$checkpoint_done" ] || return 1
|
|
for sidecar in "$HONK_STATE-wal" "$HONK_STATE-shm"; do
|
|
[ ! -L "$sidecar" ] || return 1
|
|
if [ -e "$sidecar" ]; then
|
|
[ -f "$sidecar" ] || return 1
|
|
[ "$(size_of "$sidecar")" -eq 0 ] || [ "$sidecar" = "$HONK_STATE-shm" ] || return 1
|
|
rm -f "$sidecar" || return 1
|
|
fi
|
|
done
|
|
[ ! -e "$HONK_STATE-wal" ] && [ ! -e "$HONK_STATE-shm" ]
|
|
}
|
|
|
|
is_excluded() {
|
|
case "$1" in
|
|
*/logs/*|logs/*|*/log/*|log/*|*.log|*.tmp|*.pid|*.lock|*/.lock|*/honk.db-wal|*/honk.db-shm|*/honk.db-journal)
|
|
return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
collect_tree() {
|
|
collect_root=$1
|
|
collect_out=$2
|
|
collect_error=
|
|
[ -d "$collect_root" ] && [ ! -L "$collect_root" ] || { collect_error=honk_tree_missing; return 1; }
|
|
if find "$collect_root" -type l -print 2>/dev/null | grep -q .; then collect_error=honk_tree_has_symlink; return 1; fi
|
|
if find "$collect_root" ! -type f ! -type d -print 2>/dev/null | grep -q .; then collect_error=honk_tree_has_special_file; return 1; fi
|
|
: > "$collect_out" || { collect_error=temporary_storage_unavailable; return 1; }
|
|
(
|
|
cd "$collect_root" || exit 1
|
|
find . -type f -print
|
|
) > "$collect_out.raw" || { collect_error=honk_tree_unreadable; return 1; }
|
|
: > "$collect_out"
|
|
collect_count=0
|
|
collect_total=0
|
|
while IFS= read -r collect_path; do
|
|
collect_path=${collect_path#./}
|
|
is_excluded "$collect_path" && continue
|
|
safe_relpath "$collect_path" || {
|
|
collect_error=unsafe_honk_file_name
|
|
# 文件名进面板日志(维护→日志→维护),否则用户不知道是哪个文件。
|
|
logger -t honk-maintenance "unsafe file name: $collect_path" 2>/dev/null || :
|
|
printf '%s\n' "unsafe file name: $collect_path" >&2
|
|
return 1
|
|
}
|
|
[ -f "$collect_root/$collect_path" ] && [ ! -L "$collect_root/$collect_path" ] || { collect_error=honk_tree_has_special_file; return 1; }
|
|
collect_size=$(size_of "$collect_root/$collect_path") || { collect_error=honk_tree_unreadable; return 1; }
|
|
[ "$collect_size" -le "$MAX_FILE" ] || {
|
|
collect_error=honk_backup_file_too_large
|
|
logger -t honk-maintenance "file exceeds $MAX_FILE bytes: $collect_path" 2>/dev/null || :
|
|
printf '%s\n' "file too large: $collect_path" >&2
|
|
return 1
|
|
}
|
|
collect_count=$((collect_count + 1))
|
|
collect_total=$((collect_total + collect_size))
|
|
[ "$collect_count" -lt "$MAX_FILES" ] && [ "$collect_total" -le $((MAX_EXPANDED - 1048576)) ] || {
|
|
collect_error=honk_backup_tree_too_large
|
|
logger -t honk-maintenance "tree exceeds limits after $collect_count files, $collect_total bytes: $collect_path" 2>/dev/null || :
|
|
printf '%s\n' "tree too large at: $collect_path" >&2
|
|
return 1
|
|
}
|
|
printf '%s\n' "$collect_path" >> "$collect_out" || { collect_error=temporary_storage_unavailable; return 1; }
|
|
done < "$collect_out.raw" || { collect_error=honk_tree_unreadable; return 1; }
|
|
[ -f "$collect_root/config.dae" ] && [ -f "$collect_root/system.dae" ] || { collect_error=honk_config_incomplete; return 1; }
|
|
return 0
|
|
}
|
|
|
|
validate_config_tree() {
|
|
config_root=$1
|
|
expected_device=$2
|
|
expected_ip=$3
|
|
expected_port=$4
|
|
expected_initialized=$5
|
|
main=$config_root/config.dae
|
|
system=$config_root/system.dae
|
|
[ -f "$main" ] && [ ! -L "$main" ] && [ -f "$system" ] && [ ! -L "$system" ] || return 1
|
|
if [ "$(config_owner "$main")" != system ]; then
|
|
"$HONK_CORE" config validate --config "$main" >/dev/null 2>&1 || return 2
|
|
find "$config_root" -type l -print 2>/dev/null | grep -q . && return 2
|
|
find "$config_root" ! -type f ! -type d -print 2>/dev/null | grep -q . && return 2
|
|
return 0
|
|
fi
|
|
validate_system_schema "$system" "$expected_device" "$expected_ip" "$expected_port" "$expected_initialized" || return 2
|
|
# `validate_candidate` asks honk-core's own parser to resolve every include
|
|
# and captures the complete source tree inside its private shadow. Keep this
|
|
# shell check limited to filesystem entry types and managed system fields.
|
|
find "$config_root" -type l -print 2>/dev/null | grep -q . && return 2
|
|
find "$config_root" ! -type f ! -type d -print 2>/dev/null | grep -q . && return 2
|
|
return 0
|
|
}
|
|
|
|
validate_system_schema() {
|
|
trusted_system=$1
|
|
expected_device=$2
|
|
expected_ip=$3
|
|
expected_port=$4
|
|
expected_initialized=$5
|
|
lan_iface=$(sed -n "s/^ lan_interface: '\\([^']*\\)'$/\\1/p" "$trusted_system")
|
|
listen_addr=$(sed -n "s/^ listen: '\\([^']*\\)'$/\\1/p" "$trusted_system")
|
|
case "$lan_iface" in ''|*[!A-Za-z0-9_.-]*) return 1 ;; esac
|
|
case "$listen_addr" in ''|*[!0-9.:]*) return 1 ;; esac
|
|
case "$listen_addr" in *.*.*.*:*) ;; *) return 1 ;; esac
|
|
listen_host=${listen_addr%:*}
|
|
listen_port=${listen_addr##*:}
|
|
case "$listen_host" in ''|*[!0-9.]*) return 1 ;; esac
|
|
case "$listen_port" in ''|*[!0-9]*) return 1 ;; esac
|
|
[ "$listen_port" -ge 1024 ] && [ "$listen_port" -le 65535 ] || return 1
|
|
[ "$lan_iface" = "$expected_device" ] && [ "$listen_port" = "$expected_port" ] || return 1
|
|
[ "$listen_host" = "$expected_ip" ] || [ "$listen_host" = 127.0.0.1 ] || return 1
|
|
store_subscribe=$(sed -n 's/^ store_subscribe: \(true\|false\)$/\1/p' "$trusted_system")
|
|
case "$store_subscribe" in true|false) ;; *) return 1 ;; esac
|
|
cat > "$temp_dir/system.expected" <<EOF
|
|
global {
|
|
wan_interface: auto
|
|
lan_interface: '$lan_iface'
|
|
log_level: info
|
|
dial_mode: domain
|
|
allow_insecure: false
|
|
auto_config_kernel_parameter: true
|
|
data_dir: '/etc/honk'
|
|
store_subscribe: $store_subscribe
|
|
nfqueue_enable: true
|
|
}
|
|
|
|
experimental {
|
|
native_api {
|
|
enabled: true
|
|
listen: '$listen_addr'
|
|
password_auth: true
|
|
allow_anonymous_loopback: false
|
|
config_write: true
|
|
ui: '/usr/share/doona'
|
|
}
|
|
EOF
|
|
if [ "$u_clash_enabled" = 1 ]; then
|
|
cat >> "$temp_dir/system.expected" <<EOF
|
|
clash_api {
|
|
external_controller: '$listen_host:$u_clash_port'
|
|
external_ui: '/usr/share/honk/zashboard'
|
|
secret: '$u_clash_secret'
|
|
default_mode: Rule
|
|
}
|
|
EOF
|
|
fi
|
|
cat >> "$temp_dir/system.expected" <<EOF
|
|
}
|
|
EOF
|
|
cmp -s "$trusted_system" "$temp_dir/system.expected"
|
|
}
|
|
|
|
check_tar_members() {
|
|
archive=$1
|
|
member_list=$2
|
|
[ "$(size_of "$archive")" -le "$MAX_COMPRESSED" ] || return 1
|
|
ulimit -f 262144 2>/dev/null || return 1
|
|
tar -tvzf "$archive" > "$member_list" 2>/dev/null || return 1
|
|
awk '
|
|
{
|
|
if (NF < 2 || substr($1,1,1) != "-") exit 1
|
|
name=$NF
|
|
# 空白、控制字符、通配符会打乱成员名解析,其余(含 UTF-8)放行。
|
|
if (name ~ /[[:space:][:cntrl:]]/) exit 1
|
|
if (name ~ /\*/ || name ~ /\?/ || name ~ /\[/ || name ~ /\\/) exit 1
|
|
if (name ~ /^\// || name ~ /(^|\/)\.\.(\/|$)/ || name ~ /(^|\/)\.($|\/)/ || name ~ /\/\//) exit 1
|
|
seen[name]++
|
|
if (seen[name] > 1) exit 1
|
|
count++
|
|
if (count > 513) exit 1
|
|
print name
|
|
}
|
|
END { if (!count) exit 1 }
|
|
' "$member_list" > "$member_list.names" || return 1
|
|
grep -qx 'manifest.tsv' "$member_list.names" || return 1
|
|
}
|
|
|
|
extract_archive() {
|
|
archive=$1
|
|
stage=$2
|
|
member_listing=$temp_dir/members.verbose
|
|
check_tar_members "$archive" "$member_listing" || return 1
|
|
mkdir -p "$stage" || return 1
|
|
: > "$stage/manifest.tsv" || return 1
|
|
ulimit -f 262144 2>/dev/null || return 1
|
|
tar -xOzf "$archive" manifest.tsv > "$stage/manifest.tsv" 2>/dev/null || return 1
|
|
[ "$(size_of "$stage/manifest.tsv")" -le 1048576 ] || return 1
|
|
awk -F '\t' '
|
|
NR==1 { if (NF!=2 || $1!="format" || $2!="1") exit 1; next }
|
|
NR==2 { if (NF!=2 || $1!="core_commit" || length($2)!=40 || $2 !~ /^[0-9a-f]+$/) exit 1; next }
|
|
NR==3 { if (NF!=2 || $1!="state" || ($2!="present" && $2!="absent")) exit 1; next }
|
|
$1=="file" {
|
|
if (NF!=4) exit 1
|
|
if ($2 ~ /[[:space:][:cntrl:]]/) exit 1
|
|
if ($2 ~ /\*/ || $2 ~ /\?/ || $2 ~ /\[/ || $2 ~ /\\/) exit 1
|
|
if ($2 ~ /^\// || $2 ~ /(^|\/)\.\.(\/|$)/ || $2 ~ /(^|\/)\.($|\/)/ || $2 ~ /\/\//) exit 1
|
|
if ($3 !~ /^[0-9]+$/ || $3 > 67108864 || $4 !~ /^[0-9a-f]{64}$/) exit 1
|
|
if (seen[$2]++) exit 1
|
|
total += $3; count++; next
|
|
}
|
|
{ exit 1 }
|
|
END { if (NR<4 || count<3 || count>512 || total>133169152) exit 1 }
|
|
' "$stage/manifest.tsv" || return 1
|
|
manifest_commit=$(sed -n '2s/^core_commit\t//p' "$stage/manifest.tsv")
|
|
[ -n "$manifest_commit" ] && [ "$manifest_commit" = "$(core_commit)" ] || return 2
|
|
archive_state=$(sed -n '3s/^state\t//p' "$stage/manifest.tsv")
|
|
while IFS="$(printf '\t')" read -r kind relpath expected_size expected_sha; do
|
|
[ "$kind" = file ] || continue
|
|
safe_relpath "$relpath" || return 1
|
|
is_excluded "$relpath" && return 1
|
|
grep -qx "$relpath" "$member_listing.names" || return 1
|
|
case "$relpath" in uci/honk|etc/honk/*) ;; *) return 1 ;; esac
|
|
mkdir -p "$stage/$(dirname "$relpath")" || return 1
|
|
: > "$stage/$relpath" || return 1
|
|
tar -xOzf "$archive" "$relpath" > "$stage/$relpath" 2>/dev/null || return 1
|
|
[ "$(size_of "$stage/$relpath")" -eq "$expected_size" ] || return 1
|
|
[ "$(sha_of "$stage/$relpath")" = "$expected_sha" ] || return 1
|
|
done < "$stage/manifest.tsv"
|
|
# Every tar member must be declared exactly once in the manifest.
|
|
awk -F '\t' '$1=="file" {print $2}' "$stage/manifest.tsv" | sort > "$member_listing.expected"
|
|
grep -v '^manifest.tsv$' "$member_listing.names" | sort > "$member_listing.actual"
|
|
cmp -s "$member_listing.expected" "$member_listing.actual" || return 1
|
|
[ -f "$stage/etc/honk/config.dae" ] && [ -f "$stage/etc/honk/system.dae" ] && [ -f "$stage/uci/honk" ] || return 1
|
|
if [ "$archive_state" = present ]; then
|
|
[ -f "$stage/etc/honk/state/honk.db" ] || return 1
|
|
else
|
|
[ ! -e "$stage/etc/honk/state/honk.db" ] || return 1
|
|
fi
|
|
}
|
|
|
|
parse_uci_file() {
|
|
uci_dir=$1
|
|
mkdir -p "$temp_dir/uci-tmp" || return 1
|
|
uci -c "$uci_dir" -t "$temp_dir/uci-tmp" -q export honk >/dev/null 2>&1 || return 1
|
|
load_uci_values uci "$uci_dir" || return 1
|
|
[ "$u_initialized" = 1 ] || [ ! -f "$restore_stage/etc/honk/state/honk.db" ] || return 1
|
|
}
|
|
|
|
# 清订阅、节点、策略、历史与缓存,保留管理员记录和 geo 设置。
|
|
purge_business_state() {
|
|
purge_db=$1
|
|
[ -f "$purge_db" ] && [ ! -L "$purge_db" ] || return 0
|
|
command -v sqlite3 >/dev/null 2>&1 || return 1
|
|
purge_sql='BEGIN IMMEDIATE;'
|
|
for purge_table in head revision subscription_body selector delay_sample dns_answer clash_state listener_secret; do
|
|
purge_exists=$(sqlite3 "$purge_db" "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='$purge_table';" 2>/dev/null) || return 1
|
|
[ "$purge_exists" = 1 ] || continue
|
|
purge_sql="$purge_sql DELETE FROM $purge_table;"
|
|
done
|
|
sqlite3 "$purge_db" "$purge_sql COMMIT; PRAGMA wal_checkpoint(TRUNCATE); VACUUM;" >/dev/null 2>&1 || return 1
|
|
check_sqlite "$purge_db"
|
|
}
|
|
|
|
archive_sqlite_check() {
|
|
db=$1
|
|
[ -f "$db" ] || return 0
|
|
check_sqlite "$db" || return 1
|
|
}
|
|
|
|
unused_loopback_port() {
|
|
command -v netstat >/dev/null 2>&1 || command -v ss >/dev/null 2>&1 || return 1
|
|
for port_candidate in 39127 39129 39131 39133 39135 39137 39139 39141; do
|
|
if command -v netstat >/dev/null 2>&1; then
|
|
port_listeners=$(netstat -lnt 2>/dev/null)
|
|
else
|
|
port_listeners=$(ss -lnt 2>/dev/null)
|
|
fi
|
|
if ! printf '%s\n' "$port_listeners" | awk -v p="$port_candidate" '$4 ~ (":" p "$") {found=1} END {exit !found}'; then
|
|
printf '%s\n' "$port_candidate"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
rewrite_shadow_system() {
|
|
shadow_system=$1
|
|
shadow_data=$2
|
|
shadow_port=$3
|
|
cat > "$shadow_system.new" <<EOF
|
|
global {
|
|
wan_interface: auto
|
|
lan_interface: ''
|
|
log_level: info
|
|
dial_mode: domain
|
|
allow_insecure: false
|
|
auto_config_kernel_parameter: false
|
|
data_dir: '$shadow_data'
|
|
store_subscribe: false
|
|
nfqueue_enable: false
|
|
}
|
|
|
|
experimental {
|
|
native_api {
|
|
enabled: true
|
|
listen: '127.0.0.1:$shadow_port'
|
|
password_auth: true
|
|
allow_anonymous_loopback: false
|
|
config_write: true
|
|
ui: '/usr/share/doona'
|
|
}
|
|
}
|
|
EOF
|
|
mv "$shadow_system.new" "$shadow_system"
|
|
}
|
|
|
|
write_live_system() {
|
|
live_system=$1
|
|
live_device=$2
|
|
live_ip=$3
|
|
live_port=$4
|
|
live_initialized=$5
|
|
if [ "$live_initialized" = 1 ]; then live_host=$live_ip; else live_host=127.0.0.1; fi
|
|
cat > "$live_system" <<EOF
|
|
global {
|
|
wan_interface: auto
|
|
lan_interface: '$live_device'
|
|
log_level: info
|
|
dial_mode: domain
|
|
allow_insecure: false
|
|
auto_config_kernel_parameter: true
|
|
data_dir: '/etc/honk'
|
|
store_subscribe: true
|
|
nfqueue_enable: true
|
|
}
|
|
|
|
experimental {
|
|
native_api {
|
|
enabled: true
|
|
listen: '$live_host:$live_port'
|
|
password_auth: true
|
|
allow_anonymous_loopback: false
|
|
config_write: true
|
|
ui: '/usr/share/doona'
|
|
}
|
|
EOF
|
|
if [ "$u_clash_enabled" = 1 ]; then
|
|
cat >> "$live_system" <<EOF
|
|
clash_api {
|
|
external_controller: '$live_host:$u_clash_port'
|
|
external_ui: '/usr/share/honk/zashboard'
|
|
secret: '$u_clash_secret'
|
|
default_mode: Rule
|
|
}
|
|
EOF
|
|
fi
|
|
cat >> "$live_system" <<EOF
|
|
}
|
|
EOF
|
|
}
|
|
|
|
mock_identity_ok() {
|
|
[ -n "$mock_pid" ] && [ -r "/proc/$mock_pid/cmdline" ] || return 1
|
|
mock_identity=$(tr '\000' ' ' < "/proc/$mock_pid/cmdline" 2>/dev/null) || return 1
|
|
case "$mock_identity" in *"$mock_config"*--mock-ebpf*) ;; *) return 1 ;; esac
|
|
[ "$(readlink -f "/proc/$mock_pid/exe" 2>/dev/null)" = "$HONK_CORE" ]
|
|
}
|
|
|
|
validate_candidate() {
|
|
candidate=$1
|
|
uci_dir=$2
|
|
state=$3
|
|
# local = 本机自己拍的快照(备份/重置),archive = 从别的设备导入的包。
|
|
identity=${4:-local}
|
|
config_tree=$candidate/etc/honk
|
|
mkdir -p "$temp_dir/uci-tmp" || return 1
|
|
load_uci_values uci "$uci_dir" || return 1
|
|
if [ "$(config_owner "$config_tree/config.dae")" != system ]; then
|
|
validate_config_tree "$config_tree" '' '' "$u_port" "$u_initialized" || return 2
|
|
if [ "$state" = present ] || { [ "$state" = preview ] && { [ -e "$config_tree/state/honk.db" ] || [ -L "$config_tree/state/honk.db" ]; }; }; then
|
|
archive_sqlite_check "$config_tree/state/honk.db" || return 1
|
|
fi
|
|
if [ -f "$config_tree/state/honk.db" ] && [ ! -L "$config_tree/state/honk.db" ]; then
|
|
restore_initialized=1
|
|
else
|
|
restore_initialized=0
|
|
fi
|
|
return 0
|
|
fi
|
|
. /lib/functions/network.sh || return 1
|
|
candidate_device=
|
|
candidate_ip=
|
|
network_get_device candidate_device "$u_network" || return 1
|
|
network_get_ipaddr candidate_ip "$u_network" || return 1
|
|
if [ "$identity" = archive ]; then
|
|
# 包里记的是原设备的 IP,落地时会被改写;拿本机 IP 比会拒掉换设备的备份。
|
|
archive_lan=$(sed -n "s/^ lan_interface: '\([^']*\)'$/\1/p" "$config_tree/system.dae")
|
|
archive_listen=$(sed -n "s/^ listen: '\([^']*\)'$/\1/p" "$config_tree/system.dae")
|
|
validate_config_tree "$config_tree" "$archive_lan" "${archive_listen%:*}" "${archive_listen##*:}" "$u_initialized" || return 2
|
|
else
|
|
validate_config_tree "$config_tree" "$candidate_device" "$candidate_ip" "$u_port" "$u_initialized" || return 2
|
|
fi
|
|
if [ "$state" = present ] || { [ "$state" = preview ] && { [ -e "$config_tree/state/honk.db" ] || [ -L "$config_tree/state/honk.db" ]; }; }; then
|
|
archive_sqlite_check "$config_tree/state/honk.db" || return 1
|
|
fi
|
|
mkdir -p "$temp_dir/shadow/etc" "$temp_dir/shadow-data" || return 1
|
|
chmod 0700 "$temp_dir/shadow" "$temp_dir/shadow/etc" "$temp_dir/shadow-data" || return 1
|
|
cp -pR "$config_tree" "$temp_dir/shadow/etc/honk" || return 1
|
|
find "$temp_dir/shadow" -type d -exec chmod 0700 {} + || return 1
|
|
find "$temp_dir/shadow/etc/honk" -type f -exec chmod 0600 {} + || return 1
|
|
find "$temp_dir/shadow/etc/honk" -type l -print 2>/dev/null | grep -q . && return 2
|
|
find "$temp_dir/shadow/etc/honk" ! -type f ! -type d -print 2>/dev/null | grep -q . && return 2
|
|
shadow_port=$(unused_loopback_port) || return 1
|
|
rewrite_shadow_system "$temp_dir/shadow/etc/honk/system.dae" "$temp_dir/shadow-data" "$shadow_port" || return 2
|
|
chmod 0600 "$temp_dir/shadow/etc/honk"/*.dae 2>/dev/null || true
|
|
if [ "$state" = present ] || { [ "$state" = preview ] && [ -f "$config_tree/state/honk.db" ]; }; then
|
|
mkdir -p "$temp_dir/shadow-data/state" || return 1
|
|
cp "$config_tree/state/honk.db" "$temp_dir/shadow-data/state/honk.db" || return 1
|
|
chmod 0600 "$temp_dir/shadow-data/state/honk.db" || return 1
|
|
fi
|
|
mock_config=$temp_dir/shadow/etc/honk/config.dae
|
|
mock_log=$temp_dir/mock.log
|
|
response_file=$temp_dir/discovery.json
|
|
DAE_LOCATION_ASSET=/usr/share/v2ray "$HONK_CORE" --config "$mock_config" --mock-ebpf > "$mock_log" 2>&1 &
|
|
mock_pid=$!
|
|
mock_ready=0
|
|
attempt=0
|
|
while [ "$attempt" -lt 15 ]; do
|
|
if ! kill -0 "$mock_pid" 2>/dev/null; then return 1; fi
|
|
if curl -fsS --noproxy '*' --connect-timeout 1 --max-time 2 "http://127.0.0.1:$shadow_port/api" -o "$response_file" 2>/dev/null; then
|
|
mock_identity_ok || return 1
|
|
mock_ready=1
|
|
break
|
|
fi
|
|
sleep 1
|
|
attempt=$((attempt + 1))
|
|
done
|
|
[ "$mock_ready" = 1 ] || return 1
|
|
setup_required=$(jsonfilter -i "$response_file" -e '@.auth.setup_required' 2>/dev/null)
|
|
case "$setup_required" in true) restore_initialized=0 ;; false) restore_initialized=1 ;; *) return 1 ;; esac
|
|
[ "$state" != absent ] || [ "$restore_initialized" = 0 ] || return 1
|
|
mock_identity_ok || return 1
|
|
stop_mock || return 1
|
|
return 0
|
|
}
|
|
|
|
make_manifest() {
|
|
root=$1
|
|
state=$2
|
|
manifest=$3
|
|
commit=$(core_commit)
|
|
case "$commit" in [0-9a-f][0-9a-f]*) [ "${#commit}" -eq 40 ] || return 1 ;; *) return 1 ;; esac
|
|
{
|
|
printf 'format\t1\ncore_commit\t%s\nstate\t%s\n' "$commit" "$state"
|
|
while IFS= read -r relpath; do
|
|
[ -n "$relpath" ] || continue
|
|
file=$root/$relpath
|
|
file_size=$(size_of "$file") || return 1
|
|
file_sha=$(sha_of "$file") || return 1
|
|
[ "$file_size" -le "$MAX_FILE" ] || return 1
|
|
printf 'file\t%s\t%s\t%s\n' "$relpath" "$file_size" "$file_sha"
|
|
done < "$temp_dir/files.list"
|
|
} > "$manifest" || return 1
|
|
[ "$(size_of "$manifest")" -le 1048576 ]
|
|
}
|
|
|
|
backup() {
|
|
if [ -L "$BACKUP" ] || { [ -e "$BACKUP" ] && [ ! -f "$BACKUP" ]; }; then error unsafe_backup_path; return 1; fi
|
|
if foreign_core_running; then error unmanaged_core_running; return 1; fi
|
|
if managed_core_pid >/dev/null 2>&1; then old_running=1; fi
|
|
if ! acquire_lock; then error "$lock_error"; return 1; fi
|
|
foreign_core_running && { error unmanaged_core_running; return 1; }
|
|
if [ "$old_running" = 1 ]; then managed_core_pid >/dev/null 2>&1 || { error service_state_changed; return 1; }
|
|
else managed_core_pid >/dev/null 2>&1 && { error service_state_changed; return 1; }; fi
|
|
temp_dir=$(mktemp -d /tmp/honk-maint.XXXXXX) || { error temporary_storage_unavailable; return 1; }
|
|
chmod 0700 "$temp_dir" || { error temporary_storage_unavailable; return 1; }
|
|
load_uci_values || { error uci_validation_failed; return 1; }
|
|
old_enabled=$u_enabled
|
|
old_boot=$u_boot
|
|
old_initialized=$u_initialized
|
|
uci export honk > "$temp_dir/old-uci" 2>/dev/null || { error uci_export_failed; return 1; }
|
|
if [ "$old_running" = 1 ]; then
|
|
capture_boot_links || { error boot_link_state_unavailable; return 1; }
|
|
journal_create backup || { error transaction_journal_create_failed; return 1; }
|
|
service_stopped=1
|
|
stop_managed || { backup_abort service_stop_failed; return 1; }
|
|
journal_write stopped || { backup_abort transaction_journal_write_failed; return 1; }
|
|
fi
|
|
uci export honk > "$temp_dir/uci.honk" 2>/dev/null || { backup_abort uci_export_failed; return 1; }
|
|
if [ -f "$HONK_STATE" ]; then
|
|
checkpoint_sqlite || { backup_abort sqlite_checkpoint_failed; return 1; }
|
|
state=present
|
|
[ "$u_initialized" = 1 ] || { backup_abort state_without_initialized_profile; return 1; }
|
|
else
|
|
state=absent
|
|
[ "$u_initialized" = 0 ] || { backup_abort initialized_state_missing; return 1; }
|
|
fi
|
|
mkdir -p "$temp_dir/archive/etc" "$temp_dir/archive/uci" || { backup_abort temporary_storage_unavailable; return 1; }
|
|
collect_tree "$HONK_ROOT" "$temp_dir/files.list" || { backup_abort "${collect_error:-unsupported_honk_tree_entry}"; return 1; }
|
|
while IFS= read -r relpath; do
|
|
[ -n "$relpath" ] || continue
|
|
dest=$temp_dir/archive/etc/honk/$relpath
|
|
mkdir -p "$(dirname "$dest")" || { backup_abort snapshot_failed; return 1; }
|
|
cp "$HONK_ROOT/$relpath" "$dest" || { backup_abort snapshot_failed; return 1; }
|
|
chmod 0600 "$dest" || { backup_abort snapshot_failed; return 1; }
|
|
done < "$temp_dir/files.list"
|
|
cp "$temp_dir/uci.honk" "$temp_dir/archive/uci/honk" || { backup_abort uci_snapshot_failed; return 1; }
|
|
chmod 0600 "$temp_dir/archive/uci/honk" || { backup_abort uci_snapshot_failed; return 1; }
|
|
if [ "$state" = present ]; then
|
|
check_sqlite "$temp_dir/archive/etc/honk/state/honk.db" || { backup_abort sqlite_check_failed; return 1; }
|
|
fi
|
|
validate_candidate "$temp_dir/archive" "$temp_dir/archive/uci" "$state"
|
|
validate_rc=$?
|
|
if [ "$validate_rc" -ne 0 ]; then
|
|
if ! restore_running; then
|
|
[ "$validate_rc" = 2 ] && error unsupported_candidate_config_runtime_restore_failed || error candidate_validation_failed_runtime_restore_failed
|
|
else
|
|
[ "$validate_rc" = 2 ] && error unsupported_candidate_config || error candidate_validation_failed
|
|
fi
|
|
return 1
|
|
fi
|
|
[ "$u_initialized" = "$restore_initialized" ] || { backup_abort initialized_state_mismatch; return 1; }
|
|
(
|
|
cd "$temp_dir/archive" || exit 1
|
|
find etc uci -type f -print | sort > "$temp_dir/files.list"
|
|
)
|
|
make_manifest "$temp_dir/archive" "$state" "$temp_dir/archive/manifest.tsv" || { backup_abort manifest_create_failed; return 1; }
|
|
(
|
|
cd "$temp_dir/archive" || exit 1
|
|
{ printf '%s\n' manifest.tsv; cat "$temp_dir/files.list"; } > "$temp_dir/tar.list"
|
|
ulimit -f 65536 2>/dev/null || exit 1
|
|
tar -czf "$temp_dir/backup.tar.gz" -T "$temp_dir/tar.list"
|
|
) || { backup_abort archive_create_failed; return 1; }
|
|
[ "$(size_of "$temp_dir/backup.tar.gz")" -le "$MAX_COMPRESSED" ] || { backup_abort archive_too_large; return 1; }
|
|
check_tar_members "$temp_dir/backup.tar.gz" "$temp_dir/output-members" || { backup_abort archive_self_validation_failed; return 1; }
|
|
if [ -L "$BACKUP" ]; then backup_abort unsafe_backup_path; return 1; fi
|
|
mv -f "$temp_dir/backup.tar.gz" "$BACKUP" || { backup_abort backup_write_failed; return 1; }
|
|
chmod 0600 "$BACKUP" || { backup_abort backup_permissions_failed; return 1; }
|
|
if [ "$journal_active" = 1 ]; then
|
|
journal_write committed || { backup_abort transaction_journal_write_failed; return 1; }
|
|
rm -rf "$TXN_DIR" || { error backup_created_journal_cleanup_failed; return 1; }
|
|
journal_active=0
|
|
fi
|
|
if ! restore_running; then backup_abort backup_created_service_restart_failed; return 1; fi
|
|
json_result true "$BACKUP" backup_created
|
|
}
|
|
|
|
restore() {
|
|
check_root_file "$RESTORE" || { error unsafe_restore_path; return 1; }
|
|
if foreign_core_running; then error unmanaged_core_running; return 1; fi
|
|
if managed_core_pid >/dev/null 2>&1; then old_running=1; fi
|
|
if ! acquire_lock; then error "$lock_error"; return 1; fi
|
|
foreign_core_running && { error unmanaged_core_running; return 1; }
|
|
if [ "$old_running" = 1 ]; then managed_core_pid >/dev/null 2>&1 || { error service_state_changed; return 1; }
|
|
else managed_core_pid >/dev/null 2>&1 && { error service_state_changed; return 1; }; fi
|
|
temp_dir=$(mktemp -d /tmp/honk-maint.XXXXXX) || { error temporary_storage_unavailable; return 1; }
|
|
chmod 0700 "$temp_dir" || { error temporary_storage_unavailable; return 1; }
|
|
archive_hash=$(sha_of "$RESTORE")
|
|
[ -n "$archive_hash" ] || { error archive_read_failed; return 1; }
|
|
restore_stage=$temp_dir/extracted
|
|
extract_archive "$RESTORE" "$restore_stage"
|
|
extract_rc=$?
|
|
[ "$extract_rc" = 0 ] || { [ "$extract_rc" = 2 ] && error incompatible_core_version || error invalid_archive; return 1; }
|
|
uci_dir=$restore_stage/uci
|
|
parse_uci_file "$uci_dir" || { error invalid_uci_config; return 1; }
|
|
archive_state=$(sed -n '3s/^state\t//p' "$restore_stage/manifest.tsv")
|
|
if [ "$archive_state" = present ]; then
|
|
archive_sqlite_check "$restore_stage/etc/honk/state/honk.db" || { error invalid_sqlite_state; return 1; }
|
|
fi
|
|
load_uci_values || { error current_uci_invalid; return 1; }
|
|
old_enabled=$u_enabled
|
|
old_boot=$u_boot
|
|
old_initialized=$u_initialized
|
|
uci export honk > "$temp_dir/old-uci" 2>/dev/null || { error current_uci_snapshot_failed; return 1; }
|
|
capture_boot_links || { error boot_link_state_unavailable; return 1; }
|
|
if [ "$archive_state" = absent ] && { [ "$old_enabled" = 1 ] || [ "$old_boot" = 1 ] || [ "$old_running" = 1 ] || [ "$old_start_link" != - ]; }; then
|
|
error empty_archive_would_clear_active_state
|
|
return 1
|
|
fi
|
|
validate_candidate "$restore_stage" "$uci_dir" "$archive_state" archive
|
|
validate_rc=$?
|
|
if [ "$validate_rc" -ne 0 ]; then
|
|
[ "$validate_rc" = 2 ] && error unsupported_candidate_config || error candidate_validation_failed
|
|
return 1
|
|
fi
|
|
[ "$old_running" != 1 ] || [ "$restore_initialized" = 1 ] || { error running_service_requires_initialized_restore; return 1; }
|
|
[ "$(sha_of "$RESTORE")" = "$archive_hash" ] || { error restore_archive_changed; return 1; }
|
|
if [ "$(config_owner "$restore_stage/etc/honk/config.dae")" = system ]; then
|
|
write_live_system "$restore_stage/etc/honk/system.dae" "$candidate_device" "$candidate_ip" "$u_port" "$restore_initialized" || { error system_config_render_failed; return 1; }
|
|
chmod 0600 "$restore_stage/etc/honk/system.dae" || { error system_config_render_failed; return 1; }
|
|
fi
|
|
uci -c "$uci_dir" -t "$temp_dir/uci-tmp" set "honk.main.enabled=$old_enabled" || { error restore_profile_stage_failed; return 1; }
|
|
uci -c "$uci_dir" -t "$temp_dir/uci-tmp" set "honk.main.boot_enabled=$old_boot" || { error restore_profile_stage_failed; return 1; }
|
|
uci -c "$uci_dir" -t "$temp_dir/uci-tmp" set "honk.main.initialized=$restore_initialized" || { error restore_profile_stage_failed; return 1; }
|
|
uci -c "$uci_dir" -t "$temp_dir/uci-tmp" set honk.main.config_file=/etc/honk/config.dae || { error restore_profile_stage_failed; return 1; }
|
|
uci -c "$uci_dir" -t "$temp_dir/uci-tmp" commit honk || { error restore_profile_stage_failed; return 1; }
|
|
load_uci_values uci "$uci_dir" || { error restore_profile_stage_failed; return 1; }
|
|
[ "$u_enabled:$u_boot:$u_initialized" = "$old_enabled:$old_boot:$restore_initialized" ] || { error restore_profile_stage_failed; return 1; }
|
|
uci -c "$uci_dir" -t "$temp_dir/uci-tmp" export honk > "$temp_dir/new-uci" 2>/dev/null || { error restore_profile_stage_failed; return 1; }
|
|
if [ -d "$HONK_ROOT" ] && [ ! -L "$HONK_ROOT" ]; then :; else error current_tree_unavailable; return 1; fi
|
|
journal_create restore || { error transaction_journal_create_failed; return 1; }
|
|
mkdir "$TXN_DIR/new-tree" || { restore_abort restore_stage_failed; return 1; }
|
|
cp -pR "$restore_stage/etc/honk/." "$TXN_DIR/new-tree/" || { restore_abort restore_stage_failed; return 1; }
|
|
find "$TXN_DIR/new-tree" -type d -exec chmod 0700 {} \; 2>/dev/null
|
|
find "$TXN_DIR/new-tree" -type f -exec chmod 0600 {} \; 2>/dev/null
|
|
chmod 0600 "$TXN_DIR/new-tree/system.dae" || { restore_abort restore_stage_failed; return 1; }
|
|
if [ "$old_running" = 1 ]; then
|
|
service_stopped=1
|
|
stop_managed || { restore_abort service_stop_failed; return 1; }
|
|
fi
|
|
journal_write stopped || { restore_abort transaction_journal_write_failed; return 1; }
|
|
if foreign_core_running; then restore_abort unmanaged_core_running; return 1; fi
|
|
old_tree_path=$TXN_DIR/old-tree
|
|
new_tree_path=$TXN_DIR/new-tree
|
|
if ! mv "$HONK_ROOT" "$old_tree_path"; then restore_abort tree_swap_failed; return 1; fi
|
|
swap_started=1
|
|
journal_write old_moved || { restore_abort transaction_journal_write_failed; return 1; }
|
|
if ! mv "$new_tree_path" "$HONK_ROOT"; then restore_abort tree_swap_failed; return 1; fi
|
|
journal_write new_installed || { restore_abort transaction_journal_write_failed; return 1; }
|
|
if ! uci import honk < "$temp_dir/new-uci" || ! uci commit honk; then
|
|
apply_failed=1
|
|
else
|
|
apply_failed=0
|
|
fi
|
|
if [ "$apply_failed" = 0 ]; then
|
|
load_uci_values || apply_failed=1
|
|
[ "$apply_failed" = 1 ] || [ "$u_enabled:$u_boot:$u_initialized" = "$old_enabled:$old_boot:$restore_initialized" ] || apply_failed=1
|
|
if [ "$apply_failed" = 0 ] && [ "$old_running" = 1 ]; then
|
|
issue_capability candidate_health_start || apply_failed=1
|
|
fi
|
|
fi
|
|
if [ "$apply_failed" = 1 ]; then
|
|
restore_abort restore_failed
|
|
return 1
|
|
fi
|
|
journal_write service_started || { restore_abort transaction_journal_write_failed; return 1; }
|
|
journal_write committed || { restore_abort transaction_journal_write_failed; return 1; }
|
|
rm -rf "$TXN_DIR" || { error restore_succeeded_snapshot_cleanup_pending; return 1; }
|
|
journal_active=0
|
|
swap_started=0
|
|
service_stopped=0
|
|
json_result true "" restored
|
|
}
|
|
|
|
reset_data() {
|
|
if foreign_core_running; then error unmanaged_core_running; return 1; fi
|
|
if managed_core_pid >/dev/null 2>&1; then old_running=1; fi
|
|
if ! acquire_lock; then error "$lock_error"; return 1; fi
|
|
foreign_core_running && { error unmanaged_core_running; return 1; }
|
|
if [ "$old_running" = 1 ]; then managed_core_pid >/dev/null 2>&1 || { error service_state_changed; return 1; }
|
|
else managed_core_pid >/dev/null 2>&1 && { error service_state_changed; return 1; }; fi
|
|
temp_dir=$(mktemp -d /tmp/honk-maint.XXXXXX) || { error temporary_storage_unavailable; return 1; }
|
|
chmod 0700 "$temp_dir" || { error temporary_storage_unavailable; return 1; }
|
|
load_uci_values || { error current_uci_invalid; return 1; }
|
|
old_enabled=$u_enabled
|
|
old_boot=$u_boot
|
|
old_initialized=$u_initialized
|
|
uci export honk > "$temp_dir/old-uci" 2>/dev/null || { error current_uci_snapshot_failed; return 1; }
|
|
capture_boot_links || { error boot_link_state_unavailable; return 1; }
|
|
[ -d "$HONK_ROOT" ] && [ ! -L "$HONK_ROOT" ] || { error current_tree_unavailable; return 1; }
|
|
reset_stage=$temp_dir/reset-stage
|
|
reset_uci=$temp_dir/reset-uci
|
|
mkdir -p "$reset_stage/etc/honk" "$reset_uci" || { error temporary_storage_unavailable; return 1; }
|
|
[ -f /usr/share/honk/default-config.dae ] && [ ! -L /usr/share/honk/default-config.dae ] || { error default_config_unavailable; return 1; }
|
|
cp /usr/share/honk/default-config.dae "$reset_stage/etc/honk/config.dae" || { error reset_stage_failed; return 1; }
|
|
chmod 0600 "$reset_stage/etc/honk/config.dae" || { error reset_stage_failed; return 1; }
|
|
uci -c "$reset_uci" -t "$temp_dir/uci-tmp" import honk < "$temp_dir/old-uci" || { error reset_profile_stage_failed; return 1; }
|
|
uci -c "$reset_uci" -t "$temp_dir/uci-tmp" set honk.main.enabled=0 || { error reset_profile_stage_failed; return 1; }
|
|
uci -c "$reset_uci" -t "$temp_dir/uci-tmp" set honk.main.boot_enabled=0 || { error reset_profile_stage_failed; return 1; }
|
|
uci -c "$reset_uci" -t "$temp_dir/uci-tmp" delete honk.main.config_mode >/dev/null 2>&1 || :
|
|
uci -c "$reset_uci" -t "$temp_dir/uci-tmp" set honk.main.listen_port=9527 || { error reset_profile_stage_failed; return 1; }
|
|
uci -c "$reset_uci" -t "$temp_dir/uci-tmp" set honk.main.lan_network=lan || { error reset_profile_stage_failed; return 1; }
|
|
uci -c "$reset_uci" -t "$temp_dir/uci-tmp" set honk.main.clash_api_enabled=0 || { error reset_profile_stage_failed; return 1; }
|
|
uci -c "$reset_uci" -t "$temp_dir/uci-tmp" set honk.main.clash_api_port=9090 || { error reset_profile_stage_failed; return 1; }
|
|
uci -c "$reset_uci" -t "$temp_dir/uci-tmp" set honk.main.clash_api_secret= || { error reset_profile_stage_failed; return 1; }
|
|
uci -c "$reset_uci" -t "$temp_dir/uci-tmp" commit honk || { error reset_profile_stage_failed; return 1; }
|
|
load_uci_values uci "$reset_uci" || { error reset_profile_stage_failed; return 1; }
|
|
. /lib/functions/network.sh || { error lan_network_unavailable; return 1; }
|
|
reset_device=
|
|
reset_ip=
|
|
network_get_device reset_device "$u_network" || { error lan_network_unavailable; return 1; }
|
|
network_get_ipaddr reset_ip "$u_network" || { error lan_network_unavailable; return 1; }
|
|
write_live_system "$reset_stage/etc/honk/system.dae" "$reset_device" "$reset_ip" "$u_port" "$u_initialized" || { error system_config_render_failed; return 1; }
|
|
chmod 0600 "$reset_stage/etc/honk/system.dae" || { error system_config_render_failed; return 1; }
|
|
reset_candidate=$temp_dir/reset-candidate
|
|
mkdir -p "$reset_candidate/etc" || { error reset_stage_failed; return 1; }
|
|
cp -pR "$reset_stage/etc/honk" "$reset_candidate/etc/honk" || { error reset_stage_failed; return 1; }
|
|
uci -c "$reset_uci" -t "$temp_dir/uci-tmp" export honk > "$temp_dir/new-uci" 2>/dev/null || { error reset_profile_stage_failed; return 1; }
|
|
validate_candidate "$reset_candidate" "$reset_uci" absent
|
|
validate_rc=$?
|
|
[ "$validate_rc" = 0 ] || { [ "$validate_rc" = 2 ] && error default_config_unsupported || error default_config_validation_failed; return 1; }
|
|
journal_create reset || { error transaction_journal_create_failed; return 1; }
|
|
if [ "$old_running" = 1 ]; then
|
|
service_stopped=1
|
|
stop_managed || { restore_abort service_stop_failed; return 1; }
|
|
fi
|
|
journal_write stopped || { restore_abort transaction_journal_write_failed; return 1; }
|
|
foreign_core_running && { restore_abort unmanaged_core_running; return 1; }
|
|
checkpoint_sqlite || { restore_abort sqlite_checkpoint_failed; return 1; }
|
|
rm -rf "$reset_stage/etc/honk" || { restore_abort reset_stage_failed; return 1; }
|
|
stage_reset_tree "$HONK_ROOT" "$reset_stage/etc/honk" "$reset_candidate/etc/honk/system.dae" /usr/share/honk/default-config.dae || { restore_abort reset_stage_failed; return 1; }
|
|
purge_business_state "$reset_stage/etc/honk/state/honk.db" || { restore_abort state_reset_failed; return 1; }
|
|
reset_archive_state=absent
|
|
if [ -f "$reset_stage/etc/honk/state/honk.db" ]; then reset_archive_state=present; fi
|
|
if [ "$reset_archive_state" = present ]; then archive_sqlite_check "$reset_stage/etc/honk/state/honk.db" || { restore_abort state_reset_validation_failed; return 1; }; fi
|
|
rm -rf "$reset_candidate" || { restore_abort reset_stage_failed; return 1; }
|
|
mkdir -p "$reset_candidate/etc" || { restore_abort reset_stage_failed; return 1; }
|
|
cp -pR "$reset_stage/etc/honk" "$reset_candidate/etc/honk" || { restore_abort reset_stage_failed; return 1; }
|
|
mkdir "$TXN_DIR/new-tree" || { restore_abort reset_stage_failed; return 1; }
|
|
cp -pR "$reset_candidate/etc/honk/." "$TXN_DIR/new-tree/" || { restore_abort reset_stage_failed; return 1; }
|
|
find "$TXN_DIR/new-tree" -type d -exec chmod 0700 {} \; 2>/dev/null
|
|
find "$TXN_DIR/new-tree" -type f -exec chmod 0600 {} \; 2>/dev/null
|
|
chmod 0600 "$TXN_DIR/new-tree/system.dae" || { restore_abort reset_stage_failed; return 1; }
|
|
foreign_core_running && { restore_abort unmanaged_core_running; return 1; }
|
|
old_tree_path=$TXN_DIR/old-tree
|
|
new_tree_path=$TXN_DIR/new-tree
|
|
mv "$HONK_ROOT" "$old_tree_path" || { restore_abort tree_swap_failed; return 1; }
|
|
swap_started=1
|
|
journal_write old_moved || { restore_abort transaction_journal_write_failed; return 1; }
|
|
mv "$new_tree_path" "$HONK_ROOT" || { restore_abort tree_swap_failed; return 1; }
|
|
journal_write new_installed || { restore_abort transaction_journal_write_failed; return 1; }
|
|
uci import honk < "$temp_dir/new-uci" && uci commit honk || { restore_abort reset_failed; return 1; }
|
|
load_uci_values || { restore_abort reset_failed; return 1; }
|
|
[ "$u_enabled:$u_boot:$u_initialized:$u_network:$u_port" = "0:0:$old_initialized:lan:9527" ] || { restore_abort reset_failed; return 1; }
|
|
disable_boot_links || { restore_abort boot_link_update_failed; return 1; }
|
|
journal_write uci_applied || { restore_abort transaction_journal_write_failed; return 1; }
|
|
journal_write committed || { restore_abort transaction_journal_write_failed; return 1; }
|
|
rm -rf "$TXN_DIR" || { error reset_succeeded_snapshot_cleanup_pending; return 1; }
|
|
journal_active=0
|
|
swap_started=0
|
|
service_stopped=0
|
|
json_result true "" data_reset
|
|
}
|
|
|
|
stage_reset_tree() {
|
|
stage_source=$1
|
|
stage_target=$2
|
|
stage_system=$3
|
|
stage_default=$4
|
|
[ -d "$stage_source" ] && [ ! -L "$stage_source" ] || return 1
|
|
[ -f "$stage_system" ] && [ ! -L "$stage_system" ] || return 1
|
|
[ -f "$stage_default" ] && [ ! -L "$stage_default" ] || return 1
|
|
mkdir -p "$stage_target/state" || return 1
|
|
cp "$stage_default" "$stage_target/config.dae" || return 1
|
|
cp "$stage_system" "$stage_target/system.dae" || return 1
|
|
chmod 0600 "$stage_target/config.dae" || return 1
|
|
chmod 0600 "$stage_target/system.dae" || return 1
|
|
chmod 0700 "$stage_target/state" || return 1
|
|
if [ -e "$stage_source/state/honk.db" ] || [ -L "$stage_source/state/honk.db" ]; then
|
|
[ -f "$stage_source/state/honk.db" ] && [ ! -L "$stage_source/state/honk.db" ] || return 1
|
|
cp -p "$stage_source/state/honk.db" "$stage_target/state/honk.db" || return 1
|
|
chmod 0600 "$stage_target/state/honk.db" || return 1
|
|
fi
|
|
}
|
|
|
|
if [ "${HONK_MAINT_LIBRARY:-0}" = 1 ]; then
|
|
trap - 0 HUP INT TERM
|
|
return 0 2>/dev/null || exit 0
|
|
fi
|
|
|
|
case "$1" in
|
|
backup) backup ;;
|
|
restore) restore ;;
|
|
reset) reset_data ;;
|
|
recover) recover_action ;;
|
|
*) error invalid_action ;;
|
|
esac
|