From 63c252825a3fcf5290a99048edd9ff76c70487bc Mon Sep 17 00:00:00 2001 From: grendervill Date: Sat, 27 Jun 2026 00:37:32 +0300 Subject: [PATCH] =?UTF-8?q?version=201.3.1:=20=D0=B1=D0=B5=D0=B7=D0=BE?= =?UTF-8?q?=D0=BF=D0=B0=D1=81=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=B8=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D0=B8=20=D0=B2=20=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bot.py: кнопка «Безопасность» (fail2ban статус, пресеты, установка), убрана «Статистика» - bot.py: кнопки «📋 имя» и «📱 Karing» у каждого пользователя — JSON-конфиг и поля для ручного ввода - bot.py: конфиг/Karing в блоках кода для копирования - README: обновлена дорожная карта --- README.md | 4 +- bot.py | 234 ++++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 212 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index b9183a6..949398d 100644 --- a/README.md +++ b/README.md @@ -92,9 +92,9 @@ curl -Ls https://raw.githubusercontent.com/grendervilll/mitactl/main/bootstrap.s - [x] Защита сессий: динамический Secure cookie, in-memory rate limiter - [x] Bootstrap-установщик (одна команда) - [x] Мониторинг сервера: CPU, RAM, диск на дашборде (автообновление каждые 2 сек) -- [x] Telegram-бот: inline-кнопки, создание/удаление/WARP/дашборд/системная статистика +- [x] Telegram-бот: inline-кнопки, создание/удаление/WARP/дашборд, конфиг/Karing, fail2ban-безопасность - [x] Установка бота: веб-панель, CLI, полный установщик; автоопределение Telegram ID через API -- [x] Установка бота через pip в venv (обход externally-managed на новых Debian/Ubuntu) +- [x] Бот в venv (обход externally-managed), авто-SSL и SSH-туннель через веб-панель ### ⬜ Запланировано diff --git a/bot.py b/bot.py index 7b9ad9e..565a1d2 100644 --- a/bot.py +++ b/bot.py @@ -116,7 +116,7 @@ MAIN_KEYBOARD = InlineKeyboardMarkup([ [InlineKeyboardButton("➕ Создать", callback_data="create"), InlineKeyboardButton("🗑 Удалить", callback_data="delete_menu")], [InlineKeyboardButton("🔄 WARP", callback_data="warp_menu"), - InlineKeyboardButton("📊 Статистика", callback_data="stats")], + InlineKeyboardButton("🛡 Безопасность", callback_data="security")], [InlineKeyboardButton("ℹ️ Помощь", callback_data="help")], ]) @@ -188,6 +188,7 @@ async def show_users(update: Update, context: ContextTypes.DEFAULT_TYPE): continue lines = ["👥 *Пользователи*\n"] + user_buttons = [] for u in users: name = u["name"] s = stats_map.get(name, {}) @@ -198,15 +199,21 @@ async def show_users(update: Update, context: ContextTypes.DEFAULT_TYPE): mon = f"{s.get('month_mb', 0):.1f} МБ" if s.get("month_mb") else "—" ename = escape_md(name) lines.append(f"{o} {ename} {w} день: {day} нед: {wk} мес: {mon}") + user_buttons.append([ + InlineKeyboardButton(f"📋 {name}", callback_data=f"config_{name}"), + InlineKeyboardButton(f"📱 Karing", callback_data=f"karing_{name}"), + ]) + + user_buttons.extend([ + [InlineKeyboardButton("➕ Создать", callback_data="create"), + InlineKeyboardButton("🗑 Удалить", callback_data="delete_menu")], + [InlineKeyboardButton("🔄 WARP", callback_data="warp_menu")], + [InlineKeyboardButton("« Назад", callback_data="main")], + ]) await query.edit_message_text( "\n".join(lines), - reply_markup=InlineKeyboardMarkup([ - [InlineKeyboardButton("➕ Создать", callback_data="create"), - InlineKeyboardButton("🗑 Удалить", callback_data="delete_menu")], - [InlineKeyboardButton("🔄 WARP", callback_data="warp_menu")], - [InlineKeyboardButton("« Назад", callback_data="main")], - ]), + reply_markup=InlineKeyboardMarkup(user_buttons), parse_mode="MarkdownV2" ) @@ -333,30 +340,203 @@ async def toggle_warp(update: Update, context: ContextTypes.DEFAULT_TYPE): parse_mode="MarkdownV2" ) -# ── stats ───────────────────────────────────────────────────────────────────── +def _build_client_config(name, password): + cfg = load_mita_config() + bindings = cfg.get("portBindings", [{}])[0] + proto = bindings.get("protocol", "TCP") + port_range = bindings.get("portRange", str(bindings.get("port", "?"))) + ip = get_server_ip() + return { + "profiles": [{"profileName": "default", + "user": {"name": name, "password": password}, + "servers": [{"ipAddress": ip, + "portBindings": [{"portRange": port_range, + "protocol": proto}]}]}], + "activeProfile": "default", + "rpcPort": 8964, + "socks5Port": 1080, + } + +# ── config / Karing ────────────────────────────────────────────────────────── @admin_only -async def show_stats(update: Update, context: ContextTypes.DEFAULT_TYPE): +async def show_config(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() - cpu = psutil.cpu_percent(interval=0.5) - mem = psutil.virtual_memory() - disk = psutil.disk_usage("/") - boot = datetime.fromtimestamp(psutil.boot_time()).strftime("%d.%m.%Y %H:%M") + name = query.data[len("config_"):] + cfg = load_mita_config() + user = next((u for u in cfg.get("users", []) if u["name"] == name), None) + if not user: + await query.answer("Пользователь не найден") + return + + data = json.dumps(_build_client_config(name, user["password"]), indent=2, ensure_ascii=False) + await query.edit_message_text( + f"📋 Конфиг `{escape_md(name)}`:\n```json\n{data[:3500]}\n```", + reply_markup=InlineKeyboardMarkup([ + [InlineKeyboardButton("« К пользователям", callback_data="users")], + [InlineKeyboardButton("📱 Karing", callback_data=f"karing_{name}")], + [InlineKeyboardButton("« На главную", callback_data="main")], + ]), + parse_mode="MarkdownV2" + ) + +@admin_only +async def show_karing(update: Update, context: ContextTypes.DEFAULT_TYPE): + query = update.callback_query + await query.answer() + name = query.data[len("karing_"):] + cfg = load_mita_config() + user = next((u for u in cfg.get("users", []) if u["name"] == name), None) + if not user: + await query.answer("Пользователь не найден") + return + + data = _build_client_config(name, user["password"]) + ip = data["profiles"][0]["servers"][0]["ipAddress"] + pb = data["profiles"][0]["servers"][0]["portBindings"][0] + port = (pb["portRange"] or str(pb.get("port", "?"))).split("-")[0] + + text = ( + f"📱 *Ручная настройка Karing* для `{escape_md(name)}`\n\n" + f"Поля для ввода в Karing:\n\n" + f"server:\n```\n{ip}\n```\n" + f"server port:\n```\n{port}\n```\n" + f"username:\n```\n{name}\n```\n" + f"password:\n```\n{user['password']}\n```\n" + f"transport:\n```\nTCP\n```\n" + f"multiplexing: *multiplexing\\_low*" + ) await query.edit_message_text( - f"📊 *Сервер*\n\n" - f"CPU: `{cpu:.1f}%`\n" - f"RAM: `{mem.percent:.1f}%` \\({fmt_bytes(mem.used)} / {fmt_bytes(mem.total)}\\)\n" - f"Диск: `{disk.percent:.1f}%` \\({fmt_bytes(disk.used)} / {fmt_bytes(disk.total)}\\)\n" - f"Аптайм: с `{boot}`\n" - f"mita: {'🟢 работает' if _mita_running() else '🔴 остановлен'}", + text, reply_markup=InlineKeyboardMarkup([ - [InlineKeyboardButton("🔄 Обновить", callback_data="stats")], + [InlineKeyboardButton("📋 JSON-конфиг", callback_data=f"config_{name}")], + [InlineKeyboardButton("« К пользователям", callback_data="users")], + [InlineKeyboardButton("« На главную", callback_data="main")], + ]), + parse_mode="MarkdownV2" + ) + +# ── security ───────────────────────────────────────────────────────────────── +@admin_only +async def show_security(update: Update, context: ContextTypes.DEFAULT_TYPE): + query = update.callback_query + await query.answer() + + installed = subprocess.run(["which","fail2ban-client"], capture_output=True).returncode == 0 + active = jail_active = False + 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 + + pc = load_panel_config() + max_retry = pc.get("login_max_attempts", 5) + ban_time = pc.get("login_ban_seconds", 3600) + + def fmt_sec(s): + if s >= 86400: return f"{s//86400} дн" + if s >= 3600: return f"{s//3600} ч" + if s >= 60: return f"{s//60} мин" + return f"{s} сек" + + text = ( + "🛡 *Безопасность*\n\n" + f"fail2ban: {'✅ установлен' if installed else '❌ не установлен'}\n" + f"Статус: {'🟢 активен' if active else '🔴 остановлен'}\n" + f"Jail mita\\-panel: {'🟢 активен' if jail_active else '⚪ не настроен'}\n\n" + f"Лимиты входа:\n" + f"• попыток: *{max_retry}*\n" + f"• бан: *{fmt_sec(ban_time)}*\n" + f"• встроенный лимит: {'✅' if pc.get('login_max_attempts') else '⚪'}\n\n" + "*Применить пресет:*" + ) + + await query.edit_message_text( + text, + reply_markup=InlineKeyboardMarkup([ + [InlineKeyboardButton("Строгий 3/30m", callback_data="f2b_3_1800"), + InlineKeyboardButton("Стандарт 5/1h", callback_data="f2b_5_3600")], + [InlineKeyboardButton("Мягкий 10/15m", callback_data="f2b_10_900"), + InlineKeyboardButton("Жёсткий 3/24h", callback_data="f2b_3_86400")], + [InlineKeyboardButton("Установить fail2ban", callback_data="f2b_install")], [InlineKeyboardButton("« Назад", callback_data="main")], ]), parse_mode="MarkdownV2" ) +@admin_only +async def apply_f2b_preset(update: Update, context: ContextTypes.DEFAULT_TYPE): + query = update.callback_query + await query.answer() + parts = query.data.split("_") + if len(parts) >= 3 and parts[1] == "install": + r = subprocess.run(["apt-get","install","-y","-qq","fail2ban"], capture_output=True, text=True) + if r.returncode != 0: + await query.edit_message_text( + f"❌ Ошибка установки fail2ban: {r.stderr.strip()[:500]}", + reply_markup=back_button(), + parse_mode="MarkdownV2" + ) + return + await query.edit_message_text( + "✅ fail2ban установлен. Настройте лимиты через пресеты.", + reply_markup=InlineKeyboardMarkup([ + [InlineKeyboardButton("« К безопасности", callback_data="security")], + ]), + parse_mode="MarkdownV2" + ) + return + + if len(parts) < 4: + return + max_retry = int(parts[2]) + ban_time = int(parts[3]) + + 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)) + + filter_content = f"""[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} +""" + 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(["systemctl","restart","fail2ban"], capture_output=True) + except Exception: + pass + + def fmt_sec(s): + if s >= 86400: return f"{s//86400} дн" + if s >= 3600: return f"{s//3600} ч" + if s >= 60: return f"{s//60} мин" + return f"{s} сек" + + await query.edit_message_text( + f"🛡 *Пресет применён*\n\n" + f"Попыток: *{max_retry}*\nБан: *{fmt_sec(ban_time)}*", + reply_markup=InlineKeyboardMarkup([ + [InlineKeyboardButton("« К безопасности", callback_data="security")], + [InlineKeyboardButton("« На главную", callback_data="main")], + ]), + parse_mode="MarkdownV2" + ) + # ── dashboard ───────────────────────────────────────────────────────────────── @admin_only async def show_dashboard(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -423,11 +603,11 @@ async def show_help(update: Update, context: ContextTypes.DEFAULT_TYPE): await query.edit_message_text( "📋 *Команды*\n\n" "Используйте кнопки меню:\n" - "👥 Пользователи — список с трафиком\n" + "👥 Пользователи — список с трафиком, конфиг, Karing\n" "➕ Создать — создать пользователя\n" "🗑 Удалить — выбрать и удалить\n" "🔄 WARP — включить/выключить\n" - "📊 Статистика — CPU, RAM, диск\n" + "🛡 Безопасность — fail2ban, лимиты\n" "📊 Дашборд — полный обзор", reply_markup=back_button(), parse_mode="MarkdownV2" @@ -453,8 +633,14 @@ async def router(update: Update, context: ContextTypes.DEFAULT_TYPE): return await warp_menu(update, context) elif data.startswith("warp_"): return await toggle_warp(update, context) - elif data == "stats": - return await show_stats(update, context) + elif data == "security": + return await show_security(update, context) + elif data.startswith("config_"): + return await show_config(update, context) + elif data.startswith("karing_"): + return await show_karing(update, context) + elif data.startswith("f2b_"): + return await apply_f2b_preset(update, context) elif data == "dashboard": return await show_dashboard(update, context) elif data == "help":