version 1.3.1: безопасность и конфиги в боте

- bot.py: кнопка «Безопасность» (fail2ban статус, пресеты, установка), убрана «Статистика»
- bot.py: кнопки «📋 имя» и «📱 Karing» у каждого пользователя — JSON-конфиг и поля для ручного ввода
- bot.py: конфиг/Karing в блоках кода для копирования
- README: обновлена дорожная карта
This commit is contained in:
2026-06-27 00:37:32 +03:00
parent 0ea4b0d69e
commit 63c252825a
2 changed files with 212 additions and 26 deletions
+2 -2
View File
@@ -92,9 +92,9 @@ curl -Ls https://raw.githubusercontent.com/grendervilll/mitactl/main/bootstrap.s
- [x] Защита сессий: динамический Secure cookie, in-memory rate limiter - [x] Защита сессий: динамический Secure cookie, in-memory rate limiter
- [x] Bootstrap-установщик (одна команда) - [x] Bootstrap-установщик (одна команда)
- [x] Мониторинг сервера: CPU, RAM, диск на дашборде (автообновление каждые 2 сек) - [x] Мониторинг сервера: CPU, RAM, диск на дашборде (автообновление каждые 2 сек)
- [x] Telegram-бот: inline-кнопки, создание/удаление/WARP/дашборд/системная статистика - [x] Telegram-бот: inline-кнопки, создание/удаление/WARP/дашборд, конфиг/Karing, fail2ban-безопасность
- [x] Установка бота: веб-панель, CLI, полный установщик; автоопределение Telegram ID через API - [x] Установка бота: веб-панель, CLI, полный установщик; автоопределение Telegram ID через API
- [x] Установка бота через pip в venv (обход externally-managed на новых Debian/Ubuntu) - [x] Бот в venv (обход externally-managed), авто-SSL и SSH-туннель через веб-панель
### ⬜ Запланировано ### ⬜ Запланировано
+210 -24
View File
@@ -116,7 +116,7 @@ MAIN_KEYBOARD = InlineKeyboardMarkup([
[InlineKeyboardButton(" Создать", callback_data="create"), [InlineKeyboardButton(" Создать", callback_data="create"),
InlineKeyboardButton("🗑 Удалить", callback_data="delete_menu")], InlineKeyboardButton("🗑 Удалить", callback_data="delete_menu")],
[InlineKeyboardButton("🔄 WARP", callback_data="warp_menu"), [InlineKeyboardButton("🔄 WARP", callback_data="warp_menu"),
InlineKeyboardButton("📊 Статистика", callback_data="stats")], InlineKeyboardButton("🛡 Безопасность", callback_data="security")],
[InlineKeyboardButton("️ Помощь", callback_data="help")], [InlineKeyboardButton("️ Помощь", callback_data="help")],
]) ])
@@ -188,6 +188,7 @@ async def show_users(update: Update, context: ContextTypes.DEFAULT_TYPE):
continue continue
lines = ["👥 *Пользователи*\n"] lines = ["👥 *Пользователи*\n"]
user_buttons = []
for u in users: for u in users:
name = u["name"] name = u["name"]
s = stats_map.get(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 "" mon = f"{s.get('month_mb', 0):.1f} МБ" if s.get("month_mb") else ""
ename = escape_md(name) ename = escape_md(name)
lines.append(f"{o} {ename} {w} день: {day} нед: {wk} мес: {mon}") 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( await query.edit_message_text(
"\n".join(lines), "\n".join(lines),
reply_markup=InlineKeyboardMarkup([ reply_markup=InlineKeyboardMarkup(user_buttons),
[InlineKeyboardButton(" Создать", callback_data="create"),
InlineKeyboardButton("🗑 Удалить", callback_data="delete_menu")],
[InlineKeyboardButton("🔄 WARP", callback_data="warp_menu")],
[InlineKeyboardButton("« Назад", callback_data="main")],
]),
parse_mode="MarkdownV2" parse_mode="MarkdownV2"
) )
@@ -333,30 +340,203 @@ async def toggle_warp(update: Update, context: ContextTypes.DEFAULT_TYPE):
parse_mode="MarkdownV2" 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 @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 query = update.callback_query
await query.answer() await query.answer()
cpu = psutil.cpu_percent(interval=0.5) name = query.data[len("config_"):]
mem = psutil.virtual_memory() cfg = load_mita_config()
disk = psutil.disk_usage("/") user = next((u for u in cfg.get("users", []) if u["name"] == name), None)
boot = datetime.fromtimestamp(psutil.boot_time()).strftime("%d.%m.%Y %H:%M") 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( await query.edit_message_text(
f"📊 *Сервер*\n\n" text,
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 '🔴 остановлен'}",
reply_markup=InlineKeyboardMarkup([ 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")], [InlineKeyboardButton("« Назад", callback_data="main")],
]), ]),
parse_mode="MarkdownV2" 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 = ^<HOST> .+ "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 ───────────────────────────────────────────────────────────────── # ── dashboard ─────────────────────────────────────────────────────────────────
@admin_only @admin_only
async def show_dashboard(update: Update, context: ContextTypes.DEFAULT_TYPE): 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( await query.edit_message_text(
"📋 *Команды*\n\n" "📋 *Команды*\n\n"
"Используйте кнопки меню:\n" "Используйте кнопки меню:\n"
"👥 Пользователи — список с трафиком\n" "👥 Пользователи — список с трафиком, конфиг, Karing\n"
"➕ Создать — создать пользователя\n" "➕ Создать — создать пользователя\n"
"🗑 Удалить — выбрать и удалить\n" "🗑 Удалить — выбрать и удалить\n"
"🔄 WARP — включить/выключить\n" "🔄 WARP — включить/выключить\n"
"📊 Статистика — CPU, RAM, диск\n" "🛡 Безопасность — fail2ban, лимиты\n"
"📊 Дашборд — полный обзор", "📊 Дашборд — полный обзор",
reply_markup=back_button(), reply_markup=back_button(),
parse_mode="MarkdownV2" parse_mode="MarkdownV2"
@@ -453,8 +633,14 @@ async def router(update: Update, context: ContextTypes.DEFAULT_TYPE):
return await warp_menu(update, context) return await warp_menu(update, context)
elif data.startswith("warp_"): elif data.startswith("warp_"):
return await toggle_warp(update, context) return await toggle_warp(update, context)
elif data == "stats": elif data == "security":
return await show_stats(update, context) 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": elif data == "dashboard":
return await show_dashboard(update, context) return await show_dashboard(update, context)
elif data == "help": elif data == "help":