From 3c6ce21d31c4c2a41f9801db5c715346e67cb168 Mon Sep 17 00:00:00 2001 From: kobaltgit Date: Mon, 6 Apr 2026 13:00:00 +0300 Subject: [PATCH] feat: integrate GoTelegram features into SwiftGram - Added interactive port selection (443, 8443, and manual text input). - Enhanced /status with a list of other running Docker containers. - Improved MTG container stability with DNS tag -t 1.0.0.1. - Implemented state-based text handler for custom port configuration. --- install.sh | 163 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 93 insertions(+), 70 deletions(-) diff --git a/install.sh b/install.sh index 11d1fb2..eee7c9d 100644 --- a/install.sh +++ b/install.sh @@ -1,6 +1,6 @@ #!/bin/bash # 🚀 SwiftGram MTProxy — Smart Modular Manager (Self-contained) -# v1.1.0 — Фикс звонков (UDP) + Автономный режим +# v1.2.0 — UDP Calls + Port Selection + Advanced Diagnostics # ── Цвета ──────────────────────────────────────────────────────────────────── RED='\033[0;31m' @@ -130,22 +130,8 @@ install_base_deps() { echo "" } -# ── Утилиты IP и Портов ────────────────────────────────────────────────────── +# ── Утилиты IP ────────────────────────────────────────────────────────────── get_ip4() { curl -s -4 --max-time 5 https://api.ipify.org || echo "0.0.0.0"; } -get_ip6() { curl -s -6 --max-time 5 https://api6.ipify.org || echo ""; } - -find_smart_port() { - local port=443 - if ss -tlnp | grep -qE ":${port}\b"; then - echo -e " ${YELLOW}ℹ Порт 443 занят (Hiddify/Nginx). Пробую 8443...${NC}" >&2 - port=8443 - if ss -tlnp | grep -qE ":${port}\b"; then - port=$(( (RANDOM % 10000) + 20000 )) - echo -e " ${YELLOW}ℹ Порт 8443 тоже занят. Выбран случайный: $port${NC}" >&2 - fi - fi - echo "$port" -} # ── Вшитые исходники бота ──────────────────────────────────────────────────── prepare_bot_source() { @@ -185,6 +171,8 @@ from telegram.ext import ( CommandHandler, CallbackQueryHandler, ContextTypes, + MessageHandler, + filters, ) BOT_TOKEN = os.environ.get("BOT_TOKEN") @@ -229,12 +217,6 @@ async def get_ip4() -> str: if m: return m.group(0) return "0.0.0.0" -async def get_ip6() -> str: - code, out, _ = await sh("curl", "-s", "-6", "--max-time", "5", "https://api6.ipify.org", timeout=8) - if code == 0 and out: - return out.strip() - return "" - async def check_bbr() -> bool: code, out, _ = await sh("sysctl", "net.ipv4.tcp_congestion_control", timeout=5) return "bbr" in out.lower() @@ -247,6 +229,10 @@ async def docker_val(fmt: str) -> str: code, out, _ = await sh("docker", "inspect", CONTAINER_NAME, "--format", fmt, timeout=10) return out.strip() if code == 0 else "" +async def docker_containers_info() -> str: + code, out, _ = await sh("docker", "ps", "--format", "{{.Names}}\t{{.Image}}\t{{.Ports}}", timeout=10) + return out if code == 0 else "" + async def check_port(port: int) -> str | None: if await proxy_running(): hp = await docker_val("{{range $p,$c := .HostConfig.PortBindings}}{{(index $c 0).HostPort}} {{end}}") @@ -270,16 +256,15 @@ async def proxy_info() -> dict | None: if not await proxy_running(): return None cmd_str = await docker_val("{{range .Config.Cmd}}{{.}} {{end}}") secret = cmd_str.split()[-1] if cmd_str else "" + # Фикс склеивания портов hp = await docker_val("{{range $p,$c := .HostConfig.PortBindings}}{{(index $c 0).HostPort}} {{end}}") port = hp.split()[0] if hp else "443" ip4 = await get_ip4() - ip6 = await get_ip6() cfg = load_config() return { - "ip4": ip4, "ip6": ip6, "port": port, "secret": secret, + "ip4": ip4, "port": port, "secret": secret, "domain": cfg.get("domain", "—"), - "link4": f"tg://proxy?server={ip4}&port={port}&secret={secret}", - "link6": f"tg://proxy?server={ip6}&port={port}&secret={secret}" if ip6 else None + "link4": f"tg://proxy?server={ip4}&port={port}&secret={secret}" } def main_menu_kb() -> InlineKeyboardMarkup: @@ -296,7 +281,6 @@ def main_menu_kb() -> InlineKeyboardMarkup: HELP_TEXT = ( "🚀 SwiftGram MTProxy Manager\n\n" "• TCP + UDP (звонки) активны\n" - "• IPv6 поддержка включена\n" "• BBR оптимизация\n\n" ) @@ -315,6 +299,8 @@ async def cmd_status(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: text = "❌ Прокси не запущен." else: bbr = "✅ Активен" if await check_bbr() else "❌ Выключен" + containers = await docker_containers_info() + other = "\n".join(l for l in containers.splitlines() if CONTAINER_NAME not in l) text = ( "✅ SwiftGram работает\n\n" f"🌐 IPv4: {info['ip4']}\n" @@ -322,6 +308,8 @@ async def cmd_status(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: f"🚀 BBR: {bbr}\n" f"📞 Звонки: ✅ UDP открыт\n" ) + if other: + text += f"\n📦 Другие контейнеры:\n
{html.escape(other)}
" kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]]) if update.callback_query: await update.callback_query.edit_message_text(text, parse_mode="HTML", reply_markup=kb) else: await update.message.reply_text(text, parse_mode="HTML", reply_markup=kb) @@ -330,8 +318,7 @@ async def cmd_link(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: if not update.effective_user or not _ok(update.effective_user.id): return info = await proxy_info() if not info: text = "❌ Прокси не запущен." - else: - text = f"Ссылка:\n{info['link4']}" + else: text = f"Ссылка:\n{info['link4']}" kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]]) if update.callback_query: await update.callback_query.edit_message_text(text, parse_mode="HTML", reply_markup=kb) else: await update.message.reply_text(text, parse_mode="HTML", reply_markup=kb) @@ -342,6 +329,48 @@ async def cmd_restart(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]]) if update.callback_query: await update.callback_query.edit_message_text("✅ Перезапущено", reply_markup=kb) +async def cmd_logs(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: + if not update.effective_user or not _ok(update.effective_user.id): return + code, out, err = await sh("docker", "logs", "--tail", "30", CONTAINER_NAME, timeout=15) + text = f"
{html.escape(out or err or 'Логов нет.')}
" + kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]]) + if update.callback_query: await update.callback_query.edit_message_text(text, parse_mode="HTML", reply_markup=kb) + else: await update.message.reply_text(text, parse_mode="HTML", reply_markup=kb) + +async def do_install(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: + domain = ctx.user_data.get("install_domain", "google.com") + port = ctx.user_data.get("install_port", "443") + msg = update.callback_query.message if update.callback_query else update.message + await msg.reply_text(f"⏳ Установка на порт {port}...") + + _, s_out, _ = await sh("docker", "run", "--rm", "nineseconds/mtg:2", "generate-secret", "--hex", domain) + secret = s_out.strip().split()[-1] if s_out else "" + + await sh("docker", "stop", CONTAINER_NAME) + await sh("docker", "rm", CONTAINER_NAME) + + code, _, err = await sh( + "docker", "run", "-d", "--name", CONTAINER_NAME, "--restart", "always", + "-p", f"{port}:{port}/tcp", "-p", f"{port}:{port}/udp", + "nineseconds/mtg:2", "simple-run", "-n", "1.1.1.1", "-t", "1.0.0.1", "-i", "prefer-ipv4", + f"0.0.0.0:{port}", secret + ) + + if code == 0: + save_config({"domain": domain, "port": port, "secret": secret}) + await msg.reply_text(f"✅ Установлено!") + await cmd_status(update, ctx) + else: + await msg.reply_text(f"❌ Ошибка: {err}") + +async def text_handler(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: + if not update.message or not ctx.user_data.get("install_wait_port"): return + text = (update.message.text or "").strip() + if text.isdigit() and (1 <= int(text) <= 65535): + ctx.user_data["install_port"] = text + ctx.user_data["install_wait_port"] = False + await do_install(update, ctx) + async def callback_handler(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: query = update.callback_query await query.answer() @@ -350,12 +379,40 @@ async def callback_handler(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> No elif data == "menu_status": await cmd_status(update, ctx) elif data == "menu_link": await cmd_link(update, ctx) elif data == "menu_restart": await cmd_restart(update, ctx) + elif data == "menu_logs": await cmd_logs(update, ctx) + elif data == "menu_install": + buttons = [] + row = [] + for i, d in enumerate(DOMAINS): + row.append(InlineKeyboardButton(d, callback_data=f"dom_{i}")) + if len(row) == 2: buttons.append(row); row = [] + await query.edit_message_text("🌐 Выберите домен:", reply_markup=InlineKeyboardMarkup(buttons)) + elif data.startswith("dom_"): + idx = int(data[4:]) + ctx.user_data["install_domain"] = DOMAINS[idx] + busy_443 = await check_port(443) + kb = InlineKeyboardMarkup([ + [InlineKeyboardButton("443 " + ("⚠️" if busy_443 else "✅"), callback_data="port_443"), + InlineKeyboardButton("8443", callback_data="port_8443")], + [InlineKeyboardButton("◀️ Меню", callback_data="menu_main")] + ]) + ctx.user_data["install_wait_port"] = True + await query.edit_message_text("🔌 Выберите порт или введите свой:", reply_markup=kb) + elif data.startswith("port_"): + ctx.user_data["install_port"] = data[5:] + ctx.user_data["install_wait_port"] = False + await do_install(update, ctx) + elif data == "menu_remove": + await sh("docker", "stop", CONTAINER_NAME) + await sh("docker", "rm", CONTAINER_NAME) + await query.edit_message_text("✅ Удалено.") def main() -> None: if not BOT_TOKEN: return app = Application.builder().token(BOT_TOKEN).build() app.add_handler(CommandHandler("start", start)) app.add_handler(CallbackQueryHandler(callback_handler)) + app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, text_handler)) app.run_polling() if __name__ == "__main__": @@ -384,47 +441,13 @@ show_config() { qrencode -t ANSIUTF8 "$LINK" } -# ── 1) Установка MTProxy ───────────────────────────────────────────────────── +# ── Меню установки ─────────────────────────────────────────────────────────── menu_install() { - clear - local domains=("google.com" "wikipedia.org" "github.com" "habr.com") - echo -e "${CYAN}Выберите домен (Fake TLS):${NC}" - for i in "${!domains[@]}"; do echo -e " ${YELLOW}$((i+1)))${NC} ${domains[$i]}"; done - read -p "Выбор: " d_idx - DOMAIN=${domains[$((d_idx-1))]:-google.com} - - local PORT=$(find_smart_port) - optimize_system - fix_firewall "$PORT" - - spinner_start "Запуск контейнера..." - docker pull nineseconds/mtg:2 >/dev/null 2>&1 - local SECRET=$(docker run --rm nineseconds/mtg:2 generate-secret --hex "$DOMAIN" 2>/dev/null) - - docker stop "$CONTAINER_NAME" &>/dev/null - docker rm "$CONTAINER_NAME" &>/dev/null - - # ВАЖНО: проброс UDP для звонков - docker run -d --name "$CONTAINER_NAME" --restart always \ - -p "$PORT":"$PORT"/tcp \ - -p "$PORT":"$PORT"/udp \ - nineseconds/mtg:2 simple-run \ - -n 1.1.1.1 -i prefer-ipv4 \ - 0.0.0.0:"$PORT" "$SECRET" > /dev/null 2>&1 - - sleep 2 - spinner_stop - - if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then - mkdir -p "$BOT_DIR" - echo "{\"domain\": \"$DOMAIN\", \"port\": \"$PORT\", \"secret\": \"$SECRET\"}" > "$BOT_DIR/proxy.json" - echo -e "\n${GREEN}✓ SwiftGram успешно запущен!${NC}" - show_config - fi - read -p "Нажмите Enter..." + echo -e "${CYAN}Установка производится через бота.${NC}" + echo -e "Настройте бота командой в меню, затем используйте /start в Telegram." } -# ── 3) Настройка бота ──────────────────────────────────────────────────────── +# ── Настройка бота ─────────────────────────────────────────────────────────── menu_setup_bot() { clear echo -e "${CYAN}Настройка Telegram бота...${NC}" @@ -473,12 +496,12 @@ SELF="$(realpath "$0")" while true; do clear - echo -e "${MAGENTA}SWIFTGRAM MANAGER (UDP Fixed)${NC}" + echo -e "${MAGENTA}SWIFTGRAM MANAGER (UDP + Diagnostics)${NC}" docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$" && echo -e " Прокси: ${GREEN}РАБОТАЕТ${NC}" || echo -e " Прокси: ${RED}ВЫКЛЮЧЕН${NC}" - echo -e "\n ${GREEN}1)${NC} Установить / Обновить прокси\n ${GREEN}2)${NC} Показать данные (QR)\n ${CYAN}3)${NC} Настроить бота\n ${RED}6)${NC} Удалить\n ${WHITE}0)${NC} Выход" + echo -e "\n ${GREEN}1)${NC} Установить (через бота)\n ${GREEN}2)${NC} Показать данные (QR)\n ${CYAN}3)${NC} Настроить бота\n ${RED}6)${NC} Удалить\n ${WHITE}0)${NC} Выход" read -p "Пункт: " m_idx case $m_idx in - 1) menu_install ;; 2) clear; show_config; read -p "Enter..." ;; 3) menu_setup_bot ;; + 1) menu_install; read -p "Enter..." ;; 2) clear; show_config; read -p "Enter..." ;; 3) menu_setup_bot ;; 6) docker stop "$CONTAINER_NAME" && docker rm "$CONTAINER_NAME"; rm -rf "$BOT_DIR"; exit 0 ;; 0) exit 0 ;; esac done \ No newline at end of file