version 1.3.1: безопасность и конфиги в боте
- bot.py: кнопка «Безопасность» (fail2ban статус, пресеты, установка), убрана «Статистика» - bot.py: кнопки «📋 имя» и «📱 Karing» у каждого пользователя — JSON-конфиг и поля для ручного ввода - bot.py: конфиг/Karing в блоках кода для копирования - README: обновлена дорожная карта
This commit is contained in:
@@ -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-туннель через веб-панель
|
||||||
|
|
||||||
### ⬜ Запланировано
|
### ⬜ Запланировано
|
||||||
|
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
Reference in New Issue
Block a user