diff --git a/app.py b/app.py new file mode 100644 index 0000000..70697ab --- /dev/null +++ b/app.py @@ -0,0 +1,866 @@ +#!/usr/bin/env python3 +""" +mita Web Panel — Flask backend +""" +import os, json, subprocess, secrets, string, random, ipaddress, socket +from datetime import datetime, timedelta +from functools import wraps +from pathlib import Path + +from flask import (Flask, render_template, request, jsonify, + session, redirect, url_for, abort) +from collections import defaultdict +from threading import Lock + +# ── конфиг ────────────────────────────────────────────────────────────────── +MITA_CONFIG = os.environ.get("MITA_CONFIG", "/etc/mita/server_config.json") +PANEL_CONFIG = os.environ.get("PANEL_CONFIG", "/etc/mita/panel.json") +SECRET_PATH = os.environ.get("SECRET_PATH", "") # задаётся при установке +WARP_PORT = int(os.environ.get("WARP_PORT", "40000")) +SSL_CERT = os.environ.get("SSL_CERT", "") +SSL_KEY = os.environ.get("SSL_KEY", "") + +app = Flask(__name__) +app.secret_key = os.environ.get("FLASK_SECRET", secrets.token_hex(32)) +app.config["SESSION_COOKIE_SAMESITE"] = "Lax" +app.config["SESSION_COOKIE_SECURE"] = bool(SSL_CERT) +app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(hours=8) + +# ── Rate limiter ───────────────────────────────────────────────────────────── +class RateLimiter: + def __init__(self, max_attempts=5, window_seconds=3600): + self.max_attempts = max_attempts + self.window = window_seconds + self._attempts = defaultdict(list) # ip → [timestamps] + self._lock = Lock() + + def _cleanup(self, ip): + now = datetime.now().timestamp() + self._attempts[ip] = [t for t in self._attempts[ip] if now - t < self.window] + + def check(self, ip): + """Returns (allowed: bool, remaining: int, reset_in: int).""" + with self._lock: + self._cleanup(ip) + attempts = len(self._attempts[ip]) + remaining = max(0, self.max_attempts - attempts) + return remaining > 0, remaining, self.window + + def record_failure(self, ip): + with self._lock: + self._attempts[ip].append(datetime.now().timestamp()) + self._cleanup(ip) + + def reset(self, ip): + with self._lock: + self._attempts.pop(ip, None) + + def update_limits(self, max_attempts, window_seconds): + with self._lock: + self.max_attempts = max_attempts + self.window = window_seconds + +_login_limiter = RateLimiter(max_attempts=5, window_seconds=3600) + +# ── helpers ────────────────────────────────────────────────────────────────── +def load_panel_config(): + try: + return json.loads(Path(PANEL_CONFIG).read_text()) + except Exception: + return {} + +def load_mita_config(): + try: + return json.loads(Path(MITA_CONFIG).read_text()) + except Exception: + return {} + +def save_mita_config(cfg): + Path(MITA_CONFIG).write_text(json.dumps(cfg, indent=2, ensure_ascii=False)) + subprocess.run(["mita", "apply", "config", MITA_CONFIG], capture_output=True) + +def mita_cmd(*args): + r = subprocess.run(["mita", *args], capture_output=True, text=True) + return r.stdout.strip() + +def gen_password(length=64): + # Буквы и цифры только — спецсимволы могут ломать mita protobuf парсинг + # Используем расширенный безопасный набор без проблемных символов + chars = string.ascii_letters + string.digits + "!@#%^*_-=+?." + return "".join(secrets.choice(chars) for _ in range(length)) + +def gen_username(): + adjectives = ["swift","brave","quiet","cool","sharp","calm","bright","dark","wild","free"] + nouns = ["fox","hawk","river","storm","ember","peak","orbit","tide","frost","spark"] + return f"{secrets.choice(adjectives)}_{secrets.choice(nouns)}_{secrets.randbelow(9000)+1000}" + +def get_server_ip(): + try: + return subprocess.run( + ["curl","-s","--max-time","5","ifconfig.me"], + capture_output=True, text=True).stdout.strip() or "?" + except Exception: + return "?" + +def get_warp_ip(): + try: + r = subprocess.run( + ["curl","-s","--max-time","8","--proxy", + f"socks5h://127.0.0.1:{WARP_PORT}", + "https://ifconfig.me"], + capture_output=True, text=True) + ip = r.stdout.strip() + # базовая валидация + ipaddress.ip_address(ip) + return ip + except Exception: + return "недоступен" + +def get_traffic_stats(): + """Возвращает трафик по всем пользователям за 7 и 30 дней.""" + raw = mita_cmd("get", "users") + week_total = month_total = 0.0 + users_stats = [] + + # mita get users output format (v3.x): + # USER LAST ACTIVE 1 DAY DOWN 1 DAY UP 30 DAYS DOWN 30 DAYS UP + # username 2024-01-01 1.2MiB 0.5MiB 12.3GiB 4.5GiB + lines = raw.splitlines() + data_lines = [] + for line in lines: + line = line.strip() + if not line or line.upper().startswith("USER"): + continue + data_lines.append(line) + + for line in data_lines: + # Split on 2+ spaces to handle names with spaces + import re as _re + parts = _re.split(r"\s{2,}", line.strip()) + if len(parts) < 3: + # Try single-space split as fallback + parts = line.split() + if len(parts) < 3: + continue + try: + name = parts[0] + # Find traffic columns — they contain units like MiB, GiB, B, KiB + traffic_parts = [p for p in parts[1:] if any(u in p for u in ["B","iB","KiB","MiB","GiB","TiB"])] + if len(traffic_parts) >= 4: + d30_down = _parse_traffic(traffic_parts[2]) + d30_up = _parse_traffic(traffic_parts[3]) + elif len(traffic_parts) >= 2: + d30_down = _parse_traffic(traffic_parts[0]) + d30_up = _parse_traffic(traffic_parts[1]) + else: + continue + d7_approx = (d30_down + d30_up) / 30 * 7 + month_total += d30_down + d30_up + week_total += d7_approx + users_stats.append({ + "name": name, + "month_mb": round((d30_down + d30_up) / 1024 / 1024, 1), + }) + except Exception: + continue + + return { + "week_gb": round(week_total / 1024**3, 2), + "month_gb": round(month_total / 1024**3, 2), + "users": users_stats, + } + +def _parse_traffic(s: str) -> float: + """Parse traffic string like '12.5GiB' or '1.2MiB' → bytes (float).""" + s = s.strip() + mul = {"TiB":1024**4,"GiB":1024**3,"MiB":1024**2,"KiB":1024,"B":1} + for suffix, factor in mul.items(): + if s.endswith(suffix): + try: + return float(s[:-len(suffix)]) * factor + except ValueError: + return 0.0 + try: + return float(s) + except ValueError: + return 0.0 + +def get_port_info(): + cfg = load_mita_config() + bindings = cfg.get("portBindings", []) + if not bindings: + return "?" + b = bindings[0] + return b.get("portRange", str(b.get("port", "?"))) + +def build_client_config(name, password): + cfg = load_mita_config() + bindings = cfg.get("portBindings", []) + proto = bindings[0].get("protocol","TCP") if bindings else "TCP" + port_range = bindings[0].get("portRange", str(bindings[0].get("port","?"))) if bindings else "?" + server_ip = get_server_ip() + return { + "profiles": [{ + "profileName": "default", + "user": {"name": name, "password": password}, + "servers": [{ + "ipAddress": server_ip, + "portBindings": [{"portRange": port_range, "protocol": proto}] + }] + }], + "activeProfile": "default", + "rpcPort": 8964, + "socks5Port": 1080, + } + +def build_singbox_config(name, password): + """Генерирует sing-box совместимый конфиг для Karing, Hiddify, NekoBox и др.""" + cfg = load_mita_config() + bindings = cfg.get("portBindings", []) + 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() + + return { + "log": {"level": "info", "timestamp": True}, + "dns": { + "servers": [ + {"tag": "remote", "address": "tls://8.8.8.8", "detour": "proxy"}, + {"tag": "local", "address": "223.5.5.5", "detour": "direct"} + ], + "rules": [{"outbound": "any", "server": "local"}], + "final": "remote" + }, + "inbounds": [ + { + "type": "tun", + "tag": "tun-in", + "inet4_address": "172.19.0.1/30", + "auto_route": True, + "strict_route": True, + "sniff": True + }, + { + "type": "socks", + "tag": "socks-in", + "listen": "127.0.0.1", + "listen_port": 2080 + }, + { + "type": "http", + "tag": "http-in", + "listen": "127.0.0.1", + "listen_port": 2081 + } + ], + "outbounds": [ + { + "type": "mieru", + "tag": "proxy", + "server": server_ip, + "server_port": first_port, + "transport": proto, + "username": name, + "password": password + }, + {"type": "direct", "tag": "direct"}, + {"type": "block", "tag": "block"}, + {"type": "dns", "tag": "dns-out"} + ], + "route": { + "rules": [ + {"protocol": "dns", "outbound": "dns-out"}, + {"ip_is_private": True, "outbound": "direct"}, + { + "rule_set": ["geosite-cn", "geoip-cn"], + "outbound": "direct" + } + ], + "rule_set": [ + { + "tag": "geosite-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs", + "download_detour": "proxy" + }, + { + "tag": "geoip-cn", + "type": "remote", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs", + "download_detour": "proxy" + } + ], + "final": "proxy", + "auto_detect_interface": True + } + } + +# ── auth ───────────────────────────────────────────────────────────────────── +def login_required(f): + @wraps(f) + def decorated(*args, **kwargs): + if not session.get("logged_in"): + return redirect(url_for("login_page")) + return f(*args, **kwargs) + return decorated + +def secret_required(f): + @wraps(f) + def decorated(*args, **kwargs): + if SECRET_PATH and request.path.rstrip("/") not in ( + f"/{SECRET_PATH}", f"/{SECRET_PATH}/login" + ) and not request.path.startswith(f"/{SECRET_PATH}/"): + abort(404) + return f(*args, **kwargs) + return decorated + +# ── routes ──────────────────────────────────────────────────────────────────── +BASE = f"/{SECRET_PATH}" if SECRET_PATH else "" + +@app.route(f"{BASE}/") +@app.route(f"{BASE}") +@login_required +def index(): + return render_template("index.html", base=BASE) + +@app.route(f"{BASE}/login", methods=["GET","POST"]) +def login_page(): + if request.method == "POST": + ip = request.remote_addr or "unknown" + allowed, remaining, _ = _login_limiter.check(ip) + if not allowed: + return jsonify({ + "ok": False, + "error": "Слишком много попыток. Попробуйте позже.", + "remaining": 0, + "rate_limited": True, + }), 429 + + data = request.get_json(silent=True) or {} + pc = load_panel_config() + if (data.get("username") == pc.get("admin_user") and + data.get("password") == pc.get("admin_pass")): + _login_limiter.reset(ip) + session.permanent = True + session["logged_in"] = True + return jsonify({"ok": True, "remaining": _login_limiter.max_attempts}) + + _login_limiter.record_failure(ip) + _, remaining, _ = _login_limiter.check(ip) + return jsonify({ + "ok": False, + "error": "Неверный логин или пароль", + "remaining": remaining, + }), 401 + return render_template("login.html", base=BASE) + +@app.route(f"{BASE}/logout") +def logout(): + session.clear() + return redirect(url_for("login_page")) + +# ── API: dashboard ──────────────────────────────────────────────────────────── +@app.route(f"{BASE}/api/dashboard") +@login_required +def api_dashboard(): + try: + stats = get_traffic_stats() + except Exception: + stats = {"week_gb": 0, "month_gb": 0, "users": []} + try: + warp_ip = get_warp_ip() + except Exception: + warp_ip = "недоступен" + return jsonify({ + "server_ip": get_server_ip(), + "warp_ip": warp_ip, + "week_gb": stats["week_gb"], + "month_gb": stats["month_gb"], + "mita_port": get_port_info(), + "users_count": len(load_mita_config().get("users", [])), + "mita_running": _mita_running(), + }) + +def _mita_running(): + try: + r = subprocess.run(["systemctl","is-active","mita"], + capture_output=True, text=True, timeout=3) + return r.stdout.strip() == "active" + except Exception: + return False + +# ── API: users ──────────────────────────────────────────────────────────────── +@app.route(f"{BASE}/api/users", methods=["GET"]) +@login_required +def api_users_get(): + cfg = load_mita_config() + users = cfg.get("users", []) + # egress rules для определения warp-пользователей + egress = cfg.get("egress", {}) + return jsonify({"users": [{"name": u["name"]} for u in users], "egress": egress}) + +@app.route(f"{BASE}/api/users/create", methods=["POST"]) +@login_required +def api_users_create(): + data = request.get_json(silent=True) or {} + count = int(data.get("count", 1)) + mode = data.get("mode", "manual") # manual | auto + names = data.get("names", []) # для manual + + cfg = load_mita_config() + existing = {u["name"] for u in cfg.get("users", [])} + created = [] + + for i in range(count): + if mode == "manual" and i < len(names): + name = names[i].strip() + if not name: + continue + else: + name = gen_username() + while name in existing: + name = gen_username() + + if name in existing: + continue + + password = gen_password() + cfg.setdefault("users", []).append({"name": name, "password": password}) + existing.add(name) + created.append({ + "name": name, + "password": password, + "client_config": build_client_config(name, password), + }) + + save_mita_config(cfg) + return jsonify({"created": created}) + +@app.route(f"{BASE}/api/users/delete", methods=["POST"]) +@login_required +def api_users_delete(): + data = request.get_json(silent=True) or {} + name = data.get("name", "") + cfg = load_mita_config() + cfg["users"] = [u for u in cfg.get("users", []) if u["name"] != name] + save_mita_config(cfg) + return jsonify({"ok": True}) + +@app.route(f"{BASE}/api/users/warp", methods=["POST"]) +@login_required +def api_users_warp(): + """Включить/выключить WARP для конкретного пользователя через egress.users.""" + 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: + warp_users.add(name) + else: + warp_users.discard(name) + 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"}) + +@app.route(f"{BASE}/api/users/warp_status") +@login_required +def api_users_warp_status(): + pc = load_panel_config() + return jsonify({"warp_users": pc.get("warp_users", [])}) + +# ── API: SSL ────────────────────────────────────────────────────────────────── +@app.route(f"{BASE}/api/ssl/status") +@login_required +def api_ssl_status(): + pc = load_panel_config() + return jsonify({ + "type": pc.get("ssl_type", "none"), + "domain": pc.get("ssl_domain", ""), + "cert": pc.get("ssl_cert", ""), + "expires": pc.get("ssl_expires", ""), + }) + +@app.route(f"{BASE}/api/ssl/selfsigned", methods=["POST"]) +@login_required +def api_ssl_selfsigned(): + cert_dir = "/etc/mita/ssl" + os.makedirs(cert_dir, exist_ok=True) + cert = f"{cert_dir}/selfsigned.crt" + key = f"{cert_dir}/selfsigned.key" + r = subprocess.run([ + "openssl","req","-x509","-newkey","rsa:4096","-sha256", + "-days","3650","-nodes", + "-keyout", key, "-out", cert, + "-subj", "/CN=mita-panel/O=mita/C=XX" + ], capture_output=True, text=True) + if r.returncode != 0: + return jsonify({"ok": False, "error": r.stderr}), 500 + + # Прописать в env-файл панели + _update_env("SSL_CERT", cert) + _update_env("SSL_KEY", key) + + # Получить дату истечения + exp = subprocess.run( + ["openssl","x509","-noout","-enddate","-in",cert], + capture_output=True, text=True).stdout.strip() + + pc = load_panel_config() + pc.update({"ssl_type":"selfsigned","ssl_cert":cert,"ssl_key":key,"ssl_expires":exp}) + Path(PANEL_CONFIG).write_text(json.dumps(pc,indent=2)) + + return jsonify({"ok": True, "expires": exp, + "note": "Перезапустите панель: systemctl restart mita-panel"}) + +@app.route(f"{BASE}/api/ssl/letsencrypt", methods=["POST"]) +@login_required +def api_ssl_letsencrypt(): + data = request.get_json(silent=True) or {} + domain = data.get("domain","").strip() + email = data.get("email","").strip() + if not domain: + return jsonify({"ok": False, "error": "Укажите домен"}), 400 + + # Установить certbot если нет + if subprocess.run(["which","certbot"], capture_output=True).returncode != 0: + subprocess.run(["apt-get","install","-y","-qq","certbot"], capture_output=True) + + panel_port = int(os.environ.get("PANEL_PORT", "8080")) + cmd = ["certbot","certonly","--standalone","--non-interactive", + "--agree-tos","--http-01-port","80", + "-d", domain] + if email: + cmd += ["--email", email] + else: + cmd += ["--register-unsafely-without-email"] + + r = subprocess.run(cmd, capture_output=True, text=True) + if r.returncode != 0: + return jsonify({"ok": False, "error": r.stdout + r.stderr}), 500 + + cert = f"/etc/letsencrypt/live/{domain}/fullchain.pem" + key = f"/etc/letsencrypt/live/{domain}/privkey.pem" + _update_env("SSL_CERT", cert) + _update_env("SSL_KEY", key) + + exp = subprocess.run( + ["openssl","x509","-noout","-enddate","-in",cert], + capture_output=True, text=True).stdout.strip() + + pc = load_panel_config() + pc.update({"ssl_type":"letsencrypt","ssl_domain":domain, + "ssl_cert":cert,"ssl_key":key,"ssl_expires":exp}) + Path(PANEL_CONFIG).write_text(json.dumps(pc,indent=2)) + + return jsonify({"ok": True, "expires": exp, + "note": "Перезапустите панель: systemctl restart mita-panel"}) + +def _update_env(key, value): + env_file = "/etc/mita/panel.env" + lines = [] + found = False + if os.path.exists(env_file): + for line in Path(env_file).read_text().splitlines(): + if line.startswith(f"{key}="): + lines.append(f"{key}={value}") + found = True + else: + lines.append(line) + if not found: + lines.append(f"{key}={value}") + Path(env_file).write_text("\n".join(lines) + "\n") + +# ── 404 для всего вне секретного пути ──────────────────────────────────────── +@app.before_request +def check_secret_path(): + if not SECRET_PATH: + return + path = request.path.rstrip("/") or "/" + allowed_prefix = f"/{SECRET_PATH}" + if not (path == allowed_prefix or path.startswith(allowed_prefix + "/")): + abort(404) + +if __name__ == "__main__": + port = int(os.environ.get("PANEL_PORT", "8080")) + ssl_ctx = None + if SSL_CERT and SSL_KEY and os.path.exists(SSL_CERT) and os.path.exists(SSL_KEY): + import ssl as _ssl + ssl_ctx = (SSL_CERT, SSL_KEY) + app.run(host="0.0.0.0", port=port, ssl_context=ssl_ctx) + +# ── API: получить конфиг конкретного пользователя (пароль из server_config.json) ── +@app.route(f"{BASE}/api/users/config") +@login_required +def api_user_config(): + name = request.args.get("name", "") + if not name: + return jsonify({"ok": False, "error": "Не указано имя"}), 400 + cfg = load_mita_config() + user = next((u for u in cfg.get("users", []) if u["name"] == name), None) + if not user: + return jsonify({"ok": False, "error": "Пользователь не найден"}), 404 + return jsonify({ + "ok": True, + "name": user["name"], + "password": user["password"], + "client_config": build_client_config(user["name"], user["password"]), + "singbox_config": build_singbox_config(user["name"], user["password"]), + }) + +# ── API: WARP-правила конкретного пользователя ─────────────────────────────── +@app.route(f"{BASE}/api/users/warp_rules") +@login_required +def api_warp_rules_get(): + name = request.args.get("name", "") + if not name: + return jsonify({"ok": False, "error": "Не указано имя"}), 400 + pc = load_panel_config() + rules = pc.get("warp_rules", {}).get(name, { + "domains": [], + "ips": [], + "sources": [], # URL или geosite:/geoip: ссылки + }) + return jsonify({"ok": True, "name": name, "rules": rules}) + +@app.route(f"{BASE}/api/users/warp_rules", methods=["POST"]) +@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 + + 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()], + } + Path(PANEL_CONFIG).write_text(json.dumps(pc, indent=2)) + + # Перегенерировать egress в mita config на основе всех пользователей + _rebuild_egress(pc) + + return jsonify({"ok": True}) + +def _rebuild_egress(pc): + """ + Собирает egress.rules из warp_rules всех пользователей и записывает в mita config. + mita не поддерживает per-user egress — правила глобальные, но мы объединяем + домены/IP всех пользователей у которых WARP включён. + Источники (geosite:/geoip:/URL) разворачиваем в реальные списки. + """ + import urllib.request + + warp_users = set(pc.get("warp_users", [])) + all_domains = set() + all_ips = set() + + GEOSITE_BASE = "https://raw.githubusercontent.com/v2fly/domain-list-community/release" + 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:"):] + url = f"{GEOSITE_BASE}/{cat}.txt" + lines = urllib.request.urlopen(url, timeout=10).read().decode().splitlines() + for line in lines: + line = line.strip() + if line and not line.startswith("#") and "include:" not in line and "regexp:" not in line: + all_domains.add(line.lstrip("full:")) + 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 # не критично — продолжаем без этого источника + + 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 + + cfg["egress"] = { + "proxies": [{ + "name": "warp", + "protocol": "SOCKS5_PROXY_PROTOCOL", + "host": "127.0.0.1", + "port": WARP_PORT, + }], + "rules": [ + warp_rule, + {"ipRanges": ["*"], "domainNames": ["*"], "action": "DIRECT"}, + ], + } + + save_mita_config(cfg) + +# ── API: fail2ban config ────────────────────────────────────────────────────── +@app.route(f"{BASE}/api/fail2ban/status") +@login_required +def api_fail2ban_status(): + installed = subprocess.run(["which","fail2ban-client"], + capture_output=True).returncode == 0 + active = False + jail_active = False + max_retry = _login_limiter.max_attempts + ban_time = _login_limiter.window + + if installed: + r = subprocess.run(["systemctl","is-active","fail2ban"], + capture_output=True, text=True) + active = r.stdout.strip() == "active" + if active: + r2 = subprocess.run( + ["fail2ban-client","status","mita-panel"], + capture_output=True, text=True) + jail_active = r2.returncode == 0 + + # Read current limits from panel config + pc = load_panel_config() + max_retry = pc.get("login_max_attempts", 5) + ban_time = pc.get("login_ban_seconds", 3600) + + return jsonify({ + "installed": installed, + "active": active, + "jail_active": jail_active, + "max_retry": max_retry, + "ban_time": ban_time, + }) + +@app.route(f"{BASE}/api/fail2ban/configure", methods=["POST"]) +@login_required +def api_fail2ban_configure(): + data = request.get_json(silent=True) or {} + max_retry = int(data.get("max_retry", 5)) + ban_time = int(data.get("ban_time", 3600)) + install_f2b = data.get("install", False) + + if max_retry < 1 or max_retry > 100: + return jsonify({"ok": False, "error": "max_retry должен быть от 1 до 100"}), 400 + if ban_time < 60: + return jsonify({"ok": False, "error": "ban_time минимум 60 секунд"}), 400 + + # Update in-memory limiter + _login_limiter.update_limits(max_retry, ban_time) + + # Save to panel config + pc = load_panel_config() + pc["login_max_attempts"] = max_retry + pc["login_ban_seconds"] = ban_time + Path(PANEL_CONFIG).write_text(json.dumps(pc, indent=2)) + + # Install fail2ban if requested + if install_f2b: + r = subprocess.run(["apt-get","install","-y","-qq","fail2ban"], + capture_output=True, text=True) + if r.returncode != 0: + return jsonify({"ok": False, "error": "Ошибка установки fail2ban: " + r.stderr}), 500 + + # Write fail2ban filter for mita-panel + filter_content = """[Definition] +failregex = ^.*"POST /[^"]+/login[^"]*" 4(01|29).*$ +ignoreregex = +""" + jail_content = f"""[mita-panel] +enabled = true +filter = mita-panel +backend = auto +logpath = /var/log/mita-panel-access.log +maxretry = {max_retry} +bantime = {ban_time} +findtime = {ban_time} +action = iptables-multiport[name=mita-panel, port="http,https,8080,8443", protocol=tcp] +""" + try: + Path("/etc/fail2ban/filter.d/mita-panel.conf").write_text(filter_content) + Path("/etc/fail2ban/jail.d/mita-panel.conf").write_text(jail_content) + subprocess.run(["systemctl","enable","fail2ban","--now"], capture_output=True) + subprocess.run(["fail2ban-client","reload"], capture_output=True) + ok_msg = "Настройки сохранены и применены" + except Exception as e: + ok_msg = f"Настройки сохранены (fail2ban: {e})" + + return jsonify({"ok": True, "message": ok_msg}) + +# ── API: скачать конфиг как файл ───────────────────────────────────────────── +@app.route(f"{BASE}/api/users/config/download") +@login_required +def api_user_config_download(): + from flask import Response + name = request.args.get("name", "") + fmt = request.args.get("format", "mieru") # mieru | singbox + if not name: + abort(400) + cfg = load_mita_config() + user = next((u for u in cfg.get("users", []) if u["name"] == name), None) + if not user: + abort(404) + + if fmt == "singbox": + data = json.dumps(build_singbox_config(user["name"], user["password"]), + indent=2, ensure_ascii=False) + filename = f"{name}_singbox.json" + else: + data = json.dumps(build_client_config(user["name"], user["password"]), + indent=2, ensure_ascii=False) + filename = f"{name}_mieru.json" + + return Response( + data, + mimetype="application/json", + headers={"Content-Disposition": f'attachment; filename="{filename}"'} + ) + diff --git a/install-panel.sh b/install-panel.sh new file mode 100755 index 0000000..fb7974b --- /dev/null +++ b/install-panel.sh @@ -0,0 +1,334 @@ +#!/bin/bash +# ============================================================================= +# Установка mita Web Panel +# ============================================================================= +set -euo pipefail + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' +info() { echo -e "${BLUE}[INFO]${NC} $*"; } +ok() { echo -e "${GREEN}[OK]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; } +section() { echo -e "\n${CYAN}══════ $* ══════${NC}"; } + +[[ $EUID -ne 0 ]] && error "Запустите от root: sudo bash install-panel.sh" + +# ── проверка зависимостей ────────────────────────────────────────── +section "Проверка окружения" +command -v mita &>/dev/null || error "mita не установлен — сначала запустите install.sh" + +# ── выбор порта ──────────────────────────────────────────────────── +section "Выбор порта панели" + +find_free_port() { + local start=$1 + for port in $(seq $start 65535); do + if ! ss -tlnp 2>/dev/null | grep -q ":${port} " && \ + ! ss -tlnp 2>/dev/null | grep -q ":${port}$"; then + echo $port; return + fi + done + echo 8080 +} + +echo -e "Введите порт для панели (Enter = автовыбор из свободных начиная с 8080):" +read -r -p "Порт: " USER_PORT +if [[ -z "$USER_PORT" ]]; then + PANEL_PORT=$(find_free_port 8080) + info "Автовыбран свободный порт: $PANEL_PORT" +elif [[ "$USER_PORT" =~ ^[0-9]+$ ]] && [[ $USER_PORT -ge 1024 && $USER_PORT -le 65535 ]]; then + if ss -tlnp 2>/dev/null | grep -q ":${USER_PORT}[[:space:]]"; then + warn "Порт $USER_PORT занят, выбираем следующий свободный..." + PANEL_PORT=$(find_free_port $((USER_PORT+1))) + info "Выбран порт: $PANEL_PORT" + else + PANEL_PORT=$USER_PORT + ok "Порт: $PANEL_PORT" + fi +else + error "Неверный порт: $USER_PORT" +fi + +# ── установка Python, Flask и системных утилит ────────────────────── +section "Установка зависимостей" + +apt-get update -qq +apt-get install -y -qq \ + python3 python3-pip python3-venv \ + openssl curl wget jq \ + fail2ban \ + certbot \ + iptables ufw \ + ca-certificates gnupg lsb-release \ + net-tools \ + 2>/dev/null +ok "Системные пакеты установлены (Python, fail2ban, certbot, iptables, ufw)" + +# fail2ban не запускаем автоматически — пользователь настроит лимиты +# через раздел "Безопасность" в панели или mita-ctl → пункт 7 +systemctl enable fail2ban 2>/dev/null || true +ok "fail2ban установлен (настройка — через панель или mita-ctl)" + +PANEL_DIR="/opt/mita-panel" +mkdir -p "$PANEL_DIR" + +python3 -m venv "$PANEL_DIR/venv" +"$PANEL_DIR/venv/bin/pip" install -q flask gunicorn 2>/dev/null +ok "Python venv и Flask установлены" + +# ── копирование файлов ───────────────────────────────────────────── +section "Копирование файлов панели" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cp "$SCRIPT_DIR/app.py" "$PANEL_DIR/" +cp -r "$SCRIPT_DIR/templates" "$PANEL_DIR/" +[[ -d "$SCRIPT_DIR/static" ]] && cp -r "$SCRIPT_DIR/static" "$PANEL_DIR/" +ok "Файлы скопированы в $PANEL_DIR" + +# ── генерация секретов ───────────────────────────────────────────── +section "Генерация учётных данных" + +gen_pass() { + openssl rand -base64 32 | tr -d '/+=' | head -c 24 +} +gen_path() { + openssl rand -hex 10 +} + +ADMIN_USER="admin" +ADMIN_PASS=$(gen_pass) +SECRET_PATH=$(gen_path) +FLASK_SECRET=$(openssl rand -hex 32) + +ok "Логин и секретный путь сгенерированы" + +# ── выбор способа подключения ────────────────────────────────────── +section "Способ подключения к панели" +echo -e " ${BOLD}1.${NC} По IP-адресу (открытый доступ)" +echo -e " ${BOLD}2.${NC} SSH-туннель (панель только на 127.0.0.1, недоступна снаружи)" +echo -e " ${BOLD}3.${NC} По доменному имени с Let's Encrypt" +echo "" +read -r -p "Выберите способ [1]: " ACCESS_MODE_CHOICE +ACCESS_MODE_CHOICE=${ACCESS_MODE_CHOICE:-1} + +ACCESS_MODE="ip" +PANEL_BIND="0.0.0.0" +SSL_DOMAIN="" +SSH_ACCESS_PORT=22 +LE_DOMAIN="" + +case "$ACCESS_MODE_CHOICE" in + 2) + ACCESS_MODE="ssh" + PANEL_BIND="127.0.0.1" + read -r -p "SSH-порт вашего VPS [22]: " SSH_ACCESS_PORT + SSH_ACCESS_PORT=${SSH_ACCESS_PORT:-22} + info "Панель будет доступна только через SSH-туннель" + ;; + 3) + ACCESS_MODE="domain" + PANEL_BIND="0.0.0.0" + read -r -p "Домен для панели (например panel.example.com): " LE_DOMAIN + [[ -z "$LE_DOMAIN" ]] && { warn "Домен не указан, переключаемся на IP"; ACCESS_MODE="ip"; } + ;; +esac + +# ── конфиг панели ───────────────────────────────────────────────── +PANEL_CONFIG="/etc/mita/panel.json" +mkdir -p /etc/mita + +# Получить WARP_PORT из существующего env если есть +WARP_PORT=40000 +[[ -f /etc/mita/panel.env ]] && WARP_PORT=$(grep '^WARP_PORT=' /etc/mita/panel.env | cut -d= -f2 || echo 40000) + +cat > "$PANEL_CONFIG" << EOF +{ + "admin_user": "$ADMIN_USER", + "admin_pass": "$ADMIN_PASS", + "secret_path": "$SECRET_PATH", + "panel_port": $PANEL_PORT, + "access_mode": "$ACCESS_MODE", + "ssh_port": $SSH_ACCESS_PORT, + "ssl_type": "none", + "warp_users": [] +} +EOF +ok "Конфиг панели: $PANEL_CONFIG" + +# ── env файл ────────────────────────────────────────────────────── +cat > /etc/mita/panel.env << EOF +PANEL_PORT=$PANEL_PORT +SECRET_PATH=$SECRET_PATH +FLASK_SECRET=$FLASK_SECRET +PANEL_CONFIG=$PANEL_CONFIG +MITA_CONFIG=/etc/mita/server_config.json +WARP_PORT=$WARP_PORT +SSL_CERT= +SSL_KEY= +EOF +ok "Env файл: /etc/mita/panel.env" + +# ── враппер запуска (динамически подхватывает SSL из panel.env) ── +section "Создание systemd сервиса" + +cat > /opt/mita-panel/start.sh << 'STARTEOF' +#!/bin/bash +# Читаем актуальный panel.env каждый раз при старте +set -a +source /etc/mita/panel.env +set +a + +SSL_ARGS="" +if [[ -n "$SSL_CERT" && -n "$SSL_KEY" && -f "$SSL_CERT" && -f "$SSL_KEY" ]]; then + SSL_ARGS="--certfile=$SSL_CERT --keyfile=$SSL_KEY" +fi + +exec /opt/mita-panel/venv/bin/gunicorn \ + --bind 0.0.0.0:${PANEL_PORT} \ + --workers 2 \ + --timeout 120 \ + --access-logfile /var/log/mita-panel-access.log \ + --error-logfile /var/log/mita-panel.log \ + $SSL_ARGS \ + app:app +STARTEOF +chmod +x /opt/mita-panel/start.sh +ok "Враппер запуска создан: /opt/mita-panel/start.sh" + +cat > /etc/systemd/system/mita-panel.service << EOF +[Unit] +Description=mita Web Panel +After=network.target mita.service +Wants=mita.service + +[Service] +Type=simple +User=root +WorkingDirectory=$PANEL_DIR +ExecStart=/opt/mita-panel/start.sh +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +EOF + +systemctl daemon-reload +systemctl enable mita-panel +systemctl restart mita-panel +sleep 2 + +if systemctl is-active --quiet mita-panel; then + ok "mita-panel запущен" +else + warn "Не удалось запустить — проверьте: journalctl -u mita-panel -n 30" +fi + +# ── firewall ────────────────────────────────────────────────────── +section "Открытие порта в firewall" + +if command -v ufw &>/dev/null && ufw status | grep -q "Status: active"; then + ufw allow "$PANEL_PORT/tcp" comment "mita-panel" 2>/dev/null || true + ok "UFW: порт $PANEL_PORT открыт" +elif command -v iptables &>/dev/null; then + iptables -I INPUT -p tcp --dport "$PANEL_PORT" -j ACCEPT 2>/dev/null || true + ok "iptables: порт $PANEL_PORT открыт" +fi + +# ── итог ────────────────────────────────────────────────────────── +section "Установка завершена!" + +SERVER_IP=$(curl -s --max-time 5 ifconfig.me 2>/dev/null || hostname -I | awk '{print $1}') +PROTO="http" + +echo "" +echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗" +echo -e "║ ПАНЕЛЬ УПРАВЛЕНИЯ УСТАНОВЛЕНА ║" +echo -e "╚══════════════════════════════════════════════════════════════╝${NC}" +echo "" +echo -e "${CYAN}Адрес панели:${NC}" +echo -e " ${BOLD}${YELLOW}${PROTO}://${SERVER_IP}:${PANEL_PORT}/${SECRET_PATH}/${NC}" +echo "" +echo -e "${CYAN}Учётные данные:${NC}" +echo -e " Логин: ${BOLD}${ADMIN_USER}${NC}" +echo -e " Пароль: ${BOLD}${YELLOW}${ADMIN_PASS}${NC}" +echo "" +echo -e "${CYAN}Секретный путь:${NC}" +echo -e " ${BOLD}/${SECRET_PATH}/${NC}" +echo -e " ${RED}Без него панель недоступна (возвращает 404)${NC}" +echo "" +echo -e "${CYAN}Полная ссылка для браузера:${NC}" +echo -e " ${BOLD}${GREEN}${PROTO}://${SERVER_IP}:${PANEL_PORT}/${SECRET_PATH}/${NC}" +echo "" +echo -e "${CYAN}Настройка SSL (опционально):${NC}" +echo -e " Войдите в панель → раздел SSL → выберите тип сертификата" +echo -e " После установки SSL: ${BOLD}systemctl restart mita-panel${NC}" +echo "" +echo -e "${CYAN}Полезные команды:${NC}" +echo -e " ${YELLOW}systemctl status mita-panel${NC} — статус" +echo -e " ${YELLOW}journalctl -u mita-panel -f${NC} — логи" +echo -e " ${YELLOW}systemctl restart mita-panel${NC} — перезапуск" +echo "" +echo -e "${RED}⚠ Сохраните ссылку и пароль — они больше не отобразятся!${NC}" +echo "" + +# ── SSH туннель: финальные инструкции ───────────────────────────── +if [[ "$ACCESS_MODE" == "ssh" ]]; then + echo "" + echo -e "${CYAN}┌──────────────────────────────────────────────────────────────┐${NC}" + echo -e "${CYAN}│ Подключение к панели через SSH-туннель │${NC}" + echo -e "${CYAN}└──────────────────────────────────────────────────────────────┘${NC}" + echo "" + echo -e " Выполните на ${BOLD}вашем компьютере${NC}:" + echo "" + echo -e " ${YELLOW}ssh -L ${PANEL_PORT}:127.0.0.1:${PANEL_PORT} -p ${SSH_ACCESS_PORT} root@${SERVER_IP} -N${NC}" + echo "" + echo -e " Затем откройте в браузере:" + echo -e " ${BOLD}${GREEN}http://127.0.0.1:${PANEL_PORT}/${SECRET_PATH}/${NC}" + echo "" + echo -e " Для фонового режима:" + echo -e " ${YELLOW}ssh -fN -L ${PANEL_PORT}:127.0.0.1:${PANEL_PORT} -p ${SSH_ACCESS_PORT} root@${SERVER_IP}${NC}" + echo "" +fi + +# ── Let's Encrypt ───────────────────────────────────────────────── +if [[ "$ACCESS_MODE" == "domain" && -n "$LE_DOMAIN" ]]; then + info "Получение Let's Encrypt сертификата для $LE_DOMAIN..." + command -v certbot &>/dev/null || apt-get install -y -qq certbot + if certbot certonly --standalone --non-interactive --agree-tos \ + --register-unsafely-without-email -d "$LE_DOMAIN" 2>/dev/null; then + LE_CERT="/etc/letsencrypt/live/${LE_DOMAIN}/fullchain.pem" + LE_KEY="/etc/letsencrypt/live/${LE_DOMAIN}/privkey.pem" + echo "SSL_CERT=$LE_CERT" >> /etc/mita/panel.env + echo "SSL_KEY=$LE_KEY" >> /etc/mita/panel.env + python3 -c " +import json +with open('$PANEL_CONFIG') as f: d=json.load(f) +d.update({'ssl_type':'letsencrypt','ssl_domain':'$LE_DOMAIN','ssl_cert':'$LE_CERT','ssl_key':'$LE_KEY'}) +with open('$PANEL_CONFIG','w') as f: json.dump(d,f,indent=2) +" + (crontab -l 2>/dev/null; echo "0 3 * * * certbot renew --quiet && systemctl restart mita-panel") | sort -u | crontab - + systemctl restart mita-panel 2>/dev/null || true + ok "Сертификат получен" + PROTO="https" + else + warn "Не удалось получить сертификат Let's Encrypt. Убедитесь что $LE_DOMAIN указывает на этот сервер и порт 80 открыт." + PROTO="http" + fi +fi + +# ── Установка mita-ctl как системной команды ────────────────────── +section "Установка утилиты mita-ctl" + +SCRIPT_DIR_CTL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CTL_SRC="$SCRIPT_DIR_CTL/mita-ctl.sh" + +if [[ -f "$CTL_SRC" ]]; then + cp "$CTL_SRC" /usr/local/bin/mita-ctl + chmod +x /usr/local/bin/mita-ctl + ok "mita-ctl установлен. Запуск: sudo mita-ctl" +else + warn "mita-ctl.sh не найден рядом — пропускаем" + warn "Чтобы установить вручную: cp mita-ctl.sh /usr/local/bin/mita-ctl && chmod +x /usr/local/bin/mita-ctl" +fi diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..1e10dfa --- /dev/null +++ b/install.sh @@ -0,0 +1,636 @@ +#!/bin/bash +# ============================================================================= +# mita + WARP Setup Script +# Ubuntu / Debian VPS +# ============================================================================= +set -euo pipefail + +# Директория, из которой запущен скрипт (нужна для поиска install-panel.sh рядом) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# ========================= ЦВЕТА ДЛЯ ВЫВОДА ========================= +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m' + +info() { echo -e "${BLUE}[INFO]${NC} $*"; } +ok() { echo -e "${GREEN}[OK]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; } +section() { echo -e "\n${CYAN}══════ $* ══════${NC}"; } + +# ============================================================================= +# ██████████████████████████████████████████████████████████████████████████ +# !! НАСТРОЙКИ — РЕДАКТИРОВАТЬ ЗДЕСЬ !! +# ██████████████████████████████████████████████████████████████████████████ +# ============================================================================= + +# ---- Порты mita (можно указать диапазон или одиночные порты) ---- +MITA_PORT_RANGE="2100-2110" # диапазон портов mita +MITA_PROTOCOL="TCP" # TCP или UDP + +# ---- Пользователи mita ---- +# Формат: "имя:пароль" +# Добавляйте сколько угодно строк +MITA_USERS=( + "alice:ЗАМЕНИТЕ_НА_СИЛЬНЫЙ_ПАРОЛЬ_1" + "bob:ЗАМЕНИТЕ_НА_СИЛЬНЫЙ_ПАРОЛЬ_2" +) + +# ---- WARP ---- +WARP_ENABLED=true # true — включить WARP; false — только mita без WARP +WARP_PORT=40000 # локальный порт SOCKS5 от WARP (не менять без причины) + +# ---- Логирование mita ---- +MITA_LOG_LEVEL="INFO" # INFO или DEBUG + +# ============================================================================= +# !! КОНЕЦ НАСТРОЕК !! +# ============================================================================= + +MITA_CONFIG_DIR="/etc/mita" +MITA_CONFIG_FILE="$MITA_CONFIG_DIR/server_config.json" +LISTS_DIR="/etc/mita/lists" +CUSTOM_DOMAINS_FILE="$LISTS_DIR/custom_domains.txt" +CUSTOM_IPS_FILE="$LISTS_DIR/custom_ips.txt" +AUTO_DOMAINS_FILE="$LISTS_DIR/auto_domains.txt" +AUTO_IPS_FILE="$LISTS_DIR/auto_ips.txt" +UPDATE_SCRIPT="/usr/local/bin/mita-update-lists" +CRON_FILE="/etc/cron.d/mita-update" + +# ============================================================================= +# ПРОВЕРКИ +# ============================================================================= +section "Проверка окружения" + +[[ $EUID -ne 0 ]] && error "Запустите скрипт от root: sudo bash install.sh" + +OS=$(. /etc/os-release && echo "$ID") +[[ "$OS" != "ubuntu" && "$OS" != "debian" ]] && error "Поддерживается только Ubuntu/Debian" +ok "ОС: $OS" + +# Проверка пользователей +for entry in "${MITA_USERS[@]}"; do + name="${entry%%:*}"; pass="${entry##*:}" + [[ "$pass" == *"ЗАМЕНИТЕ"* ]] && error "Замените пароль для пользователя '$name' в начале скрипта!" + [[ ${#pass} -lt 8 ]] && error "Пароль пользователя '$name' слишком короткий (минимум 8 символов)" +done +ok "Пользователи настроены корректно (${#MITA_USERS[@]} шт.)" + +# ============================================================================= +# ЗАВИСИМОСТИ +# ============================================================================= +section "Установка зависимостей" + +apt-get update -qq +apt-get install -y -qq \ + curl wget jq cron \ + ca-certificates gnupg lsb-release \ + iptables ipset ufw \ + fail2ban openssl \ + net-tools \ + 2>/dev/null +ok "Базовые пакеты установлены (включая fail2ban, ufw, ipset)" + +# ============================================================================= +# УСТАНОВКА MITA +# ============================================================================= +section "Установка mita" + +MITA_VERSION=$(curl -s https://api.github.com/repos/enfein/mieru/releases/latest \ + | jq -r '.tag_name' | tr -d 'v') +[[ -z "$MITA_VERSION" ]] && error "Не удалось получить версию mita с GitHub" +info "Последняя версия mita: $MITA_VERSION" + +ARCH=$(uname -m) +case "$ARCH" in + x86_64) DEB_ARCH="amd64" ;; + aarch64) DEB_ARCH="arm64" ;; + armv7l) DEB_ARCH="armhf" ;; + *) error "Неподдерживаемая архитектура: $ARCH" ;; +esac + +DEB_FILE="mita_${MITA_VERSION}_${DEB_ARCH}.deb" +DL_URL="https://github.com/enfein/mieru/releases/download/v${MITA_VERSION}/${DEB_FILE}" + +info "Загрузка: $DL_URL" +wget -q --show-progress "$DL_URL" -O /tmp/"$DEB_FILE" || error "Ошибка загрузки mita" +dpkg -i /tmp/"$DEB_FILE" || apt-get install -f -y -qq +rm -f /tmp/"$DEB_FILE" +ok "mita $MITA_VERSION установлен" + +# ============================================================================= +# УСТАНОВКА DOCKER + WARP +# ============================================================================= +if $WARP_ENABLED; then + section "Установка Docker и Cloudflare WARP" + + if ! command -v docker &>/dev/null; then + info "Установка Docker..." + curl -fsSL https://get.docker.com | sh -s -- -q + systemctl enable docker --now + ok "Docker установлен" + else + ok "Docker уже установлен" + fi + + info "Запуск Cloudflare WARP в Docker..." + docker rm -f cloudflare-warp 2>/dev/null || true + docker run -d \ + --name cloudflare-warp \ + --restart unless-stopped \ + -p 127.0.0.1:${WARP_PORT}:40000 \ + seiry/cloudflare-warp-proxy + + # Подождать пока WARP поднимется + info "Ожидание запуска WARP (до 30 сек)..." + for i in $(seq 1 30); do + if curl -s --max-time 3 --proxy "socks5h://127.0.0.1:${WARP_PORT}" \ + "https://www.cloudflare.com/cdn-cgi/trace" 2>/dev/null | grep -q "warp=on"; then + ok "WARP активен и работает" + break + fi + sleep 1 + [[ $i -eq 30 ]] && warn "WARP не ответил за 30 сек — продолжаем, проверьте позже: docker logs cloudflare-warp" + done +fi + +# ============================================================================= +# СОЗДАНИЕ ДИРЕКТОРИЙ И ФАЙЛОВ СПИСКОВ +# ============================================================================= +section "Создание файлов конфигурации" + +mkdir -p "$MITA_CONFIG_DIR" "$LISTS_DIR" + +# ---- custom_domains.txt ---- +cat > "$CUSTOM_DOMAINS_FILE" << 'EOF' +# ================================================================ +# ПОЛЬЗОВАТЕЛЬСКИЙ СПИСОК ДОМЕНОВ → трафик пойдёт через WARP +# ================================================================ +# Добавляйте домены по одному на строку. +# Строки начинающиеся с # — комментарии, игнорируются. +# Субдомены включаются автоматически (openai.com → *.openai.com). +# +# Примеры: +# openai.com +# chatgpt.com +# anthropic.com +# claude.ai +# ================================================================ + +EOF +ok "Создан: $CUSTOM_DOMAINS_FILE" + +# ---- custom_ips.txt ---- +cat > "$CUSTOM_IPS_FILE" << 'EOF' +# ================================================================ +# ПОЛЬЗОВАТЕЛЬСКИЙ СПИСОК IP/CIDR → трафик пойдёт через WARP +# ================================================================ +# Формат: одна подсеть CIDR или IP на строку. +# Примеры: +# 104.16.0.0/12 +# 172.64.0.0/13 +# ================================================================ + +EOF +ok "Создан: $CUSTOM_IPS_FILE" + +# ---- auto_domains.txt (источники для автообновления) ---- +cat > "$AUTO_DOMAINS_FILE" << 'EOF' +# ================================================================ +# ИСТОЧНИКИ АВТООБНОВЛЯЕМЫХ ДОМЕНОВ (ссылки или geosite-категории) +# ================================================================ +# Поддерживаемые форматы строк: +# +# 1) URL на текстовый файл (один домен на строку): +# https://raw.githubusercontent.com/v2fly/domain-list-community/release/openai.txt +# +# 2) geosite-категория из репозитория v2fly: +# geosite:openai +# geosite:google +# geosite:netflix +# geosite:category-ru ← российские сайты +# (полный список: github.com/v2fly/domain-list-community/tree/master/data) +# +# Строки с # — комментарии. +# ================================================================ + +# --- OpenAI / AI сервисы --- +geosite:openai + +# --- Примеры (раскомментируйте нужное) --- +# geosite:google +# geosite:netflix +# https://raw.githubusercontent.com/nicholaswilde/blocklists/main/domain-lists/streaming.txt +EOF +ok "Создан: $AUTO_DOMAINS_FILE" + +# ---- auto_ips.txt ---- +cat > "$AUTO_IPS_FILE" << 'EOF' +# ================================================================ +# ИСТОЧНИКИ АВТООБНОВЛЯЕМЫХ IP-ДИАПАЗОНОВ +# ================================================================ +# Формат строк: +# +# 1) URL на файл с CIDR: +# https://raw.githubusercontent.com/herrbischoff/country-ip-blocks/master/ipv4/us.cidr +# +# 2) geoip-страна (ISO-код): +# geoip:us +# geoip:gb +# geoip:de +# +# ================================================================ + +# Пример: американские IP через WARP +# geoip:us +EOF +ok "Создан: $AUTO_IPS_FILE" + +# ============================================================================= +# СКРИПТ ОБНОВЛЕНИЯ СПИСКОВ +# ============================================================================= +section "Создание скрипта обновления списков" + +cat > "$UPDATE_SCRIPT" << UPDATEEOF +#!/bin/bash +# Автообновление списков доменов/IP и перегенерация конфига mita +# Запускается cron ежедневно + +set -euo pipefail +LISTS_DIR="$LISTS_DIR" +MITA_CONFIG_FILE="$MITA_CONFIG_FILE" +WARP_ENABLED=$WARP_ENABLED +WARP_PORT=$WARP_PORT +MITA_PORT_RANGE="$MITA_PORT_RANGE" +MITA_PROTOCOL="$MITA_PROTOCOL" +MITA_LOG_LEVEL="$MITA_LOG_LEVEL" +GEOSITE_BASE="https://raw.githubusercontent.com/v2fly/domain-list-community/release" +GEOIP_BASE="https://raw.githubusercontent.com/herrbischoff/country-ip-blocks/master/ipv4" + +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] \$*" | tee -a /var/log/mita-update.log; } + +# ---------- Сбор доменов ---------- +log "Сбор доменов..." +TMP_DOMAINS=\$(mktemp) + +# Пользовательские домены +grep -v '^\s*#' "\$LISTS_DIR/custom_domains.txt" | grep -v '^\s*$' >> "\$TMP_DOMAINS" || true + +# Автоматические источники +while IFS= read -r line; do + line="\$(echo "\$line" | xargs)" + [[ -z "\$line" || "\$line" == \#* ]] && continue + + if [[ "\$line" == geosite:* ]]; then + category="\${line#geosite:}" + log " geosite:\$category" + curl -sf "\$GEOSITE_BASE/\${category}.txt" 2>/dev/null \ + | grep -v '^\s*#' | grep -v '^\s*$' | grep -v 'include:' | grep -v 'regexp:' \ + | sed 's/^full://' \ + >> "\$TMP_DOMAINS" || log " WARN: не удалось загрузить geosite:\$category" + + elif [[ "\$line" == http* ]]; then + log " URL: \$line" + curl -sf "\$line" 2>/dev/null \ + | grep -v '^\s*#' | grep -v '^\s*$' \ + >> "\$TMP_DOMAINS" || log " WARN: не удалось загрузить \$line" + fi +done < "\$LISTS_DIR/auto_domains.txt" + +# Дедупликация доменов +FINAL_DOMAINS=\$(sort -u "\$TMP_DOMAINS" | grep -v '^\s*$') +rm -f "\$TMP_DOMAINS" +DOMAIN_COUNT=\$(echo "\$FINAL_DOMAINS" | grep -c . || echo 0) +log "Доменов собрано: \$DOMAIN_COUNT" + +# ---------- Сбор IP-диапазонов ---------- +log "Сбор IP-диапазонов..." +TMP_IPS=\$(mktemp) + +grep -v '^\s*#' "\$LISTS_DIR/custom_ips.txt" | grep -v '^\s*$' >> "\$TMP_IPS" || true + +while IFS= read -r line; do + line="\$(echo "\$line" | xargs)" + [[ -z "\$line" || "\$line" == \#* ]] && continue + + if [[ "\$line" == geoip:* ]]; then + country="\${line#geoip:}" + log " geoip:\$country" + curl -sf "\$GEOIP_BASE/\${country}.cidr" 2>/dev/null \ + >> "\$TMP_IPS" || log " WARN: не удалось загрузить geoip:\$country" + + elif [[ "\$line" == http* ]]; then + log " URL: \$line" + curl -sf "\$line" 2>/dev/null | grep -v '^\s*#' | grep -v '^\s*$' \ + >> "\$TMP_IPS" || log " WARN: не удалось загрузить \$line" + fi +done < "\$LISTS_DIR/auto_ips.txt" + +FINAL_IPS=\$(sort -u "\$TMP_IPS" | grep -v '^\s*$') +rm -f "\$TMP_IPS" +IP_COUNT=\$(echo "\$FINAL_IPS" | grep -c . || echo 0) +log "IP-диапазонов собрано: \$IP_COUNT" + +# ---------- Сборка JSON egress-правил ---------- +build_egress() { + local domains="\$1" + local ips="\$2" + + if ! \$WARP_ENABLED; then + echo '{"rules": [{"ipRanges": ["*"], "domainNames": ["*"], "action": "DIRECT"}]}' + return + fi + + local domain_arr="[]" + local ip_arr="[]" + + [[ -n "\$domains" ]] && domain_arr=\$(echo "\$domains" | jq -R . | jq -s .) + [[ -n "\$ips" ]] && ip_arr=\$(echo "\$ips" | jq -R . | jq -s .) + + # Строим правила: сначала WARP-правило (если есть домены/IP), потом DIRECT для всего остального + local rules="[]" + + if [[ \$DOMAIN_COUNT -gt 0 || \$IP_COUNT -gt 0 ]]; then + warp_rule=\$(jq -n \ + --argjson d "\$domain_arr" \ + --argjson i "\$ip_arr" \ + '{action: "PROXY", proxyNames: ["warp"], domainNames: \$d, ipRanges: \$i}') + rules=\$(echo "[\$warp_rule]") + fi + + # Финальное правило — всё остальное напрямую + direct_rule='{"ipRanges": ["*"], "domainNames": ["*"], "action": "DIRECT"}' + rules=\$(echo "\$rules" | jq ". + [\$direct_rule]" --argjson direct "\$direct_rule") + + jq -n \ + --argjson rules "\$rules" \ + '{ + proxies: [{ + name: "warp", + protocol: "SOCKS5_PROXY_PROTOCOL", + host: "127.0.0.1", + port: \$ENV.WARP_PORT|tonumber + }], + rules: \$rules + }' <<< "" 2>/dev/null || jq -n \ + --argjson rules "\$rules" \ + --argjson port "\$WARP_PORT" \ + '{ + proxies: [{name: "warp", protocol: "SOCKS5_PROXY_PROTOCOL", host: "127.0.0.1", port: \$port}], + rules: \$rules + }' +} + +EGRESS_JSON=\$(build_egress "\$FINAL_DOMAINS" "\$FINAL_IPS") + +# ---------- Чтение текущих пользователей ---------- +CURRENT_USERS=\$(jq '.users' "\$MITA_CONFIG_FILE" 2>/dev/null || echo '[]') + +# ---------- Генерация итогового конфига ---------- +log "Генерация конфига mita..." + +# Разбор диапазона портов +if [[ "\$MITA_PORT_RANGE" == *"-"* ]]; then + PORT_BINDING=\$(jq -n --arg r "\$MITA_PORT_RANGE" --arg p "\$MITA_PROTOCOL" \ + '[{portRange: \$r, protocol: \$p}]') +else + PORT_BINDING=\$(jq -n --arg port "\$MITA_PORT_RANGE" --arg p "\$MITA_PROTOCOL" \ + '[{port: (\$port|tonumber), protocol: \$p}]') +fi + +jq -n \ + --argjson portBindings "\$PORT_BINDING" \ + --argjson users "\$CURRENT_USERS" \ + --arg logLevel "\$MITA_LOG_LEVEL" \ + --argjson egress "\$EGRESS_JSON" \ + '{ + portBindings: \$portBindings, + users: \$users, + loggingLevel: \$logLevel, + egress: \$egress + }' > "\${MITA_CONFIG_FILE}.tmp" + +mv "\${MITA_CONFIG_FILE}.tmp" "\$MITA_CONFIG_FILE" +log "Конфиг сохранён: \$MITA_CONFIG_FILE" + +# ---------- Применение конфига ---------- +log "Применение конфига mita..." +# Запустить mita если не запущен (нужен для apply config) +MITA_WAS_STOPPED=false +if ! systemctl is-active --quiet mita 2>/dev/null; then + log "mita не запущен — запускаем для применения конфига..." + systemctl reset-failed mita 2>/dev/null || true + /usr/bin/mita run & + MITA_BG_PID=\$! + sleep 3 + MITA_WAS_STOPPED=true +fi + +mita apply config "\$MITA_CONFIG_FILE" && log "Конфиг применён успешно" \ + || { log "ОШИБКА применения конфига"; exit 1; } + +if \$MITA_WAS_STOPPED; then + kill \$MITA_BG_PID 2>/dev/null || true + sleep 1 + systemctl reset-failed mita 2>/dev/null || true + systemctl start mita && log "mita запущен" || log "WARN: не удалось запустить mita" +else + systemctl restart mita + log "mita перезапущен" +fi + +log "Обновление завершено. Доменов: \$DOMAIN_COUNT, IP: \$IP_COUNT" +UPDATEEOF + +chmod +x "$UPDATE_SCRIPT" +ok "Скрипт обновления создан: $UPDATE_SCRIPT" + +# ============================================================================= +# ПЕРВОНАЧАЛЬНАЯ ГЕНЕРАЦИЯ КОНФИГА С ПОЛЬЗОВАТЕЛЯМИ +# ============================================================================= +section "Генерация начального конфига mita" + +# Собрать JSON пользователей +USERS_JSON="[" +for i in "${!MITA_USERS[@]}"; do + entry="${MITA_USERS[$i]}" + name="${entry%%:*}" + pass="${entry##*:}" + sep=$([[ $i -lt $((${#MITA_USERS[@]}-1)) ]] && echo "," || echo "") + USERS_JSON+=$(jq -n --arg n "$name" --arg p "$pass" \ + '{name: $n, password: $p}') + USERS_JSON+="$sep" +done +USERS_JSON+="]" + +# Разбор диапазона портов для начального конфига +if [[ "$MITA_PORT_RANGE" == *"-"* ]]; then + PORT_BINDING=$(jq -n --arg r "$MITA_PORT_RANGE" --arg p "$MITA_PROTOCOL" \ + '[{portRange: $r, protocol: $p}]') +else + PORT_BINDING=$(jq -n --arg port "$MITA_PORT_RANGE" --arg p "$MITA_PROTOCOL" \ + '[{port: ($port|tonumber), protocol: $p}]') +fi + +# Базовый конфиг без egress (применим после первого запуска update-lists) +jq -n \ + --argjson portBindings "$PORT_BINDING" \ + --argjson users "$USERS_JSON" \ + --arg logLevel "$MITA_LOG_LEVEL" \ + '{ + portBindings: $portBindings, + users: $users, + loggingLevel: $logLevel, + egress: {} + }' > "$MITA_CONFIG_FILE" + +ok "Начальный конфиг записан: $MITA_CONFIG_FILE" + +# Применить конфиг +mita apply config "$MITA_CONFIG_FILE" +ok "Конфиг mita применён" + +# ============================================================================= +# ЗАПУСК MITA +# ============================================================================= +section "Запуск mita" + +systemctl enable mita 2>/dev/null || true +systemctl restart mita +sleep 2 +if systemctl is-active --quiet mita; then + ok "mita запущен и работает" +else + warn "mita не запустился — проверьте: journalctl -u mita -n 50" +fi + +# ============================================================================= +# НАСТРОЙКА CRON +# ============================================================================= +section "Настройка автообновления (cron)" + +cat > "$CRON_FILE" << EOF +# Обновление списков доменов/IP для mita — каждый день в 04:00 +0 4 * * * root $UPDATE_SCRIPT >> /var/log/mita-update.log 2>&1 +EOF + +ok "Cron задача создана: $CRON_FILE" + +# ============================================================================= +# ПЕРВЫЙ ЗАПУСК ОБНОВЛЕНИЯ СПИСКОВ +# ============================================================================= +section "Первое обновление списков" + +info "Запуск $UPDATE_SCRIPT..." +bash "$UPDATE_SCRIPT" && ok "Списки обновлены, конфиг применён" \ + || warn "Ошибка при обновлении списков, проверьте /var/log/mita-update.log" + +# ============================================================================= +# ОТКРЫТИЕ ПОРТОВ В FIREWALL +# ============================================================================= +section "Настройка firewall" + +if command -v ufw &>/dev/null && ufw status | grep -q "Status: active"; then + if [[ "$MITA_PORT_RANGE" == *"-"* ]]; then + ufw allow "${MITA_PORT_RANGE}/${MITA_PROTOCOL,,}" comment "mita proxy" 2>/dev/null || true + else + ufw allow "${MITA_PORT_RANGE}/${MITA_PROTOCOL,,}" comment "mita proxy" 2>/dev/null || true + fi + ok "UFW: порты $MITA_PORT_RANGE/$MITA_PROTOCOL открыты" +elif command -v iptables &>/dev/null; then + if [[ "$MITA_PORT_RANGE" == *"-"* ]]; then + START_PORT="${MITA_PORT_RANGE%-*}"; END_PORT="${MITA_PORT_RANGE#*-}" + iptables -I INPUT -p "${MITA_PROTOCOL,,}" --dport "$START_PORT:$END_PORT" -j ACCEPT 2>/dev/null || true + else + iptables -I INPUT -p "${MITA_PROTOCOL,,}" --dport "$MITA_PORT_RANGE" -j ACCEPT 2>/dev/null || true + fi + ok "iptables: порты открыты" +fi + +# ============================================================================= +# ИТОГОВЫЙ ОТЧЁТ +# ============================================================================= +section "Установка завершена!" + +SERVER_IP=$(curl -s --max-time 5 ifconfig.me 2>/dev/null || hostname -I | awk '{print $1}') + +echo "" +echo -e "${GREEN}╔══════════════════════════════════════════════════════╗" +echo -e "║ УСТАНОВКА ЗАВЕРШЕНА УСПЕШНО ║" +echo -e "╚══════════════════════════════════════════════════════╝${NC}" +echo "" +echo -e "${CYAN}Параметры сервера для клиентов:${NC}" +echo -e " IP сервера: ${YELLOW}$SERVER_IP${NC}" +echo -e " Порты mita: ${YELLOW}$MITA_PORT_RANGE ($MITA_PROTOCOL)${NC}" +echo "" +echo -e "${CYAN}Пользователи:${NC}" +for entry in "${MITA_USERS[@]}"; do + name="${entry%%:*}"; pass="${entry##*:}" + echo -e " ${YELLOW}$name${NC} / $pass" +done +echo "" +echo -e "${CYAN}Файлы конфигурации:${NC}" +echo -e " Конфиг mita: ${YELLOW}$MITA_CONFIG_FILE${NC}" +echo -e " Свои домены → WARP: ${YELLOW}$CUSTOM_DOMAINS_FILE${NC}" +echo -e " Свои IP → WARP: ${YELLOW}$CUSTOM_IPS_FILE${NC}" +echo -e " Авто-источники (dom): ${YELLOW}$AUTO_DOMAINS_FILE${NC}" +echo -e " Авто-источники (IP): ${YELLOW}$AUTO_IPS_FILE${NC}" +echo -e " Лог обновлений: ${YELLOW}/var/log/mita-update.log${NC}" +echo "" +echo -e "${CYAN}Полезные команды:${NC}" +echo -e " ${YELLOW}mita status${NC} — статус сервера" +echo -e " ${YELLOW}mita get users${NC} — список пользователей и их трафик" +echo -e " ${YELLOW}mita describe config${NC} — текущий конфиг" +echo -e " ${YELLOW}$UPDATE_SCRIPT${NC} — обновить списки вручную" +echo -e " ${YELLOW}journalctl -u mita -f${NC} — логи mita в реальном времени" +if $WARP_ENABLED; then + echo -e " ${YELLOW}docker logs cloudflare-warp${NC} — логи WARP" +fi +echo "" +echo -e "${CYAN}Следующий шаг — добавьте нужные домены в:${NC}" +echo -e " ${YELLOW}$CUSTOM_DOMAINS_FILE${NC} — ваши домены" +echo -e " ${YELLOW}$AUTO_DOMAINS_FILE${NC} — geosite/URL источники" +echo -e "Затем запустите: ${YELLOW}$UPDATE_SCRIPT${NC}" +echo "" + +# ============================================================================= +# УСТАНОВКА ВЕБ-ПАНЕЛИ (опционально) +# ============================================================================= +section "Веб-панель управления" + +PANEL_INSTALLER="$SCRIPT_DIR/mita-panel-pkg/install-panel.sh" + +if [[ ! -f "$PANEL_INSTALLER" ]]; then + warn "Файл install-panel.sh не найден рядом со скриптом." + warn "Ожидаемый путь: $PANEL_INSTALLER" + warn "Скачайте архив mita-panel.zip, распакуйте рядом с install.sh и запустите install-panel.sh вручную." +else + echo -e "Установить веб-панель управления mita? (логин/пароль и секретный путь генерируются автоматически)" + echo -e " ${GREEN}y${NC} — да, установить сейчас" + echo -e " ${YELLOW}n${NC} — пропустить (можно установить позже: bash $PANEL_INSTALLER)" + echo "" + read -r -p "Установить панель? [y/N]: " INSTALL_PANEL + echo "" + + if [[ "${INSTALL_PANEL,,}" == "y" || "${INSTALL_PANEL,,}" == "yes" ]]; then + info "Запуск установки веб-панели..." + echo "" + bash "$PANEL_INSTALLER" + else + info "Пропускаем установку панели." + echo -e " Запустить позже: ${YELLOW}bash $PANEL_INSTALLER${NC}" + fi +fi + +# ── Установка mita-ctl как системной команды ────────────────────── +section "Установка утилиты управления mita-ctl" +CTL_SRC="$SCRIPT_DIR/mita-ctl.sh" +if [[ -f "$CTL_SRC" ]]; then + cp "$CTL_SRC" /usr/local/bin/mita-ctl + chmod +x /usr/local/bin/mita-ctl + ok "mita-ctl установлен!" + echo -e " Управление сервером: ${YELLOW}sudo mita-ctl${NC}" +else + warn "mita-ctl.sh не найден рядом с install.sh" + warn "Установите вручную: cp mita-ctl.sh /usr/local/bin/mita-ctl && chmod +x /usr/local/bin/mita-ctl" +fi diff --git a/manage-users.sh b/manage-users.sh new file mode 100755 index 0000000..55efb19 --- /dev/null +++ b/manage-users.sh @@ -0,0 +1,170 @@ +#!/bin/bash +# ============================================================================= +# mita — управление пользователями +# Использование: +# ./manage-users.sh add <имя> — добавить (пароль генерируется автоматически) +# ./manage-users.sh remove <имя> +# ./manage-users.sh list +# ./manage-users.sh quota <имя> <дней> +# ============================================================================= +set -euo pipefail + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' + +CONFIG_FILE="/etc/mita/server_config.json" + +[[ $EUID -ne 0 ]] && { echo -e "${RED}Запустите от root${NC}"; exit 1; } +[[ ! -f "$CONFIG_FILE" ]] && { echo -e "${RED}Конфиг не найден: $CONFIG_FILE${NC}"; exit 1; } + +# ---------- Генерация пароля ---------- +# 48 случайных байт → base64 → убираем +/= → берём 64 символа +gen_password() { + # Расширенный набор без символов ломающих protobuf (&<>|()[]{}\") + openssl rand -base64 48 | tr -d '/+=' | head -c 48 +} + +# ---------- Получить параметры сервера из конфига ---------- +get_server_info() { + SERVER_IP=$(curl -s --max-time 5 ifconfig.me 2>/dev/null \ + || hostname -I | awk '{print $1}') + + # Читаем первый portBinding из конфига + FIRST_BINDING=$(jq -r '.portBindings[0]' "$CONFIG_FILE") + PORT_PROTOCOL=$(echo "$FIRST_BINDING" | jq -r '.protocol // "TCP"') + + # Может быть portRange или port + if echo "$FIRST_BINDING" | jq -e '.portRange' &>/dev/null; then + PORT_VALUE=$(echo "$FIRST_BINDING" | jq -r '.portRange') + PORT_JSON_KEY="portRange" + else + PORT_VALUE=$(echo "$FIRST_BINDING" | jq -r '.port | tostring') + PORT_JSON_KEY="port" + fi +} + +# ---------- Вывод клиентского конфига ---------- +print_client_config() { + local name="$1" pass="$2" + + # Строим portBindings для клиентского конфига + if [[ "$PORT_JSON_KEY" == "portRange" ]]; then + PORT_BINDING_JSON=$(jq -n \ + --arg r "$PORT_VALUE" --arg p "$PORT_PROTOCOL" \ + '[{portRange: $r, protocol: $p}]') + else + PORT_BINDING_JSON=$(jq -n \ + --argjson port "$PORT_VALUE" --arg p "$PORT_PROTOCOL" \ + '[{port: $port, protocol: $p}]') + fi + + CLIENT_JSON=$(jq -n \ + --arg name "$name" \ + --arg pass "$pass" \ + --arg ip "$SERVER_IP" \ + --argjson pb "$PORT_BINDING_JSON" \ + '{ + profiles: [{ + profileName: "default", + user: {name: $name, password: $pass}, + servers: [{ + ipAddress: $ip, + portBindings: $pb + }] + }], + activeProfile: "default", + rpcPort: 8964, + socks5Port: 1080 + }') + + echo "" + echo -e "${CYAN}╔══════════════════════════════════════════════════════════════╗${NC}" + echo -e "${CYAN}║ КОНФИГ КЛИЕНТА — скопируйте целиком ║${NC}" + echo -e "${CYAN}╚══════════════════════════════════════════════════════════════╝${NC}" + echo "" + echo -e "${BOLD}Файл:${NC} mieru_client_config.json" + echo "" + echo "$CLIENT_JSON" | jq . + echo "" + echo -e "${CYAN}┌──────────────────────────────────────────────────────────────┐${NC}" + echo -e "${CYAN}│ Команды на устройстве клиента: │${NC}" + echo -e "${CYAN}│${NC} mieru apply config mieru_client_config.json ${CYAN}│${NC}" + echo -e "${CYAN}│${NC} mieru start ${CYAN}│${NC}" + echo -e "${CYAN}└──────────────────────────────────────────────────────────────┘${NC}" + echo "" + echo -e "${YELLOW}⚠ Сохраните пароль — он нигде не хранится в открытом виде:${NC}" + echo -e " Пользователь: ${BOLD}$name${NC}" + echo -e " Пароль: ${BOLD}$pass${NC}" + echo "" +} + +cmd="${1:-list}" + +case "$cmd" in + + # ------------------------------------------------------------------ + add) + [[ $# -lt 2 ]] && { echo "Использование: $0 add <имя>"; exit 1; } + NAME="$2" + + # Проверить не существует ли уже + EXISTS=$(jq --arg n "$NAME" '.users[] | select(.name == $n)' "$CONFIG_FILE") + [[ -n "$EXISTS" ]] && { echo -e "${YELLOW}Пользователь '$NAME' уже существует${NC}"; exit 1; } + + # Сгенерировать пароль + PASS=$(gen_password) + + # Добавить пользователя в конфиг + jq --arg n "$NAME" --arg p "$PASS" \ + '.users += [{name: $n, password: $p}]' \ + "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE" + + # Применить конфиг + mita apply config "$CONFIG_FILE" + echo -e "${GREEN}✓ Пользователь '${NAME}' добавлен${NC}" + + # Показать клиентский конфиг + get_server_info + print_client_config "$NAME" "$PASS" + ;; + + # ------------------------------------------------------------------ + remove) + [[ $# -lt 2 ]] && { echo "Использование: $0 remove <имя>"; exit 1; } + NAME="$2" + + jq --arg n "$NAME" 'del(.users[] | select(.name == $n))' \ + "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE" + + mita apply config "$CONFIG_FILE" + echo -e "${GREEN}✓ Пользователь '$NAME' удалён${NC}" + ;; + + # ------------------------------------------------------------------ + list) + echo -e "${CYAN}Пользователи в конфиге:${NC}" + jq -r '.users[] | " \(.name)"' "$CONFIG_FILE" + echo "" + echo -e "${CYAN}Статистика (из mita):${NC}" + mita get users 2>/dev/null || echo " (mita не запущен)" + ;; + + # ------------------------------------------------------------------ + quota) + [[ $# -lt 4 ]] && { echo "Использование: $0 quota <имя> <дней> "; exit 1; } + NAME="$2"; DAYS="$3"; GB="$4" + MB=$(echo "$GB * 1024" | bc | cut -d. -f1) + + jq --arg n "$NAME" --argjson d "$DAYS" --argjson m "$MB" \ + '(.users[] | select(.name == $n)).quotas = [{days: $d, megabytes: $m}]' \ + "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE" + + mita apply config "$CONFIG_FILE" + echo -e "${GREEN}✓ Квота для '$NAME': ${GB}GB / ${DAYS} дней${NC}" + ;; + + *) + echo "Команды: add | remove | list | quota" + exit 1 + ;; +esac diff --git a/mita-ctl.sh b/mita-ctl.sh new file mode 100755 index 0000000..750ff0d --- /dev/null +++ b/mita-ctl.sh @@ -0,0 +1,834 @@ +#!/bin/bash +# ============================================================================= +# mita-ctl — утилита управления mita сервером +# Запуск: sudo bash mita-ctl.sh +# ============================================================================= +set -euo pipefail + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' + +info() { echo -e "${BLUE}[INFO]${NC} $*"; } +ok() { echo -e "${GREEN}[OK]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERR]${NC} $*"; } +section() { echo -e "\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"; echo -e "${CYAN} $*${NC}"; echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"; } +hr() { echo -e "${BLUE}─────────────────────────────────────────────────${NC}"; } +pause() { echo ""; read -r -p "Нажмите Enter для продолжения..." _; } + +[[ $EUID -ne 0 ]] && { echo -e "${RED}Запустите от root: sudo bash mita-ctl.sh${NC}"; exit 1; } + +PANEL_CONFIG="/etc/mita/panel.json" +PANEL_ENV="/etc/mita/panel.env" +MITA_CONFIG="/etc/mita/server_config.json" + +load_panel() { + [[ -f "$PANEL_CONFIG" ]] && cat "$PANEL_CONFIG" || echo "{}" +} +panel_value() { load_panel | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null || echo ""; } +env_value() { [[ -f "$PANEL_ENV" ]] && grep "^$1=" "$PANEL_ENV" | cut -d= -f2- || echo ""; } + +gen_pass() { openssl rand -base64 48 | tr -d '/+=' | head -c 32; } + +# ── SSH tunnel helper ────────────────────────────────────────────────────── +print_ssh_instruction() { + local port="$1" + local secret="$2" + local ssh_port="${3:-22}" + local server_ip + server_ip=$(curl -s --max-time 5 ifconfig.me 2>/dev/null || hostname -I | awk '{print $1}') + echo "" + echo -e "${CYAN}┌─────────────────────────────────────────────────────────┐${NC}" + echo -e "${CYAN}│ Подключение к панели через SSH-туннель │${NC}" + echo -e "${CYAN}└─────────────────────────────────────────────────────────┘${NC}" + echo "" + echo -e " Выполните на ${BOLD}вашем компьютере${NC}:" + echo "" + echo -e " ${YELLOW}ssh -L ${port}:127.0.0.1:${port} -p ${ssh_port} root@${server_ip} -N${NC}" + echo "" + echo -e " Затем откройте в браузере:" + echo -e " ${BOLD}${GREEN}http://127.0.0.1:${port}/${secret}/${NC}" + echo "" + echo -e " ${BLUE}Флаг -N означает что SSH не выполняет команды, только туннель.${NC}" + echo -e " ${BLUE}Для фонового режима добавьте -f:${NC}" + echo -e " ${YELLOW}ssh -fN -L ${port}:127.0.0.1:${port} -p ${ssh_port} root@${server_ip}${NC}" + echo "" +} + +# ════════════════════════════════════════════════════════════════════════════ +# ГЛАВНОЕ МЕНЮ +# ════════════════════════════════════════════════════════════════════════════ +main_menu() { + while true; do + clear + echo -e "${CYAN}" + echo " ╔══════════════════════════════════════════════════════╗" + echo " ║ mita-ctl — Управление сервером ║" + echo " ╚══════════════════════════════════════════════════════╝" + echo -e "${NC}" + echo -e " ${BOLD}1.${NC} Сменить способ подключения к панели" + echo -e " ${BOLD}2.${NC} Управление портами (firewall)" + echo -e " ${BOLD}3.${NC} Перевыпустить SSL-сертификат панели" + echo -e " ${BOLD}4.${NC} Управление пользователями mita" + echo -e " ${BOLD}5.${NC} Показать логин и пароль админа панели" + echo -e " ${BOLD}6.${NC} Изменить данные администратора панели" + echo -e " ${BOLD}7.${NC} Настройка fail2ban" + echo -e " ${BOLD}8.${NC} Рекомендации по безопасности VPS" + echo -e " ${BOLD}0.${NC} Выход" + echo "" + read -r -p " Выберите пункт: " choice + case "$choice" in + 1) menu_panel_access ;; + 2) menu_firewall ;; + 3) menu_ssl ;; + 4) menu_users ;; + 5) menu_show_admin ;; + 6) menu_change_admin ;; + 7) menu_fail2ban ;; + 8) menu_security ;; + 0) exit 0 ;; + *) warn "Неверный выбор" ;; + esac + done +} + +# ════════════════════════════════════════════════════════════════════════════ +# 1. СПОСОБ ПОДКЛЮЧЕНИЯ К ПАНЕЛИ +# ════════════════════════════════════════════════════════════════════════════ +menu_panel_access() { + section "Способ подключения к панели" + local panel_port secret ssh_port + panel_port=$(env_value "PANEL_PORT"); panel_port=${panel_port:-8080} + secret=$(panel_value "secret_path") + + echo -e " Текущий порт панели: ${YELLOW}${panel_port}${NC}" + echo -e " Текущий секретный путь: ${YELLOW}/${secret}/${NC}" + echo "" + echo -e " ${BOLD}1.${NC} SSH-туннель (панель недоступна снаружи)" + echo -e " ${BOLD}2.${NC} По IP-адресу (текущий способ)" + echo -e " ${BOLD}3.${NC} По доменному имени с Let's Encrypt" + echo -e " ${BOLD}0.${NC} Назад" + echo "" + read -r -p " Выбор: " choice + + case "$choice" in + 1) + section "SSH-туннель" + echo "Панель будет слушать только на 127.0.0.1 (недоступна снаружи)." + echo "" + read -r -p "SSH-порт вашего VPS [22]: " ssh_port + ssh_port=${ssh_port:-22} + + # Привязать gunicorn к 127.0.0.1 + _update_panel_bind "127.0.0.1" "$panel_port" + + # Закрыть порт панели в firewall + _fw_close "$panel_port" "tcp" + + # Обновить panel.json + python3 -c " +import json +with open('$PANEL_CONFIG') as f: d=json.load(f) +d['access_mode']='ssh' +d['ssh_port']=$ssh_port +with open('$PANEL_CONFIG','w') as f: json.dump(d,f,indent=2) +" + systemctl restart mita-panel 2>/dev/null || true + ok "Панель переключена на SSH-туннель" + print_ssh_instruction "$panel_port" "$secret" "$ssh_port" + pause + ;; + + 2) + section "Доступ по IP" + local server_ip + server_ip=$(curl -s --max-time 5 ifconfig.me 2>/dev/null || hostname -I | awk '{print $1}') + + _update_panel_bind "0.0.0.0" "$panel_port" + _fw_open "$panel_port" "tcp" + + python3 -c " +import json +with open('$PANEL_CONFIG') as f: d=json.load(f) +d['access_mode']='ip' +with open('$PANEL_CONFIG','w') as f: json.dump(d,f,indent=2) +" + systemctl restart mita-panel 2>/dev/null || true + ok "Панель доступна по IP" + echo "" + echo -e " Адрес: ${BOLD}${GREEN}http://${server_ip}:${panel_port}/${secret}/${NC}" + pause + ;; + + 3) + section "Let's Encrypt" + read -r -p "Домен (например panel.example.com): " domain + read -r -p "Email для Let's Encrypt (Enter = пропустить): " email + [[ -z "$domain" ]] && { warn "Домен не указан"; pause; return; } + + # Установить certbot + command -v certbot &>/dev/null || apt-get install -y -qq certbot + + local le_args=("certbot" "certonly" "--standalone" "--non-interactive" "--agree-tos" "-d" "$domain") + [[ -n "$email" ]] && le_args+=("--email" "$email") || le_args+=("--register-unsafely-without-email") + + info "Получение сертификата Let's Encrypt..." + if "${le_args[@]}"; then + local cert="/etc/letsencrypt/live/${domain}/fullchain.pem" + local key="/etc/letsencrypt/live/${domain}/privkey.pem" + _update_env_val "SSL_CERT" "$cert" + _update_env_val "SSL_KEY" "$key" + _update_panel_bind "0.0.0.0" "$panel_port" + _fw_open "$panel_port" "tcp" + python3 -c " +import json +with open('$PANEL_CONFIG') as f: d=json.load(f) +d.update({'access_mode':'domain','ssl_type':'letsencrypt','ssl_domain':'$domain','ssl_cert':'$cert','ssl_key':'$key'}) +with open('$PANEL_CONFIG','w') as f: json.dump(d,f,indent=2) +" + # Настроить автопродление + (crontab -l 2>/dev/null; echo "0 3 * * * certbot renew --quiet && systemctl restart mita-panel") | sort -u | crontab - + systemctl restart mita-panel 2>/dev/null || true + ok "Сертификат получен" + echo -e " Адрес: ${BOLD}${GREEN}https://${domain}:${panel_port}/${secret}/${NC}" + else + error "Не удалось получить сертификат. Убедитесь что домен указывает на этот сервер и порт 80 открыт." + fi + pause + ;; + 0) return ;; + esac +} + +_update_panel_bind() { + local host="$1" port="$2" + # Обновить ExecStart в systemd unit через враппер start.sh + sed -i "s/--bind [0-9.:]*:[0-9]*/--bind ${host}:${port}/" /opt/mita-panel/start.sh 2>/dev/null || true + # Обновить PANEL_PORT в env + _update_env_val "PANEL_BIND_HOST" "$host" + # Пересоздать start.sh с нужным bind + cat > /opt/mita-panel/start.sh << STARTEOF +#!/bin/bash +set -a; source /etc/mita/panel.env; set +a +SSL_ARGS="" +if [[ -n "\$SSL_CERT" && -n "\$SSL_KEY" && -f "\$SSL_CERT" && -f "\$SSL_KEY" ]]; then + SSL_ARGS="--certfile=\$SSL_CERT --keyfile=\$SSL_KEY" +fi +exec /opt/mita-panel/venv/bin/gunicorn \\ + --bind ${host}:\${PANEL_PORT} \\ + --workers 2 --timeout 120 \\ + --access-logfile /var/log/mita-panel-access.log \\ + --error-logfile /var/log/mita-panel.log \\ + \$SSL_ARGS app:app +STARTEOF + chmod +x /opt/mita-panel/start.sh +} + +_update_env_val() { + local key="$1" val="$2" + if grep -q "^${key}=" "$PANEL_ENV" 2>/dev/null; then + sed -i "s|^${key}=.*|${key}=${val}|" "$PANEL_ENV" + else + echo "${key}=${val}" >> "$PANEL_ENV" + fi +} + +# ════════════════════════════════════════════════════════════════════════════ +# 2. FIREWALL +# ════════════════════════════════════════════════════════════════════════════ +menu_firewall() { + section "Управление портами (Firewall)" + + # Определить активный firewall + local FW="none" + command -v ufw &>/dev/null && ufw status | grep -q "Status: active" && FW="ufw" + [[ "$FW" == "none" ]] && command -v iptables &>/dev/null && FW="iptables" + + info "Активный firewall: $FW" + echo "" + echo -e " ${BOLD}1.${NC} Открыть порт" + echo -e " ${BOLD}2.${NC} Закрыть порт" + echo -e " ${BOLD}3.${NC} Применить рекомендуемые правила (только нужные порты)" + echo -e " ${BOLD}4.${NC} Показать текущие правила" + echo -e " ${BOLD}0.${NC} Назад" + echo "" + read -r -p " Выбор: " choice + + case "$choice" in + 1) + read -r -p "Порт: " p; read -r -p "Протокол [tcp]: " proto; proto=${proto:-tcp} + _fw_open "$p" "$proto" && ok "Порт $p/$proto открыт" + pause ;; + 2) + read -r -p "Порт: " p; read -r -p "Протокол [tcp]: " proto; proto=${proto:-tcp} + _fw_close "$p" "$proto" && ok "Порт $p/$proto закрыт" + pause ;; + 3) + section "Рекомендуемые правила" + local panel_port mita_range + panel_port=$(env_value "PANEL_PORT"); panel_port=${panel_port:-8080} + mita_range=$(python3 -c " +import json +with open('$MITA_CONFIG') as f: d=json.load(f) +b=d.get('portBindings',[{}])[0] +print(b.get('portRange',b.get('port','2100-2110'))) +" 2>/dev/null || echo "2100-2110") + + read -r -p "SSH-порт вашего VPS [22]: " ssh_port + ssh_port=${ssh_port:-22} + + local access_mode + access_mode=$(panel_value "access_mode") + + info "Настраиваю правила:" + info " SSH: $ssh_port/tcp" + info " mita: $mita_range/tcp" + [[ "$access_mode" != "ssh" ]] && info " Панель: $panel_port/tcp" || info " Панель: закрыта (SSH-туннель)" + + if [[ "$FW" == "ufw" ]]; then + ufw --force reset 2>/dev/null + ufw default deny incoming + ufw default allow outgoing + ufw allow "$ssh_port/tcp" comment "SSH" + # mita ports + local start_p end_p + if [[ "$mita_range" == *"-"* ]]; then + start_p="${mita_range%-*}"; end_p="${mita_range#*-}" + ufw allow "${start_p}:${end_p}/tcp" comment "mita" + else + ufw allow "$mita_range/tcp" comment "mita" + fi + [[ "$access_mode" != "ssh" ]] && ufw allow "$panel_port/tcp" comment "mita-panel" + ufw --force enable + else + iptables -F INPUT 2>/dev/null + iptables -P INPUT DROP + iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT + iptables -A INPUT -i lo -j ACCEPT + iptables -A INPUT -p tcp --dport "$ssh_port" -j ACCEPT + if [[ "$mita_range" == *"-"* ]]; then + start_p="${mita_range%-*}"; end_p="${mita_range#*-}" + iptables -A INPUT -p tcp --dport "${start_p}:${end_p}" -j ACCEPT + else + iptables -A INPUT -p tcp --dport "$mita_range" -j ACCEPT + fi + [[ "$access_mode" != "ssh" ]] && iptables -A INPUT -p tcp --dport "$panel_port" -j ACCEPT + command -v iptables-save &>/dev/null && iptables-save > /etc/iptables/rules.v4 2>/dev/null || true + fi + ok "Правила применены" + pause ;; + 4) + echo "" + if [[ "$FW" == "ufw" ]]; then ufw status numbered + else iptables -L INPUT -n --line-numbers; fi + pause ;; + 0) return ;; + esac +} + +_fw_open() { + local port="$1" proto="${2:-tcp}" + command -v ufw &>/dev/null && ufw status | grep -q "Status: active" \ + && ufw allow "$port/$proto" 2>/dev/null || true + iptables -I INPUT -p "$proto" --dport "$port" -j ACCEPT 2>/dev/null || true +} + +_fw_close() { + local port="$1" proto="${2:-tcp}" + command -v ufw &>/dev/null && ufw status | grep -q "Status: active" \ + && ufw delete allow "$port/$proto" 2>/dev/null || true + iptables -D INPUT -p "$proto" --dport "$port" -j ACCEPT 2>/dev/null || true +} + +# ════════════════════════════════════════════════════════════════════════════ +# 3. SSL +# ════════════════════════════════════════════════════════════════════════════ +menu_ssl() { + section "Перевыпуск SSL-сертификата" + echo -e " ${BOLD}1.${NC} Самоподписной (10 лет)" + echo -e " ${BOLD}2.${NC} Let's Encrypt" + echo -e " ${BOLD}0.${NC} Назад" + echo "" + read -r -p " Выбор: " choice + + case "$choice" in + 1) + local cert_dir="/etc/mita/ssl" + mkdir -p "$cert_dir" + openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \ + -keyout "$cert_dir/selfsigned.key" -out "$cert_dir/selfsigned.crt" \ + -subj "/CN=mita-panel/O=mita/C=XX" 2>/dev/null + _update_env_val "SSL_CERT" "$cert_dir/selfsigned.crt" + _update_env_val "SSL_KEY" "$cert_dir/selfsigned.key" + python3 -c " +import json +with open('$PANEL_CONFIG') as f: d=json.load(f) +d.update({'ssl_type':'selfsigned','ssl_cert':'$cert_dir/selfsigned.crt','ssl_key':'$cert_dir/selfsigned.key'}) +with open('$PANEL_CONFIG','w') as f: json.dump(d,f,indent=2) +" + _rebuild_start_sh + systemctl restart mita-panel 2>/dev/null || true + ok "Самоподписной сертификат создан и применён" + pause ;; + 2) + local domain + read -r -p "Домен: " domain + [[ -z "$domain" ]] && { warn "Не указан домен"; pause; return; } + command -v certbot &>/dev/null || apt-get install -y -qq certbot + if certbot renew --cert-name "$domain" --quiet 2>/dev/null || \ + certbot certonly --standalone --non-interactive --agree-tos \ + --register-unsafely-without-email -d "$domain"; then + local cert="/etc/letsencrypt/live/${domain}/fullchain.pem" + local key="/etc/letsencrypt/live/${domain}/privkey.pem" + _update_env_val "SSL_CERT" "$cert" + _update_env_val "SSL_KEY" "$key" + python3 -c " +import json +with open('$PANEL_CONFIG') as f: d=json.load(f) +d.update({'ssl_type':'letsencrypt','ssl_domain':'$domain','ssl_cert':'$cert','ssl_key':'$key'}) +with open('$PANEL_CONFIG','w') as f: json.dump(d,f,indent=2) +" + _rebuild_start_sh + systemctl restart mita-panel 2>/dev/null || true + ok "Сертификат Let's Encrypt выпущен и применён" + else + error "Не удалось выпустить сертификат" + fi + pause ;; + 0) return ;; + esac +} + +_rebuild_start_sh() { + local bind_host + bind_host=$(panel_value "access_mode") + [[ "$bind_host" == "ssh" ]] && bind_host="127.0.0.1" || bind_host="0.0.0.0" + _update_panel_bind "$bind_host" "$(env_value PANEL_PORT)" +} + +# ════════════════════════════════════════════════════════════════════════════ +# 4. ПОЛЬЗОВАТЕЛИ +# ════════════════════════════════════════════════════════════════════════════ +menu_users() { + section "Управление пользователями mita" + + # Показать текущих пользователей + echo -e "${CYAN}Текущие пользователи:${NC}" + python3 -c " +import json +with open('$MITA_CONFIG') as f: d=json.load(f) +users=d.get('users',[]) +if not users: print(' (нет пользователей)') +for u in users: print(f' - {u[\"name\"]}') +" + echo "" + echo -e " ${BOLD}1.${NC} Создать одного пользователя (своё имя)" + echo -e " ${BOLD}2.${NC} Создать несколько с рандомными именами" + echo -e " ${BOLD}3.${NC} Удалить пользователя" + echo -e " ${BOLD}4.${NC} Показать конфиг существующего пользователя" + echo -e " ${BOLD}0.${NC} Назад" + echo "" + read -r -p " Выбор: " choice + + case "$choice" in + 1) + read -r -p "Имя пользователя: " uname + [[ -z "$uname" ]] && { warn "Имя не указано"; pause; return; } + _create_user "$uname" + pause ;; + 2) + read -r -p "Количество пользователей: " cnt + [[ ! "$cnt" =~ ^[0-9]+$ ]] && { warn "Неверное число"; pause; return; } + for i in $(seq 1 "$cnt"); do + local adj noun rnd uname + adj=$(shuf -n1 -e swift brave quiet cool sharp calm bright dark wild free 2>/dev/null || echo "user") + noun=$(shuf -n1 -e fox hawk river storm ember peak orbit tide frost spark 2>/dev/null || echo "node") + rnd=$((RANDOM % 9000 + 1000)) + uname="${adj}_${noun}_${rnd}" + _create_user "$uname" + done + pause ;; + 3) + read -r -p "Имя пользователя для удаления: " uname + python3 << PYEOF +import json, subprocess +with open('$MITA_CONFIG') as f: d=json.load(f) +before=len(d.get('users',[])) +d['users']=[u for u in d.get('users',[]) if u['name']!='$uname'] +after=len(d['users']) +with open('$MITA_CONFIG','w') as f: json.dump(d,f,indent=2) +if before>after: + subprocess.run(['mita','apply','config','$MITA_CONFIG'],capture_output=True) + print(f"Пользователь '$uname' удалён") +else: + print(f"Пользователь '$uname' не найден") +PYEOF + pause ;; + 4) + read -r -p "Имя пользователя: " uname + _show_user_config "$uname" + pause ;; + 0) return ;; + esac +} + +_show_user_config() { + local uname="$1" + python3 << PYEOF +import json +with open('$MITA_CONFIG') as f: d=json.load(f) +user = next((u for u in d.get('users',[]) if u['name']=='$uname'), None) +if not user: + print("Пользователь '$uname' не найден") +else: + import subprocess + server_ip=subprocess.run(['curl','-s','--max-time','5','ifconfig.me'],capture_output=True,text=True).stdout.strip() + cfg=d.get('portBindings',[{}])[0] + port=cfg.get('portRange',str(cfg.get('port','2100-2110'))) + first_port = port.split('-')[0] if '-' in port else port + print(f"\n Параметры для ручного ввода в Karing/sing-box клиентах:") + print(f" server: {server_ip}") + print(f" server port: {first_port}") + print(f" username: {user['name']}") + print(f" password: {user['password']}") + print(f" transport: TCP") + print(f" multiplexing: multiplexing_low") +PYEOF + echo "" + read -r -p "Показать подробную инструкцию по добавлению в Karing? [y/N]: " show_karing + [[ "${show_karing,,}" == "y" ]] && print_karing_instructions +} + +_create_user() { + local uname="$1" + local pass + pass=$(openssl rand -base64 48 | tr -d '/+=' | head -c 48) + + python3 << PYEOF +import json, subprocess +with open('$MITA_CONFIG') as f: d=json.load(f) +if any(u['name']=='$uname' for u in d.get('users',[])): + print(f"Пользователь '$uname' уже существует") +else: + d.setdefault('users',[]).append({'name':'$uname','password':'$pass'}) + with open('$MITA_CONFIG','w') as f: json.dump(d,f,indent=2) + r=subprocess.run(['mita','apply','config','$MITA_CONFIG'],capture_output=True,text=True) + print(f"✓ Создан: $uname") + print(f" Пароль: $pass") + # Показать клиентский конфиг + server_ip=subprocess.run(['curl','-s','--max-time','5','ifconfig.me'],capture_output=True,text=True).stdout.strip() + cfg=d.get('portBindings',[{}])[0] + port=cfg.get('portRange',str(cfg.get('port','2100-2110'))) + proto=cfg.get('protocol','TCP') + client={ + 'profiles':[{'profileName':'default','user':{'name':'$uname','password':'$pass'}, + 'servers':[{'ipAddress':server_ip,'portBindings':[{'portRange':port,'protocol':proto}]}]}], + 'activeProfile':'default','rpcPort':8964,'socks5Port':1080 + } + print(f"\n Клиентский конфиг (mieru CLI):") + print(json.dumps(client,indent=2,ensure_ascii=False)) + + first_port = port.split('-')[0] if '-' in port else port + print(f"\n Параметры для ручного ввода в Karing/sing-box клиентах:") + print(f" server: {server_ip}") + print(f" server port: {first_port}") + print(f" username: $uname") + print(f" password: $pass") + print(f" transport: TCP") + print(f" multiplexing: multiplexing_low") +PYEOF + + echo "" + read -r -p "Показать подробную инструкцию по добавлению в Karing? [y/N]: " show_karing + [[ "${show_karing,,}" == "y" ]] && print_karing_instructions +} + +print_karing_instructions() { + echo "" + echo -e "${CYAN}┌─────────────────────────────────────────────────────────────────┐${NC}" + echo -e "${CYAN}│ Ручное добавление сервера в Karing │${NC}" + echo -e "${CYAN}└─────────────────────────────────────────────────────────────────┘${NC}" + cat << 'KARINGEOF' + + 1. Откройте Karing → добавить профиль + 2. Выберите тип "Custom" и впишите любое имя + 3. Перейдите в раздел "My profiles" + 4. Нажмите "+" в поле только что созданного профиля + 5. В выпадающем списке протоколов выберите "Mieru" + 6. В поле "tag" впишите любое название (например: proxy) + 7. Заполните "server" и "server port" — значения выше (IP и порт) + 8. Заполните "username" и "password" — значения выше + 9. В поле "multiplexing" выберите "multiplexing_low" + 10. В поле "transport" впишите ЗАГЛАВНЫМИ БУКВАМИ: TCP + Затем нажмите ✓ в правом верхнем углу. + Убедитесь что тумблер профиля в "My profiles" зелёный, + затем нажмите кнопку подключения в главном меню Karing. + 11. Diversion Rules (разделение трафика) — направить конкретные + сайты/приложения мимо VPN или через него: + a) Нажмите карандаш (✏) в правом верхнем углу "Diversion Rules" + b) Нажмите "⋮" там же → "+ Add" + c) Введите название правила, прокрутите список вниз, + выберите созданное правило + d) Добавьте нужные адреса/приложения, нажмите ✓ вверху справа + e) Вернитесь в "Diversion Rules", найдите правило внизу списка + f) Нажмите на него → "Direct" (мимо VPN) + или раскройте имя профиля → выберите его (через VPN) + +KARINGEOF + pause +} + +# ════════════════════════════════════════════════════════════════════════════ +# 5. ПОКАЗАТЬ ДАННЫЕ АДМИНА +# ════════════════════════════════════════════════════════════════════════════ +menu_show_admin() { + section "Данные администратора панели" + local user pass secret panel_port access_mode + user=$(panel_value "admin_user") + pass=$(panel_value "admin_pass") + secret=$(panel_value "secret_path") + panel_port=$(env_value "PANEL_PORT"); panel_port=${panel_port:-8080} + access_mode=$(panel_value "access_mode") + local server_ip + server_ip=$(curl -s --max-time 5 ifconfig.me 2>/dev/null || hostname -I | awk '{print $1}') + + echo "" + echo -e " Логин: ${BOLD}${YELLOW}${user}${NC}" + echo -e " Пароль: ${BOLD}${YELLOW}${pass}${NC}" + echo -e " Секретный путь: ${BOLD}/${secret}/${NC}" + echo "" + + if [[ "$access_mode" == "ssh" ]]; then + local ssh_port + ssh_port=$(panel_value "ssh_port"); ssh_port=${ssh_port:-22} + print_ssh_instruction "$panel_port" "$secret" "$ssh_port" + else + local proto="http" + [[ -n "$(panel_value ssl_cert)" ]] && proto="https" + local domain + domain=$(panel_value "ssl_domain") + if [[ -n "$domain" ]]; then + echo -e " Адрес: ${BOLD}${GREEN}${proto}://${domain}:${panel_port}/${secret}/${NC}" + else + echo -e " Адрес: ${BOLD}${GREEN}${proto}://${server_ip}:${panel_port}/${secret}/${NC}" + fi + fi + pause +} + +# ════════════════════════════════════════════════════════════════════════════ +# 6. ИЗМЕНИТЬ ДАННЫЕ АДМИНА +# ════════════════════════════════════════════════════════════════════════════ +menu_change_admin() { + section "Изменение данных администратора" + echo -e " ${BOLD}1.${NC} Сгенерировать новый пароль автоматически" + echo -e " ${BOLD}2.${NC} Задать свой пароль" + echo -e " ${BOLD}0.${NC} Назад" + echo "" + read -r -p " Выбор: " choice + + local new_pass + case "$choice" in + 1) new_pass=$(gen_pass) ;; + 2) + read -r -s -p "Новый пароль (мин. 12 символов): " new_pass; echo "" + [[ ${#new_pass} -lt 12 ]] && { warn "Пароль слишком короткий"; pause; return; } + ;; + 0) return ;; + *) warn "Неверный выбор"; pause; return ;; + esac + + python3 -c " +import json +with open('$PANEL_CONFIG') as f: d=json.load(f) +d['admin_pass']='$new_pass' +with open('$PANEL_CONFIG','w') as f: json.dump(d,f,indent=2) +" + systemctl restart mita-panel 2>/dev/null || true + ok "Пароль изменён" + echo "" + echo -e " Новый пароль: ${BOLD}${YELLOW}${new_pass}${NC}" + echo -e " Логин: ${BOLD}$(panel_value admin_user)${NC}" + pause +} + +# ════════════════════════════════════════════════════════════════════════════ +# 7. БЕЗОПАСНОСТЬ VPS +# ════════════════════════════════════════════════════════════════════════════ +menu_security() { + section "Рекомендации по безопасности VPS" + echo "" + cat << 'SECEOF' + ┌─────────────────────────────────────────────────────────────────┐ + │ КРИТИЧЕСКИ ВАЖНО │ + └─────────────────────────────────────────────────────────────────┘ + + 1. ВХОД ПО SSH-КЛЮЧУ (отключить пароли) + # Сгенерируйте ключ на своём компьютере: + ssh-keygen -t ed25519 -C "vps-key" + # Скопируйте на сервер: + ssh-copy-id -p 22 root@YOUR_IP + # Отключите вход по паролю (/etc/ssh/sshd_config): + PasswordAuthentication no + PubkeyAuthentication yes + systemctl restart sshd + + 2. СМЕНИТЕ ПОРТ SSH (заменить 22 на нестандартный) + # В /etc/ssh/sshd_config: + Port 2222 # любой свободный порт + systemctl restart sshd + # Не забудьте открыть новый порт в firewall! + + 3. FAIL2BAN (блокировка перебора паролей) + apt-get install -y fail2ban + systemctl enable fail2ban --now + + ┌─────────────────────────────────────────────────────────────────┐ + │ ВАЖНО │ + └─────────────────────────────────────────────────────────────────┘ + + 4. АВТООБНОВЛЕНИЕ БЕЗОПАСНОСТИ + apt-get install -y unattended-upgrades + dpkg-reconfigure -plow unattended-upgrades + + 5. МИНИМУМ ОТКРЫТЫХ ПОРТОВ + Используйте пункт 2 этого меню → "Рекомендуемые правила". + Открытыми должны быть только: SSH, порты mita, порт панели (если не SSH-туннель). + + 6. ПАНЕЛЬ ЧЕРЕЗ SSH-ТУННЕЛЬ + Самый безопасный вариант — панель вообще не торчит наружу. + Используйте пункт 1 главного меню для переключения. + + 7. СЕКРЕТНЫЙ ПУТЬ ПАНЕЛИ + Никогда не делитесь ссылкой на панель. Без секретного пути + сервер возвращает 404 на все запросы. + + ┌─────────────────────────────────────────────────────────────────┐ + │ ДОПОЛНИТЕЛЬНО │ + └─────────────────────────────────────────────────────────────────┘ + + 8. МОНИТОРИНГ ВХОДОВ + last # последние входы + journalctl -u ssh --since "1 hour ago" + grep "Failed" /var/log/auth.log | tail -20 + + 9. ПРОВЕРКА ОТКРЫТЫХ ПОРТОВ + ss -tlnp # что слушает на сервере + nmap -p- YOUR_IP # что видно снаружи (запускать со своего ПК) + + 10. РЕГУЛЯРНЫЕ ОБНОВЛЕНИЯ + apt-get update && apt-get upgrade -y + +SECEOF + pause +} + +# ════════════════════════════════════════════════════════════════════════════ +# 8. FAIL2BAN +# ════════════════════════════════════════════════════════════════════════════ +menu_fail2ban() { + section "Настройка fail2ban для панели" + + local installed=false active=false + command -v fail2ban-client &>/dev/null && installed=true + $installed && systemctl is-active --quiet fail2ban && active=true + + echo -e " fail2ban установлен: $($installed && echo "${GREEN}да${NC}" || echo "${RED}нет${NC}")" + $installed && echo -e " fail2ban активен: $($active && echo "${GREEN}да${NC}" || echo "${RED}нет${NC}")" + echo "" + + local pc_maxretry pc_bantime + pc_maxretry=$(python3 -c "import json; d=json.load(open('$PANEL_CONFIG')); print(d.get('login_max_attempts',5))" 2>/dev/null || echo 5) + pc_bantime=$(python3 -c "import json; d=json.load(open('$PANEL_CONFIG')); print(d.get('login_ban_seconds',3600))" 2>/dev/null || echo 3600) + echo -e " Текущие лимиты: ${YELLOW}${pc_maxretry} попыток${NC} / ${YELLOW}${pc_bantime} сек${NC}" + echo "" + echo -e " ${BOLD}1.${NC} Строгий — 3 попытки / 30 минут" + echo -e " ${BOLD}2.${NC} Стандарт — 5 попыток / 1 час" + echo -e " ${BOLD}3.${NC} Мягкий — 10 попыток / 15 минут" + echo -e " ${BOLD}4.${NC} Жёсткий — 3 попытки / 24 часа" + echo -e " ${BOLD}5.${NC} Ручная настройка" + echo -e " ${BOLD}6.${NC} Установить/переустановить fail2ban" + echo -e " ${BOLD}0.${NC} Назад" + echo "" + read -r -p " Выбор: " choice + + local max_retry ban_time + case "$choice" in + 1) max_retry=3; ban_time=1800 ;; + 2) max_retry=5; ban_time=3600 ;; + 3) max_retry=10; ban_time=900 ;; + 4) max_retry=3; ban_time=86400 ;; + 5) + read -r -p " Попыток до блокировки [5]: " max_retry + max_retry=${max_retry:-5} + echo " Время блокировки:" + echo " 1) 5 минут 2) 15 минут 3) 30 минут" + echo " 4) 1 час 5) 24 часа 6) 7 дней 7) Своё (сек)" + read -r -p " Выбор [4]: " bt_choice + case "${bt_choice:-4}" in + 1) ban_time=300 ;; 2) ban_time=900 ;; 3) ban_time=1800 ;; + 4) ban_time=3600 ;; 5) ban_time=86400 ;; 6) ban_time=604800;; + 7) read -r -p " Секунды: " ban_time ;; + *) ban_time=3600 ;; + esac + ;; + 6) + info "Установка fail2ban..." + apt-get install -y -qq fail2ban + systemctl enable fail2ban --now + ok "fail2ban установлен и запущен" + pause; return ;; + 0) return ;; + *) warn "Неверный выбор"; pause; return ;; + esac + + # Применить + _apply_fail2ban "$max_retry" "$ban_time" + pause +} + +_apply_fail2ban() { + local max_retry="$1" ban_time="$2" + + # Обновить panel.json + python3 -c " +import json +with open('$PANEL_CONFIG') as f: d=json.load(f) +d['login_max_attempts']=$max_retry +d['login_ban_seconds']=$ban_time +with open('$PANEL_CONFIG','w') as f: json.dump(d,f,indent=2) +" + + # Записать fail2ban конфиг + if command -v fail2ban-client &>/dev/null; then + mkdir -p /etc/fail2ban/filter.d /etc/fail2ban/jail.d + cat > /etc/fail2ban/filter.d/mita-panel.conf << EOF +[Definition] +failregex = ^.*"POST /[^"]+/login[^"]*" 4(01|29).*$ +ignoreregex = +EOF + cat > /etc/fail2ban/jail.d/mita-panel.conf << EOF +[mita-panel] +enabled = true +filter = mita-panel +backend = auto +logpath = /var/log/mita-panel-access.log +maxretry = $max_retry +bantime = $ban_time +findtime = $ban_time +action = iptables-multiport[name=mita-panel, port="http,https,8080,8443", protocol=tcp] +EOF + systemctl restart fail2ban 2>/dev/null || true + ok "fail2ban настроен: $max_retry попыток / ${ban_time}с" + else + warn "fail2ban не установлен — лимиты сохранены только в panel.json" + fi + + # Перезапустить панель чтобы подхватила новые лимиты + systemctl restart mita-panel 2>/dev/null || true + ok "Настройки применены. Попыток: $max_retry, бан: ${ban_time}с" +} + +# ── Точка входа ─────────────────────────────────────────────────────────── +main_menu + diff --git a/mswp.zip b/mswp.zip deleted file mode 100644 index 4f77073..0000000 Binary files a/mswp.zip and /dev/null differ diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..0e5b11f --- /dev/null +++ b/templates/base.html @@ -0,0 +1,188 @@ + + + + + +{% block title %}mita panel{% endblock %} + + + + +
{% block content %}{% endblock %}
+ + + +{% block scripts %}{% endblock %} + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..c84793f --- /dev/null +++ b/templates/index.html @@ -0,0 +1,750 @@ +{% extends "base.html" %} +{% block title %}mita panel{% endblock %} +{% block content %} + + +
+

