#!/bin/sh

. /usr/share/libubox/jshn.sh

# Security: Define allowed operations only
# No arbitrary command execution allowed

# --- Moon helpers -------------------------------------------------------
#
# A ZeroTier moon is identified by a 40-bit address rendered as exactly 10
# hex digits, but `zerotier-cli listmoons` reports the world id zero-padded to
# 16 (e.g. 000000123456abcd). The daemon's control-plane route is
#   /moon/([0-9a-fA-F]{10})
# which matches EXACTLY 10 characters -- so passing the padded form, a
# stripped form, or the docs' 14-char form all 404. Every id crossing this
# boundary is therefore normalized to exactly 10 hex digits.
#
# `zerotier-cli orbit` is also a silent no-op (exit 0, no output, nothing
# done) when the seed argument is zero -- the C++ is
# `if ((worldId) && (seed)) { ... }` with no else branch. And
# `zerotier-cli deorbit` returns "200 deorbit OK" for a moon that does not
# exist. Exit codes are therefore never trusted: both mutating methods
# verify the actual result by diffing `listmoons`.

# Strip leading zeros (keeping >=1 char), then left-pad back to 10 digits.
# Rejects non-hex input and anything still wider than 10 digits after
# normalization, which could never match the route anyway.
moon_norm_id() {
	local v="$1"
	while [ "${#v}" -gt 1 ]; do
		case "$v" in
			0*) v="${v#0}" ;;
			*) break ;;
		esac
	done
	while [ "${#v}" -lt 10 ]; do
		v="0$v"
	done
	case "$v" in
		*[!0-9a-fA-F]*) return 1 ;;
	esac
	[ "${#v}" -eq 10 ] || return 1
	# the zero address is never a valid moon id
	[ "$v" != "0000000000" ] || return 1
	printf '%s' "$v"
}

# Persisted moon world files, one 16-digit hex name per line, from the
# configured persistent config dir. Orbit persistence in ZeroTier 1.16 IS
# the moons.d/<id>.moon file: there is no separate .orbit state object
# (ZT_STATE_OBJECT_* covers only identity/planet/moon/peer/network-config),
# and `deorbit` unlinks that file. zerotier-sync.sh already mirrors moons.d.
moon_persisted_list() {
	local d
	d=$(uci -q get zerotier.global.config_path 2>/dev/null)
	[ -n "$d" ] || d="/etc/zerotier"
	[ -d "$d/moons.d" ] || return 0
	find "$d/moons.d" -type f -name '*.moon' 2>/dev/null | while read -r f; do
		printf '%s\n' "$(basename "$f" .moon)"
	done
}

moon_is_listed() {
	# $1 = 10-digit id, $2 = listmoons output.
	# The daemon reports world ids as %.16llx, i.e. 16 hex digits zero-padded
	# on the left. Our id is always normalized to 10 digits, so padding it
	# with 6 zeros reproduces exactly the daemon's form for every id whose
	# significant length is <= 10 (16-L zeros + L digits either way).
	local id="$1" out="$2"
	case "$out" in
		*"\"000000$id\""*) return 0 ;;
	esac
	return 1
}

# Wait for an orbited moon to show up in `listmoons`.
#
# An orbit RPC only registers the seed in RAM (Topology::_moonSeeds); the moon
# becomes visible in `listmoons` -- and only becomes persistent as
# moons.d/<id>.moon -- once a root peer has delivered the signed world. That
# handshake takes a moment, so an immediate re-check would report a false
# failure for a perfectly valid orbit against a reachable moon. Poll for a
# bounded grace period instead; a genuinely unreachable moon never appears.
#
# $1 = 10-digit id, $2 = attempts, $3 = seconds between attempts.
moon_wait_listed() {
	local id="$1" tries="$2" nap="$3" i out=""
	i=0
	while [ "$i" -lt "$tries" ]; do
		out=$(zerotier-cli listmoons 2>/dev/null)
		if moon_is_listed "$id" "$out"; then
			return 0
		fi
		i=$((i + 1))
		[ "$i" -lt "$tries" ] && sleep "$nap"
	done
	return 1
}

