Compare commits

..
Author SHA1 Message Date
gitea-actions 117a97ba9e build 2026-09-20 03:18 UTC (6cd10434a3) 2026-09-20 03:18:25 +00:00
62 changed files with 1144 additions and 1018 deletions
-141
View File
@@ -1,141 +0,0 @@
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
-4
View File
@@ -1,4 +0,0 @@
dist/
bin/
__pycache__/
*.pyc
+19 -21
View File
@@ -6,8 +6,8 @@
Собирается в несколько форматов. Основной — `geosite.dat` (protobuf v2ray/Xray) для 3x-ui.
CI на Gitea Actions пересобирает всё при каждом изменении `data/` и раз в сутки по расписанию.
> Во всех примерах ниже `gitea.example.com/mrleo1nid/ipcheck-domains` — замените на адрес
> своего инстанса и репозитория.
> Если форкнули к себе — поменяйте `gitea.hsrv.site/mrleo1nid/ipcheck-domains` в примерах
> ниже и в `BASE_URL` внутри [`tools/update-geosite.sh`](tools/update-geosite.sh).
## Категории
@@ -24,7 +24,7 @@ CI на Gitea Actions пересобирает всё при каждом изм
Ветка `release` перезаписывается при каждой сборке, ссылки стабильные:
```
https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release/<путь>
https://gitea.hsrv.site/mrleo1nid/ipcheck-domains/raw/branch/release/<путь>
```
| Файл | Для чего |
@@ -52,7 +52,7 @@ https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release/<путь
`/usr/local/x-ui/bin` (установка скриптом) или `/usr/local/share/xray`:
```bash
curl -fsSL -o /usr/local/x-ui/bin/ipcheck.dat https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release/ipcheck.dat
curl -fsSL -o /usr/local/x-ui/bin/ipcheck.dat https://gitea.hsrv.site/mrleo1nid/ipcheck-domains/raw/branch/release/ipcheck.dat
```
Дальше в панели: **Xray Settings → Routing Rules → Add Rule**, поле `domain`:
@@ -91,17 +91,15 @@ Xray читает `.dat` только при старте, поэтому обн
### Установка
```bash
curl -fsSL -o /usr/local/bin/update-geosite.sh https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release/update-geosite.sh
curl -fsSL -o /usr/local/bin/update-geosite.sh https://gitea.hsrv.site/mrleo1nid/ipcheck-domains/raw/branch/release/update-geosite.sh
chmod +x /usr/local/bin/update-geosite.sh
```
Проверьте вручную — первый запуск скачает файл и перезапустит панель:
`BASE_URL` и `DEST` уже прописаны в скрипте по умолчанию, так что задать нужно только
команду перезапуска. Проверьте вручную — первый запуск скачает файл и перезапустит панель:
```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
RESTART_CMD='x-ui restart' /usr/local/bin/update-geosite.sh
```
Второй запуск подряд должен отработать молча и мгновенно — значит сравнение сумм работает.
@@ -111,23 +109,23 @@ RESTART_CMD='x-ui restart' \
`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
17 4 * * * RESTART_CMD='x-ui restart' /usr/local/bin/update-geosite.sh >> /var/log/ipcheck-update.log 2>&1
```
Или, если не хочется длинной строки, положите настройки в `/etc/default/ipcheck`:
Если путь до `.dat` другой или репозиторий свой, добавьте в ту же строку `DEST=…`
и `BASE_URL=…`. Когда переменных много, вынесите их в `/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
export BASE_URL=https://gitea.hsrv.site/mrleo1nid/ipcheck-domains/raw/branch/release
export DEST=/usr/local/x-ui/bin/ipcheck.dat
export 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` последней строкой.
`export` здесь обязателен: скрипт читает переменные из окружения, а не из своего шелла.
### Вариант 2: systemd timer
@@ -144,7 +142,7 @@ Wants=network-online.target
[Service]
Type=oneshot
Environment=BASE_URL=https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release
Environment=BASE_URL=https://gitea.hsrv.site/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
@@ -184,7 +182,7 @@ DEST=/var/lib/x-ui-bin/ipcheck.dat RESTART_CMD='docker restart 3x-ui' /usr/local
| Переменная | По умолчанию | Значение |
| --- | --- | --- |
| `BASE_URL` | ссылка на ветку `release` | база, откуда качать |
| `BASE_URL` | `https://gitea.hsrv.site/mrleo1nid/ipcheck-domains/raw/branch/release` | база, откуда качать |
| `DEST` | `/usr/local/x-ui/bin/ipcheck.dat` | куда положить файл |
| `NAME` | имя файла из `DEST` | имя файла в репозитории |
| `RESTART_CMD` | пусто | что выполнить после успешного обновления |
@@ -204,7 +202,7 @@ DEST=/var/lib/x-ui-bin/ipcheck.dat RESTART_CMD='docker restart 3x-ui' /usr/local
"type": "remote",
"tag": "ipcheck",
"format": "binary",
"url": "https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release/sing-box/ipcheck.srs",
"url": "https://gitea.hsrv.site/mrleo1nid/ipcheck-domains/raw/branch/release/sing-box/ipcheck.srs",
"download_detour": "direct",
"update_interval": "1d"
}
@@ -223,7 +221,7 @@ rule-providers:
type: http
behavior: domain
format: yaml
url: "https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release/clash/ipcheck.yaml"
url: "https://gitea.hsrv.site/mrleo1nid/ipcheck-domains/raw/branch/release/clash/ipcheck.yaml"
path: ./ruleset/ipcheck.yaml
interval: 86400
+51
View File
@@ -0,0 +1,51 @@
1c1c3f6ca2303141ecbc62b7eb948a62c173a8b8b28f2b4b86cf737a9aa52427 adguard/fraud-score.txt
5ab46aa0ca887111b61135b66f9447f51b55c93d6588252ea3490c93919aae97 adguard/geoip-db.txt
ec0e452c5a359e2156056da21330a0440b0e817aff1b81b559a80ad39b85e1a9 adguard/ip-lookup.txt
9412216ee6701726a4f2c8fe4d57546d78aa0be624adc69c0bdfc902749edef7 adguard/ipcheck.txt
1b5c5b3341f2e95adb7c7664bab3b8a02270bb646e7045196ca584511087f4e1 adguard/leak-test.txt
c475f3d485e2830b904621857958908345e2baafe0e410207c61163ad5b9fd64 clash/fraud-score.list
faddbb13d3c9a2a47c559d23aa0fadba0018d461dc292e60219b8a56634877b7 clash/fraud-score.yaml
c7913328a44f93b61a41545aceb6c60557f9ee1eb2d73b3d6180a05ec12dbd72 clash/geoip-db.list
77dfcb3cfa6efad45adfd2e5f5c310fae148099907f783336f77451f2309e1e6 clash/geoip-db.yaml
664e02852f5b132103d475055ddc8af2f0782d02cba607e5d1444ef8d8770cb8 clash/ip-lookup.list
d653aa6b6c8b9fcc980489b3a65c10cad3799c1260a98488abae49026bab450f clash/ip-lookup.yaml
690fcdab44b059c31be6c2656de8cf27b7568d7868cedcb1542adfa6d958a6fe clash/ipcheck.list
896904b80a6fb32b64fc98fa36e3f3018dcdc8bf5c493e3151478bbfdeff6cd4 clash/ipcheck.yaml
2ce43be4508d5b44c9283f4c9831ddb541ba6386293d50f752f7e3eb6c740638 clash/leak-test.list
4c73658860aacb7597d979c335b0b91eb0fed567789c746bb413ac846cca6bea clash/leak-test.yaml
40ac1b41d51aecabf178ff98ade8dfe2587bd6b49c66260331990774588b040c geosite.dat
67df6d4260d20cc0a77588ff264a6672293b30cbfd406fd0852cd10e07c2bbfa geosite.dat.sha256sum
40ac1b41d51aecabf178ff98ade8dfe2587bd6b49c66260331990774588b040c ipcheck.dat
70ee904b3608e34b985598f8c4d9d70b4deca9778fe1d709fff038688efecb6b ipcheck.dat.sha256sum
817556a7f8f3d74499d8d77459ead2e9d84a42c45e3ba2ac107928c79482e309 json/fraud-score.json
e5172358f352144b1f13ec8442e1e9b3adf490c86dd3411617be408b524179f3 json/geoip-db.json
60b9e442f55454c3d168c0f0fec7ff768be54d04adada068d294f71994d8cbd3 json/ip-lookup.json
22dcc71ef7c7cdd92992037022225f390e00d35c75c49151d3c09fa0abaf1d3a json/ipcheck.json
a4ebdd1db3795f1726d2c4a9bbe3564bf8f2d1ccb575f93e0799ee3a6a1fae9b json/leak-test.json
40ad4fdba0c2709b6240910756ad59337598dc7482c08dbfb95a66130d0ec8cf metadata.json
4909af8cff1593c6e264ecad0e69887bcb881ba3870fa47b0935c2c33a4de63f mihomo/fraud-score.mrs
467e31f4829571d49360f081ab6fbf7453714ee3339fc96e69fc9409b40cc0b3 mihomo/geoip-db.mrs
4a3fe564d7e22ce2e7e317b1727edea9503f08f779ee8969e3eca4b1bcd07d9d mihomo/ip-lookup.mrs
37340c6a9d3e3039eb34d74776d76c88393588cbfc8f273351bc5b7466431fd9 mihomo/ipcheck.mrs
b3e409367c2240fb61f88867686e0708ef9ef3761da1c2b2d5afefa10853ed3c mihomo/leak-test.mrs
f37da541dfebd9389d911b899485e38cd85843a4018733caa4959c5853692662 sing-box/fraud-score.json
68bd460e9119e4b792254ca79b40267a33968d501f40c1841cbad2e2a8448318 sing-box/fraud-score.srs
a08238aa218a4dbe0949b2aad1b5400d447b75fd09c1dc825189c3d657043585 sing-box/geoip-db.json
8d3a1c06cec32a290b868ed27c5432650e11b253b120e293423070a7d8146aeb sing-box/geoip-db.srs
0bc4678629314106f6f4b2d8bcc58c03ea1b55f0971306a9a77f8e5d93aa81b0 sing-box/ip-lookup.json
76a4e6ee48b47c0b25522f6c203873a8560f1fe55dd99d381ff46f507e87e5e3 sing-box/ip-lookup.srs
538890422358a2d094f15ff24e3e21835ab0e7a3ee6d3abd978c4ab4aeb316a8 sing-box/ipcheck.json
feffddcb48734fc5bbfd72d8d85d3d533ae225edae935ea45c9a8b475907c86a sing-box/ipcheck.srs
01bd53410b788ca85e0b09ce729da1355681e422c3c49068da44c9284e457742 sing-box/leak-test.json
c90764bb5463108ff734f531d7c1f7288fa4a1672c6c79a53d769e63f85a5763 sing-box/leak-test.srs
b67ff3c4ea1c371be2d777dcf2b88614673ffd7532f2f8cff7377e7993aa91b6 src/fraud-score.txt
0514a090c68e117030b2eb478360f0eba79740db1c02d5ade6ea7c96e8c864e2 src/geoip-db.txt
0bb286f849b93defebceaec7ec6f3f820aaf3821f7954590b5e7d46a7b559726 src/ip-lookup.txt
c6f4b98ddcdafcafe7bd2c4c580a1ee020d10841e41e9be9404115243bbfdcf8 src/ipcheck.txt
eaffb62f3e7edb9746ca12e15478091127d3891589c3f2cdc15d47fa326df238 src/leak-test.txt
ae44fea51df8eb856c094068205d2b574dea2e54abd2c9832e6cf1b92c916fb4 txt/fraud-score.txt
903d74a61b3832168e793ca3eb940d162de64f965f8df247f4a561791e8690b9 txt/geoip-db.txt
a9f7ee701e36a0b5210945f6d5799447be35a8fe3b412781431b4d2660542aa0 txt/ip-lookup.txt
1fad7449ab2f8beb6f46a7936c2b53d045eba34945bed9e5e36b658eb09338ad txt/ipcheck.txt
5f3438791d5c49b345d26fd8c9bfdba582bed80c3c314fa572e0cf66135e626e txt/leak-test.txt
ff655b22d14d5c0a267ca8fea34ad6dcc18d40a8aef1c0b6a1c6e8a34e2238c2 update-geosite.sh
+12
View File
@@ -0,0 +1,12 @@
! ipcheck-domains — категория: fraud-score
! Правил: 9. Файл генерируется автоматически, не редактируйте вручную.
! Источник: data/fraud-score.txt
||criminalip.io^
||fraudguard.io^
||getipintel.net^
||iphub.info^
||ipqualityscore.com^
||proxycheck.io^
||scamalytics.com^
||spur.us^
||vpnapi.io^
+12
View File
@@ -0,0 +1,12 @@
! ipcheck-domains — категория: geoip-db
! Правил: 9. Файл генерируется автоматически, не редактируйте вручную.
! Источник: data/geoip-db.txt
||bigdatacloud.net^
||db-ip.com^
||ip2location.com^
||ipbase.com^
||ipdata.co^
||ipgeolocation.io^
||ipregistry.co^
||ipwhois.io^
||maxmind.com^
+21
View File
@@ -0,0 +1,21 @@
! ipcheck-domains — категория: ip-lookup
! Правил: 18. Файл генерируется автоматически, не редактируйте вручную.
! Источник: data/ip-lookup.txt
||2ip.io^
||2ip.ru^
||icanhazip.com^
||ifconfig.co^
||ifconfig.io^
||ifconfig.me^
||ip-api.com^
||ip.sb^
||ipaddress.is^
||ipapi.co^
||ipapi.is^
||ipify.org^
||ipinfo.io^
||myip.com^
||whatismyip.com^
||whatismyipaddress.com^
||whoer.com^
||whoer.net^
+42
View File
@@ -0,0 +1,42 @@
! ipcheck-domains — категория: ipcheck
! Правил: 39. Файл генерируется автоматически, не редактируйте вручную.
! Источник: data/ipcheck.txt
||2ip.io^
||2ip.ru^
||bigdatacloud.net^
||browserleaks.com^
||criminalip.io^
||db-ip.com^
||dnsleaktest.com^
||fraudguard.io^
||getipintel.net^
||icanhazip.com^
||ifconfig.co^
||ifconfig.io^
||ifconfig.me^
||ip-api.com^
||ip.sb^
||ip2location.com^
||ipaddress.is^
||ipapi.co^
||ipapi.is^
||ipbase.com^
||ipdata.co^
||ipgeolocation.io^
||iphub.info^
||ipify.org^
||ipinfo.io^
||ipleak.net^
||ipqualityscore.com^
||ipregistry.co^
||ipwhois.io^
||maxmind.com^
||myip.com^
||proxycheck.io^
||scamalytics.com^
||spur.us^
||vpnapi.io^
||whatismyip.com^
||whatismyipaddress.com^
||whoer.com^
||whoer.net^
+6
View File
@@ -0,0 +1,6 @@
! ipcheck-domains — категория: leak-test
! Правил: 3. Файл генерируется автоматически, не редактируйте вручную.
! Источник: data/leak-test.txt
||browserleaks.com^
||dnsleaktest.com^
||ipleak.net^
+12
View File
@@ -0,0 +1,12 @@
# ipcheck-domains — категория: fraud-score
# Правил: 9. Файл генерируется автоматически, не редактируйте вручную.
# Источник: data/fraud-score.txt
DOMAIN-SUFFIX,criminalip.io
DOMAIN-SUFFIX,fraudguard.io
DOMAIN-SUFFIX,getipintel.net
DOMAIN-SUFFIX,iphub.info
DOMAIN-SUFFIX,ipqualityscore.com
DOMAIN-SUFFIX,proxycheck.io
DOMAIN-SUFFIX,scamalytics.com
DOMAIN-SUFFIX,spur.us
DOMAIN-SUFFIX,vpnapi.io
+13
View File
@@ -0,0 +1,13 @@
# ipcheck-domains — категория: fraud-score
# Правил: 9. Файл генерируется автоматически, не редактируйте вручную.
# Источник: data/fraud-score.txt
payload:
- '+.criminalip.io'
- '+.fraudguard.io'
- '+.getipintel.net'
- '+.iphub.info'
- '+.ipqualityscore.com'
- '+.proxycheck.io'
- '+.scamalytics.com'
- '+.spur.us'
- '+.vpnapi.io'
+12
View File
@@ -0,0 +1,12 @@
# ipcheck-domains — категория: geoip-db
# Правил: 9. Файл генерируется автоматически, не редактируйте вручную.
# Источник: data/geoip-db.txt
DOMAIN-SUFFIX,bigdatacloud.net
DOMAIN-SUFFIX,db-ip.com
DOMAIN-SUFFIX,ip2location.com
DOMAIN-SUFFIX,ipbase.com
DOMAIN-SUFFIX,ipdata.co
DOMAIN-SUFFIX,ipgeolocation.io
DOMAIN-SUFFIX,ipregistry.co
DOMAIN-SUFFIX,ipwhois.io
DOMAIN-SUFFIX,maxmind.com
+13
View File
@@ -0,0 +1,13 @@
# ipcheck-domains — категория: geoip-db
# Правил: 9. Файл генерируется автоматически, не редактируйте вручную.
# Источник: data/geoip-db.txt
payload:
- '+.bigdatacloud.net'
- '+.db-ip.com'
- '+.ip2location.com'
- '+.ipbase.com'
- '+.ipdata.co'
- '+.ipgeolocation.io'
- '+.ipregistry.co'
- '+.ipwhois.io'
- '+.maxmind.com'
+21
View File
@@ -0,0 +1,21 @@
# ipcheck-domains — категория: ip-lookup
# Правил: 18. Файл генерируется автоматически, не редактируйте вручную.
# Источник: data/ip-lookup.txt
DOMAIN-SUFFIX,2ip.io
DOMAIN-SUFFIX,2ip.ru
DOMAIN-SUFFIX,icanhazip.com
DOMAIN-SUFFIX,ifconfig.co
DOMAIN-SUFFIX,ifconfig.io
DOMAIN-SUFFIX,ifconfig.me
DOMAIN-SUFFIX,ip-api.com
DOMAIN-SUFFIX,ip.sb
DOMAIN-SUFFIX,ipaddress.is
DOMAIN-SUFFIX,ipapi.co
DOMAIN-SUFFIX,ipapi.is
DOMAIN-SUFFIX,ipify.org
DOMAIN-SUFFIX,ipinfo.io
DOMAIN-SUFFIX,myip.com
DOMAIN-SUFFIX,whatismyip.com
DOMAIN-SUFFIX,whatismyipaddress.com
DOMAIN-SUFFIX,whoer.com
DOMAIN-SUFFIX,whoer.net
+22
View File
@@ -0,0 +1,22 @@
# ipcheck-domains — категория: ip-lookup
# Правил: 18. Файл генерируется автоматически, не редактируйте вручную.
# Источник: data/ip-lookup.txt
payload:
- '+.2ip.io'
- '+.2ip.ru'
- '+.icanhazip.com'
- '+.ifconfig.co'
- '+.ifconfig.io'
- '+.ifconfig.me'
- '+.ip-api.com'
- '+.ip.sb'
- '+.ipaddress.is'
- '+.ipapi.co'
- '+.ipapi.is'
- '+.ipify.org'
- '+.ipinfo.io'
- '+.myip.com'
- '+.whatismyip.com'
- '+.whatismyipaddress.com'
- '+.whoer.com'
- '+.whoer.net'
+42
View File
@@ -0,0 +1,42 @@
# ipcheck-domains — категория: ipcheck
# Правил: 39. Файл генерируется автоматически, не редактируйте вручную.
# Источник: data/ipcheck.txt
DOMAIN-SUFFIX,2ip.io
DOMAIN-SUFFIX,2ip.ru
DOMAIN-SUFFIX,bigdatacloud.net
DOMAIN-SUFFIX,browserleaks.com
DOMAIN-SUFFIX,criminalip.io
DOMAIN-SUFFIX,db-ip.com
DOMAIN-SUFFIX,dnsleaktest.com
DOMAIN-SUFFIX,fraudguard.io
DOMAIN-SUFFIX,getipintel.net
DOMAIN-SUFFIX,icanhazip.com
DOMAIN-SUFFIX,ifconfig.co
DOMAIN-SUFFIX,ifconfig.io
DOMAIN-SUFFIX,ifconfig.me
DOMAIN-SUFFIX,ip-api.com
DOMAIN-SUFFIX,ip.sb
DOMAIN-SUFFIX,ip2location.com
DOMAIN-SUFFIX,ipaddress.is
DOMAIN-SUFFIX,ipapi.co
DOMAIN-SUFFIX,ipapi.is
DOMAIN-SUFFIX,ipbase.com
DOMAIN-SUFFIX,ipdata.co
DOMAIN-SUFFIX,ipgeolocation.io
DOMAIN-SUFFIX,iphub.info
DOMAIN-SUFFIX,ipify.org
DOMAIN-SUFFIX,ipinfo.io
DOMAIN-SUFFIX,ipleak.net
DOMAIN-SUFFIX,ipqualityscore.com
DOMAIN-SUFFIX,ipregistry.co
DOMAIN-SUFFIX,ipwhois.io
DOMAIN-SUFFIX,maxmind.com
DOMAIN-SUFFIX,myip.com
DOMAIN-SUFFIX,proxycheck.io
DOMAIN-SUFFIX,scamalytics.com
DOMAIN-SUFFIX,spur.us
DOMAIN-SUFFIX,vpnapi.io
DOMAIN-SUFFIX,whatismyip.com
DOMAIN-SUFFIX,whatismyipaddress.com
DOMAIN-SUFFIX,whoer.com
DOMAIN-SUFFIX,whoer.net
+43
View File
@@ -0,0 +1,43 @@
# ipcheck-domains — категория: ipcheck
# Правил: 39. Файл генерируется автоматически, не редактируйте вручную.
# Источник: data/ipcheck.txt
payload:
- '+.2ip.io'
- '+.2ip.ru'
- '+.bigdatacloud.net'
- '+.browserleaks.com'
- '+.criminalip.io'
- '+.db-ip.com'
- '+.dnsleaktest.com'
- '+.fraudguard.io'
- '+.getipintel.net'
- '+.icanhazip.com'
- '+.ifconfig.co'
- '+.ifconfig.io'
- '+.ifconfig.me'
- '+.ip-api.com'
- '+.ip.sb'
- '+.ip2location.com'
- '+.ipaddress.is'
- '+.ipapi.co'
- '+.ipapi.is'
- '+.ipbase.com'
- '+.ipdata.co'
- '+.ipgeolocation.io'
- '+.iphub.info'
- '+.ipify.org'
- '+.ipinfo.io'
- '+.ipleak.net'
- '+.ipqualityscore.com'
- '+.ipregistry.co'
- '+.ipwhois.io'
- '+.maxmind.com'
- '+.myip.com'
- '+.proxycheck.io'
- '+.scamalytics.com'
- '+.spur.us'
- '+.vpnapi.io'
- '+.whatismyip.com'
- '+.whatismyipaddress.com'
- '+.whoer.com'
- '+.whoer.net'
+6
View File
@@ -0,0 +1,6 @@
# ipcheck-domains — категория: leak-test
# Правил: 3. Файл генерируется автоматически, не редактируйте вручную.
# Источник: data/leak-test.txt
DOMAIN-SUFFIX,browserleaks.com
DOMAIN-SUFFIX,dnsleaktest.com
DOMAIN-SUFFIX,ipleak.net
+7
View File
@@ -0,0 +1,7 @@
# ipcheck-domains — категория: leak-test
# Правил: 3. Файл генерируется автоматически, не редактируйте вручную.
# Источник: data/leak-test.txt
payload:
- '+.browserleaks.com'
- '+.dnsleaktest.com'
- '+.ipleak.net'
-21
View File
@@ -1,21 +0,0 @@
# ip-lookup — сервисы "какой у меня IP" и общая информация об IP.
# Формат строки: [type:]value [@attr ...] Комментарии — от символа "#".
# Без префикса значение трактуется как domain: (сам домен + все поддомены).
2ip.ru
2ip.io
whoer.net
whoer.com
whatismyipaddress.com
whatismyip.com
myip.com
ipinfo.io
ip-api.com
ipapi.co
ipapi.is
ipify.org
ifconfig.me
ifconfig.co
ifconfig.io
icanhazip.com
ip.sb
-7
View File
@@ -1,7 +0,0 @@
# ipcheck — сводная категория: всё, что определяет или проверяет IP-адрес.
# Именно её используют в роутинге как geosite:ipcheck.
include:ip-lookup
include:leak-test
include:geoip-db
include:fraud-score
-5
View File
@@ -1,5 +0,0 @@
# leak-test — тесты утечек: WebRTC, DNS, отпечаток браузера.
browserleaks.com
ipleak.net
dnsleaktest.com
+25
View File
@@ -0,0 +1,25 @@
³
FRAUD-SCORE␍criminalip.io␍fraudguard.iogetipintel.net
iphub.infoipqualityscore.com␍proxycheck.ioscamalytics.com spur.us vpnapi.io
­
GEOIP-DBbigdatacloud.net db-ip.comip2location.com
ipbase.com ipdata.coipgeolocation.io␍ipregistry.co
ipwhois.io maxmind.com
«
IP-LOOKUP
2ip.io
2ip.ru␍icanhazip.com ifconfig.co ifconfig.io ifconfig.me
ip-api.com ip.sb ipaddress.is ipapi.co ipapi.is ipify.org ipinfo.io myip.comwhatismyip.comwhatismyipaddress.com whoer.com whoer.net
­
IPCHECK
2ip.io
2ip.rubigdatacloud.netbrowserleaks.com␍criminalip.io db-ip.comdnsleaktest.com␍fraudguard.iogetipintel.net␍icanhazip.com ifconfig.co ifconfig.io ifconfig.me
ip-api.com ip.sbip2location.com ipaddress.is ipapi.co ipapi.is
ipbase.com ipdata.coipgeolocation.io
iphub.info ipify.org ipinfo.io
ipleak.netipqualityscore.com␍ipregistry.co
ipwhois.io maxmind.com myip.com␍proxycheck.ioscamalytics.com spur.us vpnapi.iowhatismyip.comwhatismyipaddress.com whoer.com whoer.net
F
LEAK-TESTbrowserleaks.comdnsleaktest.com
ipleak.net
+1
View File
@@ -0,0 +1 @@
40ac1b41d51aecabf178ff98ade8dfe2587bd6b49c66260331990774588b040c geosite.dat
+25
View File
@@ -0,0 +1,25 @@
³
FRAUD-SCORE␍criminalip.io␍fraudguard.iogetipintel.net
iphub.infoipqualityscore.com␍proxycheck.ioscamalytics.com spur.us vpnapi.io
­
GEOIP-DBbigdatacloud.net db-ip.comip2location.com
ipbase.com ipdata.coipgeolocation.io␍ipregistry.co
ipwhois.io maxmind.com
«
IP-LOOKUP
2ip.io
2ip.ru␍icanhazip.com ifconfig.co ifconfig.io ifconfig.me
ip-api.com ip.sb ipaddress.is ipapi.co ipapi.is ipify.org ipinfo.io myip.comwhatismyip.comwhatismyipaddress.com whoer.com whoer.net
­
IPCHECK
2ip.io
2ip.rubigdatacloud.netbrowserleaks.com␍criminalip.io db-ip.comdnsleaktest.com␍fraudguard.iogetipintel.net␍icanhazip.com ifconfig.co ifconfig.io ifconfig.me
ip-api.com ip.sbip2location.com ipaddress.is ipapi.co ipapi.is
ipbase.com ipdata.coipgeolocation.io
iphub.info ipify.org ipinfo.io
ipleak.netipqualityscore.com␍ipregistry.co
ipwhois.io maxmind.com myip.com␍proxycheck.ioscamalytics.com spur.us vpnapi.iowhatismyip.comwhatismyipaddress.com whoer.com whoer.net
F
LEAK-TESTbrowserleaks.comdnsleaktest.com
ipleak.net
+1
View File
@@ -0,0 +1 @@
40ac1b41d51aecabf178ff98ade8dfe2587bd6b49c66260331990774588b040c ipcheck.dat
+42
View File
@@ -0,0 +1,42 @@
{
"category": "fraud-score",
"count": 9,
"rules": [
{
"type": "domain",
"value": "criminalip.io"
},
{
"type": "domain",
"value": "fraudguard.io"
},
{
"type": "domain",
"value": "getipintel.net"
},
{
"type": "domain",
"value": "iphub.info"
},
{
"type": "domain",
"value": "ipqualityscore.com"
},
{
"type": "domain",
"value": "proxycheck.io"
},
{
"type": "domain",
"value": "scamalytics.com"
},
{
"type": "domain",
"value": "spur.us"
},
{
"type": "domain",
"value": "vpnapi.io"
}
]
}
+42
View File
@@ -0,0 +1,42 @@
{
"category": "geoip-db",
"count": 9,
"rules": [
{
"type": "domain",
"value": "bigdatacloud.net"
},
{
"type": "domain",
"value": "db-ip.com"
},
{
"type": "domain",
"value": "ip2location.com"
},
{
"type": "domain",
"value": "ipbase.com"
},
{
"type": "domain",
"value": "ipdata.co"
},
{
"type": "domain",
"value": "ipgeolocation.io"
},
{
"type": "domain",
"value": "ipregistry.co"
},
{
"type": "domain",
"value": "ipwhois.io"
},
{
"type": "domain",
"value": "maxmind.com"
}
]
}
+78
View File
@@ -0,0 +1,78 @@
{
"category": "ip-lookup",
"count": 18,
"rules": [
{
"type": "domain",
"value": "2ip.io"
},
{
"type": "domain",
"value": "2ip.ru"
},
{
"type": "domain",
"value": "icanhazip.com"
},
{
"type": "domain",
"value": "ifconfig.co"
},
{
"type": "domain",
"value": "ifconfig.io"
},
{
"type": "domain",
"value": "ifconfig.me"
},
{
"type": "domain",
"value": "ip-api.com"
},
{
"type": "domain",
"value": "ip.sb"
},
{
"type": "domain",
"value": "ipaddress.is"
},
{
"type": "domain",
"value": "ipapi.co"
},
{
"type": "domain",
"value": "ipapi.is"
},
{
"type": "domain",
"value": "ipify.org"
},
{
"type": "domain",
"value": "ipinfo.io"
},
{
"type": "domain",
"value": "myip.com"
},
{
"type": "domain",
"value": "whatismyip.com"
},
{
"type": "domain",
"value": "whatismyipaddress.com"
},
{
"type": "domain",
"value": "whoer.com"
},
{
"type": "domain",
"value": "whoer.net"
}
]
}
+162
View File
@@ -0,0 +1,162 @@
{
"category": "ipcheck",
"count": 39,
"rules": [
{
"type": "domain",
"value": "2ip.io"
},
{
"type": "domain",
"value": "2ip.ru"
},
{
"type": "domain",
"value": "bigdatacloud.net"
},
{
"type": "domain",
"value": "browserleaks.com"
},
{
"type": "domain",
"value": "criminalip.io"
},
{
"type": "domain",
"value": "db-ip.com"
},
{
"type": "domain",
"value": "dnsleaktest.com"
},
{
"type": "domain",
"value": "fraudguard.io"
},
{
"type": "domain",
"value": "getipintel.net"
},
{
"type": "domain",
"value": "icanhazip.com"
},
{
"type": "domain",
"value": "ifconfig.co"
},
{
"type": "domain",
"value": "ifconfig.io"
},
{
"type": "domain",
"value": "ifconfig.me"
},
{
"type": "domain",
"value": "ip-api.com"
},
{
"type": "domain",
"value": "ip.sb"
},
{
"type": "domain",
"value": "ip2location.com"
},
{
"type": "domain",
"value": "ipaddress.is"
},
{
"type": "domain",
"value": "ipapi.co"
},
{
"type": "domain",
"value": "ipapi.is"
},
{
"type": "domain",
"value": "ipbase.com"
},
{
"type": "domain",
"value": "ipdata.co"
},
{
"type": "domain",
"value": "ipgeolocation.io"
},
{
"type": "domain",
"value": "iphub.info"
},
{
"type": "domain",
"value": "ipify.org"
},
{
"type": "domain",
"value": "ipinfo.io"
},
{
"type": "domain",
"value": "ipleak.net"
},
{
"type": "domain",
"value": "ipqualityscore.com"
},
{
"type": "domain",
"value": "ipregistry.co"
},
{
"type": "domain",
"value": "ipwhois.io"
},
{
"type": "domain",
"value": "maxmind.com"
},
{
"type": "domain",
"value": "myip.com"
},
{
"type": "domain",
"value": "proxycheck.io"
},
{
"type": "domain",
"value": "scamalytics.com"
},
{
"type": "domain",
"value": "spur.us"
},
{
"type": "domain",
"value": "vpnapi.io"
},
{
"type": "domain",
"value": "whatismyip.com"
},
{
"type": "domain",
"value": "whatismyipaddress.com"
},
{
"type": "domain",
"value": "whoer.com"
},
{
"type": "domain",
"value": "whoer.net"
}
]
}
+18
View File
@@ -0,0 +1,18 @@
{
"category": "leak-test",
"count": 3,
"rules": [
{
"type": "domain",
"value": "browserleaks.com"
},
{
"type": "domain",
"value": "dnsleaktest.com"
},
{
"type": "domain",
"value": "ipleak.net"
}
]
}
+13
View File
@@ -0,0 +1,13 @@
{
"built_at": "2026-08-07T11:33:25Z",
"geosite_dat_sha256": "40ac1b41d51aecabf178ff98ade8dfe2587bd6b49c66260331990774588b040c",
"geosite_dat_size": 1420,
"total_rules": 78,
"categories": {
"fraud-score": 9,
"geoip-db": 9,
"ip-lookup": 18,
"ipcheck": 39,
"leak-test": 3
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+29
View File
@@ -0,0 +1,29 @@
{
"version": 1,
"rules": [
{
"domain": [
"criminalip.io",
"fraudguard.io",
"getipintel.net",
"iphub.info",
"ipqualityscore.com",
"proxycheck.io",
"scamalytics.com",
"spur.us",
"vpnapi.io"
],
"domain_suffix": [
".criminalip.io",
".fraudguard.io",
".getipintel.net",
".iphub.info",
".ipqualityscore.com",
".proxycheck.io",
".scamalytics.com",
".spur.us",
".vpnapi.io"
]
}
]
}
Binary file not shown.
+29
View File
@@ -0,0 +1,29 @@
{
"version": 1,
"rules": [
{
"domain": [
"bigdatacloud.net",
"db-ip.com",
"ip2location.com",
"ipbase.com",
"ipdata.co",
"ipgeolocation.io",
"ipregistry.co",
"ipwhois.io",
"maxmind.com"
],
"domain_suffix": [
".bigdatacloud.net",
".db-ip.com",
".ip2location.com",
".ipbase.com",
".ipdata.co",
".ipgeolocation.io",
".ipregistry.co",
".ipwhois.io",
".maxmind.com"
]
}
]
}
Binary file not shown.
+47
View File
@@ -0,0 +1,47 @@
{
"version": 1,
"rules": [
{
"domain": [
"2ip.io",
"2ip.ru",
"icanhazip.com",
"ifconfig.co",
"ifconfig.io",
"ifconfig.me",
"ip-api.com",
"ip.sb",
"ipaddress.is",
"ipapi.co",
"ipapi.is",
"ipify.org",
"ipinfo.io",
"myip.com",
"whatismyip.com",
"whatismyipaddress.com",
"whoer.com",
"whoer.net"
],
"domain_suffix": [
".2ip.io",
".2ip.ru",
".icanhazip.com",
".ifconfig.co",
".ifconfig.io",
".ifconfig.me",
".ip-api.com",
".ip.sb",
".ipaddress.is",
".ipapi.co",
".ipapi.is",
".ipify.org",
".ipinfo.io",
".myip.com",
".whatismyip.com",
".whatismyipaddress.com",
".whoer.com",
".whoer.net"
]
}
]
}
Binary file not shown.
+89
View File
@@ -0,0 +1,89 @@
{
"version": 1,
"rules": [
{
"domain": [
"2ip.io",
"2ip.ru",
"bigdatacloud.net",
"browserleaks.com",
"criminalip.io",
"db-ip.com",
"dnsleaktest.com",
"fraudguard.io",
"getipintel.net",
"icanhazip.com",
"ifconfig.co",
"ifconfig.io",
"ifconfig.me",
"ip-api.com",
"ip.sb",
"ip2location.com",
"ipaddress.is",
"ipapi.co",
"ipapi.is",
"ipbase.com",
"ipdata.co",
"ipgeolocation.io",
"iphub.info",
"ipify.org",
"ipinfo.io",
"ipleak.net",
"ipqualityscore.com",
"ipregistry.co",
"ipwhois.io",
"maxmind.com",
"myip.com",
"proxycheck.io",
"scamalytics.com",
"spur.us",
"vpnapi.io",
"whatismyip.com",
"whatismyipaddress.com",
"whoer.com",
"whoer.net"
],
"domain_suffix": [
".2ip.io",
".2ip.ru",
".bigdatacloud.net",
".browserleaks.com",
".criminalip.io",
".db-ip.com",
".dnsleaktest.com",
".fraudguard.io",
".getipintel.net",
".icanhazip.com",
".ifconfig.co",
".ifconfig.io",
".ifconfig.me",
".ip-api.com",
".ip.sb",
".ip2location.com",
".ipaddress.is",
".ipapi.co",
".ipapi.is",
".ipbase.com",
".ipdata.co",
".ipgeolocation.io",
".iphub.info",
".ipify.org",
".ipinfo.io",
".ipleak.net",
".ipqualityscore.com",
".ipregistry.co",
".ipwhois.io",
".maxmind.com",
".myip.com",
".proxycheck.io",
".scamalytics.com",
".spur.us",
".vpnapi.io",
".whatismyip.com",
".whatismyipaddress.com",
".whoer.com",
".whoer.net"
]
}
]
}
Binary file not shown.
+17
View File
@@ -0,0 +1,17 @@
{
"version": 1,
"rules": [
{
"domain": [
"browserleaks.com",
"dnsleaktest.com",
"ipleak.net"
],
"domain_suffix": [
".browserleaks.com",
".dnsleaktest.com",
".ipleak.net"
]
}
]
}
Binary file not shown.
+13
View File
@@ -0,0 +1,13 @@
# ipcheck-domains — категория: fraud-score
# Правил: 9. Файл генерируется автоматически, не редактируйте вручную.
# Источник: data/fraud-score.txt
criminalip.io
fraudguard.io
getipintel.net
iphub.info
ipqualityscore.com
proxycheck.io
scamalytics.com
spur.us
vpnapi.io
+13
View File
@@ -0,0 +1,13 @@
# ipcheck-domains — категория: geoip-db
# Правил: 9. Файл генерируется автоматически, не редактируйте вручную.
# Источник: data/geoip-db.txt
bigdatacloud.net
db-ip.com
ip2location.com
ipbase.com
ipdata.co
ipgeolocation.io
ipregistry.co
ipwhois.io
maxmind.com
+22
View File
@@ -0,0 +1,22 @@
# ipcheck-domains — категория: ip-lookup
# Правил: 18. Файл генерируется автоматически, не редактируйте вручную.
# Источник: data/ip-lookup.txt
2ip.io
2ip.ru
icanhazip.com
ifconfig.co
ifconfig.io
ifconfig.me
ip-api.com
ip.sb
ipaddress.is
ipapi.co
ipapi.is
ipify.org
ipinfo.io
myip.com
whatismyip.com
whatismyipaddress.com
whoer.com
whoer.net
+43
View File
@@ -0,0 +1,43 @@
# ipcheck-domains — категория: ipcheck
# Правил: 39. Файл генерируется автоматически, не редактируйте вручную.
# Источник: data/ipcheck.txt
2ip.io
2ip.ru
bigdatacloud.net
browserleaks.com
criminalip.io
db-ip.com
dnsleaktest.com
fraudguard.io
getipintel.net
icanhazip.com
ifconfig.co
ifconfig.io
ifconfig.me
ip-api.com
ip.sb
ip2location.com
ipaddress.is
ipapi.co
ipapi.is
ipbase.com
ipdata.co
ipgeolocation.io
iphub.info
ipify.org
ipinfo.io
ipleak.net
ipqualityscore.com
ipregistry.co
ipwhois.io
maxmind.com
myip.com
proxycheck.io
scamalytics.com
spur.us
vpnapi.io
whatismyip.com
whatismyipaddress.com
whoer.com
whoer.net
+7
View File
@@ -0,0 +1,7 @@
# ipcheck-domains — категория: leak-test
# Правил: 3. Файл генерируется автоматически, не редактируйте вручную.
# Источник: data/leak-test.txt
browserleaks.com
dnsleaktest.com
ipleak.net
-529
View File
@@ -1,529 +0,0 @@
#!/usr/bin/env python3
"""Собирает geosite-артефакты из data/ в несколько форматов.
Основной формат — geosite.dat (protobuf v2ray/Xray, подходит для 3x-ui).
Дополнительно: plain text, JSON, sing-box rule-set, Clash/mihomo, AdGuard.
Зависимостей нет — только стандартная библиотека. Внешние бинарники
(sing-box, mihomo) нужны лишь для компиляции .srs/.mrs и опциональны.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DATA_DIR = ROOT / "data"
TYPE_DOMAIN = "domain"
TYPE_FULL = "full"
TYPE_KEYWORD = "keyword"
TYPE_REGEXP = "regexp"
# Значения enum Domain.Type из v2ray router protobuf.
PB_TYPE = {TYPE_KEYWORD: 0, TYPE_REGEXP: 1, TYPE_DOMAIN: 2, TYPE_FULL: 3}
# Порядок вывода: сначала суффиксы, потом точные, потом keyword/regexp.
TYPE_ORDER = {TYPE_DOMAIN: 0, TYPE_FULL: 1, TYPE_KEYWORD: 2, TYPE_REGEXP: 3}
LABEL_RE = r"(?!-)[a-z0-9_-]{1,63}(?<!-)"
DOMAIN_RE = re.compile(rf"^{LABEL_RE}(\.{LABEL_RE})+$")
ATTR_RE = re.compile(r"^[a-z0-9_-]+$")
CATEGORY_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
class BuildError(Exception):
"""Ошибка в исходных данных — сборка должна упасть."""
@dataclass(frozen=True)
class Rule:
type: str
value: str
attrs: tuple[str, ...] = ()
@property
def sort_key(self) -> tuple[int, str]:
return (TYPE_ORDER[self.type], self.value)
def __str__(self) -> str:
base = self.value if self.type == TYPE_DOMAIN else f"{self.type}:{self.value}"
return " ".join([base, *(f"@{a}" for a in self.attrs)])
# --------------------------------------------------------------------------- #
# Разбор исходников
# --------------------------------------------------------------------------- #
def source_path(category: str) -> Path:
return DATA_DIR / f"{category}.txt"
def list_categories() -> list[str]:
return sorted(p.stem for p in DATA_DIR.glob("*.txt"))
def parse_token(token: str, where: str) -> Rule:
prefix, sep, value = token.partition(":")
if sep and prefix.lower() in (TYPE_DOMAIN, TYPE_FULL, TYPE_KEYWORD, TYPE_REGEXP):
rtype = prefix.lower()
else:
rtype, value = TYPE_DOMAIN, token
if not value:
raise BuildError(f"{where}: пустое значение в '{token}'")
if rtype in (TYPE_DOMAIN, TYPE_FULL):
try:
value = value.encode("idna").decode("ascii")
except UnicodeError:
pass # ниже отловит DOMAIN_RE
value = value.lower().strip(".")
if not DOMAIN_RE.match(value):
raise BuildError(f"{where}: невалидный домен '{value}'")
elif rtype == TYPE_KEYWORD:
value = value.lower()
if any(c.isspace() for c in value):
raise BuildError(f"{where}: keyword не может содержать пробелы")
elif rtype == TYPE_REGEXP:
try:
re.compile(value)
except re.error as exc:
raise BuildError(f"{where}: невалидный regexp '{value}': {exc}") from exc
return Rule(rtype, value)
def parse_category(category: str) -> list[tuple[str, Rule | str]]:
"""Возвращает список ('rule', Rule) / ('include', category) в порядке файла."""
path = source_path(category)
if not path.is_file():
raise BuildError(f"категория '{category}' не найдена: {path}")
entries: list[tuple[str, Rule | str]] = []
for lineno, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
line = raw.split("#", 1)[0].strip()
if not line:
continue
where = f"{path.name}:{lineno}"
token, *rest = line.split()
attrs = []
for part in rest:
if not part.startswith("@"):
raise BuildError(f"{where}: ожидался атрибут '@name', получено '{part}'")
attr = part[1:].lower()
if not ATTR_RE.match(attr):
raise BuildError(f"{where}: невалидный атрибут '@{attr}'")
attrs.append(attr)
if token.startswith("include:"):
if attrs:
raise BuildError(f"{where}: include не поддерживает атрибуты")
target = token[len("include:") :]
if not CATEGORY_RE.match(target):
raise BuildError(f"{where}: невалидное имя категории '{target}'")
entries.append(("include", target))
continue
rule = parse_token(token, where)
entries.append(("rule", Rule(rule.type, rule.value, tuple(sorted(set(attrs))))))
return entries
def resolve(category: str, stack: tuple[str, ...] = ()) -> list[Rule]:
"""Разворачивает include-и, убирает дубликаты, сортирует детерминированно."""
if category in stack:
chain = " -> ".join([*stack, category])
raise BuildError(f"циклический include: {chain}")
collected: dict[Rule, None] = {}
for kind, payload in parse_category(category):
if kind == "include":
for rule in resolve(str(payload), (*stack, category)):
collected.setdefault(rule, None)
else:
collected.setdefault(payload, None) # type: ignore[arg-type]
return sorted(collected, key=lambda r: r.sort_key)
# --------------------------------------------------------------------------- #
# protobuf: geosite.dat
# --------------------------------------------------------------------------- #
def _varint(value: int) -> bytes:
out = bytearray()
while True:
chunk = value & 0x7F
value >>= 7
if value:
out.append(chunk | 0x80)
else:
out.append(chunk)
return bytes(out)
def _len_field(field: int, payload: bytes) -> bytes:
return _varint(field << 3 | 2) + _varint(len(payload)) + payload
def _str_field(field: int, value: str) -> bytes:
return _len_field(field, value.encode("utf-8"))
def _varint_field(field: int, value: int) -> bytes:
return _varint(field << 3 | 0) + _varint(value)
def encode_geosite_dat(categories: dict[str, list[Rule]]) -> bytes:
"""GeoSiteList{ repeated GeoSite entry = 1 }."""
out = bytearray()
for name in sorted(categories):
entry = bytearray(_str_field(1, name.upper())) # GeoSite.country_code
for rule in categories[name]:
domain = bytearray()
pb_type = PB_TYPE[rule.type]
if pb_type: # proto3 опускает значение по умолчанию (0 == Plain)
domain += _varint_field(1, pb_type)
domain += _str_field(2, rule.value)
for attr in rule.attrs:
# Attribute{ key = 1; bool_value = 2 }
domain += _len_field(3, _str_field(1, attr) + _varint_field(2, 1))
entry += _len_field(2, bytes(domain)) # GeoSite.domain
out += _len_field(1, bytes(entry))
return bytes(out)
def decode_geosite_dat(blob: bytes) -> dict[str, list[Rule]]:
"""Обратный разбор — используется для самопроверки собранного файла."""
pb_to_type = {v: k for k, v in PB_TYPE.items()}
def fields(buf: bytes):
pos = 0
while pos < len(buf):
key, pos = _read_varint(buf, pos)
field, wire = key >> 3, key & 7
if wire == 0:
value, pos = _read_varint(buf, pos)
yield field, value
elif wire == 2:
length, pos = _read_varint(buf, pos)
yield field, buf[pos : pos + length]
pos += length
else:
raise BuildError(f"неподдерживаемый wire type {wire}")
result: dict[str, list[Rule]] = {}
for field, payload in fields(blob):
if field != 1:
continue
name, rules = "", []
for f, p in fields(payload):
if f == 1:
name = p.decode("utf-8")
elif f == 2:
rtype, value, attrs = TYPE_KEYWORD, "", []
for df, dp in fields(p):
if df == 1:
rtype = pb_to_type[dp]
elif df == 2:
value = dp.decode("utf-8")
elif df == 3:
for af, ap in fields(dp):
if af == 1:
attrs.append(ap.decode("utf-8"))
rules.append(Rule(rtype, value, tuple(attrs)))
result[name] = rules
return result
def _read_varint(buf: bytes, pos: int) -> tuple[int, int]:
value = shift = 0
while True:
byte = buf[pos]
pos += 1
value |= (byte & 0x7F) << shift
if not byte & 0x80:
return value, pos
shift += 7
# --------------------------------------------------------------------------- #
# Текстовые форматы
# --------------------------------------------------------------------------- #
def write(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content.replace("\r\n", "\n"), encoding="utf-8", newline="\n")
def header(category: str, count: int, comment: str = "#") -> str:
return (
f"{comment} ipcheck-domains — категория: {category}\n"
f"{comment} Правил: {count}. Файл генерируется автоматически, не редактируйте вручную.\n"
f"{comment} Источник: data/{category}.txt\n"
)
def render_plain(rules: list[Rule]) -> str:
"""Только «голые» домены — для скриптов и ручной вставки."""
seen = {r.value for r in rules if r.type in (TYPE_DOMAIN, TYPE_FULL)}
return "".join(f"{value}\n" for value in sorted(seen))
def render_source(category: str, rules: list[Rule]) -> str:
return header(category, len(rules)) + "\n" + "".join(f"{r}\n" for r in rules)
def render_json(category: str, rules: list[Rule]) -> str:
payload = {
"category": category,
"count": len(rules),
"rules": [
{"type": r.type, "value": r.value, **({"attrs": list(r.attrs)} if r.attrs else {})}
for r in rules
],
}
return json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
def render_singbox(rules: list[Rule]) -> str:
"""sing-box rule-set (source). domain: apex + domain_suffix: .apex == v2ray domain:."""
rule: dict[str, list[str]] = {}
for kind, values in (
("domain", sorted({r.value for r in rules if r.type in (TYPE_DOMAIN, TYPE_FULL)})),
("domain_suffix", sorted({f".{r.value}" for r in rules if r.type == TYPE_DOMAIN})),
("domain_keyword", sorted({r.value for r in rules if r.type == TYPE_KEYWORD})),
("domain_regex", sorted({r.value for r in rules if r.type == TYPE_REGEXP})),
):
if values:
rule[kind] = values
return json.dumps({"version": 1, "rules": [rule] if rule else []}, indent=2) + "\n"
def render_clash_yaml(category: str, rules: list[Rule]) -> str:
"""rule-provider с behavior: domain — keyword/regexp этим форматом не поддерживаются."""
lines = [header(category, len(rules)), "payload:\n"]
for rule in rules:
if rule.type == TYPE_DOMAIN:
lines.append(f" - '+.{rule.value}'\n")
elif rule.type == TYPE_FULL:
lines.append(f" - '{rule.value}'\n")
return "".join(lines)
def render_clash_list(category: str, rules: list[Rule]) -> str:
"""rule-provider с behavior: classical."""
mapping = {
TYPE_DOMAIN: "DOMAIN-SUFFIX",
TYPE_FULL: "DOMAIN",
TYPE_KEYWORD: "DOMAIN-KEYWORD",
TYPE_REGEXP: "DOMAIN-REGEX",
}
body = "".join(f"{mapping[r.type]},{r.value}\n" for r in rules)
return header(category, len(rules)) + body
def render_adguard(category: str, rules: list[Rule]) -> str:
lines = [header(category, len(rules), comment="!")]
for rule in rules:
if rule.type == TYPE_DOMAIN:
lines.append(f"||{rule.value}^\n")
elif rule.type == TYPE_FULL:
lines.append(f"||{rule.value}^$important\n")
return "".join(lines)
# --------------------------------------------------------------------------- #
# Компиляция бинарных rule-set (опционально)
# --------------------------------------------------------------------------- #
def compile_binaries(out_dir: Path, categories: list[str], singbox: str | None, mihomo: str | None) -> list[str]:
produced = []
if singbox:
for category in categories:
src = out_dir / "sing-box" / f"{category}.json"
dst = out_dir / "sing-box" / f"{category}.srs"
subprocess.run([singbox, "rule-set", "compile", "--output", str(dst), str(src)], check=True)
produced.append(str(dst.relative_to(out_dir)))
else:
print(" sing-box не найден — .srs пропущены", file=sys.stderr)
if mihomo:
for category in categories:
src = out_dir / "clash" / f"{category}.yaml"
dst = out_dir / "mihomo" / f"{category}.mrs"
dst.parent.mkdir(parents=True, exist_ok=True)
subprocess.run([mihomo, "convert-ruleset", "domain", "yaml", str(src), str(dst)], check=True)
produced.append(str(dst.relative_to(out_dir)))
else:
print(" mihomo не найден — .mrs пропущены", file=sys.stderr)
return produced
# --------------------------------------------------------------------------- #
# Точка входа
# --------------------------------------------------------------------------- #
def build(out_dir: Path, singbox: str | None, mihomo: str | None) -> int:
categories = list_categories()
if not categories:
raise BuildError(f"в {DATA_DIR} нет ни одного .txt")
resolved = {name: resolve(name) for name in categories}
if out_dir.exists():
shutil.rmtree(out_dir)
out_dir.mkdir(parents=True)
dat = encode_geosite_dat(resolved)
(out_dir / "geosite.dat").write_bytes(dat)
# Копия под отдельным именем: её можно положить рядом со штатным
# geosite.dat и адресовать как ext:ipcheck.dat:ipcheck, ничего не перезаписывая.
(out_dir / "ipcheck.dat").write_bytes(dat)
# Самопроверка: файл должен читаться обратно ровно в то, что мы записали.
decoded = decode_geosite_dat(dat)
expected = {name.upper(): rules for name, rules in resolved.items()}
if decoded != expected:
raise BuildError("geosite.dat не проходит round-trip проверку")
digest = hashlib.sha256(dat).hexdigest()
write(out_dir / "geosite.dat.sha256sum", f"{digest} geosite.dat\n")
write(out_dir / "ipcheck.dat.sha256sum", f"{digest} ipcheck.dat\n")
for name, rules in resolved.items():
write(out_dir / "txt" / f"{name}.txt", render_plain(rules))
write(out_dir / "src" / f"{name}.txt", render_source(name, rules))
write(out_dir / "json" / f"{name}.json", render_json(name, rules))
write(out_dir / "sing-box" / f"{name}.json", render_singbox(rules))
write(out_dir / "clash" / f"{name}.yaml", render_clash_yaml(name, rules))
write(out_dir / "clash" / f"{name}.list", render_clash_list(name, rules))
write(out_dir / "adguard" / f"{name}.txt", render_adguard(name, rules))
compile_binaries(out_dir, categories, singbox, mihomo)
epoch = os.environ.get("SOURCE_DATE_EPOCH")
built_at = (
datetime.fromtimestamp(int(epoch), tz=timezone.utc)
if epoch
else datetime.now(tz=timezone.utc)
)
write(
out_dir / "metadata.json",
json.dumps(
{
"built_at": built_at.replace(microsecond=0).isoformat().replace("+00:00", "Z"),
"geosite_dat_sha256": digest,
"geosite_dat_size": len(dat),
"total_rules": sum(len(r) for r in resolved.values()),
"categories": {name: len(rules) for name, rules in resolved.items()},
},
ensure_ascii=False,
indent=2,
)
+ "\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]}")
for name in categories:
print(f" {name:<12} {len(resolved[name]):>4} правил")
print(f"Готово: {out_dir}")
return 0
def check() -> int:
categories = list_categories()
if not categories:
raise BuildError(f"в {DATA_DIR} нет ни одного .txt")
problems = []
for name in categories:
if not CATEGORY_RE.match(name):
problems.append(f"{name}: имя категории должно быть [a-z0-9-]")
entries = parse_category(name)
seen: dict[Rule, int] = {}
for index, (kind, payload) in enumerate(entries):
if kind != "rule":
continue
rule: Rule = payload # type: ignore[assignment]
if rule in seen:
problems.append(f"{name}.txt: дубликат '{rule}'")
seen[rule] = index
suffixes = {r.value for r in seen if r.type == TYPE_DOMAIN}
for rule in seen:
if rule.type != TYPE_DOMAIN:
continue
parent = rule.value.split(".", 1)[1] if "." in rule.value else ""
while parent:
if parent in suffixes:
problems.append(f"{name}.txt: '{rule.value}' уже покрыт '{parent}'")
break
parent = parent.split(".", 1)[1] if "." in parent else ""
resolve(name) # ловит циклы и битые include
for problem in problems:
print(f"error: {problem}", file=sys.stderr)
if problems:
return 1
print(f"ok: {len(categories)} категорий, замечаний нет")
return 0
def main(argv: list[str] | None = None) -> int:
for stream in (sys.stdout, sys.stderr):
stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--out", type=Path, default=ROOT / "dist", help="каталог сборки (по умолчанию dist/)")
parser.add_argument("--check", action="store_true", help="только проверить исходники, ничего не собирать")
parser.add_argument("--sing-box", dest="singbox", help="путь к бинарю sing-box для сборки .srs")
parser.add_argument("--mihomo", help="путь к бинарю mihomo для сборки .mrs")
args = parser.parse_args(argv)
try:
if args.check:
return check()
return build(
args.out.resolve(),
args.singbox or shutil.which("sing-box"),
args.mihomo or shutil.which("mihomo"),
)
except BuildError as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())
-48
View File
@@ -1,48 +0,0 @@
#!/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
-50
View File
@@ -1,50 +0,0 @@
#!/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
-174
View File
@@ -1,174 +0,0 @@
#!/usr/bin/env python3
"""Тесты сборщика: python -m unittest discover -s tools -t tools"""
from __future__ import annotations
import contextlib
import json
import tempfile
import unittest
from pathlib import Path
import build
from build import BuildError, Rule
@contextlib.contextmanager
def data(**files: str):
"""Подменяет data/ временным каталогом с заданными категориями."""
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
for name, content in files.items():
(root / f"{name.replace('_', '-')}.txt").write_text(content, encoding="utf-8")
original = build.DATA_DIR
build.DATA_DIR = root
try:
yield root
finally:
build.DATA_DIR = original
class ParseTest(unittest.TestCase):
def test_default_type_is_domain(self):
with data(a="example.com\n"):
self.assertEqual(build.resolve("a"), [Rule("domain", "example.com")])
def test_all_prefixes(self):
source = "domain:a.com\nfull:b.com\nkeyword:cc\nregexp:^d[0-9]+\\.com$\n"
with data(a=source):
self.assertEqual(
build.resolve("a"),
[
Rule("domain", "a.com"),
Rule("full", "b.com"),
Rule("keyword", "cc"),
Rule("regexp", r"^d[0-9]+\.com$"),
],
)
def test_comments_blank_lines_and_attributes(self):
with data(a="# заголовок\n\na.com @ads @cn # хвост\n"):
self.assertEqual(build.resolve("a"), [Rule("domain", "a.com", ("ads", "cn"))])
def test_normalisation(self):
with data(a="Example.COM.\nDOMAIN:пример.рф\n"):
values = [r.value for r in build.resolve("a")]
self.assertIn("example.com", values)
self.assertIn("xn--e1afmkfd.xn--p1ai", values)
def test_include_dedupes_and_sorts(self):
with data(a="include:b\nz.com\na.com\n", b="a.com\nfull:m.com\n"):
self.assertEqual(
build.resolve("a"),
[Rule("domain", "a.com"), Rule("domain", "z.com"), Rule("full", "m.com")],
)
def test_rejects_bad_input(self):
cases = {
"битый домен": "not_a_domain\n",
"битый regexp": "regexp:[unclosed\n",
"атрибут без @": "a.com ads\n",
"неизвестный include": "include:missing\n",
}
for label, source in cases.items():
with self.subTest(label), data(a=source):
with self.assertRaises(BuildError):
build.resolve("a")
def test_detects_include_cycle(self):
with data(a="include:b\n", b="include:a\n"):
with self.assertRaisesRegex(BuildError, "цикл"):
build.resolve("a")
class ProtobufTest(unittest.TestCase):
def test_known_byte_layout(self):
blob = build.encode_geosite_dat({"t": [Rule("domain", "a.com")]})
# 0a 0e GeoSiteList.entry, 14 байт
# 0a 01 54 country_code = "T" (в .dat теги в верхнем регистре)
# 12 09 GeoSite.domain, 9 байт
# 08 02 type = Domain(2)
# 12 05 612e636f6d value = "a.com"
self.assertEqual(blob.hex(), "0a0e" "0a0154" "1209" "0802" "1205" "612e636f6d")
def test_keyword_type_is_omitted_as_proto3_default(self):
blob = build.encode_geosite_dat({"t": [Rule("keyword", "ip")]})
self.assertNotIn(b"\x08\x00", blob)
self.assertEqual(build.decode_geosite_dat(blob)["T"], [Rule("keyword", "ip")])
def test_round_trip_with_attributes(self):
categories = {
"one": [Rule("domain", "a.com", ("ads",)), Rule("full", "b.com")],
"two": [Rule("keyword", "ip"), Rule("regexp", r"^x\d+$")],
}
decoded = build.decode_geosite_dat(build.encode_geosite_dat(categories))
self.assertEqual(decoded, {k.upper(): v for k, v in categories.items()})
class RenderTest(unittest.TestCase):
def test_singbox_expands_suffix_to_apex_plus_dot(self):
rendered = json.loads(build.render_singbox([Rule("domain", "a.com"), Rule("full", "b.com")]))
self.assertEqual(rendered["rules"][0]["domain"], ["a.com", "b.com"])
self.assertEqual(rendered["rules"][0]["domain_suffix"], [".a.com"])
def test_clash_yaml_skips_unsupported_types(self):
rules = [Rule("domain", "a.com"), Rule("full", "b.com"), Rule("keyword", "ip")]
payload = build.render_clash_yaml("t", rules)
self.assertIn("- '+.a.com'", payload)
self.assertIn("- 'b.com'", payload)
self.assertNotIn("ip", payload.split("payload:")[1])
def test_clash_list_keeps_all_types(self):
rules = [Rule("domain", "a.com"), Rule("keyword", "ip"), Rule("regexp", "^x$")]
body = build.render_clash_list("t", rules)
self.assertIn("DOMAIN-SUFFIX,a.com", body)
self.assertIn("DOMAIN-KEYWORD,ip", body)
self.assertIn("DOMAIN-REGEX,^x$", body)
class CheckTest(unittest.TestCase):
def test_clean_sources_pass(self):
with data(a="a.com\nb.com\n"):
self.assertEqual(build.check(), 0)
def test_duplicate_is_reported(self):
with data(a="a.com\na.com\n"):
self.assertEqual(build.check(), 1)
def test_subdomain_covered_by_parent_is_reported(self):
with data(a="a.com\ndeep.sub.a.com\n"):
self.assertEqual(build.check(), 1)
def test_real_repository_data_is_clean(self):
self.assertEqual(build.check(), 0)
class BuildTest(unittest.TestCase):
def test_produces_every_format(self):
expected = [
"geosite.dat",
"ipcheck.dat",
"geosite.dat.sha256sum",
"metadata.json",
"txt/b.txt",
"src/b.txt",
"json/b.json",
"sing-box/b.json",
"clash/b.yaml",
"clash/b.list",
"adguard/b.txt",
]
with data(a="a.com\n", b="include:a\nfull:c.com\n"), tempfile.TemporaryDirectory() as out:
out_dir = Path(out) / "dist"
self.assertEqual(build.build(out_dir, None, None), 0)
for rel in expected:
self.assertTrue((out_dir / rel).is_file(), rel)
self.assertEqual(
(out_dir / "geosite.dat").read_bytes(), (out_dir / "ipcheck.dat").read_bytes()
)
meta = json.loads((out_dir / "metadata.json").read_text(encoding="utf-8"))
self.assertEqual(meta["categories"], {"a": 1, "b": 2})
if __name__ == "__main__":
unittest.main()
+6 -8
View File
@@ -1,11 +1,9 @@
# fraud-score — детекторы прокси/VPN и скоринг "качества" IP.
spur.us
scamalytics.com
criminalip.io
fraudguard.io
getipintel.net
iphub.info
ipqualityscore.com
proxycheck.io
scamalytics.com
spur.us
vpnapi.io
iphub.info
getipintel.net
fraudguard.io
criminalip.io
+6 -8
View File
@@ -1,11 +1,9 @@
# geoip-db — провайдеры GeoIP-баз и справочных данных об IP.
ipdata.co
ipbase.com
ipwhois.io
ipgeolocation.io
ipregistry.co
bigdatacloud.net
db-ip.com
maxmind.com
ip2location.com
ipbase.com
ipdata.co
ipgeolocation.io
ipregistry.co
ipwhois.io
maxmind.com
+18
View File
@@ -0,0 +1,18 @@
2ip.io
2ip.ru
icanhazip.com
ifconfig.co
ifconfig.io
ifconfig.me
ip-api.com
ip.sb
ipaddress.is
ipapi.co
ipapi.is
ipify.org
ipinfo.io
myip.com
whatismyip.com
whatismyipaddress.com
whoer.com
whoer.net
+39
View File
@@ -0,0 +1,39 @@
2ip.io
2ip.ru
bigdatacloud.net
browserleaks.com
criminalip.io
db-ip.com
dnsleaktest.com
fraudguard.io
getipintel.net
icanhazip.com
ifconfig.co
ifconfig.io
ifconfig.me
ip-api.com
ip.sb
ip2location.com
ipaddress.is
ipapi.co
ipapi.is
ipbase.com
ipdata.co
ipgeolocation.io
iphub.info
ipify.org
ipinfo.io
ipleak.net
ipqualityscore.com
ipregistry.co
ipwhois.io
maxmind.com
myip.com
proxycheck.io
scamalytics.com
spur.us
vpnapi.io
whatismyip.com
whatismyipaddress.com
whoer.com
whoer.net
+3
View File
@@ -0,0 +1,3 @@
browserleaks.com
dnsleaktest.com
ipleak.net
+2 -2
View File
@@ -6,14 +6,14 @@
# изменился. Молчит, если обновлять нечего, — cron не будет слать письма впустую.
#
# ./update-geosite.sh
# BASE_URL=https://gitea.example.com/mrleo1nid/ipcheck-domains/raw/branch/release \
# BASE_URL=https://gitea.hsrv.site/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}"
BASE_URL="${BASE_URL:-https://gitea.hsrv.site/mrleo1nid/ipcheck-domains/raw/branch/release}"
DEST="${DEST:-/usr/local/x-ui/bin/ipcheck.dat}"
NAME="${NAME:-$(basename "$DEST")}"
RESTART_CMD="${RESTART_CMD:-}"