diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f8408b4..fab1767 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -161,7 +161,10 @@ is open and back to accessory when all are closed. type, native / broadcast and the cleanliness score come from `cleanip.io`, queried directly from each Mac by `PublicAddressLookup` (one request per address family; results are cached per IP for an hour in memory and for a week on disk, and a 429 stops further calls until the next UTC day). Country - codes are validated before being used as flag file names. + codes are validated before being used as flag file names. `server/geoip/` still holds the systemd + timer that syncs MaxMind GeoLite2 onto `getopenstats.com/geoip/` (account and key in + `/etc/openstats/maxmind.env` on the server); the app no longer downloads those files, they are kept + for other uses. - **Per-process traffic** — cumulative bytes from `/usr/bin/nettop`, diffed between samples. - **DNS** — `networksetup -setdnsservers` through the helper (protocol 3), which re-validates the service name and every address; without the helper, a one-off administrator prompt runs the diff --git a/Scripts/deploy_site.sh b/Scripts/deploy_site.sh index 2864dda..d80c59e 100755 --- a/Scripts/deploy_site.sh +++ b/Scripts/deploy_site.sh @@ -24,8 +24,8 @@ echo "内容指纹 v=$STAMP" # 目录归部署用户所有,Caddy 只需读取 "${SSH[@]}" "$HOST" "sudo mkdir -p $ROOT && sudo chown -R \$(id -un):\$(id -gn) $ROOT" -# download/ 是 publish_release.sh 上传的安装包,geoip/ 是以前 GeoLite2 定时任务留下的目录(应用已不再下载), -# 都不在 web/ 里——排除掉,否则 --delete 会把它们删了 +# geoip/ 由服务器上的定时任务生成,download/ 是 publish_release.sh 上传的安装包,都不在 web/ 里—— +# 排除掉,否则 --delete 会把它们删了 rsync -az --delete --exclude /geoip/ --exclude /download/ -e "${SSH[*]}" web/ "$HOST:$ROOT/" "${SSH[@]}" "$HOST" "sudo chmod -R a+rX $ROOT" echo "已同步到 $HOST:$ROOT" diff --git a/server/geoip/configure.sh b/server/geoip/configure.sh new file mode 100755 index 0000000..b30efcc --- /dev/null +++ b/server/geoip/configure.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# 把 MaxMind 账号与 License Key 写到服务器的 /etc/openstats/maxmind.env,然后立刻同步一次 GeoLite2 数据库。 +# 值在本机终端里输入(Key 不回显),只经 ssh 传到服务器,不进仓库、不进日志。 +# +# ./server/geoip/configure.sh +set -euo pipefail +HOST="${SITE_HOST:-debian@51.38.126.148}" +KEY="${SITE_KEY:-$HOME/.ssh/gentpan.pem}" +SSH=(ssh -i "$KEY" -o BatchMode=yes "$HOST") + +read -r -p "MaxMind Account ID: " ACCOUNT +read -r -s -p "MaxMind License Key: " LICENSE; echo +[ -n "$ACCOUNT" ] && [ -n "$LICENSE" ] || { echo "两项都要填" >&2; exit 1; } +case "$ACCOUNT$LICENSE" in *[!A-Za-z0-9_]*) echo "账号或 Key 含有意外字符" >&2; exit 1 ;; esac + +# 通过标准输入写入,命令行参数里不出现 Key +printf 'MAXMIND_ACCOUNT_ID=%s\nMAXMIND_LICENSE_KEY=%s\n' "$ACCOUNT" "$LICENSE" \ + | "${SSH[@]}" 'sudo install -d -m 700 /etc/openstats && sudo tee /etc/openstats/maxmind.env >/dev/null && sudo chmod 600 /etc/openstats/maxmind.env && echo "已写入 /etc/openstats/maxmind.env"' +echo "开始第一次同步(三个库约 80 MB,需要一两分钟)…" +"${SSH[@]}" 'sudo systemctl start openstats-geoip.service && cat /var/www/getopenstats.com/geoip/manifest.json && echo' diff --git a/server/geoip/install.sh b/server/geoip/install.sh new file mode 100755 index 0000000..f2a70d8 --- /dev/null +++ b/server/geoip/install.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# 把 GeoLite2 更新脚本与定时任务装到服务器(不包含 License Key)。 +# ./server/geoip/install.sh +set -euo pipefail +cd "$(dirname "$0")" +HOST="${SITE_HOST:-debian@51.38.126.148}" +KEY="${SITE_KEY:-$HOME/.ssh/gentpan.pem}" +SSH=(ssh -i "$KEY" -o BatchMode=yes "$HOST") + +scp -i "$KEY" -o BatchMode=yes openstats-geoip-update.sh openstats-geoip.service openstats-geoip.timer "$HOST:/tmp/" +"${SSH[@]}" 'set -e + sudo install -m 755 /tmp/openstats-geoip-update.sh /usr/local/bin/openstats-geoip-update + sudo install -m 644 /tmp/openstats-geoip.service /tmp/openstats-geoip.timer /etc/systemd/system/ + sudo install -d -m 755 /var/www/getopenstats.com/geoip + sudo install -d -m 700 /etc/openstats + sudo systemctl daemon-reload + sudo systemctl enable --now openstats-geoip.timer + rm -f /tmp/openstats-geoip-update.sh /tmp/openstats-geoip.service /tmp/openstats-geoip.timer + systemctl list-timers openstats-geoip.timer --no-pager | head -3 + if sudo test -f /etc/openstats/maxmind.env; then echo "已找到 License Key 配置"; else echo "还没有 /etc/openstats/maxmind.env"; fi' diff --git a/server/geoip/openstats-geoip-update.sh b/server/geoip/openstats-geoip-update.sh new file mode 100755 index 0000000..7c191e4 --- /dev/null +++ b/server/geoip/openstats-geoip-update.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# 在服务器上定时下载 MaxMind GeoLite2 数据库,放到官网 /geoip/ 下供 OpenStats 更新。 +# +# 账号与 License Key 只放在 /etc/openstats/maxmind.env(root 所有,权限 600), +# 不进仓库、不进应用。文件内容: +# MAXMIND_ACCOUNT_ID=你的账号 +# MAXMIND_LICENSE_KEY=你的 Key +# +# 每个库先下载 tar.gz 与官方 sha256 校验,解出 .mmdb 后原子替换,最后重写 manifest.json。 +set -euo pipefail + +ENV_FILE="${ENV_FILE:-/etc/openstats/maxmind.env}" +OUT="${OUT:-/var/www/getopenstats.com/geoip}" +EDITIONS=(${EDITIONS:-GeoLite2-ASN GeoLite2-Country GeoLite2-City}) + +[ -r "$ENV_FILE" ] || { echo "缺少 $ENV_FILE" >&2; exit 1; } +# shellcheck disable=SC1090 +source "$ENV_FILE" +: "${MAXMIND_ACCOUNT_ID:?}" "${MAXMIND_LICENSE_KEY:?}" + +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT +mkdir -p "$OUT" + +fetch() { + # 凭据通过 curl 配置从标准输入传入,不出现在进程参数里 + printf 'user = "%s:%s"\n' "$MAXMIND_ACCOUNT_ID" "$MAXMIND_LICENSE_KEY" \ + | curl --config - -fsSL --retry 3 -o "$2" "https://download.maxmind.com/geoip/databases/$1/download?suffix=$3" +} + +entries=() +for edition in "${EDITIONS[@]}"; do + fetch "$edition" "$WORK/$edition.tar.gz" "tar.gz" + fetch "$edition" "$WORK/$edition.tar.gz.sha256" "tar.gz.sha256" + expected="$(cut -d' ' -f1 "$WORK/$edition.tar.gz.sha256")" + actual="$(sha256sum "$WORK/$edition.tar.gz" | cut -d' ' -f1)" + [ "$expected" = "$actual" ] || { echo "$edition 校验失败" >&2; exit 1; } + + tar -xzf "$WORK/$edition.tar.gz" -C "$WORK" + folder="$(find "$WORK" -maxdepth 1 -type d -name "${edition}_*" | sort | tail -1)" + build="${folder##*_}" # 目录名形如 GeoLite2-ASN_20260912 + install -m 644 "$folder/$edition.mmdb" "$OUT/.$edition.mmdb.new" + mv -f "$OUT/.$edition.mmdb.new" "$OUT/$edition.mmdb" + + sha="$(sha256sum "$OUT/$edition.mmdb" | cut -d' ' -f1)" + size="$(stat -c %s "$OUT/$edition.mmdb")" + entries+=("{\"edition\":\"$edition\",\"file\":\"$edition.mmdb\",\"build\":\"$build\",\"sha256\":\"$sha\",\"size\":$size}") + echo "$edition $build $size 字节" +done + +joined="$(IFS=,; echo "${entries[*]}")" +printf '{"generated":"%s","attribution":"This product includes GeoLite2 data created by MaxMind, available from https://www.maxmind.com.","databases":[%s]}\n' \ + "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$joined" > "$OUT/.manifest.json.new" +mv -f "$OUT/.manifest.json.new" "$OUT/manifest.json" +chmod 644 "$OUT/manifest.json" +echo "manifest.json 已更新" diff --git a/server/geoip/openstats-geoip.service b/server/geoip/openstats-geoip.service new file mode 100644 index 0000000..1369b2e --- /dev/null +++ b/server/geoip/openstats-geoip.service @@ -0,0 +1,13 @@ +[Unit] +Description=下载 MaxMind GeoLite2 数据库供 OpenStats 更新 +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/openstats-geoip-update +# 只需要写官网的 geoip 目录 +ProtectSystem=strict +ReadWritePaths=/var/www/getopenstats.com/geoip /tmp +PrivateTmp=true +NoNewPrivileges=true diff --git a/server/geoip/openstats-geoip.timer b/server/geoip/openstats-geoip.timer new file mode 100644 index 0000000..06060c9 --- /dev/null +++ b/server/geoip/openstats-geoip.timer @@ -0,0 +1,10 @@ +[Unit] +Description=每周二、周五更新 GeoLite2(MaxMind 在这两天发布新数据) + +[Timer] +OnCalendar=Tue,Fri *-*-* 08:00:00 UTC +RandomizedDelaySec=30m +Persistent=true + +[Install] +WantedBy=timers.target