mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-13 20:08:05 +08:00
🌴 Sync 2026-10-16 01:08:07
This commit is contained in:
1 parent
f95bba4fb6
commit
16f740db5d
186 files changed
+9575
-5557
No files matched your search
@@ -1,8 +1,9 @@
|
||||
#!/bin/sh
|
||||
ACME_HOME="/etc/minigate/acme/data"
|
||||
ACME_HOME="${MINIGATE_ACME_HOME:-/etc/minigate/acme/data}"
|
||||
ACME_BIN="${ACME_HOME}/acme.sh"
|
||||
CERT_DIR="/etc/minigate/certs"
|
||||
LOGFILE="/var/log/minigate-acme.log"
|
||||
CERT_DIR="${MINIGATE_CERT_DIR:-/etc/minigate/certs}"
|
||||
LOGFILE="${MINIGATE_ACME_LOG:-/var/log/minigate-acme.log}"
|
||||
PROXY_SCRIPT="${MINIGATE_PROXY_SCRIPT:-/usr/lib/minigate/proxy.sh}"
|
||||
|
||||
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') [ACME] $*" >> "$LOGFILE"; }
|
||||
|
||||
@@ -32,7 +33,7 @@ keep_existing_cert_ok() {
|
||||
uci -q set minigate.acme.cert_expiry="$expiry"
|
||||
uci -q set minigate.acme.last_error="$reason"
|
||||
uci commit minigate
|
||||
[ -f /usr/lib/minigate/proxy.sh ] && /bin/sh /usr/lib/minigate/proxy.sh reload 2>/dev/null
|
||||
[ -f "$PROXY_SCRIPT" ] && /bin/sh "$PROXY_SCRIPT" reload 2>/dev/null
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
@@ -144,7 +145,7 @@ do_issue() {
|
||||
local base=$(echo "$domain" | sed 's/^\*\.//')
|
||||
[ ! -e "${CERT_DIR}/${base}" ] && ln -sf "${safe}" "${CERT_DIR}/${base}"
|
||||
fi
|
||||
[ -f /usr/lib/minigate/proxy.sh ] && /bin/sh /usr/lib/minigate/proxy.sh reload 2>/dev/null
|
||||
[ -f "$PROXY_SCRIPT" ] && /bin/sh "$PROXY_SCRIPT" reload 2>/dev/null
|
||||
return 0
|
||||
else
|
||||
keep_existing_cert_ok "$domain" "签发失败 (exit=$ret)" && return 0
|
||||
@@ -152,6 +153,28 @@ do_issue() {
|
||||
fi
|
||||
}
|
||||
|
||||
do_renew() { do_install || return 1; log "续期..."; "$ACME_BIN" --home "$ACME_HOME" --renew-all --server letsencrypt >> "$LOGFILE" 2>&1; log "完成"; }
|
||||
certs_fingerprint() {
|
||||
cat "$CERT_DIR"/*/fullchain.pem 2>/dev/null | md5sum | cut -d' ' -f1
|
||||
}
|
||||
|
||||
do_renew() {
|
||||
local before domain cert
|
||||
do_install || return 1
|
||||
before=$(certs_fingerprint)
|
||||
log "续期..."
|
||||
"$ACME_BIN" --home "$ACME_HOME" --renew-all --server letsencrypt >> "$LOGFILE" 2>&1
|
||||
log "完成"
|
||||
# acme.sh 只替换证书文件,运行中的 Nginx 必须重载才会使用新证书。
|
||||
[ "$(certs_fingerprint)" = "$before" ] && return 0
|
||||
log "证书已更新,重载反向代理"
|
||||
domain=$(uci -q get minigate.acme.last_domain)
|
||||
cert="$(cert_path_for_domain "$domain")"
|
||||
if [ -n "$domain" ] && [ -f "$cert" ]; then
|
||||
uci -q set minigate.acme.cert_expiry="$(cert_expiry "$cert")"
|
||||
uci commit minigate
|
||||
fi
|
||||
[ -f "$PROXY_SCRIPT" ] && /bin/sh "$PROXY_SCRIPT" reload 2>/dev/null
|
||||
return 0
|
||||
}
|
||||
|
||||
case "${1:-issue}" in install) do_install;; issue) do_issue "$2";; renew) do_renew;; *) echo "用法: $0 {install|issue [域名]|renew}";; esac
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
#!/bin/sh
|
||||
# LuCI 的 on_after_commit 在浏览器提交配置之前执行。这里先记录当前配置摘要,
|
||||
# 后台等配置文件变化(最多 WAIT 秒)后再应用,避免后台脚本读到旧配置。
|
||||
CONFIG="${MINIGATE_CONFIG_FILE:-/etc/config/minigate}"
|
||||
WAIT="${MINIGATE_COMMIT_WAIT:-15}"
|
||||
INIT="${MINIGATE_INIT:-/etc/init.d/minigate}"
|
||||
LIB_DIR="${MINIGATE_LIB_DIR:-/usr/lib/minigate}"
|
||||
|
||||
config_digest() {
|
||||
md5sum "$CONFIG" 2>/dev/null | cut -d' ' -f1
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
restart|stop|reload|ddns|proxy) ;;
|
||||
*)
|
||||
echo "用法: $0 {restart|stop|reload|ddns|proxy}" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
before=$(config_digest)
|
||||
(
|
||||
i=0
|
||||
while [ "$i" -lt "$WAIT" ] && [ "$(config_digest)" = "$before" ]; do
|
||||
sleep 1
|
||||
i=$((i + 1))
|
||||
done
|
||||
case "$1" in
|
||||
restart|stop|reload) exec "$INIT" "$1" ;;
|
||||
ddns) exec /bin/sh "$LIB_DIR/ddns.sh" force ;;
|
||||
proxy) exec /bin/sh "$LIB_DIR/proxy.sh" reload ;;
|
||||
esac
|
||||
) </dev/null >/dev/null 2>&1 &
|
||||
@@ -1,11 +1,29 @@
|
||||
#!/bin/sh
|
||||
LOGFILE="/var/log/minigate-ddns.log"
|
||||
LOGFILE="${MINIGATE_DDNS_LOG:-/var/log/minigate-ddns.log}"
|
||||
HISTORY_DIR="${MINIGATE_HISTORY_DIR:-/etc/minigate/ddns-history}"
|
||||
HISTORY_WINDOW="${MINIGATE_HISTORY_WINDOW:-86400}"
|
||||
CACHE_DIR="${MINIGATE_CACHE_DIR:-/tmp}"
|
||||
SINGBOX_CONFIG="${MINIGATE_SINGBOX_CONFIG:-/opt/open-box/etc/config.json}"
|
||||
CLASH_API_URL="${MINIGATE_CLASH_API_URL:-http://127.0.0.1:9095}"
|
||||
LOCK_FILE="${MINIGATE_DDNS_LOCK:-/var/lock/minigate-ddns.lock}"
|
||||
|
||||
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') [DDNS] $*" >> "$LOGFILE"; }
|
||||
trim_log() { [ -f "$LOGFILE" ] && [ "$(wc -l < "$LOGFILE")" -gt 300 ] && { tail -n 150 "$LOGFILE" > "${LOGFILE}.tmp"; mv "${LOGFILE}.tmp" "$LOGFILE"; }; }
|
||||
|
||||
# 定时、保存页面和手动同步可能同时运行,串行化避免重复创建 Cloudflare 记录。
|
||||
# 定时检查遇到正在运行的同步直接跳过,强制同步排队等待。
|
||||
lock_ddns() {
|
||||
command -v flock >/dev/null 2>&1 || return 0
|
||||
mkdir -p "$(dirname "$LOCK_FILE")" 2>/dev/null
|
||||
touch "$LOCK_FILE" 2>/dev/null || return 0
|
||||
exec 7>"$LOCK_FILE"
|
||||
if [ "$1" = "wait" ]; then
|
||||
flock -x 7
|
||||
else
|
||||
flock -n 7
|
||||
fi
|
||||
}
|
||||
|
||||
history_now() {
|
||||
if [ -n "${MINIGATE_HISTORY_NOW:-}" ]; then
|
||||
printf '%s\n' "$MINIGATE_HISTORY_NOW"
|
||||
@@ -83,6 +101,55 @@ record_ip_history() {
|
||||
fi
|
||||
}
|
||||
|
||||
# 只清运行中 DNS 缓存,不重启 sing-box、dnsmasq 或代理链路。
|
||||
flush_singbox_dns_cache() {
|
||||
local secret http_code
|
||||
[ -r "$SINGBOX_CONFIG" ] || return 2
|
||||
command -v jsonfilter >/dev/null 2>&1 || return 2
|
||||
command -v curl >/dev/null 2>&1 || return 2
|
||||
|
||||
secret=$(jsonfilter -i "$SINGBOX_CONFIG" -e '@.experimental.clash_api.secret' 2>/dev/null)
|
||||
if [ -n "$secret" ]; then
|
||||
http_code=$(printf 'Authorization: Bearer %s\n' "$secret" | \
|
||||
curl -sS -o /dev/null -w '%{http_code}' --connect-timeout 1 --max-time 3 \
|
||||
-X POST -H @- "${CLASH_API_URL}/cache/dns/flush" 2>/dev/null)
|
||||
else
|
||||
http_code=$(curl -sS -o /dev/null -w '%{http_code}' --connect-timeout 1 --max-time 3 \
|
||||
-X POST "${CLASH_API_URL}/cache/dns/flush" 2>/dev/null)
|
||||
fi
|
||||
case "$http_code" in 2??) return 0 ;; *) return 1 ;; esac
|
||||
}
|
||||
|
||||
send_hup() {
|
||||
kill -HUP "$@"
|
||||
}
|
||||
|
||||
flush_dnsmasq_cache() {
|
||||
local pids
|
||||
command -v pidof >/dev/null 2>&1 || return 2
|
||||
pids=$(pidof dnsmasq 2>/dev/null)
|
||||
[ -n "$pids" ] || return 2
|
||||
send_hup $pids >/dev/null 2>&1
|
||||
}
|
||||
|
||||
refresh_local_dns_caches() {
|
||||
local domain="$1" result
|
||||
if flush_singbox_dns_cache; then
|
||||
log "$domain: sing-box DNS缓存已刷新"
|
||||
else
|
||||
result=$?
|
||||
[ "$result" = "2" ] || log "$domain: sing-box DNS缓存刷新失败(接口不可用)"
|
||||
fi
|
||||
|
||||
if flush_dnsmasq_cache; then
|
||||
log "$domain: dnsmasq DNS缓存已刷新"
|
||||
else
|
||||
result=$?
|
||||
[ "$result" = "2" ] || log "$domain: dnsmasq DNS缓存刷新失败"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ====== IPv4 获取 ======
|
||||
get_ip4_iface() {
|
||||
local ip=""
|
||||
@@ -105,13 +172,13 @@ get_ip4_url() {
|
||||
[ -z "$port" ] && port="80"
|
||||
|
||||
# 方法1: 直连
|
||||
ip=$(curl -4 -s --connect-timeout 3 "$url" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||||
ip=$(curl -4 -s --connect-timeout 3 --max-time 8 "$url" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||||
[ -n "$ip" ] && { echo "$ip"; return; }
|
||||
|
||||
# 方法2: DNS 解析后用 Host 头直连(绕过代理)
|
||||
local resolved=$(nslookup "$host" 223.5.5.5 2>/dev/null | grep -A1 'Name:' | grep 'Address:' | head -1 | awk '{print $2}')
|
||||
if [ -n "$resolved" ]; then
|
||||
ip=$(curl -4 -s --connect-timeout 3 -H "Host: ${host}" "http://${resolved}:${port}${path}" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||||
ip=$(curl -4 -s --connect-timeout 3 --max-time 8 -H "Host: ${host}" "http://${resolved}:${port}${path}" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||||
[ -n "$ip" ] && { echo "$ip"; return; }
|
||||
fi
|
||||
done
|
||||
@@ -136,7 +203,7 @@ get_ip6_url() {
|
||||
local port=$(echo "$url" | grep -oE ':[0-9]+' | head -1 | tr -d ':')
|
||||
|
||||
# 先尝试直连
|
||||
ip=$(curl -6 -s --connect-timeout 5 "$url" 2>/dev/null | grep -oE '([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}' | head -1)
|
||||
ip=$(curl -6 -s --connect-timeout 5 --max-time 10 "$url" 2>/dev/null | grep -oE '([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}' | head -1)
|
||||
|
||||
# 如果失败,DNS 解析后直连绕过代理
|
||||
if [ -z "$ip" ]; then
|
||||
@@ -145,60 +212,87 @@ get_ip6_url() {
|
||||
local scheme="http"
|
||||
echo "$url" | grep -q '^https' && scheme="https"
|
||||
[ -z "$port" ] && { [ "$scheme" = "https" ] && port="443" || port="80"; }
|
||||
ip=$(curl -6 -s --connect-timeout 5 --resolve "${host}:${port}:${resolved}" "$url" 2>/dev/null | grep -oE '([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}' | head -1)
|
||||
ip=$(curl -6 -s --connect-timeout 5 --max-time 10 --resolve "${host}:${port}:${resolved}" "$url" 2>/dev/null | grep -oE '([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}' | head -1)
|
||||
fi
|
||||
fi
|
||||
echo "$ip"
|
||||
}
|
||||
|
||||
# Token 经标准输入交给 curl,不出现在进程参数中。
|
||||
cf_api() {
|
||||
local token="$1"
|
||||
shift
|
||||
printf 'Authorization: Bearer %s\n' "$token" | \
|
||||
curl -s --connect-timeout 10 --max-time 20 -H @- -H "Content-Type: application/json" "$@" 2>/dev/null
|
||||
}
|
||||
|
||||
# ====== Cloudflare API:支持 A 和 AAAA 记录 ======
|
||||
cf_update() {
|
||||
local zone_id="$1" token="$2" domain="$3" ip="$4" record_type="$5"
|
||||
# record_type: A 或 AAAA
|
||||
[ -z "$record_type" ] && record_type="A"
|
||||
local resp match_resp rids rid keep_id result
|
||||
resp=$(curl -s --connect-timeout 10 -H "Authorization: Bearer $token" -H "Content-Type: application/json" \
|
||||
"https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records?type=${record_type}&name=${domain}" 2>/dev/null)
|
||||
local resp match_resp rids rid keep_id result changed=0
|
||||
resp=$(cf_api "$token" "https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records?type=${record_type}&name=${domain}")
|
||||
result=$(echo "$resp" | jsonfilter -e '$.success' 2>/dev/null)
|
||||
[ "$result" = "true" ] || { echo "false"; return 1; }
|
||||
|
||||
rids=$(echo "$resp" | jsonfilter -e '$.result[*].id' 2>/dev/null)
|
||||
if [ -n "$rids" ]; then
|
||||
match_resp=$(curl -s --connect-timeout 10 -H "Authorization: Bearer $token" -H "Content-Type: application/json" \
|
||||
"https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records?type=${record_type}&name=${domain}&content=${ip}" 2>/dev/null)
|
||||
match_resp=$(cf_api "$token" "https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records?type=${record_type}&name=${domain}&content=${ip}")
|
||||
result=$(echo "$match_resp" | jsonfilter -e '$.success' 2>/dev/null)
|
||||
[ "$result" = "true" ] || { echo "false"; return 1; }
|
||||
keep_id=$(echo "$match_resp" | jsonfilter -e '$.result[0].id' 2>/dev/null)
|
||||
|
||||
if [ -z "$keep_id" ]; then
|
||||
for rid in $rids; do keep_id="$rid"; break; done
|
||||
resp=$(curl -s --connect-timeout 10 -X PUT -H "Authorization: Bearer $token" -H "Content-Type: application/json" \
|
||||
resp=$(cf_api "$token" -X PUT \
|
||||
-d "{\"type\":\"$record_type\",\"name\":\"$domain\",\"content\":\"$ip\",\"ttl\":120,\"proxied\":false}" \
|
||||
"https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records/${keep_id}" 2>/dev/null)
|
||||
"https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records/${keep_id}")
|
||||
result=$(echo "$resp" | jsonfilter -e '$.success' 2>/dev/null)
|
||||
[ "$result" = "true" ] || { echo "false"; return 1; }
|
||||
changed=1
|
||||
fi
|
||||
|
||||
for rid in $rids; do
|
||||
[ "$rid" = "$keep_id" ] && continue
|
||||
resp=$(curl -s --connect-timeout 10 -X DELETE -H "Authorization: Bearer $token" -H "Content-Type: application/json" \
|
||||
"https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records/${rid}" 2>/dev/null)
|
||||
resp=$(cf_api "$token" -X DELETE "https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records/${rid}")
|
||||
result=$(echo "$resp" | jsonfilter -e '$.success' 2>/dev/null)
|
||||
[ "$result" = "true" ] || { echo "false"; return 1; }
|
||||
changed=1
|
||||
done
|
||||
echo "true"
|
||||
[ "$changed" = "1" ] && echo "changed" || echo "unchanged"
|
||||
else
|
||||
resp=$(curl -s --connect-timeout 10 -X POST -H "Authorization: Bearer $token" -H "Content-Type: application/json" \
|
||||
resp=$(cf_api "$token" -X POST \
|
||||
-d "{\"type\":\"$record_type\",\"name\":\"$domain\",\"content\":\"$ip\",\"ttl\":120,\"proxied\":false}" \
|
||||
"https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records" 2>/dev/null)
|
||||
echo "$resp" | jsonfilter -e '$.success' 2>/dev/null
|
||||
"https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records")
|
||||
result=$(echo "$resp" | jsonfilter -e '$.success' 2>/dev/null)
|
||||
[ "$result" = "true" ] && echo "changed" || { echo "false"; return 1; }
|
||||
fi
|
||||
}
|
||||
|
||||
ddns_interval() {
|
||||
local interval
|
||||
interval=$(uci -q get minigate.${1}.check_interval)
|
||||
case "$interval" in ''|*[!0-9]*) interval=300 ;; esac
|
||||
[ "$interval" -ge 300 ] 2>/dev/null || interval=300
|
||||
echo "$interval"
|
||||
}
|
||||
|
||||
# 定时任务每 5 分钟触发;未到本记录的检查间隔时跳过,留 30 秒余量吸收启动抖动。
|
||||
ddns_due() {
|
||||
local sec="$1" last now
|
||||
last=$(cat "${CACHE_DIR}/minigate_ddns_${sec}.checked" 2>/dev/null)
|
||||
case "$last" in ''|*[!0-9]*) return 0 ;; esac
|
||||
now=$(date +%s)
|
||||
[ "$now" -ge "$last" ] || return 0
|
||||
[ $((now - last + 30)) -ge "$(ddns_interval "$sec")" ]
|
||||
}
|
||||
|
||||
# 下一次实际执行的整 5 分钟时刻,与 ddns_due 的判断一致
|
||||
calc_next() {
|
||||
local interval="${1:-300}"
|
||||
local now=$(date +%s)
|
||||
local next=$((now + interval))
|
||||
local next=$(( (now + interval + 269) / 300 * 300 ))
|
||||
date -d "@${next}" '+%H:%M:%S' 2>/dev/null || echo "${interval}秒后"
|
||||
}
|
||||
|
||||
@@ -206,6 +300,7 @@ update_one() {
|
||||
local sec="$1" force="$2"
|
||||
local enabled=$(uci -q get minigate.${sec}.enabled)
|
||||
[ "$enabled" = "1" ] || return 0
|
||||
date +%s > "${CACHE_DIR}/minigate_ddns_${sec}.checked"
|
||||
|
||||
local domain=$(uci -q get minigate.${sec}.domain)
|
||||
local zone_id=$(uci -q get minigate.${sec}.cf_zone_id)
|
||||
@@ -213,7 +308,7 @@ update_one() {
|
||||
local ip_source=$(uci -q get minigate.${sec}.ip_source)
|
||||
local iface=$(uci -q get minigate.${sec}.interface)
|
||||
local ip_url=$(uci -q get minigate.${sec}.ip_url)
|
||||
local interval=$(uci -q get minigate.${sec}.check_interval); interval=${interval:-300}
|
||||
local interval=$(ddns_interval "$sec")
|
||||
local ip_version=$(uci -q get minigate.${sec}.ip_version); ip_version=${ip_version:-ipv4}
|
||||
local iface6=$(uci -q get minigate.${sec}.interface6); iface6=${iface6:-wan6}
|
||||
local ip_url6=$(uci -q get minigate.${sec}.ip_url6); ip_url6=${ip_url6:-https://api6.ipify.org}
|
||||
@@ -224,14 +319,14 @@ update_one() {
|
||||
return 1
|
||||
}
|
||||
|
||||
local cache="/tmp/minigate_ddns_${sec}.cache"
|
||||
local cache6="/tmp/minigate_ddns_${sec}_v6.cache"
|
||||
local cache="${CACHE_DIR}/minigate_ddns_${sec}.cache"
|
||||
local cache6="${CACHE_DIR}/minigate_ddns_${sec}_v6.cache"
|
||||
local cached=$(cat "$cache" 2>/dev/null)
|
||||
local cached6=$(cat "$cache6" 2>/dev/null)
|
||||
local next_time=$(calc_next "$interval")
|
||||
local any_ok=0 any_fail=0
|
||||
local result_ip="" result_ip6=""
|
||||
local msgs=""
|
||||
local msgs="" refresh_dns=0
|
||||
|
||||
# ====== IPv4 处理 ======
|
||||
if [ "$ip_version" = "ipv4" ] || [ "$ip_version" = "dual" ]; then
|
||||
@@ -249,12 +344,15 @@ update_one() {
|
||||
if [ -n "$ip4" ] && { [ "$force" = "1" ] || [ "$ip4" != "$cached" ]; }; then
|
||||
log "$domain: IPv4 ${cached:-无} -> $ip4"
|
||||
local result=$(cf_update "$zone_id" "$token" "$domain" "$ip4" "A")
|
||||
if [ "$result" = "true" ]; then
|
||||
if [ "$result" = "changed" ] || [ "$result" = "unchanged" ]; then
|
||||
log "$domain: A记录同步成功"
|
||||
echo "$ip4" > "$cache"
|
||||
result_ip="$ip4"
|
||||
msgs="${msgs}A:${ip4} ✓; "
|
||||
any_ok=1
|
||||
if [ "$result" = "changed" ] || { [ -n "$cached" ] && [ "$cached" != "$ip4" ]; }; then
|
||||
refresh_dns=1
|
||||
fi
|
||||
else
|
||||
log "$domain: A记录同步失败"
|
||||
msgs="${msgs}A记录失败; "
|
||||
@@ -285,12 +383,15 @@ update_one() {
|
||||
if [ -n "$ip6" ] && { [ "$force" = "1" ] || [ "$ip6" != "$cached6" ]; }; then
|
||||
log "$domain: IPv6 ${cached6:-无} -> $ip6"
|
||||
local result=$(cf_update "$zone_id" "$token" "$domain" "$ip6" "AAAA")
|
||||
if [ "$result" = "true" ]; then
|
||||
if [ "$result" = "changed" ] || [ "$result" = "unchanged" ]; then
|
||||
log "$domain: AAAA记录同步成功"
|
||||
echo "$ip6" > "$cache6"
|
||||
result_ip6="$ip6"
|
||||
msgs="${msgs}AAAA:${ip6} ✓; "
|
||||
any_ok=1
|
||||
if [ "$result" = "changed" ] || { [ -n "$cached6" ] && [ "$cached6" != "$ip6" ]; }; then
|
||||
refresh_dns=1
|
||||
fi
|
||||
else
|
||||
log "$domain: AAAA记录同步失败"
|
||||
msgs="${msgs}AAAA记录失败; "
|
||||
@@ -317,6 +418,7 @@ update_one() {
|
||||
[ -n "$result_ip6" ] && uci -q set minigate.${sec}.last_ip6="$result_ip6"
|
||||
uci -q set minigate.${sec}.last_update="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
uci -q set minigate.${sec}.next_sync="$next_time"
|
||||
[ "$refresh_dns" = "1" ] && refresh_local_dns_caches "$domain" || true
|
||||
}
|
||||
|
||||
main() {
|
||||
@@ -327,10 +429,24 @@ main() {
|
||||
fi
|
||||
|
||||
local force=0; [ "$1" = "force" ] && force=1
|
||||
if [ "$force" = "1" ]; then
|
||||
lock_ddns wait
|
||||
elif ! lock_ddns; then
|
||||
log "上一轮同步仍在运行,跳过本次定时检查"
|
||||
return 0
|
||||
fi
|
||||
# 与 LuCI 手动同步使用相同的节名,两条路径共享同一份 IP 缓存
|
||||
local sections=$(uci -q -X show minigate | grep '=ddns$' | cut -d. -f2 | cut -d= -f1)
|
||||
local due="" sec
|
||||
for sec in $sections; do
|
||||
[ "$(uci -q get minigate.${sec}.enabled)" = "1" ] || continue
|
||||
if [ "$force" = "1" ] || ddns_due "$sec"; then due="$due $sec"; fi
|
||||
done
|
||||
# 定时检查没有到期记录时静默退出,不写日志、不提交配置
|
||||
[ -n "$sections" ] && [ -z "$due" ] && return 0
|
||||
log "--- 开始同步 ---"
|
||||
local sections=$(uci -q show minigate | grep '=ddns$' | cut -d. -f2 | cut -d= -f1)
|
||||
local c=0
|
||||
for sec in $sections; do update_one "$sec" "$force"; c=$((c + 1)); done
|
||||
for sec in $due; do update_one "$sec" "$force"; c=$((c + 1)); done
|
||||
[ "$c" = "0" ] && log "无 DDNS 记录"
|
||||
uci commit minigate
|
||||
log "--- 同步完成 ($c 条) ---"
|
||||
@@ -343,6 +459,7 @@ fi
|
||||
|
||||
# 手动单条同步
|
||||
if [ "$1" = "single" ] && [ -n "$2" ]; then
|
||||
lock_ddns wait
|
||||
log "手动同步: $2"
|
||||
update_one "$2" "1"
|
||||
uci commit minigate
|
||||
|
||||
@@ -17,11 +17,11 @@ THRESHOLD=$(uci -q get minigate.login_guard.threshold || echo "3")
|
||||
BANTIME=$(uci -q get minigate.login_guard.bantime || echo "43200")
|
||||
WINDOW=$(uci -q get minigate.login_guard.window || echo "600")
|
||||
|
||||
DATA_DIR=/etc/minigate/login-guard
|
||||
RUN_DIR=/var/run/minigate/login-guard
|
||||
DATA_DIR="${MINIGATE_LG_DATA_DIR:-/etc/minigate/login-guard}"
|
||||
RUN_DIR="${MINIGATE_LG_RUN_DIR:-/var/run/minigate/login-guard}"
|
||||
BANS_FILE="$DATA_DIR/bans.txt"
|
||||
COUNTER_DIR="$RUN_DIR/counters"
|
||||
LOG_FILE=/var/log/minigate-login-guard.log
|
||||
LOG_FILE="${MINIGATE_LG_LOG:-/var/log/minigate-login-guard.log}"
|
||||
ACCESS_LOG=/var/log/minigate-access.log
|
||||
SET="login_banned_v4"
|
||||
TABLE="inet fw4"
|
||||
@@ -64,6 +64,7 @@ is_whitelisted() {
|
||||
}
|
||||
|
||||
# === 确保 nft set + drop rule 存在(fw4 reload 后自动重建) ===
|
||||
# 返回 0 表示刚重建过,调用方需要从持久化文件恢复封禁。
|
||||
ensure_nft() {
|
||||
local recreated=0
|
||||
if ! nft list set $TABLE $SET >/dev/null 2>&1; then
|
||||
@@ -74,8 +75,9 @@ ensure_nft() {
|
||||
nft "insert rule $TABLE input ip saddr @$SET counter drop comment \"minigate-login-guard\"" 2>/dev/null
|
||||
recreated=1
|
||||
fi
|
||||
[ $recreated -eq 1 ] && log "已创建 nft set + drop 规则"
|
||||
return $recreated
|
||||
[ $recreated -eq 1 ] || return 1
|
||||
log "已创建 nft set + drop 规则"
|
||||
return 0
|
||||
}
|
||||
|
||||
# === 把 IP 加入封禁集合 + 持久化 ===
|
||||
@@ -138,7 +140,7 @@ handle_failure() {
|
||||
if [ "$count" -ge "$THRESHOLD" ]; then
|
||||
# 加 ban 前再确认 nft 还在(fw4 可能重载)
|
||||
local ban_target="$ip"
|
||||
ensure_nft && restore_bans
|
||||
repair_nft
|
||||
ban_ip "$ban_target"
|
||||
fi
|
||||
}
|
||||
@@ -169,13 +171,24 @@ access_log_loop() {
|
||||
done
|
||||
}
|
||||
|
||||
repair_nft() {
|
||||
ensure_nft && restore_bans
|
||||
return 0
|
||||
}
|
||||
|
||||
# 日志在内存 /var/log 中,长期遭遇爆破时需要限制大小
|
||||
trim_log() {
|
||||
[ -f "$LOG_FILE" ] || return 0
|
||||
[ "$(wc -c < "$LOG_FILE")" -gt 262144 ] || return 0
|
||||
tail -n 500 "$LOG_FILE" > "${LOG_FILE}.tmp" && mv -f "${LOG_FILE}.tmp" "$LOG_FILE"
|
||||
}
|
||||
|
||||
# === 后台 watchdog:每 5 分钟检查 nft 资源 ===
|
||||
watchdog_loop() {
|
||||
while true; do
|
||||
sleep 300
|
||||
if ensure_nft; then
|
||||
restore_bans
|
||||
fi
|
||||
repair_nft
|
||||
trim_log
|
||||
done
|
||||
}
|
||||
|
||||
@@ -326,6 +339,10 @@ cmd_log() {
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "${MINIGATE_LG_LIBRARY_ONLY:-0}" = "1" ]; then
|
||||
return 0 2>/dev/null || exit 0
|
||||
fi
|
||||
|
||||
# === 入口 ===
|
||||
case "${1:-help}" in
|
||||
run) run_watcher ;;
|
||||
|
||||
@@ -10,6 +10,7 @@ NGINX_ERROR_LOG="${MINIGATE_NGINX_ERROR_LOG:-/var/log/minigate-nginx-error.log}"
|
||||
ACCESS_LOG="${MINIGATE_ACCESS_LOG:-/var/log/minigate-access.log}"
|
||||
SOCKET_DIR="${MINIGATE_SOCKET_DIR:-/var/run/minigate}"
|
||||
STREAM_MODULE="${MINIGATE_STREAM_MODULE:-/usr/lib/nginx/modules/ngx_stream_module.so}"
|
||||
ACCESS_LOG_MAX="${MINIGATE_ACCESS_LOG_MAX:-1048576}"
|
||||
|
||||
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') [PROXY] $*" >> "$LOGFILE"; }
|
||||
|
||||
@@ -427,10 +428,26 @@ do_reload() {
|
||||
fi
|
||||
}
|
||||
|
||||
# /var/log 在内存中且 nginx 不会自行轮转。访问日志保留最近记录到 .1 后原地清空:
|
||||
# nginx 以追加方式继续写入,登录防护的 tail -f 从头读取新内容,不会重放旧记录。
|
||||
trim_logs() {
|
||||
local log_file
|
||||
if [ -f "$ACCESS_LOG" ] && [ "$(wc -c < "$ACCESS_LOG")" -gt "$ACCESS_LOG_MAX" ]; then
|
||||
tail -n 1000 "$ACCESS_LOG" > "${ACCESS_LOG}.1.tmp" && mv -f "${ACCESS_LOG}.1.tmp" "${ACCESS_LOG}.1"
|
||||
: > "$ACCESS_LOG"
|
||||
fi
|
||||
for log_file in "$NGINX_ERROR_LOG" "$LOGFILE"; do
|
||||
[ -f "$log_file" ] && [ "$(wc -c < "$log_file")" -gt 262144 ] || continue
|
||||
tail -n 300 "$log_file" > "${log_file}.tmp" && cat "${log_file}.tmp" > "$log_file"
|
||||
rm -f "${log_file}.tmp"
|
||||
done
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start) do_start ;;
|
||||
stop) do_stop ;;
|
||||
reload) do_reload ;;
|
||||
test) prepare_config ;;
|
||||
*) echo "用法: $0 {start|stop|reload|test}" ;;
|
||||
trim-logs) trim_logs ;;
|
||||
*) echo "用法: $0 {start|stop|reload|test|trim-logs}" ;;
|
||||
esac
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/bin/sh
|
||||
set -u
|
||||
umask 077
|
||||
|
||||
PURGE="${1:-0}"
|
||||
ROOT="${MINIGATE_ROOT:-}"
|
||||
@@ -108,11 +109,12 @@ stop_process_tree() {
|
||||
}
|
||||
|
||||
stop_services() {
|
||||
local init pid
|
||||
local init pid program_dir
|
||||
[ "$SKIP_SERVICES" = "1" ] && return 0
|
||||
program_dir="$(root_path /usr/lib/minigate)"
|
||||
|
||||
if [ -z "$ROOT" ] && command -v pgrep >/dev/null 2>&1; then
|
||||
for pid in $(pgrep -f '/usr/lib/minigate/login_guard.sh run' 2>/dev/null); do
|
||||
if command -v pgrep >/dev/null 2>&1; then
|
||||
for pid in $(pgrep -f "^/bin/sh ${program_dir}/login_guard.sh run" 2>/dev/null); do
|
||||
stop_process_tree "$pid"
|
||||
done
|
||||
fi
|
||||
@@ -129,11 +131,14 @@ stop_services() {
|
||||
''|*[!0-9]*) ;;
|
||||
*) kill "$pid" 2>/dev/null || true ;;
|
||||
esac
|
||||
if command -v pgrep >/dev/null 2>&1; then
|
||||
for pid in $(pgrep -f '/usr/lib/minigate/' 2>/dev/null); do
|
||||
stop_process_tree "$pid"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# 只结束以 /bin/sh 直接运行的 minigate 脚本。BusyBox pgrep 只排除自身,
|
||||
# 宽泛匹配会命中本脚本或 LuCI 启动卸载的外层 sh -c,导致卸载中途自杀。
|
||||
if command -v pgrep >/dev/null 2>&1; then
|
||||
for pid in $(pgrep -f "^/bin/sh ${program_dir}/" 2>/dev/null); do
|
||||
[ "$pid" = "$$" ] || stop_process_tree "$pid"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -224,10 +229,11 @@ remove_runtime_files() {
|
||||
"$tmp_dir"/minigate_proxy_*.tmp \
|
||||
"$tmp_dir"/minigate_default_*.tmp \
|
||||
"$tmp_dir"/minigate_stream_*.tmp \
|
||||
"$tmp_dir"/minigate_ddns_*.cache
|
||||
"$tmp_dir"/minigate_ddns_*.cache \
|
||||
"$tmp_dir"/minigate_ddns_*.checked
|
||||
|
||||
if [ "$PURGE" = "1" ]; then
|
||||
rm -f "$(root_path /etc/config/minigate)" "$log_dir"/minigate-*.log
|
||||
rm -f "$(root_path /etc/config/minigate)" "$log_dir"/minigate-*.log "$log_dir"/minigate-*.log.1
|
||||
rm -rf "$(root_path /etc/minigate)"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
||||
export PATH
|
||||
|
||||
CURRENT_VERSION="2026.9.18-2"
|
||||
CURRENT_VERSION="2026.10.14-3"
|
||||
REPOSITORY="tpxcer/minigate"
|
||||
API_URL="https://api.github.com/repos/${REPOSITORY}/releases/latest"
|
||||
DOWNLOAD_ROOT="https://github.com/${REPOSITORY}/releases/download"
|
||||
|
||||
Reference in new issue
Block a user