kit net split: маршрутизация для Happ (заголовок Routing) и JSON-подписки Xray через настройки панели, kit net split apply; проверено клиентами Mihomo и Xray

This commit is contained in:
maxdmitrov
2026-10-06 11:16:20 +03:00
parent d74ef02d65
commit 73bbaadd48
5 changed files with 117 additions and 8 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 идёт только список иностранных сервисов (по умолчанию популярные иностранные сервисы: нейросети, видео, соцсети, мессенджеры, GitHub, Google и другие), а всё остальное напрямую. Правила приезжают вместе с подпиской: домашним ничего настраивать не нужно. DNS не течёт: имена из списка у клиента не разрешаются, а запросы про них идут по DoH через VPN. Список в `/etc/kit-sub/rules.yaml`, проверка `kit net split check`, он входит в `kit backup`. Идея и первый набросок – [VasilevVitalii](https://github.com/VasilevVitalii), спасибо. У Karing, Happ, Hiddify, v2rayN и других на sing-box или Xray подписка прежняя.
* Раздельная маршрутизация: `kit net split on` – и через VPN идёт только список популярных иностранских сервисов (нейросети, видео, соцсети, мессенджеры, GitHub, Google и другие), а всё остальное напрямую. Правила приезжают вместе с подпиской, домашним ничего настраивать не нужно: приложения на Mihomo (Clash Verge, FlClash, Mihomo Party) берут их из подписки, Happ – заголовком, приложения на JSON-подписке Xray – из конфига. DNS не течёт: имена из списка у клиента не разрешаются, а запросы про них идут по DoH через VPN (проверено настоящими клиентами Mihomo и Xray счётчиком пакетов). Список в `/etc/kit-sub/rules.yaml`, проверка по базам категорий `kit net split check`, после правки `kit net split apply`, он входит в `kit backup`. Идея и первый набросок – [VasilevVitalii](https://github.com/VasilevVitalii), спасибо. У v2rayN, Karing, Hiddify и приложений с обычным списком ссылок подписка прежняя.
* Исправил `kit backup`: из `/root` он падал с «No such file or directory», работал только из `/`. Теперь работает откуда угодно. Если панель не принимает запрос, `kit` говорит по-человечески (код ответа и вероятная причина), а не «Unsupported HTTP version in response».
Сервер обновится сам этой ночью, нетерпеливым – `kit update`.
+10 -3
View File
@@ -189,14 +189,21 @@ kit net split on # через VPN только список (по ум
### Раздельная маршрутизация (`kit net split`)
Если нужно, чтобы через VPN шли только выбранные сервисы, а всё остальное напрямую, включите: `kit net split on`. На сервере появится
файл `/etc/kit-sub/rules.yaml` со списком иностранных сервисов (по умолчанию популярные: нейросети, видео и музыка, соцсети и мессенджеры, GitHub, Google и другие, всего около 50 категорий; тяжёлые и региональные – в комментариях, раскомментируйте нужные). Российские сайты в список не добавляйте: всё, чего в нём нет, и так идёт напрямую. Правила приезжают вместе с подпиской, на телефонах и компьютерах
ничего настраивать не нужно: достаточно обновить подписку в приложении. Работает в приложениях на Mihomo (Clash Verge, FlClash, Mihomo Party);
в Karing, Happ, Hiddify, v2rayN и других на ядре sing-box или Xray подписка остаётся прежней, весь трафик идёт через VPN (там раздельную маршрутизацию настраивают в самом приложении).
файл `/etc/kit-sub/rules.yaml` со списком иностранных сервисов (по умолчанию популярные: нейросети, видео и музыка, соцсети и мессенджеры, GitHub, Google и другие, всего около 50 категорий; тяжёлые и региональные – в комментариях, раскомментируйте нужные). Российские сайты в список не добавляйте: всё, чего в нём нет, и так идёт напрямую.
Правила приезжают вместе с подпиской, на телефонах и компьютерах ничего настраивать не нужно: достаточно обновить подписку в приложении.
| Приложение | Как получает правила |
|---|---|
| Clash Verge, FlClash, Mihomo Party (ядро Mihomo) | из подписки, сразу (режим «Rule», не «Global») |
| Happ | заголовком подписки (профиль «3X-UI KIT» включается сам) |
| Приложения, которые берут JSON-подписку Xray (адрес «JSON» со страницы подписки) | из конфига подписки |
| v2rayN, v2rayNG и другие с обычным списком ссылок, Karing, Hiddify, sing-box | не получают: подписка прежняя, весь трафик через VPN; раздельную маршрутизацию там настраивают в самом приложении |
```
kit net split on # создать список и включить
nano /etc/kit-sub/rules.yaml
kit net split check # проверить файл: сколько правил применится и что отброшено
kit net split apply # после правки списка: обновить профили Happ и Xray в панели (Mihomo берёт файл сам)
kit net split off # выключить (список сохранится в rules.yaml.off)
```
+40
View File
@@ -417,6 +417,39 @@ class Server(http.server.ThreadingHTTPServer):
address_family = socket.AF_INET6 if ":" in CONF.get("listen", "") else socket.AF_INET
def routing_payload(kind, loaded):
"""Профиль маршрутизации для самой панели 3X-UI (она раздаёт его приложениям на Xray): kind = happ | json.
happ – заголовок Routing для Happ: через VPN только список, GlobalProxy=false.
json – правила для JSON-подписки Xray (серверы DoH 1.1.1.1 и 8.8.8.8 – тоже через VPN); конструктор панели всегда ставит в конец «всё через прокси», поэтому остальное
отправляем напрямую раньше (порядок block-proxy-direct, прямые regexp:.* и 0.0.0.0/0)."""
rules, direct_dns = loaded
sites, ips = [], []
for k, v in rules:
if k == "GEOSITE":
sites.append("geosite:" + v)
elif k == "DOMAIN-SUFFIX":
sites.append("domain:" + v)
elif k == "DOMAIN":
sites.append("full:" + v)
elif k == "DOMAIN-KEYWORD":
sites.append(v)
elif k == "GEOIP":
ips.append("geoip:" + v)
else:
ips.append(v)
if kind == "happ":
data = {"Name": "3X-UI KIT", "GlobalProxy": "false", "DomainStrategy": "IPIfNonMatch", "ProxySites": sites,
"ProxyIp": ips, "DirectIp": ["geoip:private"]}
else:
# localhost – системный DNS клиента для прямых сайтов (как в подписке Mihomo); список direct_dns его заменяет.
data = {"Name": "3X-UI KIT", "DomainStrategy": "IPIfNonMatch", "RouteOrder": "block-proxy-direct",
"ProxySites": sites, "ProxyIp": ["1.1.1.1", "8.8.8.8"] + ips, "DirectSites": ["regexp:.*"],
"DirectIp": ["geoip:private", "0.0.0.0/0", "::/0"], "DomesticDNSDomain": direct_dns[0] if direct_dns else "localhost"}
text = json.dumps(data, ensure_ascii=False, separators=(",", ":"))
# Happ ждёт в заголовке ссылку happ://routing/onadd/<base64>; панели для JSON-подписки нужен обычный JSON.
return "happ://routing/onadd/" + base64.b64encode(text.encode()).decode() if kind == "happ" else text
GEO_URL = "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/{}.dat"
@@ -435,6 +468,13 @@ def unknown_geo(kind, names):
def main():
if "--routing-payload" in sys.argv:
# kit net split: профиль для панели (для Happ и Xray); пусто и код 1, если правил нет.
loaded = load_rules(report=lambda m: print(m, file=sys.stderr))
if not loaded:
raise SystemExit(1)
print(routing_payload(sys.argv[sys.argv.index("--routing-payload") + 1], loaded))
raise SystemExit(0)
if "--check-rules" in sys.argv:
# kit net split check: тот же разбор, что и при выдаче подписки, но с отчётом на экран.
loaded = load_rules(report=print)
+39 -4
View File
@@ -1825,6 +1825,35 @@ via_vpn:
YAML
}
# Приложениям на Xray правила раздаёт сама панель (3X-UI 3.9+): заголовок Routing для Happ и правила JSON-подписки.
# Собираем их из того же rules.yaml. Чужой профиль, уже стоящий в панели, не трогаем.
split_panel_ours() { # наш профиль: JSON или ссылка happ://routing/onadd/<base64> с Name "3X-UI KIT"
local v=$1
[[ $v == happ://routing/onadd/* ]] && v=$(printf '%s' "${v#happ://routing/onadd/}" | base64 -d 2>/dev/null || true)
grep -q '"Name":"3X-UI KIT"' <<<"$v"
}
split_panel_sync() { # on|off
local all cur_h cur_j h j out
all=$(api POST setting/all '{}') || return 1
cur_h=$(jq -r '.subRoutingRules // ""' <<<"$all"); cur_j=$(jq -r '.subJsonRoutingRules // ""' <<<"$all")
if [[ $1 == on ]]; then
if { [[ -n $cur_h ]] && ! split_panel_ours "$cur_h"; } || { [[ -n $cur_j ]] && ! split_panel_ours "$cur_j"; }; then
warn "В панели уже настроена своя маршрутизация подписки (Happ или JSON) – не меняю. Приложения на Mihomo получат список как обычно."
return 0
fi
h=$(python3 /usr/local/lib/kit-sub/kit_sub.py --routing-payload happ 2>/dev/null) && j=$(python3 /usr/local/lib/kit-sub/kit_sub.py --routing-payload json 2>/dev/null) \
|| { warn "Список не разобран – для Happ и Xray ничего не менял (kit net split check)."; return 1; }
out=$(jq -c --arg h "$h" --arg j "$j" '.subEnableRouting = true | .subRoutingRules = $h | .subJsonRoutingRules = $j' <<<"$all")
else
[[ -n $cur_h || -n $cur_j ]] || return 0
if { [[ -n $cur_h ]] && ! split_panel_ours "$cur_h"; } || { [[ -n $cur_j ]] && ! split_panel_ours "$cur_j"; }; then return 0; fi
out=$(jq -c '.subEnableRouting = false | .subRoutingRules = "" | .subJsonRoutingRules = ""' <<<"$all")
fi
api POST setting/update "$out" >/dev/null || { warn "Панель не приняла маршрутизацию для Happ и Xray."; return 1; }
systemctl restart x-ui; panel_healthy || warn "Панель не ответила после перезапуска: проверьте kit check."
}
# kit-sub работает без root и читает файл сам: если права закрыты (после правки редактором, копирования), список молча не применится.
split_perms() {
[[ -f $SPLIT_FILE ]] || return 0
@@ -1843,7 +1872,7 @@ net_split() { # [on|off|check]
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; Karing, Hiddify, Happ и другие получают подписку как раньше."
echo "Mihomo-приложения берут список из подписки сами; Happ и JSON-подписка Xray – из панели (после правки файла: kit net split apply)."
else
echo "Раздельная маршрутизация выключена: через VPN идёт весь трафик. Включить: kit net split on"
fi ;;
@@ -1853,12 +1882,14 @@ 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"
split_panel_sync on || true
echo "Сейчас через VPN идут только перечисленные сервисы (популярные иностранные сервисы: YouTube, ChatGPT, Telegram и другие), остальное напрямую."
echo "Список правится так: nano $SPLIT_FILE, затем kit net split check."
echo "Приложения на Mihomo подхватят при обновлении подписки." ;;
echo "Приложения на Mihomo подхватят при обновлении подписки; после правки списка для Happ и Xray: kit net split apply." ;;
off)
[[ -f $SPLIT_FILE ]] || { say "Уже выключено."; return 0; }
mv "$SPLIT_FILE" "$SPLIT_FILE.off"
split_panel_sync off || true
say "Выключил: подписка снова отдаёт весь трафик через VPN (список сохранён: $SPLIT_FILE.off)." ;;
check)
[[ -f $SPLIT_FILE ]] || die "Файла нет: $SPLIT_FILE (включить: kit net split on)"
@@ -1867,7 +1898,11 @@ net_split() { # [on|off|check]
2) die "Исправьте список: с неизвестной категорией приложения на Mihomo не смогут запустить конфиг." ;;
*) die "Список не применится – подписка останется без раздельной маршрутизации." ;;
esac ;;
*) die "kit net split [on|off|check]" ;;
apply)
[[ -f $SPLIT_FILE ]] || die "Файла нет: $SPLIT_FILE (включить: kit net split on)"
python3 /usr/local/lib/kit-sub/kit_sub.py --check-rules >/dev/null 2>&1 || die "Список не применится: сначала kit net split check."
split_panel_sync on && say "Списки для Happ и Xray в панели обновлены (приложения на Mihomo подхватывают файл сами)." ;;
*) die "kit net split [on|off|check|apply]" ;;
esac
}
@@ -2224,7 +2259,7 @@ cmd_net() {
panel) net_panel "$@" ;;
split) net_split "$@" ;;
vision) case ${1:-} in on) cmd_vision --all ;; off) cmd_vision --all off ;; *) die "kit net vision on|off" ;; esac ;;
*) die "kit net [site | port имя порт | off имя | on имя | fp отпечаток | masq on|off | dns on|off | panel domain|ip | split on|off|check | vision on|off]" ;;
*) die "kit net [site | port имя порт | off имя | on имя | fp отпечаток | masq on|off | dns on|off | panel domain|ip | split on|off|check|apply | vision on|off]" ;;
esac
}
+27
View File
@@ -121,6 +121,33 @@ class Rules(unittest.TestCase):
for ua in ("Karing/1.2.5", "Karing/1.2.5 clash.meta", "Hiddify/2.0 clash", "sing-box/1.9", "Happ/1.0", "Stash/2.7", "v2rayN/6.0", "Mozilla/5.0"):
self.assertFalse(gets_rules(ua), ua)
def test_routing_payloads_for_panel(self):
import base64
write("via_vpn:\n - GEOSITE,youtube\n - DOMAIN-SUFFIX,example.org\n - DOMAIN,api.example.com\n - GEOIP,telegram\n - IP-CIDR,203.0.113.0/24\n")
loaded = ks.load_rules(report=lambda m: None)
link = ks.routing_payload("happ", loaded)
self.assertTrue(link.startswith("happ://routing/onadd/"))
h = json.loads(base64.b64decode(link.split("/onadd/")[1]))
self.assertEqual(h["Name"], "3X-UI KIT")
self.assertEqual(h["GlobalProxy"], "false")
self.assertEqual(h["ProxySites"], ["geosite:youtube", "domain:example.org", "full:api.example.com"])
self.assertEqual(h["ProxyIp"], ["geoip:telegram", "203.0.113.0/24"])
j = json.loads(ks.routing_payload("json", loaded))
self.assertEqual(j["RouteOrder"], "block-proxy-direct")
self.assertEqual(j["DirectSites"], ["regexp:.*"]) # остальное напрямую: панель сама добавляет «всё через прокси» в конец
self.assertIn("0.0.0.0/0", j["DirectIp"])
self.assertEqual(j["ProxyIp"][:2], ["1.1.1.1", "8.8.8.8"])
self.assertEqual(j["DomesticDNSDomain"], "localhost")
self.assertLess(len(link), 8192) # предел панели на значение заголовка Routing
def test_full_default_template_fits_panel_limit(self):
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)
loaded = ks.load_rules(report=lambda m: self.fail(m))
self.assertLess(len(ks.routing_payload("happ", loaded)), 8192)
if __name__ == "__main__":
unittest.main(verbosity=1)