Files
swiftgram/install.sh
kobaltgit 5da628ed70 fix: improve docker logs retrieval and diagnostics
- Updated cmd_logs to combine stdout and stderr streams.
- Added a check for container existence before fetching logs.
- Increased log tail limit to 50 lines.
- Improved user feedback when logs are empty but the container is running.
2026-04-06 13:05:46 +03:00

521 lines
23 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# 🚀 SwiftGram MTProxy — Smart Modular Manager (Self-contained)
# v1.2.0 — UDP Calls + Port Selection + Advanced Diagnostics
# ── Цвета ────────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
CYAN='\033[0;36m'
YELLOW='\033[1;33m'
MAGENTA='\033[0;35m'
BLUE='\033[0;34m'
WHITE='\033[1;37m'
NC='\033[0m'
# ── Конфиг ───────────────────────────────────────────────────────────────────
CONTAINER_NAME="swiftgram-proxy"
BOT_DIR="/opt/swiftgram"
SERVICE_NAME="swiftgram-bot"
# ── Спиннер и прогресс-бар ────────────────────────────────────────────────────
spin_pid=""
spinner_start() {
local msg="${1:-Подождите...}"
(
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while true; do
printf "\r ${CYAN}${frames[$i]}${NC} ${msg}" >&2
i=$(( (i+1) % ${#frames[@]} ))
sleep 0.12
done
) &
spin_pid=$!
}
spinner_stop() {
[ -n "$spin_pid" ] && kill "$spin_pid" 2>/dev/null && wait "$spin_pid" 2>/dev/null
spin_pid=""
printf "\r\033[K" >&2
}
progress_bar() {
local current="$1" total="$2" label="${3:-}"
local pct=$(( current * 100 / total ))
local filled=$(( pct / 2 ))
local empty=$(( 50 - filled ))
local bar=""
for ((i=0; i<filled; i++)); do bar+="█"; done
for ((i=0; i<empty; i++)); do bar+="░"; done
printf "\r ${GREEN}[${bar}]${NC} ${pct}%% ${label}" >&2
[ "$current" -eq "$total" ] && echo "" >&2
}
run_with_progress() {
local label="$1"; shift
spinner_start "$label"
"$@" >/dev/null 2>&1
local rc=$?
spinner_stop
if [ $rc -eq 0 ]; then
echo -e " ${GREEN}${NC} $label"
else
echo -e " ${RED}${NC} $label ${RED}(ошибка)${NC}"
fi
return $rc
}
# ── Проверки системы ─────────────────────────────────────────────────────────
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}Запустите с sudo / root.${NC}"
exit 1
fi
install_pkg() {
if command -v apt-get &>/dev/null; then
apt-get update -qq && apt-get install -y -qq "$@"
elif command -v dnf &>/dev/null; then
dnf install -y "$@" 2>/dev/null
elif command -v yum &>/dev/null; then
yum install -y "$@"
fi
}
# ── Оптимизация Сети (BBR) ───────────────────────────────────────────────────
optimize_system() {
if ! sysctl net.ipv4.tcp_congestion_control | grep -q "bbr"; then
spinner_start "Оптимизация сетевого стека (BBR)..."
{
echo "net.core.default_qdisc=fq"
echo "net.ipv4.tcp_congestion_control=bbr"
echo "net.ipv4.ip_local_port_range=1024 65535"
echo "net.core.somaxconn=65535"
} >> /etc/sysctl.conf
sysctl -p >/dev/null 2>&1
spinner_stop
echo -e " ${GREEN}${NC} BBR ускорение включено"
fi
}
# ── Firewall (Фикс звонков) ──────────────────────────────────────────────────
fix_firewall() {
local port="$1"
if command -v ufw &>/dev/null && ufw status | grep -q "active"; then
ufw allow "$port"/tcp >/dev/null 2>&1
ufw allow "$port"/udp >/dev/null 2>&1
elif command -v firewall-cmd &>/dev/null && systemctl is-active --quiet firewalld; then
firewall-cmd --permanent --add-port="$port"/tcp >/dev/null 2>&1
firewall-cmd --permanent --add-port="$port"/udp >/dev/null 2>&1
firewall-cmd --reload >/dev/null 2>&1
fi
echo -e " ${GREEN}${NC} Firewall: порты $port (TCP/UDP) открыты"
}
# ── Установка зависимостей ───────────────────────────────────────────────────
install_base_deps() {
local steps=0 total=4
progress_bar $steps $total "Проверка зависимостей..."
if ! command -v curl &>/dev/null; then run_with_progress "Установка curl" install_pkg curl; fi
steps=$((steps+1)); progress_bar $steps $total "curl"
if ! command -v docker &>/dev/null; then
spinner_start "Установка Docker..."
curl -fsSL https://get.docker.com | sh >/dev/null 2>&1
systemctl enable --now docker >/dev/null 2>&1
spinner_stop
fi
steps=$((steps+1)); progress_bar $steps $total "docker"
if ! command -v qrencode &>/dev/null; then run_with_progress "Установка qrencode" install_pkg qrencode; fi
steps=$((steps+1)); progress_bar $steps $total "qrencode"
if ! docker info &>/dev/null 2>&1; then systemctl start docker 2>/dev/null; sleep 2; fi
steps=$((steps+1)); progress_bar $steps $total "Готово"
echo ""
}
# ── Утилиты IP ──────────────────────────────────────────────────────────────
get_ip4() { curl -s -4 --max-time 5 https://api.ipify.org || echo "0.0.0.0"; }
# ── Вшитые исходники бота ────────────────────────────────────────────────────
prepare_bot_source() {
mkdir -p "$BOT_DIR"
cat << 'EOF' > "$BOT_DIR/requirements.txt"
python-telegram-bot==20.8
EOF
cat << 'EOF' > "$BOT_DIR/bot.py"
#!/usr/bin/env python3
"""
SwiftGram MTProxy — Telegram-бот для управления MTProxy на сервере.
"""
import asyncio
import html
import json
import os
import re
from pathlib import Path
_env_path = Path(__file__).resolve().parent / ".env"
if not _env_path.exists():
_env_path = Path("/opt/swiftgram/.env")
if _env_path.exists():
with open(_env_path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, v = line.split("=", 1)
os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import (
Application,
CommandHandler,
CallbackQueryHandler,
ContextTypes,
MessageHandler,
filters,
)
BOT_TOKEN = os.environ.get("BOT_TOKEN")
_allowed = os.environ.get("ALLOWED_IDS", "").strip()
try:
ALLOWED_IDS = set(int(x) for x in _allowed.split(",") if x.strip()) if _allowed else None
except ValueError:
ALLOWED_IDS = None
CONTAINER_NAME = os.environ.get("CONTAINER_NAME", "swiftgram-proxy")
CONFIG_FILE = Path(os.environ.get("CONFIG_PATH", "/opt/swiftgram/proxy.json"))
DOMAINS = [
"google.com", "wikipedia.org", "habr.com", "github.com",
"coursera.org", "udemy.com", "medium.com", "stackoverflow.com",
"bbc.com", "cnn.com", "reuters.com", "nytimes.com",
]
def _ok(uid: int) -> bool:
return ALLOWED_IDS is None or uid in ALLOWED_IDS
def _decode(data: bytes) -> str:
return (data or b"").decode("utf-8", errors="replace").strip()
async def sh(*args: str, timeout: int = 60) -> tuple[int, str, str]:
proc = await asyncio.create_subprocess_exec(
*args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
)
try:
out, err = await asyncio.wait_for(proc.communicate(), timeout=timeout)
except asyncio.TimeoutError:
proc.kill()
await proc.wait()
return -1, "", "Timeout"
return proc.returncode or 0, _decode(out), _decode(err)
async def get_ip4() -> str:
for url in ("https://api.ipify.org", "https://icanhazip.com", "https://ifconfig.me"):
code, out, _ = await sh("curl", "-s", "-4", "--max-time", "5", url, timeout=8)
if code == 0 and out:
m = re.search(r"(\d{1,3}\.){3}\d{1,3}", out)
if m: return m.group(0)
return "0.0.0.0"
async def check_bbr() -> bool:
code, out, _ = await sh("sysctl", "net.ipv4.tcp_congestion_control", timeout=5)
return "bbr" in out.lower()
async def proxy_running() -> bool:
code, out, _ = await sh("docker", "ps", "--format", "{{.Names}}", timeout=10)
return code == 0 and CONTAINER_NAME in out
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}}")
if str(port) in hp.split(): return None
code, out, _ = await sh("ss", "-tlnp", timeout=5)
for line in out.splitlines():
if f":{port} " in line or f":{port}\t" in line: return line
return None
def save_config(data: dict) -> None:
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
CONFIG_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
def load_config() -> dict:
if CONFIG_FILE.exists():
try: return json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
except: pass
return {}
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()
cfg = load_config()
return {
"ip4": ip4, "port": port, "secret": secret,
"domain": cfg.get("domain", "—"),
"link4": f"tg://proxy?server={ip4}&port={port}&secret={secret}"
}
def main_menu_kb() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup([
[InlineKeyboardButton("🔧 Установить / Обновить", callback_data="menu_install")],
[InlineKeyboardButton("📊 Статус", callback_data="menu_status"),
InlineKeyboardButton("🔗 Ссылка", callback_data="menu_link")],
[InlineKeyboardButton("📤 Поделиться", callback_data="menu_share")],
[InlineKeyboardButton("🔄 Рестарт", callback_data="menu_restart"),
InlineKeyboardButton("📋 Логи", callback_data="menu_logs")],
[InlineKeyboardButton("🗑 Удалить прокси", callback_data="menu_remove")],
])
HELP_TEXT = (
"🚀 <b>SwiftGram MTProxy Manager</b>\n\n"
"• TCP + UDP (звонки) активны\n"
"• BBR оптимизация\n\n"
)
async def start(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
if not update.effective_user or not _ok(update.effective_user.id): return
msg = update.message or (update.callback_query and update.callback_query.message)
if update.message:
await update.message.reply_text(HELP_TEXT, parse_mode="HTML", reply_markup=main_menu_kb())
elif update.callback_query:
await update.callback_query.edit_message_text(HELP_TEXT, parse_mode="HTML", reply_markup=main_menu_kb())
async def cmd_status(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 = "❌ <b>Прокси не запущен.</b>"
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 = (
"✅ <b>SwiftGram работает</b>\n\n"
f"🌐 IPv4: <code>{info['ip4']}</code>\n"
f"🔌 Порт: <code>{info['port']}</code>\n"
f"🚀 BBR: <code>{bbr}</code>\n"
f"📞 Звонки: <code>✅ UDP открыт</code>\n"
)
if other:
text += f"\n📦 <b>Другие контейнеры:</b>\n<pre>{html.escape(other)}</pre>"
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 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"<b>Ссылка:</b>\n<code>{info['link4']}</code>"
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 cmd_restart(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
if not update.effective_user or not _ok(update.effective_user.id): return
await sh("docker", "restart", CONTAINER_NAME)
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
running = await proxy_running()
if not running:
text = "❌ <b>Контейнер не найден или остановлен.</b>"
else:
# Запрашиваем 50 строк и объединяем выводы
code, out, err = await sh("docker", "logs", "--tail", "50", CONTAINER_NAME, timeout=15)
combined_logs = (out.strip() + "\n" + err.strip()).strip()
if not combined_logs:
text = "📋 <b>Контейнер запущен, но логи пока пусты.</b>\n<i>Попробуйте подключиться к прокси, чтобы появились записи.</i>"
else:
text = f"<pre>{html.escape(combined_logs)}</pre>"
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()
data = query.data
if data == "menu_main": await start(update, ctx)
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__":
main()
EOF
chmod +x "$BOT_DIR/bot.py"
}
# ── Показать данные подключения ──────────────────────────────────────────────
show_config() {
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
echo -e "${RED}Прокси не запущен!${NC}"; return
fi
local DATA=$(cat "$BOT_DIR/proxy.json" 2>/dev/null)
local PORT=$(echo "$DATA" | grep -oP '(?<="port": ")[^"]*')
local SECRET=$(echo "$DATA" | grep -oP '(?<="secret": ")[^"]*')
local IP4=$(get_ip4)
local LINK="tg://proxy?server=$IP4&port=$PORT&secret=$SECRET"
echo -e "\n${CYAN}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e " IPv4: ${WHITE}$IP4${NC}"
echo -e " Порт: ${WHITE}$PORT${NC} (TCP + UDP)"
echo -e " Secret: ${WHITE}$SECRET${NC}"
echo -e "\n Ссылка: ${BLUE}$LINK${NC}"
echo ""
qrencode -t ANSIUTF8 "$LINK"
}
# ── Меню установки ───────────────────────────────────────────────────────────
menu_install() {
echo -e "${CYAN}Установка производится через бота.${NC}"
echo -e "Настройте бота командой в меню, затем используйте /start в Telegram."
}
# ── Настройка бота ───────────────────────────────────────────────────────────
menu_setup_bot() {
clear
echo -e "${CYAN}Настройка Telegram бота...${NC}"
if ! command -v python3 &>/dev/null; then run_with_progress "Python3" install_pkg python3 python3-pip python3-venv; fi
prepare_bot_source
cd "$BOT_DIR"
[ ! -d "venv" ] && python3 -m venv venv >/dev/null 2>&1
./venv/bin/pip install -r requirements.txt -q
read -p "Введите BOT_TOKEN: " TOKEN
read -p "Ваш Telegram ID: " ADMIN_ID
{
echo "BOT_TOKEN=$TOKEN"
echo "ALLOWED_IDS=$ADMIN_ID"
echo "CONTAINER_NAME=$CONTAINER_NAME"
echo "CONFIG_PATH=$BOT_DIR/proxy.json"
} > .env
cat > "/etc/systemd/system/${SERVICE_NAME}.service" << EOF
[Unit]
Description=SwiftGram Bot Service
After=network.target docker.service
[Service]
Type=simple
WorkingDirectory=$BOT_DIR
ExecStart=$BOT_DIR/venv/bin/python $BOT_DIR/bot.py
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now "$SERVICE_NAME"
systemctl restart "$SERVICE_NAME"
echo -e "\n${GREEN}✓ Бот запущен!${NC}"
read -p "Нажмите Enter..."
}
# ── Главный цикл ─────────────────────────────────────────────────────────────
install_base_deps
SELF="$(realpath "$0")"
[ "$SELF" != "/usr/local/bin/swiftgram" ] && { cp "$SELF" /usr/local/bin/swiftgram; chmod +x /usr/local/bin/swiftgram; }
while true; do
clear
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} Выход"
read -p "Пункт: " m_idx
case $m_idx in
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