kit net split: полный список популярных иностранных сервисов, проверка GEOSITE и GEOIP по базе, прощающий разбор файла, проверка прав на файл

This commit is contained in:
maxdmitrov
2026-10-05 21:54:28 +03:00
parent 88f2c1066b
commit a8a4aae6b3
5 changed files with 139 additions and 45 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
* Сайт для маскировки подбирается среди соседей по подсети, а не «у всех один». Свой домен по-прежнему лучший вариант. REALITY теперь с `xtls-rprx-vision`, Hysteria2 отвечает на чужой запрос страницей сайта.
* `kit check --deep` подключается к серверу настоящим клиентом, `--fix` лечит безопасное. Общий лимит трафика считается по всем подключениям пользователя, а в подписке Clash есть «Авто»: клиент сам выбирает быстрейший протокол. DNS в подписках теперь по DoH через прокси: запросы имён не гуляют где попало.
* Ссылка подписки в браузере открывается страницей с трафиком, сроком и ссылками (а не белым экраном). Рядом с подпиской теперь ссылки «открыть в приложении»: одно нажатие, и подписка добавлена с названием. Со своим доменом панель и подписку можно открывать и по домену: `--panel-on domain` при установке или `kit net panel domain` потом. Вернулся `--restore`: чистый сервер поднимается из `kit backup`, как из резервной копии настроек телефона. Ссылки `kit user link --amnezia` и `--telegram` для AmneziaVPN и Telegram теперь одной командой, а исправление времени на Debian 13 прислал [xlok1e](https://github.com/xlok1e), спасибо.
* Раздельная маршрутизация: `kit net split on` – и в приложениях на Mihomo (Clash Verge, FlClash, Mihomo Party) через VPN идёт только список иностранных сервисов (по умолчанию YouTube, ChatGPT и Claude), а всё остальное напрямую. Правила приезжают вместе с подпиской: домашним ничего настраивать не нужно. DNS не течёт: имена из списка у клиента не разрешаются, а запросы про них идут по DoH через VPN. Список в `/etc/kit-sub/rules.yaml`, проверка `kit net split check`, он входит в `kit backup`. Идея и первый набросок – [VasilevVitalii](https://github.com/VasilevVitalii), спасибо. У Happ, Hiddify, v2rayN и sing-box подписка прежняя.
* Раздельная маршрутизация: `kit net split on` – и в приложениях на Mihomo (Clash Verge, FlClash, Mihomo Party) через VPN идёт только список иностранных сервисов (по умолчанию популярные иностранные сервисы: нейросети, видео, соцсети, мессенджеры, GitHub, Google и другие), а всё остальное напрямую. Правила приезжают вместе с подпиской: домашним ничего настраивать не нужно. DNS не течёт: имена из списка у клиента не разрешаются, а запросы про них идут по DoH через VPN. Список в `/etc/kit-sub/rules.yaml`, проверка `kit net split check`, он входит в `kit backup`. Идея и первый набросок – [VasilevVitalii](https://github.com/VasilevVitalii), спасибо. У Happ, Hiddify, v2rayN и sing-box подписка прежняя.
* Исправил `kit backup`: из `/root` он падал с «No such file or directory», работал только из `/`. Теперь работает откуда угодно. Если панель не принимает запрос, `kit` говорит по-человечески (код ответа и вероятная причина), а не «Unsupported HTTP version in response».
Сервер обновится сам этой ночью, нетерпеливым – `kit update`.
+2 -2
View File
@@ -180,7 +180,7 @@ kit net on reality2 # создать второй REALITY на высо
kit net fp firefox # сменить отпечаток клиента у всех подключений
kit net masq on # Hysteria2 отвечает на чужой запрос страницей сайта
kit net panel domain # панель и подписка открываются и по домену (только со своим доменом); kit net panel ip – как раньше
kit net split on # через VPN только список (по умолчанию YouTube, ChatGPT и Claude), остальное напрямую; kit net split check | off
kit net split on # через VPN только список (по умолчанию популярные иностранные сервисы), остальное напрямую; kit net split check | off
```
Со своим доменом панель и подписка по умолчанию открываются по IP (`https://IP/путь/`). Если нужен адрес с доменом (например, для Telegram-бота панели
@@ -189,7 +189,7 @@ kit net split on # через VPN только список (по ум
### Раздельная маршрутизация (`kit net split`)
Если нужно, чтобы через VPN шли только выбранные сервисы, а всё остальное напрямую, включите: `kit net split on`. На сервере появится
файл `/etc/kit-sub/rules.yaml` со списком иностранных сервисов (по умолчанию YouTube, ChatGPT и Claude; остальные – в комментариях, раскомментируйте нужные). Российские сайты в список не добавляйте: всё, чего в нём нет, и так идёт напрямую. Правила приезжают вместе с подпиской, на телефонах и компьютерах
файл `/etc/kit-sub/rules.yaml` со списком иностранных сервисов (по умолчанию популярные: нейросети, видео и музыка, соцсети и мессенджеры, GitHub, Google и другие, всего около 50 категорий; тяжёлые и региональные – в комментариях, раскомментируйте нужные). Российские сайты в список не добавляйте: всё, чего в нём нет, и так идёт напрямую. Правила приезжают вместе с подпиской, на телефонах и компьютерах
ничего настраивать не нужно: достаточно обновить подписку в приложении. Работает в приложениях на Mihomo (Clash Verge, FlClash, Mihomo Party);
в Happ, Hiddify, v2rayN и sing-box подписка остаётся прежней, весь трафик идёт через VPN.
+32 -21
View File
@@ -16,6 +16,7 @@ https://github.com/itsnotkubrick/3X-UI_KIT
import base64
import http.client
import http.server
import ipaddress
import json
import os
import re
@@ -218,7 +219,7 @@ def load_rules(report=None):
say(f"правила: файл больше {RULES_MAX_BYTES // 1024} КБ – не применяю")
return None
try:
data = yaml.safe_load(raw.decode("utf-8")) or {}
data = yaml.safe_load(raw.decode("utf-8-sig")) or {}
except (yaml.YAMLError, UnicodeError) as e:
say(f"правила: файл не разобран ({e}) – не применяю")
return None
@@ -229,12 +230,27 @@ def load_rules(report=None):
rules, seen = [], set()
for item in items[:RULES_MAX_COUNT + 50]:
r = re.sub(r"\s*,\s*", ",", str(item).strip())
kind, _, value = r.partition(",")
r = kind.upper() + "," + value # «domain-suffix,…» тоже понимаем
m = RULE_RE.match(r)
if not m:
say(f"правила: пропускаю непонятное правило {str(item)[:80]!r}")
elif r not in seen and len(rules) < RULES_MAX_COUNT:
seen.add(r)
rules.append((m.group(1), m.group(2)))
continue
kind, value = m.group(1), m.group(2)
if kind in ("IP-CIDR", "IP-CIDR6"):
try:
net = ipaddress.ip_network(value, strict=False)
except ValueError:
net = None
if net is None or net.version != (6 if kind == "IP-CIDR6" else 4):
say(f"правила: неверная подсеть {value!r} – пропускаю")
continue
if kind == "DOMAIN-KEYWORD" and len(value) < 4:
say(f"правила: слово {value!r} слишком короткое (под него попадёт почти всё) – пропускаю")
continue
if (kind, value) not in seen and len(rules) < RULES_MAX_COUNT:
seen.add((kind, value))
rules.append((kind, value))
if not rules:
say("правила: ни одного пригодного – не применяю")
return None
@@ -399,26 +415,21 @@ class Server(http.server.ThreadingHTTPServer):
address_family = socket.AF_INET6 if ":" in CONF.get("listen", "") else socket.AF_INET
GEOSITE_URL = "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
GEO_URL = "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/{}.dat"
def unknown_geosites(names):
"""Сверяет категории GEOSITE с базой, которую скачают приложения: неизвестное имя ломает запуск конфига у клиента.
Нет доступа к GitHub – не проверяем (None-результат пустой)."""
def unknown_geo(kind, names):
"""Сверяет категории GEOSITE/GEOIP с базой, которую скачают приложения: неизвестное имя ломает запуск конфига у клиента.
Нет доступа к GitHub – не проверяем."""
if not names:
return []
try:
with urllib.request.urlopen(GEOSITE_URL, timeout=30) as r:
data = r.read(32 * 1024 * 1024)
except (urllib.error.URLError, OSError):
print("Категории GEOSITE не проверил: база недоступна. Названия сверьте по github.com/MetaCubeX/meta-rules-dat (geo/geosite).")
with urllib.request.urlopen(GEO_URL.format(kind), timeout=30) as r:
data = r.read(48 * 1024 * 1024)
except (urllib.error.URLError, OSError) as e:
print(f"Категории {kind.upper()} не проверил: база недоступна ({e}). Названия сверьте по github.com/MetaCubeX/meta-rules-dat.")
return []
bad = []
for n in names:
b = n.upper().encode()
if b"\x0a" + bytes([len(b)]) + b not in data:
bad.append(n)
return bad
return [n for n in names if b"\x0a" + bytes([len(n)]) + n.upper().encode() not in data]
def main():
@@ -427,9 +438,9 @@ def main():
loaded = load_rules(report=print)
if loaded:
print(f"Правил через VPN: {len(loaded[0])}; DNS для остального: {', '.join(loaded[1]) or 'системный'}")
unknown = unknown_geosites([v for k, v in loaded[0] if k == "GEOSITE"])
if unknown:
print("Нет такой категории GEOSITE: " + ", ".join(unknown) + ". Приложение не сможет запустить конфиг – исправьте или удалите эти строки.")
bad = [f"{k} {n}" for k in ("geosite", "geoip") for n in unknown_geo(k, [v for kk, v in loaded[0] if kk == k.upper()])]
if bad:
print("Нет такой категории: " + ", ".join(bad) + ". Приложение не сможет запустить конфиг – исправьте или удалите эти строки.")
raise SystemExit(2)
raise SystemExit(0 if loaded else 1)
cert, key = CONF.get("cert"), CONF.get("key")
+79 -21
View File
@@ -1746,29 +1746,77 @@ split_template() {
# DOMAIN,api.example.org – ровно этот домен
# DOMAIN-KEYWORD,example – домен, в котором есть слово
# IP-CIDR,203.0.113.0/24 – подсеть
# GEOSITE: приложение один раз скачивает базу категорий с GitHub.
# GEOSITE и GEOIP: приложение один раз скачивает базу категорий с GitHub.
via_vpn:
- GEOSITE,youtube
# Нейросети
- GEOSITE,category-ai-!cn # ChatGPT, Claude, Gemini, Perplexity и другие
- GEOSITE,openai
- GEOSITE,anthropic
# Раскомментируйте то, чем пользуетесь (уберите «# » в начале строки):
# - GEOSITE,telegram
# - GEOSITE,meta # Instagram, Facebook, WhatsApp
# - GEOSITE,twitter # X (Twitter)
# - GEOSITE,discord
# - GEOSITE,github
# - GEOSITE,linkedin
# - GEOSITE,netflix
# - GEOSITE,spotify
# - GEOSITE,tiktok
# - GEOSITE,reddit
# - GEOSITE,twitch
# - GEOSITE,notion
# - GEOSITE,figma
# - GEOSITE,zoom
# - GEOSITE,google # все сервисы Google
# - GEOSITE,perplexity
- GEOSITE,perplexity
- GEOSITE,huggingface
# Видео и музыка
- GEOSITE,youtube
- GEOSITE,netflix
- GEOSITE,spotify
- GEOSITE,twitch
- GEOSITE,disney
- GEOSITE,hbo
- GEOSITE,primevideo
- GEOSITE,soundcloud
- GEOSITE,vimeo
- GEOSITE,tiktok
# Соцсети и мессенджеры
- GEOSITE,telegram
- GEOIP,telegram # Telegram подключается по IP-адресам, без имён
- GEOSITE,meta # Instagram, Facebook, WhatsApp
- GEOSITE,twitter # X (Twitter)
- GEOSITE,discord
- GEOSITE,reddit
- GEOSITE,linkedin
- GEOSITE,pinterest
- GEOSITE,tumblr
- GEOSITE,quora
- GEOSITE,medium
- GEOSITE,signal
- GEOSITE,viber
# Работа и разработка
- GEOSITE,google # все сервисы Google
- GEOSITE,github
- GEOSITE,gitlab
- GEOSITE,docker
- GEOSITE,npmjs
- GEOSITE,jetbrains
- GEOSITE,atlassian
- GEOSITE,slack
- GEOSITE,notion
- GEOSITE,figma
- GEOSITE,canva
- GEOSITE,adobe
- GEOSITE,zoom
- GEOSITE,trello
- GEOSITE,dropbox
- GEOSITE,protonmail
- GEOSITE,mega
- GEOSITE,imgur
- GEOSITE,duckduckgo
- GEOSITE,wikimedia
# Иностранные СМИ
- GEOSITE,bbc
- GEOSITE,cnn
- GEOSITE,nytimes
- GEOSITE,reuters
- GEOSITE,bloomberg
# Раскомментируйте, если нужно (уберите «# » в начале строки). Это тяжёлый трафик или сервисы с привязкой к региону:
# - GEOSITE,microsoft
# - GEOSITE,apple
# - GEOSITE,steam
# - GEOSITE,epicgames
# - GEOSITE,playstation
# - GEOSITE,xbox
# - GEOSITE,nintendo
# - GEOSITE,cloudflare
# - GEOSITE,paypal
# - DOMAIN-SUFFIX,example.org
# Необязательно: DNS для всего остального (по умолчанию – системный DNS приложения). Только адреса https://…
@@ -1777,12 +1825,22 @@ via_vpn:
YAML
}
# kit-sub работает без root и читает файл сам: если права закрыты (после правки редактором, копирования), список молча не применится.
split_perms() {
[[ -f $SPLIT_FILE ]] || return 0
if ! [[ $(stat -c %a "$SPLIT_FILE") =~ ^[0-7][0-7][4-7]$ && $(stat -c %a /etc/kit-sub) =~ ^[0-7][0-7][1357]$ ]]; then
chmod 644 "$SPLIT_FILE"; chmod 755 /etc/kit-sub
warn "Права на $SPLIT_FILE были закрыты для подписки (kit-sub работает не от root) – исправил."
fi
}
net_split() { # [on|off|check]
local act=${1:-} n
split_perms
case $act in
"")
if [[ -f $SPLIT_FILE ]]; then
n=$(python3 /usr/local/lib/kit-sub/kit_sub.py --check-rules 2>&1 | tail -1 || true)
n=$(python3 /usr/local/lib/kit-sub/kit_sub.py --check-rules 2>&1 | grep -a '^Правил через VPN' || true)
echo "Раздельная маршрутизация включена: $n"
echo "Файл: $SPLIT_FILE (править, проверить: kit net split check, выключить: kit net split off)"
echo "Работает в приложениях на Mihomo; Xray и sing-box приложения получают подписку как раньше."
@@ -1795,7 +1853,7 @@ net_split() { # [on|off|check]
if [[ -f $SPLIT_FILE.off ]]; then install -m 644 "$SPLIT_FILE.off" "$SPLIT_FILE"; else split_template >"$SPLIT_FILE"; fi
chmod 755 /etc/kit-sub; chmod 644 "$SPLIT_FILE"
say "Включил: $SPLIT_FILE"
echo "Сейчас через VPN идут только перечисленные сервисы (YouTube, ChatGPT, Claude), остальное напрямую."
echo "Сейчас через VPN идут только перечисленные сервисы (популярные иностранные сервисы: YouTube, ChatGPT, Telegram и другие), остальное напрямую."
echo "Список правится так: nano $SPLIT_FILE, затем kit net split check."
echo "Приложения на Mihomo подхватят при обновлении подписки." ;;
off)
+25
View File
@@ -86,6 +86,31 @@ class Rules(unittest.TestCase):
loaded = ks.load_rules(report=lambda m: None)
self.assertEqual(ks.apply_rules(b"proxies: []\n", loaded), b"proxies: []\n")
def test_forgiving_input(self):
# Что если: строчные типы, BOM и CRLF из Windows-редактора, лишние пробелы
with open(ks.RULES_FILE, "wb") as f:
f.write("\ufeffvia_vpn:\r\n - geosite , youtube\r\n - domain-suffix,Example.ORG\r\n".encode("utf-8"))
rules, _ = ks.load_rules(report=lambda m: None)
self.assertEqual(rules, [("GEOSITE", "youtube"), ("DOMAIN-SUFFIX", "Example.ORG")])
def test_bad_cidr_and_short_keyword(self):
write("via_vpn:\n - IP-CIDR,999.1.1.1/8\n - IP-CIDR,1.2.3.4/99\n - IP-CIDR6,1.2.3.0/24\n - IP-CIDR,2001:db8::/32\n"
" - IP-CIDR,203.0.113.0/24\n - IP-CIDR6,2001:db8::/32\n - DOMAIN-KEYWORD,a\n - DOMAIN-KEYWORD,video\n")
rules, _ = ks.load_rules(report=lambda m: None)
self.assertEqual(rules, [("IP-CIDR", "203.0.113.0/24"), ("IP-CIDR6", "2001:db8::/32"), ("DOMAIN-KEYWORD", "video")])
def test_template_from_kit_sh_is_valid(self):
# Что если: шаблон kit net split on сам окажется с ошибкой
import re
t = open(os.path.join(root, "scripts", "kit.sh"), encoding="utf-8").read()
body = t[t.index("split_template() {"):]
body = body[body.index("<<'YAML'\n") + 9: body.index("\nYAML\n")]
write(body)
rules, _ = ks.load_rules(report=lambda m: self.fail(m))
self.assertGreater(len(rules), 40)
self.assertIn(("GEOIP", "telegram"), rules)
self.assertNotIn(("DOMAIN-SUFFIX", "example.org"), rules) # пример закомментирован
if __name__ == "__main__":
unittest.main(verbosity=1)