version 1.4.1: per-user WARP флаг, fallback-установка PyPI

- app.py: full_warp флаг в warp_rules, _rebuild_egress читает с диска, race condition fix
- bot.py: full_warp поддержка в toggle_warp
- templates/index.html: чекбокс «Весь трафик через WARP» с full_warp, не авто-включается
- install-panel.sh: зеркала PyPI (aliyun), таймаут 120с на pip, fallback на apt --system-site-packages
- install-bot.sh: зеркала и fallback для python-telegram-bot
- README: версия 1.4, обновлён roadmap
This commit is contained in:
2026-06-28 23:59:47 +03:00
parent 43d99ba867
commit 423ee4fdbb
6 changed files with 189 additions and 166 deletions
+59 -137
View File
@@ -296,7 +296,6 @@ def build_singbox_config(name, password):
proto = bindings[0].get("protocol", "TCP").upper() if bindings else "TCP"
port_range = bindings[0].get("portRange",
str(bindings[0].get("port", "2100"))) if bindings else "2100"
# Берём первый порт из диапазона для sing-box (он не поддерживает диапазоны)
first_port = int(port_range.split("-")[0]) if "-" in port_range else int(port_range)
server_ip = get_server_ip()
@@ -554,15 +553,11 @@ def api_users_delete():
@app.route(f"{BASE}/api/users/warp", methods=["POST"])
@login_required
def api_users_warp():
"""Включить/выключить WARP для конкретного пользователя через egress.users."""
"""Включить/выключить WARP для конкретного пользователя. При включении весь трафик идёт через WARP."""
data = request.get_json(silent=True) or {}
name = data.get("name", "")
enabled = bool(data.get("enabled", False))
cfg = load_mita_config()
# egress.userGroups не поддерживается в mita — используем workaround:
# храним список warp-пользователей в panel.json,
# добавляем их домены в egress (все остальные — DIRECT).
pc = load_panel_config()
warp_users = set(pc.get("warp_users", []))
if enabled:
@@ -572,10 +567,8 @@ def api_users_warp():
pc["warp_users"] = list(warp_users)
Path(PANEL_CONFIG).write_text(json.dumps(pc, indent=2))
# Примечание: mita не поддерживает per-user egress нативно.
# Флаг хранится в panel.json и отображается в UI как справочная информация.
# Реальное разделение — через отдельные порты (см. README).
return jsonify({"ok": True, "note": "stored_in_panel_config"})
_rebuild_egress()
return jsonify({"ok": True})
@app.route(f"{BASE}/api/users/warp_status")
@login_required
@@ -861,7 +854,8 @@ def api_warp_rules_get():
rules = pc.get("warp_rules", {}).get(name, {
"domains": [],
"ips": [],
"sources": [], # URL или geosite:/geoip: ссылки
"sources": [],
"full_warp": False,
})
return jsonify({"ok": True, "name": name, "rules": rules})
@@ -869,159 +863,87 @@ def api_warp_rules_get():
@login_required
def api_warp_rules_set():
data = request.get_json(silent=True) or {}
name = data.get("name", "")
domains = data.get("domains", []) # список строк
ips = data.get("ips", [])
sources = data.get("sources", []) # geosite:xxx / geoip:xxx / URL
name = data.get("name", "")
domains = data.get("domains", [])
ips = data.get("ips", [])
sources = data.get("sources", [])
full_warp = data.get("full_warp", False)
if not name:
return jsonify({"ok": False, "error": "Не указано имя"}), 400
pc = load_panel_config()
pc.setdefault("warp_rules", {})[name] = {
"domains": [d.strip() for d in domains if d.strip()],
"ips": [i.strip() for i in ips if i.strip()],
"sources": [s.strip() for s in sources if s.strip()],
"domains": [d.strip() for d in domains if d.strip()],
"ips": [i.strip() for i in ips if i.strip()],
"sources": [s.strip() for s in sources if s.strip()],
"full_warp": bool(full_warp),
}
Path(PANEL_CONFIG).write_text(json.dumps(pc, indent=2))
# Перегенерировать egress в mita config на основе всех пользователей
_rebuild_egress(pc)
_rebuild_egress()
return jsonify({"ok": True})
def _get_geosite_data():
def _rebuild_egress(pc=None):
"""
Скачивает (с кэшированием на 7 дней) единый YAML файл со всеми
geosite-категориями и возвращает распарсенный dict.
Старый способ (отдельный .txt на каждую категорию на ветке release)
больше не поддерживается проектом v2fly — теперь только единый
dlc.dat_plain.yml в latest release.
Глобальный WARP-egress:
- full_warp=True у любого пользователя → весь трафик через WARP
- full_warp=False + есть правила → указанные домены/IP через WARP, остальное DIRECT
- WARP выкл у всех → egress удаляется
"""
import urllib.request, os, time, yaml
cache_path = "/var/cache/mita-geosite.yml"
url = "https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat_plain.yml"
need_download = True
if os.path.exists(cache_path):
age_days = (time.time() - os.path.getmtime(cache_path)) / 86400
if age_days < 7:
need_download = False
if need_download:
try:
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
req = urllib.request.Request(url, headers={"User-Agent": "mita-panel"})
with urllib.request.urlopen(req, timeout=60) as resp:
data = resp.read()
with open(cache_path, "wb") as f:
f.write(data)
except Exception:
pass # используем старый кэш, если есть
if not os.path.exists(cache_path):
return None
try:
with open(cache_path) as f:
return yaml.safe_load(f)
except Exception:
return None
def _geosite_category_domains(category):
"""Возвращает список доменов для одной geosite-категории."""
data = _get_geosite_data()
if not data:
return []
domains = []
cat_lower = category.lower()
for entry in data.get("lists", []):
if entry.get("name", "").lower() == cat_lower:
for rule in entry.get("rules", []):
for prefix in ("domain:", "full:"):
if rule.startswith(prefix):
domains.append(rule[len(prefix):])
break
# regexp: и include: пропускаем — не прямые доменные правила
break
return domains
def _rebuild_egress(pc):
"""
Собирает egress.rules из warp_rules всех пользователей и записывает в mita config.
mita не поддерживает per-user egress — правила глобальные, но мы объединяем
домены/IP всех пользователей у которых WARP включён.
Источники (geosite:/geoip:/URL) разворачиваем в реальные списки.
"""
import urllib.request
pc = load_panel_config()
warp_users = set(pc.get("warp_users", []))
if not warp_users:
cfg = load_mita_config()
cfg.pop("egress", None)
save_mita_config(cfg)
return
warp_proxy = {
"name": "warp",
"protocol": "SOCKS5_PROXY_PROTOCOL",
"host": "127.0.0.1",
"port": WARP_PORT,
}
all_domains = set()
all_ips = set()
has_full = False
GEOIP_BASE = "https://raw.githubusercontent.com/herrbischoff/country-ip-blocks/master/ipv4"
for uname, rules in pc.get("warp_rules", {}).items():
if uname not in warp_users:
continue # пользователь не включён в WARP — пропускаем
all_domains.update(rules.get("domains", []))
all_ips.update(rules.get("ips", []))
for src in rules.get("sources", []):
src = src.strip()
try:
if src.startswith("geosite:"):
cat = src[len("geosite:"):]
all_domains.update(_geosite_category_domains(cat))
elif src.startswith("geoip:"):
country = src[len("geoip:"):]
url = f"{GEOIP_BASE}/{country}.cidr"
lines = urllib.request.urlopen(url, timeout=10).read().decode().splitlines()
for line in lines:
line = line.strip()
if line and not line.startswith("#"):
all_ips.add(line)
elif src.startswith("http"):
lines = urllib.request.urlopen(src, timeout=10).read().decode().splitlines()
for line in lines:
line = line.strip()
if line and not line.startswith("#"):
# Определяем домен или IP по наличию /
if "/" in line or line.replace(".","").isdigit():
all_ips.add(line)
else:
all_domains.add(line)
except Exception:
pass # не критично — продолжаем без этого источника
for uname in warp_users:
rules = pc.get("warp_rules", {}).get(uname, {})
if rules.get("full_warp"):
has_full = True
all_domains.update(d for d in rules.get("domains", []) if d)
all_ips.update(i for i in rules.get("ips", []) if i)
cfg = load_mita_config()
if not all_domains and not all_ips:
# Нет правил — убираем egress полностью
cfg.pop("egress", None)
else:
domain_list = sorted(all_domains)
ip_list = sorted(all_ips)
warp_rule = {"action": "PROXY", "proxyNames": ["warp"]}
if domain_list:
warp_rule["domainNames"] = domain_list
if ip_list:
warp_rule["ipRanges"] = ip_list
if has_full:
cfg["egress"] = {
"proxies": [{
"name": "warp",
"protocol": "SOCKS5_PROXY_PROTOCOL",
"host": "127.0.0.1",
"port": WARP_PORT,
}],
"proxies": [warp_proxy],
"rules": [
{"ipRanges": ["*"], "domainNames": ["*"],
"action": "PROXY", "proxyNames": ["warp"]},
],
}
elif all_domains or all_ips:
warp_rule = {"action": "PROXY", "proxyNames": ["warp"]}
if all_domains:
warp_rule["domainNames"] = sorted(all_domains)
if all_ips:
warp_rule["ipRanges"] = sorted(all_ips)
cfg["egress"] = {
"proxies": [warp_proxy],
"rules": [
warp_rule,
{"ipRanges": ["*"], "domainNames": ["*"], "action": "DIRECT"},
],
}
else:
cfg.pop("egress", None)
save_mita_config(cfg)