diff --git a/DOCS_AI.md b/DOCS_AI.md index b862b98..3460874 100644 --- a/DOCS_AI.md +++ b/DOCS_AI.md @@ -1,6 +1,6 @@ # GoTelegram Pro — техническая документация для ИИ-агентов -**Версия:** 2.4.3 +**Версия:** 2.5.0 **Репозиторий:** `anten-ka/gotelegram_pro` **Активная ветка:** `alfa-test` (ветка `test` заморожена и содержит stable для конечных пользователей) **Целевая ОС:** Ubuntu 20.04+, Debian 11+ @@ -391,7 +391,7 @@ Fallback: если по известным правилам index.html не на Коды ошибок: `missing_arg`, `invalid_domain`, `wrong_mode`, `unknown_template`, `download_failed`, `deploy_failed`, `no_secret`, `gen_failed`, `validate_failed`, `restart_failed`, `unknown_action`, `lock_timeout`. -**Сериализация (v2.4.3):** `bot_action_dispatch` оборачивает вызов в `flock -w 30 9 < /var/lock/gotelegram-bot-action.lock`. Это защищает от гонок при: +**Сериализация (v2.4.3+):** `bot_action_dispatch` оборачивает вызов в `flock -w 30 9 < /var/lock/gotelegram-bot-action.lock`. Это защищает от гонок при: 1. Одновременных callback'ах внутри бота (asyncio.Lock уже ловит это, но flock — defense-in-depth). 2. Параллельных CLI-вызовах (бот + ручной SSH, или два бот-процесса — теоретически). @@ -446,7 +446,7 @@ switch_language ru|en 3. Напиши `C:\Temp\push_<описание>.py`: ```python import os, base64, json, urllib.request, ssl - TOKEN = "github_pat_..." + TOKEN = os.environ["GOTELEGRAM_PAT"] REPO = "anten-ka/gotelegram_pro" BRANCH = "alfa-test" API = f"https://api.github.com/repos/{REPO}" @@ -596,6 +596,8 @@ with socket.create_connection(("95.163.176.222", 443), timeout=5) as s: ## 17. Changelog +- **2.5.0 (2026-04-24)** — крупный maintenance pass в ветке `codex`: единая версия `2.5.0` в runtime и документации; удалён дефолтный PAT из `bootstrap.sh` (токен теперь только через `GOTELEGRAM_PAT`); `generate_telemt_toml` добавляет `[server.api]` на `127.0.0.1:9091` и metrics на `127.0.0.1:9090`, что нужно для управления пользователями и статистики; Telegram-бот получил меню `🔑 Keys` для `[access.users]` (добавить/удалить/показать ссылку/runtime info); исправлено чтение traffic CSV в боте (header больше не ломает parsing); бот сам делает `stats_collect` перед показом статистики; `iptables` добавлен в optional deps и stats collector пытается установить его; CLI-смена шаблона теперь обновляет `config.json.template_id`, чтобы бот не показывал первый установленный шаблон; backup/restore версии `1.2` сохраняет bot `.env`, bot lang files, custom templates, templates catalog, stats history и полноценную структуру Let's Encrypt (`live/archive/renewal`) для переезда на новый сервер; добавлен безопасный детект 3x-ui/Xray на 443 и генерируется `/opt/gotelegram/shared-443-3xui.md` с объяснением shared-443 ограничений. +- **2.4.6 (2026-04-10)** — universal `apt_lock_wait` helper: ожидание dpkg/apt lock при unattended-upgrades, исправляет установку nginx/certbot/python на свежих VPS. - **2.4.3 (2026-04-10)** — iter3-фикс: `bot_action_dispatch` оборачивается во `flock -w 30` на `/var/lock/gotelegram-bot-action.lock`. Обнаружена гонка: параллельные `change-lite-domain` получали `"no secret in config"`, потому что один процесс читал `config.json`, пока другой делал `jq ... > tmp && mv`. `util-linux` (содержит `flock`) добавлен в `critical` deps, `check_deps_present` и маппинги `apt_pkg_for_cmd`/`dnf_pkg_for_cmd`. - **2.4.2 (2026-04-10)** — реализация non-interactive `bot_action_*` в install.sh (change-template + change-lite-domain с JSON-ответом). bot.py подключает `run_bot_action()` и делает реальную работу вместо stub'ов. Критфиксы: (a) `safe_edit_message` принимает `disable_web_page_preview` (иначе TypeError в success-пути cb_pro_confirm); (b) чтение/запись `config['template_id']` вместо `config['template']` (save_gotelegram_config всегда писал `template_id`, бот смотрел не туда); (c) `bot_update_config_field` использует shell `date -Iseconds` вместо `jq now|todate` (jq 1.5 совместимость для Debian 10); (d) `asyncio.Lock _BOT_ACTION_LOCK` сериализует callback'и в процессе бота; (e) валидация `_TPL_ID_RE`/`_DOMAIN_RE` до subprocess. Полный аудит и автоустановка зависимостей: `ensure_deps` разделяет critical/optional, дедуплицирует пакеты, re-verify после install; `apt_pkg_for_cmd` и `dnf_pkg_for_cmd` мапят команды на пакеты (dig→dnsutils/bind-utils, xxd→xxd/vim-common, flock→util-linux). `check_deps_present` — быстрый чек без `apt-get update`. - **2.4.1 (2026-04-10)** — баг #23: `start_telemt` делает `restart` если сервис активен (иначе stale in-memory config после переустановки Lite поверх Pro). Полная документация проекта — `DOCS_HUMAN.md` и `DOCS_AI.md` (этот файл). diff --git a/DOCS_HUMAN.md b/DOCS_HUMAN.md index d4ba91c..c982040 100644 --- a/DOCS_HUMAN.md +++ b/DOCS_HUMAN.md @@ -1,6 +1,6 @@ # GoTelegram Pro — руководство пользователя -**Версия:** 2.4.3 +**Версия:** 2.5.0 **Репозиторий:** `anten-ka/gotelegram_pro` **Для кого:** владельцы VPS, которым нужен надёжный MTProxy для Telegram с маскировкой под обычный HTTPS-сайт. @@ -26,6 +26,12 @@ GoTelegram Pro — это готовый менеджер прокси-серв bash <(curl -sL "https://raw.githubusercontent.com/anten-ka/gotelegram_pro/test/bootstrap.sh?token=YOUR_PAT") ``` +Для приватного репозитория безопаснее передавать токен через переменную окружения: + +```bash +GOTELEGRAM_PAT=YOUR_PAT bash <(curl -sL "https://raw.githubusercontent.com/anten-ka/gotelegram_pro/test/bootstrap.sh") +``` + `bootstrap.sh` скачает все файлы из приватного репозитория, создаст симлинк `/usr/local/bin/gotelegram` и запустит главное меню. Через минуту команда `gotelegram` уже будет работать откуда угодно. Дальше в меню: @@ -214,6 +220,8 @@ A: Сам MTProxy — да, это публичная технология из ## Changelog (коротко) +- **2.5.0** — единая версия по коду и документации; удалён дефолтный PAT из `bootstrap.sh`; исправлена статистика в боте (CSV header больше не ломает чтение истории, бот сам обновляет snapshot); CLI-смена шаблона теперь обновляет `config.json.template_id`, поэтому бот показывает текущий шаблон; telemt TOML включает локальный API `127.0.0.1:9091` и metrics `127.0.0.1:9090`; добавлено меню Telegram-бота для отдельных ключей пользователей (`[access.users]`): список, добавление, удаление, ссылка и runtime/API-информация; backup/restore сохраняет bot `.env`, языки бота, custom templates, stats history и структуру Let's Encrypt для переезда на новый VPS; добавлен безопасный детект 3x-ui/Xray на 443 с предупреждением и заметкой по shared-443. +- **2.4.6** — ожидание apt/dpkg lock на свежих Ubuntu/Debian, чтобы установка nginx/certbot/Python не падала во время unattended-upgrades. - **2.4.3** — фикс гонки в `bot_action_dispatch`: параллельные вызовы `change-lite-domain`/`change-template` (например, два пользователя бота одновременно) могли получить ошибку «no secret in config», если один процесс читал `config.json` в момент, когда другой его перезаписывал через `jq`. Теперь диспетчер оборачивается в `flock(1)` с таймаутом 30 с; `util-linux` (содержит `flock`) добавлен в критические зависимости. - **2.4.2** — смена шаблона и домена маскировки **прямо из Telegram-бота** без SSH. Раньше эти пункты меню показывали сообщение «сделай через CLI», теперь бот вызывает `install.sh --action=change-template --json` / `--action=change-lite-domain --json` и разбирает ответ. Плюс: безопасный `safe_edit_message` принимает `disable_web_page_preview`; поле статуса шаблона наконец-то отображается (раньше читалось не из того ключа JSON); полный аудит и автоустановка системных зависимостей при первом запуске (`curl jq openssl git xxd tar dig flock` + опциональные `qrencode bc`); `asyncio.Lock` в боте сериализует параллельные callback'и; валидация tpl\_id (`[A-Za-z0-9_-]{1,64}`) и домена до subprocess. - **2.4.1** — фикс: `start_telemt` теперь делает `restart` если сервис уже запущен. Раньше переустановка Lite поверх Pro оставляла в памяти старый конфиг, и клиенты получали «Unknown TLS SNI drop». Плюс полная документация проекта (этот файл и `DOCS_AI.md`). diff --git a/bootstrap.sh b/bootstrap.sh index ef01d61..604f120 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -6,7 +6,7 @@ set -euo pipefail REPO="anten-ka/gotelegram_pro" BRANCH="${GOTELEGRAM_BRANCH:-test}" -PAT="${GOTELEGRAM_PAT:-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}" +PAT="${GOTELEGRAM_PAT:-}" INSTALL_DIR="/opt/gotelegram" # Use raw.githubusercontent.com (CDN) — faster and avoids Contents API caching # issues that occasionally return 404 for recently added files on non-default branches. @@ -34,6 +34,12 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi +if [ -z "$PAT" ]; then + echo -e " ${RED}✗${NC} Не задан GitHub token." + echo -e " ${YELLOW}Запустите так:${NC} ${CYAN}GOTELEGRAM_PAT=YOUR_PAT sudo -E bash bootstrap.sh${NC}" + exit 1 +fi + # Check dependencies for cmd in curl jq; do if ! command -v "$cmd" &>/dev/null; then diff --git a/gotelegram-bot/README.md b/gotelegram-bot/README.md index fa9cbfc..4c9d190 100644 --- a/gotelegram-bot/README.md +++ b/gotelegram-bot/README.md @@ -1,4 +1,4 @@ -# GoTelegram v2.2 Bot +# GoTelegram v2.5.0 Bot Production-quality Telegram bot for managing MTProxy (telemt engine) on Linux servers. @@ -19,6 +19,7 @@ Production-quality Telegram bot for managing MTProxy (telemt engine) on Linux se - Promotional links - **Template Browsing** - Browse categories → templates → preview → install +- **Per-user MTProxy Keys** - Manage telemt `[access.users]` from inline bot menus - **V1 Migration** - Detects old mtg Docker container and offers migration - **Access Control** - ALLOWED_IDS from .env - **Async/Await** - Full async support via python-telegram-bot v21+ @@ -144,4 +145,4 @@ code, stdout, stderr = await sh("command", "arg1", "arg2") ## License -GoTelegram v2.2 - Open source community project +GoTelegram v2.5.0 - Open source community project diff --git a/gotelegram-bot/bot.py b/gotelegram-bot/bot.py index ca7138e..c92b613 100644 --- a/gotelegram-bot/bot.py +++ b/gotelegram-bot/bot.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -GoTelegram v2.4 Bot - MTProxy Management for Linux +GoTelegram v2.5.0 Bot - MTProxy Management for Linux Manages telemt engine via Telegram interface with full CLI feature parity Uses python-telegram-bot v21+ Supports EN/RU UI with per-user language preferences. @@ -23,6 +23,7 @@ from datetime import datetime from io import StringIO from pathlib import Path from typing import Tuple, Optional, List, Dict, Any +from urllib.parse import quote from dotenv import load_dotenv from telegram import ( @@ -100,7 +101,7 @@ logger = logging.getLogger(__name__) # CONFIGURATION # ============================================================================ -GOTELEGRAM_VERSION = "2.4.6" +GOTELEGRAM_VERSION = "2.5.0" GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json" TELEMT_CONFIG = "/etc/telemt/config.toml" TELEMT_SERVICE = "telemt" @@ -256,6 +257,7 @@ _DOMAIN_RE = re.compile( r"^(?=.{1,253}$)(?:(?!-)[A-Za-z0-9-]{1,63}(? Dict: @@ -342,6 +344,22 @@ def save_json(path: str, data: Dict) -> bool: return False +def template_display_name(template_id: str) -> str: + """Resolve a template id to a human-friendly name from catalog/config.""" + if not template_id: + return "" + if template_id.startswith("custom_"): + config = load_json(GOTELEGRAM_CONFIG) or {} + source = config.get("template_source", "") + return f"{template_id} ({source})" if source else template_id + catalog = load_json(TEMPLATES_CATALOG) or {} + for cat in catalog.get("categories", []): + for tpl in cat.get("templates", []): + if tpl.get("id") == template_id: + return f"{tpl.get('name', template_id)} ({template_id})" + return template_id + + async def safe_edit_message( query, text: str, @@ -498,14 +516,17 @@ def get_main_menu(user_id: Optional[int] = None) -> InlineKeyboardMarkup: ], [ InlineKeyboardButton(_t(user_id, "menu_stats"), callback_data="menu_stats"), + InlineKeyboardButton(_t(user_id, "menu_users"), callback_data="menu_users"), + ], + [ InlineKeyboardButton(_t(user_id, "menu_remove"), callback_data="menu_remove"), - ], - [ InlineKeyboardButton(_t(user_id, "menu_admins"), callback_data="menu_admins"), - InlineKeyboardButton(_t(user_id, "menu_credits"), callback_data="menu_credits"), ], [ + InlineKeyboardButton(_t(user_id, "menu_credits"), callback_data="menu_credits"), InlineKeyboardButton(_t(user_id, "menu_language"), callback_data="menu_lang"), + ], + [ InlineKeyboardButton(_t(user_id, "menu_close"), callback_data="close_menu"), ], ] @@ -653,7 +674,7 @@ async def get_status_text(user_id: Optional[int] = None) -> str: # install.sh/save_gotelegram_config uses "template_id" (not "template") tpl = config.get("template_id") or config.get("template") if tpl: - lines.append(f"{_t(user_id, 'status_template')}: {html.escape(str(tpl))}") + lines.append(f"{_t(user_id, 'status_template')}: {html.escape(template_display_name(str(tpl)))}") if config.get("domain"): lines.append(f"{_t(user_id, 'status_domain')}: {html.escape(str(config['domain']))}") if config.get("port"): @@ -689,6 +710,15 @@ async def get_status_text(user_id: Optional[int] = None) -> str: async def get_traffic_stats() -> str: """Get formatted traffic statistics.""" + await sh( + "bash", + "-lc", + "source /opt/gotelegram/lib/common.sh; " + "source /opt/gotelegram/lib/stats.sh; " + "stats_init >/dev/null 2>&1 || true; stats_collect >/dev/null 2>&1 || true", + timeout=15, + ) + # Read current snapshot current_file = "/run/gotelegram/stats_current.json" history_file = "/opt/gotelegram/stats_history.csv" @@ -706,6 +736,8 @@ async def get_traffic_stats() -> str: reader = csv.reader(f) for row in reader: if len(row) >= 3: + if not row[0].isdigit(): + continue history.append({ "ts": int(row[0]), "proxy": int(row[1]), @@ -824,12 +856,12 @@ async def cb_menu_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> # ============================================================================ -def get_install_mode_menu() -> InlineKeyboardMarkup: +def get_install_mode_menu(user_id: Optional[int] = None) -> InlineKeyboardMarkup: """Install mode selection menu.""" buttons = [ [InlineKeyboardButton("⚡ Lite", callback_data="install_mode_lite")], [InlineKeyboardButton("🛡 Pro", callback_data="install_mode_pro")], - [InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")], + [InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_main")], ] return InlineKeyboardMarkup(buttons) @@ -858,7 +890,7 @@ async def cb_menu_install(update: Update, context: ContextTypes.DEFAULT_TYPE) -> keyboard = InlineKeyboardMarkup(buttons) else: text = "Select installation mode:" - keyboard = get_install_mode_menu() + keyboard = get_install_mode_menu(_uid(update)) await safe_edit_message(query, text, reply_markup=keyboard, parse_mode="HTML" @@ -1362,6 +1394,96 @@ async def cb_pro_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> # PROXY LINK & SHARE # ============================================================================ +def load_telemt_users() -> Dict[str, str]: + """Return users from [access.users] in telemt config.""" + telemt_cfg = load_toml(TELEMT_CONFIG) or {} + users = telemt_cfg.get("access", {}).get("users", {}) + if not isinstance(users, dict): + return {} + return { + str(name): str(secret) + for name, secret in users.items() + if isinstance(name, str) and isinstance(secret, str) + } + + +def save_telemt_users(users: Dict[str, str]) -> bool: + """Persist [access.users] while keeping the rest of the TOML structure.""" + telemt_cfg = load_toml(TELEMT_CONFIG) or {} + access = telemt_cfg.setdefault("access", {}) + access["users"] = dict(sorted(users.items())) + try: + os.makedirs(os.path.dirname(TELEMT_CONFIG), exist_ok=True) + with open(TELEMT_CONFIG, "w") as f: + toml.dump(telemt_cfg, f) + os.chmod(TELEMT_CONFIG, 0o600) + return True + except Exception as e: + logger.error(f"Failed to save telemt users: {e}") + return False + + +async def refresh_telemt_after_user_change() -> bool: + """Restart telemt after config user changes.""" + code, _, _ = await sh("systemctl", "restart", TELEMT_SERVICE, timeout=20) + return code == 0 + + +async def telemt_api_get(path: str) -> Optional[Dict[str, Any]]: + """Read telemt local API if it is enabled in config.""" + code, stdout, _ = await sh( + "curl", + "-sS", + "--max-time", + "3", + f"http://127.0.0.1:9091{path}", + timeout=5, + ) + if code != 0 or not stdout.strip(): + return None + try: + data = json.loads(stdout) + return data if isinstance(data, dict) else None + except json.JSONDecodeError: + return None + + +def _extract_traffic_value(data: Any, keys: List[str]) -> int: + if isinstance(data, dict): + total = 0 + for key, value in data.items(): + if key in keys and isinstance(value, (int, float)): + total += int(value) + elif isinstance(value, (dict, list)): + total += _extract_traffic_value(value, keys) + return total + if isinstance(data, list): + return sum(_extract_traffic_value(item, keys) for item in data) + return 0 + + +async def get_proxy_link_for_secret(secret: str) -> Optional[str]: + """Generate a fake-TLS proxy link for an arbitrary telemt user secret.""" + config = load_json(GOTELEGRAM_CONFIG) or {} + if not secret: + return None + + mode = config.get("mode", "lite") + domain = config.get("domain", "") + port = config.get("port", 443) + + if mode == "pro" and domain: + domain_hex = str(domain).encode().hex() + return f"tg://proxy?server={domain}&port={port}&secret=ee{secret}{domain_hex}" + + code, stdout, _ = await sh("curl", "-s", "-4", "--max-time", "5", "https://api.ipify.org") + server = stdout.strip() if code == 0 and stdout.strip() else "0.0.0.0" + mask_host = config.get("mask_host", "") + if mask_host: + domain_hex = str(mask_host).encode().hex() + return f"tg://proxy?server={server}&port={port}&secret=ee{secret}{domain_hex}" + return f"tg://proxy?server={server}&port={port}&secret={secret}" + async def get_proxy_link() -> Optional[str]: """Generate proxy link from config. Pro-mode uses domain + fake-TLS secret.""" @@ -1381,27 +1503,7 @@ async def get_proxy_link() -> Optional[str]: if not secret: return None - mode = config.get("mode", "lite") - domain = config.get("domain", "") - port = config.get("port", 443) - - # Pro-режим: ссылка с доменом и fake-TLS секретом (ee + secret + hex domain) - if mode == "pro" and domain: - domain_hex = domain.encode().hex() - faketls_secret = f"ee{secret}{domain_hex}" - return f"tg://proxy?server={domain}&port={port}&secret={faketls_secret}" - - # Lite-режим: IP + fake-TLS с mask_host - code, stdout, _ = await sh("curl", "-s", "-4", "--max-time", "5", "https://api.ipify.org") - server = stdout.strip() if code == 0 and stdout.strip() else "0.0.0.0" - - mask_host = config.get("mask_host", "") - if mask_host: - domain_hex = mask_host.encode().hex() - faketls_secret = f"ee{secret}{domain_hex}" - return f"tg://proxy?server={server}&port={port}&secret={faketls_secret}" - - return f"tg://proxy?server={server}&port={port}&secret={secret}" + return await get_proxy_link_for_secret(secret) async def cb_menu_link(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: @@ -1477,6 +1579,201 @@ async def cb_menu_share(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N ) +# ============================================================================ +# TELEMT USERS +# ============================================================================ + + +def _users_keyboard(users: Dict[str, str], user_id: Optional[int]) -> InlineKeyboardMarkup: + rows = [] + for name in sorted(users): + rows.append([InlineKeyboardButton(f"👤 {name}", callback_data=f"user_view_{name}")]) + rows.append([InlineKeyboardButton("➕ Добавить ключ", callback_data="user_add")]) + rows.append([ + InlineKeyboardButton(_t(user_id, "btn_refresh"), callback_data="menu_users"), + InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_main"), + ]) + return InlineKeyboardMarkup(rows) + + +async def cb_menu_users(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + query = update.callback_query + await query.answer() + user_id = _uid(update) + users = load_telemt_users() + + if users: + user_lines = "\n".join(f"• {html.escape(name)}" for name in sorted(users)) + else: + user_lines = "Ключей пока нет" + + api_summary = await telemt_api_get("/v1/stats/summary") + api_note = "" + if api_summary and isinstance(api_summary.get("data"), dict): + data = api_summary["data"] + configured = data.get("configured_users") + active = data.get("active_connections") or data.get("connections_active") + bits = [] + if configured is not None: + bits.append(f"users: {configured}") + if active is not None: + bits.append(f"active: {active}") + if bits: + api_note = "\n\nAPI: " + ", ".join(bits) + + text = ( + "🔑 Ключи пользователей\n\n" + f"{user_lines}" + f"{api_note}\n\n" + "Нажмите на пользователя, чтобы увидеть ссылку, статистику и действия." + ) + await safe_edit_message(query, text, reply_markup=_users_keyboard(users, user_id), parse_mode="HTML") + + +async def _user_detail_text(name: str, secret: str) -> str: + link = await get_proxy_link_for_secret(secret) + api = await telemt_api_get(f"/v1/users/{quote(name, safe='')}") + details = "" + if api: + data = api.get("data", api) + up = _extract_traffic_value(data, ["upload_bytes", "uplink_bytes", "tx_bytes", "sent_bytes", "up"]) + down = _extract_traffic_value(data, ["download_bytes", "downlink_bytes", "rx_bytes", "received_bytes", "down"]) + active_ips = _extract_traffic_value(data, ["active_ips", "unique_ips"]) + parts = [] + if up: + parts.append(f"↑ {up} B") + if down: + parts.append(f"↓ {down} B") + if active_ips: + parts.append(f"active IPs: {active_ips}") + if parts: + details = "\n" + "\n".join(parts) + else: + compact = json.dumps(data, ensure_ascii=False)[:600] + details = f"\n
{html.escape(compact)}
" + else: + details = "\nRuntime API недоступен. Новые установки GoTelegram включают его автоматически." + + link_line = html.escape(link) if link else "link unavailable" + return ( + f"👤 {html.escape(name)}\n\n" + f"Secret: {html.escape(secret)}\n\n" + f"Ссылка:\n{link_line}\n" + f"{details}" + ) + + +async def cb_user_view(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + query = update.callback_query + await query.answer() + user_id = _uid(update) + name = query.data.removeprefix("user_view_") + users = load_telemt_users() + secret = users.get(name) + if not secret: + await safe_edit_message( + query, + "❌ Пользователь не найден.", + reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_users")]]), + ) + return + + buttons = [ + [InlineKeyboardButton("🗑 Удалить", callback_data=f"user_del_{name}")], + [InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_users")], + ] + await safe_edit_message( + query, + await _user_detail_text(name, secret), + reply_markup=InlineKeyboardMarkup(buttons), + parse_mode="HTML", + disable_web_page_preview=True, + ) + + +async def cb_user_add(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + query = update.callback_query + await query.answer() + user_id = _uid(update) + context.user_data["awaiting_user_name"] = True + text = ( + "➕ Новый ключ\n\n" + "Отправьте имя пользователя: латиница, цифры, _ . -, до 48 символов.\n" + "Пример: ivan или family-1." + ) + await safe_edit_message( + query, + text, + reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(_t(user_id, "btn_cancel"), callback_data="menu_users")]]), + parse_mode="HTML", + ) + + +async def cb_user_delete(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + query = update.callback_query + await query.answer() + user_id = _uid(update) + name = query.data.removeprefix("user_del_") + if name == "main": + await query.answer("main нельзя удалить", show_alert=True) + return + text = f"Удалить ключ {html.escape(name)}?" + buttons = [ + [InlineKeyboardButton("✅ Удалить", callback_data=f"user_del_yes_{name}")], + [InlineKeyboardButton(_t(user_id, "btn_cancel"), callback_data=f"user_view_{name}")], + ] + await safe_edit_message(query, text, reply_markup=InlineKeyboardMarkup(buttons), parse_mode="HTML") + + +async def cb_user_delete_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + query = update.callback_query + await query.answer() + user_id = _uid(update) + name = query.data.removeprefix("user_del_yes_") + users = load_telemt_users() + if name == "main" or name not in users: + await query.answer("Нельзя удалить этот ключ", show_alert=True) + return + users.pop(name, None) + if not save_telemt_users(users): + await safe_edit_message(query, "❌ Не удалось сохранить config.toml") + return + await refresh_telemt_after_user_change() + await safe_edit_message( + query, + f"✅ Ключ {html.escape(name)} удалён.", + reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_users")]]), + parse_mode="HTML", + ) + + +async def create_user_from_text(update: Update, context: ContextTypes.DEFAULT_TYPE, name: str) -> None: + user_id = update.effective_user.id + if not _USER_NAME_RE.match(name): + await update.message.reply_text("❌ Некорректное имя. Используйте латиницу, цифры, _ . - и до 48 символов.") + return + users = load_telemt_users() + if name in users: + await update.message.reply_text("❌ Такой пользователь уже есть.") + return + secret = hashlib.sha256(f"{name}:{time.time()}:{os.urandom(16).hex()}".encode()).hexdigest()[:32] + users[name] = secret + if not save_telemt_users(users): + await update.message.reply_text("❌ Не удалось сохранить /etc/telemt/config.toml") + return + await refresh_telemt_after_user_change() + link = await get_proxy_link_for_secret(secret) + await update.message.reply_text( + f"✅ Ключ создан\n\n" + f"Пользователь: {html.escape(name)}\n" + f"Secret: {secret}\n\n" + f"{html.escape(link or '')}", + reply_markup=_users_keyboard(load_telemt_users(), user_id), + parse_mode="HTML", + disable_web_page_preview=True, + ) + + # ============================================================================ # RESTART & LOGS # ============================================================================ @@ -1795,7 +2092,7 @@ async def cb_install_migrate(update: Update, context: ContextTypes.DEFAULT_TYPE) "✅ v1 container stopped and removed\n\n" "Now select installation mode for v2:" ) - keyboard = get_install_mode_menu() + keyboard = get_install_mode_menu(_uid(update)) await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") @@ -2218,6 +2515,7 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> "menu_promo": cb_menu_promo, "menu_credits": cb_menu_credits, "menu_admins": cb_menu_admins, + "menu_users": cb_menu_users, "menu_remove": cb_menu_remove, "install_mode_lite": cb_install_mode_lite, "install_mode_pro": cb_install_mode_pro, @@ -2251,6 +2549,14 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> # Pattern-based handlers if data.startswith("lite_dom_"): await cb_lite_domain(update, context) + elif data == "user_add": + await cb_user_add(update, context) + elif data.startswith("user_view_"): + await cb_user_view(update, context) + elif data.startswith("user_del_yes_"): + await cb_user_delete_confirm(update, context) + elif data.startswith("user_del_"): + await cb_user_delete(update, context) elif data.startswith("pro_cat_"): await cb_pro_category(update, context) elif data.startswith("pro_tpl_"): @@ -2277,6 +2583,10 @@ async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE if not is_user_allowed(update.effective_user.id): return user_id = update.effective_user.id + if context.user_data.pop("awaiting_user_name", False): + await create_user_from_text(update, context, update.message.text.strip()) + return + # Only act when we're explicitly waiting for a custom-git URL if not _CUSTOM_GIT_WAITERS.pop(user_id, False): return @@ -2295,6 +2605,24 @@ async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE config["template_id"] = tpl_id config["template_source"] = url save_json(GOTELEGRAM_CONFIG, config) + if config.get("mode") == "pro" and os.path.isdir(info): + try: + os.makedirs(WEBSITE_ROOT, exist_ok=True) + for entry in os.listdir(WEBSITE_ROOT): + path = os.path.join(WEBSITE_ROOT, entry) + if os.path.isdir(path) and not os.path.islink(path): + shutil.rmtree(path) + else: + os.remove(path) + for entry in os.listdir(info): + src = os.path.join(info, entry) + dst = os.path.join(WEBSITE_ROOT, entry) + if os.path.isdir(src): + shutil.copytree(src, dst) + else: + shutil.copy2(src, dst) + except OSError as e: + logger.error("custom template deploy failed: %s", e) await update.message.reply_text( _tf(user_id, "cg_ok_fmt", html.escape(tpl_id)), reply_markup=get_main_menu(user_id), diff --git a/gotelegram-bot/config.example.env b/gotelegram-bot/config.example.env index a2bf535..55fb309 100644 --- a/gotelegram-bot/config.example.env +++ b/gotelegram-bot/config.example.env @@ -1,4 +1,4 @@ -# GoTelegram v2.2 Bot Configuration +# GoTelegram v2.5.0 Bot Configuration # Copy this to .env and fill in your values # Telegram Bot Token from @BotFather diff --git a/gotelegram-bot/i18n.py b/gotelegram-bot/i18n.py index 83151fe..ede5fa4 100644 --- a/gotelegram-bot/i18n.py +++ b/gotelegram-bot/i18n.py @@ -1,5 +1,5 @@ """ -GoTelegram v2.4 Bot — i18n module +GoTelegram v2.5.0 Bot — i18n module Provides per-user language preferences and a simple t()/tf() API. Usage: diff --git a/gotelegram-bot/lang/en.json b/gotelegram-bot/lang/en.json index 5ae9002..647ae6a 100644 --- a/gotelegram-bot/lang/en.json +++ b/gotelegram-bot/lang/en.json @@ -33,6 +33,7 @@ "menu_website": "🌐 Website/SSL", "menu_promo": "🎁 Promo", "menu_stats": "📊 Traffic Stats", + "menu_users": "🔑 Keys", "menu_remove": "🗑️ Remove", "menu_admins": "👤 Admins", "menu_credits": "ℹ️ Credits", diff --git a/gotelegram-bot/lang/ru.json b/gotelegram-bot/lang/ru.json index a8a195f..ed32566 100644 --- a/gotelegram-bot/lang/ru.json +++ b/gotelegram-bot/lang/ru.json @@ -33,6 +33,7 @@ "menu_website": "🌐 Сайт/SSL", "menu_promo": "🎁 Промо", "menu_stats": "📊 Трафик", + "menu_users": "🔑 Ключи", "menu_remove": "🗑️ Удалить", "menu_admins": "👤 Админы", "menu_credits": "ℹ️ О проекте", diff --git a/install.sh b/install.sh index 7204897..fad9459 100755 --- a/install.sh +++ b/install.sh @@ -1,6 +1,6 @@ #!/bin/bash # ══════════════════════════════════════════════════════════════════════════════ -# GoTelegram v2.4.0 — MTProxy powered by telemt (Rust + Tokio) +# GoTelegram v2.5.0 — MTProxy powered by telemt (Rust + Tokio) # Anti-DPI • Fake TLS • TCP Splice • JA3/JA4 Resistance • i18n (EN/RU) # # Install: @@ -294,6 +294,9 @@ install_lite_mode() { local port port=$(select_port) [ $? -ne 0 ] && return + if [ "$port" = "443" ]; then + warn_3xui_443_conflict || true + fi # Generate secret local secret @@ -342,6 +345,8 @@ install_lite_mode() { install_pro_mode() { log_step "$(t install_pro_step)" + warn_3xui_443_conflict || true + # Enter domain echo "" echo -ne " ${WHITE}$(t install_enter_domain)${NC} " @@ -536,6 +541,21 @@ menu_logs() { } # ── Change mode / template ────────────────────────────────────────────────── +update_current_template_id() { + local template_dir="$1" + local tpl_id + tpl_id=$(basename "$template_dir") + [ -z "$tpl_id" ] && return 0 + + if [ -f "$template_dir/.custom_git_source" ]; then + local source_url + source_url=$(head -1 "$template_dir/.custom_git_source" 2>/dev/null || echo "") + bot_update_config_field "template_source" "$source_url" || true + fi + bot_update_config_field "template_id" "$tpl_id" || \ + log_warning "Не удалось обновить template_id в config.json" +} + menu_change_mode() { local current_mode current_mode=$(config_get mode 2>/dev/null) @@ -558,6 +578,7 @@ menu_change_mode() { template_dir=$(interactive_template_selection) [ $? -ne 0 ] && return switch_template "$template_dir" + update_current_template_id "$template_dir" ;; 2) log_warning "$(t change_requires_reinstall)" @@ -601,6 +622,7 @@ menu_website() { template_dir=$(interactive_template_selection) [ $? -ne 0 ] && return switch_template "$template_dir" + update_current_template_id "$template_dir" ;; esac } diff --git a/install_gotelegram_bot.sh b/install_gotelegram_bot.sh index bb0f1a6..eb73c5a 100755 --- a/install_gotelegram_bot.sh +++ b/install_gotelegram_bot.sh @@ -1,5 +1,5 @@ #!/bin/bash -# GoTelegram v2.2.1 — Установка Telegram-бота +# GoTelegram v2.5.0 — Установка Telegram-бота # Создаёт venv, ставит зависимости, настраивает systemd set -e @@ -19,7 +19,7 @@ if [ "$EUID" -ne 0 ]; then fi echo -e "${CYAN}╔═══════════════════════════════════════════╗${NC}" -echo -e "${CYAN}║${NC} ${GREEN}GoTelegram v2.2.1 — Установка бота${NC} ${CYAN}║${NC}" +echo -e "${CYAN}║${NC} ${GREEN}GoTelegram v2.5.0 — Установка бота${NC} ${CYAN}║${NC}" echo -e "${CYAN}╚═══════════════════════════════════════════╝${NC}" echo "" @@ -93,7 +93,7 @@ fi # ── Systemd ────────────────────────────────────────────────────────────────── cat > "/etc/systemd/system/${SERVICE_NAME}.service" << EOF [Unit] -Description=GoTelegram v2.2.1 Telegram Bot +Description=GoTelegram v2.5.0 Telegram Bot After=network.target [Service] diff --git a/lib/backup.sh b/lib/backup.sh index f5d3cd6..2b44667 100755 --- a/lib/backup.sh +++ b/lib/backup.sh @@ -1,5 +1,5 @@ #!/bin/bash -# GoTelegram v2.4 — backup and restore (i18n-aware) +# GoTelegram v2.5.0 — backup and restore (i18n-aware) # ── Создание бекапа ────────────────────────────────────────────────────────── create_backup() { @@ -35,13 +35,16 @@ create_backup() { cp "$NGINX_SITE_CONF" "$tmp_dir/nginx.conf" fi - # SSL сертификаты + # SSL сертификаты и renewal metadata для переносов между VPS local domain domain=$(config_get domain 2>/dev/null) if [ -n "$domain" ] && [ -d "/etc/letsencrypt/live/$domain" ]; then - mkdir -p "$tmp_dir/certs" - cp "/etc/letsencrypt/live/$domain/fullchain.pem" "$tmp_dir/certs/" 2>/dev/null - cp "/etc/letsencrypt/live/$domain/privkey.pem" "$tmp_dir/certs/" 2>/dev/null + mkdir -p "$tmp_dir/letsencrypt/live" "$tmp_dir/letsencrypt/archive" "$tmp_dir/letsencrypt/renewal" + cp -a "/etc/letsencrypt/live/$domain" "$tmp_dir/letsencrypt/live/" 2>/dev/null + [ -d "/etc/letsencrypt/archive/$domain" ] && \ + cp -a "/etc/letsencrypt/archive/$domain" "$tmp_dir/letsencrypt/archive/" 2>/dev/null + [ -f "/etc/letsencrypt/renewal/$domain.conf" ] && \ + cp -a "/etc/letsencrypt/renewal/$domain.conf" "$tmp_dir/letsencrypt/renewal/" 2>/dev/null log_dim "SSL сертификаты включены" fi @@ -52,6 +55,28 @@ create_backup() { log_dim "$(_t_or backup_site_included 'Шаблон сайта включён')" fi + # Custom templates and catalog + if [ -d "$GOTELEGRAM_DIR/custom_templates" ]; then + mkdir -p "$tmp_dir/custom_templates" + cp -a "$GOTELEGRAM_DIR/custom_templates/." "$tmp_dir/custom_templates/" 2>/dev/null + fi + if [ -f "$GOTELEGRAM_DIR/templates_catalog.json" ]; then + cp "$GOTELEGRAM_DIR/templates_catalog.json" "$tmp_dir/templates_catalog.json" 2>/dev/null + fi + + # Bot state (.env has BotFather token, so encrypted backups are strongly recommended) + if [ -d "$BOT_DIR" ]; then + mkdir -p "$tmp_dir/bot" + [ -f "$BOT_DIR/.env" ] && cp "$BOT_DIR/.env" "$tmp_dir/bot/.env" 2>/dev/null + [ -f "$BOT_DIR/i18n.py" ] && cp "$BOT_DIR/i18n.py" "$tmp_dir/bot/i18n.py" 2>/dev/null + [ -d "$BOT_DIR/lang" ] && cp -a "$BOT_DIR/lang" "$tmp_dir/bot/" 2>/dev/null + fi + + # Traffic history + if [ -f "$GOTELEGRAM_DIR/stats_history.csv" ]; then + cp "$GOTELEGRAM_DIR/stats_history.csv" "$tmp_dir/stats_history.csv" 2>/dev/null + fi + # Метаданные local ip mode engine lang port domain ip=$(get_server_ip) @@ -65,7 +90,7 @@ create_backup() { cat > "$tmp_dir/metadata.json" << EOMETA { - "backup_version": "1.1", + "backup_version": "1.2", "gotelegram_version": "$GOTELEGRAM_VERSION", "created_at": "$(date -Iseconds)", "hostname": "$(hostname)", @@ -231,8 +256,14 @@ restore_backup() { log_success "$(_t_or backup_restored_nginx 'nginx конфиг восстановлен')" fi - # Восстанавливаем SSL - if [ -d "$backup_dir/certs" ]; then + # Восстанавливаем SSL / Let's Encrypt structure + if [ -d "$backup_dir/letsencrypt" ]; then + mkdir -p /etc/letsencrypt/live /etc/letsencrypt/archive /etc/letsencrypt/renewal + [ -d "$backup_dir/letsencrypt/live" ] && cp -a "$backup_dir/letsencrypt/live/." /etc/letsencrypt/live/ 2>/dev/null + [ -d "$backup_dir/letsencrypt/archive" ] && cp -a "$backup_dir/letsencrypt/archive/." /etc/letsencrypt/archive/ 2>/dev/null + [ -d "$backup_dir/letsencrypt/renewal" ] && cp -a "$backup_dir/letsencrypt/renewal/." /etc/letsencrypt/renewal/ 2>/dev/null + log_success "$(_t_or backup_restored_ssl 'SSL сертификаты восстановлены')" + elif [ -d "$backup_dir/certs" ]; then local domain domain=$(config_get domain 2>/dev/null) if [ -n "$domain" ]; then @@ -251,11 +282,38 @@ restore_backup() { log_success "$(_t_or backup_restored_site 'Шаблон сайта восстановлен')" fi + # Восстанавливаем custom templates/catalog/statistics + if [ -d "$backup_dir/custom_templates" ]; then + mkdir -p "$GOTELEGRAM_DIR/custom_templates" + cp -a "$backup_dir/custom_templates/." "$GOTELEGRAM_DIR/custom_templates/" 2>/dev/null + log_success "Пользовательские шаблоны восстановлены" + fi + if [ -f "$backup_dir/templates_catalog.json" ]; then + cp "$backup_dir/templates_catalog.json" "$GOTELEGRAM_DIR/templates_catalog.json" 2>/dev/null + fi + if [ -f "$backup_dir/stats_history.csv" ]; then + cp "$backup_dir/stats_history.csv" "$GOTELEGRAM_DIR/stats_history.csv" 2>/dev/null + log_success "История статистики восстановлена" + fi + + # Восстанавливаем состояние бота + if [ -d "$backup_dir/bot" ]; then + mkdir -p "$BOT_DIR" + [ -f "$backup_dir/bot/.env" ] && cp "$backup_dir/bot/.env" "$BOT_DIR/.env" 2>/dev/null && chmod 600 "$BOT_DIR/.env" + [ -d "$backup_dir/bot/lang" ] && cp -a "$backup_dir/bot/lang" "$BOT_DIR/" 2>/dev/null + [ -f "$backup_dir/bot/i18n.py" ] && cp "$backup_dir/bot/i18n.py" "$BOT_DIR/i18n.py" 2>/dev/null + log_success "Конфигурация Telegram-бота восстановлена" + fi + # Запускаем сервисы + if is_telemt_installed && [ ! -f "/etc/systemd/system/${TELEMT_SERVICE}.service" ]; then + install_telemt_service + fi if is_telemt_installed; then start_telemt fi - systemctl start nginx 2>/dev/null + command -v nginx &>/dev/null && systemctl start nginx 2>/dev/null + systemctl restart gotelegram-bot 2>/dev/null || true # Очистка rm -rf "$tmp_dir" diff --git a/lib/common.sh b/lib/common.sh index b5291c8..0a02eef 100755 --- a/lib/common.sh +++ b/lib/common.sh @@ -1,9 +1,9 @@ #!/bin/bash -# GoTelegram v2.4 — common utilities +# GoTelegram v2.5.0 — common utilities # Colors, logging, spinner, system helpers, v1 compat, i18n-aware # ── Version ─────────────────────────────────────────────────────────────────── -GOTELEGRAM_VERSION="2.4.6" +GOTELEGRAM_VERSION="2.5.0" GOTELEGRAM_NAME="GoTelegram" # ── Пути ────────────────────────────────────────────────────────────────────── @@ -333,6 +333,7 @@ apt_pkg_for_cmd() { ss) echo "iproute2" ;; netstat) echo "net-tools" ;; flock) echo "util-linux" ;; + iptables) echo "iptables" ;; *) echo "$1" ;; # команда == имя пакета esac } @@ -344,6 +345,7 @@ dnf_pkg_for_cmd() { ss) echo "iproute" ;; netstat) echo "net-tools" ;; flock) echo "util-linux" ;; + iptables) echo "iptables" ;; *) echo "$1" ;; esac } @@ -355,7 +357,7 @@ ensure_deps() { # change-lite-domain из бота). local critical=(curl jq openssl git xxd tar dig flock) # Желательные — есть fallback, устанавливать всё равно, но не падать если не смогли - local optional=(qrencode bc) + local optional=(qrencode bc iptables) local missing_critical=() missing_optional=() cmd for cmd in "${critical[@]}"; do @@ -463,6 +465,46 @@ check_port() { return 1 # свободен } +detect_3xui() { + if systemctl list-unit-files 2>/dev/null | grep -Eq '^(x-ui|3x-ui)\.service'; then + return 0 + fi + [ -d /etc/x-ui ] || [ -d /usr/local/x-ui ] || [ -f /etc/x-ui/x-ui.db ] +} + +detect_3xui_443_listener() { + ss -ltnp 2>/dev/null | grep -E '(:|])443[[:space:]]' | grep -Eiq '(xray|x-ui|3x-ui)' +} + +warn_3xui_443_conflict() { + detect_3xui_443_listener || return 1 + log_warning "Обнаружен 3x-ui/Xray, который уже слушает TCP/443." + log_warning "GoTelegram не будет молча останавливать или переписывать 3x-ui." + log_dim "Для настоящего shared-443 нужен один фронтовой TLS/SNI-диспетчер и разные SNI-домены для Xray и GoTelegram." + mkdir -p "$GOTELEGRAM_DIR" 2>/dev/null + cat > "$GOTELEGRAM_DIR/shared-443-3xui.md" <<'EOF' 2>/dev/null || true +# GoTelegram + 3x-ui on one TCP/443 + +GoTelegram detected that 3x-ui/Xray already owns TCP/443. Two independent +processes cannot bind the same IP:port at the same time. A safe shared setup +needs one front TLS/SNI dispatcher on 443 and internal backends, for example: + +- dispatcher: 0.0.0.0:443 +- GoTelegram telemt: 127.0.0.1:7443 +- 3x-ui/Xray inbound: 127.0.0.1:9443 +- GoTelegram nginx mask site: 127.0.0.1:8443 + +The dispatcher must route Xray SNI domains to Xray and route the GoTelegram +SNI domain to telemt. If Xray and GoTelegram use the same SNI domain, automatic +sharing is not reliable: the first TLS ClientHello is intentionally identical. + +GoTelegram intentionally does not rewrite the 3x-ui SQLite database or generated +Xray config without explicit operator confirmation, because 3x-ui can overwrite +manual JSON edits on the next panel change. +EOF + return 0 +} + check_disk_space() { local min_mb="${1:-500}" local avail_mb diff --git a/lib/i18n.sh b/lib/i18n.sh index ca8107a..6fc2767 100755 --- a/lib/i18n.sh +++ b/lib/i18n.sh @@ -1,5 +1,5 @@ #!/bin/bash -# GoTelegram v2.4 — i18n engine +# GoTelegram v2.5.0 — i18n engine # Internationalization support: EN (English) / RU (Русский) # # Usage: diff --git a/lib/lang/en.sh b/lib/lang/en.sh index fd3046e..fc0f3d6 100755 --- a/lib/lang/en.sh +++ b/lib/lang/en.sh @@ -1,5 +1,5 @@ #!/bin/bash -# GoTelegram v2.4 — English translations +# GoTelegram v2.5.0 — English translations # shellcheck disable=SC2034,SC2148 # ── Common words ──────────────────────────────────────────────────────── diff --git a/lib/lang/ru.sh b/lib/lang/ru.sh index 6101c23..797b1fb 100755 --- a/lib/lang/ru.sh +++ b/lib/lang/ru.sh @@ -1,5 +1,5 @@ #!/bin/bash -# GoTelegram v2.4 — Russian translations +# GoTelegram v2.5.0 — Russian translations # shellcheck disable=SC2034,SC2148 # ── Common words ──────────────────────────────────────────────────────── diff --git a/lib/stats.sh b/lib/stats.sh index a52e382..f7bf753 100644 --- a/lib/stats.sh +++ b/lib/stats.sh @@ -1,5 +1,5 @@ #!/bin/bash -# stats.sh — Traffic statistics module for GoTelegram +# stats.sh — Traffic statistics module for GoTelegram v2.5.0 # Tracks proxy (telemt port 443) and site (nginx port 8443) traffic # Uses iptables counters + real-time snapshots + historical CSV @@ -18,6 +18,11 @@ CONFIG_FILE="/opt/gotelegram/config.json" # Initialize stats infrastructure stats_init() { + if ! command -v iptables &>/dev/null; then + log_warning "iptables не найден: установите пакет iptables или запустите установку зависимостей" + return 1 + fi + # Create runtime directory mkdir -p "$STATS_DIR" "$SNAPSHOTS_DIR" 2>/dev/null chmod 755 "$STATS_DIR" "$SNAPSHOTS_DIR" 2>/dev/null @@ -57,6 +62,13 @@ stats_collect() { local ts=$(date +%s) local temp_file=$(mktemp) + if ! command -v iptables &>/dev/null; then + mkdir -p "$STATS_DIR" 2>/dev/null + echo "{\"ts\":$ts,\"proxy_bytes\":0,\"proxy_pkts\":0,\"site_bytes\":0,\"site_pkts\":0,\"error\":\"iptables_missing\"}" > "$CURRENT_SNAPSHOT" 2>/dev/null + rm -f "$temp_file" 2>/dev/null + return 1 + fi + # Parse iptables output: format is "pkts bytes target" # We need to extract bytes (2nd column) for each rule local iptables_output=$(iptables -L GOTELEGRAM_STATS -v -n -x 2>/dev/null) @@ -350,6 +362,14 @@ install_stats_collector() { return 1 fi + if ! command -v iptables &>/dev/null; then + log_info "Установка iptables для подсчёта трафика..." + install_pkg "$(apt_pkg_for_cmd iptables)" || { + echo "Не удалось установить iptables" >&2 + return 1 + } + fi + # Get script directory (resolve symlinks) local script_dir=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") local lib_dir=$(dirname "$script_dir") diff --git a/lib/telemt.sh b/lib/telemt.sh index 3af9484..91a8d80 100755 --- a/lib/telemt.sh +++ b/lib/telemt.sh @@ -1,5 +1,5 @@ #!/bin/bash -# GoTelegram v2.2 — Управление telemt binary +# GoTelegram v2.5.0 — Управление telemt binary # Скачивание, обновление, запуск, остановка через systemd TELEMT_GITHUB="telemt/telemt" diff --git a/lib/telemt_config.sh b/lib/telemt_config.sh index 01d3482..6ae502c 100755 --- a/lib/telemt_config.sh +++ b/lib/telemt_config.sh @@ -1,5 +1,5 @@ #!/bin/bash -# GoTelegram v2.2 — Генерация TOML конфигурации для telemt +# GoTelegram v2.5.0 — Генерация TOML конфигурации для telemt # ── Популярные домены (не заблокированные в РФ) ────────────────────────────── QUICK_DOMAINS=( @@ -48,15 +48,38 @@ generate_telemt_toml() { # Сгенерировано: $(date -Iseconds) # Режим: ${mask_mode} +[general] +use_middle_proxy = true +log_level = "normal" + +[general.modes] +classic = false +secure = false +tls = true + +[general.links] +show = "*" +public_port = ${port} + [server] port = ${port} listen_addr_ipv4 = "0.0.0.0" +metrics_listen = "127.0.0.1:9090" +metrics_whitelist = ["127.0.0.1/32", "::1/128"] + +[server.api] +enabled = true +listen = "127.0.0.1:9091" +whitelist = ["127.0.0.1/32", "::1/128"] +minimal_runtime_enabled = false +minimal_runtime_cache_ttl_ms = 1000 [censorship] tls_domain = "${mask_domain}" mask = true mask_port = ${mask_port} tls_emulation = $([ "$mask_mode" = "pro" ] && echo "false" || echo "true") +unknown_sni_action = "mask" [access.users] main = "${secret}" @@ -106,21 +129,57 @@ get_config_value() { case "$key" in secret) # [access.users] main = "..." - grep -A5 '\[access.users\]' "$config" | grep -m1 '=' | sed 's/.*=\s*"\(.*\)"/\1/' | tr -d ' ' + awk ' + /^\[access\.users\]/ { in_users=1; next } + /^\[/ && in_users { exit } + in_users && $1 == "main" { + sub(/^[^=]*=[[:space:]]*"/, "") + sub(/".*$/, "") + print + exit + } + ' "$config" | tr -d ' ' ;; port) # [server] port = 443 - grep -A5 '\[server\]' "$config" | grep 'port\s*=' | head -1 | sed 's/.*=\s*\([0-9]*\)/\1/' | tr -d ' ' + awk ' + /^\[server\]/ { in_server=1; next } + /^\[/ && in_server { exit } + in_server && $1 == "port" { + sub(/^[^=]*=[[:space:]]*/, "") + gsub(/[[:space:]]/, "") + print + exit + } + ' "$config" ;; mask_host|tls_domain) # [censorship] tls_domain = "..." - grep -A10 '\[censorship\]' "$config" | grep 'tls_domain\s*=' | sed 's/.*=\s*"\(.*\)"/\1/' + awk ' + /^\[censorship\]/ { in_cens=1; next } + /^\[/ && in_cens { exit } + in_cens && $1 == "tls_domain" { + sub(/^[^=]*=[[:space:]]*"/, "") + sub(/".*$/, "") + print + exit + } + ' "$config" ;; mask_port) - grep -A10 '\[censorship\]' "$config" | grep 'mask_port\s*=' | sed 's/.*=\s*\([0-9]*\)/\1/' | tr -d ' ' + awk ' + /^\[censorship\]/ { in_cens=1; next } + /^\[/ && in_cens { exit } + in_cens && $1 == "mask_port" { + sub(/^[^=]*=[[:space:]]*/, "") + gsub(/[[:space:]]/, "") + print + exit + } + ' "$config" ;; *) - grep "$key" "$config" | head -1 | sed 's/.*=\s*"\?\(.*\)"\?/\1/' | tr -d ' "' + grep "$key" "$config" | head -1 | sed 's/^[^=]*=[[:space:]]*//; s/^"//; s/"$//' | tr -d ' ' ;; esac } diff --git a/lib/templates_catalog.sh b/lib/templates_catalog.sh index 01503a1..7c3eeca 100755 --- a/lib/templates_catalog.sh +++ b/lib/templates_catalog.sh @@ -1,5 +1,5 @@ #!/bin/bash -# GoTelegram v2.4 — website templates catalog +# GoTelegram v2.5.0 — website templates catalog # Pick from ~1800 templates, preview links, git sparse-checkout downloads, # + custom git URL templates (user-supplied public repos) diff --git a/lib/website.sh b/lib/website.sh index 9f01504..788386c 100755 --- a/lib/website.sh +++ b/lib/website.sh @@ -1,5 +1,5 @@ #!/bin/bash -# GoTelegram v2.2 — Управление сайтом (nginx + certbot + шаблоны) +# GoTelegram v2.5.0 — Управление сайтом (nginx + certbot + шаблоны) # ── Установка nginx ────────────────────────────────────────────────────────── install_nginx() { @@ -39,7 +39,7 @@ generate_nginx_config() { mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled cat > "$NGINX_SITE_CONF" << 'EONGINX' -# GoTelegram v2.3 — nginx config +# GoTelegram v2.5.0 — nginx config # Pro: nginx на 127.0.0.1:8443 (внутренний), telemt на 0.0.0.0:443 (внешний) # Обычный браузер → :443 → telemt → 127.0.0.1:8443 → nginx (сайт)