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