case "$1" in
	list)
		json_init
		json_add_object "reload"
		json_close_object
		json_add_object "status"
		json_close_object
		json_add_object "get_networks_peers"
		json_close_object
		json_add_object "list_moons"
		json_close_object
		json_add_object "orbit_moon"
			json_add_string "moon_id" "string"
			json_add_string "seed" "string"
		json_close_object
		json_add_object "deorbit_moon"
			json_add_string "moon_id" "string"
		json_close_object
		json_add_object "ping_networks"
		json_close_object
		json_add_object "sync_config"
			json_add_string "local_conf_path" "path"
		json_close_object
		json_add_object "remote_list"
		json_close_object
		json_add_object "remote_host_set"
			json_add_string "section" "string"
			json_add_string "name" "string"
			json_add_string "host" "string"
			json_add_string "port" "string"
			json_add_string "user" "string"
			json_add_string "controller_port" "string"
			json_add_string "private_key" "string"
			json_add_string "mode" "string"
			json_add_string "api_key" "string"
		json_close_object
		json_add_object "remote_host_del"
			json_add_string "section" "string"
		json_close_object
		json_add_object "remote_diagnose"
			json_add_string "section" "string"
		json_close_object
		json_add_object "remote_ctl_get"
			json_add_string "section" "string"
			json_add_string "path" "string"
		json_close_object
		json_add_object "remote_member_list"
			json_add_string "section" "string"
			json_add_string "nwid" "string"
		json_close_object
		json_add_object "remote_network_list"
			json_add_string "section" "string"
		json_close_object
		json_add_object "remote_peer_list"
			json_add_string "section" "string"
		json_close_object
		json_add_object "remote_network_set"
			json_add_string "section" "string"
			json_add_string "nwid" "string"
			json_add_string "body" "string"
		json_close_object
		json_add_object "remote_network_del"
			json_add_string "section" "string"
			json_add_string "nwid" "string"
		json_close_object
		json_add_object "remote_member_set"
			json_add_string "section" "string"
			json_add_string "nwid" "string"
			json_add_string "member_id" "string"
			json_add_string "body" "string"
		json_close_object
		json_add_object "remote_moon_plan"
			json_add_string "section" "string"
		json_close_object
		json_add_object "remote_moon_apply"
			json_add_string "section" "string"
			json_add_string "confirm" "string"
		json_close_object
		json_add_object "restart_service"
		json_close_object
		json_dump
		;;
	call)
		case "$2" in
			reload)
				# Reload luci-zerotier service - SAFE: specific operation
				json_init

				# Validate that the service exists before reloading
				if [ ! -f "/etc/init.d/luci-zerotier" ]; then
					json_add_int "code" 1
					json_add_string "stdout" ""
					json_add_string "stderr" "luci-zerotier service not found"
				else
					output=$(/etc/init.d/luci-zerotier reload 2>&1)
					exit_code=$?

					json_add_int "code" "$exit_code"
					json_add_string "stdout" "$output"
					json_add_string "stderr" ""
				fi

				json_dump
				;;
			status)
				# Read-only: one liveness flag plus the node address, so the
				# settings page needs a single RPC instead of two.
				#
				# The check is pid + /proc/<pid>/comm rather than
				# `ps | grep zerotier-one`: comm confirms the pid was not
				# recycled by an unrelated process, and both cat and read are
				# builtins, so this costs zero forks. `uci get`/`uci show`
				# results were also dropped -- nothing ever rendered them.
				json_init

				_pid=$(cat /var/lib/zerotier-one/zerotier-one.pid 2>/dev/null)
				if [ -n "$_pid" ] && read -r _comm < "/proc/$_pid/comm" 2>/dev/null && [ "$_comm" = "zerotier-one" ]; then
					json_add_boolean "running" 1
				else
					json_add_boolean "running" 0
				fi

				if command -v zerotier-cli >/dev/null 2>&1; then
					_id=$(zerotier-cli -j info 2>/dev/null | jsonfilter -e '@.address' 2>/dev/null)
					json_add_string "identity" "$_id"
				else
					json_add_string "identity" ""
				fi

				json_dump
				;;
			get_networks_peers)
				# Merged on purpose: zerotier-cli is the 1.2 MB zerotier-one
				# binary, so every call loads it and opens a connection to the
				# local control port. The info page polls both data sets on the
				# same 5s timer, so one method halves the process churn.
				json_init

				if ! command -v zerotier-cli >/dev/null 2>&1; then
					json_add_int "code" 1
					json_add_string "networks" ""
					json_add_string "peers" ""
					json_add_string "stderr" "zerotier-cli not found"
					json_dump
					return
				fi

				_networks=$(zerotier-cli -j listnetworks 2>/dev/null)
				_rc_n=$?
				_peers=$(zerotier-cli -j listpeers 2>/dev/null)
				_rc_p=$?

				# Report failure if either half failed, so the UI can tell
				# "service unreachable" apart from "nothing joined".
				if [ "$_rc_n" -ne 0 ]; then
					_rc=$_rc_n
				else
					_rc=$_rc_p
				fi

				json_add_int "code" "$_rc"
				json_add_string "networks" "$_networks"
				json_add_string "peers" "$_peers"
				json_add_string "stderr" ""
				json_dump
				;;
			list_moons)
				# Read-only: the daemon's own /moon control plane via
				# `zerotier-cli listmoons`. Returns the raw JSON so the
				# frontend can normalize the 16-digit world ids and read
				# the per-moon `waiting` flag itself.
				json_init

				if ! command -v zerotier-cli >/dev/null 2>&1; then
					json_add_int "code" 1
					json_add_string "moons" ""
					json_add_string "persisted" ""
					json_add_string "stderr" "zerotier-cli not found"
					json_dump
					return
				fi

				_out=$(zerotier-cli listmoons 2>&1)
				_rc=$?
				json_add_int "code" "$_rc"
				json_add_string "moons" "$_out"
				json_add_string "persisted" "$(moon_persisted_list | tr '\n' ' ')"
				json_add_string "stderr" ""
				json_dump
				;;
			orbit_moon)
				# Orbit a moon. The seed must be the address of one of the
				# moon's roots; the node fetches the signed world from it and
				# only then persists moons.d/<id>.moon. Until that happens the
				# orbit lives purely in RAM (Topology::_moonSeeds is never
				# persisted) and does not survive a daemon restart -- the
				# daemon's `waiting:true` flag reports exactly that state and
				# the UI surfaces it.
				read -r input
				json_load "$input" 2>/dev/null
				json_get_var _mid moon_id 2>/dev/null
				json_get_var _seed seed 2>/dev/null
				unset input

				json_init

				if ! command -v zerotier-cli >/dev/null 2>&1; then
					json_add_int "code" 1
					json_add_string "moon_id" ""
					json_add_boolean "orbited" 0
					json_add_string "stderr" "zerotier-cli not found"
					json_dump
					return
				fi

				_nid=$(moon_norm_id "$_mid")
				if [ -z "$_nid" ]; then
					json_add_int "code" 1
					json_add_string "moon_id" ""
					json_add_boolean "orbited" 0
					json_add_string "stderr" "Invalid Moon ID: expected hex digits (10, or the 16-digit padded form), not the zero address"
					json_dump
					return
				fi

				_sid=$(moon_norm_id "$_seed")
				if [ -z "$_sid" ]; then
					json_add_int "code" 1
					json_add_string "moon_id" "$_nid"
					json_add_boolean "orbited" 0
					json_add_string "stderr" "Invalid Moon Seed: use the 10-digit address of one of the moon's roots (a zero seed is silently ignored by zerotier-cli)"
					json_dump
					return
				fi

				_out=$(zerotier-cli orbit "$_nid" "$_sid" 2>&1)
				_rc=$?

				if moon_wait_listed "$_nid" 6 2; then
					# Definition arrived: the moon is fully in effect and
					# moons.d/<id>.moon now exists, so it can be persisted.
					json_add_int "code" 0
					json_add_boolean "orbited" 1
					json_add_boolean "pending" 0
					json_add_string "stderr" ""
					/usr/bin/zerotier-sync.sh >/dev/null 2>&1
				elif [ "$_rc" -eq 0 ]; then
					# The orbit itself SUCCEEDED. Topology::addMoon always
					# records a non-zero seed in _moonSeeds, so the daemon has
					# accepted it; only the signed world definition is still
					# in flight (it arrives out-of-band from the root, which
					# can take a while behind a relay or an unseen NAT).
					# Reporting this as a failure would be wrong: the moon is
					# already orbited. It is just not persistent yet -- which
					# is exactly what the `waiting` state means.
					json_add_int "code" 0
					json_add_boolean "orbited" 1
					json_add_boolean "pending" 1
					json_add_string "stderr" ""
				else
					json_add_int "code" 1
					json_add_boolean "orbited" 0
					json_add_boolean "pending" 0
					json_add_string "stderr" "zerotier-cli could not reach the ZeroTier service (rc=$_rc${_out:+, out=$_out})"
				fi
				json_add_string "moon_id" "$_nid"
				json_dump
				;;
			deorbit_moon)
				# `zerotier-cli deorbit` returns "200 deorbit OK" even for a
				# moon that was never orbited (Topology::removeMoon just
				# iterates the current set). So: snapshot, deorbit, re-check,
				# and only report success when the moon actually disappeared.
				read -r input
				json_load "$input" 2>/dev/null
				json_get_var _mid moon_id 2>/dev/null
				unset input

				json_init

				if ! command -v zerotier-cli >/dev/null 2>&1; then
					json_add_int "code" 1
					json_add_string "moon_id" ""
					json_add_boolean "deorbited" 0
					json_add_string "stderr" "zerotier-cli not found"
					json_dump
					return
				fi

				_nid=$(moon_norm_id "$_mid")
				if [ -z "$_nid" ]; then
					json_add_int "code" 1
					json_add_string "moon_id" ""
					json_add_boolean "deorbited" 0
					json_add_string "stderr" "Invalid Moon ID: expected hex digits (10, or the 16-digit padded form), not the zero address"
					json_dump
					return
				fi

				_before=$(zerotier-cli listmoons 2>/dev/null)
				if ! moon_is_listed "$_nid" "$_before"; then
					json_add_int "code" 0
					json_add_boolean "deorbited" 0
					json_add_string "stderr" "moon $_nid is not orbited"
					json_add_string "moon_id" "$_nid"
					json_dump
					return
				fi

				_out=$(zerotier-cli deorbit "$_nid" 2>&1)
				_rc=$?
				_after=$(zerotier-cli listmoons 2>/dev/null)

				if moon_is_listed "$_nid" "$_after"; then
					json_add_int "code" 1
					json_add_boolean "deorbited" 0
					json_add_string "stderr" "moon still listed after deorbit (rc=$_rc${_out:+, out=$_out})"
				else
					json_add_int "code" 0
					json_add_boolean "deorbited" 1
					json_add_string "stderr" ""
					# deorbit unlinks moons.d/<id>.moon; mirror the removal
					# into the persistent dir so it does not come back.
					/usr/bin/zerotier-sync.sh >/dev/null 2>&1
				fi
				json_add_string "moon_id" "$_nid"
				json_dump
				;;
			ping_networks)
				json_init

				# 并发锁：防止多个 ping_networks 同时执行（多标签页、ubus 直调等）。
				# noclobber (set -C) 原子创建，消除 check-then-create 竞态。
				# 锁文件写入 "$PID $DEADLINE"（绝对期限，随扫描规模重算）。
				# 双重校验：PID 还活着（kill -0）且未过 DEADLINE —— 兜底进程
				# 暴毙（SIGKILL）后的锁复活。
				LOCKFILE="/tmp/zt_ping.lock"
				_lock_max=120
				_lock_try() {
					( set -C; echo "$$ $(( $(date +%s) + _lock_max ))" > "$LOCKFILE" ) 2>/dev/null
				}
				_lock_busy() {
					json_add_int "code" 1
					json_add_string "result" ""
					json_add_string "stderr" "scan already in progress"
					json_dump
				}
				# 只释放本进程持有的锁：扫描超过 deadline 后锁可能已被其他进程
				# 判定过期并重建，无条件 rm 会破坏新锁，放任第三个并发扫描
				_lock_release() {
					if [ -f "$LOCKFILE" ]; then
						read -r _lp _ld < "$LOCKFILE" 2>/dev/null
						[ "$_lp" = "$$" ] && rm -f "$LOCKFILE"
					fi
				}
				if ! _lock_try; then
					read -r _lp _ld < "$LOCKFILE" 2>/dev/null
					if [ -n "$_lp" ] && kill -0 "$_lp" 2>/dev/null && [ "$(date +%s)" -lt "${_ld:-0}" ]; then
						_lock_busy
						return
					fi
					rm -f "$LOCKFILE"
					if ! _lock_try; then
						_lock_busy
						return
					fi
				fi

				tmpfile=$(mktemp /tmp/zt_ping_XXXXXX)
				_sem=""
				trap '_lock_release; rm -f "$tmpfile" "$_sem" "$LOCKFILE.$$.tmp"; exec 9>&- 2>/dev/null' EXIT
				trap 'exit 1' INT TERM

				if ! command -v ping >/dev/null 2>&1; then
					json_add_int "code" 1
					json_add_string "result" ""
					json_add_string "stderr" "ping not found"
					json_dump
					return
				fi

				if ! command -v zerotier-cli >/dev/null 2>&1; then
					json_add_int "code" 1
					json_add_string "result" ""
					json_add_string "stderr" "zerotier-cli not found"
					json_dump
					return
				fi

				# Extract assigned IP addresses via JSON output (reliable field names
				# instead of fragile column-position parsing of text output)
				ip_list=$(zerotier-cli -j listnetworks 2>/dev/null | jsonfilter -e '$[*].assignedAddresses[*]' 2>/dev/null)
				if [ -z "$ip_list" ]; then
					json_add_int "code" 1
					json_add_string "result" ""
					json_add_string "stderr" "no assigned addresses or zerotier-cli failed"
					json_dump
					return
				fi

				# 已知扫描规模后重算锁期限（每个 v4 网段约 60s 最差耗时 + 60s 余量），
				# 覆盖多网络/低并发场景，避免锁被误判过期
				_nnets=0
				for _ipc in $ip_list; do
					case "$_ipc" in
						*.*.*.*/*) _nnets=$((_nnets + 1)) ;;
					esac
				done
				_lock_max=$((60 + _nnets * 60))
				# 临时文件 + rename 原子更新期限：直接 > 覆写存在被并发申请者
				# 读到空文件并误判锁过期的窗口
				echo "$$ $(( $(date +%s) + _lock_max ))" > "$LOCKFILE.$$.tmp" && mv -f "$LOCKFILE.$$.tmp" "$LOCKFILE"

				# 并发限流：FIFO 信号量模式。
				# 预先向命名管道写入 MAX_RUNNING 个 token（换行符）；ping_one() 每次
				# read -u 9 消费一个 token（管道空时自动阻塞，不消耗 CPU）；后台子 shell
				# 在 ping 结束后 echo >&9 归还 token。
				#
				# 效果：始终精确保持 MAX_RUNNING 个并发 ping。一个快 ping（100ms 完成）
				# 立刻让出 slot 给下一个 IP，不像 batch 模式那样要等齐一批里最慢的那个。
				#
				# 依赖 busybox ash 的 read 逐字节语义（不越过换行符吞掉多个 token）。
				# OpenWrt 的 /bin/sh 永远是 busybox ash，所以保证可用。
				# 不依赖 usleep / sleep 分数延时 / xargs -P（这些在 stock OpenWrt
				# busybox 默认配置下均不可用）。
				#
				# MAX_RUNNING 自适应：根据当前可用内存动态计算。
				# 实测每个 ping slot（subshell + ping 进程）并发时占 ~150 kB RSS
				# （共享文本段 + COW 后的实际物理内存）。按 300 kB/slot（2x 安全余量）、
				# 仅使用 1/3 可用内存计算，范围 [16, 512]。
				_avail_kb=$(awk '/^MemAvailable:/ {print $2}' /proc/meminfo 2>/dev/null)
				if [ -n "$_avail_kb" ] && [ "$_avail_kb" -gt 0 ]; then
					MAX_RUNNING=$((_avail_kb / 3 / 300))
					[ "$MAX_RUNNING" -lt 16 ] && MAX_RUNNING=16
					[ "$MAX_RUNNING" -gt 512 ] && MAX_RUNNING=512
				else
					MAX_RUNNING=64
				fi
				_sem=$(mktemp -u /tmp/zt_ping_sem_XXXXXX)
				if mkfifo "$_sem" 2>/dev/null; then
					exec 9<>"$_sem"
					_i=0
					while [ "$_i" -lt "$MAX_RUNNING" ]; do
						echo >&9
						_i=$((_i + 1))
					done
					ping_one() {
						read -u 9 2>/dev/null
						(
							ping -c 2 -W 1 "$1" >/dev/null 2>&1 && echo "OK:$1" || echo "FAIL:$1"
							echo >&9 2>/dev/null
						) >> "$tmpfile" 2>&1 &
					}
				else
					rm -f "$_sem" 2>/dev/null; _sem=""
					ping_one() {
						( ping -c 2 -W 1 "$1" >/dev/null 2>&1 && echo "OK:$1" || echo "FAIL:$1" ) >> "$tmpfile" 2>&1 &
					}
				fi

				# 设计说明：扫描器覆盖典型 ZeroTier 部署场景（/24 ~ /23，最多 512 个主机）。
				# 对于更大的网段（/22、/21、/16 等），仅扫描当前 IP 所在的 /24，因为：
				#   - /23 已覆盖普通用户使用 zerotier 的实际上限
				#   - 扫描 /22+（1024+ 个 IP）对常规连通性检查属于过度操作
				#   - 大网段用户应改用专门的扫描工具（nmap 等）
				for ip_cidr in $ip_list; do
					case "$ip_cidr" in
						*.*.*.*/*)
							ip=${ip_cidr%%/*}
							cidr=${ip_cidr##*/}
							if ! echo "$ip" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
								continue
							fi
							# Glob-validate the prefix BEFORE any test/arithmetic:
							# a non-numeric cidr makes `[ -lt/-gt ]` error out on
							# both sides (so the range check below would NOT skip
							# it) and `$((32 - cidr))` would then evaluate as 32,
							# yielding a ~4-billion-iteration ping loop as root.
							case "$cidr" in
								''|*[!0-9]*) continue ;;
							esac
							if [ "$cidr" -lt 1 ] || [ "$cidr" -gt 32 ]; then
								continue
							fi
							_oldIFS="$IFS"
							IFS='.'
							set -- $ip
							i1="$1"; i2="$2"; i3="$3"; i4="$4"
							IFS="$_oldIFS"
							if [ "$cidr" -le 24 ] && [ "$cidr" -ge 23 ]; then
								if [ "$cidr" = "23" ]; then
									base_i3=$((i3 & ~1))
									for j in $(seq 0 255); do
										ping_one "$i1.$i2.$base_i3.$j"
									done
									_base_i3=$((base_i3 + 1))
									for j in $(seq 0 255); do
										ping_one "$i1.$i2.$_base_i3.$j"
									done
								else
									for j in $(seq 0 255); do
										ping_one "$i1.$i2.$i3.$j"
									done
								fi
							elif [ "$cidr" -lt 23 ]; then
								# 大网段（/22、/21、/16...）只扫描当前所在 /24，详见上方设计说明
								for j in $(seq 0 255); do
									ping_one "$i1.$i2.$i3.$j"
								done
							else
								shifts=$((32 - cidr))
								host_count=1
								while [ "$shifts" -gt 0 ]; do
									host_count=$((host_count * 2))
									shifts=$((shifts - 1))
								done
								net_mask=$((256 - host_count))
								net_addr=$((i4 & net_mask))
								for j in $(seq "$net_addr" "$((net_addr + host_count - 1))"); do
									ping_one "$i1.$i2.$i3.$j"
								done
							fi
							;;
					esac
				done

				wait

				# Release semaphore fd and clean up
				[ -n "$_sem" ] && exec 9>&- 2>/dev/null
				rm -f "$_sem" 2>/dev/null

				if [ -f "$tmpfile" ]; then
					result=$(sort -u "$tmpfile" | tr '\n' '|')
					rm -f "$tmpfile"
				else
					result="no_result"
				fi

				json_add_int "code" 0
				json_add_string "result" "$result"
				json_add_string "stderr" ""
				json_dump
				;;
			sync_config)
				# Persist runtime state to the config dir - SAFE: specific operation
				#
				# 可选参数 local_conf_path：表单里尚未提交的新值（保存并应用时
				# 由 general.js 传入）；缺省时回退到已提交的配置值（Backup Now
				# 按钮路径）。参数经 stdin 以 JSON 传入（rpcd shell 插件惯例）。
				read -r input
				json_load "$input" 2>/dev/null
				json_get_var _lc local_conf_path 2>/dev/null
				[ -n "$_lc" ] || _lc=$(uci -q get zerotier.global.local_conf_path 2>/dev/null)

				json_init

				output=$(/usr/bin/zerotier-sync.sh 2>&1)
				exit_code=$?

				# 收敛 local_conf_path 到持久化配置目录：上游 init 用裸 `ln -s`
				# 把它链入运行目录，当 config_path 已提供 local.conf（本应用会播种）
				# 时静默失败。改为把文件内容导入 config_path/local.conf，上游的
				# cp -r 自然会带上正确内容，ln -s 失败也无害。
				# 必须在 sync 之后执行，避免被运行目录里的旧拷贝覆盖。
				if [ -n "$_lc" ] && [ -f "$_lc" ]; then
					_cp=$(uci -q get zerotier.global.config_path 2>/dev/null)
					[ -n "$_cp" ] || _cp="/etc/zerotier"
					[ "$_lc" != "$_cp/local.conf" ] && cp -f "$_lc" "$_cp/local.conf" 2>/dev/null
				fi

				json_add_int "code" "$exit_code"
				json_add_string "stdout" "$output"
				json_add_string "stderr" ""
				json_dump
				;;
			restart_service)
				# Restart ZeroTier service - SAFE: specific operation
				json_init

				# Validate that the service exists
				if [ ! -f "/etc/init.d/zerotier" ]; then
					json_add_int "code" 1
					json_add_string "stdout" ""
					json_add_string "stderr" "zerotier service not found"
				else
					# Persist runtime state first: the zerotier init's stop_service
					# does rm -rf /var/lib/zerotier-one, discarding anything not yet
					# synced (CLI-joined networks, controller/moon data)
					/usr/bin/zerotier-sync.sh >/dev/null 2>&1

					# Restart the main zerotier service
					output=$(/etc/init.d/zerotier restart 2>&1)
					exit_code=$?

					json_add_int "code" "$exit_code"
					json_add_string "stdout" "$output"
					json_add_string "stderr" ""
				fi

				json_dump
				;;
			remote_list)
				# Read-only: enumerate configured remote hosts. Key material is
				# never returned -- only whether a key file exists.
				json_init
				json_add_array "hosts"
				for _s in $(uci -q show zerotier 2>/dev/null | sed -n 's/^zerotier\.\([^.=]*\)=remote$/\1/p'); do
					_h=$(uci -q get "zerotier.$_s.host")
					[ -n "$_h" ] || continue
					_kp=$(uci -q get "zerotier.$_s.key_path")
					[ -n "$_kp" ] || _kp="/etc/zerotier/remote/$_s.key"
					json_add_object ""
					json_add_string "section" "$_s"
					json_add_string "name" "$(uci -q get "zerotier.$_s.name" || echo "$_s")"
					json_add_string "host" "$_h"
					json_add_string "port" "$(uci -q get "zerotier.$_s.port" || echo 22)"
					json_add_string "user" "$(uci -q get "zerotier.$_s.user" || echo root)"
				# No fake default: an unset controller_port reports empty and the
				# view says it will be auto-discovered (it is, on first use).
				json_add_string "controller_port" "$(uci -q get "zerotier.$_s.controller_port")"
				json_add_string "mode" "$(uci -q get "zerotier.$_s.mode" || echo ssh)"
				json_add_string "key_path" "$_kp"
				if [ -f "$_kp" ]; then
					json_add_boolean "key_present" 1
					# Public half only: a public key is not secret, and the
					# editor shows it so the operator can (re)authorize the
					# router on the remote host.
					_pb=""
					if [ -f "$_kp.pub" ]; then
						_pb=$(cat "$_kp.pub" 2>/dev/null)
					else
						# grep is load-bearing: dropbear's ssh-keygen prints a
						# "Public key portion is:" header and a Fingerprint line too.
						_pb=$(ssh-keygen -y -f "$_kp" 2>/dev/null | grep -m1 "^ssh-")
					fi
					[ -n "$_pb" ] && json_add_string "pubkey" "$_pb"
				else
					json_add_boolean "key_present" 0
				fi
				if [ -f "/etc/zerotier/remote/$_s.token" ]; then
					json_add_boolean "token_present" 1
				else
					json_add_boolean "token_present" 0
				fi
				if [ -f "/etc/zerotier/remote/$_s.direct" ]; then
					json_add_boolean "direct" 1
				else
					json_add_boolean "direct" 0
				fi
				json_close_object
				done
				json_close_array
				json_dump
				;;
			remote_host_set)
				# The private key is written to a 0600 file, never into UCI:
				# /etc/config is world-readable and is swept into sysupgrade
				# backups, whereas a key file can be excluded from keep.d.
				read -r input
				json_load "$input" 2>/dev/null
				json_get_var _sect section 2>/dev/null
				json_get_var _hname name 2>/dev/null
				json_get_var _rhost host 2>/dev/null
				json_get_var _rport port 2>/dev/null
				json_get_var _ruser user 2>/dev/null
				json_get_var _cport controller_port 2>/dev/null
			json_get_var _pkey private_key 2>/dev/null
			json_get_var _mode mode 2>/dev/null
			json_get_var _apikey api_key 2>/dev/null
			unset input

				json_init

			case "$_sect" in
				''|*[!A-Za-z0-9_]*) _err="invalid section name" ;;
				*) _err="" ;;
			esac
			[ -z "$_err" ] || { json_add_int "code" 1; json_add_string "error" "$_err"; json_dump; return; }

			# A label is free text, but UCI values cannot hold line breaks:
			# one would corrupt /etc/config/zerotier on commit. The byte-count
			# comparison, not grep, is load-bearing: grep can never see \n
			# because it is grep's own line delimiter. CJK and other UTF-8
			# labels pass untouched.
			if [ "$(printf '%s' "$_hname" | wc -c)" -ne "$(printf '%s' "$_hname" | tr -d '\r\n\t' | wc -c)" ]; then
				json_add_int "code" 1
				json_add_string "error" "label must not contain line breaks or control characters"
				json_dump
				return
			fi

				case "$_rhost" in
					''|*[!A-Za-z0-9.:_-]*) _err="host must be a hostname or IP literal" ;;
					*) case "$_rhost" in *[!0-9A-Za-z]*) _err="" ;; *) _err="host must contain a letter or digit" ;; esac ;;
				esac
				[ -z "$_err" ] || { json_add_int "code" 1; json_add_string "error" "$_err"; json_dump; return; }

			_mode=${_mode:-ssh}
			case "$_mode" in
				ssh|direct) ;;
				*) json_add_int "code" 1; json_add_string "error" "access mode must be ssh or direct"; json_dump; return ;;
			esac

			if [ "$_mode" = "ssh" ]; then
				case "$_ruser" in
					''|[a-zA-Z_]*) case "$_ruser" in *[!a-zA-Z0-9_-]*) _err="invalid ssh user" ;; esac ;;
					*) _err="invalid ssh user" ;;
				esac
				[ -z "$_err" ] || { json_add_int "code" 1; json_add_string "error" "$_err"; json_dump; return; }
				case "$_rport" in
					''|*[!0-9]*) _err="ports must be numeric" ;;
					*) [ "$_rport" -ge 1 ] 2>/dev/null && [ "$_rport" -le 65535 ] 2>/dev/null || _err="port out of range" ;;
				esac
				[ -z "$_err" ] || { json_add_int "code" 1; json_add_string "error" "$_err"; json_dump; return; }
				# SSH port is required; the controller port is OPTIONAL here --
				# empty means the helper discovers it from the remote daemon
				# and remembers it. Validate only when supplied.
				if [ -n "$_cport" ]; then
					case "$_cport" in
						*[!0-9]*) _err="controller port must be numeric" ;;
						*) [ "$_cport" -ge 1 ] 2>/dev/null && [ "$_cport" -le 65535 ] 2>/dev/null || _err="port out of range" ;;
					esac
					[ -z "$_err" ] || { json_add_int "code" 1; json_add_string "error" "$_err"; json_dump; return; }
				fi
			else
				case "$_cport" in
					''|*[!0-9]*) _err="controller port must be numeric" ;;
					*) [ "$_cport" -ge 1 ] 2>/dev/null && [ "$_cport" -le 65535 ] 2>/dev/null || _err="port out of range" ;;
				esac
				[ -z "$_err" ] || { json_add_int "code" 1; json_add_string "error" "$_err"; json_dump; return; }
			fi

			if [ "$_mode" = "direct" ]; then
				case "$_apikey" in
					'') [ -f "/etc/zerotier/remote/$_sect.token" ] || { json_add_int "code" 1; json_add_string "error" "api_key is required for direct mode"; json_dump; return; } ;;
					*[!A-Za-z0-9]*) json_add_int "code" 1; json_add_string "error" "api_key must be alphanumeric"; json_dump; return ;;
				esac
				if [ -n "$_apikey" ]; then
					(umask 077; printf '%s\n' "$_apikey" > "/etc/zerotier/remote/$_sect.token") || { json_add_int "code" 1; json_add_string "error" "could not write the token file"; json_dump; return; }
					chmod 600 "/etc/zerotier/remote/$_sect.token" 2>/dev/null
				fi
			fi

		_rport=${_rport:-22}
		_ruser=${_ruser:-root}
		_hname=${_hname:-$_sect}
		_kp="/etc/zerotier/remote/$_sect.key"

				mkdir -p /etc/zerotier/remote || { json_add_int "code" 1; json_add_string "error" "cannot create /etc/zerotier/remote"; json_dump; return; }
				chmod 700 /etc/zerotier/remote 2>/dev/null

			# An existing key is preserved when the field is left blank, so
			# editing a host's name does not silently destroy its key.
			if [ -n "$_pkey" ]; then
				case "$_pkey" in
					*"PRIVATE KEY"*) ;;
					*) json_add_int "code" 1; json_add_string "error" "that does not look like a private key"; json_dump; return ;;
				esac
				(umask 077; printf '%s\n' "$_pkey" > "$_kp") || { json_add_int "code" 1; json_add_string "error" "could not write the key file"; json_dump; return; }
				chmod 600 "$_kp" 2>/dev/null
			fi

			# No key pasted and none stored: generate a dedicated keypair on
			# the router. The private half is written straight to its 0600
			# file and never crosses the browser; the public half travels
			# back in the response so the UI can show the operator the exact
			# command to authorize it on the remote host (which they can run
			# there because they already have their own access).
			_keygen=0
			if [ "$_mode" = "ssh" ] && [ -z "$_pkey" ] && [ ! -f "$_kp" ]; then
				if (umask 077 && ssh-keygen -t ed25519 -N '' -C "luci-zerotier-$_sect" -f "$_kp" -q >/dev/null 2>&1); then
					_keygen=1
				else
					rm -f "$_kp" "$_kp.pub" 2>/dev/null
					json_add_int "code" 1
					json_add_string "error" "could not generate a key pair (ssh-keygen failed)"
					json_dump
					return
				fi
			fi

				uci -q delete "zerotier.$_sect" 2>/dev/null
				uci set "zerotier.$_sect=remote"
				uci set "zerotier.$_sect.name=$_hname"
				uci set "zerotier.$_sect.host=$_rhost"
			uci set "zerotier.$_sect.port=$_rport"
			uci set "zerotier.$_sect.user=$_ruser"
			# Direct mode needs the port on file (default 9993); SSH mode with
			# an empty value deletes any stale setting so the helper's
			# discover-and-remember path runs again.
			if [ "$_mode" = "direct" ] || [ -n "$_cport" ]; then
				uci set "zerotier.$_sect.controller_port=$( [ "$_mode" = "direct" ] && echo "${_cport:-9993}" || echo "$_cport" )"
			else
				uci -q delete "zerotier.$_sect.controller_port"
			fi
			uci set "zerotier.$_sect.key_path=$_kp"
			uci set "zerotier.$_sect.mode=$_mode"
			# A config change invalidates any cached transport decision: the old
			# direct marker or probe-failure note belongs to the old settings.
			rm -f "/etc/zerotier/remote/$_sect.direct" "/etc/zerotier/remote/$_sect.nodirect" 2>/dev/null
				uci commit zerotier || { json_add_int "code" 1; json_add_string "error" "uci commit failed"; json_dump; return; }

			json_add_int "code" 0
			json_add_string "key_path" "$_kp"
			# The public half rides along whenever it is known, so the editor
			# can display it for an existing key and the save flow can show
			# the authorize-on-remote command for a freshly generated one.
			_pub=""
			if [ -f "$_kp.pub" ]; then
				_pub=$(cat "$_kp.pub" 2>/dev/null)
			elif [ -f "$_kp" ]; then
				_pub=$(ssh-keygen -y -f "$_kp" 2>/dev/null | grep -m1 "^ssh-")
			fi
			[ -n "$_pub" ] && json_add_string "pubkey" "$_pub"
			[ "$_keygen" = "1" ] && json_add_boolean "key_generated" 1
			json_dump
			;;
			remote_host_del)
				read -r input
				json_load "$input" 2>/dev/null
				json_get_var _sect section 2>/dev/null
				unset input

				json_init
				case "$_sect" in
					''|*[!A-Za-z0-9_]*) json_add_int "code" 1; json_add_string "error" "invalid section name"; json_dump; return ;;
				esac
				_kp=$(uci -q get "zerotier.$_sect.key_path")
				[ -n "$_kp" ] || _kp="/etc/zerotier/remote/$_sect.key"
				# Only unlink inside our own managed directory.
			case "$_kp" in
				/etc/zerotier/remote/*.key)
					[ -f "$_kp" ] && rm -f "$_kp"
					# *.key does not match <key>.pub; drop it too or it leaks.
					rm -f "$_kp.pub" 2>/dev/null
					;;
			esac
			rm -f "/etc/zerotier/remote/$_sect.token" "/etc/zerotier/remote/$_sect.direct" "/etc/zerotier/remote/$_sect.nodirect" 2>/dev/null
				uci -q delete "zerotier.$_sect"
				uci commit zerotier
				json_add_int "code" 0
				json_dump
				;;
			remote_diagnose|remote_ctl_get|remote_member_list|remote_network_list|remote_peer_list|remote_network_set|remote_network_del|remote_member_set|remote_moon_plan|remote_moon_apply)
				# Thin delegation to zerotier-remote.sh, which owns all SSH,
				# tunnel and validation logic. Its stdout is already the JSON we
				# return verbatim, so nothing is re-encoded here.
				read -r input
				json_load "$input" 2>/dev/null
				json_get_var _sect section 2>/dev/null
				json_get_var _path path 2>/dev/null
				json_get_var _nwid nwid 2>/dev/null
				json_get_var _body body 2>/dev/null
				json_get_var _mid member_id 2>/dev/null
				json_get_var _cf confirm 2>/dev/null
				unset input

				case "$2" in
					remote_diagnose)    set -- diagnose "$_sect" ;;
					remote_ctl_get)     set -- ctl-get "$_sect" "$_path" ;;
					remote_member_list) set -- member-list "$_sect" "$_nwid" ;;
					remote_network_list) set -- network-list "$_sect" ;;
					remote_peer_list)   set -- peer-list "$_sect" ;;
					remote_network_set) set -- ctl-network-set "$_sect" "$_nwid" "$_body" ;;
					remote_network_del) set -- ctl-network-del "$_sect" "$_nwid" ;;
					remote_member_set)  set -- ctl-member-set "$_sect" "$_nwid" "$_mid" "$_body" ;;
					remote_moon_plan)   set -- moon-plan "$_sect" ;;
					remote_moon_apply)  set -- moon-apply "$_sect" "$_cf" ;;
				esac

if [ ! -x /usr/bin/zerotier-remote.sh ]; then
				json_init
				json_add_int "code" 1
				if [ -f /usr/bin/zerotier-remote.sh ]; then
					json_add_string "error" "zerotier-remote.sh is not executable (installed 0644; it must be 0755)"
				else
					json_add_string "error" "zerotier-remote.sh is missing"
				fi
				json_dump
				return
			fi

				# "$@" not $_args: the body is free-form JSON from the browser,
				# and a member or network name may contain spaces. Building a
				# command string and letting the shell split it handed the
				# helper only the first fragment, so its '{'*'}' check rejected
				# the call and setting a name with a space in it always failed
				# with "member body must be a JSON object".
				_out=$(/usr/bin/zerotier-remote.sh "$@" 2>/dev/null)
				_rc=$?
				if [ -z "$_out" ]; then
					json_init
					json_add_int "code" 1
					if [ "$_rc" -eq 124 ]; then
						json_add_string "error" "the remote host did not respond in time"
					else
						json_add_string "error" "remote helper failed (rc=$_rc)"
					fi
					json_dump
					return
				fi
				printf '%s\n' "$_out"
				;;
			*)
				# Invalid method - return error
				json_init
				json_add_int "code" 1
				json_add_string "stdout" ""
				json_add_string "stderr" "Invalid method: $2"
				json_dump
				;;
		esac
		;;
esac
