💐 Sync 2026-10-06 12:33:44

This commit is contained in:
github-actions[bot] committed 2026-10-06 12:33:44 +08:00
1 parent 8be03ce939
commit 7fe69dbb3a
246 files changed
+50330 -3414

No files matched your search

+157
View File
@@ -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
+353
View File
@@ -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 "$@"
+177
View File
@@ -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
View File
@@ -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
+436
View File
@@ -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
+266
View File
@@ -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})"
+357
View File
@@ -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