mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-30 11:55:13 +08:00
💐 Sync 2026-10-06 12:33:44
This commit is contained in:
1 parent
8be03ce939
commit
7fe69dbb3a
246 files changed
+50330
-3414
No files matched your search
@@ -0,0 +1,157 @@
|
||||
#!/bin/sh
|
||||
ACME_HOME="/etc/minigate/acme/data"
|
||||
ACME_BIN="${ACME_HOME}/acme.sh"
|
||||
CERT_DIR="/etc/minigate/certs"
|
||||
LOGFILE="/var/log/minigate-acme.log"
|
||||
|
||||
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') [ACME] $*" >> "$LOGFILE"; }
|
||||
|
||||
safe_dir() { echo "$1" | sed 's/\*/_wildcard_/g'; }
|
||||
|
||||
cert_path_for_domain() {
|
||||
echo "${CERT_DIR}/$(safe_dir "$1")/fullchain.pem"
|
||||
}
|
||||
|
||||
cert_expiry() {
|
||||
openssl x509 -in "$1" -noout -enddate 2>/dev/null | cut -d= -f2
|
||||
}
|
||||
|
||||
cert_is_valid() {
|
||||
local cert="$1"
|
||||
[ -f "$cert" ] && openssl x509 -checkend 0 -noout -in "$cert" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
keep_existing_cert_ok() {
|
||||
local domain="$1" reason="$2"
|
||||
local cert="$(cert_path_for_domain "$domain")"
|
||||
if cert_is_valid "$cert"; then
|
||||
local expiry=$(cert_expiry "$cert")
|
||||
log "失败: $reason;现有证书仍有效: $domain (过期: $expiry)"
|
||||
uci -q set minigate.acme.status="ok"
|
||||
uci -q set minigate.acme.last_domain="$domain"
|
||||
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
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
find_token_for_domain() {
|
||||
local target="$1"
|
||||
local sections=$(uci -q show minigate | grep '=ddns$' | cut -d. -f2 | cut -d= -f1)
|
||||
for sec in $sections; do
|
||||
local d=$(uci -q get minigate.${sec}.domain)
|
||||
if [ "$d" = "$target" ]; then
|
||||
echo "$(uci -q get minigate.${sec}.cf_api_token)|$(uci -q get minigate.${sec}.cf_zone_id)"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
local parent=$(echo "$target" | sed 's/^[^.]*\.//')
|
||||
for sec in $sections; do
|
||||
local d=$(uci -q get minigate.${sec}.domain)
|
||||
local db=$(echo "$d" | sed 's/^\*\.//')
|
||||
if [ "$db" = "$parent" ] || [ "$d" = "*.$parent" ]; then
|
||||
echo "$(uci -q get minigate.${sec}.cf_api_token)|$(uci -q get minigate.${sec}.cf_zone_id)"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
for sec in $sections; do
|
||||
local t=$(uci -q get minigate.${sec}.cf_api_token)
|
||||
local z=$(uci -q get minigate.${sec}.cf_zone_id)
|
||||
[ -n "$t" ] && echo "${t}|${z}" && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
do_install() {
|
||||
if [ -x "$ACME_BIN" ] && [ -d "${ACME_HOME}/dnsapi" ]; then
|
||||
log "acme.sh 已安装"; return 0
|
||||
fi
|
||||
log "下载 acme.sh ..."
|
||||
mkdir -p "$ACME_HOME" "$CERT_DIR"
|
||||
cd /tmp; rm -rf acme.sh-master acme-master.tar.gz
|
||||
local ok=0
|
||||
for url in "https://github.com/acmesh-official/acme.sh/archive/master.tar.gz"; do
|
||||
wget -qO /tmp/acme-master.tar.gz "$url" 2>/dev/null && ok=1 && break
|
||||
curl -sLo /tmp/acme-master.tar.gz "$url" 2>/dev/null && ok=1 && break
|
||||
done
|
||||
[ "$ok" != "1" ] && { log "下载失败"; return 1; }
|
||||
tar xzf /tmp/acme-master.tar.gz -C /tmp/ 2>/dev/null
|
||||
cd /tmp/acme.sh-master
|
||||
./acme.sh --install --home "$ACME_HOME" --no-cron --no-profile 2>>"$LOGFILE"
|
||||
cd /tmp; rm -rf /tmp/acme-master.tar.gz /tmp/acme.sh-master
|
||||
if [ -x "$ACME_BIN" ] && [ -d "${ACME_HOME}/dnsapi" ]; then
|
||||
"$ACME_BIN" --home "$ACME_HOME" --set-default-ca --server letsencrypt 2>>"$LOGFILE"
|
||||
log "安装成功"; uci -q set minigate.acme.status="installed"; uci commit minigate; return 0
|
||||
else
|
||||
log "安装失败"; return 1
|
||||
fi
|
||||
}
|
||||
|
||||
do_issue() {
|
||||
local domain="$1"
|
||||
local email=$(uci -q get minigate.acme.email)
|
||||
local staging=$(uci -q get minigate.acme.staging)
|
||||
local key_type=$(uci -q get minigate.acme.key_type)
|
||||
|
||||
if [ -z "$domain" ]; then
|
||||
local fs=$(uci -q show minigate | grep '=ddns$' | head -1 | cut -d. -f2 | cut -d= -f1)
|
||||
[ -n "$fs" ] && domain=$(uci -q get minigate.${fs}.domain)
|
||||
fi
|
||||
[ -z "$domain" ] && { log "未指定域名"; uci -q set minigate.acme.status="error"; uci commit minigate; return 1; }
|
||||
|
||||
local ti=$(find_token_for_domain "$domain")
|
||||
local token=$(echo "$ti" | cut -d'|' -f1)
|
||||
local zone_id=$(echo "$ti" | cut -d'|' -f2)
|
||||
[ -z "$token" ] && { log "无API令牌"; uci -q set minigate.acme.status="error"; uci commit minigate; return 1; }
|
||||
|
||||
local safe=$(safe_dir "$domain")
|
||||
mkdir -p "${CERT_DIR}/${safe}"
|
||||
if ! do_install; then
|
||||
keep_existing_cert_ok "$domain" "acme.sh 安装失败" && return 0
|
||||
uci -q set minigate.acme.status="error"; uci commit minigate
|
||||
return 1
|
||||
fi
|
||||
export CF_Token="$token"
|
||||
[ -n "$zone_id" ] && export CF_Zone_ID="$zone_id"
|
||||
|
||||
local kl="ec-256"
|
||||
case "$key_type" in ec-256|ec-384) kl="$key_type";; rsa-2048) kl="2048";; rsa-4096) kl="4096";; esac
|
||||
local sf="--server letsencrypt"
|
||||
[ "$staging" = "1" ] && sf="--staging --server letsencrypt"
|
||||
|
||||
log "签发: $domain (staging=$staging)"
|
||||
uci -q set minigate.acme.status="issuing"; uci commit minigate
|
||||
|
||||
"$ACME_BIN" --home "$ACME_HOME" --issue --dns dns_cf $sf -d "$domain" --keylength "$kl" \
|
||||
${email:+--accountemail "$email"} \
|
||||
--cert-file "${CERT_DIR}/${safe}/cert.pem" --key-file "${CERT_DIR}/${safe}/key.pem" \
|
||||
--fullchain-file "${CERT_DIR}/${safe}/fullchain.pem" --force >> "$LOGFILE" 2>&1
|
||||
local ret=$?
|
||||
|
||||
if [ $ret -eq 0 ] && [ -f "${CERT_DIR}/${safe}/fullchain.pem" ]; then
|
||||
local expiry=$(cert_expiry "${CERT_DIR}/${safe}/fullchain.pem")
|
||||
log "成功: $domain (过期: $expiry)"
|
||||
uci -q set minigate.acme.status="ok"
|
||||
uci -q set minigate.acme.last_domain="$domain"
|
||||
uci -q set minigate.acme.last_issue="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
uci -q set minigate.acme.cert_expiry="$expiry"
|
||||
uci -q delete minigate.acme.last_error
|
||||
uci commit minigate
|
||||
if echo "$domain" | grep -q '^\*\.'; then
|
||||
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
|
||||
return 0
|
||||
else
|
||||
keep_existing_cert_ok "$domain" "签发失败 (exit=$ret)" && return 0
|
||||
log "失败 (exit=$ret)"; uci -q set minigate.acme.status="error"; uci -q set minigate.acme.last_error="签发失败 (exit=$ret)"; uci commit minigate; return 1
|
||||
fi
|
||||
}
|
||||
|
||||
do_renew() { do_install || return 1; log "续期..."; "$ACME_BIN" --home "$ACME_HOME" --renew-all --server letsencrypt >> "$LOGFILE" 2>&1; log "完成"; }
|
||||
|
||||
case "${1:-issue}" in install) do_install;; issue) do_issue "$2";; renew) do_renew;; *) echo "用法: $0 {install|issue [域名]|renew}";; esac
|
||||
@@ -0,0 +1,353 @@
|
||||
#!/bin/sh
|
||||
LOGFILE="/var/log/minigate-ddns.log"
|
||||
HISTORY_DIR="${MINIGATE_HISTORY_DIR:-/etc/minigate/ddns-history}"
|
||||
HISTORY_WINDOW="${MINIGATE_HISTORY_WINDOW:-86400}"
|
||||
|
||||
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"; }; }
|
||||
|
||||
history_now() {
|
||||
if [ -n "${MINIGATE_HISTORY_NOW:-}" ]; then
|
||||
printf '%s\n' "$MINIGATE_HISTORY_NOW"
|
||||
else
|
||||
date +%s
|
||||
fi
|
||||
}
|
||||
|
||||
compact_history_file() {
|
||||
local file="$1" now="$2" cutoff tmp
|
||||
cutoff=$((now - HISTORY_WINDOW))
|
||||
tmp="${file}.tmp.$$"
|
||||
|
||||
awk -F '\t' -v cutoff="$cutoff" '
|
||||
$1 ~ /^[0-9]+$/ && NF >= 2 {
|
||||
if ($1 < cutoff) {
|
||||
anchor = $0
|
||||
next
|
||||
}
|
||||
if (!emitted && anchor != "") {
|
||||
print anchor
|
||||
emitted = 1
|
||||
}
|
||||
print
|
||||
emitted = 1
|
||||
}
|
||||
END {
|
||||
if (!emitted && anchor != "") print anchor
|
||||
}
|
||||
' "$file" > "$tmp" && chmod 600 "$tmp" 2>/dev/null && mv "$tmp" "$file"
|
||||
}
|
||||
|
||||
record_ip_history_locked() {
|
||||
local file="$1" ip="$2" now="$3" last_ip
|
||||
if [ -f "$file" ]; then
|
||||
last_ip=$(awk -F '\t' 'NF >= 2 { value=$2 } END { print value }' "$file" 2>/dev/null)
|
||||
else
|
||||
last_ip=""
|
||||
fi
|
||||
[ "$last_ip" = "$ip" ] && return 0
|
||||
|
||||
printf '%s\t%s\n' "$now" "$ip" >> "$file" || return 1
|
||||
chmod 600 "$file" 2>/dev/null || true
|
||||
compact_history_file "$file" "$now"
|
||||
}
|
||||
|
||||
record_ip_history() {
|
||||
local section="$1" family="$2" ip="$3" safe_section file now last_ip
|
||||
case "$family" in ipv4|ipv6) ;; *) return 1 ;; esac
|
||||
[ -n "$ip" ] || return 1
|
||||
|
||||
safe_section=$(printf '%s' "$section" | tr -c 'A-Za-z0-9_-' '_')
|
||||
[ -n "$safe_section" ] || return 1
|
||||
if [ ! -d "$HISTORY_DIR" ]; then
|
||||
mkdir -p "$HISTORY_DIR" || return 1
|
||||
chmod 700 "$HISTORY_DIR" 2>/dev/null || true
|
||||
fi
|
||||
file="${HISTORY_DIR}/${safe_section}.${family}.tsv"
|
||||
if [ -f "$file" ]; then
|
||||
last_ip=$(awk -F '\t' 'NF >= 2 { value=$2 } END { print value }' "$file" 2>/dev/null)
|
||||
[ "$last_ip" = "$ip" ] && return 0
|
||||
fi
|
||||
now=$(history_now)
|
||||
case "$now" in ''|*[!0-9]*) return 1 ;; esac
|
||||
|
||||
if command -v flock >/dev/null 2>&1; then
|
||||
(
|
||||
umask 077
|
||||
exec 8>"${file}.lock"
|
||||
flock -x 8 || exit 1
|
||||
record_ip_history_locked "$file" "$ip" "$now"
|
||||
)
|
||||
else
|
||||
record_ip_history_locked "$file" "$ip" "$now"
|
||||
fi
|
||||
}
|
||||
|
||||
# ====== IPv4 获取 ======
|
||||
get_ip4_iface() {
|
||||
local ip=""
|
||||
ip=$(ubus call network.interface.${1:-wan} status 2>/dev/null | jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null)
|
||||
[ -z "$ip" ] && ip=$(ifstatus "${1:-wan}" 2>/dev/null | jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null)
|
||||
echo "$ip" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'
|
||||
}
|
||||
|
||||
get_ip4_url() {
|
||||
local ip=""
|
||||
# 备用 URL 列表(主选 + 自动回退)
|
||||
local fallback_urls="http://ip.3322.net http://members.3322.org/dyndns/getip http://ns1.dnspod.net:6666 http://ip.tool.chinaz.com/getip"
|
||||
local urls="$1 $fallback_urls"
|
||||
|
||||
for url in $urls; do
|
||||
local host=$(echo "$url" | sed -E 's|https?://||;s|/.*||;s|:.*||')
|
||||
local port=$(echo "$url" | grep -oE ':[0-9]+' | head -1 | tr -d ':')
|
||||
local path=$(echo "$url" | sed -E 's|https?://[^/]*||')
|
||||
[ -z "$path" ] && path="/"
|
||||
[ -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)
|
||||
[ -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)
|
||||
[ -n "$ip" ] && { echo "$ip"; return; }
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ====== IPv6 获取 ======
|
||||
get_ip6_iface() {
|
||||
local ip="" iface="${1:-wan6}"
|
||||
# 优先取全局 IPv6 地址(排除 link-local fe80::)
|
||||
ip=$(ubus call network.interface.${iface} status 2>/dev/null | jsonfilter -e '@["ipv6-address"][0].address' 2>/dev/null)
|
||||
[ -z "$ip" ] && ip=$(ifstatus "${iface}" 2>/dev/null | jsonfilter -e '@["ipv6-address"][0].address' 2>/dev/null)
|
||||
# 也尝试从 ipv6-prefix-assignment 获取
|
||||
[ -z "$ip" ] && ip=$(ubus call network.interface.${iface} status 2>/dev/null | jsonfilter -e '@["ipv6-prefix-assignment"][0]["local-address"].address' 2>/dev/null)
|
||||
# 验证是有效的 IPv6(排除 link-local)
|
||||
echo "$ip" | grep -v '^fe80' | grep -oE '^[0-9a-fA-F:]+$' | head -1
|
||||
}
|
||||
|
||||
get_ip6_url() {
|
||||
local url="$1" ip=""
|
||||
local host=$(echo "$url" | sed -E 's|https?://||;s|/.*||;s|:.*||')
|
||||
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)
|
||||
|
||||
# 如果失败,DNS 解析后直连绕过代理
|
||||
if [ -z "$ip" ]; then
|
||||
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
|
||||
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)
|
||||
fi
|
||||
fi
|
||||
echo "$ip"
|
||||
}
|
||||
|
||||
# ====== 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)
|
||||
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)
|
||||
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" \
|
||||
-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)
|
||||
result=$(echo "$resp" | jsonfilter -e '$.success' 2>/dev/null)
|
||||
[ "$result" = "true" ] || { echo "false"; return 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)
|
||||
result=$(echo "$resp" | jsonfilter -e '$.success' 2>/dev/null)
|
||||
[ "$result" = "true" ] || { echo "false"; return 1; }
|
||||
done
|
||||
echo "true"
|
||||
else
|
||||
resp=$(curl -s --connect-timeout 10 -X POST -H "Authorization: Bearer $token" -H "Content-Type: application/json" \
|
||||
-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
|
||||
fi
|
||||
}
|
||||
|
||||
calc_next() {
|
||||
local interval="${1:-300}"
|
||||
local now=$(date +%s)
|
||||
local next=$((now + interval))
|
||||
date -d "@${next}" '+%H:%M:%S' 2>/dev/null || echo "${interval}秒后"
|
||||
}
|
||||
|
||||
update_one() {
|
||||
local sec="$1" force="$2"
|
||||
local enabled=$(uci -q get minigate.${sec}.enabled)
|
||||
[ "$enabled" = "1" ] || return 0
|
||||
|
||||
local domain=$(uci -q get minigate.${sec}.domain)
|
||||
local zone_id=$(uci -q get minigate.${sec}.cf_zone_id)
|
||||
local token=$(uci -q get minigate.${sec}.cf_api_token)
|
||||
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 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}
|
||||
|
||||
[ -z "$domain" ] || [ -z "$zone_id" ] || [ -z "$token" ] && {
|
||||
log "$domain: 配置不完整"
|
||||
uci -q set minigate.${sec}.status="error"; uci -q set minigate.${sec}.status_msg="配置不完整"
|
||||
return 1
|
||||
}
|
||||
|
||||
local cache="/tmp/minigate_ddns_${sec}.cache"
|
||||
local cache6="/tmp/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=""
|
||||
|
||||
# ====== IPv4 处理 ======
|
||||
if [ "$ip_version" = "ipv4" ] || [ "$ip_version" = "dual" ]; then
|
||||
local ip4=""
|
||||
case "$ip_source" in url) ip4=$(get_ip4_url "$ip_url");; *) ip4=$(get_ip4_iface "$iface");; esac
|
||||
|
||||
if [ -z "$ip4" ]; then
|
||||
log "$domain: 获取IPv4失败"
|
||||
msgs="${msgs}IPv4获取失败; "
|
||||
any_fail=1
|
||||
else
|
||||
record_ip_history "$domain" "ipv4" "$ip4" || log "$domain: IPv4历史记录写入失败"
|
||||
fi
|
||||
|
||||
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
|
||||
log "$domain: A记录同步成功"
|
||||
echo "$ip4" > "$cache"
|
||||
result_ip="$ip4"
|
||||
msgs="${msgs}A:${ip4} ✓; "
|
||||
any_ok=1
|
||||
else
|
||||
log "$domain: A记录同步失败"
|
||||
msgs="${msgs}A记录失败; "
|
||||
any_fail=1
|
||||
fi
|
||||
elif [ -n "$ip4" ]; then
|
||||
log "$domain: IPv4地址一致($ip4)"
|
||||
result_ip="$ip4"
|
||||
msgs="${msgs}A:${ip4} (未变); "
|
||||
any_ok=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ====== IPv6 处理 ======
|
||||
if [ "$ip_version" = "ipv6" ] || [ "$ip_version" = "dual" ]; then
|
||||
local ip6=""
|
||||
case "$ip_source" in url) ip6=$(get_ip6_url "$ip_url6");; *) ip6=$(get_ip6_iface "$iface6");; esac
|
||||
|
||||
if [ -z "$ip6" ]; then
|
||||
log "$domain: 获取IPv6失败"
|
||||
msgs="${msgs}IPv6获取失败; "
|
||||
# 双栈模式下 IPv6 获取失败不算整体失败
|
||||
[ "$ip_version" = "ipv6" ] && any_fail=1
|
||||
else
|
||||
record_ip_history "$domain" "ipv6" "$ip6" || log "$domain: IPv6历史记录写入失败"
|
||||
fi
|
||||
|
||||
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
|
||||
log "$domain: AAAA记录同步成功"
|
||||
echo "$ip6" > "$cache6"
|
||||
result_ip6="$ip6"
|
||||
msgs="${msgs}AAAA:${ip6} ✓; "
|
||||
any_ok=1
|
||||
else
|
||||
log "$domain: AAAA记录同步失败"
|
||||
msgs="${msgs}AAAA记录失败; "
|
||||
any_fail=1
|
||||
fi
|
||||
elif [ -n "$ip6" ]; then
|
||||
log "$domain: IPv6地址一致($ip6)"
|
||||
result_ip6="$ip6"
|
||||
msgs="${msgs}AAAA:${ip6} (未变); "
|
||||
any_ok=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ====== 更新状态 ======
|
||||
if [ "$any_ok" = "1" ] && [ "$any_fail" = "0" ]; then
|
||||
uci -q set minigate.${sec}.status="ok"
|
||||
elif [ "$any_ok" = "1" ] && [ "$any_fail" = "1" ]; then
|
||||
uci -q set minigate.${sec}.status="partial"
|
||||
else
|
||||
uci -q set minigate.${sec}.status="error"
|
||||
fi
|
||||
uci -q set minigate.${sec}.status_msg="$msgs"
|
||||
[ -n "$result_ip" ] && uci -q set minigate.${sec}.last_ip="$result_ip"
|
||||
[ -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"
|
||||
}
|
||||
|
||||
main() {
|
||||
local gen=$(uci -q get minigate.global.enabled)
|
||||
if [ "$gen" != "1" ]; then
|
||||
log "全局未启用,跳过 DDNS"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local force=0; [ "$1" = "force" ] && force=1
|
||||
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
|
||||
[ "$c" = "0" ] && log "无 DDNS 记录"
|
||||
uci commit minigate
|
||||
log "--- 同步完成 ($c 条) ---"
|
||||
trim_log
|
||||
}
|
||||
|
||||
if [ "${MINIGATE_DDNS_LIBRARY_ONLY:-0}" = "1" ]; then
|
||||
return 0 2>/dev/null || exit 0
|
||||
fi
|
||||
|
||||
# 手动单条同步
|
||||
if [ "$1" = "single" ] && [ -n "$2" ]; then
|
||||
log "手动同步: $2"
|
||||
update_one "$2" "1"
|
||||
uci commit minigate
|
||||
trim_log
|
||||
exit 0
|
||||
fi
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,177 @@
|
||||
#!/bin/sh
|
||||
# MiniGate GeoFence - 地区访问控制
|
||||
# 检查反代访问日志,非允许地区的 IP 用防火墙封禁
|
||||
LOGFILE="/var/log/minigate-geofence.log"
|
||||
CACHE_DIR="/tmp/minigate_geo"
|
||||
BLOCK_SET="minigate_blocked"
|
||||
ACCESS_LOG="/var/log/minigate-access.log"
|
||||
|
||||
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') [GEOFENCE] $*" >> "$LOGFILE"; }
|
||||
|
||||
# 查询 IP 归属地,返回省份名
|
||||
lookup_geo() {
|
||||
local ip="$1"
|
||||
local cache="$CACHE_DIR/${ip}"
|
||||
|
||||
# 缓存命中
|
||||
[ -f "$cache" ] && cat "$cache" && return 0
|
||||
|
||||
local geo=""
|
||||
|
||||
# API 1: pconline
|
||||
if [ -z "$geo" ]; then
|
||||
local raw=$(curl -s --connect-timeout 3 --max-time 5 "https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true" 2>/dev/null)
|
||||
geo=$(echo "$raw" | jsonfilter -e '$.pro' 2>/dev/null)
|
||||
fi
|
||||
|
||||
# API 2: ip9.com.cn
|
||||
if [ -z "$geo" ]; then
|
||||
local raw=$(curl -s --connect-timeout 3 --max-time 5 "https://ip9.com.cn/get?ip=${ip}" 2>/dev/null)
|
||||
geo=$(echo "$raw" | jsonfilter -e '$.data.prov' 2>/dev/null)
|
||||
fi
|
||||
|
||||
# API 3: ip-api.com
|
||||
if [ -z "$geo" ]; then
|
||||
local raw=$(curl -s --connect-timeout 3 --max-time 5 "http://ip-api.com/json/${ip}?fields=regionName" 2>/dev/null)
|
||||
geo=$(echo "$raw" | jsonfilter -e '$.regionName' 2>/dev/null)
|
||||
fi
|
||||
|
||||
[ -z "$geo" ] && geo="unknown"
|
||||
|
||||
# 写入缓存
|
||||
mkdir -p "$CACHE_DIR"
|
||||
echo "$geo" > "$cache"
|
||||
echo "$geo"
|
||||
}
|
||||
|
||||
# 检查 IP 是否在允许的地区列表中
|
||||
is_allowed() {
|
||||
local geo="$1"
|
||||
local allowed=$(uci -q get minigate.global.geo_allowed)
|
||||
|
||||
[ -z "$allowed" ] && return 0 # 未配置地区限制,全部放行
|
||||
|
||||
# allowed 是逗号分隔的省份列表,如 "四川,重庆,贵州"
|
||||
echo "$allowed" | tr ',' '\n' | while read prov; do
|
||||
[ -z "$prov" ] && continue
|
||||
echo "$geo" | grep -q "$prov" && echo "yes" && return 0
|
||||
done | grep -q "yes"
|
||||
}
|
||||
|
||||
# 初始化 ipset(nftables 或 iptables)
|
||||
init_block_set() {
|
||||
if command -v nft >/dev/null 2>&1; then
|
||||
# nftables (OpenWrt 22+)
|
||||
nft list set inet fw4 "$BLOCK_SET" >/dev/null 2>&1 || {
|
||||
nft add set inet fw4 "$BLOCK_SET" '{ type ipv4_addr; flags timeout; }'
|
||||
nft add rule inet fw4 input ip saddr @"$BLOCK_SET" drop 2>/dev/null
|
||||
log "nftables 封禁集合已创建"
|
||||
}
|
||||
else
|
||||
# iptables fallback
|
||||
ipset list "$BLOCK_SET" >/dev/null 2>&1 || {
|
||||
ipset create "$BLOCK_SET" hash:ip timeout 43200 2>/dev/null # 12小时超时
|
||||
iptables -I INPUT -m set --match-set "$BLOCK_SET" src -j DROP 2>/dev/null
|
||||
log "ipset 封禁集合已创建"
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
# 封禁 IP
|
||||
block_ip() {
|
||||
local ip="$1" reason="$2"
|
||||
if command -v nft >/dev/null 2>&1; then
|
||||
nft add element inet fw4 "$BLOCK_SET" "{ $ip timeout 12h }" 2>/dev/null
|
||||
else
|
||||
ipset add "$BLOCK_SET" "$ip" timeout 43200 2>/dev/null
|
||||
fi
|
||||
log "封禁: $ip ($reason) 12小时"
|
||||
|
||||
# 记录到 UCI
|
||||
local ts=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
uci -q add_list minigate.global.blocked_ips="${ip}|${reason}|${ts}"
|
||||
uci commit minigate 2>/dev/null
|
||||
}
|
||||
|
||||
# 清理过期缓存(超过24小时的)
|
||||
clean_cache() {
|
||||
[ -d "$CACHE_DIR" ] && find "$CACHE_DIR" -type f -mmin +1440 -delete 2>/dev/null
|
||||
}
|
||||
|
||||
# 主逻辑:扫描 access log 中的新 IP
|
||||
scan_and_block() {
|
||||
local enabled=$(uci -q get minigate.global.geo_enabled)
|
||||
[ "$enabled" != "1" ] && return 0
|
||||
|
||||
local allowed=$(uci -q get minigate.global.geo_allowed)
|
||||
[ -z "$allowed" ] && { log "未配置允许地区,跳过"; return 0; }
|
||||
|
||||
init_block_set
|
||||
|
||||
# 提取所有外网 IP(排除内网)
|
||||
local ips=$(grep -oP '"client":"[^"]*"' "$ACCESS_LOG" 2>/dev/null | \
|
||||
grep -oP '[\d\.]+' | \
|
||||
grep -v '^192\.168\.' | grep -v '^10\.' | grep -v '^172\.(1[6-9]|2[0-9]|3[01])\.' | grep -v '^127\.' | \
|
||||
sort -u)
|
||||
|
||||
local checked=0 blocked=0
|
||||
for ip in $ips; do
|
||||
# 已经封禁的跳过
|
||||
if command -v nft >/dev/null 2>&1; then
|
||||
nft list set inet fw4 "$BLOCK_SET" 2>/dev/null | grep -q "$ip" && continue
|
||||
else
|
||||
ipset test "$BLOCK_SET" "$ip" 2>/dev/null && continue
|
||||
fi
|
||||
|
||||
local geo=$(lookup_geo "$ip")
|
||||
checked=$((checked + 1))
|
||||
|
||||
if [ "$geo" = "unknown" ]; then
|
||||
log "警告: $ip 归属地未知,暂时放行"
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! is_allowed "$geo"; then
|
||||
block_ip "$ip" "$geo"
|
||||
blocked=$((blocked + 1))
|
||||
else
|
||||
log "放行: $ip ($geo)"
|
||||
fi
|
||||
|
||||
# 限速,避免 API 过载
|
||||
sleep 1
|
||||
done
|
||||
|
||||
[ "$checked" -gt 0 ] && log "扫描完成: 检查 $checked 个IP,封禁 $blocked 个"
|
||||
clean_cache
|
||||
}
|
||||
|
||||
# 手动解封
|
||||
unblock_ip() {
|
||||
local ip="$1"
|
||||
if command -v nft >/dev/null 2>&1; then
|
||||
nft delete element inet fw4 "$BLOCK_SET" "{ $ip }" 2>/dev/null
|
||||
else
|
||||
ipset del "$BLOCK_SET" "$ip" 2>/dev/null
|
||||
fi
|
||||
log "解封: $ip"
|
||||
}
|
||||
|
||||
# 清除所有封禁
|
||||
flush_blocks() {
|
||||
if command -v nft >/dev/null 2>&1; then
|
||||
nft flush set inet fw4 "$BLOCK_SET" 2>/dev/null
|
||||
else
|
||||
ipset flush "$BLOCK_SET" 2>/dev/null
|
||||
fi
|
||||
uci -q delete minigate.global.blocked_ips
|
||||
uci commit minigate 2>/dev/null
|
||||
log "已清除所有封禁"
|
||||
}
|
||||
|
||||
case "${1:-scan}" in
|
||||
scan) scan_and_block ;;
|
||||
unblock) unblock_ip "$2" ;;
|
||||
flush) flush_blocks ;;
|
||||
*) echo "用法: $0 {scan|unblock <ip>|flush}" ;;
|
||||
esac
|
||||
+351
@@ -0,0 +1,351 @@
|
||||
#!/bin/sh
|
||||
# MiniGate · 登录防护(Login Guard)
|
||||
# 监控 SSH (dropbear) 和 LuCI 网页登录失败,达到阈值自动封禁源 IP
|
||||
#
|
||||
# 用法:
|
||||
# login_guard.sh run 后台监控(init.d 调用)
|
||||
# login_guard.sh list 列出当前已封禁的 IP
|
||||
# login_guard.sh status 状态总览
|
||||
# login_guard.sh ban <IP> 手动封禁
|
||||
# login_guard.sh unban <IP> 解封
|
||||
# login_guard.sh flush 清空所有封禁
|
||||
# login_guard.sh log 最近日志
|
||||
|
||||
# === 从 uci 读配置 ===
|
||||
ENABLED=$(uci -q get minigate.login_guard.enabled || echo "0")
|
||||
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
|
||||
BANS_FILE="$DATA_DIR/bans.txt"
|
||||
COUNTER_DIR="$RUN_DIR/counters"
|
||||
LOG_FILE=/var/log/minigate-login-guard.log
|
||||
ACCESS_LOG=/var/log/minigate-access.log
|
||||
SET="login_banned_v4"
|
||||
TABLE="inet fw4"
|
||||
|
||||
mkdir -p "$DATA_DIR" "$COUNTER_DIR"
|
||||
touch "$BANS_FILE"
|
||||
|
||||
log() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >> "$LOG_FILE"
|
||||
logger -t minigate-lg "$*"
|
||||
}
|
||||
|
||||
# === 白名单:从 uci 读 list whitelist + 默认私有段 ===
|
||||
is_whitelisted() {
|
||||
local ip="$1"
|
||||
case "$ip" in
|
||||
192.168.*|10.*|127.*|172.1[6-9].*|172.2[0-9].*|172.3[01].*) return 0 ;;
|
||||
esac
|
||||
local wl
|
||||
wl=$(uci -q get minigate.login_guard.whitelist 2>/dev/null)
|
||||
[ -z "$wl" ] && return 1
|
||||
for net in $wl; do
|
||||
case "$net" in
|
||||
"$ip") return 0 ;;
|
||||
*/[0-9]*)
|
||||
# 简单 CIDR 匹配(前缀 byte 比较)
|
||||
local prefix="${net%/*}"
|
||||
local mask="${net#*/}"
|
||||
# 只比 /8 /16 /24 /32 这种整字节边界(够用)
|
||||
case "$mask" in
|
||||
8) [ "${ip%%.*}" = "${prefix%%.*}" ] && return 0 ;;
|
||||
16) [ "${ip%.*.*}" = "${prefix%.*.*}" ] && return 0 ;;
|
||||
24) [ "${ip%.*}" = "${prefix%.*}" ] && return 0 ;;
|
||||
32) [ "$ip" = "$prefix" ] && return 0 ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# === 确保 nft set + drop rule 存在(fw4 reload 后自动重建) ===
|
||||
ensure_nft() {
|
||||
local recreated=0
|
||||
if ! nft list set $TABLE $SET >/dev/null 2>&1; then
|
||||
nft "add set $TABLE $SET { type ipv4_addr; flags timeout; timeout ${BANTIME}s; }" 2>/dev/null
|
||||
recreated=1
|
||||
fi
|
||||
if ! nft list chain $TABLE input 2>/dev/null | grep -q "@$SET"; then
|
||||
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
|
||||
}
|
||||
|
||||
# === 把 IP 加入封禁集合 + 持久化 ===
|
||||
ban_ip() {
|
||||
local ip="$1"
|
||||
local exp=$(($(date +%s) + BANTIME))
|
||||
nft add element $TABLE $SET "{ $ip timeout ${BANTIME}s }" 2>/dev/null
|
||||
# 去重写入持久化文件
|
||||
grep -v "^$ip " "$BANS_FILE" > "${BANS_FILE}.tmp" 2>/dev/null || true
|
||||
echo "$ip $exp" >> "${BANS_FILE}.tmp"
|
||||
mv "${BANS_FILE}.tmp" "$BANS_FILE"
|
||||
log "BAN $ip 12小时(达到 $THRESHOLD 次失败)"
|
||||
rm -f "$COUNTER_DIR/$ip"
|
||||
}
|
||||
|
||||
# === 启动时从持久化文件恢复封禁 ===
|
||||
restore_bans() {
|
||||
[ -f "$BANS_FILE" ] || return 0
|
||||
local now=$(date +%s)
|
||||
local tmp="${BANS_FILE}.tmp"
|
||||
> "$tmp"
|
||||
local count=0
|
||||
while read -r ip exp; do
|
||||
[ -z "$ip" ] && continue
|
||||
if [ "$exp" -gt "$now" ]; then
|
||||
local remaining=$((exp - now))
|
||||
nft add element $TABLE $SET "{ $ip timeout ${remaining}s }" 2>/dev/null
|
||||
echo "$ip $exp" >> "$tmp"
|
||||
count=$((count + 1))
|
||||
fi
|
||||
done < "$BANS_FILE"
|
||||
mv "$tmp" "$BANS_FILE"
|
||||
[ $count -gt 0 ] && log "从持久化文件恢复 $count 条封禁"
|
||||
}
|
||||
|
||||
# === 处理一条登录失败 ===
|
||||
handle_failure() {
|
||||
local ip="$1"
|
||||
[ -z "$ip" ] && return
|
||||
is_whitelisted "$ip" && return
|
||||
|
||||
# 滑动窗口:超过 window 秒的旧失败丢弃
|
||||
local f="$COUNTER_DIR/$ip"
|
||||
local now=$(date +%s)
|
||||
local first=0
|
||||
local count=0
|
||||
if [ -f "$f" ]; then
|
||||
read -r first count < "$f"
|
||||
if [ -n "$first" ] && [ $((now - first)) -gt "$WINDOW" ]; then
|
||||
first=$now
|
||||
count=0
|
||||
fi
|
||||
else
|
||||
first=$now
|
||||
fi
|
||||
count=$((count + 1))
|
||||
echo "$first $count" > "$f"
|
||||
log "FAIL $ip ($count/$THRESHOLD)"
|
||||
|
||||
if [ "$count" -ge "$THRESHOLD" ]; then
|
||||
# 加 ban 前再确认 nft 还在(fw4 可能重载)
|
||||
local ban_target="$ip"
|
||||
ensure_nft && restore_bans
|
||||
ban_ip "$ban_target"
|
||||
fi
|
||||
}
|
||||
|
||||
extract_ip() {
|
||||
echo "$1" | grep -oE "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | head -1
|
||||
}
|
||||
|
||||
extract_json_field() {
|
||||
local key="$1"
|
||||
sed -n "s/.*\"${key}\":\"\([^\"]*\)\".*/\1/p" | head -1
|
||||
}
|
||||
|
||||
is_luci_failed_post() {
|
||||
local line="$1"
|
||||
echo "$line" | grep -q '"method":"POST"' || return 1
|
||||
echo "$line" | grep -q '"status":403' || return 1
|
||||
echo "$line" | grep -Eq '"uri":"/cgi-bin/luci(/|")' || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
access_log_loop() {
|
||||
touch "$ACCESS_LOG" 2>/dev/null
|
||||
tail -n 0 -f "$ACCESS_LOG" 2>/dev/null | while read -r line; do
|
||||
is_luci_failed_post "$line" || continue
|
||||
ip=$(echo "$line" | extract_json_field client)
|
||||
handle_failure "$ip"
|
||||
done
|
||||
}
|
||||
|
||||
# === 后台 watchdog:每 5 分钟检查 nft 资源 ===
|
||||
watchdog_loop() {
|
||||
while true; do
|
||||
sleep 300
|
||||
if ensure_nft; then
|
||||
restore_bans
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# === 主监控循环 ===
|
||||
run_watcher() {
|
||||
[ "$ENABLED" = "1" ] || {
|
||||
log "未启用(minigate.login_guard.enabled=0),退出"
|
||||
exit 0
|
||||
}
|
||||
|
||||
ensure_nft
|
||||
restore_bans
|
||||
log "登录防护启动 阈值=$THRESHOLD 窗口=${WINDOW}s 封禁=${BANTIME}s"
|
||||
|
||||
# 后台 watchdog
|
||||
watchdog_loop &
|
||||
|
||||
# MiniGate 反代访问 LuCI 时,LuCI 日志只会看到反代内网 IP。
|
||||
# 这里补充监听 nginx access log,用 JSON client 字段记录真实公网 IP。
|
||||
access_log_loop &
|
||||
|
||||
# 主循环:监控系统日志
|
||||
logread -f 2>/dev/null | while read -r line; do
|
||||
case "$line" in
|
||||
*"Bad password attempt"*"from"*|\
|
||||
*"Login attempt for nonexistent user"*"from"*|\
|
||||
*"luci: failed login"*"from"*)
|
||||
ip=$(extract_ip "$line")
|
||||
handle_failure "$ip"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# === CLI 命令 ===
|
||||
|
||||
# 用 nft -j 拿封禁列表,输出每行: <IP> <剩余秒数>
|
||||
# 注意 nft -j 输出可能有空格、字段顺序也不固定(有的 elem 多个 timeout 字段)
|
||||
parse_set_json() {
|
||||
nft -j list set $TABLE $SET 2>/dev/null | awk '
|
||||
{
|
||||
# 在每条 elem 块({...})内匹配 val + expires
|
||||
# [^{}]* 限制不跨越 brace 边界
|
||||
while (match($0, /"val":[ ]*"[0-9.]+"[^{}]*"expires":[ ]*[0-9]+/)) {
|
||||
block = substr($0, RSTART, RLENGTH)
|
||||
$0 = substr($0, RSTART + RLENGTH)
|
||||
# 提取 IP
|
||||
if (match(block, /[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)) {
|
||||
ip = substr(block, RSTART, RLENGTH)
|
||||
}
|
||||
# 提取 expires
|
||||
if (match(block, /"expires":[ ]*[0-9]+/)) {
|
||||
exp_str = substr(block, RSTART, RLENGTH)
|
||||
gsub(/[^0-9]/, "", exp_str)
|
||||
print ip, exp_str
|
||||
}
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
# 把秒数转人类可读
|
||||
fmt_duration() {
|
||||
local s=$1
|
||||
if [ "$s" -ge 3600 ]; then
|
||||
printf "%dh%dm" $((s/3600)) $(((s%3600)/60))
|
||||
elif [ "$s" -ge 60 ]; then
|
||||
printf "%dm%ds" $((s/60)) $((s%60))
|
||||
else
|
||||
printf "%ds" "$s"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
echo "=== 当前已封禁 IP ==="
|
||||
local out
|
||||
out=$(parse_set_json)
|
||||
if [ -z "$out" ]; then
|
||||
echo "(暂无)"
|
||||
return
|
||||
fi
|
||||
echo "$out" | while read -r ip secs; do
|
||||
printf " %-18s 剩余 %s\n" "$ip" "$(fmt_duration "$secs")"
|
||||
done
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
local banned watching
|
||||
banned=$(parse_set_json | wc -l)
|
||||
watching=$(ls "$COUNTER_DIR" 2>/dev/null | wc -l)
|
||||
echo "已封禁 IP 数: $banned"
|
||||
echo "失败计数中的 IP: $watching"
|
||||
if [ "$banned" -gt 0 ]; then
|
||||
echo "--- 已封禁 ---"
|
||||
cmd_list | tail -n +2
|
||||
fi
|
||||
if [ "$watching" -gt 0 ]; then
|
||||
echo "--- 失败计数详情 ---"
|
||||
for f in "$COUNTER_DIR"/*; do
|
||||
[ -f "$f" ] || continue
|
||||
local ip count first now age
|
||||
ip=$(basename "$f")
|
||||
read -r first count < "$f"
|
||||
now=$(date +%s)
|
||||
age=$((now - first))
|
||||
printf " %-18s %d/%d (距首次失败 %ds)\n" "$ip" "$count" "$THRESHOLD" "$age"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_unban() {
|
||||
local ip="$1"
|
||||
[ -z "$ip" ] && { echo "用法: $0 unban <IP>"; exit 1; }
|
||||
nft delete element $TABLE $SET "{ $ip }" 2>&1
|
||||
grep -v "^$ip " "$BANS_FILE" > "${BANS_FILE}.tmp" 2>/dev/null || true
|
||||
mv "${BANS_FILE}.tmp" "$BANS_FILE"
|
||||
rm -f "$COUNTER_DIR/$ip"
|
||||
echo "✓ 已解封 $ip"
|
||||
log "MANUAL unban $ip"
|
||||
}
|
||||
|
||||
cmd_ban() {
|
||||
local ip="$1"
|
||||
[ -z "$ip" ] && { echo "用法: $0 ban <IP>"; exit 1; }
|
||||
ensure_nft
|
||||
local exp=$(($(date +%s) + BANTIME))
|
||||
nft add element $TABLE $SET "{ $ip timeout ${BANTIME}s }" 2>&1
|
||||
grep -v "^$ip " "$BANS_FILE" > "${BANS_FILE}.tmp" 2>/dev/null || true
|
||||
echo "$ip $exp" >> "${BANS_FILE}.tmp"
|
||||
mv "${BANS_FILE}.tmp" "$BANS_FILE"
|
||||
echo "✓ 已封禁 $ip"
|
||||
log "MANUAL ban $ip"
|
||||
}
|
||||
|
||||
cmd_flush() {
|
||||
nft flush set $TABLE $SET 2>&1 || true
|
||||
> "$BANS_FILE"
|
||||
rm -rf "$COUNTER_DIR"/*
|
||||
echo "✓ 已清空所有封禁"
|
||||
log "MANUAL flush all bans"
|
||||
}
|
||||
|
||||
cmd_log() {
|
||||
local n="${1:-30}"
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
tail -n "$n" "$LOG_FILE"
|
||||
else
|
||||
logread -e minigate-lg | tail -n "$n"
|
||||
fi
|
||||
}
|
||||
|
||||
# === 入口 ===
|
||||
case "${1:-help}" in
|
||||
run) run_watcher ;;
|
||||
list|ls) cmd_list ;;
|
||||
status) cmd_status ;;
|
||||
ban) cmd_ban "$2" ;;
|
||||
unban) cmd_unban "$2" ;;
|
||||
flush) cmd_flush ;;
|
||||
log|logs) cmd_log "$2" ;;
|
||||
*)
|
||||
cat <<EOH
|
||||
MiniGate 登录防护
|
||||
|
||||
$0 run 后台监控(init.d 用)
|
||||
$0 list 列出已封禁 IP
|
||||
$0 status 状态总览
|
||||
$0 ban <IP> 手动封禁
|
||||
$0 unban <IP> 解封
|
||||
$0 flush 清空所有封禁
|
||||
$0 log [N] 最近 N 条日志(默认 30)
|
||||
EOH
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,436 @@
|
||||
#!/bin/sh
|
||||
NGINX_CONF="${MINIGATE_NGINX_CONF:-/etc/minigate/nginx/minigate.conf}"
|
||||
SITES_DIR="${MINIGATE_SITES_DIR:-/etc/minigate/nginx/sites}"
|
||||
STREAMS_DIR="${MINIGATE_STREAMS_DIR:-/etc/minigate/nginx/streams}"
|
||||
CERT_DIR="${MINIGATE_CERT_DIR:-/etc/minigate/certs}"
|
||||
DEFAULT_CERT_DIR="${MINIGATE_DEFAULT_CERT_DIR:-${CERT_DIR}/_default}"
|
||||
LOGFILE="${MINIGATE_LOGFILE:-/var/log/minigate-proxy.log}"
|
||||
PID_FILE="${MINIGATE_PID_FILE:-/var/run/minigate-nginx.pid}"
|
||||
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}"
|
||||
|
||||
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') [PROXY] $*" >> "$LOGFILE"; }
|
||||
|
||||
generate_main_conf() {
|
||||
local http_redirect=$(get_http_redirect)
|
||||
local use_stream=0
|
||||
mkdir -p "$(dirname "$NGINX_CONF")" "$SITES_DIR" "$STREAMS_DIR" "$SOCKET_DIR"
|
||||
|
||||
if [ "$http_redirect" = "1" ] && ls "$STREAMS_DIR"/*.conf >/dev/null 2>&1; then
|
||||
use_stream=1
|
||||
fi
|
||||
if [ "$use_stream" = "1" ] && [ ! -f "$STREAM_MODULE" ]; then
|
||||
log "同端口HTTP跳转需要 nginx-mod-stream"
|
||||
return 1
|
||||
fi
|
||||
|
||||
: > "$NGINX_CONF"
|
||||
[ "$use_stream" = "1" ] && echo "load_module ${STREAM_MODULE};" >> "$NGINX_CONF"
|
||||
cat >> "$NGINX_CONF" <<EOF
|
||||
worker_processes auto;
|
||||
pid ${PID_FILE};
|
||||
error_log ${NGINX_ERROR_LOG} warn;
|
||||
events { worker_connections 512; }
|
||||
EOF
|
||||
if [ "$use_stream" = "1" ]; then
|
||||
cat >> "$NGINX_CONF" <<EOF
|
||||
stream {
|
||||
include ${STREAMS_DIR}/*.conf;
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
cat >> "$NGINX_CONF" <<'EOF'
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
server_names_hash_bucket_size 64;
|
||||
sendfile on; keepalive_timeout 65; client_max_body_size 100m;
|
||||
gzip on; gzip_types text/plain text/css application/json application/javascript text/xml;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
map $proxy_protocol_addr $minigate_client_addr {
|
||||
default $proxy_protocol_addr;
|
||||
"" $remote_addr;
|
||||
}
|
||||
log_format minigate_json escape=json
|
||||
'{"time":"$time_iso8601",'
|
||||
'"domain":"$server_name",'
|
||||
'"client":"$minigate_client_addr",'
|
||||
'"method":"$request_method",'
|
||||
'"uri":"$request_uri",'
|
||||
'"status":$status,'
|
||||
'"size":$body_bytes_sent,'
|
||||
'"referer":"$http_referer",'
|
||||
'"ua":"$http_user_agent"}';
|
||||
EOF
|
||||
echo " access_log ${ACCESS_LOG} minigate_json;" >> "$NGINX_CONF"
|
||||
echo " include ${SITES_DIR}/*.conf;" >> "$NGINX_CONF"
|
||||
cat >> "$NGINX_CONF" <<'EOF'
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
find_cert() {
|
||||
local domain="$1"
|
||||
local parent=$(echo "$domain" | sed 's/^[^.]*\.//')
|
||||
for try in "$domain" "_wildcard_.${parent}" "$parent"; do
|
||||
[ -f "${CERT_DIR}/${try}/fullchain.pem" ] && { echo "${CERT_DIR}/${try}"; return 0; }
|
||||
done
|
||||
for try in "$parent" "$domain"; do
|
||||
[ -L "${CERT_DIR}/${try}" ] && {
|
||||
local t=$(readlink -f "${CERT_DIR}/${try}")
|
||||
[ -f "${t}/fullchain.pem" ] && { echo "$t"; return 0; }
|
||||
}
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
check_h2() {
|
||||
local m=$(nginx -v 2>&1 | grep -oE '[0-9]+\.[0-9]+' | head -1 | cut -d. -f2)
|
||||
[ "${m:-0}" -ge 25 ] && echo "new" || echo "old"
|
||||
}
|
||||
|
||||
# 读取全局 IPv6 监听设置
|
||||
get_ipv6_listen() {
|
||||
local v6=$(uci -q get minigate.global.ipv6_listen)
|
||||
echo "${v6:-0}"
|
||||
}
|
||||
|
||||
get_http_redirect() {
|
||||
local enabled=$(uci -q get minigate.global.http_redirect)
|
||||
echo "${enabled:-0}"
|
||||
}
|
||||
|
||||
ensure_default_cert() {
|
||||
mkdir -p "$DEFAULT_CERT_DIR"
|
||||
[ -f "${DEFAULT_CERT_DIR}/fullchain.pem" ] && [ -f "${DEFAULT_CERT_DIR}/key.pem" ] && return 0
|
||||
openssl req -x509 -nodes -newkey rsa:2048 -days 3650 \
|
||||
-subj "/CN=minigate.invalid" \
|
||||
-keyout "${DEFAULT_CERT_DIR}/key.pem" \
|
||||
-out "${DEFAULT_CERT_DIR}/fullchain.pem" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
write_default_server() {
|
||||
local conf="$1" lport="$2" ssl="$3" h2s="$4" ipv6_listen="$5" mux="$6"
|
||||
local ll="listen ${lport} default_server"
|
||||
local ll6=""
|
||||
local ex=""
|
||||
|
||||
if [ "$mux" = "1" ]; then
|
||||
if [ "$ssl" = "1" ]; then
|
||||
ll="listen unix:${SOCKET_DIR}/https_${lport}.sock default_server ssl proxy_protocol"
|
||||
else
|
||||
ll="listen unix:${SOCKET_DIR}/http_${lport}.sock default_server proxy_protocol"
|
||||
fi
|
||||
elif [ "$ssl" = "1" ]; then
|
||||
ll="${ll} ssl"
|
||||
fi
|
||||
if [ "$mux" != "1" ] && [ "$ipv6_listen" = "1" ]; then
|
||||
ll6="listen [::]:${lport} default_server"
|
||||
[ "$ssl" = "1" ] && ll6="${ll6} ssl"
|
||||
fi
|
||||
|
||||
if [ "$ssl" = "1" ]; then
|
||||
if [ "$h2s" = "new" ]; then
|
||||
ex=" http2 on;"
|
||||
else
|
||||
ll="${ll} http2"
|
||||
[ -n "$ll6" ] && ll6="${ll6} http2"
|
||||
fi
|
||||
ensure_default_cert
|
||||
fi
|
||||
|
||||
cat > "$conf" <<SEOF
|
||||
server {
|
||||
${ll};
|
||||
SEOF
|
||||
[ -n "$ll6" ] && echo " ${ll6};" >> "$conf"
|
||||
cat >> "$conf" <<SEOF
|
||||
server_name _;
|
||||
${ex}
|
||||
SEOF
|
||||
if [ "$ssl" = "1" ]; then
|
||||
cat >> "$conf" <<SEOF
|
||||
ssl_certificate ${DEFAULT_CERT_DIR}/fullchain.pem;
|
||||
ssl_certificate_key ${DEFAULT_CERT_DIR}/key.pem;
|
||||
SEOF
|
||||
fi
|
||||
cat >> "$conf" <<'SEOF'
|
||||
return 444;
|
||||
}
|
||||
SEOF
|
||||
}
|
||||
|
||||
ensure_default_server() {
|
||||
local lport="$1" ssl="$2" h2s="$3" ipv6_listen="$4" mux="$5"
|
||||
local key=$(echo "${lport}_${ssl}_${mux}" | tr -c 'A-Za-z0-9_' '_')
|
||||
local mark="/tmp/minigate_default_${key}.tmp"
|
||||
[ -f "$mark" ] && return 0
|
||||
: > "$mark"
|
||||
write_default_server "${SITES_DIR}/000_default_${key}.conf" "$lport" "$ssl" "$h2s" "$ipv6_listen" "$mux"
|
||||
}
|
||||
|
||||
write_redirect_server() {
|
||||
local conf="$1" domain="$2" lport="$3"
|
||||
local authority="$domain"
|
||||
[ "$lport" = "443" ] || authority="${domain}:${lport}"
|
||||
|
||||
cat >> "$conf" <<SEOF
|
||||
server {
|
||||
listen unix:${SOCKET_DIR}/http_${lport}.sock proxy_protocol;
|
||||
server_name ${domain};
|
||||
return 308 https://${authority}\$request_uri;
|
||||
}
|
||||
SEOF
|
||||
}
|
||||
|
||||
ensure_stream_server() {
|
||||
local lport="$1" ipv6_listen="$2"
|
||||
local mark="/tmp/minigate_stream_${lport}.tmp"
|
||||
local conf="${STREAMS_DIR}/stream_${lport}.conf"
|
||||
[ -f "$mark" ] && return 0
|
||||
: > "$mark"
|
||||
|
||||
cat > "$conf" <<SEOF
|
||||
map \$ssl_preread_protocol \$minigate_backend_${lport} {
|
||||
"" unix:${SOCKET_DIR}/http_${lport}.sock;
|
||||
default unix:${SOCKET_DIR}/https_${lport}.sock;
|
||||
}
|
||||
server {
|
||||
listen ${lport};
|
||||
SEOF
|
||||
[ "$ipv6_listen" = "1" ] && echo " listen [::]:${lport};" >> "$conf"
|
||||
cat >> "$conf" <<SEOF
|
||||
ssl_preread on;
|
||||
proxy_protocol on;
|
||||
proxy_pass \$minigate_backend_${lport};
|
||||
}
|
||||
SEOF
|
||||
}
|
||||
|
||||
# 写一个完整的 server block(带 proxy_pass),同时支持 IPv6
|
||||
write_server() {
|
||||
local conf="$1" domain="$2" lport="$3" taddr="$4" tport="$5" ssl="$6" h2="$7" ws="$8" h2s="$9"
|
||||
shift 9
|
||||
local ipv6_listen="${1:-0}"
|
||||
local mux="${2:-0}"
|
||||
|
||||
log "生成: $domain:${lport} -> ${taddr}:${tport} (ipv6=$ipv6_listen)"
|
||||
|
||||
local ll="listen ${lport}"; local ll6=""; local ex=""
|
||||
if [ "$mux" = "1" ]; then
|
||||
ll="listen unix:${SOCKET_DIR}/https_${lport}.sock ssl proxy_protocol"
|
||||
elif [ "$ssl" = "1" ]; then
|
||||
ll="${ll} ssl"
|
||||
fi
|
||||
|
||||
# IPv6 listen
|
||||
if [ "$mux" != "1" ] && [ "$ipv6_listen" = "1" ]; then
|
||||
ll6="listen [::]:${lport}"
|
||||
[ "$ssl" = "1" ] && ll6="${ll6} ssl"
|
||||
fi
|
||||
|
||||
# HTTPS 模式自动启用 HTTP/2
|
||||
if [ "$ssl" = "1" ]; then
|
||||
if [ "$h2s" = "new" ]; then
|
||||
ex=" http2 on;"
|
||||
else
|
||||
ll="${ll} http2"
|
||||
[ -n "$ll6" ] && ll6="${ll6} http2"
|
||||
fi
|
||||
fi
|
||||
|
||||
cat >> "$conf" <<SEOF
|
||||
server {
|
||||
${ll};
|
||||
SEOF
|
||||
[ -n "$ll6" ] && echo " ${ll6};" >> "$conf"
|
||||
cat >> "$conf" <<SEOF
|
||||
server_name ${domain};
|
||||
${ex}
|
||||
SEOF
|
||||
if [ "$ssl" = "1" ]; then
|
||||
local cp=$(find_cert "$domain")
|
||||
[ -n "$cp" ] && cat >> "$conf" <<SEOF
|
||||
ssl_certificate ${cp}/fullchain.pem;
|
||||
ssl_certificate_key ${cp}/key.pem;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
SEOF
|
||||
fi
|
||||
|
||||
# 判断目标地址是否为 IPv6
|
||||
local target_host="$taddr"
|
||||
echo "$taddr" | grep -q ':' && target_host="[${taddr}]"
|
||||
|
||||
cat >> "$conf" <<SEOF
|
||||
location / {
|
||||
# 拒绝直接 IP 访问(非域名)
|
||||
if (\$host ~* "^\d+\.\d+\.\d+\.\d+") {
|
||||
return 444;
|
||||
}
|
||||
proxy_pass http://${target_host}:${tport};
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$minigate_client_addr;
|
||||
proxy_set_header X-Forwarded-For \$minigate_client_addr;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
proxy_http_version 1.1;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_buffering off;
|
||||
SEOF
|
||||
[ "$ws" = "1" ] && cat >> "$conf" <<SEOF
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
SEOF
|
||||
echo " }" >> "$conf"; echo "}" >> "$conf"
|
||||
}
|
||||
|
||||
generate_sites() {
|
||||
mkdir -p "$SITES_DIR" "$STREAMS_DIR" "$SOCKET_DIR"
|
||||
rm -f "$SITES_DIR"/*.conf
|
||||
rm -f "$STREAMS_DIR"/*.conf
|
||||
local h2s=$(check_h2)
|
||||
local idx=0
|
||||
local ipv6_listen=$(get_ipv6_listen)
|
||||
local http_redirect=$(get_http_redirect)
|
||||
|
||||
# === 通配符域名设置 (proxy_wildcard sections) ===
|
||||
local wc_sections=$(uci -q show minigate | grep '=proxy_wildcard$' | cut -d. -f2 | cut -d= -f1)
|
||||
for sec in $wc_sections; do
|
||||
local enabled=$(uci -q get minigate.${sec}.enabled)
|
||||
[ "$enabled" = "1" ] || continue
|
||||
local domain=$(uci -q get minigate.${sec}.domain)
|
||||
[ -z "$domain" ] && continue
|
||||
local lport=$(uci -q get minigate.${sec}.listen_port); lport=${lport:-443}
|
||||
local ssl=$(uci -q get minigate.${sec}.ssl); ssl=${ssl:-0}
|
||||
local base=$(echo "$domain" | sed 's/^\*\.//')
|
||||
echo "${lport}|${ssl}" > "/tmp/minigate_proxy_${base}.tmp"
|
||||
log "通配符: $domain (端口:$lport https:$ssl ipv6:$ipv6_listen)"
|
||||
done
|
||||
|
||||
# === 非通配符代理规则 (proxy sections) ===
|
||||
local sections=$(uci -q show minigate | grep '=proxy$' | cut -d. -f2 | cut -d= -f1)
|
||||
for sec in $sections; do
|
||||
local enabled=$(uci -q get minigate.${sec}.enabled)
|
||||
[ "$enabled" = "1" ] || continue
|
||||
local domain=$(uci -q get minigate.${sec}.domain)
|
||||
local lport=$(uci -q get minigate.${sec}.listen_port); lport=${lport:-443}
|
||||
local taddr=$(uci -q get minigate.${sec}.target_addr)
|
||||
local tport=$(uci -q get minigate.${sec}.target_port); tport=${tport:-80}
|
||||
local ssl=$(uci -q get minigate.${sec}.ssl); ssl=${ssl:-1}
|
||||
local h2=$(uci -q get minigate.${sec}.http2); h2=${h2:-1}
|
||||
local ws=$(uci -q get minigate.${sec}.websocket); ws=${ws:-0}
|
||||
[ -z "$domain" ] || [ -z "$taddr" ] && continue
|
||||
local mux=0
|
||||
if [ "$http_redirect" = "1" ] && [ "$ssl" = "1" ]; then
|
||||
mux=1
|
||||
ensure_stream_server "$lport" "$ipv6_listen"
|
||||
ensure_default_server "$lport" "0" "$h2s" "$ipv6_listen" "1"
|
||||
fi
|
||||
ensure_default_server "$lport" "$ssl" "$h2s" "$ipv6_listen" "$mux"
|
||||
idx=$((idx + 1))
|
||||
local conf="${SITES_DIR}/site_${idx}.conf"; > "$conf"
|
||||
write_server "$conf" "$domain" "$lport" "$taddr" "$tport" "$ssl" "$h2" "$ws" "$h2s" "$ipv6_listen" "$mux"
|
||||
[ "$mux" = "1" ] && write_redirect_server "$conf" "$domain" "$lport"
|
||||
done
|
||||
|
||||
# === 子域名规则:继承通配符主域名设置 ===
|
||||
local subs=$(uci -q show minigate | grep '=subproxy$' | cut -d. -f2 | cut -d= -f1)
|
||||
for sec in $subs; do
|
||||
local parent=$(uci -q get minigate.${sec}.parent_domain)
|
||||
local prefix=$(uci -q get minigate.${sec}.prefix)
|
||||
local taddr=$(uci -q get minigate.${sec}.target_addr)
|
||||
local tport=$(uci -q get minigate.${sec}.target_port); tport=${tport:-80}
|
||||
local ws=$(uci -q get minigate.${sec}.websocket); ws=${ws:-0}
|
||||
[ -z "$parent" ] || [ -z "$prefix" ] || [ -z "$taddr" ] && continue
|
||||
|
||||
local domain="${prefix}.${parent}"
|
||||
|
||||
local parent_conf="/tmp/minigate_proxy_${parent}.tmp"
|
||||
local lport="443" ssl="0"
|
||||
if [ -f "$parent_conf" ]; then
|
||||
local settings=$(cat "$parent_conf")
|
||||
lport=$(echo "$settings" | cut -d'|' -f1)
|
||||
ssl=$(echo "$settings" | cut -d'|' -f2)
|
||||
fi
|
||||
|
||||
idx=$((idx + 1))
|
||||
local mux=0
|
||||
if [ "$http_redirect" = "1" ] && [ "$ssl" = "1" ]; then
|
||||
mux=1
|
||||
ensure_stream_server "$lport" "$ipv6_listen"
|
||||
ensure_default_server "$lport" "0" "$h2s" "$ipv6_listen" "1"
|
||||
fi
|
||||
ensure_default_server "$lport" "$ssl" "$h2s" "$ipv6_listen" "$mux"
|
||||
local conf="${SITES_DIR}/site_${idx}.conf"; > "$conf"
|
||||
write_server "$conf" "$domain" "$lport" "$taddr" "$tport" "$ssl" "1" "$ws" "$h2s" "$ipv6_listen" "$mux"
|
||||
[ "$mux" = "1" ] && write_redirect_server "$conf" "$domain" "$lport"
|
||||
done
|
||||
|
||||
rm -f /tmp/minigate_proxy_*.tmp
|
||||
rm -f /tmp/minigate_default_*.tmp
|
||||
rm -f /tmp/minigate_stream_*.tmp
|
||||
}
|
||||
|
||||
do_stop() {
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
local pid=$(cat "$PID_FILE" 2>/dev/null)
|
||||
if [ -n "$pid" ]; then
|
||||
kill "$pid" 2>/dev/null
|
||||
local i=0
|
||||
while [ $i -lt 10 ] && kill -0 "$pid" 2>/dev/null; do
|
||||
sleep 1; i=$((i + 1))
|
||||
done
|
||||
kill -0 "$pid" 2>/dev/null && kill -9 "$pid" 2>/dev/null
|
||||
fi
|
||||
rm -f "$PID_FILE"
|
||||
rm -f "$SOCKET_DIR"/*.sock
|
||||
log "已停止"
|
||||
fi
|
||||
}
|
||||
|
||||
prepare_config() {
|
||||
generate_sites || return 1
|
||||
generate_main_conf || return 1
|
||||
nginx -t -c "$NGINX_CONF" >> "$LOGFILE" 2>&1 || { log "配置错误"; return 1; }
|
||||
}
|
||||
|
||||
start_prepared() {
|
||||
do_stop 2>/dev/null
|
||||
sleep 1
|
||||
mkdir -p "$SOCKET_DIR"
|
||||
rm -f "$SOCKET_DIR"/*.sock
|
||||
nginx -c "$NGINX_CONF" >> "$LOGFILE" 2>&1 && log "已启动" || { log "启动失败"; return 1; }
|
||||
}
|
||||
|
||||
do_start() {
|
||||
[ -z "$(which nginx 2>/dev/null)" ] && { log "nginx未安装"; return 1; }
|
||||
prepare_config || return 1
|
||||
start_prepared
|
||||
}
|
||||
|
||||
do_reload() {
|
||||
local old_mux=0 new_mux=0
|
||||
grep -q '^stream {' "$NGINX_CONF" 2>/dev/null && old_mux=1
|
||||
prepare_config || return 1
|
||||
grep -q '^stream {' "$NGINX_CONF" 2>/dev/null && new_mux=1
|
||||
if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE" 2>/dev/null) 2>/dev/null; then
|
||||
if [ "$old_mux" != "$new_mux" ]; then
|
||||
start_prepared
|
||||
else
|
||||
kill -HUP $(cat "$PID_FILE") && log "已重载"
|
||||
fi
|
||||
else
|
||||
start_prepared
|
||||
fi
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start) do_start ;;
|
||||
stop) do_stop ;;
|
||||
reload) do_reload ;;
|
||||
test) prepare_config ;;
|
||||
*) echo "用法: $0 {start|stop|reload|test}" ;;
|
||||
esac
|
||||
Executable
+266
@@ -0,0 +1,266 @@
|
||||
#!/bin/sh
|
||||
set -u
|
||||
|
||||
PURGE="${1:-0}"
|
||||
ROOT="${MINIGATE_ROOT:-}"
|
||||
SKIP_PACKAGES="${MINIGATE_SKIP_PACKAGES:-0}"
|
||||
SKIP_SERVICES="${MINIGATE_SKIP_SERVICES:-0}"
|
||||
PRESERVE_DIR=""
|
||||
LOCK_HELD=0
|
||||
|
||||
[ "$PURGE" = "0" ] || [ "$PURGE" = "1" ] || {
|
||||
echo "用法: $0 0|1" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
root_path() {
|
||||
printf '%s%s\n' "$ROOT" "$1"
|
||||
}
|
||||
|
||||
LOG_FILE="$(root_path /tmp/minigate-uninstall.log)"
|
||||
|
||||
log() {
|
||||
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true
|
||||
printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE"
|
||||
[ -n "$ROOT" ] || logger -t minigate-uninstall "$*" 2>/dev/null || true
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [ "$LOCK_HELD" = "1" ]; then
|
||||
flock -u 9 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
trap 'exit 1' HUP INT TERM
|
||||
|
||||
if [ -z "$ROOT" ] && command -v flock >/dev/null 2>&1; then
|
||||
exec 9>"$(root_path /var/lock/minigate-update.lock)"
|
||||
if ! flock -n 9; then
|
||||
log "已有更新或卸载任务运行,取消本次卸载"
|
||||
exit 1
|
||||
fi
|
||||
LOCK_HELD=1
|
||||
fi
|
||||
|
||||
preserve_data() {
|
||||
local config data etc_dir
|
||||
config="$(root_path /etc/config/minigate)"
|
||||
data="$(root_path /etc/minigate)"
|
||||
etc_dir="$(root_path /etc)"
|
||||
PRESERVE_DIR="$(root_path /tmp)/minigate-uninstall-preserve.$$"
|
||||
mkdir -p "$PRESERVE_DIR" || return 1
|
||||
|
||||
if [ -f "$config" ]; then
|
||||
cp -p "$config" "$PRESERVE_DIR/config" || return 1
|
||||
fi
|
||||
if [ -d "$data" ]; then
|
||||
tar -cf "$PRESERVE_DIR/data.tar" -C "$etc_dir" minigate || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
restore_data() {
|
||||
local config etc_dir
|
||||
[ -n "$PRESERVE_DIR" ] || return 0
|
||||
config="$(root_path /etc/config/minigate)"
|
||||
etc_dir="$(root_path /etc)"
|
||||
|
||||
if [ -f "$PRESERVE_DIR/config" ]; then
|
||||
mkdir -p "$(dirname "$config")" || return 1
|
||||
cp -p "$PRESERVE_DIR/config" "$config" || return 1
|
||||
fi
|
||||
if [ -f "$PRESERVE_DIR/data.tar" ]; then
|
||||
mkdir -p "$etc_dir" || return 1
|
||||
tar -xf "$PRESERVE_DIR/data.tar" -C "$etc_dir" || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
restore_or_keep_backup() {
|
||||
if restore_data; then
|
||||
rm -rf "$PRESERVE_DIR"
|
||||
PRESERVE_DIR=""
|
||||
return 0
|
||||
fi
|
||||
log "保留数据恢复失败,临时备份位于 ${PRESERVE_DIR}"
|
||||
PRESERVE_DIR=""
|
||||
return 1
|
||||
}
|
||||
|
||||
restart_after_failure() {
|
||||
local init
|
||||
[ "$SKIP_SERVICES" = "1" ] && return 0
|
||||
init="$(root_path /etc/init.d/minigate)"
|
||||
if [ -x "$init" ]; then
|
||||
"$init" enable >/dev/null 2>&1 || true
|
||||
"$init" start 9>&- >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
stop_process_tree() {
|
||||
local pid child
|
||||
pid="$1"
|
||||
if command -v pgrep >/dev/null 2>&1; then
|
||||
for child in $(pgrep -P "$pid" 2>/dev/null); do
|
||||
stop_process_tree "$child"
|
||||
done
|
||||
fi
|
||||
kill "$pid" 2>/dev/null || true
|
||||
}
|
||||
|
||||
stop_services() {
|
||||
local init pid
|
||||
[ "$SKIP_SERVICES" = "1" ] && return 0
|
||||
|
||||
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
|
||||
stop_process_tree "$pid"
|
||||
done
|
||||
fi
|
||||
|
||||
init="$(root_path /etc/init.d/minigate)"
|
||||
if [ -x "$init" ]; then
|
||||
"$init" stop 9>&- >/dev/null 2>&1 || true
|
||||
"$init" disable >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if [ -z "$ROOT" ]; then
|
||||
pid="$(cat "$(root_path /var/run/minigate-nginx.pid)" 2>/dev/null || true)"
|
||||
case "$pid" in
|
||||
''|*[!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
|
||||
}
|
||||
|
||||
remove_nft_rule_for_set() {
|
||||
local set_name handle
|
||||
set_name="$1"
|
||||
command -v nft >/dev/null 2>&1 || return 0
|
||||
for handle in $(nft -a list chain inet fw4 input 2>/dev/null |
|
||||
awk -v marker="@${set_name}" 'index($0, marker) { for (i=1; i<=NF; i++) if ($i=="handle") print $(i+1) }'); do
|
||||
nft delete rule inet fw4 input handle "$handle" 2>/dev/null || true
|
||||
done
|
||||
nft delete set inet fw4 "$set_name" 2>/dev/null || true
|
||||
}
|
||||
|
||||
remove_firewall_state() {
|
||||
[ "$SKIP_SERVICES" = "1" ] && return 0
|
||||
[ -z "$ROOT" ] || return 0
|
||||
|
||||
remove_nft_rule_for_set login_banned_v4
|
||||
remove_nft_rule_for_set minigate_blocked
|
||||
if command -v iptables >/dev/null 2>&1; then
|
||||
while iptables -D INPUT -m set --match-set minigate_blocked src -j DROP >/dev/null 2>&1; do :; done
|
||||
fi
|
||||
command -v ipset >/dev/null 2>&1 && ipset destroy minigate_blocked >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
remove_cron() {
|
||||
local cron_file cron_tmp
|
||||
cron_file="$(root_path /etc/crontabs/root)"
|
||||
if [ -f "$cron_file" ]; then
|
||||
cron_tmp="${cron_file}.minigate.$$"
|
||||
if sed '/minigate/d' "$cron_file" > "$cron_tmp"; then
|
||||
cat "$cron_tmp" > "$cron_file"
|
||||
fi
|
||||
rm -f "$cron_tmp"
|
||||
fi
|
||||
if [ "$SKIP_SERVICES" != "1" ] && [ -z "$ROOT" ] && [ -x /etc/init.d/cron ]; then
|
||||
/etc/init.d/cron restart 9>&- >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
remove_registered_packages() {
|
||||
local package
|
||||
[ "$SKIP_PACKAGES" = "1" ] && return 0
|
||||
[ -z "$ROOT" ] || return 0
|
||||
|
||||
if command -v opkg >/dev/null 2>&1; then
|
||||
for package in minigate luci-app-minigate; do
|
||||
if opkg list-installed 2>/dev/null | awk -v name="$package" '$1 == name { found=1 } END { exit !found }'; then
|
||||
opkg remove "$package" 9>&- >> "$LOG_FILE" 2>&1 || return 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
for package in minigate luci-app-minigate; do
|
||||
if apk info -e "$package" >/dev/null 2>&1; then
|
||||
apk del "$package" 9>&- >> "$LOG_FILE" 2>&1 || return 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
remove_program_files() {
|
||||
rm -f \
|
||||
"$(root_path /etc/init.d/minigate)" \
|
||||
"$(root_path /usr/lib/lua/luci/controller/minigate.lua)" \
|
||||
"$(root_path /usr/lib/lua/luci/i18n/minigate.zh-cn.lmo)"
|
||||
rm -rf \
|
||||
"$(root_path /usr/lib/minigate)" \
|
||||
"$(root_path /usr/lib/lua/luci/model/cbi/minigate)" \
|
||||
"$(root_path /usr/lib/lua/luci/view/minigate)"
|
||||
}
|
||||
|
||||
remove_runtime_files() {
|
||||
local tmp_dir log_dir
|
||||
tmp_dir="$(root_path /tmp)"
|
||||
log_dir="$(root_path /var/log)"
|
||||
rm -rf \
|
||||
"$(root_path /var/run/minigate)" \
|
||||
"$(root_path /tmp/minigate-update)" \
|
||||
"$(root_path /tmp/minigate-geo-cache)" \
|
||||
"$(root_path /tmp/minigate_geo)"
|
||||
rm -f \
|
||||
"$(root_path /var/run/minigate-nginx.pid)" \
|
||||
"$(root_path /tmp/minigate-update-state.json)" \
|
||||
"$(root_path /tmp/minigate-update-release.json)" \
|
||||
"$(root_path /tmp/minigate-update-last-check)" \
|
||||
"$tmp_dir"/minigate_proxy_*.tmp \
|
||||
"$tmp_dir"/minigate_default_*.tmp \
|
||||
"$tmp_dir"/minigate_stream_*.tmp \
|
||||
"$tmp_dir"/minigate_ddns_*.cache
|
||||
|
||||
if [ "$PURGE" = "1" ]; then
|
||||
rm -f "$(root_path /etc/config/minigate)" "$log_dir"/minigate-*.log
|
||||
rm -rf "$(root_path /etc/minigate)"
|
||||
fi
|
||||
}
|
||||
|
||||
refresh_luci() {
|
||||
rm -f "$(root_path /tmp/luci-indexcache)" "$(root_path /tmp/luci-modulecache)" 2>/dev/null || true
|
||||
if [ "$SKIP_SERVICES" != "1" ] && [ -z "$ROOT" ] && [ -x /etc/init.d/uhttpd ]; then
|
||||
/etc/init.d/uhttpd restart 9>&- >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
log "开始卸载(清除数据=${PURGE})"
|
||||
if [ "$PURGE" != "1" ] && ! preserve_data; then
|
||||
log "无法创建保留数据的临时备份,已取消卸载"
|
||||
[ -z "$PRESERVE_DIR" ] || rm -rf "$PRESERVE_DIR"
|
||||
PRESERVE_DIR=""
|
||||
exit 1
|
||||
fi
|
||||
stop_services
|
||||
remove_firewall_state
|
||||
remove_cron
|
||||
|
||||
if ! remove_registered_packages; then
|
||||
log "包管理器卸载失败,已停止清理"
|
||||
[ "$PURGE" = "1" ] || restore_or_keep_backup
|
||||
restart_after_failure
|
||||
exit 1
|
||||
fi
|
||||
|
||||
remove_program_files
|
||||
remove_runtime_files
|
||||
if [ "$PURGE" != "1" ] && ! restore_or_keep_backup; then
|
||||
exit 1
|
||||
fi
|
||||
refresh_luci
|
||||
log "卸载完成(清除数据=${PURGE})"
|
||||
Executable
+357
@@ -0,0 +1,357 @@
|
||||
#!/bin/sh
|
||||
|
||||
PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
||||
export PATH
|
||||
|
||||
CURRENT_VERSION="2026.9.18-2"
|
||||
REPOSITORY="tpxcer/minigate"
|
||||
API_URL="https://api.github.com/repos/${REPOSITORY}/releases/latest"
|
||||
DOWNLOAD_ROOT="https://github.com/${REPOSITORY}/releases/download"
|
||||
STATE_FILE="/tmp/minigate-update-state.json"
|
||||
RELEASE_FILE="/tmp/minigate-update-release.json"
|
||||
LAST_CHECK_FILE="/tmp/minigate-update-last-check"
|
||||
LOCK_FILE="/var/lock/minigate-update.lock"
|
||||
TMP_ROOT="/tmp/minigate-update"
|
||||
RUN_LOG="/var/log/minigate-update.log"
|
||||
STATE_CURRENT="$CURRENT_VERSION"
|
||||
|
||||
umask 077
|
||||
|
||||
json_escape() {
|
||||
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\r/ /g'
|
||||
}
|
||||
|
||||
write_state() {
|
||||
local status="$1"
|
||||
local progress="$2"
|
||||
local latest="$3"
|
||||
local available="$4"
|
||||
local running="$5"
|
||||
local success="$6"
|
||||
local message="$7"
|
||||
local tmp="${STATE_FILE}.$$"
|
||||
|
||||
mkdir -p "$TMP_ROOT" "$(dirname "$LOCK_FILE")"
|
||||
printf '{"status":"%s","progress":%s,"current":"%s","latest":"%s","available":%s,"running":%s,"success":%s,"message":"%s"}\n' \
|
||||
"$status" "$progress" "$STATE_CURRENT" "$latest" "$available" "$running" "$success" "$(json_escape "$message")" > "$tmp"
|
||||
mv -f "$tmp" "$STATE_FILE"
|
||||
}
|
||||
|
||||
emit_state() {
|
||||
if [ ! -s "$STATE_FILE" ]; then
|
||||
write_state "idle" 0 "" false false true "点击检查更新获取最新版本"
|
||||
fi
|
||||
cat "$STATE_FILE"
|
||||
}
|
||||
|
||||
download_file() {
|
||||
local url="$1"
|
||||
local output="$2"
|
||||
curl -fL --proto '=https' --tlsv1.2 \
|
||||
--connect-timeout 10 --max-time 120 --retry 2 --retry-delay 1 \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'User-Agent: MiniGate-Updater/1' \
|
||||
-o "$output" "$url"
|
||||
}
|
||||
|
||||
valid_version() {
|
||||
printf '%s' "$1" | grep -Eq '^[0-9]{4}\.(0?[1-9]|1[0-2])\.(0?[1-9]|[12][0-9]|3[01])(-[1-9][0-9]*)?$'
|
||||
}
|
||||
|
||||
version_newer() {
|
||||
awk -v left="$1" -v right="$2" 'BEGIN {
|
||||
split(left, left_parts, "-");
|
||||
split(right, right_parts, "-");
|
||||
split(left_parts[1], a, ".");
|
||||
split(right_parts[1], b, ".");
|
||||
a[4] = left_parts[2] == "" ? 0 : left_parts[2] + 0;
|
||||
b[4] = right_parts[2] == "" ? 0 : right_parts[2] + 0;
|
||||
for (i = 1; i <= 4; i++) {
|
||||
if ((a[i] + 0) > (b[i] + 0)) exit 0;
|
||||
if ((a[i] + 0) < (b[i] + 0)) exit 1;
|
||||
}
|
||||
exit 1;
|
||||
}'
|
||||
}
|
||||
|
||||
fetch_latest() {
|
||||
local metadata="${TMP_ROOT}/latest.$$.json"
|
||||
local tag
|
||||
|
||||
mkdir -p "$TMP_ROOT"
|
||||
if ! download_file "$API_URL" "$metadata" >> "$RUN_LOG" 2>&1; then
|
||||
rm -f "$metadata"
|
||||
return 1
|
||||
fi
|
||||
|
||||
tag=$(jsonfilter -q -i "$metadata" -e '@.tag_name' 2>/dev/null)
|
||||
tag=${tag#v}
|
||||
if ! valid_version "$tag"; then
|
||||
rm -f "$metadata"
|
||||
return 1
|
||||
fi
|
||||
LATEST_VERSION="$tag"
|
||||
chmod 600 "$metadata"
|
||||
mv -f "$metadata" "$RELEASE_FILE"
|
||||
return 0
|
||||
}
|
||||
|
||||
run_check() {
|
||||
local checked_file
|
||||
mkdir -p "$TMP_ROOT" "$(dirname "$LOCK_FILE")"
|
||||
exec 8>"$LOCK_FILE" || return 1
|
||||
if ! flock -n 8; then
|
||||
emit_state
|
||||
return 0
|
||||
fi
|
||||
write_state "checking" 10 "" false true true "正在检查 GitHub Release"
|
||||
if ! fetch_latest; then
|
||||
write_state "error" 0 "" false false false "检查失败,请确认路由器可以访问 GitHub"
|
||||
emit_state
|
||||
return 1
|
||||
fi
|
||||
checked_file=$(mktemp "${TMP_ROOT}/last-check.XXXXXX")
|
||||
if [ -n "$checked_file" ]; then
|
||||
date +%s > "$checked_file"
|
||||
mv -f "$checked_file" "$LAST_CHECK_FILE"
|
||||
fi
|
||||
|
||||
if version_newer "$LATEST_VERSION" "$CURRENT_VERSION"; then
|
||||
write_state "available" 100 "$LATEST_VERSION" true false true "发现新版本,可直接更新"
|
||||
elif [ "$LATEST_VERSION" = "$CURRENT_VERSION" ]; then
|
||||
write_state "up_to_date" 100 "$LATEST_VERSION" false false true "当前已经是最新版本"
|
||||
else
|
||||
write_state "up_to_date" 100 "$LATEST_VERSION" false false true "当前版本高于 GitHub 最新正式版"
|
||||
fi
|
||||
emit_state
|
||||
}
|
||||
|
||||
run_auto_check() {
|
||||
local now last age current status
|
||||
|
||||
current=$(jsonfilter -q -i "$STATE_FILE" -e '@.current' 2>/dev/null)
|
||||
status=$(jsonfilter -q -i "$STATE_FILE" -e '@.status' 2>/dev/null)
|
||||
if [ "$current" = "$CURRENT_VERSION" ] && [ -s "$LAST_CHECK_FILE" ] && \
|
||||
{ [ "$status" = "available" ] || [ "$status" = "up_to_date" ]; }; then
|
||||
now=$(date +%s)
|
||||
last=$(cat "$LAST_CHECK_FILE" 2>/dev/null)
|
||||
case "$last" in
|
||||
''|*[!0-9]*) last=0 ;;
|
||||
esac
|
||||
age=$((now - last))
|
||||
if [ "$age" -ge 0 ] && [ "$age" -lt 600 ]; then
|
||||
emit_state
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
run_check
|
||||
}
|
||||
|
||||
backup_program() {
|
||||
local output="$1"
|
||||
local paths=""
|
||||
local path
|
||||
|
||||
for path in \
|
||||
etc/init.d/minigate \
|
||||
etc/config/minigate \
|
||||
usr/lib/minigate \
|
||||
usr/lib/lua/luci/controller/minigate.lua \
|
||||
usr/lib/lua/luci/model/cbi/minigate \
|
||||
usr/lib/lua/luci/view/minigate; do
|
||||
[ -e "/$path" ] && paths="$paths $path"
|
||||
done
|
||||
|
||||
[ -n "$paths" ] || return 1
|
||||
tar -czf "${output}.new" -C / $paths || return 1
|
||||
tar -tzf "${output}.new" >/dev/null 2>&1 || return 1
|
||||
chmod 600 "${output}.new"
|
||||
mv -f "${output}.new" "$output"
|
||||
}
|
||||
|
||||
restore_program() {
|
||||
local backup="$1"
|
||||
|
||||
/etc/init.d/minigate stop >> "$RUN_LOG" 2>&1 || true
|
||||
rm -f /etc/init.d/minigate /usr/lib/lua/luci/controller/minigate.lua
|
||||
rm -rf /usr/lib/minigate \
|
||||
/usr/lib/lua/luci/model/cbi/minigate \
|
||||
/usr/lib/lua/luci/view/minigate
|
||||
tar -xzf "$backup" -C / >> "$RUN_LOG" 2>&1 || return 1
|
||||
chmod +x /etc/init.d/minigate /usr/lib/minigate/*.sh 2>/dev/null || true
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-modulecache
|
||||
/etc/init.d/minigate start >> "$RUN_LOG" 2>&1 9>&- || true
|
||||
/etc/init.d/uhttpd restart >> "$RUN_LOG" 2>&1 9>&- || true
|
||||
|
||||
if [ "$(uci -q get minigate.global.enabled)" = "1" ]; then
|
||||
local pid
|
||||
pid=$(cat /var/run/minigate-nginx.pid 2>/dev/null)
|
||||
[ -n "$pid" ] && kill -0 "$pid" 2>/dev/null || return 1
|
||||
nginx -t -c /etc/minigate/nginx/minigate.conf >> "$RUN_LOG" 2>&1 || return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
verify_install() {
|
||||
grep -Fxq "CURRENT_VERSION=\"${LATEST_VERSION}\"" /usr/lib/minigate/update.sh || return 1
|
||||
[ -x /etc/init.d/minigate ] || return 1
|
||||
[ -x /usr/lib/minigate/proxy.sh ] || return 1
|
||||
|
||||
if [ "$(uci -q get minigate.global.enabled)" = "1" ]; then
|
||||
nginx -t -c /etc/minigate/nginx/minigate.conf >> "$RUN_LOG" 2>&1 || return 1
|
||||
local pid
|
||||
pid=$(cat /var/run/minigate-nginx.pid 2>/dev/null)
|
||||
[ -n "$pid" ] && kill -0 "$pid" 2>/dev/null || return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
run_apply_locked() {
|
||||
local work_dir source_name source_file sums_file expected actual valid_hash backup
|
||||
local confirmed_version="$1"
|
||||
|
||||
if ! valid_version "$confirmed_version"; then
|
||||
write_state "error" 0 "" false false false "请先检查更新并确认本次更新内容"
|
||||
return 1
|
||||
fi
|
||||
|
||||
: > "$RUN_LOG"
|
||||
write_state "checking" 5 "" false true true "正在检查最新版本"
|
||||
if ! fetch_latest; then
|
||||
write_state "error" 0 "" false false false "检查失败,请确认路由器可以访问 GitHub"
|
||||
return 1
|
||||
fi
|
||||
if [ "$LATEST_VERSION" != "$confirmed_version" ]; then
|
||||
write_state "error" 0 "$LATEST_VERSION" true false false "最新版本已变化,请重新查看更新内容并确认"
|
||||
return 1
|
||||
fi
|
||||
if ! version_newer "$LATEST_VERSION" "$CURRENT_VERSION"; then
|
||||
write_state "up_to_date" 100 "$LATEST_VERSION" false false true "当前已经是最新版本"
|
||||
return 0
|
||||
fi
|
||||
|
||||
work_dir=$(mktemp -d "${TMP_ROOT}/work.XXXXXX") || {
|
||||
write_state "error" 0 "$LATEST_VERSION" true false false "无法创建更新临时目录"
|
||||
return 1
|
||||
}
|
||||
source_name="minigate-v${LATEST_VERSION}-src.tar.gz"
|
||||
source_file="${work_dir}/${source_name}"
|
||||
sums_file="${work_dir}/SHA256SUMS"
|
||||
|
||||
write_state "downloading" 20 "$LATEST_VERSION" true true true "正在下载版本清单"
|
||||
if ! download_file "${DOWNLOAD_ROOT}/v${LATEST_VERSION}/SHA256SUMS" "$sums_file" >> "$RUN_LOG" 2>&1; then
|
||||
write_state "error" 0 "$LATEST_VERSION" true false false "下载校验清单失败,未修改当前版本"
|
||||
rm -rf "$work_dir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
write_state "downloading" 40 "$LATEST_VERSION" true true true "正在下载更新包"
|
||||
if ! download_file "${DOWNLOAD_ROOT}/v${LATEST_VERSION}/${source_name}" "$source_file" >> "$RUN_LOG" 2>&1; then
|
||||
write_state "error" 0 "$LATEST_VERSION" true false false "下载更新包失败,未修改当前版本"
|
||||
rm -rf "$work_dir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
write_state "verifying" 55 "$LATEST_VERSION" true true true "正在校验 SHA-256 和版本信息"
|
||||
expected=$(awk -v file="$source_name" '$2 == file || $2 == ("*" file) { print $1; exit }' "$sums_file")
|
||||
valid_hash=$(printf '%s' "$expected" | grep -Ec '^[0-9a-fA-F]{64}$')
|
||||
actual=$(sha256sum "$source_file" | awk '{print $1}')
|
||||
if [ "$valid_hash" != "1" ] || [ "$(printf '%s' "$expected" | tr 'A-F' 'a-f')" != "$actual" ]; then
|
||||
write_state "error" 0 "$LATEST_VERSION" true false false "SHA-256 校验失败,未修改当前版本"
|
||||
rm -rf "$work_dir"
|
||||
return 1
|
||||
fi
|
||||
if tar -tzf "$source_file" | awk '{
|
||||
path=$0; sub(/^\.\//, "", path);
|
||||
if (path ~ /^\// || path ~ /(^|\/)\.\.(\/|$)/) bad=1;
|
||||
} END { exit bad ? 0 : 1 }'; then
|
||||
write_state "error" 0 "$LATEST_VERSION" true false false "更新包路径不安全,已拒绝安装"
|
||||
rm -rf "$work_dir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mkdir -p "${work_dir}/source"
|
||||
if ! tar -xzf "$source_file" -C "${work_dir}/source" >> "$RUN_LOG" 2>&1; then
|
||||
write_state "error" 0 "$LATEST_VERSION" true false false "更新包无法解压,未修改当前版本"
|
||||
rm -rf "$work_dir"
|
||||
return 1
|
||||
fi
|
||||
if ! grep -Fxq "PKG_VERSION:=${LATEST_VERSION}" "${work_dir}/source/Makefile" || \
|
||||
! grep -Fxq "PKG_VERSION=\"${LATEST_VERSION}\"" "${work_dir}/source/scripts/build-ipk.sh" || \
|
||||
! grep -Fxq "CURRENT_VERSION=\"${LATEST_VERSION}\"" "${work_dir}/source/root/usr/lib/minigate/update.sh"; then
|
||||
write_state "error" 0 "$LATEST_VERSION" true false false "更新包版本信息不一致,已拒绝安装"
|
||||
rm -rf "$work_dir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
backup="${TMP_ROOT}/backup-before-${CURRENT_VERSION}.tar.gz"
|
||||
write_state "backing_up" 65 "$LATEST_VERSION" true true true "正在备份当前程序和配置"
|
||||
if ! backup_program "$backup"; then
|
||||
write_state "error" 0 "$LATEST_VERSION" true false false "备份失败,未修改当前版本"
|
||||
rm -rf "$work_dir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
write_state "installing" 75 "$LATEST_VERSION" true true true "正在安装新版本,请勿断电"
|
||||
if ! /bin/sh "${work_dir}/source/install.sh" >> "$RUN_LOG" 2>&1; then
|
||||
write_state "rolling_back" 85 "$LATEST_VERSION" true true false "安装失败,正在恢复原版本"
|
||||
if restore_program "$backup"; then
|
||||
write_state "rolled_back" 100 "$LATEST_VERSION" true false false "安装失败,已恢复原版本"
|
||||
else
|
||||
write_state "rollback_failed" 100 "$LATEST_VERSION" true false false "安装失败且自动恢复未完成,请查看更新日志"
|
||||
fi
|
||||
rm -rf "$work_dir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
write_state "restarting" 90 "$LATEST_VERSION" true true true "正在验证并重启服务"
|
||||
sleep 2
|
||||
if ! verify_install; then
|
||||
write_state "rolling_back" 95 "$LATEST_VERSION" true true false "服务验证失败,正在恢复原版本"
|
||||
if restore_program "$backup"; then
|
||||
write_state "rolled_back" 100 "$LATEST_VERSION" true false false "服务验证失败,已恢复原版本"
|
||||
else
|
||||
write_state "rollback_failed" 100 "$LATEST_VERSION" true false false "服务验证失败且自动恢复未完成,请查看更新日志"
|
||||
fi
|
||||
rm -rf "$work_dir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-modulecache
|
||||
/etc/init.d/uhttpd restart >> "$RUN_LOG" 2>&1 9>&- || true
|
||||
STATE_CURRENT="$LATEST_VERSION"
|
||||
write_state "success" 100 "$LATEST_VERSION" false false true "更新成功,服务已恢复"
|
||||
rm -rf "$work_dir"
|
||||
return 0
|
||||
}
|
||||
|
||||
run_apply() {
|
||||
local result
|
||||
|
||||
mkdir -p "$TMP_ROOT" "$(dirname "$LOCK_FILE")"
|
||||
exec 9>"$LOCK_FILE" || return 1
|
||||
if ! flock -n 9; then
|
||||
[ -s "$STATE_FILE" ] || write_state "busy" 0 "" false true false "已有更新任务正在运行"
|
||||
exec 9>&-
|
||||
return 2
|
||||
fi
|
||||
|
||||
run_apply_locked "$1"
|
||||
result=$?
|
||||
flock -u 9 2>/dev/null || true
|
||||
exec 9>&-
|
||||
return "$result"
|
||||
}
|
||||
|
||||
case "${1:-status}" in
|
||||
status) emit_state ;;
|
||||
auto) run_auto_check ;;
|
||||
check) run_check ;;
|
||||
apply) run_apply "${2:-}" ;;
|
||||
*)
|
||||
write_state "error" 0 "" false false false "不支持的操作"
|
||||
emit_state
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
Reference in new issue
Block a user