Вместо GitHub Actions — .gitea/workflows/build.yml: проверка, тесты, сборка, валидация настоящими Xray и sing-box, публикация в ветку release и релиз через Gitea API (tools/gitea-release.sh). Ни gh CLI, ни GitHub-only экшенов. Для серверов — tools/update-geosite.sh: сверяет sha256 до закачки и выходит молча, если файл не менялся, проверяет сумму до подмены, подменяет атомарно и дёргает перезапуск только когда файл реально обновился. Едет в dist/, поэтому на сервере нужен один базовый URL. В README — cron и systemd timer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f905a76caf
commit
c05f6aa0d7
@@ -0,0 +1,141 @@
|
|||||||
|
name: build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- "data/**"
|
||||||
|
- "tools/**"
|
||||||
|
- ".gitea/workflows/build.yml"
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "data/**"
|
||||||
|
- "tools/**"
|
||||||
|
- ".gitea/workflows/build.yml"
|
||||||
|
schedule:
|
||||||
|
- cron: "17 3 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ github.server_url }}
|
||||||
|
GITEA_REPO: ${{ github.repository }}
|
||||||
|
# Автоматического токена обычно хватает. Если у раннера нет прав на push
|
||||||
|
# или на релизы — заведите секрет RELEASE_TOKEN с правами repo:write.
|
||||||
|
AUTO_TOKEN: ${{ github.token }}
|
||||||
|
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Окружение
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
missing=""
|
||||||
|
for cmd in python3 curl unzip tar git; do
|
||||||
|
command -v "$cmd" >/dev/null 2>&1 || missing="$missing $cmd"
|
||||||
|
done
|
||||||
|
if [ -n "$missing" ]; then
|
||||||
|
echo "не хватает:$missing — доустанавливаем"
|
||||||
|
sudo=""; [ "$(id -u)" -eq 0 ] || sudo=sudo
|
||||||
|
$sudo apt-get update -qq
|
||||||
|
$sudo apt-get install -y -qq python3 curl unzip tar git ca-certificates
|
||||||
|
fi
|
||||||
|
python3 --version
|
||||||
|
|
||||||
|
- name: Проверка исходников
|
||||||
|
run: python3 tools/build.py --check
|
||||||
|
|
||||||
|
- name: Тесты
|
||||||
|
run: python3 -m unittest discover -s tools -t tools
|
||||||
|
|
||||||
|
- name: Внешние инструменты
|
||||||
|
run: |
|
||||||
|
sh tools/fetch-tools.sh "$GITHUB_WORKSPACE/bin"
|
||||||
|
echo "$GITHUB_WORKSPACE/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Сборка
|
||||||
|
run: |
|
||||||
|
SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) python3 tools/build.py
|
||||||
|
|
||||||
|
- name: Проверка .dat настоящим Xray
|
||||||
|
run: |
|
||||||
|
# Роутинг-правило заставляет Xray реально распарсить файл:
|
||||||
|
# битый .dat или отсутствующий тег дают ненулевой код возврата.
|
||||||
|
cat > /tmp/xray-test.json <<'EOF'
|
||||||
|
{
|
||||||
|
"inbounds": [{"port": 10800, "protocol": "socks"}],
|
||||||
|
"outbounds": [{"protocol": "freedom", "tag": "direct"}],
|
||||||
|
"routing": {
|
||||||
|
"rules": [
|
||||||
|
{"type": "field", "domain": ["ext:ipcheck.dat:ipcheck"], "outboundTag": "direct"},
|
||||||
|
{"type": "field", "domain": ["ext:geosite.dat:leak-test"], "outboundTag": "direct"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
XRAY_LOCATION_ASSET="$GITHUB_WORKSPACE/dist" xray run -test -c /tmp/xray-test.json
|
||||||
|
|
||||||
|
- name: Проверка rule-set для sing-box
|
||||||
|
run: |
|
||||||
|
sing-box rule-set decompile --output /tmp/ipcheck.json dist/sing-box/ipcheck.srs
|
||||||
|
grep -q '"2ip.ru"' /tmp/ipcheck.json
|
||||||
|
|
||||||
|
- name: Описание релиза
|
||||||
|
id: notes
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
tag=$(date -u '+%Y.%m.%d-%H%M')
|
||||||
|
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||||
|
{
|
||||||
|
echo "Собрано из \`$(git rev-parse --short HEAD)\`."
|
||||||
|
echo
|
||||||
|
echo '| Категория | Правил |'
|
||||||
|
echo '| --- | ---: |'
|
||||||
|
python3 -c "import json; [print(f'| {k} | {v} |') for k, v in json.load(open('dist/metadata.json'))['categories'].items()]"
|
||||||
|
echo
|
||||||
|
echo "sha256(ipcheck.dat) = \`$(cut -d' ' -f1 dist/ipcheck.dat.sha256sum)\`"
|
||||||
|
} > /tmp/notes.md
|
||||||
|
cat /tmp/notes.md
|
||||||
|
|
||||||
|
- name: Публикация в ветку release
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
token="${RELEASE_TOKEN:-$AUTO_TOKEN}"
|
||||||
|
[ -n "$token" ] || { echo "нет токена для push" >&2; exit 1; }
|
||||||
|
host="${GITEA_URL#*://}"
|
||||||
|
scheme="${GITEA_URL%%://*}"
|
||||||
|
|
||||||
|
cp README.md LICENSE dist/
|
||||||
|
cd dist
|
||||||
|
git init -q -b release
|
||||||
|
git config user.name "gitea-actions"
|
||||||
|
git config user.email "gitea-actions@noreply.local"
|
||||||
|
git add .
|
||||||
|
git commit -q -m "build $(date -u '+%Y-%m-%d %H:%M UTC') ($GITHUB_SHA)"
|
||||||
|
git push -qf "$scheme://gitea-actions:$token@$host/$GITEA_REPO.git" release
|
||||||
|
cd ..
|
||||||
|
rm -f dist/README.md dist/LICENSE
|
||||||
|
echo "ветка release обновлена"
|
||||||
|
|
||||||
|
- name: Релиз в Gitea
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
export GITEA_TOKEN="${RELEASE_TOKEN:-$AUTO_TOKEN}"
|
||||||
|
# Собираем архив вне dist/, иначе tar пакует сам себя на лету.
|
||||||
|
tar -czf /tmp/ipcheck-domains-all.tar.gz -C dist .
|
||||||
|
mv /tmp/ipcheck-domains-all.tar.gz dist/
|
||||||
|
sh tools/gitea-release.sh "${{ steps.notes.outputs.tag }}" /tmp/notes.md \
|
||||||
|
dist/ipcheck.dat \
|
||||||
|
dist/ipcheck.dat.sha256sum \
|
||||||
|
dist/geosite.dat \
|
||||||
|
dist/geosite.dat.sha256sum \
|
||||||
|
dist/sing-box/ipcheck.srs \
|
||||||
|
dist/mihomo/ipcheck.mrs \
|
||||||
|
dist/metadata.json \
|
||||||
|
dist/SHA256SUMS \
|
||||||
|
dist/ipcheck-domains-all.tar.gz
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
name: build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
paths:
|
|
||||||
- "data/**"
|
|
||||||
- "tools/**"
|
|
||||||
- ".github/workflows/build.yml"
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "data/**"
|
|
||||||
- "tools/**"
|
|
||||||
- ".github/workflows/build.yml"
|
|
||||||
schedule:
|
|
||||||
- cron: "17 3 * * *"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.12"
|
|
||||||
|
|
||||||
- name: Lint sources
|
|
||||||
run: python tools/build.py --check
|
|
||||||
|
|
||||||
- name: Unit tests
|
|
||||||
run: python -m unittest discover -s tools -t tools
|
|
||||||
|
|
||||||
- name: Install sing-box
|
|
||||||
run: |
|
|
||||||
mkdir -p "$GITHUB_WORKSPACE/bin"
|
|
||||||
url=$(gh api repos/SagerNet/sing-box/releases/latest \
|
|
||||||
--jq '.assets[] | select(.name | test("-linux-amd64\\.tar\\.gz$")) | .browser_download_url')
|
|
||||||
echo "sing-box: $url"
|
|
||||||
curl -fsSL "$url" | tar -xz --strip-components=1 -C "$GITHUB_WORKSPACE/bin" --wildcards '*/sing-box'
|
|
||||||
echo "$GITHUB_WORKSPACE/bin" >> "$GITHUB_PATH"
|
|
||||||
|
|
||||||
- name: Install mihomo
|
|
||||||
run: |
|
|
||||||
url=$(gh api repos/MetaCubeX/mihomo/releases/latest \
|
|
||||||
--jq '.assets[] | select(.name | test("^mihomo-linux-amd64-v[0-9.]+\\.gz$")) | .browser_download_url')
|
|
||||||
echo "mihomo: $url"
|
|
||||||
curl -fsSL "$url" | gunzip > "$GITHUB_WORKSPACE/bin/mihomo"
|
|
||||||
chmod +x "$GITHUB_WORKSPACE/bin/mihomo"
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: |
|
|
||||||
export SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)
|
|
||||||
python tools/build.py
|
|
||||||
|
|
||||||
- name: Validate geosite.dat with Xray
|
|
||||||
run: |
|
|
||||||
url=$(gh api repos/XTLS/Xray-core/releases/latest \
|
|
||||||
--jq '.assets[] | select(.name == "Xray-linux-64.zip") | .browser_download_url')
|
|
||||||
curl -fsSL -o xray.zip "$url"
|
|
||||||
unzip -o -j xray.zip xray -d "$GITHUB_WORKSPACE/bin"
|
|
||||||
chmod +x "$GITHUB_WORKSPACE/bin/xray"
|
|
||||||
|
|
||||||
# Роутинг-правило заставляет Xray реально распарсить geosite.dat.
|
|
||||||
# Битый файл или отсутствующий тег -> ненулевой exit code.
|
|
||||||
cat > /tmp/xray-test.json <<'EOF'
|
|
||||||
{
|
|
||||||
"inbounds": [{"port": 10800, "protocol": "socks"}],
|
|
||||||
"outbounds": [{"protocol": "freedom", "tag": "direct"}],
|
|
||||||
"routing": {
|
|
||||||
"rules": [
|
|
||||||
{"type": "field", "domain": ["ext:geosite.dat:ipcheck"], "outboundTag": "direct"},
|
|
||||||
{"type": "field", "domain": ["ext:geosite.dat:leak-test"], "outboundTag": "direct"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
XRAY_LOCATION_ASSET="$GITHUB_WORKSPACE/dist" xray run -test -c /tmp/xray-test.json
|
|
||||||
|
|
||||||
- name: Validate sing-box rule-set
|
|
||||||
run: |
|
|
||||||
sing-box rule-set decompile --output /tmp/ipcheck.json dist/sing-box/ipcheck.srs
|
|
||||||
grep -q '"2ip.ru"' /tmp/ipcheck.json
|
|
||||||
|
|
||||||
- name: Release notes
|
|
||||||
id: notes
|
|
||||||
run: |
|
|
||||||
echo "tag=$(date -u '+%Y%m%d%H%M')" >> "$GITHUB_OUTPUT"
|
|
||||||
{
|
|
||||||
echo "Собрано из \`${GITHUB_SHA::7}\`."
|
|
||||||
echo
|
|
||||||
echo '| Категория | Правил |'
|
|
||||||
echo '| --- | ---: |'
|
|
||||||
python -c "import json;[print(f'| {k} | {v} |') for k,v in json.load(open('dist/metadata.json'))['categories'].items()]"
|
|
||||||
echo
|
|
||||||
echo "sha256(geosite.dat) = \`$(cut -d' ' -f1 dist/geosite.dat.sha256sum)\`"
|
|
||||||
} > /tmp/notes.md
|
|
||||||
cat /tmp/notes.md >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ipcheck-domains
|
|
||||||
path: dist/
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Publish to release branch
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
run: |
|
|
||||||
cp README.md dist/README.md
|
|
||||||
cp LICENSE dist/LICENSE
|
|
||||||
cd dist
|
|
||||||
git init -q -b release
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
||||||
git add .
|
|
||||||
git commit -q -m "build: $(date -u '+%Y-%m-%d %H:%M UTC') ($GITHUB_SHA)"
|
|
||||||
git push -qf "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" release
|
|
||||||
cd ..
|
|
||||||
rm -f dist/README.md dist/LICENSE
|
|
||||||
|
|
||||||
- name: Create release
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
tag_name: ${{ steps.notes.outputs.tag }}
|
|
||||||
name: ${{ steps.notes.outputs.tag }}
|
|
||||||
body_path: /tmp/notes.md
|
|
||||||
files: |
|
|
||||||
dist/geosite.dat
|
|
||||||
dist/geosite.dat.sha256sum
|
|
||||||
dist/metadata.json
|
|
||||||
dist/sing-box/*.srs
|
|
||||||
dist/mihomo/*.mrs
|
|
||||||
dist/clash/*
|
|
||||||
dist/txt/*
|
|
||||||
dist/json/*
|
|
||||||
dist/adguard/*
|
|
||||||
@@ -4,7 +4,10 @@
|
|||||||
тесты утечек DNS/WebRTC, GeoIP-базы и скоринг прокси/VPN.
|
тесты утечек DNS/WebRTC, GeoIP-базы и скоринг прокси/VPN.
|
||||||
|
|
||||||
Собирается в несколько форматов. Основной — `geosite.dat` (protobuf v2ray/Xray) для 3x-ui.
|
Собирается в несколько форматов. Основной — `geosite.dat` (protobuf v2ray/Xray) для 3x-ui.
|
||||||
Сборка идёт автоматически при каждом изменении `data/` и раз в сутки по расписанию.
|
CI на Gitea Actions пересобирает всё при каждом изменении `data/` и раз в сутки по расписанию.
|
||||||
|
|
||||||
|
> Во всех примерах ниже `gitea.example.com/mrleo1nid/ipcheck-domains` — замените на адрес
|
||||||
|
> своего инстанса и репозитория.
|
||||||
|
|
||||||
## Категории
|
## Категории
|
||||||
|
|
||||||
@@ -18,16 +21,18 @@
|
|||||||
|
|
||||||
## Скачать
|
## Скачать
|
||||||
|
|
||||||
Ветка `release` обновляется при каждой сборке — ссылки стабильны:
|
Ветка `release` перезаписывается при каждой сборке, ссылки стабильные:
|
||||||
|
|
||||||
```
|
```
|
||||||
https://raw.githubusercontent.com/mrleo1nid/ipcheck-domains/release/<путь>
|
https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release/<путь>
|
||||||
```
|
```
|
||||||
|
|
||||||
| Файл | Для чего |
|
| Файл | Для чего |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `ipcheck.dat` | Xray / v2ray / 3x-ui — кладётся рядом со штатным `geosite.dat` |
|
| `ipcheck.dat` | Xray / v2ray / 3x-ui — кладётся рядом со штатным `geosite.dat` |
|
||||||
| `geosite.dat` | то же самое, но именем по умолчанию — если хотите заменить штатный файл |
|
| `geosite.dat` | то же самое, но именем по умолчанию — если хотите заменить штатный файл |
|
||||||
|
| `ipcheck.dat.sha256sum` | контрольная сумма, по ней работает автообновление |
|
||||||
|
| `update-geosite.sh` | скрипт обновления для cron (см. ниже) |
|
||||||
| `sing-box/<категория>.srs` | sing-box, скомпилированный rule-set |
|
| `sing-box/<категория>.srs` | sing-box, скомпилированный rule-set |
|
||||||
| `sing-box/<категория>.json` | sing-box, исходный rule-set |
|
| `sing-box/<категория>.json` | sing-box, исходный rule-set |
|
||||||
| `mihomo/<категория>.mrs` | mihomo, бинарный rule-provider |
|
| `mihomo/<категория>.mrs` | mihomo, бинарный rule-provider |
|
||||||
@@ -36,17 +41,18 @@ https://raw.githubusercontent.com/mrleo1nid/ipcheck-domains/release/<путь>
|
|||||||
| `txt/<категория>.txt` | просто домены, по одному на строку |
|
| `txt/<категория>.txt` | просто домены, по одному на строку |
|
||||||
| `json/<категория>.json` | структурированный список с типами правил |
|
| `json/<категория>.json` | структурированный список с типами правил |
|
||||||
| `adguard/<категория>.txt` | AdGuard Home / uBlock — если нужно блокировать, а не роутить |
|
| `adguard/<категория>.txt` | AdGuard Home / uBlock — если нужно блокировать, а не роутить |
|
||||||
| `metadata.json` | время сборки, счётчики, sha256 |
|
| `metadata.json`, `SHA256SUMS` | время сборки, счётчики, суммы по всем файлам |
|
||||||
|
|
||||||
Плюс каждая сборка публикуется как GitHub Release с датой в теге.
|
Плюс каждая сборка публикуется как релиз Gitea с датой в теге — там же лежит
|
||||||
|
`ipcheck-domains-all.tar.gz` со всем содержимым `dist/` разом.
|
||||||
|
|
||||||
## 3x-ui
|
## 3x-ui
|
||||||
|
|
||||||
Файл кладётся туда же, где лежат `geoip.dat` и `geosite.dat` панели. Обычно это
|
Файл кладётся туда же, где у панели лежат `geoip.dat` и `geosite.dat`. Обычно это
|
||||||
`/usr/local/x-ui/bin` (панель, установленная скриптом) или `/usr/local/share/xray`:
|
`/usr/local/x-ui/bin` (установка скриптом) или `/usr/local/share/xray`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL -o /usr/local/x-ui/bin/ipcheck.dat https://raw.githubusercontent.com/mrleo1nid/ipcheck-domains/release/ipcheck.dat
|
curl -fsSL -o /usr/local/x-ui/bin/ipcheck.dat https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release/ipcheck.dat
|
||||||
```
|
```
|
||||||
|
|
||||||
Дальше в панели: **Xray Settings → Routing Rules → Add Rule**, поле `domain`:
|
Дальше в панели: **Xray Settings → Routing Rules → Add Rule**, поле `domain`:
|
||||||
@@ -70,6 +76,123 @@ ext:ipcheck.dat:ipcheck
|
|||||||
|
|
||||||
После замены файла Xray нужно перезапустить (в 3x-ui — **Restart Xray**).
|
После замены файла Xray нужно перезапустить (в 3x-ui — **Restart Xray**).
|
||||||
|
|
||||||
|
## Автообновление на сервере
|
||||||
|
|
||||||
|
Xray читает `.dat` только при старте, поэтому обновление — это «скачать файл и
|
||||||
|
перезапустить сервис». Этим занимается [`update-geosite.sh`](tools/update-geosite.sh):
|
||||||
|
|
||||||
|
- сначала тянет `.sha256sum` и, если сумма совпала с локальным файлом, **молча выходит** —
|
||||||
|
ни лишней закачки, ни лишнего перезапуска Xray;
|
||||||
|
- проверяет контрольную сумму скачанного файла **до** подмены;
|
||||||
|
- подменяет файл атомарно (`mv` в пределах каталога), старый сохраняет в `.bak`;
|
||||||
|
- перезапускает сервис только если файл действительно изменился;
|
||||||
|
- пишет в stdout только когда что-то произошло, так что cron не будет слать письма впустую.
|
||||||
|
|
||||||
|
### Установка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL -o /usr/local/bin/update-geosite.sh https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release/update-geosite.sh
|
||||||
|
chmod +x /usr/local/bin/update-geosite.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверьте вручную — первый запуск скачает файл и перезапустит панель:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BASE_URL=https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release \
|
||||||
|
DEST=/usr/local/x-ui/bin/ipcheck.dat \
|
||||||
|
RESTART_CMD='x-ui restart' \
|
||||||
|
/usr/local/bin/update-geosite.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Второй запуск подряд должен отработать молча и мгновенно — значит сравнение сумм работает.
|
||||||
|
|
||||||
|
### Вариант 1: cron
|
||||||
|
|
||||||
|
`crontab -e` от root, одна строка (04:17 ночи, чтобы не совпадать с чужими крон-джобами):
|
||||||
|
|
||||||
|
```cron
|
||||||
|
17 4 * * * BASE_URL=https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release DEST=/usr/local/x-ui/bin/ipcheck.dat RESTART_CMD='x-ui restart' /usr/local/bin/update-geosite.sh >> /var/log/ipcheck-update.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
Или, если не хочется длинной строки, положите настройки в `/etc/default/ipcheck`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BASE_URL=https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release
|
||||||
|
DEST=/usr/local/x-ui/bin/ipcheck.dat
|
||||||
|
RESTART_CMD=x-ui restart
|
||||||
|
```
|
||||||
|
|
||||||
|
```cron
|
||||||
|
17 4 * * * . /etc/default/ipcheck && /usr/local/bin/update-geosite.sh >> /var/log/ipcheck-update.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт не экспортирует переменные сам — при таком варианте добавьте в файл
|
||||||
|
`export BASE_URL DEST RESTART_CMD` последней строкой.
|
||||||
|
|
||||||
|
### Вариант 2: systemd timer
|
||||||
|
|
||||||
|
Точнее cron: `Persistent=true` догоняет пропущенный запуск после перезагрузки,
|
||||||
|
`RandomizedDelaySec` размазывает нагрузку, а логи идут в journald.
|
||||||
|
|
||||||
|
`/etc/systemd/system/ipcheck-geosite.service`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Обновление ipcheck.dat для Xray
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
Environment=BASE_URL=https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release
|
||||||
|
Environment=DEST=/usr/local/x-ui/bin/ipcheck.dat
|
||||||
|
Environment=RESTART_CMD=x-ui restart
|
||||||
|
ExecStart=/usr/local/bin/update-geosite.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
`/etc/systemd/system/ipcheck-geosite.timer`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Ежедневное обновление ipcheck.dat
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=daily
|
||||||
|
RandomizedDelaySec=1h
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now ipcheck-geosite.timer
|
||||||
|
systemctl start ipcheck-geosite.service # разовая проверка
|
||||||
|
journalctl -u ipcheck-geosite.service -n 20
|
||||||
|
```
|
||||||
|
|
||||||
|
### Если 3x-ui в Docker
|
||||||
|
|
||||||
|
Путь указывайте внутрь примонтированного тома, а перезапуск делайте через docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DEST=/var/lib/x-ui-bin/ipcheck.dat RESTART_CMD='docker restart 3x-ui' /usr/local/bin/update-geosite.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Переменные скрипта
|
||||||
|
|
||||||
|
| Переменная | По умолчанию | Значение |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `BASE_URL` | ссылка на ветку `release` | база, откуда качать |
|
||||||
|
| `DEST` | `/usr/local/x-ui/bin/ipcheck.dat` | куда положить файл |
|
||||||
|
| `NAME` | имя файла из `DEST` | имя файла в репозитории |
|
||||||
|
| `RESTART_CMD` | пусто | что выполнить после успешного обновления |
|
||||||
|
| `TIMEOUT` | `30` | таймаут curl, секунды |
|
||||||
|
|
||||||
|
Коды возврата: `0` — обновлено или уже актуально, `1` — ошибка (не скачалось,
|
||||||
|
не совпала сумма, не удался перезапуск).
|
||||||
|
|
||||||
## sing-box
|
## sing-box
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -81,14 +204,17 @@ ext:ipcheck.dat:ipcheck
|
|||||||
"type": "remote",
|
"type": "remote",
|
||||||
"tag": "ipcheck",
|
"tag": "ipcheck",
|
||||||
"format": "binary",
|
"format": "binary",
|
||||||
"url": "https://raw.githubusercontent.com/mrleo1nid/ipcheck-domains/release/sing-box/ipcheck.srs",
|
"url": "https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release/sing-box/ipcheck.srs",
|
||||||
"download_detour": "direct"
|
"download_detour": "direct",
|
||||||
|
"update_interval": "1d"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Здесь cron не нужен — sing-box обновляет rule-set сам.
|
||||||
|
|
||||||
## Clash / mihomo
|
## Clash / mihomo
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -97,7 +223,7 @@ rule-providers:
|
|||||||
type: http
|
type: http
|
||||||
behavior: domain
|
behavior: domain
|
||||||
format: yaml
|
format: yaml
|
||||||
url: "https://raw.githubusercontent.com/mrleo1nid/ipcheck-domains/release/clash/ipcheck.yaml"
|
url: "https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release/clash/ipcheck.yaml"
|
||||||
path: ./ruleset/ipcheck.yaml
|
path: ./ruleset/ipcheck.yaml
|
||||||
interval: 86400
|
interval: 86400
|
||||||
|
|
||||||
@@ -105,7 +231,7 @@ rules:
|
|||||||
- RULE-SET,ipcheck,PROXY
|
- RULE-SET,ipcheck,PROXY
|
||||||
```
|
```
|
||||||
|
|
||||||
Для `.mrs` укажите `behavior: domain` и `format: mrs`.
|
Для `.mrs` укажите `behavior: domain` и `format: mrs`. Обновление тоже своё, по `interval`.
|
||||||
|
|
||||||
## Как добавить домен
|
## Как добавить домен
|
||||||
|
|
||||||
@@ -131,26 +257,45 @@ example.com @ads # атрибут — попадёт в .dat как geosit
|
|||||||
Нужен только Python 3.10+, зависимостей нет:
|
Нужен только Python 3.10+, зависимостей нет:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python tools/build.py
|
python3 tools/build.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Результат — в `dist/`. Проверка исходников без сборки (синтаксис, дубликаты,
|
Результат — в `dist/`. Проверка исходников без сборки (синтаксис, дубликаты,
|
||||||
домены, уже покрытые родительским суффиксом, циклические `include`):
|
домены, уже покрытые родительским суффиксом, циклические `include`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python tools/build.py --check
|
python3 tools/build.py --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Тесты сборщика:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m unittest discover -s tools -t tools
|
||||||
```
|
```
|
||||||
|
|
||||||
`.srs` и `.mrs` собираются, только если в `PATH` есть `sing-box` и `mihomo` —
|
`.srs` и `.mrs` собираются, только если в `PATH` есть `sing-box` и `mihomo` —
|
||||||
без них остальные форматы всё равно соберутся. Пути можно указать явно:
|
без них остальные форматы всё равно соберутся. Скачать их вместе с `xray`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python tools/build.py --sing-box ./bin/sing-box --mihomo ./bin/mihomo
|
sh tools/fetch-tools.sh ./bin
|
||||||
```
|
```
|
||||||
|
|
||||||
Сборщик проверяет сам себя: после записи `geosite.dat` он разбирает файл обратно
|
Сборщик проверяет сам себя: после записи `geosite.dat` он разбирает файл обратно
|
||||||
и сверяет с исходником. В CI дополнительно поднимается настоящий Xray с правилом
|
и сверяет с исходником. В CI дополнительно поднимается настоящий Xray с правилом
|
||||||
`ext:geosite.dat:ipcheck` — если файл не читается, сборка падает.
|
`ext:ipcheck.dat:ipcheck` — если файл не читается, сборка падает.
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
[`.gitea/workflows/build.yml`](.gitea/workflows/build.yml): проверка → тесты →
|
||||||
|
сборка → валидация настоящими Xray и sing-box → публикация в ветку `release` →
|
||||||
|
релиз в Gitea.
|
||||||
|
|
||||||
|
Раннеру нужен образ с `python3`, `curl`, `unzip`, `tar`, `git` (например
|
||||||
|
`catthehacker/ubuntu:act-latest`); если чего-то нет, первый шаг доставит пакеты сам.
|
||||||
|
|
||||||
|
Публикация идёт под автоматическим токеном `${{ github.token }}`. Если у раннера
|
||||||
|
нет прав на push в репозиторий или на создание релизов — заведите секрет
|
||||||
|
`RELEASE_TOKEN` с правами `repo:write`, workflow предпочтёт его.
|
||||||
|
|
||||||
## Лицензия
|
## Лицензия
|
||||||
|
|
||||||
|
|||||||
@@ -443,6 +443,19 @@ def build(out_dir: Path, singbox: str | None, mihomo: str | None) -> int:
|
|||||||
+ "\n",
|
+ "\n",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Скрипт обновления едет вместе с артефактами: на сервере тогда нужен
|
||||||
|
# ровно один базовый URL — и для .dat, и для самого апдейтера.
|
||||||
|
updater = Path(__file__).with_name("update-geosite.sh")
|
||||||
|
if updater.is_file():
|
||||||
|
write(out_dir / updater.name, updater.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
checksums = "".join(
|
||||||
|
f"{hashlib.sha256(path.read_bytes()).hexdigest()} {path.relative_to(out_dir).as_posix()}\n"
|
||||||
|
for path in sorted(out_dir.rglob("*"))
|
||||||
|
if path.is_file()
|
||||||
|
)
|
||||||
|
write(out_dir / "SHA256SUMS", checksums)
|
||||||
|
|
||||||
print(f"geosite.dat: {len(dat)} байт, sha256 {digest[:16]}…")
|
print(f"geosite.dat: {len(dat)} байт, sha256 {digest[:16]}…")
|
||||||
for name in categories:
|
for name in categories:
|
||||||
print(f" {name:<12} {len(resolved[name]):>4} правил")
|
print(f" {name:<12} {len(resolved[name]):>4} правил")
|
||||||
|
|||||||
Executable
+48
@@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Кладёт sing-box, mihomo и xray в каталог (по умолчанию ./bin).
|
||||||
|
# Нужен только для сборки .srs/.mrs и для проверки .dat настоящим Xray.
|
||||||
|
#
|
||||||
|
# ./tools/fetch-tools.sh [каталог]
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
BIN="${1:-./bin}"
|
||||||
|
mkdir -p "$BIN"
|
||||||
|
BIN=$(cd "$BIN" && pwd)
|
||||||
|
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64 | amd64) sb_arch=amd64; mh_arch=amd64; xr_arch=64 ;;
|
||||||
|
aarch64 | arm64) sb_arch=arm64; mh_arch=arm64; xr_arch=arm64-v8a ;;
|
||||||
|
*) echo "неподдерживаемая архитектура: $(uname -m)" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Печатает ссылку на ассет последнего релиза, имя которого совпало с regex.
|
||||||
|
asset_url() {
|
||||||
|
curl -fsSL "https://api.github.com/repos/$1/releases/latest" | python3 -c '
|
||||||
|
import json, re, sys
|
||||||
|
assets = json.load(sys.stdin)["assets"]
|
||||||
|
match = next((a for a in assets if re.search(sys.argv[1], a["name"])), None)
|
||||||
|
if match is None:
|
||||||
|
sys.exit(f"в релизе нет ассета по шаблону {sys.argv[1]}")
|
||||||
|
print(match["browser_download_url"])
|
||||||
|
' "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "sing-box…"
|
||||||
|
curl -fsSL "$(asset_url SagerNet/sing-box "-linux-$sb_arch\\.tar\\.gz\$")" |
|
||||||
|
tar -xz --strip-components=1 -C "$BIN" --wildcards '*/sing-box'
|
||||||
|
|
||||||
|
echo "mihomo…"
|
||||||
|
curl -fsSL "$(asset_url MetaCubeX/mihomo "^mihomo-linux-$mh_arch-v[0-9.]+\\.gz\$")" |
|
||||||
|
gunzip > "$BIN/mihomo"
|
||||||
|
|
||||||
|
echo "xray…"
|
||||||
|
curl -fsSL -o "$BIN/xray.zip" "$(asset_url XTLS/Xray-core "^Xray-linux-$xr_arch\\.zip\$")"
|
||||||
|
unzip -oqj "$BIN/xray.zip" xray -d "$BIN"
|
||||||
|
rm -f "$BIN/xray.zip"
|
||||||
|
|
||||||
|
chmod +x "$BIN/sing-box" "$BIN/mihomo" "$BIN/xray"
|
||||||
|
echo "готово: $BIN"
|
||||||
|
"$BIN/sing-box" version | head -1
|
||||||
|
"$BIN/mihomo" -v | head -1
|
||||||
|
"$BIN/xray" version | head -1
|
||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Создаёт (или обновляет) релиз в Gitea и заливает в него файлы.
|
||||||
|
#
|
||||||
|
# GITEA_URL=https://gitea.example.com GITEA_REPO=owner/repo GITEA_TOKEN=… \
|
||||||
|
# ./tools/gitea-release.sh <тег> <файл-с-описанием> <файл> [файл …]
|
||||||
|
#
|
||||||
|
# Имя ассета — путь относительно dist/ с заменой "/" на "-", чтобы
|
||||||
|
# sing-box/ipcheck.json и json/ipcheck.json не конфликтовали.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
[ $# -ge 3 ] || { echo "использование: $0 <тег> <notes.md> <файл...>" >&2; exit 1; }
|
||||||
|
|
||||||
|
TAG="$1"; NOTES="$2"; shift 2
|
||||||
|
: "${GITEA_URL:?не задан GITEA_URL}"
|
||||||
|
: "${GITEA_REPO:?не задан GITEA_REPO}"
|
||||||
|
: "${GITEA_TOKEN:?не задан GITEA_TOKEN}"
|
||||||
|
|
||||||
|
API="$GITEA_URL/api/v1/repos/$GITEA_REPO/releases"
|
||||||
|
auth="Authorization: token $GITEA_TOKEN"
|
||||||
|
|
||||||
|
json_field() { python3 -c 'import json,sys; print(json.load(sys.stdin).get(sys.argv[1],""))' "$1"; }
|
||||||
|
|
||||||
|
payload=$(NOTES_FILE="$NOTES" TAG="$TAG" python3 -c '
|
||||||
|
import json, os
|
||||||
|
print(json.dumps({
|
||||||
|
"tag_name": os.environ["TAG"],
|
||||||
|
"name": os.environ["TAG"],
|
||||||
|
"body": open(os.environ["NOTES_FILE"], encoding="utf-8").read(),
|
||||||
|
"draft": False,
|
||||||
|
"prerelease": False,
|
||||||
|
}))')
|
||||||
|
|
||||||
|
# Релиз с таким тегом мог остаться от прошлого прогона — тогда переиспользуем его.
|
||||||
|
id=$(curl -fsS -X POST "$API" -H "$auth" -H 'Content-Type: application/json' \
|
||||||
|
-d "$payload" 2>/dev/null | json_field id || true)
|
||||||
|
if [ -z "$id" ]; then
|
||||||
|
id=$(curl -fsS "$API/tags/$TAG" -H "$auth" | json_field id) ||
|
||||||
|
{ echo "не удалось ни создать, ни найти релиз $TAG" >&2; exit 1; }
|
||||||
|
echo "релиз $TAG уже существует (id $id), дозаливаем ассеты"
|
||||||
|
fi
|
||||||
|
echo "релиз $TAG: id $id"
|
||||||
|
|
||||||
|
for file in "$@"; do
|
||||||
|
[ -f "$file" ] || { echo "нет файла: $file" >&2; exit 1; }
|
||||||
|
name=$(printf '%s' "${file#dist/}" | tr '/' '-')
|
||||||
|
curl -fsS -X POST "$API/$id/assets?name=$name" -H "$auth" \
|
||||||
|
-F "attachment=@$file" >/dev/null
|
||||||
|
echo " + $name"
|
||||||
|
done
|
||||||
Executable
+74
@@ -0,0 +1,74 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Обновляет локальный ipcheck.dat из ветки release. Рассчитан на запуск по cron.
|
||||||
|
#
|
||||||
|
# Скачивает файл, только если его sha256 отличается от локального, проверяет
|
||||||
|
# контрольную сумму до подмены и перезапускает сервис только когда файл реально
|
||||||
|
# изменился. Молчит, если обновлять нечего, — cron не будет слать письма впустую.
|
||||||
|
#
|
||||||
|
# ./update-geosite.sh
|
||||||
|
# BASE_URL=https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release \
|
||||||
|
# DEST=/usr/local/x-ui/bin/ipcheck.dat RESTART_CMD='x-ui restart' ./update-geosite.sh
|
||||||
|
#
|
||||||
|
# Коды возврата: 0 — обновлено или уже актуально, 1 — ошибка.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
BASE_URL="${BASE_URL:-https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release}"
|
||||||
|
DEST="${DEST:-/usr/local/x-ui/bin/ipcheck.dat}"
|
||||||
|
NAME="${NAME:-$(basename "$DEST")}"
|
||||||
|
RESTART_CMD="${RESTART_CMD:-}"
|
||||||
|
TIMEOUT="${TIMEOUT:-30}"
|
||||||
|
|
||||||
|
log() { printf '%s update-geosite: %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*"; }
|
||||||
|
die() { log "ОШИБКА: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
command -v curl >/dev/null 2>&1 || die "нужен curl"
|
||||||
|
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then
|
||||||
|
hash_of() { sha256sum "$1" | cut -d' ' -f1; }
|
||||||
|
elif command -v shasum >/dev/null 2>&1; then
|
||||||
|
hash_of() { shasum -a 256 "$1" | cut -d' ' -f1; }
|
||||||
|
else
|
||||||
|
die "нужен sha256sum или shasum"
|
||||||
|
fi
|
||||||
|
|
||||||
|
fetch() { curl -fsSL --retry 3 --retry-delay 2 --max-time "$TIMEOUT" "$1"; }
|
||||||
|
|
||||||
|
# Без промежуточной переменной статус вернул бы cut, а не curl.
|
||||||
|
remote_line=$(fetch "$BASE_URL/$NAME.sha256sum") ||
|
||||||
|
die "не удалось скачать $BASE_URL/$NAME.sha256sum"
|
||||||
|
remote_sum=$(printf '%s' "$remote_line" | cut -d' ' -f1)
|
||||||
|
case "$remote_sum" in
|
||||||
|
[0-9a-f][0-9a-f]*) [ "${#remote_sum}" -eq 64 ] || die "битая контрольная сумма: $remote_sum" ;;
|
||||||
|
*) die "битая контрольная сумма: $remote_sum" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -f "$DEST" ] && [ "$(hash_of "$DEST")" = "$remote_sum" ]; then
|
||||||
|
exit 0 # уже актуально — тишина
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp=$(mktemp "${TMPDIR:-/tmp}/ipcheck.XXXXXX") || die "не удалось создать временный файл"
|
||||||
|
trap 'rm -f "$tmp"' EXIT INT TERM
|
||||||
|
|
||||||
|
fetch "$BASE_URL/$NAME" > "$tmp" || die "не удалось скачать $BASE_URL/$NAME"
|
||||||
|
|
||||||
|
got=$(hash_of "$tmp")
|
||||||
|
[ "$got" = "$remote_sum" ] || die "контрольная сумма не совпала: ожидалось $remote_sum, получено $got"
|
||||||
|
[ -s "$tmp" ] || die "скачан пустой файл"
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$DEST")"
|
||||||
|
if [ -f "$DEST" ]; then
|
||||||
|
cp -p "$DEST" "$DEST.bak" || die "не удалось сохранить резервную копию"
|
||||||
|
fi
|
||||||
|
# mv в пределах одного каталога атомарен: Xray никогда не увидит половину файла.
|
||||||
|
cat "$tmp" > "$DEST.new" && mv -f "$DEST.new" "$DEST" || die "не удалось записать $DEST"
|
||||||
|
|
||||||
|
log "обновлён $DEST (sha256 $remote_sum)"
|
||||||
|
|
||||||
|
if [ -n "$RESTART_CMD" ]; then
|
||||||
|
if sh -c "$RESTART_CMD"; then
|
||||||
|
log "выполнено: $RESTART_CMD"
|
||||||
|
else
|
||||||
|
die "перезапуск не удался: $RESTART_CMD"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user