mirror of
https://github.com/andrey271192/Keenetic-Split-DNS.git
synced 2026-09-20 11:55:36 +00:00
Initial release: install, uninstall, SmartDNS, web UI
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
208
scripts/api.sh
Executable file
208
scripts/api.sh
Executable file
@@ -0,0 +1,208 @@
|
||||
#!/bin/sh
|
||||
# CGI API helpers for Keenetic-Split-DNS
|
||||
|
||||
KSD_ETC="${KSD_ETC:-/opt/etc/keenetic-split-dns}"
|
||||
CONFIG="${KSD_ETC}/config.yaml"
|
||||
TOKEN_FILE="${KSD_ETC}/token"
|
||||
LOG_FILE="${KSD_ETC}/apply.log"
|
||||
|
||||
json_escape() {
|
||||
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' '
|
||||
}
|
||||
|
||||
api_send_json() {
|
||||
code="${1:-200}"
|
||||
body="$2"
|
||||
printf 'Status: %s\r\n' "$code"
|
||||
printf 'Content-Type: application/json; charset=utf-8\r\n'
|
||||
printf 'Cache-Control: no-store\r\n'
|
||||
printf '\r\n'
|
||||
printf '%s' "$body"
|
||||
}
|
||||
|
||||
api_unauthorized() {
|
||||
api_send_json "401" '{"ok":false,"error":"unauthorized"}'
|
||||
exit 0
|
||||
}
|
||||
|
||||
api_check_auth() {
|
||||
expected=""
|
||||
[ -f "$TOKEN_FILE" ] && expected="$(cat "$TOKEN_FILE" | tr -d '\n\r ')"
|
||||
[ -n "$expected" ] || return 0
|
||||
|
||||
got=""
|
||||
case "$HTTP_AUTHORIZATION" in
|
||||
Bearer*) got="${HTTP_AUTHORIZATION#Bearer }" ;;
|
||||
esac
|
||||
[ -z "$got" ] && got="$HTTP_X_KSD_TOKEN"
|
||||
[ -z "$got" ] && got="$HTTP_X_DNS_SPLIT_TOKEN"
|
||||
|
||||
if [ "$got" != "$expected" ]; then
|
||||
api_unauthorized
|
||||
fi
|
||||
}
|
||||
|
||||
api_read_body() {
|
||||
if [ -n "$CONTENT_LENGTH" ] && [ "$CONTENT_LENGTH" -gt 0 ] 2>/dev/null; then
|
||||
dd bs=1 count="$CONTENT_LENGTH" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
smartdns_running() {
|
||||
if pidof smartdns >/dev/null 2>&1; then
|
||||
echo "true"
|
||||
else
|
||||
echo "false"
|
||||
fi
|
||||
}
|
||||
|
||||
smartdns_pid() {
|
||||
pidof smartdns 2>/dev/null | awk '{print $1}'
|
||||
}
|
||||
|
||||
count_domains() {
|
||||
n=0
|
||||
if [ -d "${KSD_ETC}/domain-sets" ]; then
|
||||
for f in "${KSD_ETC}"/domain-sets/*.txt; do
|
||||
[ -f "$f" ] || continue
|
||||
n=$((n + $(grep -cve '^\s*$' -e '^\s*#' "$f" 2>/dev/null || echo 0)))
|
||||
done
|
||||
fi
|
||||
echo "$n"
|
||||
}
|
||||
|
||||
api_status() {
|
||||
lan="$(grep '^lan_ip:' "$CONFIG" 2>/dev/null | sed 's/.*"\([^"]*\)".*/\1/')"
|
||||
port="$(grep '^web_port:' "$CONFIG" 2>/dev/null | awk '{print $2}')"
|
||||
[ -n "$port" ] || port="3200"
|
||||
domains="$(count_domains)"
|
||||
running="$(smartdns_running)"
|
||||
pid="$(smartdns_pid)"
|
||||
last_apply=""
|
||||
[ -f "$LOG_FILE" ] && last_apply="$(tail -1 "$LOG_FILE" 2>/dev/null | json_escape)"
|
||||
|
||||
api_send_json "200" "{\"ok\":true,\"smartdns\":{\"running\":${running},\"pid\":\"${pid}\"},\"domains\":${domains},\"lan_ip\":\"${lan}\",\"web_port\":${port},\"url\":\"http://${lan}:${port}\"}"
|
||||
}
|
||||
|
||||
api_get_config_raw() {
|
||||
if [ ! -f "$CONFIG" ]; then
|
||||
api_send_json "404" '{"ok":false,"error":"config not found"}'
|
||||
exit 0
|
||||
fi
|
||||
printf 'Status: 200\r\n'
|
||||
printf 'Content-Type: application/x-yaml; charset=utf-8\r\n'
|
||||
printf 'Cache-Control: no-store\r\n'
|
||||
printf '\r\n'
|
||||
cat "$CONFIG"
|
||||
exit 0
|
||||
}
|
||||
|
||||
api_save_config() {
|
||||
body="$(api_read_body)"
|
||||
[ -n "$body" ] || { api_send_json "400" '{"ok":false,"error":"empty body"}'; exit 0; }
|
||||
|
||||
# Accept raw YAML body or {"content":"..."}
|
||||
case "$body" in
|
||||
\{*)
|
||||
content="$(printf '%s' "$body" | sed -n 's/.*"content"[[:space:]]*:[[:space:]]*"\(.*\)".*/\1/p' | sed 's/\\n/\n/g; s/\\"/"/g')"
|
||||
;;
|
||||
*)
|
||||
content="$body"
|
||||
;;
|
||||
esac
|
||||
|
||||
[ -n "$content" ] || { api_send_json "400" '{"ok":false,"error":"no content"}'; exit 0; }
|
||||
cp "$CONFIG" "${CONFIG}.bak" 2>/dev/null || true
|
||||
printf '%s\n' "$content" > "$CONFIG"
|
||||
api_send_json "200" '{"ok":true,"message":"saved"}'
|
||||
}
|
||||
|
||||
api_reload() {
|
||||
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||
if "$SCRIPT_DIR/apply.sh" >>"$LOG_FILE" 2>&1; then
|
||||
ts="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "${ts} reload OK" >>"$LOG_FILE"
|
||||
api_send_json "200" '{"ok":true,"message":"applied"}'
|
||||
else
|
||||
api_send_json "500" '{"ok":false,"error":"apply failed"}'
|
||||
fi
|
||||
}
|
||||
|
||||
api_test_dns() {
|
||||
domain="${QUERY_STRING#*domain=}"
|
||||
domain="${domain%%&*}"
|
||||
domain="$(printf '%s' "$domain" | sed 's/%\([0-9A-F][0-9A-F]\)/\\x\1/g' | xargs -0 printf '%b' 2>/dev/null || echo "$domain")"
|
||||
[ -n "$domain" ] || domain="vk.com"
|
||||
rtype="A"
|
||||
case "$QUERY_STRING" in
|
||||
*type=AAAA*) rtype="AAAA" ;;
|
||||
*type=HTTPS*) rtype="HTTPS" ;;
|
||||
esac
|
||||
|
||||
if command -v dig >/dev/null 2>&1; then
|
||||
dns_port="$(grep '^dns_port:' "$CONFIG" 2>/dev/null | awk '{print $2}')"
|
||||
[ -n "$dns_port" ] || dns_port="53"
|
||||
start_ms="$(date +%s)"
|
||||
out="$(dig @"127.0.0.1" -p "$dns_port" "$domain" "$rtype" +time=3 +tries=1 2>&1)" || out="dig failed"
|
||||
end_ms="$(date +%s)"
|
||||
ms=$(( (end_ms - start_ms) * 1000 ))
|
||||
esc="$(printf '%s' "$out" | json_escape)"
|
||||
api_send_json "200" "{\"ok\":true,\"domain\":\"${domain}\",\"type\":\"${rtype}\",\"ms\":${ms},\"output\":\"${esc}\"}"
|
||||
else
|
||||
api_send_json "503" '{"ok":false,"error":"dig not installed"}'
|
||||
fi
|
||||
}
|
||||
|
||||
api_domains_list() {
|
||||
# Build simple JSON array from domain sets
|
||||
echo -n '{"ok":true,"domains":['
|
||||
first=1
|
||||
if [ -d "${KSD_ETC}/domain-sets" ]; then
|
||||
for f in "${KSD_ETC}"/domain-sets/*.txt; do
|
||||
[ -f "$f" ] || continue
|
||||
group="$(basename "$f" .txt)"
|
||||
upstream="$(awk -v g="$group" '$0 ~ "^ " g ":$" {f=1} f && $0 ~ "upstream:" {print $2; exit}' "$CONFIG" 2>/dev/null)"
|
||||
while IFS= read -r d || [ -n "$d" ]; do
|
||||
d="$(echo "$d" | tr -d '\r')"
|
||||
[ -z "$d" ] && continue
|
||||
case "$d" in \#*) continue ;; esac
|
||||
[ "$first" -eq 1 ] || echo -n ','
|
||||
first=0
|
||||
printf '{"domain":"%s","group":"%s","upstream":"%s"}' "$d" "$group" "${upstream:-yandex-dot}"
|
||||
done < "$f"
|
||||
done
|
||||
fi
|
||||
echo ']}'
|
||||
}
|
||||
|
||||
api_route() {
|
||||
path="${PATH_INFO:-/}"
|
||||
path="${path#/}"
|
||||
method="${REQUEST_METHOD:-GET}"
|
||||
|
||||
api_check_auth
|
||||
|
||||
case "$path" in
|
||||
status|"") api_status ;;
|
||||
config)
|
||||
case "$method" in
|
||||
GET) api_get_config_raw ;;
|
||||
POST) api_save_config ;;
|
||||
*) api_send_json "405" '{"ok":false,"error":"method"}' ;;
|
||||
esac
|
||||
;;
|
||||
domains)
|
||||
body="$(api_domains_list)"
|
||||
api_send_json "200" "$body"
|
||||
;;
|
||||
reload|apply)
|
||||
api_reload
|
||||
;;
|
||||
test)
|
||||
api_test_dns
|
||||
;;
|
||||
*)
|
||||
api_send_json "404" '{"ok":false,"error":"not found"}'
|
||||
;;
|
||||
esac
|
||||
}
|
||||
28
scripts/apply.sh
Executable file
28
scripts/apply.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
# Apply configuration: compile, restart services, netfilter
|
||||
|
||||
set -e
|
||||
|
||||
KSD_ETC="${KSD_ETC:-/opt/etc/keenetic-split-dns}"
|
||||
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||
|
||||
"$SCRIPT_DIR/compile-config.sh"
|
||||
|
||||
if [ -x /opt/etc/init.d/S98smartdns ]; then
|
||||
/opt/etc/init.d/S98smartdns restart || /opt/etc/init.d/S98smartdns start
|
||||
elif [ -x /opt/etc/init.d/S56smartdns ]; then
|
||||
/opt/etc/init.d/S56smartdns restart || true
|
||||
fi
|
||||
|
||||
if [ -x /opt/etc/init.d/S99ksd-web ]; then
|
||||
/opt/etc/init.d/S99ksd-web restart || /opt/etc/init.d/S99ksd-web start
|
||||
fi
|
||||
|
||||
NF="/opt/etc/ndm/netfilter.d/010-keenetic-split-dns.sh"
|
||||
if [ -x "$NF" ]; then
|
||||
"$NF" restart 2>/dev/null || true
|
||||
ndm -p netfilter restart 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "Applied keenetic-split-dns configuration."
|
||||
exit 0
|
||||
148
scripts/compile-config.sh
Executable file
148
scripts/compile-config.sh
Executable file
@@ -0,0 +1,148 @@
|
||||
#!/bin/sh
|
||||
# YAML config -> SmartDNS conf + domain sets + lighttpd bind
|
||||
|
||||
set -e
|
||||
|
||||
KSD_ETC="${KSD_ETC:-/opt/etc/keenetic-split-dns}"
|
||||
KSD_SHARE="${KSD_SHARE:-/opt/share/keenetic-split-dns}"
|
||||
CONFIG="${KSD_ETC}/config.yaml"
|
||||
OUT_SMART="${KSD_ETC}/smartdns.conf"
|
||||
OUT_LIGHT="${KSD_ETC}/lighttpd.conf"
|
||||
DOMAIN_DIR="${KSD_ETC}/domain-sets"
|
||||
HEAD="${KSD_SHARE}/smartdns/smartdns.conf.head"
|
||||
|
||||
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||
DETECT="${SCRIPT_DIR}/detect-lan.sh"
|
||||
|
||||
[ -f "$CONFIG" ] || { echo "Missing $CONFIG" >&2; exit 1; }
|
||||
mkdir -p "$DOMAIN_DIR" "$(dirname "$OUT_SMART")"
|
||||
|
||||
# --- simple YAML getters (key: value / nested under upstreams:) ---
|
||||
yaml_val() {
|
||||
key="$1"
|
||||
awk -v k="$key" '
|
||||
$0 ~ "^" k ": " {
|
||||
v = $0; sub("^" k ": ", "", v)
|
||||
gsub(/^["'\'']|["'\'']$/, "", v)
|
||||
print v; exit
|
||||
}
|
||||
' "$CONFIG"
|
||||
}
|
||||
|
||||
yaml_upstream_field() {
|
||||
uid="$1"
|
||||
field="$2"
|
||||
awk -v id="$uid" -v f="$field" '
|
||||
$0 ~ "^ " id ":$" { inb=1; next }
|
||||
inb && $0 ~ "^ [a-zA-Z0-9_-]+:$" { exit }
|
||||
inb && $0 ~ "^ " f ": " {
|
||||
v=$0; sub("^ " f ": ", "", v)
|
||||
gsub(/^["'\'']|["'\'']$/, "", v)
|
||||
print v; exit
|
||||
}
|
||||
' "$CONFIG"
|
||||
}
|
||||
|
||||
yaml_group_field() {
|
||||
gid="$1"
|
||||
field="$2"
|
||||
awk -v id="$gid" -v f="$field" '
|
||||
$0 ~ "^ " id ":$" { inb=1; next }
|
||||
inb && $0 ~ "^ [a-zA-Z0-9_-]+:$" { exit }
|
||||
inb && $0 ~ "^ " f ": " {
|
||||
v=$0; sub("^ " f ": ", "", v)
|
||||
gsub(/^["'\'']|["'\'']$/, "", v)
|
||||
print v; exit
|
||||
}
|
||||
' "$CONFIG"
|
||||
}
|
||||
|
||||
LAN_IP="$(yaml_val lan_ip)"
|
||||
[ -n "$LAN_IP" ] || LAN_IP="$("$DETECT" lan)"
|
||||
WEB_PORT="$(yaml_val web_port)"
|
||||
[ -n "$WEB_PORT" ] || WEB_PORT="3200"
|
||||
DNS_PORT="$(yaml_val dns_port)"
|
||||
[ -n "$DNS_PORT" ] || DNS_PORT="53"
|
||||
DNS_LISTEN="$(yaml_val dns_listen)"
|
||||
[ -n "$DNS_LISTEN" ] || DNS_LISTEN="0.0.0.0"
|
||||
CACHE_TTL="$(yaml_val cache_ttl_max)"
|
||||
[ -n "$CACHE_TTL" ] || CACHE_TTL="3600"
|
||||
DEFAULT_UP="$(yaml_val default_upstream)"
|
||||
[ -n "$DEFAULT_UP" ] || DEFAULT_UP="isp-default"
|
||||
LOG_Q="$(yaml_val log_queries)"
|
||||
|
||||
# --- SmartDNS header ---
|
||||
{
|
||||
[ -f "$HEAD" ] && cat "$HEAD"
|
||||
echo "bind ${DNS_LISTEN}:${DNS_PORT}"
|
||||
echo "cache-persist no"
|
||||
echo "rr-ttl-max ${CACHE_TTL}"
|
||||
if [ "$LOG_Q" = "true" ] || [ "$LOG_Q" = "1" ]; then
|
||||
echo "log-file /opt/var/log/keenetic-split-dns/smartdns.log"
|
||||
echo "log-size 256k"
|
||||
fi
|
||||
echo ""
|
||||
} > "$OUT_SMART"
|
||||
|
||||
# --- upstream servers ---
|
||||
list_upstream_ids() {
|
||||
awk '/^upstreams:$/,/^[^ ]/ { if ($0 ~ /^ [a-zA-Z0-9_-]+:$/) { gsub(/:$/,"",$1); print substr($1,3) } }' "$CONFIG" | head -20
|
||||
}
|
||||
|
||||
for uid in $(list_upstream_ids); do
|
||||
typ="$(yaml_upstream_field "$uid" type)"
|
||||
addr="$(yaml_upstream_field "$uid" address)"
|
||||
port="$(yaml_upstream_field "$uid" port)"
|
||||
sni="$(yaml_upstream_field "$uid" sni)"
|
||||
[ -n "$port" ] || port="53"
|
||||
|
||||
case "$typ" in
|
||||
dot)
|
||||
host="${sni:-$addr}"
|
||||
echo "server-tls ${host} -address ${addr}:${port} -host-name ${host} -group ${uid} -no-check-certificate" >> "$OUT_SMART"
|
||||
;;
|
||||
doh)
|
||||
url="$(yaml_upstream_field "$uid" url)"
|
||||
[ -n "$url" ] || url="https://${addr}/dns-query"
|
||||
echo "server-https ${url} -group ${uid}" >> "$OUT_SMART"
|
||||
;;
|
||||
*)
|
||||
if [ "$addr" = "auto" ]; then
|
||||
addr="$("$DETECT" isp)"
|
||||
fi
|
||||
echo "server ${addr}:${port} -group ${uid}" >> "$OUT_SMART"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "" >> "$OUT_SMART"
|
||||
|
||||
# --- domain groups ---
|
||||
list_group_ids() {
|
||||
awk '/^domain_groups:$/,/^[^ ]/ { if ($0 ~ /^ [a-zA-Z0-9_-]+:$/) { gsub(/:$/,"",$1); print substr($1,3) } }' "$CONFIG"
|
||||
}
|
||||
|
||||
for gid in $(list_group_ids); do
|
||||
upstream="$(yaml_group_field "$gid" upstream)"
|
||||
dset="$(yaml_group_field "$gid" domain_set)"
|
||||
[ -n "$dset" ] || dset="${gid}.txt"
|
||||
dpath="${DOMAIN_DIR}/${dset}"
|
||||
if [ ! -f "$dpath" ] && [ -f "${KSD_SHARE}/domain-sets/${dset}" ]; then
|
||||
cp "${KSD_SHARE}/domain-sets/${dset}" "$dpath"
|
||||
fi
|
||||
[ -f "$dpath" ] || continue
|
||||
echo "domain-set -name ${gid} -file ${dpath}" >> "$OUT_SMART"
|
||||
echo "domain-rules /domain-set:${gid}/ -nameserver ${upstream} -speed-check-mode none" >> "$OUT_SMART"
|
||||
done
|
||||
|
||||
echo "nameserver ${DEFAULT_UP}" >> "$OUT_SMART"
|
||||
echo "default-nameserver ${DEFAULT_UP}" >> "$OUT_SMART"
|
||||
|
||||
# --- lighttpd ---
|
||||
LIGHT_SRC="${KSD_SHARE}/lighttpd/lighttpd.conf"
|
||||
if [ -f "$LIGHT_SRC" ]; then
|
||||
sed "s/LAN_IP_PLACEHOLDER/${LAN_IP}/g; s/server.port = 3200/server.port = ${WEB_PORT}/" "$LIGHT_SRC" > "$OUT_LIGHT"
|
||||
fi
|
||||
|
||||
echo "Compiled: $OUT_SMART"
|
||||
exit 0
|
||||
42
scripts/detect-lan.sh
Executable file
42
scripts/detect-lan.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/bin/sh
|
||||
# Detect Keenetic LAN IP (br0 / ndm)
|
||||
|
||||
set -e
|
||||
|
||||
detect_lan_ip() {
|
||||
ip=""
|
||||
if command -v ip >/dev/null 2>&1; then
|
||||
ip="$(ip -4 addr show br0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1 | head -1)"
|
||||
fi
|
||||
if [ -z "$ip" ] && [ -f /proc/net/route ]; then
|
||||
ip="$(awk '$1=="br0" && $3=="00000000" {print; exit}' /proc/net/route 2>/dev/null | \
|
||||
awk '{printf "%d.%d.%d.%d\n", strtonum("0x" substr($2,7,2)), strtonum("0x" substr($2,5,2)), strtonum("0x" substr($2,3,2)), strtonum("0x" substr($2,1,2))}' 2>/dev/null || true)"
|
||||
fi
|
||||
if [ -z "$ip" ] && command -v ndm >/dev/null 2>&1; then
|
||||
ip="$(ndm -p ip address 2>/dev/null | awk '/inet / && /br0/ {gsub(/\/.*/,"",$2); print $2; exit}')"
|
||||
fi
|
||||
if [ -z "$ip" ]; then
|
||||
ip="192.168.1.1"
|
||||
fi
|
||||
printf '%s' "$ip"
|
||||
}
|
||||
|
||||
detect_isp_dns() {
|
||||
dns=""
|
||||
if command -v ndm >/dev/null 2>&1; then
|
||||
dns="$(ndm -p show dns-proxy 2>/dev/null | awk '/server/ {print $3; exit}' | tr -d "'\")"
|
||||
fi
|
||||
if [ -z "$dns" ]; then
|
||||
dns="$(grep '^nameserver' /etc/resolv.conf 2>/dev/null | awk '{print $2}' | head -1)"
|
||||
fi
|
||||
if [ -z "$dns" ]; then
|
||||
dns="$(detect_lan_ip)"
|
||||
fi
|
||||
printf '%s' "$dns"
|
||||
}
|
||||
|
||||
case "${1:-lan}" in
|
||||
lan) detect_lan_ip ;;
|
||||
isp) detect_isp_dns ;;
|
||||
*) detect_lan_ip ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user