mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-11-11 05:07:51 +08:00
1267 lines
59 KiB
Bash
Executable File
1267 lines
59 KiB
Bash
Executable File
#!/bin/sh
|
||
|
||
PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
||
export PATH
|
||
|
||
HONK_CORE=/usr/bin/honk-core
|
||
HONK_MAINT_DIR=/etc/.honk-maintenance
|
||
HONK_MAINT_JOURNAL=$HONK_MAINT_DIR/journal
|
||
HONK_MAINT_CAP_FILE=$HONK_MAINT_DIR/capability
|
||
|
||
. /lib/functions.sh
|
||
. /usr/share/honk/lock.sh
|
||
. /usr/share/honk/lifecycle.sh
|
||
|
||
fail() {
|
||
echo "honk: $*" >&2
|
||
return 1
|
||
}
|
||
|
||
load_settings() {
|
||
config_load honk
|
||
config_get_bool enabled main enabled 0
|
||
config_get_bool boot_enabled main boot_enabled 0
|
||
config_get_bool initialized main initialized 0
|
||
config_get config_file main config_file /etc/honk/config.dae
|
||
config_get listen_port main listen_port 9527
|
||
config_get_bool clash_api_enabled main clash_api_enabled 0
|
||
config_get clash_api_port main clash_api_port 9090
|
||
config_get clash_api_secret main clash_api_secret ''
|
||
config_get lan_network main lan_network lan
|
||
settings_owner=$(config_owner "$config_file")
|
||
}
|
||
|
||
valid_clash_api() {
|
||
[ "$clash_api_enabled" = 1 ] || return 0
|
||
case "$clash_api_port" in ''|*[!0-9]*) return 1 ;; esac
|
||
[ "$clash_api_port" -ge 1024 ] && [ "$clash_api_port" -le 65535 ] || return 1
|
||
[ "$clash_api_port" != "$listen_port" ] || return 1
|
||
case "$clash_api_secret" in ''|*[!A-Za-z0-9._-]*) return 1 ;; esac
|
||
[ "${#clash_api_secret}" -ge 6 ] && [ "${#clash_api_secret}" -le 128 ]
|
||
}
|
||
|
||
valid_network_name() {
|
||
case "$lan_network" in
|
||
''|*[!A-Za-z0-9_.-]*) return 1 ;;
|
||
*) return 0 ;;
|
||
esac
|
||
}
|
||
|
||
module_loaded() {
|
||
[ -d "/sys/module/$1" ] || grep -q "^$1 " /proc/modules
|
||
}
|
||
|
||
module_available() {
|
||
module_loaded "$1" && return 0
|
||
module_path=$(printf '%s' "$1" | tr '_' '-')
|
||
module_dir=/lib/modules/$(uname -r)
|
||
[ -r "$module_dir/modules.builtin" ] && grep -Eq "/${1}\.ko$|/${module_path}\.ko$" "$module_dir/modules.builtin" && return 0
|
||
find "$module_dir" -type f \( -name "$1.ko" -o -name "$1.ko.gz" -o -name "$1.ko.xz" -o -name "$1.ko.zst" \
|
||
-o -name "$module_path.ko" -o -name "$module_path.ko.gz" -o -name "$module_path.ko.xz" -o -name "$module_path.ko.zst" \) -print 2>/dev/null | grep -q .
|
||
}
|
||
|
||
system_config_readonly() {
|
||
permissions=$(ls -ldn /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
|
||
[ "$permissions" = '-rw-------' ] && return 0
|
||
[ "$permissions" = '-r--r--r--' ] && ! grep -q 'clash_api {' /etc/honk/system.dae
|
||
}
|
||
|
||
no_honk_resources() {
|
||
honk_lifecycle_reclaim_resources || :
|
||
for link in dae0 dae0peer; do
|
||
ip link show "$link" >/dev/null 2>&1 && { fail "$link 已存在(上次没有正常退出),请先停止服务或执行系统恢复"; return 1; }
|
||
done
|
||
ip netns list 2>/dev/null | awk '{print $1}' | grep -qx daens && { fail "daens 网络命名空间已存在,请先停止服务或执行系统恢复"; return 1; }
|
||
return 0
|
||
}
|
||
|
||
check_port() {
|
||
checked_port=${1:-$listen_port}
|
||
if command -v netstat >/dev/null 2>&1; then
|
||
netstat -lnt 2>/dev/null | awk '{print $4}' | grep -Eq "(^|:)${checked_port}$" && { fail "监听端口已被占用,请换一个端口"; return 1; }
|
||
elif command -v ss >/dev/null 2>&1; then
|
||
ss -lnt 2>/dev/null | awk '{print $4}' | grep -Eq "(^|:)${checked_port}$" && { fail "监听端口已被占用,请换一个端口"; return 1; }
|
||
else
|
||
fail "缺少 netstat/ss,无法检查端口占用(装 ip-full 或 net-tools)"
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
check_network() {
|
||
valid_network_name || { fail "LAN 网络名包含非法字符"; return 1; }
|
||
command -v ubus >/dev/null 2>&1 || { fail "ubus 不可用,无法校验网络设置"; return 1; }
|
||
ubus call "network.interface.$lan_network" status >/dev/null 2>&1 || { fail "选择的 LAN 网络在 netifd 里不存在,请在服务设置里重新选择"; return 1; }
|
||
}
|
||
|
||
# 本机 LAN/WAN 设备地址(跳过链路本地),与 honk 的 local_direct_cidrs 对齐
|
||
local_direct_addresses() {
|
||
list=''
|
||
. /lib/functions/network.sh
|
||
for net in "$lan_network" wan; do
|
||
network_get_device dev "$net" || continue
|
||
case "$dev" in ''|*[!A-Za-z0-9_.-]*) continue ;; esac
|
||
for addr in $(ip -o addr show dev "$dev" 2>/dev/null | awk '/inet6? / { split($4, a, "/"); print a[1] }'); do
|
||
case "$addr" in fe80:*|169.254.*) continue ;; esac
|
||
case "$addr" in ''|*[!0-9a-fA-F:.]*) continue ;; esac
|
||
[ "${#addr}" -le 45 ] || continue
|
||
case " $list " in *" $addr "*) continue ;; esac
|
||
list="${list:+$list, }$addr"
|
||
done
|
||
done
|
||
printf '%s\n' "$list"
|
||
}
|
||
|
||
# 把标记区块换成当前本机地址,honk 的 LAN self-protection 检查才认
|
||
sync_local_direct() {
|
||
begin=' # honk:local-direct-begin'
|
||
end=' # honk:local-direct-end'
|
||
grep -qxF "$begin" /etc/honk/config.dae || return 0
|
||
grep -qxF "$end" /etc/honk/config.dae || return 0
|
||
ips=$(local_direct_addresses)
|
||
[ -n "$ips" ] && rule=" dip($ips) && !dport(53) -> direct(must)" || rule=''
|
||
# honk-core 只认 .dae 后缀,临时文件必须带它
|
||
temp=/etc/honk/.config.$$.dae
|
||
( umask 077; : > "$temp" ) || return 1
|
||
awk -v begin="$begin" -v end="$end" -v rule="$rule" '
|
||
$0 == begin { print; if (rule != "") print rule; skip = 1; next }
|
||
skip && $0 == end { print; skip = 0; next }
|
||
skip { next }
|
||
{ print }
|
||
' /etc/honk/config.dae > "$temp" || { rm -f "$temp"; return 1; }
|
||
cmp -s "$temp" /etc/honk/config.dae && { rm -f "$temp"; return 0; }
|
||
"$HONK_CORE" config validate --config "$temp" >/dev/null 2>&1 || { rm -f "$temp"; return 1; }
|
||
chmod 0600 "$temp"
|
||
mv -f "$temp" /etc/honk/config.dae || { rm -f "$temp"; return 1; }
|
||
}
|
||
|
||
check_config() {
|
||
[ "$config_file" = /etc/honk/config.dae ] || { fail "配置路径必须为 /etc/honk/config.dae"; return 1; }
|
||
[ -f /etc/honk/config.dae ] && [ ! -L /etc/honk/config.dae ] || { fail "/etc/honk/config.dae 不是普通文件(可能被替换成软链)"; return 1; }
|
||
"$HONK_CORE" config validate --config /etc/honk/config.dae >/dev/null || { fail "config.dae 未通过静态校验,请先修正配置"; return 1; }
|
||
[ -r /usr/share/doona/index.html ] || { fail "缺少 Doona 静态文件(/usr/share/doona/index.html),请重新安装 luci-app-honk"; return 1; }
|
||
[ "$settings_owner" = system ] || return 0
|
||
[ -f /etc/honk/system.dae ] && [ ! -L /etc/honk/system.dae ] || { fail "/etc/honk/system.dae 不是普通文件(可能被替换成软链)"; return 1; }
|
||
system_config_readonly || { fail "/etc/honk/system.dae 权限不对,应为 0600"; return 1; }
|
||
case "$listen_port" in
|
||
''|*[!0-9]*) fail "监听端口必须是数字"; return 1 ;;
|
||
esac
|
||
[ "$listen_port" -ge 1024 ] && [ "$listen_port" -le 65535 ] || { fail "监听端口必须在 1024-65535 之间"; return 1; }
|
||
valid_clash_api || { fail "Clash API 设置不合法(端口 1024-65535,密钥 6-128 位)"; return 1; }
|
||
}
|
||
|
||
# 顺序同 honk-core:设了 HONK_VMLINUX_BTF 就只认它
|
||
kernel_btf_path() {
|
||
if [ -n "${HONK_VMLINUX_BTF:-}" ]; then
|
||
[ -r "$HONK_VMLINUX_BTF" ] && { printf '%s\n' "$HONK_VMLINUX_BTF"; return 0; }
|
||
return 1
|
||
fi
|
||
for btf_file in /sys/kernel/btf/vmlinux /usr/lib/debug/boot/vmlinux; do
|
||
[ -r "$btf_file" ] && { printf '%s\n' "$btf_file"; return 0; }
|
||
done
|
||
return 1
|
||
}
|
||
|
||
check_kernel() {
|
||
kernel_error=""
|
||
kernel=$(uname -r | cut -d- -f1)
|
||
major=$(printf '%s' "$kernel" | cut -d. -f1)
|
||
minor=$(printf '%s' "$kernel" | cut -d. -f2)
|
||
if [ "$(jsonfilter -i /usr/share/honk/core-capabilities.json -e '@.lane' 2>/dev/null)" = linux66 ]; then
|
||
expected_hash=$(jsonfilter -i /usr/share/honk/core-capabilities.json -e '@.core_sha256' 2>/dev/null)
|
||
actual_hash=$(sha256sum "$HONK_CORE" 2>/dev/null | cut -d' ' -f1)
|
||
[ -n "$expected_hash" ] && [ "$expected_hash" = "$actual_hash" ] || { kernel_error=core_capabilities_mismatch; fail "核心与兼容信息不匹配,请重新安装 honk"; return 1; }
|
||
patch=$(printf '%s' "$kernel" | cut -d. -f3 | sed 's/[^0-9].*//')
|
||
[ "$major" -gt 6 ] || { [ "$major" -eq 6 ] && { [ "$minor" -gt 6 ] || { [ "$minor" -eq 6 ] && [ "${patch:-0}" -ge 40 ]; }; }; } || { kernel_error=kernel_compat_too_old; fail "兼容核心需要 Linux 6.6.40 或更新"; return 1; }
|
||
else
|
||
[ "$major" -gt 6 ] || { [ "$major" -eq 6 ] && [ "$minor" -ge 12 ]; } || { kernel_error=kernel_too_old; fail "内核版本过低,需要 Linux 6.12 或更新"; return 1; }
|
||
fi
|
||
if ! kernel_btf_path >/dev/null; then
|
||
if [ -n "${HONK_VMLINUX_BTF:-}" ]; then
|
||
kernel_error=kernel_btf_override_invalid
|
||
fail "HONK_VMLINUX_BTF 指向的 $HONK_VMLINUX_BTF 不可读,honk-core 不会回退到默认路径"
|
||
else
|
||
kernel_error=kernel_btf_missing
|
||
fail "内核缺少 BTF(/sys/kernel/btf/vmlinux 或 /usr/lib/debug/boot/vmlinux),请安装 vmlinux-btf 或换用带 BTF 的固件"
|
||
fi
|
||
return 1
|
||
fi
|
||
[ -d /sys/fs/bpf ] && grep -q ' /sys/fs/bpf bpf ' /proc/mounts || { kernel_error=bpffs_not_mounted; fail "bpffs 未挂载到 /sys/fs/bpf,请检查内核或手工挂载"; return 1; }
|
||
command -v ip >/dev/null 2>&1 || { kernel_error=ip_full_missing; fail "缺少 ip 命令(装 ip-full)"; return 1; }
|
||
command -v modprobe >/dev/null 2>&1 || { kernel_error=kmod_missing; fail "缺少 modprobe 命令(装 kmod)"; return 1; }
|
||
for module in sch_ingress cls_bpf veth nft_queue nfnetlink_queue; do
|
||
module_available "$module" || { kernel_error="kernel_module_missing:$module"; fail "缺少内核模块 $module,请确认 kmod-sched-core / kmod-sched-bpf / kmod-veth / kmod-nft-queue 已安装"; return 1; }
|
||
done
|
||
}
|
||
|
||
check_conflicts() {
|
||
for process in dae daed clashoo mihomo sing-box; do
|
||
pidof "$process" >/dev/null 2>&1 && { fail "检测到冲突进程 $process 正在运行,请先停掉它(多代理同时开会互相抢流量)"; return 1; }
|
||
done
|
||
for service in dae daed clashoo mihomo sing-box; do
|
||
if [ -x "/etc/init.d/$service" ] && "/etc/init.d/$service" running >/dev/null 2>&1; then
|
||
fail "检测到冲突服务 $service 正在运行,请先停用它(多代理同时开会互相抢流量)"
|
||
return 1
|
||
fi
|
||
done
|
||
}
|
||
|
||
maintenance_value() {
|
||
awk -F= -v key="$1" '$1 == key { value = substr($0, length($1) + 2); count++ } END { if (count == 1) print value; else exit 1 }' "$2"
|
||
}
|
||
|
||
proc_starttime() {
|
||
[ -r "/proc/$1/stat" ] || return 1
|
||
awk '{ sub(/^.*\) /, ""); split($0, fields, /[[:space:]]+/); if (fields[20] ~ /^[0-9]+$/) print fields[20]; else exit 1 }' "/proc/$1/stat" 2>/dev/null
|
||
}
|
||
|
||
protected_root_file() {
|
||
[ -f "$1" ] && [ ! -L "$1" ] || return 1
|
||
protected_permissions=$(ls -ln "$1" 2>/dev/null | awk 'NR == 1 { print $1 ":" $3 ":" $4 }')
|
||
[ "$protected_permissions" = '-rw-------:0:0' ]
|
||
}
|
||
|
||
consume_maintenance_capability() {
|
||
[ -n "${HONK_MAINT_CAP:-}" ] || return 1
|
||
[ -d "$HONK_MAINT_DIR" ] && [ ! -L "$HONK_MAINT_DIR" ] || return 1
|
||
protected_root_file "$HONK_MAINT_JOURNAL" && protected_root_file "$HONK_MAINT_CAP_FILE" || return 1
|
||
awk -F= 'BEGIN { split("format nonce txn_id phase owner_pid owner_starttime", keys, " ") } NR > 6 || index($0, "=") == 0 || $1 != keys[NR] { exit 1 } END { if (NR != 6) exit 1 }' "$HONK_MAINT_CAP_FILE" || return 1
|
||
consumed_cap=$HONK_MAINT_DIR/.capability-consumed.$$
|
||
[ ! -e "$consumed_cap" ] || return 1
|
||
mv "$HONK_MAINT_CAP_FILE" "$consumed_cap" 2>/dev/null || return 1
|
||
# Chain with any existing EXIT trap instead of overwriting it.
|
||
_prev_exit_trap=$(trap -p EXIT | sed "s/^trap -- '//;s/' EXIT$//")
|
||
trap 'rm -f "$consumed_cap"; [ -n "$_prev_exit_trap" ] && eval "$_prev_exit_trap"' 0
|
||
cap_format=$(maintenance_value format "$consumed_cap") || return 1
|
||
cap_nonce=$(maintenance_value nonce "$consumed_cap") || return 1
|
||
cap_txn=$(maintenance_value txn_id "$consumed_cap") || return 1
|
||
cap_phase=$(maintenance_value phase "$consumed_cap") || return 1
|
||
cap_pid=$(maintenance_value owner_pid "$consumed_cap") || return 1
|
||
cap_starttime=$(maintenance_value owner_starttime "$consumed_cap") || return 1
|
||
journal_format=$(maintenance_value format "$HONK_MAINT_JOURNAL") || return 1
|
||
journal_operation=$(maintenance_value operation "$HONK_MAINT_JOURNAL") || return 1
|
||
journal_phase=$(maintenance_value phase "$HONK_MAINT_JOURNAL") || return 1
|
||
journal_txn=$(maintenance_value txn_id "$HONK_MAINT_JOURNAL") || return 1
|
||
case "$cap_nonce:$cap_txn:$cap_pid:$cap_starttime" in *[!0-9a-f:]*|::*|:|'' ) return 1 ;; esac
|
||
case "$cap_nonce" in *[!0-9a-f]*|'') return 1 ;; esac
|
||
[ "${#cap_nonce}" -eq 32 ] && [ "${#cap_txn}" -eq 32 ] || return 1
|
||
[ "$cap_format" = 1 ] && [ "$journal_format" = 1 ] || return 1
|
||
case "$cap_pid:$cap_starttime" in *[!0-9:]*) return 1 ;; esac
|
||
[ "$cap_pid" -gt 1 ] && [ "$cap_starttime" -gt 0 ] || return 1
|
||
[ "$HONK_MAINT_CAP" = "$cap_nonce" ] && [ "$cap_txn" = "$journal_txn" ] || return 1
|
||
case "$cap_phase:$journal_operation:$journal_phase" in
|
||
candidate_health_start:restore:new_installed|candidate_health_start:import_apply:new_installed|rollback_old_start:restore:rollback_old_start|rollback_old_start:reset:rollback_old_start|rollback_old_start:import_apply:rollback_old_start) ;;
|
||
*) return 1 ;;
|
||
esac
|
||
read -r lock_pid lock_starttime < "$HONK_LOCK_DIR/pid" || return 1
|
||
[ "$lock_pid:$lock_starttime" = "$cap_pid:$cap_starttime" ] || return 1
|
||
[ "$(proc_starttime "$cap_pid")" = "$cap_starttime" ] || return 1
|
||
return 0
|
||
}
|
||
|
||
maintenance_start_guard() {
|
||
if [ -e "$HONK_MAINT_JOURNAL" ] || [ -L "$HONK_MAINT_JOURNAL" ]; then
|
||
consume_maintenance_capability || { fail "maintenance_recovery_required"; return 1; }
|
||
elif [ -n "${HONK_MAINT_CAP:-}" ]; then
|
||
fail "maintenance_capability_without_journal"
|
||
return 1
|
||
fi
|
||
if lock_owner_invalid || honk_lock_is_stale; then
|
||
# The recorded owner is gone, so nothing is in flight. Reclaim the lock
|
||
# instead of refusing every later start: a caller that timed out cannot
|
||
# release it, and leaving it wedges the service until someone SSHes in.
|
||
# A live lock still blocks below through honk_lock_acquire.
|
||
logger -t honk "reclaiming stale operation lock" 2>/dev/null || :
|
||
rm -rf "$HONK_LOCK_DIR"
|
||
fi
|
||
}
|
||
|
||
lock_owner_invalid() {
|
||
[ -d "$HONK_LOCK_DIR" ] || return 1
|
||
lock_owner=$(cat "$HONK_LOCK_DIR/pid" 2>/dev/null)
|
||
# pid file now stores "pid starttime"; only the leading PID matters here.
|
||
lock_owner=${lock_owner%% *}
|
||
case "$lock_owner" in ''|*[!0-9]*) return 0 ;; esac
|
||
return 1
|
||
}
|
||
|
||
preflight() {
|
||
load_settings
|
||
[ "$enabled" = 1 ] && [ "$initialized" = 1 ] || { fail "请先在 LuCI 里启用并完成初始化"; return 1; }
|
||
# 只有 system.dae 负责这些字段时才检查 UCI 端口与网络;完整配置自行定义时不碰。
|
||
if [ "$settings_owner" = system ]; then
|
||
maintenance_start_guard && check_config && check_network && check_kernel && no_honk_resources && check_conflicts && check_port && {
|
||
[ "$clash_api_enabled" = 0 ] || check_port "$clash_api_port"
|
||
}
|
||
return $?
|
||
fi
|
||
maintenance_start_guard && check_config && check_kernel && no_honk_resources && check_conflicts
|
||
}
|
||
|
||
prepare_runtime() {
|
||
preflight || return 1
|
||
for module in sch_ingress cls_bpf veth nft_queue nfnetlink_queue; do
|
||
module_loaded "$module" || modprobe "$module" || { fail "无法加载内核模块 $module,请确认对应 kmod 包已安装"; return 1; }
|
||
done
|
||
mkdir -p /etc/honk/state || { fail "无法创建状态目录(/etc/honk/state),请检查磁盘空间与权限"; return 1; }
|
||
chmod 0700 /etc/honk/state || return 1
|
||
}
|
||
|
||
valid_username() {
|
||
case "$1" in
|
||
''|*[!A-Za-z0-9_.-]*) return 1 ;;
|
||
esac
|
||
[ "$(printf '%s' "$1" | wc -c)" -le 64 ]
|
||
}
|
||
|
||
valid_password() {
|
||
password_bytes=$(printf '%s' "$1" | wc -c)
|
||
[ "$password_bytes" -ge 8 ] && [ "$password_bytes" -le 512 ]
|
||
}
|
||
|
||
read_credentials() {
|
||
json_file=$(mktemp /tmp/honk-credentials.XXXXXX) || return 1
|
||
chmod 0600 "$json_file" || { rm -f "$json_file"; return 1; }
|
||
cat > "$json_file" || { rm -f "$json_file"; return 1; }
|
||
json_bytes=$(wc -c < "$json_file")
|
||
[ "$json_bytes" -le 2048 ] || { rm -f "$json_file"; return 1; }
|
||
grep -Fq '\u0000' "$json_file" && { rm -f "$json_file"; return 1; }
|
||
newline='
|
||
'
|
||
username_raw=$(jsonfilter -i "$json_file" -e '@.username' 2>/dev/null; printf '__HNK_END__')
|
||
password_raw=$(jsonfilter -i "$json_file" -e '@.password' 2>/dev/null; printf '__HNK_END__')
|
||
username=${username_raw%__HNK_END__}
|
||
password=${password_raw%__HNK_END__}
|
||
username=${username%"$newline"}
|
||
password=${password%"$newline"}
|
||
rm -f "$json_file"
|
||
[ -n "$username" ] && [ -n "$password" ] || return 1
|
||
valid_username "$username" && valid_password "$password"
|
||
}
|
||
|
||
network_settings() {
|
||
. /lib/functions/network.sh
|
||
network_get_device lan_device "$lan_network" || return 1
|
||
network_get_ipaddr lan_ip "$lan_network" || return 1
|
||
case "$lan_device" in
|
||
''|*[!A-Za-z0-9_.-]*) return 1 ;;
|
||
esac
|
||
[ "$(printf '%s' "$lan_device" | wc -c)" -le 15 ] || return 1
|
||
printf '%s\n' "$lan_ip" | awk -F. 'NF == 4 { for (i=1;i<=4;i++) if ($i !~ /^[0-9]+$/ || $i > 255) exit 1; exit 0 } { exit 1 }' || return 1
|
||
}
|
||
|
||
ensure_loopback_system() {
|
||
current_hash=$(sha256sum /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
|
||
[ "$current_hash" = "$system_written_hash" ] || { fail "初始化期间 system.dae 被其他进程改动,已中止"; return 1; }
|
||
current_listen=$(sed -n "s/^[[:space:]]*listen:[[:space:]]*'\([^']*\)'.*/\1/p" /etc/honk/system.dae | head -n 1)
|
||
[ "$current_listen" = "127.0.0.1:$listen_port" ] && return 0
|
||
temp_system=$(mktemp /etc/honk/.system.dae.XXXXXX) || return 1
|
||
sed "s/^[[:space:]]*listen:[[:space:]]*'[^']*'.*/ listen: '127.0.0.1:$listen_port'/" /etc/honk/system.dae > "$temp_system" || { rm -f "$temp_system"; return 1; }
|
||
chmod 0600 "$temp_system" || { rm -f "$temp_system"; return 1; }
|
||
current_hash=$(sha256sum /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
|
||
[ "$current_hash" = "$system_written_hash" ] || { rm -f "$temp_system"; fail "初始化期间 system.dae 被其他进程改动,已中止"; return 1; }
|
||
mv -f "$temp_system" /etc/honk/system.dae || { rm -f "$temp_system"; return 1; }
|
||
system_written_hash=$(sha256sum /etc/honk/system.dae | awk '{print $1}')
|
||
}
|
||
|
||
restore_system() {
|
||
[ -n "$system_written_hash" ] || return 0
|
||
current_hash=$(sha256sum /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
|
||
[ "$current_hash" = "$system_written_hash" ] || { fail "回滚期间 system.dae 被其他进程改动,已中止"; return 1; }
|
||
[ "$current_hash" != "$original_system_hash" ] || { system_written_hash=""; return 0; }
|
||
if [ -z "$original_system_hash" ]; then
|
||
rm -f /etc/honk/system.dae || return 1
|
||
system_written_hash=""
|
||
return 0
|
||
fi
|
||
temp_restore=$(mktemp /etc/honk/.system.dae.restore.XXXXXX) || return 1
|
||
cp -p "$old_system" "$temp_restore" || { rm -f "$temp_restore"; return 1; }
|
||
current_hash=$(sha256sum /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
|
||
[ "$current_hash" = "$system_written_hash" ] || { rm -f "$temp_restore"; fail "回滚期间 system.dae 被其他进程改动,已中止"; return 1; }
|
||
mv -f "$temp_restore" /etc/honk/system.dae || { rm -f "$temp_restore"; return 1; }
|
||
system_written_hash=""
|
||
}
|
||
|
||
cleanup_initialize() {
|
||
if mock_stop >/dev/null 2>&1; then
|
||
if [ -n "$system_written_hash" ] && ! restore_system; then
|
||
logger -t honk "system config rollback refused or failed; inspect /etc/honk/system.dae"
|
||
fi
|
||
rm -rf "$temp_dir"
|
||
honk_lock_release >/dev/null 2>&1
|
||
else
|
||
logger -t honk "mock process cleanup failed; keeping operation lock and temporary files"
|
||
fi
|
||
}
|
||
|
||
# 交还面板管理时也用它,保证两条路径生成的 system.dae 一致。
|
||
render_runtime_system() {
|
||
printf "global {\n wan_interface: auto\n lan_interface: '%s'\n" "$1"
|
||
printf " log_level: info\n dial_mode: domain\n allow_insecure: false\n"
|
||
printf " auto_config_kernel_parameter: true\n data_dir: '/etc/honk'\n"
|
||
printf " store_subscribe: true\n nfqueue_enable: true\n}\n\n"
|
||
printf "experimental {\n native_api {\n enabled: true\n listen: '%s:%s'\n" "$2" "$3"
|
||
printf " password_auth: true\n allow_anonymous_loopback: false\n"
|
||
printf " config_write: true\n ui: '/usr/share/doona'\n }\n"
|
||
if [ "$clash_api_enabled" = 1 ]; then
|
||
printf " clash_api {\n external_controller: '%s:%s'\n external_ui: '/usr/share/honk/zashboard'\n secret: '%s'\n default_mode: Rule\n }\n" "$2" "$clash_api_port" "$clash_api_secret"
|
||
fi
|
||
printf "}\n"
|
||
}
|
||
|
||
write_runtime_system() {
|
||
lan_device=$1
|
||
lan_ip=$2
|
||
system_port=${3:-$listen_port}
|
||
system_listen=${4:-$lan_ip}
|
||
valid_clash_api || { fail "Clash API 设置不合法(端口 1024-65535,密钥 6-128 位)"; return 1; }
|
||
current_hash=$(sha256sum /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
|
||
[ "$current_hash" = "$system_written_hash" ] || { fail "写入运行配置时 system.dae 被其他进程改动,已中止"; return 1; }
|
||
temp_system=$(mktemp /etc/honk/.system.dae.XXXXXX) || return 1
|
||
render_runtime_system "$lan_device" "$system_listen" "$system_port" > "$temp_system" || { rm -f "$temp_system"; return 1; }
|
||
chmod 0600 "$temp_system" || { rm -f "$temp_system"; return 1; }
|
||
current_hash=$(sha256sum /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
|
||
[ "$current_hash" = "$system_written_hash" ] || { rm -f "$temp_system"; fail "写入运行配置时 system.dae 被其他进程改动,已中止"; return 1; }
|
||
mv -f "$temp_system" /etc/honk/system.dae || { rm -f "$temp_system"; return 1; }
|
||
system_written_hash=$(sha256sum /etc/honk/system.dae | awk '{print $1}')
|
||
}
|
||
|
||
managed_core_pid() {
|
||
honk_lifecycle_managed_pid
|
||
}
|
||
|
||
configured_system_port() {
|
||
sed -n "s/^[[:space:]]*listen:[[:space:]]*'[^:]*:\([0-9][0-9]*\)'.*/\1/p" /etc/honk/system.dae 2>/dev/null | head -n 1
|
||
}
|
||
|
||
configured_clash_port() {
|
||
sed -n "s/^[[:space:]]*external_controller:[[:space:]]*'[^:]*:\([0-9][0-9]*\)'.*/\1/p" /etc/honk/system.dae 2>/dev/null | head -n 1
|
||
}
|
||
|
||
sync_boot_link() {
|
||
if [ "$1" = 1 ]; then
|
||
/etc/init.d/honk enable
|
||
else
|
||
/etc/init.d/honk disable
|
||
fi
|
||
}
|
||
|
||
restore_old_runtime() {
|
||
[ "$old_running" = 1 ] || return 0
|
||
runtime_enabled=$(uci -q get honk.main.enabled || echo 0)
|
||
if [ "$runtime_enabled" != 1 ]; then uci set honk.main.enabled=1 && uci commit honk || return 1; fi
|
||
honk_lifecycle_start 30 || {
|
||
[ "$runtime_enabled" = 1 ] || { uci set "honk.main.enabled=$runtime_enabled" && uci commit honk; }
|
||
return 1
|
||
}
|
||
if [ "$runtime_enabled" != 1 ]; then uci set "honk.main.enabled=$runtime_enabled" && uci commit honk || return 1; fi
|
||
}
|
||
|
||
settings_rollback() {
|
||
rollback_ok=1
|
||
honk_lifecycle_stop 30 || return 1
|
||
if [ -n "$system_written_hash" ] && ! restore_system; then rollback_ok=0; fi
|
||
if ! uci import honk < "$old_uci" || ! uci commit honk; then rollback_ok=0; fi
|
||
if ! sync_boot_link "$old_boot_enabled"; then rollback_ok=0; fi
|
||
restore_old_runtime || rollback_ok=0
|
||
[ "$rollback_ok" = 1 ]
|
||
}
|
||
|
||
settings() {
|
||
if [ -e /etc/.honk-update/journal ] || [ -L /etc/.honk-update/journal ]; then echo '{"ok":false,"message":"update_recovery_required"}'; return 1; fi
|
||
load_settings
|
||
[ "$settings_owner" = system ] || { echo '{"ok":false,"message":"config_settings_owned_by_file"}'; return 1; }
|
||
. /usr/share/libubox/jshn.sh || { echo '{"ok":false,"message":"jshn_unavailable"}'; return 1; }
|
||
# Bound stdin to 64KiB (DoS protection); read one extra byte to detect truncation.
|
||
settings_json=$(head -c 65537)
|
||
if [ "${#settings_json}" -gt 65536 ]; then
|
||
echo '{"ok":false,"message":"settings_too_large"}'; return 1
|
||
fi
|
||
json_load "$settings_json" || { echo '{"ok":false,"message":"invalid_settings_json"}'; return 1; }
|
||
json_get_var new_network lan_network
|
||
json_get_type network_type lan_network
|
||
json_get_var new_port listen_port
|
||
json_get_type port_type listen_port
|
||
json_get_var new_boot_enabled boot_enabled
|
||
json_get_type boot_enabled_type boot_enabled
|
||
json_get_var new_clash_enabled clash_api_enabled
|
||
json_get_type clash_enabled_type clash_api_enabled
|
||
json_get_var new_clash_port clash_api_port
|
||
json_get_type clash_port_type clash_api_port
|
||
json_get_var new_clash_secret clash_api_secret
|
||
json_get_type clash_secret_type clash_api_secret
|
||
[ "$network_type" = string ] || { echo '{"ok":false,"message":"invalid_lan_network"}'; return 1; }
|
||
case "$port_type" in int|integer) ;; *) echo '{"ok":false,"message":"invalid_listen_port"}'; return 1 ;; esac
|
||
# boot_enabled 已内置(恒为 1),前端不再提交,缺省就按当前值处理
|
||
case "$boot_enabled_type" in ''|null|boolean) ;; *) echo '{"ok":false,"message":"invalid_boolean_settings"}'; return 1 ;; esac
|
||
case "$clash_enabled_type" in ''|null|boolean) ;; *) echo '{"ok":false,"message":"invalid_clash_api_settings"}'; return 1 ;; esac
|
||
case "$clash_port_type" in ''|null|int|integer) ;; *) echo '{"ok":false,"message":"invalid_clash_api_settings"}'; return 1 ;; esac
|
||
case "$clash_secret_type" in ''|null|string) ;; *) echo '{"ok":false,"message":"invalid_clash_api_settings"}'; return 1 ;; esac
|
||
case "$new_network" in ''|*[!A-Za-z0-9_.-]*) echo '{"ok":false,"message":"invalid_lan_network"}'; return 1 ;; esac
|
||
case "$new_port" in ''|*[!0-9]*) echo '{"ok":false,"message":"invalid_listen_port"}'; return 1 ;; esac
|
||
[ "$new_port" -ge 1024 ] && [ "$new_port" -le 65535 ] || { echo '{"ok":false,"message":"invalid_listen_port"}'; return 1; }
|
||
load_settings
|
||
case "$boot_enabled_type" in ''|null) new_boot_enabled=1 ;; esac
|
||
new_boot_enabled=1
|
||
case "$clash_enabled_type" in ''|null) new_clash_enabled=$clash_api_enabled ;; esac
|
||
case "$clash_port_type" in ''|null) new_clash_port=$clash_api_port ;; esac
|
||
[ -n "$new_clash_secret" ] || new_clash_secret=$clash_api_secret
|
||
new_enabled=$enabled
|
||
old_network=$lan_network
|
||
old_port=$listen_port
|
||
old_enabled=$enabled
|
||
old_boot_enabled=$boot_enabled
|
||
old_initialized=$initialized
|
||
old_clash_enabled=$clash_api_enabled
|
||
old_clash_port=$clash_api_port
|
||
old_clash_secret=$clash_api_secret
|
||
[ "$initialized" = 1 ] || { [ "$new_enabled" = 0 ] || { echo '{"ok":false,"message":"initialize_before_enabling_service"}'; return 1; }; }
|
||
lan_network=$new_network
|
||
listen_port=$new_port
|
||
clash_api_enabled=$new_clash_enabled
|
||
clash_api_port=$new_clash_port
|
||
clash_api_secret=$new_clash_secret
|
||
valid_clash_api || { echo '{"ok":false,"message":"invalid_clash_api_settings"}'; return 1; }
|
||
check_network || { echo '{"ok":false,"message":"lan_network_unavailable"}'; return 1; }
|
||
network_settings || { echo '{"ok":false,"message":"lan_ipv4_unavailable"}'; return 1; }
|
||
old_running=0
|
||
if managed_core_pid >/dev/null 2>&1; then
|
||
old_running=1
|
||
elif pidof honk-core >/dev/null 2>&1; then
|
||
echo '{"ok":false,"message":"unmanaged_core_running"}'
|
||
return 1
|
||
fi
|
||
if ! { [ "$old_running" = 1 ] && [ "$(configured_system_port)" = "$new_port" ]; }; then
|
||
check_port || { echo '{"ok":false,"message":"listen_port_occupied"}'; return 1; }
|
||
fi
|
||
if [ "$clash_api_enabled" = 1 ] && ! { [ "$old_running" = 1 ] && [ "$(configured_clash_port)" = "$new_clash_port" ]; }; then
|
||
check_port "$new_clash_port" || { echo '{"ok":false,"message":"clash_api_port_occupied"}'; return 1; }
|
||
fi
|
||
honk_lock_acquire || { echo '{"ok":false,"message":"operation_in_progress"}'; return 1; }
|
||
trap 'honk_lock_release >/dev/null 2>&1' 0
|
||
load_settings
|
||
[ "$lan_network" = "$old_network" ] && [ "$listen_port" = "$old_port" ] && [ "$enabled" = "$old_enabled" ] && [ "$boot_enabled" = "$old_boot_enabled" ] && [ "$initialized" = "$old_initialized" ] && [ "$clash_api_enabled" = "$old_clash_enabled" ] && [ "$clash_api_port" = "$old_clash_port" ] && [ "$clash_api_secret" = "$old_clash_secret" ] || { echo '{"ok":false,"message":"settings_changed_concurrently"}'; return 1; }
|
||
new_enabled=$enabled
|
||
managed_core_pid >/dev/null 2>&1 && recheck_running=1 || recheck_running=0
|
||
[ "$recheck_running" = "$old_running" ] || { echo '{"ok":false,"message":"service_state_changed_concurrently"}'; return 1; }
|
||
old_running=$recheck_running
|
||
lan_network=$new_network
|
||
listen_port=$new_port
|
||
clash_api_enabled=$new_clash_enabled
|
||
clash_api_port=$new_clash_port
|
||
clash_api_secret=$new_clash_secret
|
||
check_network && network_settings || { echo '{"ok":false,"message":"lan_network_changed_during_settings"}'; return 1; }
|
||
if ! { [ "$old_running" = 1 ] && [ "$(configured_system_port)" = "$new_port" ]; }; then
|
||
check_port || { echo '{"ok":false,"message":"listen_port_occupied"}'; return 1; }
|
||
fi
|
||
if [ "$clash_api_enabled" = 1 ] && ! { [ "$old_running" = 1 ] && [ "$(configured_clash_port)" = "$new_clash_port" ]; }; then
|
||
check_port "$new_clash_port" || { echo '{"ok":false,"message":"clash_api_port_occupied"}'; return 1; }
|
||
fi
|
||
if [ "$initialized" = 1 ] && ! check_config; then echo '{"ok":false,"message":"package_config_unavailable"}'; return 1; fi
|
||
umask 077
|
||
settings_dir=$(mktemp -d /tmp/honk-settings.XXXXXX) || { echo '{"ok":false,"message":"temporary_storage_unavailable"}'; return 1; }
|
||
old_uci=$settings_dir/honk.uci
|
||
uci export honk > "$old_uci" || { rm -rf "$settings_dir"; echo '{"ok":false,"message":"uci_snapshot_failed"}'; return 1; }
|
||
old_system=$settings_dir/system.dae
|
||
system_written_hash=""
|
||
if [ "$initialized" = 1 ]; then
|
||
[ -f /etc/honk/system.dae ] && [ ! -L /etc/honk/system.dae ] || { rm -rf "$settings_dir"; echo '{"ok":false,"message":"system_config_unavailable"}'; return 1; }
|
||
cp -p /etc/honk/system.dae "$old_system" || { rm -rf "$settings_dir"; echo '{"ok":false,"message":"system_snapshot_failed"}'; return 1; }
|
||
original_system_hash=$(sha256sum "$old_system" | awk '{print $1}')
|
||
system_written_hash=$original_system_hash
|
||
fi
|
||
if [ "$old_running" = 1 ]; then
|
||
honk_lifecycle_stop 30 || {
|
||
settings_rollback; rollback_status=$?
|
||
if [ "$rollback_status" = 0 ]; then rm -rf "$settings_dir"; echo '{"ok":false,"message":"service_stop_failed_rolled_back"}'; else logger -t honk "settings rollback failed; snapshot retained at $settings_dir"; echo '{"ok":false,"message":"service_stop_and_rollback_failed_snapshot_retained"}'; fi
|
||
return 1
|
||
}
|
||
fi
|
||
if [ "$initialized" = 1 ] && ! write_runtime_system "$lan_device" "$lan_ip" "$new_port"; then
|
||
settings_rollback; rollback_status=$?
|
||
if [ "$rollback_status" = 0 ]; then rm -rf "$settings_dir"; echo '{"ok":false,"message":"system_config_write_failed_rolled_back"}'; else logger -t honk "settings rollback failed; snapshot retained at $settings_dir"; echo '{"ok":false,"message":"system_config_write_and_rollback_failed_snapshot_retained"}'; fi
|
||
return 1
|
||
fi
|
||
uci set "honk.main.lan_network=$new_network" && uci set "honk.main.listen_port=$new_port" && uci set "honk.main.enabled=$new_enabled" && uci set "honk.main.boot_enabled=$new_boot_enabled" && uci set "honk.main.clash_api_enabled=$new_clash_enabled" && uci set "honk.main.clash_api_port=$new_clash_port" && uci set "honk.main.clash_api_secret=$new_clash_secret" && uci commit honk && chmod 0600 /etc/config/honk || {
|
||
settings_rollback; rollback_status=$?
|
||
if [ "$rollback_status" = 0 ]; then rm -rf "$settings_dir"; echo '{"ok":false,"message":"settings_commit_failed_rolled_back"}'; else logger -t honk "settings rollback failed; snapshot retained at $settings_dir"; echo '{"ok":false,"message":"settings_commit_and_rollback_failed_snapshot_retained"}'; fi
|
||
return 1
|
||
}
|
||
if ! sync_boot_link "$new_boot_enabled"; then
|
||
settings_rollback; rollback_status=$?
|
||
if [ "$rollback_status" = 0 ]; then rm -rf "$settings_dir"; echo '{"ok":false,"message":"boot_link_update_failed_rolled_back"}'; else logger -t honk "settings rollback failed; snapshot retained at $settings_dir"; echo '{"ok":false,"message":"boot_link_update_and_rollback_failed_snapshot_retained"}'; fi
|
||
return 1
|
||
fi
|
||
if [ "$initialized" = 1 ]; then
|
||
service_ok=0
|
||
if [ "$new_enabled" = 1 ]; then
|
||
honk_lifecycle_start 30 || service_ok=1
|
||
else
|
||
honk_lifecycle_stop 30 || service_ok=1
|
||
fi
|
||
if [ "$service_ok" != 0 ]; then
|
||
settings_rollback; rollback_status=$?
|
||
if [ "$rollback_status" = 0 ]; then rm -rf "$settings_dir"; echo '{"ok":false,"message":"service_apply_failed_rolled_back"}'; else logger -t honk "settings rollback failed; snapshot retained at $settings_dir"; echo '{"ok":false,"message":"service_apply_and_rollback_failed_snapshot_retained"}'; fi
|
||
return 1
|
||
fi
|
||
fi
|
||
system_written_hash=""
|
||
rm -rf "$settings_dir"
|
||
echo '{"ok":true,"message":"settings_saved"}'
|
||
}
|
||
|
||
# 把 global/experimental 摊平成「路径=值」,取值和比对共用。
|
||
system_entries() {
|
||
# 逐字符扫描:大括号可以跟值写在同一行,按整行判断会把归属解析错。
|
||
awk '
|
||
function flush(stmt, key, val, p, i, fields) {
|
||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", stmt)
|
||
if (stmt == "" || d == 0) return
|
||
if (path[1] != "global" && path[1] != "experimental") return
|
||
if (stmt !~ /^[A-Za-z_][A-Za-z0-9_]*[[:space:]]*:/) return
|
||
# 一行写多个字段无法可靠拆分,交给调用方拒绝,不能只取第一个就当成全部。
|
||
fields = stmt
|
||
gsub(/"[^"]*"/, "", fields)
|
||
gsub(/\047[^\047]*\047/, "", fields)
|
||
if (gsub(/[A-Za-z_][A-Za-z0-9_]*[[:space:]]*:/, ":", fields) > 1) {
|
||
print "!unsupported"
|
||
return
|
||
}
|
||
key = stmt
|
||
sub(/[[:space:]]*:.*/, "", key)
|
||
val = stmt
|
||
sub(/^[^:]*:[[:space:]]*/, "", val)
|
||
if (val ~ /^\047/) { val = substr(val, 2); sub(/\047.*/, "", val) }
|
||
else if (val ~ /^"/) { val = substr(val, 2); sub(/".*/, "", val) }
|
||
p = path[1]
|
||
for (i = 2; i <= d; i++) p = p "." path[i]
|
||
print p "." key "=" val
|
||
}
|
||
{
|
||
line = $0
|
||
sub(/\r$/, "", line)
|
||
n = length(line)
|
||
for (i = 1; i <= n; i++) {
|
||
c = substr(line, i, 1)
|
||
if (quote != "") {
|
||
buf = buf c
|
||
if (c == quote) quote = ""
|
||
continue
|
||
}
|
||
if (c == "\047" || c == "\"") { quote = c; buf = buf c; continue }
|
||
if (c == "#") break
|
||
if (c == "{") {
|
||
name = buf
|
||
sub(/[[:space:]]*:.*/, "", name)
|
||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", name)
|
||
path[++d] = name
|
||
buf = ""
|
||
continue
|
||
}
|
||
if (c == "}") {
|
||
flush(buf)
|
||
buf = ""
|
||
if (d > 0) d--
|
||
continue
|
||
}
|
||
buf = buf c
|
||
}
|
||
flush(buf)
|
||
buf = ""
|
||
}
|
||
' "$1"
|
||
}
|
||
|
||
entry_value() {
|
||
awk -v key="$1" 'index($0, key "=") == 1 { print substr($0, length(key) + 2); exit }' "$2"
|
||
}
|
||
|
||
# 只删顶级 global/experimental,缺 include 时在第一块的位置补上,其余内容原样保留。
|
||
strip_system_blocks() {
|
||
# 第一遍只摸底:已有 include 就不重复插入,重复 include 会被核心判成循环引用。
|
||
awk '
|
||
NR == FNR { if ($0 ~ /^[[:space:]]*include[[:space:]]*\{/) added = 1; next }
|
||
skip {
|
||
n = length($0)
|
||
for (i = 1; i <= n; i++) {
|
||
c = substr($0, i, 1)
|
||
if (q != "") { if (c == q) q = ""; continue }
|
||
if (c == "\047" || c == "\"") { q = c; continue }
|
||
if (c == "#") break
|
||
if (c == "{") depth++
|
||
else if (c == "}") depth--
|
||
}
|
||
if (depth <= 0) skip = 0
|
||
next
|
||
}
|
||
/^(global|experimental)[[:space:]]*\{/ {
|
||
if (!added) { print "include {\n system.dae\n}"; added = 1 }
|
||
depth = gsub(/\{/, "{")
|
||
depth -= gsub(/\}/, "}")
|
||
skip = depth > 0
|
||
next
|
||
}
|
||
{ print }
|
||
' "$1" "$1"
|
||
}
|
||
|
||
device_network() {
|
||
. /lib/functions/network.sh
|
||
network_get_device dn_dev "$lan_network" && [ "$dn_dev" = "$1" ] && { echo "$lan_network"; return 0; }
|
||
for dn_iface in $(ubus list 'network.interface.*' 2>/dev/null | sed 's/^network\.interface\.//'); do
|
||
network_get_device dn_dev "$dn_iface" || continue
|
||
[ "$dn_dev" = "$1" ] && { echo "$dn_iface"; return 0; }
|
||
done
|
||
return 1
|
||
}
|
||
|
||
valid_port() {
|
||
case "$1" in ''|*[!0-9]*) return 1 ;; esac
|
||
[ "$1" -ge 1024 ] && [ "$1" -le 65535 ]
|
||
}
|
||
|
||
detach_fail() {
|
||
echo "{\"ok\":false,\"message\":\"$1\"}"
|
||
return 1
|
||
}
|
||
|
||
detach_rollback() {
|
||
dr_ok=1
|
||
honk_lifecycle_stop 30 >/dev/null 2>&1 || dr_ok=0
|
||
cp -p "$detach_dir/old-system.dae" /etc/honk/.system.dae.rollback && mv -f /etc/honk/.system.dae.rollback /etc/honk/system.dae || dr_ok=0
|
||
cp -p "$detach_dir/old-config.dae" /etc/honk/.config.dae.rollback && mv -f /etc/honk/.config.dae.rollback /etc/honk/config.dae || dr_ok=0
|
||
{ uci import honk < "$detach_dir/old.uci" && uci commit honk && chmod 0600 /etc/config/honk; } || dr_ok=0
|
||
if [ "$dr_ok" = 1 ] && [ "$old_running" = 1 ]; then honk_lifecycle_start 30 || dr_ok=0; fi
|
||
[ "$dr_ok" = 1 ]
|
||
}
|
||
|
||
# 把文件里的 global/experimental 交还 system.dae 管理;dry_run 只返回会变动的项。
|
||
detach() {
|
||
if [ -e /etc/.honk-update/journal ] || [ -L /etc/.honk-update/journal ]; then detach_fail update_recovery_required; return 1; fi
|
||
. /usr/share/libubox/jshn.sh || { detach_fail jshn_unavailable; return 1; }
|
||
detach_request=$(head -c 4096)
|
||
dry_run=
|
||
[ -z "$detach_request" ] || json_load "$detach_request" 2>/dev/null || { detach_fail invalid_request; return 1; }
|
||
[ -z "$detach_request" ] || json_get_var dry_run dry_run
|
||
case "$dry_run" in 1|true) dry_run=1 ;; *) dry_run=0 ;; esac
|
||
load_settings
|
||
[ "$initialized" = 1 ] || { detach_fail not_initialized; return 1; }
|
||
# 混合写法(include + 自带 global/experimental)也给一键切换;strip 后仍不规范会在下面被拒绝。
|
||
case "$settings_owner" in file|unknown) ;; *) detach_fail config_not_file_owned; return 1 ;; esac
|
||
[ -f /etc/honk/system.dae ] && [ ! -L /etc/honk/system.dae ] || { detach_fail system_config_unavailable; return 1; }
|
||
umask 077
|
||
detach_dir=$(mktemp -d /etc/honk/.detach.XXXXXX) || { detach_fail config_write_failed; return 1; }
|
||
trap 'rm -rf "$detach_dir"' 0
|
||
old_hash=$(sha256sum /etc/honk/config.dae | awk '{print $1}')
|
||
system_entries /etc/honk/config.dae > "$detach_dir/old.entries"
|
||
grep -q '^!unsupported$' "$detach_dir/old.entries" && { detach_fail detach_layout_unsupported; return 1; }
|
||
|
||
# 能对应到面板的值照搬,对不上的沿用面板现值。
|
||
file_device=$(entry_value global.lan_interface "$detach_dir/old.entries")
|
||
if [ -n "$file_device" ] && detach_network=$(device_network "$file_device"); then lan_network=$detach_network; fi
|
||
file_port=$(entry_value experimental.native_api.listen "$detach_dir/old.entries")
|
||
file_port=${file_port##*:}
|
||
valid_port "$file_port" && listen_port=$file_port
|
||
old_clash_port=$clash_api_port
|
||
old_clash_secret=$clash_api_secret
|
||
clash_api_enabled=0
|
||
if grep -q '^experimental\.clash_api\.' "$detach_dir/old.entries"; then
|
||
clash_api_enabled=1
|
||
file_clash=$(entry_value experimental.clash_api.external_controller "$detach_dir/old.entries")
|
||
clash_api_port=${file_clash##*:}
|
||
clash_api_secret=$(entry_value experimental.clash_api.secret "$detach_dir/old.entries")
|
||
valid_clash_api || { clash_api_enabled=0; clash_api_port=$old_clash_port; clash_api_secret=$old_clash_secret; }
|
||
fi
|
||
check_network && network_settings || { detach_fail lan_network_unavailable; return 1; }
|
||
|
||
render_runtime_system "$lan_device" "$lan_ip" "$listen_port" > "$detach_dir/system.dae" || { detach_fail config_write_failed; return 1; }
|
||
strip_system_blocks /etc/honk/config.dae > "$detach_dir/config.dae" || { detach_fail config_write_failed; return 1; }
|
||
[ "$(config_owner "$detach_dir/config.dae")" = system ] || { detach_fail detach_layout_unsupported; return 1; }
|
||
detach_check=$("$HONK_CORE" config validate --config "$detach_dir/config.dae" 2>/dev/null)
|
||
[ "$(printf '%s' "$detach_check" | jsonfilter -e '@.ok' 2>/dev/null)" = true ] || { detach_fail detach_validation_failed; return 1; }
|
||
|
||
if [ "$dry_run" = 1 ]; then
|
||
system_entries "$detach_dir/system.dae" > "$detach_dir/new.entries"
|
||
json_init
|
||
json_add_boolean ok 1
|
||
json_add_string message detach_preview
|
||
json_add_string lan_network "$lan_network"
|
||
json_add_int listen_port "$listen_port"
|
||
json_add_boolean clash_api_enabled "$clash_api_enabled"
|
||
json_add_int clash_api_port "$clash_api_port"
|
||
json_add_array changes
|
||
awk '
|
||
{ i = index($0, "="); k = substr($0, 1, i - 1); v = substr($0, i + 1) }
|
||
NR == FNR { seen[k] = 1; val[k] = v; next }
|
||
k ~ /\.secret$/ { if (!seen[k]) print k "\t***\t"; else if (val[k] != v) print k "\t***\t***"; next }
|
||
!seen[k] { print k "\t" v "\t"; next }
|
||
val[k] != v { print k "\t" v "\t" val[k] }
|
||
' "$detach_dir/new.entries" "$detach_dir/old.entries" > "$detach_dir/changes"
|
||
while IFS=' ' read -r change_key change_old change_new; do
|
||
json_add_object ""
|
||
json_add_string key "$change_key"
|
||
json_add_string old "$change_old"
|
||
json_add_string new "$change_new"
|
||
json_close_object
|
||
done < "$detach_dir/changes"
|
||
json_close_array
|
||
json_dump
|
||
return 0
|
||
fi
|
||
|
||
honk_lock_acquire || { detach_fail operation_in_progress; return 1; }
|
||
trap 'honk_lock_release >/dev/null 2>&1; rm -rf "$detach_dir"' 0
|
||
[ "$(sha256sum /etc/honk/config.dae | awk '{print $1}')" = "$old_hash" ] || { detach_fail config_changed_concurrently; return 1; }
|
||
old_running=0
|
||
if managed_core_pid >/dev/null 2>&1; then
|
||
old_running=1
|
||
elif pidof honk-core >/dev/null 2>&1; then
|
||
detach_fail unmanaged_core_running
|
||
return 1
|
||
fi
|
||
cp -p /etc/honk/config.dae "$detach_dir/old-config.dae" && cp -p /etc/honk/system.dae "$detach_dir/old-system.dae" && uci export honk > "$detach_dir/old.uci" || { detach_fail config_write_failed; return 1; }
|
||
cp -p /etc/honk/config.dae /etc/honk/.config-before-detach.tmp && mv -f /etc/honk/.config-before-detach.tmp /etc/honk/config-before-detach.dae || { detach_fail config_write_failed; return 1; }
|
||
if [ "$old_running" = 1 ]; then honk_lifecycle_stop 30 || { detach_fail service_stop_unconfirmed; return 1; }; fi
|
||
chmod 0600 "$detach_dir/system.dae" "$detach_dir/config.dae"
|
||
if ! { cp -p "$detach_dir/system.dae" /etc/honk/.system.dae.detach && mv -f /etc/honk/.system.dae.detach /etc/honk/system.dae && \
|
||
cp -p "$detach_dir/config.dae" /etc/honk/.config.dae.detach && mv -f /etc/honk/.config.dae.detach /etc/honk/config.dae && \
|
||
uci set "honk.main.lan_network=$lan_network" && uci set "honk.main.listen_port=$listen_port" && \
|
||
uci set "honk.main.clash_api_enabled=$clash_api_enabled" && uci set "honk.main.clash_api_port=$clash_api_port" && \
|
||
uci set "honk.main.clash_api_secret=$clash_api_secret" && uci commit honk && chmod 0600 /etc/config/honk; } || \
|
||
{ [ "$old_running" = 1 ] && ! honk_lifecycle_start 30; }; then
|
||
if detach_rollback; then detach_fail detach_failed_rolled_back; else logger -t honk "detach rollback failed; snapshot kept at $detach_dir"; trap 'honk_lock_release >/dev/null 2>&1' 0; detach_fail detach_rollback_failed; fi
|
||
return 1
|
||
fi
|
||
# 草稿原本就是旧配置时跟着换,免得配置页一直提示「有未应用的草稿」。
|
||
if [ -f /etc/honk/config-draft.dae ] && [ "$(sha256sum /etc/honk/config-draft.dae | awk '{print $1}')" = "$old_hash" ]; then
|
||
cp -p /etc/honk/config.dae /etc/honk/.config-draft.detach && mv -f /etc/honk/.config-draft.detach /etc/honk/config-draft.dae
|
||
fi
|
||
echo '{"ok":true,"message":"config_detached"}'
|
||
}
|
||
|
||
diagnose() {
|
||
load_settings
|
||
errors=""
|
||
managed_core_pid >/dev/null 2>&1 && running=1 || running=0
|
||
check_config >/dev/null 2>&1 || errors="$errors invalid_package_config"
|
||
# 内核检查自报具体缺哪项,面板才能给出可操作的提示
|
||
check_kernel >/dev/null 2>&1 || errors="$errors ${kernel_error:-kernel_requirements_unmet}"
|
||
if [ "$settings_owner" != system ]; then
|
||
if [ "$running" != 1 ]; then
|
||
no_honk_resources >/dev/null 2>&1 || errors="$errors honk_resources_occupied"
|
||
pidof honk-core >/dev/null 2>&1 && errors="$errors unmanaged_core_running"
|
||
fi
|
||
else
|
||
check_network >/dev/null 2>&1 || errors="$errors lan_network_unavailable"
|
||
network_settings >/dev/null 2>&1 || errors="$errors lan_ipv4_unavailable"
|
||
command -v curl >/dev/null 2>&1 || errors="$errors curl_missing"
|
||
if [ "$running" != 1 ]; then
|
||
no_honk_resources >/dev/null 2>&1 || errors="$errors honk_resources_occupied"
|
||
pidof honk-core >/dev/null 2>&1 && errors="$errors unmanaged_core_running"
|
||
check_port >/dev/null 2>&1 || errors="$errors listen_port_occupied"
|
||
if [ "$clash_api_enabled" = 1 ]; then
|
||
check_port "$clash_api_port" >/dev/null 2>&1 || errors="$errors clash_api_port_occupied"
|
||
fi
|
||
fi
|
||
fi
|
||
check_conflicts >/dev/null 2>&1 || errors="$errors proxy_conflict"
|
||
if [ -d "$HONK_LOCK_DIR" ]; then
|
||
if lock_owner_invalid || honk_lock_is_stale; then
|
||
errors="$errors stale_operation_lock maintenance_recovery_required"
|
||
else
|
||
errors="$errors operation_in_progress"
|
||
fi
|
||
fi
|
||
{ [ -e "$HONK_MAINT_JOURNAL" ] || [ -L "$HONK_MAINT_JOURNAL" ]; } && errors="$errors maintenance_recovery_required"
|
||
if [ -n "$errors" ]; then
|
||
printf '%s\n' "$errors" | sed 's/^ *//'
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
main_has_system_include() {
|
||
awk '
|
||
/^[[:space:]]*include[[:space:]]*\{/ { inside = 1 }
|
||
inside && /(^|[^[:alnum:]_.-])system\.dae([^[:alnum:]_.-]|$)/ { found = 1 }
|
||
inside && /\}/ { inside = 0 }
|
||
END { exit !found }
|
||
' /etc/honk/config.dae
|
||
}
|
||
|
||
clash_system_matches() {
|
||
if [ "$clash_api_enabled" = 0 ]; then
|
||
! grep -q 'clash_api {' /etc/honk/system.dae
|
||
return
|
||
fi
|
||
permissions=$(ls -ldn /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
|
||
[ "$permissions" = '-rw-------' ] || return 1
|
||
grep -Fqx " external_controller: '$system_host:$clash_api_port'" /etc/honk/system.dae || return 1
|
||
grep -Fqx " external_ui: '/usr/share/honk/zashboard'" /etc/honk/system.dae || return 1
|
||
system_secret=$(sed -n "s/^ secret: '\([^']*\)'$/\1/p" /etc/honk/system.dae)
|
||
[ "$system_secret" = "$clash_api_secret" ]
|
||
}
|
||
|
||
repair() {
|
||
if [ -e /etc/.honk-update/journal ] || [ -L /etc/.honk-update/journal ]; then echo '{"ok":false,"message":"update_recovery_required"}'; return 1; fi
|
||
load_settings
|
||
if [ "$settings_owner" != system ]; then
|
||
check_config && { echo '{"ok":true,"message":"config_valid"}'; return 0; }
|
||
echo '{"ok":false,"message":"config_invalid"}'
|
||
return 1
|
||
fi
|
||
old_network=$lan_network
|
||
old_port=$listen_port
|
||
old_enabled=$enabled
|
||
old_boot_enabled=$boot_enabled
|
||
old_initialized=$initialized
|
||
old_clash_enabled=$clash_api_enabled
|
||
old_clash_port=$clash_api_port
|
||
old_clash_secret=$clash_api_secret
|
||
[ "$config_file" = /etc/honk/config.dae ] || { echo '{"ok":false,"message":"invalid_config_path"}'; return 1; }
|
||
[ -f /etc/honk/config.dae ] && [ ! -L /etc/honk/config.dae ] || { echo '{"ok":false,"message":"main_config_unavailable"}'; return 1; }
|
||
main_hash=$(sha256sum /etc/honk/config.dae | awk '{print $1}')
|
||
main_has_system_include || { echo '{"ok":false,"message":"required_system_include_missing"}'; return 1; }
|
||
valid_network_name || { echo '{"ok":false,"message":"invalid_lan_network"}'; return 1; }
|
||
case "$listen_port" in ''|*[!0-9]*) echo '{"ok":false,"message":"invalid_listen_port"}'; return 1 ;; esac
|
||
[ "$listen_port" -ge 1024 ] && [ "$listen_port" -le 65535 ] || { echo '{"ok":false,"message":"invalid_listen_port"}'; return 1; }
|
||
valid_clash_api || { echo '{"ok":false,"message":"invalid_clash_api_settings"}'; return 1; }
|
||
check_network || { echo '{"ok":false,"message":"lan_network_unavailable"}'; return 1; }
|
||
network_settings || { echo '{"ok":false,"message":"lan_ipv4_unavailable"}'; return 1; }
|
||
old_running=0
|
||
managed_core_pid >/dev/null 2>&1 && old_running=1
|
||
if pidof honk-core >/dev/null 2>&1 && [ "$old_running" = 0 ]; then echo '{"ok":false,"message":"unmanaged_core_running"}'; return 1; fi
|
||
if ! { [ "$old_running" = 1 ] && [ "$(configured_system_port)" = "$listen_port" ]; }; then
|
||
check_port || { echo '{"ok":false,"message":"listen_port_occupied"}'; return 1; }
|
||
fi
|
||
if [ "$clash_api_enabled" = 1 ] && ! { [ "$old_running" = 1 ] && [ "$(configured_clash_port)" = "$clash_api_port" ]; }; then
|
||
check_port "$clash_api_port" || { echo '{"ok":false,"message":"clash_api_port_occupied"}'; return 1; }
|
||
fi
|
||
honk_lock_acquire || { echo '{"ok":false,"message":"operation_in_progress"}'; return 1; }
|
||
trap 'honk_lock_release >/dev/null 2>&1' 0
|
||
load_settings
|
||
[ "$lan_network" = "$old_network" ] && [ "$listen_port" = "$old_port" ] && [ "$enabled" = "$old_enabled" ] && [ "$boot_enabled" = "$old_boot_enabled" ] && [ "$initialized" = "$old_initialized" ] && [ "$clash_api_enabled" = "$old_clash_enabled" ] && [ "$clash_api_port" = "$old_clash_port" ] && [ "$clash_api_secret" = "$old_clash_secret" ] || { echo '{"ok":false,"message":"settings_changed_concurrently"}'; return 1; }
|
||
managed_core_pid >/dev/null 2>&1 && recheck_running=1 || recheck_running=0
|
||
[ "$recheck_running" = "$old_running" ] || { echo '{"ok":false,"message":"service_state_changed_concurrently"}'; return 1; }
|
||
[ "$(sha256sum /etc/honk/config.dae | awk '{print $1}')" = "$main_hash" ] || { echo '{"ok":false,"message":"main_config_changed_concurrently"}'; return 1; }
|
||
check_network && network_settings || { echo '{"ok":false,"message":"lan_network_changed_during_repair"}'; return 1; }
|
||
if ! { [ "$old_running" = 1 ] && [ "$(configured_system_port)" = "$listen_port" ]; }; then
|
||
check_port || { echo '{"ok":false,"message":"listen_port_occupied"}'; return 1; }
|
||
fi
|
||
if [ "$clash_api_enabled" = 1 ] && ! { [ "$old_running" = 1 ] && [ "$(configured_clash_port)" = "$clash_api_port" ]; }; then
|
||
check_port "$clash_api_port" || { echo '{"ok":false,"message":"clash_api_port_occupied"}'; return 1; }
|
||
fi
|
||
if [ "$initialized" = 1 ]; then system_host=$lan_ip; else system_host=127.0.0.1; fi
|
||
if [ -r /usr/share/doona/index.html ] && [ -f /etc/honk/system.dae ] && [ ! -L /etc/honk/system.dae ] && system_config_readonly && \
|
||
grep -Fqx ' wan_interface: auto' /etc/honk/system.dae && grep -Fqx " lan_interface: '$lan_device'" /etc/honk/system.dae && \
|
||
grep -Fqx " data_dir: '/etc/honk'" /etc/honk/system.dae && grep -Fqx ' nfqueue_enable: true' /etc/honk/system.dae && \
|
||
grep -Fqx " listen: '$system_host:$listen_port'" /etc/honk/system.dae && \
|
||
grep -Fqx " ui: '/usr/share/doona'" /etc/honk/system.dae && grep -Fqx ' password_auth: true' /etc/honk/system.dae && \
|
||
grep -Fqx ' allow_anonymous_loopback: false' /etc/honk/system.dae && grep -Fqx ' config_write: true' /etc/honk/system.dae && clash_system_matches; then
|
||
echo '{"ok":true,"message":"system_config_healthy"}'
|
||
return 0
|
||
fi
|
||
if [ -e /etc/honk/system.dae ] && { [ ! -f /etc/honk/system.dae ] || [ -L /etc/honk/system.dae ]; }; then
|
||
echo '{"ok":false,"message":"system_config_not_regular_file"}'
|
||
return 1
|
||
fi
|
||
[ -r /usr/share/doona/index.html ] || { echo '{"ok":false,"message":"ui_assets_missing"}'; return 1; }
|
||
umask 077
|
||
repair_dir=$(mktemp -d /tmp/honk-repair.XXXXXX) || { echo '{"ok":false,"message":"temporary_storage_unavailable"}'; return 1; }
|
||
old_system=$repair_dir/system.dae
|
||
original_system_hash=""
|
||
if [ -f /etc/honk/system.dae ]; then
|
||
cp -p /etc/honk/system.dae "$old_system" || { rm -rf "$repair_dir"; echo '{"ok":false,"message":"system_snapshot_failed"}'; return 1; }
|
||
original_system_hash=$(sha256sum "$old_system" | awk '{print $1}')
|
||
fi
|
||
system_written_hash=$original_system_hash
|
||
if [ "$old_running" = 1 ]; then
|
||
if ! honk_lifecycle_stop 30; then
|
||
logger -t honk "repair stop/drain unconfirmed; no restart attempted; snapshot retained at $repair_dir"
|
||
echo '{"ok":false,"message":"service_stop_unconfirmed_snapshot_retained"}'
|
||
return 1
|
||
fi
|
||
fi
|
||
if [ "$initialized" = 1 ]; then system_listen=$lan_ip; else system_listen=127.0.0.1; fi
|
||
if ! write_runtime_system "$lan_device" "$lan_ip" "$listen_port" "$system_listen"; then
|
||
repair_rollback_ok=1
|
||
[ -n "$system_written_hash" ] && ! restore_system && repair_rollback_ok=0
|
||
restore_old_runtime || repair_rollback_ok=0
|
||
if [ "$repair_rollback_ok" = 1 ]; then
|
||
rm -rf "$repair_dir"
|
||
echo '{"ok":false,"message":"system_config_repair_failed_rolled_back"}'
|
||
else
|
||
logger -t honk "repair rollback failed; snapshot retained at $repair_dir"
|
||
echo '{"ok":false,"message":"system_config_repair_rollback_failed_snapshot_retained"}'
|
||
fi
|
||
return 1
|
||
fi
|
||
if [ "$initialized" = 1 ] && { [ "$old_running" = 1 ] || [ "$enabled" = 1 ]; }; then
|
||
if ! honk_lifecycle_start 30; then
|
||
repair_rollback_ok=1
|
||
honk_lifecycle_stop 30 || repair_rollback_ok=0
|
||
if [ "$repair_rollback_ok" = 1 ]; then restore_system || repair_rollback_ok=0; fi
|
||
restore_old_runtime || repair_rollback_ok=0
|
||
if [ "$repair_rollback_ok" = 1 ]; then
|
||
rm -rf "$repair_dir"
|
||
echo '{"ok":false,"message":"repair_restart_failed_rolled_back"}'
|
||
else
|
||
logger -t honk "repair restart rollback failed; snapshot retained at $repair_dir"
|
||
echo '{"ok":false,"message":"repair_restart_and_rollback_failed_snapshot_retained"}'
|
||
fi
|
||
return 1
|
||
fi
|
||
fi
|
||
system_written_hash=""
|
||
rm -rf "$repair_dir"
|
||
echo '{"ok":true,"message":"system_config_repaired"}'
|
||
}
|
||
|
||
api_discovery() {
|
||
curl -fsS --noproxy '*' --connect-timeout 1 --max-time 3 "$1/api" -o "$2"
|
||
}
|
||
|
||
mock_start() {
|
||
mock_config=$1
|
||
mock_log=$2
|
||
DAE_LOCATION_ASSET=/usr/share/v2ray "$HONK_CORE" --config "$mock_config" --mock-ebpf > "$mock_log" 2>&1 &
|
||
mock_pid=$!
|
||
mock_identity=""
|
||
attempt=0
|
||
while [ "$attempt" -lt 20 ]; do
|
||
if ! kill -0 "$mock_pid" 2>/dev/null; then
|
||
return 1
|
||
fi
|
||
if api_discovery "$3" "$4" 2>/dev/null; then
|
||
mock_identity=$(tr '\000' ' ' < "/proc/$mock_pid/cmdline" 2>/dev/null)
|
||
case "$mock_identity" in
|
||
*"$mock_config"*--mock-ebpf*)
|
||
[ "$(readlink -f "/proc/$mock_pid/exe" 2>/dev/null)" = "$HONK_CORE" ] && return 0
|
||
;;
|
||
esac
|
||
return 1
|
||
fi
|
||
sleep 1
|
||
attempt=$((attempt + 1))
|
||
done
|
||
return 1
|
||
}
|
||
|
||
mock_stop() {
|
||
[ -n "$mock_pid" ] || return 0
|
||
[ -r "/proc/$mock_pid/cmdline" ] || { mock_pid=""; return 0; }
|
||
current=$(tr '\000' ' ' < "/proc/$mock_pid/cmdline" 2>/dev/null)
|
||
case "$current" in
|
||
*"$mock_config"*--mock-ebpf*)
|
||
[ "$(readlink -f "/proc/$mock_pid/exe" 2>/dev/null)" = "$HONK_CORE" ] || return 1
|
||
kill -TERM "$mock_pid" 2>/dev/null || return 1
|
||
;;
|
||
*) return 1 ;;
|
||
esac
|
||
attempt=0
|
||
while kill -0 "$mock_pid" 2>/dev/null && [ "$attempt" -lt 10 ]; do sleep 1; attempt=$((attempt + 1)); done
|
||
if kill -0 "$mock_pid" 2>/dev/null; then return 1; fi
|
||
wait "$mock_pid" 2>/dev/null || true
|
||
mock_pid=""
|
||
}
|
||
|
||
post_credentials() {
|
||
endpoint=$1
|
||
response_file=$2
|
||
json_init
|
||
json_add_string username "$username"
|
||
json_add_string password "$password"
|
||
json_dump | curl -sS --noproxy '*' --connect-timeout 1 --max-time 5 -o "$response_file" -w '%{http_code}' \
|
||
-H 'Content-Type: application/json' --data-binary @- "$endpoint"
|
||
}
|
||
|
||
initialize_preflight() {
|
||
load_settings
|
||
[ "$enabled" = 0 ] && [ "$initialized" = 0 ] || { echo '{"ok":false,"message":"disable_service_before_initialization"}'; return 1; }
|
||
[ "$config_file" = /etc/honk/config.dae ] || { echo '{"ok":false,"message":"invalid_config_path"}'; return 1; }
|
||
[ -r /usr/share/doona/index.html ] || { echo '{"ok":false,"message":"ui_assets_missing"}'; return 1; }
|
||
if [ "$1" = read_credentials ]; then
|
||
read_credentials || { echo '{"ok":false,"message":"invalid_credentials"}'; return 1; }
|
||
fi
|
||
[ -x "$HONK_CORE" ] || { echo '{"ok":false,"message":"core_missing"}'; return 1; }
|
||
command -v curl >/dev/null 2>&1 || { echo '{"ok":false,"message":"curl_missing"}'; return 1; }
|
||
pidof honk-core >/dev/null 2>&1 && { echo '{"ok":false,"message":"core_already_running"}'; return 1; }
|
||
no_honk_resources || { echo '{"ok":false,"message":"honk_resources_already_exist"}'; return 1; }
|
||
check_port || { echo '{"ok":false,"message":"listen_port_occupied"}'; return 1; }
|
||
check_network || { echo '{"ok":false,"message":"lan_network_unavailable"}'; return 1; }
|
||
network_settings || { echo '{"ok":false,"message":"lan_ipv4_unavailable"}'; return 1; }
|
||
check_config || { echo '{"ok":false,"message":"invalid_package_config"}'; return 1; }
|
||
return 0
|
||
}
|
||
|
||
initialize() {
|
||
if [ -e /etc/.honk-update/journal ] || [ -L /etc/.honk-update/journal ]; then echo '{"ok":false,"message":"update_recovery_required"}'; return 1; fi
|
||
. /usr/share/libubox/jshn.sh || return 1
|
||
initialize_preflight read_credentials || return 1
|
||
honk_lock_acquire || { echo '{"ok":false,"message":"operation_in_progress"}'; return 1; }
|
||
trap 'honk_lock_release >/dev/null 2>&1' 0
|
||
initialize_preflight || return 1
|
||
|
||
umask 077
|
||
temp_dir=$(mktemp -d /tmp/honk-init.XXXXXX) || { echo '{"ok":false,"message":"temporary_storage_unavailable"}'; return 1; }
|
||
mock_pid=""
|
||
trap cleanup_initialize 0
|
||
trap 'exit 1' HUP INT TERM
|
||
old_system=$temp_dir/system.dae
|
||
cp -p /etc/honk/system.dae "$old_system" || { echo '{"ok":false,"message":"system_config_backup_failed"}'; return 1; }
|
||
original_system_hash=$(sha256sum "$old_system" | awk '{print $1}')
|
||
system_written_hash=$original_system_hash
|
||
ensure_loopback_system || { echo '{"ok":false,"message":"loopback_listener_write_failed"}'; return 1; }
|
||
mock_config=$temp_dir/mock.dae
|
||
mock_log=$temp_dir/core.log
|
||
response_file=$temp_dir/response.json
|
||
cat > "$mock_config" <<EOF
|
||
global {
|
||
wan_interface: auto
|
||
lan_interface: ''
|
||
data_dir: '/etc/honk'
|
||
nfqueue_enable: false
|
||
store_subscribe: false
|
||
}
|
||
routing {
|
||
fallback: direct
|
||
}
|
||
experimental {
|
||
native_api {
|
||
enabled: true
|
||
listen: '127.0.0.1:$listen_port'
|
||
password_auth: true
|
||
allow_anonymous_loopback: false
|
||
config_write: true
|
||
}
|
||
}
|
||
EOF
|
||
chmod 0600 "$mock_config" "$mock_log" 2>/dev/null || true
|
||
loopback_url="http://127.0.0.1:$listen_port"
|
||
if ! mock_start "$mock_config" "$mock_log" "$loopback_url" "$response_file"; then
|
||
echo '{"ok":false,"message":"mock_core_start_failed"}'
|
||
return 1
|
||
fi
|
||
setup_required=$(jsonfilter -i "$response_file" -e '@.auth.setup_required' 2>/dev/null)
|
||
case "$setup_required" in
|
||
true)
|
||
status=$(post_credentials "$loopback_url/api/v1/auth/setup" "$response_file")
|
||
[ "$status" = 201 ] || { echo '{"ok":false,"message":"administrator_setup_failed"}'; return 1; }
|
||
;;
|
||
false)
|
||
status=$(post_credentials "$loopback_url/api/v1/auth/login" "$response_file")
|
||
[ "$status" = 200 ] || { echo '{"ok":false,"message":"administrator_exists_reuse_credentials_or_reset_state"}'; return 1; }
|
||
;;
|
||
*) echo '{"ok":false,"message":"native_api_setup_state_unavailable"}'; return 1 ;;
|
||
esac
|
||
api_discovery "$loopback_url" "$response_file" || { echo '{"ok":false,"message":"setup_verification_failed"}'; return 1; }
|
||
setup_required=$(jsonfilter -i "$response_file" -e '@.auth.setup_required' 2>/dev/null)
|
||
[ "$setup_required" = false ] || { echo '{"ok":false,"message":"setup_verification_failed"}'; return 1; }
|
||
mock_stop || { echo '{"ok":false,"message":"mock_core_stop_failed"}'; return 1; }
|
||
if ! mock_start "$mock_config" "$mock_log" "$loopback_url" "$response_file"; then
|
||
echo '{"ok":false,"message":"mock_core_restart_failed"}'
|
||
return 1
|
||
fi
|
||
api_discovery "$loopback_url" "$response_file" || { echo '{"ok":false,"message":"persistence_verification_failed"}'; return 1; }
|
||
setup_required=$(jsonfilter -i "$response_file" -e '@.auth.setup_required' 2>/dev/null)
|
||
[ "$setup_required" = false ] || { echo '{"ok":false,"message":"persistence_verification_failed"}'; return 1; }
|
||
status=$(post_credentials "$loopback_url/api/v1/auth/login" "$response_file")
|
||
[ "$status" = 200 ] || { echo '{"ok":false,"message":"persistence_login_failed"}'; return 1; }
|
||
mock_stop || { echo '{"ok":false,"message":"mock_core_stop_failed"}'; return 1; }
|
||
|
||
write_runtime_system "$lan_device" "$lan_ip" || { restore_system; echo '{"ok":false,"message":"system_config_write_failed"}'; return 1; }
|
||
lan_url="http://$lan_ip:$listen_port"
|
||
if ! mock_start /etc/honk/config.dae "$mock_log" "$lan_url" "$response_file"; then
|
||
restore_system
|
||
echo '{"ok":false,"message":"generated_config_invalid_restored_loopback"}'
|
||
return 1
|
||
fi
|
||
api_discovery "$lan_url" "$response_file" || {
|
||
mock_stop >/dev/null 2>&1
|
||
restore_system
|
||
echo '{"ok":false,"message":"lan_api_verification_failed_restored_loopback"}'
|
||
return 1
|
||
}
|
||
mock_stop || {
|
||
restore_system
|
||
echo '{"ok":false,"message":"mock_core_stop_failed_restored_loopback"}'
|
||
return 1
|
||
}
|
||
uci set honk.main.initialized='1' && uci commit honk || {
|
||
restore_system
|
||
uci set honk.main.initialized='0' >/dev/null 2>&1
|
||
uci commit honk >/dev/null 2>&1
|
||
echo '{"ok":false,"message":"uci_commit_failed_restored_loopback"}'
|
||
return 1
|
||
}
|
||
system_written_hash=""
|
||
password=""
|
||
username=""
|
||
echo '{"ok":true,"message":"administrator_initialized"}'
|
||
}
|
||
|
||
# 收尾被中断的维护事务:死锁优先清,再交给 maintenance.sh 回滚日志。
|
||
# 只在确认没有 honk-core 在跑时动手,避免清掉别人正在用的锁。
|
||
recover() {
|
||
if [ -e /etc/.honk-update/journal ] || [ -L /etc/.honk-update/journal ]; then
|
||
echo '{"ok":false,"message":"update_recovery_required"}'
|
||
return 1
|
||
fi
|
||
if [ -d "$HONK_LOCK_DIR" ]; then
|
||
lock_readable=0
|
||
[ -r "$HONK_LOCK_DIR/pid" ] && lock_readable=1
|
||
if [ "$lock_readable" = 1 ] && ! honk_lock_is_stale; then
|
||
echo '{"ok":false,"message":"operation_in_progress"}'
|
||
return 1
|
||
fi
|
||
if pidof honk-core >/dev/null 2>&1; then
|
||
echo '{"ok":false,"message":"unmanaged_core_running"}'
|
||
return 1
|
||
fi
|
||
rm -rf "$HONK_LOCK_DIR" || { echo '{"ok":false,"message":"lock_clear_failed"}'; return 1; }
|
||
fi
|
||
if [ -d /etc/.honk-maintenance ]; then
|
||
if ! maint_out=$(/usr/share/honk/maintenance.sh recover 2>/dev/null); then
|
||
echo "${maint_out:-{\"ok\":false,\"message\":\"recovery_failed\"}}"
|
||
return 1
|
||
fi
|
||
fi
|
||
honk_lifecycle_reclaim_resources || :
|
||
echo '{"ok":true,"message":"transaction_recovered"}'
|
||
}
|
||
|
||
case "$1" in
|
||
check)
|
||
preflight
|
||
;;
|
||
diagnose)
|
||
diagnose
|
||
;;
|
||
preflight)
|
||
prepare_runtime
|
||
;;
|
||
local-direct)
|
||
load_settings
|
||
sync_local_direct
|
||
;;
|
||
initialize)
|
||
initialize
|
||
;;
|
||
settings)
|
||
settings
|
||
;;
|
||
repair)
|
||
repair
|
||
;;
|
||
detach)
|
||
detach
|
||
;;
|
||
recover)
|
||
recover
|
||
;;
|
||
*)
|
||
echo "usage: $0 {check|diagnose|preflight|local-direct|initialize|settings|repair|detach|recover}" >&2
|
||
exit 2
|
||
;;
|
||
esac
|