update 2026-09-05 22:03:25

This commit is contained in:
action committed 2026-09-05 22:03:25 +08:00
1 parent f3074ed22d
commit 063eca5fe2
2070 files changed
+46005 -1523828

No files matched your search

@@ -0,0 +1,71 @@
#!/bin/sh
# config-backup.sh export|import — back up/restore the whole daede config
# (dae + daed + active backend). export: prints base64(tar.gz) to stdout.
# import: decodes /tmp/daede-import.b64, validates, restores, restarts backend.
set -eu
ACTION="${1:-}"
LOG="/tmp/luci-app-daede.backup.log"
IMPORT_B64="/tmp/daede-import.b64"
MAX_TAR=184320 # 180 KiB tar.gz -> ~240 KiB base64, under the ubus limit
# relative paths (no leading /) so tar entries match the extraction whitelist
WHITELIST="etc/config/dae etc/config/daed etc/config/daede etc/dae/config.dae etc/daed/wing.db"
b64enc() { ucode -e 'let f=require("fs"); print(b64enc(f.open(ARGV[0],"r").read("all")));' -- "$1"; }
b64dec() { ucode -e 'let f=require("fs"); let r=b64dec(trim(f.open(ARGV[0],"r").read("all"))); if(!r)exit(1); let o=f.open(ARGV[1],"w"); o.write(r); o.close();' -- "$1" "$2"; }
case "$ACTION" in
export)
exist=""
for p in $WHITELIST; do [ -e "/$p" ] && exist="$exist $p"; done
[ -n "$exist" ] || { echo "no config to back up" >&2; exit 1; }
tmp="$(mktemp)"
( cd / && tar -czf "$tmp" $exist ) 2>/dev/null || { rm -f "$tmp"; echo "tar failed" >&2; exit 1; }
if [ "$(wc -c < "$tmp")" -gt "$MAX_TAR" ]; then
rm -f "$tmp"; echo "config too large; use System Backup instead" >&2; exit 2
fi
b64enc "$tmp"
rm -f "$tmp"
;;
import)
[ -f "$IMPORT_B64" ] || { echo "no upload found" >&2; exit 1; }
(
exec >"$LOG" 2>&1
echo "$(date '+%F %T') begin import"
rc=0
tmp="$(mktemp)"
if ! b64dec "$IMPORT_B64" "$tmp"; then
echo "decode failed"; rc=1
elif ! gzip -t "$tmp" 2>/dev/null; then
echo "not a valid backup archive"; rc=1
else
# reject any entry outside the whitelist (path-traversal guard)
bad="$(tar -tzf "$tmp" 2>/dev/null | grep -vxE 'etc/config/dae|etc/config/daed|etc/config/daede|etc/dae/config\.dae|etc/daed/wing\.db' | head -1)"
if [ -n "$bad" ]; then
echo "rejected: unexpected entry '$bad'"; rc=1
else
ab="$(uci -q get daede.config.active_backend || echo dae)"
[ -x "/etc/init.d/$ab" ] && /etc/init.d/"$ab" stop 2>/dev/null || true
if ( cd / && tar -xzf "$tmp" ); then
echo "restored config"
else
echo "extract failed"; rc=1
fi
ab="$(uci -q get daede.config.active_backend || echo "$ab")"
[ -x "/etc/init.d/$ab" ] && { /etc/init.d/"$ab" enabled || /etc/init.d/"$ab" enable; /etc/init.d/"$ab" restart 2>/dev/null || /etc/init.d/"$ab" start 2>/dev/null; } || true
fi
fi
rm -f "$tmp" "$IMPORT_B64"
if [ "$rc" = 0 ]; then echo "result: config restored, backend restarted"; else echo "result: import failed"; fi
if [ "$rc" = 0 ]; then echo "$(date '+%F %T') ✓ 完成"; else echo "$(date '+%F %T') ✗ 失败 (rc=$rc)"; fi
) </dev/null >/dev/null 2>&1 &
echo "started in background, see $LOG"
;;
*)
echo "usage: $0 export|import" >&2
exit 64
;;
esac
@@ -0,0 +1,38 @@
#!/bin/sh
# Manage daed subscription auto-update cron entry.
ACTION="$1"
CRONTAB="/etc/crontabs/root"
TAG="# luci-app-daede daed-sub-update"
SCRIPT="/usr/share/luci-app-daede/daed-sub-update.sh"
clean() {
[ -f "$CRONTAB" ] || return 0
grep -vF -e "$TAG" -e "$SCRIPT" "$CRONTAB" > "$CRONTAB.tmp" 2>/dev/null
mv "$CRONTAB.tmp" "$CRONTAB"
}
clean
if [ "$ACTION" = "enable" ] && [ "$(uci -q get daed.config.subscribe_auto_update)" = "1" ]; then
hour="$(uci -q get daed.config.subscribe_update_hour)"
case "$hour" in
''|*[!0-9]*) hour=4 ;;
esac
[ "$hour" -ge 0 ] 2>/dev/null && [ "$hour" -le 23 ] 2>/dev/null || hour=4
case "$(uci -q get daed.config.subscribe_update_cycle)" in
weekly) sched="17 $hour * * 0" ;;
*) sched="17 $hour * * *" ;;
esac
mkdir -p /etc/crontabs
{
echo "$TAG"
echo "$sched $SCRIPT >/dev/null 2>&1"
} >> "$CRONTAB"
/etc/init.d/cron enable >/dev/null 2>&1
fi
/etc/init.d/cron restart >/dev/null 2>&1
exit 0
@@ -0,0 +1,148 @@
#!/bin/sh
# Update all daed subscriptions through dae-wing GraphQL.
LOG="/tmp/luci-app-daede.daed-sub-update.log"
LOCK="/tmp/luci-app-daede.daed-sub-update.lock"
log() {
echo "$(date '+%F %T') $*" >> "$LOG"
}
fail() {
local code="$1"; shift
log "$*"
echo "$*" >&2
exit "$code"
}
json_escape() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}
cleanup() {
[ -n "$TMPDIR" ] && rm -rf "$TMPDIR"
rmdir "$LOCK" 2>/dev/null
}
if ! mkdir "$LOCK" 2>/dev/null; then
fail 1 "daed subscription update already running"
fi
trap cleanup EXIT INT TERM
TMPDIR="$(mktemp -d /tmp/daed-sub-update.XXXXXX)" || exit 1
port="$(uci -q get daed.config.listen_addr | sed -n 's/.*:\([0-9]\{1,5\}\)$/\1/p')"
[ -n "$port" ] || port=2023
endpoint="http://127.0.0.1:$port/graphql"
username="$(uci -q get daed.config.dashboard_username)"
password="$(uci -q get daed.config.dashboard_password)"
if [ -z "$username" ] || [ -z "$password" ]; then
fail 2 "missing daed dashboard username or password"
fi
if ! /bin/pidof daed >/dev/null 2>&1; then
fail 3 "daed is not running"
fi
post_graphql() {
local body="$1" outfile="$2" auth="$3"
if [ -n "$auth" ]; then
uclient-fetch -q -O "$outfile" \
--header="Content-Type: application/json" \
--header="Authorization: Bearer $auth" \
--post-file="$body" \
"$endpoint" 2>>"$LOG"
else
uclient-fetch -q -O "$outfile" \
--header="Content-Type: application/json" \
--post-file="$body" \
"$endpoint" 2>>"$LOG"
fi
}
login_body="$TMPDIR/login.json"
login_resp="$TMPDIR/login.out"
printf '{"query":"query Login($username:String!,$password:String!){token(username:$username,password:$password)}","variables":{"username":"%s","password":"%s"}}' \
"$(json_escape "$username")" "$(json_escape "$password")" > "$login_body"
log "starting daed subscription update"
if ! post_graphql "$login_body" "$login_resp"; then
fail 4 "login request failed"
fi
token="$(sed -n 's/.*"token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$login_resp")"
if [ -z "$token" ]; then
fail 5 "login failed: $(cat "$login_resp" 2>/dev/null)"
fi
list_body="$TMPDIR/list.json"
list_resp="$TMPDIR/list.out"
printf '{"query":"query Subscriptions{subscriptions{id tag link}}","variables":{}}' > "$list_body"
if ! post_graphql "$list_body" "$list_resp" "$token"; then
fail 6 "subscription list request failed"
fi
if grep -q '"errors"' "$list_resp"; then
fail 7 "subscription list failed: $(cat "$list_resp" 2>/dev/null)"
fi
sed 's/"id"/\
"id"/g' "$list_resp" | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' > "$TMPDIR/ids"
count="$(grep -c . "$TMPDIR/ids" 2>/dev/null || echo 0)"
if [ "${count:-0}" -eq 0 ]; then
log "no daed subscriptions to update"
exit 0
fi
ok=0
fail=0
while IFS= read -r id || [ -n "$id" ]; do
[ -n "$id" ] || continue
update_body="$TMPDIR/update.json"
update_resp="$TMPDIR/update.out"
printf '{"query":"mutation Update($id:ID!){updateSubscription(id:$id){id tag status}}","variables":{"id":"%s"}}' \
"$(json_escape "$id")" > "$update_body"
if post_graphql "$update_body" "$update_resp" "$token" && ! grep -q '"errors"' "$update_resp"; then
ok=$((ok + 1))
log "updated subscription $id"
else
fail=$((fail + 1))
log "failed subscription $id: $(cat "$update_resp" 2>/dev/null)"
fi
done < "$TMPDIR/ids"
log "finished daed subscription update: ok=$ok fail=$fail total=$count"
if [ "$fail" -ne 0 ]; then
exit 1
fi
if [ "$count" -gt 0 ] && [ "$ok" -gt 0 ] && /bin/pidof daed >/dev/null 2>&1; then
state_body="$TMPDIR/state.json"
state_resp="$TMPDIR/state.out"
printf '%s' '{"query":"query General{general{dae{running modified}}}","variables":{}}' > "$state_body"
if ! post_graphql "$state_body" "$state_resp" "$token" || grep -q '"errors"' "$state_resp"; then
fail 8 "failed to query daed runtime state: $(cat "$state_resp" 2>/dev/null)"
fi
if ! grep -q '"running"[[:space:]]*:[[:space:]]*true' "$state_resp"; then
log "daed proxy is stopped; skipping runtime apply"
exit 0
fi
if ! grep -q '"modified"[[:space:]]*:[[:space:]]*true' "$state_resp"; then
log "daed runtime is already up to date"
exit 0
fi
run_body="$TMPDIR/run.json"
run_resp="$TMPDIR/run.out"
printf '{"query":"mutation Run($dry:Boolean!){run(dry:$dry)}","variables":{"dry":false}}' > "$run_body"
log "applying updated subscriptions"
if ! post_graphql "$run_body" "$run_resp" "$token" || grep -q '"errors"' "$run_resp"; then
fail 9 "failed to apply updated subscriptions: $(cat "$run_resp" 2>/dev/null)"
fi
fi
exit 0
@@ -0,0 +1,134 @@
#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
set -eu
CHUNK_BYTES=16384
ACTION="${1:-}"
case "$ACTION" in
chunk)
TOKEN="${2:-}"
INDEX="${3:-}"
case "$TOKEN" in *[!A-Za-z0-9]*|'') exit 2 ;; esac
case "$INDEX" in *[!0-9]*|'') exit 2 ;; esac
FILE="/tmp/daede-clash-yaml-result.$TOKEN"
[ -f "$FILE" ] || exit 4
dd if="$FILE" bs="$CHUNK_BYTES" skip="$INDEX" count=1 2>/dev/null | hexdump -ve '1/1 "%02x"'
exit 0
;;
cleanup)
TOKEN="${2:-}"
case "$TOKEN" in *[!A-Za-z0-9]*|'') exit 2 ;; esac
rm -f "/tmp/daede-clash-yaml-result.$TOKEN"
exit 0
;;
esac
URL="${1:-}"
UA_MODE="${2:-auto}"
OUTPUT_MODE="${3:-stdout}"
MAX_BYTES=5242880
FETCH_BIN="${DAEDE_FETCH_BIN:-uclient-fetch}"
case "$URL" in
http://*|https://*) ;;
*)
echo "Subscription URL must use HTTP or HTTPS" >&2
exit 2
;;
esac
TMP="$(mktemp /tmp/daede-clash-yaml.XXXXXX)"
FIRST="$(mktemp /tmp/daede-clash-yaml-first.XXXXXX)"
ERR="$(mktemp /tmp/daede-clash-yaml-error.XXXXXX)"
trap 'rm -f "$TMP" "$FIRST" "$ERR"' EXIT INT TERM
chmod 600 "$TMP"
chmod 600 "$FIRST"
chmod 600 "$ERR"
case "$UA_MODE" in
auto|browser|ClashMeta|clash-verge/v2.4.2|ClashForWindows/0.20.39|Clash) ;;
*)
echo "Unsupported subscription User-Agent" >&2
exit 2
;;
esac
case "$OUTPUT_MODE" in
stdout|handle) ;;
*)
echo "Unsupported output mode" >&2
exit 2
;;
esac
LAST_ERROR="Failed to fetch subscription"
emit_result() {
local source="$1" result token size
if [ "$OUTPUT_MODE" = "stdout" ]; then
cat "$source"
return
fi
# Keep each RPC response small. The browser reads this root-only temporary
# file through the chunk action and removes it in a finally handler.
find /tmp -maxdepth 1 -name 'daede-clash-yaml-result.*' -mmin +10 -delete 2>/dev/null || true
result="$(mktemp /tmp/daede-clash-yaml-result.XXXXXX)"
chmod 600 "$result"
cp "$source" "$result"
token="${result##*.}"
size="$(wc -c <"$result" | tr -d ' ')"
printf '%s\t%s\n' "$token" "$size"
}
fetch_one() {
local ua="$1" size response_error
if [ "$ua" = "browser" ]; then
ua="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0.0.0 Safari/537.36"
fi
: >"$TMP"
: >"$ERR"
if ! "$FETCH_BIN" -q -T 20 -U "$ua" -O "$TMP" "$URL" 2>"$ERR"; then
response_error="$(head -c 512 "$TMP" | tr '\r\n\t' ' ')"
LAST_ERROR="${response_error:-$(cat "$ERR")}"
[ -n "$LAST_ERROR" ] || LAST_ERROR="Failed to fetch subscription"
return 1
fi
size="$(wc -c <"$TMP" | tr -d ' ')"
if [ "$size" -eq 0 ]; then
LAST_ERROR="Subscription response is empty"
return 1
fi
if [ "$size" -gt "$MAX_BYTES" ]; then
LAST_ERROR="Subscription response is too large (maximum 5 MiB)"
return 1
fi
return 0
}
if [ "$UA_MODE" = "auto" ]; then
for ua in ClashMeta clash-verge/v2.4.2 ClashForWindows/0.20.39 Clash browser; do
if fetch_one "$ua"; then
[ -s "$FIRST" ] || cp "$TMP" "$FIRST"
if grep -Eq '^[[:space:]]*proxies[[:space:]]*:' "$TMP"; then
emit_result "$TMP"
exit 0
fi
fi
done
if [ -s "$FIRST" ]; then
emit_result "$FIRST"
exit 0
fi
else
if fetch_one "$UA_MODE"; then
emit_result "$TMP"
exit 0
fi
fi
echo "$LAST_ERROR" >&2
exit 3
@@ -0,0 +1,422 @@
#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
#
# gen-dae-config.sh - build /etc/dae/config.dae from the dae UCI form sections,
# validate with `dae validate`, then overwrite + hot reload only on success.
#
# generate read UCI -> render config.dae -> validate -> overwrite + hot reload
# import parse existing config.dae subscription{}/node{} -> fill UCI form
#
# UCI model (package dae):
# config subscription tag/url, enabled
# config node tag/link, enabled
# config group name/policy, list source, list name_filter
# (old list filter_sub/filter_node still read as fallback)
# config routing 'routing' private_direct/cn_direct/block_ads/fallback, list custom
# config dns 'dns' cn_upstream/fallback_upstream/response_ttl
. /lib/functions.sh
CONFIG_DAE="/etc/dae/config.dae"
TMP_GEN="/tmp/dae-gen.dae"
DAE_BIN="/usr/bin/dae"
DAE_INITD="/etc/init.d/dae"
SUBS_DIR="/etc/dae/subscriptions"
SUB_STAGE="/tmp/daede-sub.txt"
# dae single-quoted strings have no escaping; strip quotes/newlines defensively.
sanitize() {
printf '%s' "$1" | tr -d "'" | tr -d '\n\r'
}
# dae subscription/node tags are bareword identifiers: only [A-Za-z0-9_] are
# valid tokens, so a CJK/spaced tag (e.g. 白月光) makes the parser choke. Map
# any such tag to a stable ASCII slug; pure-word tags pass through unchanged.
# Deterministic on the input, so a group's source reference resolves to the
# same slug as the subscription it points at. Empty stays empty (anonymous).
dae_tag() {
[ -n "$1" ] || return 0
case "$1" in
*[!A-Za-z0-9_]*) printf 's_%s' "$(printf '%s' "$1" | md5sum | cut -c1-10)" ;;
*) printf '%s' "$1" ;;
esac
}
# ===================== generate =====================
SUB_BUF=""
NODE_BUF=""
GROUP_BUF=""
FIRST_GROUP=""
emit_sub() {
local s="$1" tag url enabled
config_get tag "$s" tag ""
config_get url "$s" url ""
config_get_bool enabled "$s" enabled 1
[ "$enabled" = "1" ] || return 0
url="$(sanitize "$url")"
[ -n "$url" ] || return 0
tag="$(dae_tag "$(sanitize "$tag")")"
if [ -n "$tag" ]; then
SUB_BUF="${SUB_BUF} ${tag}: '${url}'
"
else
SUB_BUF="${SUB_BUF} '${url}'
"
fi
}
emit_node() {
local s="$1" tag link enabled
config_get tag "$s" tag ""
config_get link "$s" link ""
config_get_bool enabled "$s" enabled 1
[ "$enabled" = "1" ] || return 0
link="$(sanitize "$link")"
[ -n "$link" ] || return 0
tag="$(dae_tag "$(sanitize "$tag")")"
if [ -n "$tag" ]; then
NODE_BUF="${NODE_BUF} ${tag}: '${link}'
"
else
NODE_BUF="${NODE_BUF} '${link}'
"
fi
}
emit_group() {
local s="$1" name policy
config_get name "$s" name ""
config_get policy "$s" policy "min_moving_avg"
name="$(sanitize "$name")"
[ -n "$name" ] || return 0
# dae fatals on duplicate outbound names; keep the first, skip later dupes
case " $GROUP_NAMES_SEEN " in *" $name "*) return 0 ;; esac
GROUP_NAMES_SEEN="$GROUP_NAMES_SEEN $name"
[ -n "$FIRST_GROUP" ] || FIRST_GROUP="$name"
GROUP_BUF="${GROUP_BUF} ${name} {
"
# Build the source: 'source' entries that match a subscription tag become
# subtag(), the rest (node tags / keywords) become name(). Old filter_sub /
# filter_node configs still work as a fallback when 'source' is unset.
SUBS_ACC=""; NODES_ACC=""; KW_ACC=""
# source: new field, else old filter_sub (decoupled from the name filter so
# editing one never silently drops the other)
config_list_foreach "$s" source _collect_source
[ -z "$SUBS_ACC" ] && [ -z "$NODES_ACC" ] && config_list_foreach "$s" filter_sub _collect_source
# optional name filter narrows the subscription pool only (AND); hand-picked
# nodes are explicit choices and stay in unconditionally
config_list_foreach "$s" name_filter _collect_kw
[ -z "$KW_ACC" ] && config_list_foreach "$s" filter_node _collect_kw
local andclause=""
[ -n "$KW_ACC" ] && andclause=" && name(${KW_ACC})"
local emitted=0
# multiple filter lines = OR, so subtag and name lines union the sources
if [ -n "$SUBS_ACC" ]; then
GROUP_BUF="${GROUP_BUF} filter: subtag(${SUBS_ACC})${andclause}
"
emitted=1
fi
if [ -n "$NODES_ACC" ]; then
GROUP_BUF="${GROUP_BUF} filter: name(${NODES_ACC})
"
emitted=1
fi
# name filter with no source = filter the whole pool by name
if [ "$emitted" = 0 ] && [ -n "$KW_ACC" ]; then
GROUP_BUF="${GROUP_BUF} filter: name(${KW_ACC})
"
fi
GROUP_BUF="${GROUP_BUF} policy: ${policy}
}
"
SUBS_ACC=""; NODES_ACC=""; KW_ACC=""
}
SUBS_ACC=""
NODES_ACC=""
KW_ACC=""
# Subscription tags, space-padded for membership tests (" tag " match).
SUB_TAGS=""
collect_subtag() {
local t; config_get t "$1" tag ""
t="$(dae_tag "$(sanitize "$t")")"
[ -n "$t" ] && SUB_TAGS="${SUB_TAGS} ${t} "
}
is_subtag() {
case "$SUB_TAGS" in *" $1 "*) return 0 ;; *) return 1 ;; esac
}
# dae filter args are bare only for plain identifiers; anything with regex
# chars / CJK / spaces must be wrapped as regex: '...' or the lexer chokes.
filter_arg() {
case "$1" in
*[!A-Za-z0-9_]*) printf "regex: '%s'" "$1" ;;
*) printf '%s' "$1" ;;
esac
}
# name-filter args do substring matching: regex metachars -> regex:, else keyword:.
# Intentionally stricter than filter_arg: a plain CJK keyword here stays a
# substring `keyword:` match, whereas filter_arg wraps any non-word char as regex.
kw_arg() {
if printf '%s' "$1" | grep -q '[][|().*+?^$\\]'; then
printf "regex: '%s'" "$1"
else
printf "keyword: '%s'" "$1"
fi
}
# classify a source entry: subscription tag -> subtag bucket, else name bucket
_collect_source() {
local v="$(sanitize "$1")"
[ -n "$v" ] || return 0
# subscription refs resolve to the (ASCII) dae tag; node-name refs keep the
# original text (matched as a quoted regex against node names)
local dt="$(dae_tag "$v")"
if is_subtag "$dt"; then
SUBS_ACC="${SUBS_ACC:+$SUBS_ACC, }$(filter_arg "$dt")"
else
NODES_ACC="${NODES_ACC:+$NODES_ACC, }$(filter_arg "$v")"
fi
}
_collect_kw() {
local v="$(sanitize "$1")"
[ -n "$v" ] || return 0
KW_ACC="${KW_ACC:+$KW_ACC, }$(kw_arg "$v")"
}
generate() {
config_load dae
# global overridable knobs (fall back to defaults)
local dial_mode log_level wan_interface lan_interface
config_get dial_mode config dial_mode "domain"
config_get log_level config log_level "info"
config_get wan_interface config wan_interface "auto"
config_get lan_interface config lan_interface "br-lan"
SUB_BUF=""; NODE_BUF=""; GROUP_BUF=""; FIRST_GROUP=""; GROUP_NAMES_SEEN=""
SUB_TAGS=""
config_foreach collect_subtag subscription
config_foreach emit_sub subscription
config_foreach emit_node node
config_foreach emit_group group
# fall back to a default group when none defined
if [ -z "$GROUP_BUF" ]; then
GROUP_BUF=" proxy {
policy: min_moving_avg
}
"
FIRST_GROUP="proxy"
fi
# routing / dns singletons
local private_direct cn_direct block_ads fallback cn_up fb_up response_ttl
config_get_bool private_direct routing private_direct 1
config_get_bool cn_direct routing cn_direct 1
config_get_bool block_ads routing block_ads 0
config_get fallback routing fallback "$FIRST_GROUP"
[ -n "$fallback" ] || fallback="$FIRST_GROUP"
fallback="$(sanitize "$fallback")"
config_get cn_up dns cn_upstream "udp://dns.alidns.com:53"
config_get fb_up dns fallback_upstream "tcp+udp://dns.google:53"
config_get response_ttl dns response_ttl "0"
cn_up="$(sanitize "$cn_up")"
fb_up="$(sanitize "$fb_up")"
case "$response_ttl" in
''|*[!0-9]*) response_ttl=0 ;;
esac
{
echo "# 本配置由 luci-app-daede 表单自动生成,再次保存表单会覆盖你的手动修改。"
echo "global {"
echo " tproxy_port: 12345"
echo " tproxy_port_protect: true"
echo " so_mark_from_dae: 0"
echo " log_level: ${log_level}"
[ -n "$lan_interface" ] && echo " lan_interface: ${lan_interface}"
echo " wan_interface: ${wan_interface}"
echo " auto_config_kernel_parameter: true"
echo " dial_mode: ${dial_mode}"
echo " tcp_check_url: 'http://cp.cloudflare.com,1.1.1.1,2606:4700:4700::1111'"
echo " udp_check_dns: 'dns.google:53,8.8.8.8,2001:4860:4860::8888'"
echo " check_interval: 30s"
echo " check_tolerance: 50ms"
echo "}"
echo ""
if [ -n "$SUB_BUF" ]; then
echo "subscription {"
printf '%s' "$SUB_BUF"
echo "}"
echo ""
fi
if [ -n "$NODE_BUF" ]; then
echo "node {"
printf '%s' "$NODE_BUF"
echo "}"
echo ""
fi
echo "dns {"
echo " ipversion_prefer: 4"
[ "$response_ttl" -gt 0 ] && echo " response_ttl: ${response_ttl}"
echo " upstream {"
echo " cndns: '${cn_up}'"
echo " fallbackdns: '${fb_up}'"
echo " }"
echo " routing {"
echo " request {"
[ "$block_ads" = "1" ] && echo " qname(geosite:category-ads-all) -> reject"
echo " qname(geosite:cn) -> cndns"
echo " fallback: fallbackdns"
echo " }"
echo " }"
echo "}"
echo ""
echo "group {"
printf '%s' "$GROUP_BUF"
echo "}"
echo ""
echo "routing {"
echo " pname(NetworkManager) -> direct"
# multicast / broadcast direct (geoip:private doesn't cover these)
echo " dip(224.0.0.0/3) -> direct"
echo " dip(255.255.255.255/32) -> direct"
echo " dip('ff00::/8') -> direct"
[ "$private_direct" = "1" ] && echo " dip(geoip:private) -> direct"
# NTP time sync direct
echo " l4proto(udp) && dport(123) -> direct"
# OS connectivity checks direct (avoid captive-portal false positives)
echo " domain(connectivitycheck.gstatic.com) -> direct"
echo " domain(msftconnecttest.com) -> direct"
if [ "$cn_direct" = "1" ]; then
echo " dip(geoip:cn) -> direct"
echo " domain(geosite:cn) -> direct"
fi
[ "$block_ads" = "1" ] && echo " domain(geosite:category-ads-all) -> block"
config_list_foreach routing custom _emit_custom
echo " fallback: ${fallback}"
echo "}"
} > "$TMP_GEN"
chmod 600 "$TMP_GEN" # dae rejects world/group-accessible config files
# overwrite live config only if validation passes
local out
out="$("$DAE_BIN" validate -c "$TMP_GEN" 2>&1)"
if [ $? -ne 0 ]; then
echo "validate failed: $(echo "$out" | head -1)" >&2
rm -f "$TMP_GEN"
return 1
fi
mkdir -p /etc/dae
cat "$TMP_GEN" > "$CONFIG_DAE"
chmod 600 "$CONFIG_DAE"
rm -f "$TMP_GEN"
if /usr/bin/pgrep -x dae >/dev/null 2>&1; then
"$DAE_INITD" hot_reload >/dev/null 2>&1
fi
echo "ok"
return 0
}
_emit_custom() {
local line="$(printf '%s' "$1" | tr -d '\n\r')"
[ -n "$line" ] || return 0
echo " ${line}"
}
# ===================== import =====================
# Fill the UCI form from an existing config.dae subscription{}/node{} block.
# Idempotent: wipe same-type sections first, then rebuild from file.
import_block() {
# $1=block name (subscription|node) $2=uci section type $3=link/url field
local block="$1" stype="$2" field="$3"
awk -v blk="$block" '
$0 ~ "^[[:space:]]*"blk"[[:space:]]*\\{" { inb=1; next }
inb && /^[[:space:]]*\}/ { inb=0 }
inb { print }
' "$CONFIG_DAE" 2>/dev/null | while IFS= read -r raw; do
# drop comment, trim
line="$(printf '%s' "$raw" | sed 's/#.*$//' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
[ -n "$line" ] || continue
tag=""; val=""
case "$line" in
\'*|\"*)
# starts with a quote -> untagged link
val="$line"
;;
[A-Za-z0-9_]*:*)
# identifier ':' link -> tagged (split on first colon)
tag="$(printf '%s' "$line" | sed "s/[[:space:]]*:.*//")"
val="$(printf '%s' "$line" | sed "s/^[^:]*:[[:space:]]*//")"
;;
*)
val="$line"
;;
esac
# strip surrounding quotes
val="$(printf '%s' "$val" | sed "s/^['\"]//; s/['\"]$//")"
[ -n "$val" ] || continue
# skip dae example placeholders (ss://LINK, server-ip:port, localhost:1080 ...)
case "$val" in
*://LINK|*LINK*|*server-ip*|*://localhost:*|*example.com*) continue ;;
esac
sid="$(uci add dae "$stype")"
[ -n "$tag" ] && uci set "dae.$sid.tag=$tag"
uci set "dae.$sid.$field=$val"
uci set "dae.$sid.enabled=1"
done
}
do_import() {
[ -f "$CONFIG_DAE" ] || { echo "no config.dae" >&2; return 1; }
while uci -q delete dae.@subscription[0] >/dev/null 2>&1; do :; done
while uci -q delete dae.@node[0] >/dev/null 2>&1; do :; done
import_block subscription subscription url
import_block node node link
uci commit dae
echo "ok"
}
# Converter writes a batch of share links to a local file that dae reads as a
# file:// subscription, so a converted airport is one subscription instead of
# hundreds of manual nodes. $1=id (airport id), reads links from SUB_STAGE.
# id is restricted to [A-Za-z0-9_] to keep the path inside SUBS_DIR.
do_write_sub() {
local id="$1"
case "$id" in ''|*[!A-Za-z0-9_]*) echo "bad id" >&2; return 2 ;; esac
[ -f "$SUB_STAGE" ] || { echo "no staged data" >&2; return 1; }
mkdir -p "$SUBS_DIR"
# dae rejects subscription/config files readable by group/other (needs <=0640)
cp "$SUB_STAGE" "$SUBS_DIR/$id.sub.tmp" || { echo "write failed" >&2; return 1; }
chmod 0600 "$SUBS_DIR/$id.sub.tmp"
mv "$SUBS_DIR/$id.sub.tmp" "$SUBS_DIR/$id.sub"
rm -f "$SUB_STAGE"
echo "ok"
}
do_delete_sub() {
local id="$1"
case "$id" in ''|*[!A-Za-z0-9_]*) return 0 ;; esac
rm -f "$SUBS_DIR/$id.sub"
echo "ok"
}
# ===================== entry =====================
case "$1" in
generate) generate ;;
import) do_import ;;
write-sub) do_write_sub "$2" ;;
delete-sub) do_delete_sub "$2" ;;
*) echo "usage: $0 {generate|import|write-sub <id>|delete-sub <id>}" >&2; exit 2 ;;
esac
+34
View File
@@ -0,0 +1,34 @@
#!/bin/sh
# geo-cron.sh enable|disable
# Manage the geo auto-update crontab entry. enable reads the schedule from
# daede.config.geo_auto_freq (daily|weekly); both actions are idempotent.
ACTION="$1"
CRONTAB="/etc/crontabs/root"
TAG="# luci-app-daede geo-update"
SCRIPT="/usr/share/luci-app-daede/update-geo.sh"
# Drop any previous daede geo entry (tag line + its command line).
clean() {
[ -f "$CRONTAB" ] || return 0
grep -vF -e "$TAG" -e "$SCRIPT" "$CRONTAB" > "$CRONTAB.tmp" 2>/dev/null
mv "$CRONTAB.tmp" "$CRONTAB"
}
clean
if [ "$ACTION" = "enable" ]; then
case "$(uci -q get daede.config.geo_auto_freq)" in
weekly) sched="17 4 * * 0" ;; # Sunday 04:17
*) sched="17 4 * * *" ;; # every day 04:17
esac
mkdir -p /etc/crontabs
{
echo "$TAG"
echo "$sched $SCRIPT geoip; $SCRIPT geosite"
} >> "$CRONTAB"
/etc/init.d/cron enable >/dev/null 2>&1
fi
/etc/init.d/cron restart >/dev/null 2>&1
exit 0
+50
View File
@@ -0,0 +1,50 @@
#!/bin/sh
# pkg-info.sh <package>
# Prints "<installed>\t<latest>" for the named package, where either field is
# empty when unknown. Used by the Updates view to decide whether to enable
# the [Upgrade] button.
PKG="$1"
case "$PKG" in
dae|daed|luci-app-daede) ;;
*) echo "" ; exit 64 ;;
esac
installed=""
latest=""
if command -v apk >/dev/null 2>&1; then
if apk info -e "$PKG" >/dev/null 2>&1; then
installed=$(apk list -I "$PKG" 2>/dev/null | awk -v p="$PKG" '
$1 ~ "^" p "-" {
sub("^" p "-", "", $1);
print $1;
exit
}
')
fi
# apk search treats ^ and $ as literal chars (not regex anchors), so the old
# `apk search "^pkg$"` matched nothing. Enumerate the exact-name package
# across all feeds via `apk list` and take the highest version. Restricting
# to "<pkg>-<digit>" avoids sibling names like <pkg>-geoip / luci-app-<pkg>.
latest=$(apk list "$PKG" 2>/dev/null | awk -v p="$PKG" '
$1 ~ "^" p "-[0-9]" {
v = $1; sub("^" p "-", "", v);
print v
}
' | sort -V | tail -1)
elif command -v opkg >/dev/null 2>&1; then
installed=$(opkg status "$PKG" 2>/dev/null | awk -F': ' '$1=="Version"{print $2; exit}')
latest=$(opkg info "$PKG" 2>/dev/null | awk -F': ' '$1=="Version"{print $2; exit}')
fi
case "$PKG" in
dae|daed)
case "$installed" in
20[0-9][0-9].[0-9]*) ;;
*) installed="" ;;
esac
;;
esac
printf '%s\t%s\n' "$installed" "$latest"
@@ -0,0 +1,27 @@
#!/bin/sh
# proxy-check.sh - test whether dae's transparent proxy can actually reach a
# blocked site (default YouTube). dae proxies the router's own WAN-bound traffic
# (wan_interface), so a plain request from here is routed through dae just like a
# LAN client would be. Prints one line: ok=<0|1> code=<http> ms=<n>
URL="${1:-https://www.youtube.com/generate_204}"
code=000
ms=0
if command -v curl >/dev/null 2>&1; then
out="$(curl -4 -sL --max-time 8 --connect-timeout 4 \
-o /dev/null -w '%{http_code} %{time_total}' "$URL" 2>/dev/null)"
code="$(printf '%s' "$out" | awk '{print $1}')"
t="$(printf '%s' "$out" | awk '{print $2}')"
[ -n "$code" ] || code=000
ms="$(awk -v x="${t:-0}" 'BEGIN{printf "%d", x*1000}')"
else
# busybox wget has no status/timing output — success is exit 0
if wget -q -T 8 -O /dev/null "$URL" 2>/dev/null; then code=204; else code=000; fi
fi
ok=0
case "$code" in 200|204|301|302) ok=1 ;; esac
echo "ok=$ok code=$code ms=$ms"
@@ -0,0 +1,21 @@
#!/bin/sh
# Background apk/opkg update for the Updates view. Returns at once; throttled.
LOCK=/tmp/luci-app-daede.idx.lock
if [ -f "$LOCK" ]; then
mtime=$(date -r "$LOCK" +%s 2>/dev/null || echo 0)
[ "$(( $(date +%s) - mtime ))" -lt 90 ] && exit 0
fi
: > "$LOCK"
(
# -n: skip if a package op holds the shared apk lock (no need to refresh then)
if command -v apk >/dev/null 2>&1; then
flock -n /tmp/luci-app-daede.apk.lock apk update
elif command -v opkg >/dev/null 2>&1; then
opkg update
fi
) >/dev/null 2>&1 </dev/null &
exit 0
+111
View File
@@ -0,0 +1,111 @@
#!/bin/sh
# update-geo.sh <geoip|geosite>
# Fork-and-detach so the LuCI RPC call returns immediately. The diagnostics
# card polls the .dat file's mtime/size to detect completion. A lock file
# in /tmp prevents concurrent runs.
TYPE="$1"
# Default source: Loyalsoldier/v2ray-rules-dat — daily-built, China-optimized,
# ships both geoip.dat and geosite.dat. Users can override per-type via
# daede.config.geoip_url / geosite_url (empty falls back to the default below).
case "$TYPE" in
geoip)
DEF_URL="https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
URL="$(uci -q get daede.config.geoip_url)"
DEST="/usr/share/v2ray/geoip.dat"
;;
geosite)
DEF_URL="https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
URL="$(uci -q get daede.config.geosite_url)"
DEST="/usr/share/v2ray/geosite.dat"
;;
*)
echo "usage: $0 <geoip|geosite>" >&2
exit 64
;;
esac
[ -n "$URL" ] || URL="$DEF_URL"
LOCK="/tmp/luci-app-daede.${TYPE}.lock"
LOG="/tmp/luci-app-daede.${TYPE}.log"
RLOCK="/tmp/luci-app-daede.geo-reload.lock"
schedule_backend_reload() {
if [ -d "$RLOCK" ]; then
rmt=$(date -r "$RLOCK" +%s 2>/dev/null || echo 0)
[ $(( $(date +%s) - rmt )) -gt 300 ] && rmdir "$RLOCK" 2>/dev/null
fi
mkdir "$RLOCK" 2>/dev/null || return 0
(
sleep 25
ab="$(uci -q get daede.config.active_backend 2>/dev/null || echo daed)"
if [ "$ab" = dae ]; then
bin=dae; act=hot_reload
else
bin=daed; act=reload
fi
if /bin/pidof "$bin" >/dev/null 2>&1; then
if "/etc/init.d/$bin" "$act" >/dev/null 2>&1; then
echo "$(date '+%F %T') reloaded $bin"
else
echo "$(date '+%F %T') $bin reload failed"
fi
else
echo "$(date '+%F %T') $bin not running; skip reload"
fi
rmdir "$RLOCK" 2>/dev/null
) </dev/null >>"$LOG" 2>&1 &
}
if [ -f "$LOCK" ]; then
# Lock is fresh (< 5 min)? Refuse. Stale? Remove and proceed.
mtime=$(date -r "$LOCK" +%s 2>/dev/null || echo 0)
age=$(( $(date +%s) - mtime ))
if [ "$age" -lt 300 ]; then
echo "${TYPE} update already in progress (PID $(cat "$LOCK" 2>/dev/null), age ${age}s)" >&2
exit 75
fi
rm -f "$LOCK"
fi
if ! ( set -C; echo "$$" >"$LOCK" ) 2>/dev/null; then
echo "${TYPE} update already in progress" >&2
exit 75
fi
# Spawn detached worker — parent returns immediately to LuCI.
(
exec >"$LOG" 2>&1
trap 'rm -f "$LOCK"' EXIT INT TERM
rc=0
TMP="${DEST}.new"
mkdir -p "$(dirname "$DEST")"
echo "$(date '+%F %T') begin: $URL"
if ! curl -fsSL --connect-timeout 15 --max-time 240 -o "$TMP" "$URL"; then
echo "$(date '+%F %T') download failed"
rm -f "$TMP"
rc=1
else
size=$(wc -c < "$TMP" 2>/dev/null || echo 0)
magic=$(hexdump -n 1 -e '1/1 "%02x"' "$TMP" 2>/dev/null)
if [ "$size" -lt 1024 ]; then
echo "$(date '+%F %T') file too small ($size bytes)"
rm -f "$TMP"
rc=2
elif [ "$magic" != "0a" ]; then
echo "$(date '+%F %T') invalid geodata format"
rm -f "$TMP"
rc=3
else
mv "$TMP" "$DEST"
echo "$(date '+%F %T') updated $DEST ($size bytes)"
schedule_backend_reload
fi
fi
if [ "$rc" = 0 ]; then echo "$(date '+%F %T') ✓ 完成"; else echo "$(date '+%F %T') ✗ 失败 (rc=$rc)"; fi
) </dev/null >/dev/null 2>&1 &
echo "started in background, see $LOG"
exit 0
+105
View File
@@ -0,0 +1,105 @@
#!/bin/sh
# update-pkg.sh <dae|daed|luci-app-daede>
# Refresh package indexes and upgrade the named package via apk (25.12+) or
# opkg (24.10). Forks the work to background so the LuCI RPC call returns
# immediately; the result is streamed to /tmp/luci-app-daede.pkg.<name>.log.
PKG="$1"
case "$PKG" in
dae|daed|luci-app-daede) ;;
*)
echo "usage: $0 <dae|daed|luci-app-daede>" >&2
exit 64
;;
esac
# run from a /tmp copy so upgrading luci-app-daede (which replaces this script)
# can't corrupt the in-flight upgrade
case "$0" in
/tmp/.daede-upd-*) ;;
*)
_self="/tmp/.daede-upd-$$"
cp "$0" "$_self" 2>/dev/null && exec sh "$_self" "$@"
;;
esac
LOCK="/tmp/luci-app-daede.pkg-${PKG}.lock"
LOG="/tmp/luci-app-daede.pkg-${PKG}.log"
if [ -f "$LOCK" ]; then
mtime=$(date -r "$LOCK" +%s 2>/dev/null || echo 0)
age=$(( $(date +%s) - mtime ))
if [ "$age" -lt 300 ]; then
echo "${PKG} update already in progress (PID $(cat "$LOCK" 2>/dev/null), age ${age}s)" >&2
exit 75
fi
rm -f "$LOCK"
fi
if ! ( set -C; echo "$$" >"$LOCK" ) 2>/dev/null; then
echo "${PKG} update already in progress" >&2
exit 75
fi
(
exec >"$LOG" 2>&1
trap 'rm -f "$LOCK"; [ "${0#/tmp/.daede-upd-}" != "$0" ] && rm -f "$0"' EXIT INT TERM
echo "$(date '+%F %T') begin upgrade: $PKG"
if command -v apk >/dev/null 2>&1; then
# shared apk lock with the bg index refresh (avoid "Unable to lock database")
(
flock 9
apk update 2>&1
ver=$(apk list "$PKG" 2>/dev/null | awk -v p="$PKG" '$1 ~ "^" p "-[0-9]" { v=$1; sub("^" p "-", "", v); print v }' | sort -V | tail -1)
if [ -n "$ver" ]; then
constraint="$PKG=$ver"
else
echo "result: 软件源中没有找到 $PKG,请检查网络或软件源配置"
exit 1
fi
echo "--- apk add -s $constraint ---"
if ! apk add -s "$constraint" 2>&1; then
echo "note: apk 预检失败,通常是系统上其它软件包的问题,继续尝试升级"
fi
echo "--- apk add $constraint ---"
apk add "$constraint" 2>&1
exit 0
) 9>/tmp/luci-app-daede.apk.lock
rc=$?
if [ "$rc" != 0 ]; then
:
elif ! apk list --installed 2>/dev/null | grep -q "^${PKG}-"; then
echo "result: $PKG is not installed"
rc=1
elif apk list -u 2>/dev/null | grep -q "^${PKG}-"; then
echo "result: $PKG still has a pending upgrade"
rc=1
else
echo "result: $PKG is at the latest available version"
rc=0
fi
elif command -v opkg >/dev/null 2>&1; then
echo "--- opkg update ---"
opkg update 2>&1
echo "--- opkg upgrade $PKG ---"
opkg upgrade "$PKG" 2>&1
rc=$?
else
echo "no package manager found"
exit 3
fi
if [ "$rc" = 0 ]; then echo "$(date '+%F %T') ✓ 完成"; else echo "$(date '+%F %T') ✗ 失败 (rc=$rc)"; fi
# luci-app-daede upgrade replaces ACL JSON — reload rpcd so changes apply.
if [ "$PKG" = "luci-app-daede" ] && [ "$rc" = "0" ]; then
echo "reloading rpcd to pick up new ACL"
/etc/init.d/rpcd reload 2>&1
fi
) </dev/null >/dev/null 2>&1 &
echo "started in background, see $LOG"
exit 0
@@ -0,0 +1,44 @@
{
"admin/services/daede": {
"title": "daede",
"order": 20,
"action": {
"type": "firstchild"
},
"depends": {
"acl": [ "luci-app-daede" ]
}
},
"admin/services/daede/config": {
"title": "Settings",
"order": 10,
"action": {
"type": "view",
"path": "daede/config"
}
},
"admin/services/daede/converter": {
"title": "Subscription Converter",
"order": 20,
"action": {
"type": "view",
"path": "daede/converter"
}
},
"admin/services/daede/updates": {
"title": "Updates",
"order": 30,
"action": {
"type": "view",
"path": "daede/updates"
}
},
"admin/services/daede/log": {
"title": "Log",
"order": 40,
"action": {
"type": "view",
"path": "daede/log"
}
}
}
@@ -0,0 +1,91 @@
{
"luci-app-daede": {
"description": "Grant access to dae/daed configuration",
"read": {
"file": {
"/etc/init.d/dae": [ "read" ],
"/etc/init.d/daed": [ "read" ],
"/etc/config/daede": [ "read" ],
"/etc/dae/config.dae": [ "read" ],
"/etc/dae/example.dae": [ "read" ],
"/var/log/dae/dae.log": [ "read" ],
"/var/log/daed/daed.log": [ "read" ],
"/usr/share/v2ray/geoip.dat": [ "read" ],
"/usr/share/v2ray/geosite.dat": [ "read" ],
"/sys/kernel/btf/vmlinux": [ "read" ],
"/usr/lib/debug/boot/vmlinux": [ "read" ],
"/run/netns/daens": [ "read" ],
"/tmp/luci-app-daede.geoip.log": [ "read" ],
"/tmp/luci-app-daede.geosite.log": [ "read" ],
"/tmp/luci-app-daede.pkg-dae.log": [ "read" ],
"/tmp/luci-app-daede.pkg-daed.log": [ "read" ],
"/tmp/luci-app-daede.pkg-luci-app-daede.log": [ "read" ],
"/tmp/luci-app-daede.backup.log": [ "read" ],
"/bin/pidof dae": [ "exec" ],
"/bin/pidof daed": [ "exec" ],
"/usr/bin/dae validate -c /tmp/dae-validate.dae": [ "exec" ],
"/usr/bin/dae --version": [ "exec" ],
"/bin/rm -f /tmp/dae-validate.dae": [ "exec" ],
"/usr/share/luci-app-daede/pkg-info.sh dae": [ "exec" ],
"/usr/share/luci-app-daede/pkg-info.sh daed": [ "exec" ],
"/usr/share/luci-app-daede/pkg-info.sh luci-app-daede": [ "exec" ],
"/usr/share/luci-app-daede/refresh-index.sh": [ "exec" ],
"/usr/share/luci-app-daede/proxy-check.sh": [ "exec" ],
"/usr/share/luci-app-daede/config-backup.sh export": [ "exec" ]
},
"ubus": {
"service": [ "list" ]
},
"uci": [ "dae", "daed", "daede" ]
},
"write": {
"file": {
"/etc/dae/config.dae": [ "write" ],
"/tmp/dae-validate.dae": [ "write" ],
"/tmp/daede-import.b64": [ "write" ],
"/tmp/daede-sub.txt": [ "write" ],
"/tmp/daede-daedsub-*": [ "write" ],
"/bin/rm -f /tmp/daede-daedsub-*": [ "exec" ],
"/usr/share/luci-app-daede/config-backup.sh import": [ "exec" ],
"/usr/share/luci-app-daede/gen-dae-config.sh write-sub *": [ "exec" ],
"/usr/share/luci-app-daede/gen-dae-config.sh delete-sub *": [ "exec" ],
"/etc/config/daede": [ "write" ],
"/var/log/dae/dae.log": [ "write" ],
"/var/log/daed/daed.log": [ "write" ],
"/etc/init.d/dae start": [ "exec" ],
"/etc/init.d/dae stop": [ "exec" ],
"/etc/init.d/dae restart": [ "exec" ],
"/etc/init.d/dae enable": [ "exec" ],
"/etc/init.d/dae disable": [ "exec" ],
"/etc/init.d/dae hot_reload": [ "exec" ],
"/etc/init.d/daed start": [ "exec" ],
"/etc/init.d/daed stop": [ "exec" ],
"/etc/init.d/daed restart": [ "exec" ],
"/etc/init.d/daed enable": [ "exec" ],
"/etc/init.d/daed disable": [ "exec" ],
"/sbin/uci set dae.config.enabled=*": [ "exec" ],
"/sbin/uci set daed.config.enabled=*": [ "exec" ],
"/sbin/uci set daede.config=daede": [ "exec" ],
"/sbin/uci set daede.config.active_backend=*": [ "exec" ],
"/sbin/uci commit dae": [ "exec" ],
"/sbin/uci commit daed": [ "exec" ],
"/sbin/uci commit daede": [ "exec" ],
"/sbin/ip netns del daens": [ "exec" ],
"/usr/share/luci-app-daede/update-geo.sh geoip": [ "exec" ],
"/usr/share/luci-app-daede/update-geo.sh geosite": [ "exec" ],
"/usr/share/luci-app-daede/geo-cron.sh enable": [ "exec" ],
"/usr/share/luci-app-daede/geo-cron.sh disable": [ "exec" ],
"/usr/share/luci-app-daede/update-pkg.sh dae": [ "exec" ],
"/usr/share/luci-app-daede/update-pkg.sh daed": [ "exec" ],
"/usr/share/luci-app-daede/update-pkg.sh luci-app-daede": [ "exec" ],
"/usr/share/luci-app-daede/fetch-clash-yaml.sh *": [ "exec" ],
"/usr/share/luci-app-daede/gen-dae-config.sh generate": [ "exec" ],
"/usr/share/luci-app-daede/gen-dae-config.sh import": [ "exec" ],
"/usr/share/luci-app-daede/daed-sub-update.sh": [ "exec" ],
"/usr/share/luci-app-daede/daed-sub-cron.sh enable": [ "exec" ],
"/usr/share/luci-app-daede/daed-sub-cron.sh disable": [ "exec" ]
},
"uci": [ "dae", "daed", "daede" ]
}
}
}