Дашборд

+

Обзор сервера в реальном времени

+
+
IP сервера
+
+
IP WARP
+
+
+
+
Трафик за неделю
ГБ
+
Трафик за месяц
ГБ
+
+
+
+
Порты mita
+
+
+
+
Пользователей
+
+
+
+
Статус mita
+
+
+
+
+
Быстрые действия
+
+ + + + +
+
+ + + +
+ + +
+
+

Пользователи

Создание и управление доступом

+
+ +
+
Создать пользователей
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+ +
+
Список пользователей
+
+ + + +
ИмяWARP вклПравила WARPКонфиг клиентаДействия
Загрузка...
+
+
+
+ + +
+

SSL-сертификаты

+

Защита панели управления

+ +
+
Текущий сертификат
+
Загрузка...
+
+ +
+
+
Самоподписной сертификат
+

+ Генерируется мгновенно, действует 10 лет. + Браузер покажет предупреждение — его можно принять. + Подходит для личного использования. +

+ +
+
+
+
Let's Encrypt (бесплатный)
+

+ Доверенный сертификат на 90 дней, продлевается автоматически. + Требует домен, указывающий на этот сервер, и открытый порт 80. +

+
+ + +
+
+ + +
+ +
+
+
+ +
+
Применение сертификата
+
+ После смены сертификата необходимо перезапустить панель:
+ systemctl restart mita-panel +
+
+
+ +{% endblock %} + +
+

Безопасность

+

Защита панели и настройка fail2ban

+
+
Статус fail2ban
+
Загрузка...
+
+
+
Настройка лимита попыток входа
+

+ Лимит работает на двух уровнях: встроенный счётчик панели (мгновенно) и fail2ban (блокировка на сетевом уровне). +

+
+
Быстрые варианты
+
+ + + + +
+
+
+
Ручная настройка
+
+
+ + +
+
+ + +
+ +
+
+ + Установить fail2ban если не установлен +
+ +
+
+
+ + + +{% block scripts %} + +{% endblock %} diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..cd03a7c --- /dev/null +++ b/templates/login.html @@ -0,0 +1,93 @@ + + + + +mita panel — вход + + + +
+ +
+
+ + +
+
+ + +
+ +
+ +
+
+ + +