🌴 Sync 2026-10-16 01:08:07

This commit is contained in:
github-actions[bot] committed 2026-10-16 01:08:07 +08:00
1 parent f95bba4fb6
commit 16f740db5d
186 files changed
+9575 -5557

No files matched your search

+29 -6
View File
@@ -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
View File
@@ -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 &
+144 -27
View File
@@ -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
+26 -9
View File
@@ -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 ;;
+18 -1
View File
@@ -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
+16 -10
View File
@@ -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
}
+1 -1
View File
@@ -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"