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:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user