Files
Go-VPN-Client/internal/cli/cli.go
arkonsadter b809e84220 feat: add security system with system-wide proxy, DNS protection and encryption
- System-wide proxy: automatic Windows proxy configuration for all apps
- DNS leak protection: force all DNS queries through VPN
- Config encryption: AES-256-GCM encryption for all config files
- File protection: strict access permissions for config directory
- Leak detection: built-in security check system
- Kill Switch: temporarily disabled (will be improved in next version)

Security features:
✓ Automatic system proxy setup
✓ DNS leak protection (optional)
✓ AES-256-GCM config encryption
✓ File and directory protection
✓ Security leak checker
⚠ Kill Switch disabled (caused internet blocking issues)

Emergency recovery scripts included:
- ОТКЛЮЧИТЬ_KILLSWITCH.bat
- EMERGENCY_FIX_INTERNET.bat
- ЕСЛИ_СЛОМАЛСЯ_ИНТЕРНЕТ.txt

Documentation:
- Markdown/SECURITY_GUIDE.md - full security guide
- БЕЗОПАСНОСТЬ_БЫСТРЫЙ_СТАРТ.md - quick start guide
- CHANGELOG_SECURITY.md - detailed changelog
2026-04-12 19:01:24 +06:00

1055 lines
28 KiB
Go
Raw Permalink 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.
package cli
import (
"bufio"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"vpn-client/internal/config"
"vpn-client/internal/subscription"
"vpn-client/internal/vless"
"vpn-client/internal/vpn"
"vpn-client/internal/wireguard"
"github.com/fatih/color"
)
var (
green = color.New(color.FgGreen).SprintFunc()
red = color.New(color.FgRed).SprintFunc()
yellow = color.New(color.FgYellow).SprintFunc()
cyan = color.New(color.FgCyan).SprintFunc()
bold = color.New(color.Bold).SprintFunc()
)
// Run запускает CLI интерфейс
func Run() error {
for {
clearScreen()
showMainMenu()
choice := readInput("Выберите действие: ")
state, _ := vpn.GetStatus()
isConnected := state != nil && state.Connected
switch choice {
case "1":
wireguardMenu()
case "2":
vlessMenu()
case "3":
subscriptionsMenu()
case "4":
if err := vpn.ShowStatus(); err != nil {
fmt.Printf("%s %v\n", red("✗"), err)
}
pause()
case "5":
if isConnected {
// Мониторинг в реальном времени
if err := MonitorConnection(); err != nil {
fmt.Printf("%s %v\n", red("✗"), err)
pause()
}
} else {
// Отключение
if err := vpn.Disconnect(config.LogsDir); err != nil {
fmt.Printf("%s %v\n", red("✗"), err)
}
pause()
}
case "6":
if isConnected {
if err := vpn.Disconnect(config.LogsDir); err != nil {
fmt.Printf("%s %v\n", red("✗"), err)
}
pause()
} else {
// Настройки
settingsMenu()
}
case "7":
if isConnected {
// Настройки
settingsMenu()
} else {
// Безопасность
securityMenu()
}
case "8":
if isConnected {
// Безопасность
securityMenu()
} else {
fmt.Printf("%s Неверный выбор\n", red("✗"))
pause()
}
case "0":
fmt.Println("До свидания!")
return nil
default:
fmt.Printf("%s Неверный выбор\n", red("✗"))
pause()
}
}
}
func showMainMenu() {
state, _ := vpn.GetStatus()
// Получаем актуальный статус с временем
statusLine := ShowQuickStatus()
// Показываем текущее ядро
settings, _ := config.LoadSettings()
coreName := "Xray"
if settings != nil && settings.CoreType == "v2ray" {
coreName = "V2Ray"
}
fmt.Println("\n" + strings.Repeat("=", 60))
fmt.Println(bold("VPN Клиент (Go)"))
fmt.Println(strings.Repeat("=", 60))
fmt.Printf("Статус: %s\n", statusLine)
fmt.Printf("Ядро VLESS: %s\n", cyan(coreName))
// Показываем текущее время
fmt.Printf("Время: %s\n", time.Now().Format("15:04:05"))
fmt.Println(strings.Repeat("=", 60))
fmt.Println("1. WireGuard")
fmt.Println("2. VLESS")
fmt.Println("3. Управление подписками")
fmt.Println("4. Показать статус подключения")
if state != nil && state.Connected {
fmt.Println("5. " + green("Мониторинг в реальном времени"))
fmt.Println("6. Отключиться от VPN")
fmt.Println("7. " + yellow("Настройки (выбор ядра)"))
fmt.Println("8. " + cyan("🔒 Безопасность и защита"))
} else {
fmt.Println("5. Отключиться от VPN")
fmt.Println("6. " + yellow("Настройки (выбор ядра)"))
fmt.Println("7. " + cyan("🔒 Безопасность и защита"))
}
fmt.Println("0. Выход")
fmt.Println(strings.Repeat("=", 60))
}
func vlessMenu() {
for {
clearScreen()
fmt.Println("\n" + strings.Repeat("=", 50))
fmt.Println(bold("VLESS"))
fmt.Println(strings.Repeat("=", 50))
fmt.Println("1. Список конфигураций")
fmt.Println("2. Добавить конфиг")
fmt.Println("3. Удалить конфиг")
fmt.Println("4. Подключиться")
fmt.Println("5. Тестировать конфиг (пинг)")
fmt.Println("6. " + yellow("Показать логи ошибок"))
fmt.Println("0. Назад")
fmt.Println(strings.Repeat("=", 50))
choice := readInput("\nВыберите действие: ")
switch choice {
case "0":
return
case "1":
listVLESSConfigs()
pause()
case "2":
addVLESSConfig()
pause()
case "3":
deleteVLESSConfig()
pause()
case "4":
connectVLESS()
pause()
case "5":
testVLESSConfig()
pause()
case "6":
showVLESSLogs()
pause()
default:
fmt.Printf("%s Неверный выбор\n", red("✗"))
pause()
}
}
}
func listVLESSConfigs() {
configs, err := config.LoadConfigs()
if err != nil {
fmt.Printf("%s Ошибка загрузки конфигураций: %v\n", red("✗"), err)
return
}
fmt.Println("\n=== VLESS конфиги ===")
if len(configs.VLESS) == 0 {
fmt.Println("Нет конфигов")
return
}
for i, cfg := range configs.VLESS {
protocol := cfg.Protocol
if protocol == "" {
protocol = "VLESS"
}
fmt.Printf("%d. [%s] %s\n", i+1, protocol, cfg.Name)
}
}
func addVLESSConfig() {
name := readInput("Имя конфига: ")
if name == "" {
fmt.Printf("%s Имя не может быть пустым\n", red("✗"))
return
}
vlessURL := readInput("VLESS URL: ")
if vlessURL == "" {
fmt.Printf("%s URL не может быть пустым\n", red("✗"))
return
}
configs, err := config.LoadConfigs()
if err != nil {
fmt.Printf("%s Ошибка загрузки конфигураций: %v\n", red("✗"), err)
return
}
configs.VLESS = append(configs.VLESS, config.VLESSConfig{
Name: name,
URL: vlessURL,
Protocol: "VLESS",
})
if err := config.SaveConfigs(configs); err != nil {
fmt.Printf("%s Ошибка сохранения конфигураций: %v\n", red("✗"), err)
return
}
fmt.Printf("%s VLESS конфиг '%s' добавлен\n", green("✓"), name)
}
func deleteVLESSConfig() {
configs, err := config.LoadConfigs()
if err != nil {
fmt.Printf("%s Ошибка загрузки конфигураций: %v\n", red("✗"), err)
return
}
if len(configs.VLESS) == 0 {
fmt.Println("Нет конфигов для удаления")
return
}
fmt.Println("\n=== VLESS конфиги ===")
for i, cfg := range configs.VLESS {
fmt.Printf("%d. %s\n", i+1, cfg.Name)
}
numStr := readInput("\nНомер конфига для удаления: ")
num, err := strconv.Atoi(numStr)
if err != nil || num < 1 || num > len(configs.VLESS) {
fmt.Printf("%s Неверный номер конфига\n", red("✗"))
return
}
name := configs.VLESS[num-1].Name
configs.VLESS = append(configs.VLESS[:num-1], configs.VLESS[num:]...)
if err := config.SaveConfigs(configs); err != nil {
fmt.Printf("%s Ошибка сохранения конфигураций: %v\n", red("✗"), err)
return
}
fmt.Printf("%s Конфиг '%s' удален\n", green("✓"), name)
}
func connectVLESS() {
configs, err := config.LoadConfigs()
if err != nil {
fmt.Printf("%s Ошибка загрузки конфигураций: %v\n", red("✗"), err)
return
}
if len(configs.VLESS) == 0 {
fmt.Println("Нет конфигов для подключения")
return
}
fmt.Println("\n=== VLESS конфиги ===")
for i, cfg := range configs.VLESS {
protocol := cfg.Protocol
if protocol == "" {
protocol = "VLESS"
}
fmt.Printf("%d. [%s] %s\n", i+1, protocol, cfg.Name)
}
numStr := readInput("\nНомер конфига для подключения: ")
num, err := strconv.Atoi(numStr)
if err != nil || num < 1 || num > len(configs.VLESS) {
fmt.Printf("%s Неверный номер конфига\n", red("✗"))
return
}
configName := configs.VLESS[num-1].Name
// Показываем текущее ядро
settings, _ := config.LoadSettings()
coreName := "Xray"
if settings != nil && settings.CoreType == "v2ray" {
coreName = "V2Ray"
}
fmt.Printf("\nПодключение к '%s' через %s...\n", configName, coreName)
if err := vless.Connect(configName, config.LogsDir); err != nil {
fmt.Printf("%s Ошибка подключения: %v\n", red("✗"), err)
return
}
// Предлагаем запустить мониторинг
fmt.Println("\n" + strings.Repeat("─", 60))
choice := readInput("Запустить мониторинг в реальном времени? (y/n): ")
if strings.ToLower(choice) == "y" || strings.ToLower(choice) == "д" {
if err := MonitorConnection(); err != nil {
fmt.Printf("%s %v\n", red("✗"), err)
}
}
}
func testVLESSConfig() {
configs, err := config.LoadConfigs()
if err != nil {
fmt.Printf("%s Ошибка загрузки конфигураций: %v\n", red("✗"), err)
return
}
if len(configs.VLESS) == 0 {
fmt.Println("Нет конфигов для тестирования")
return
}
fmt.Println("\n=== VLESS конфиги ===")
for i, cfg := range configs.VLESS {
fmt.Printf("%d. %s\n", i+1, cfg.Name)
}
numStr := readInput("\nНомер конфига для тестирования: ")
num, err := strconv.Atoi(numStr)
if err != nil || num < 1 || num > len(configs.VLESS) {
fmt.Printf("%s Неверный номер конфига\n", red("✗"))
return
}
cfg := configs.VLESS[num-1]
fmt.Printf("\nТестирование '%s'...\n", cfg.Name)
fmt.Println("Проверка доступности сервера...")
success, ping, err := vless.PingServer(cfg.URL, 5000000000) // 5 секунд
if err != nil || !success {
fmt.Printf("%s Сервер недоступен\n", red("✗"))
if err != nil {
fmt.Printf(" Ошибка: %v\n", err)
}
return
}
fmt.Printf("%s Сервер доступен\n", green("✓"))
fmt.Printf(" Пинг: %.2f мс\n", ping)
// Оценка качества
var quality string
if ping < 50 {
quality = green("Отлично")
} else if ping < 100 {
quality = cyan("Хорошо")
} else if ping < 200 {
quality = yellow("Средне")
} else {
quality = red("Плохо")
}
fmt.Printf(" Качество: %s\n", quality)
}
func subscriptionsMenu() {
for {
clearScreen()
fmt.Println("\n" + strings.Repeat("=", 50))
fmt.Println(bold("Управление подписками"))
fmt.Println(strings.Repeat("=", 50))
fmt.Println("1. Список подписок")
fmt.Println("2. Добавить подписку")
fmt.Println("3. Удалить подписку")
fmt.Println("4. Обновить конфиги из подписки")
fmt.Println("5. Показать конфиги из подписки")
fmt.Println("6. Тестировать конфиги из подписки (пинг)")
fmt.Println("0. Назад")
fmt.Println(strings.Repeat("=", 50))
choice := readInput("\nВыберите действие: ")
switch choice {
case "0":
return
case "1":
listSubscriptions()
pause()
case "2":
addSubscription()
pause()
case "3":
deleteSubscription()
pause()
case "4":
updateSubscription()
pause()
case "5":
showSubscriptionConfigs()
pause()
case "6":
testSubscriptionConfigs()
pause()
default:
fmt.Printf("%s Неверный выбор\n", red("✗"))
pause()
}
}
}
func listSubscriptions() {
subs, err := config.LoadSubscriptions()
if err != nil {
fmt.Printf("%s Ошибка загрузки подписок: %v\n", red("✗"), err)
return
}
fmt.Println("\nПодписки:")
if len(subs.Subscriptions) == 0 {
fmt.Println("Нет подписок")
return
}
for i, sub := range subs.Subscriptions {
fmt.Printf("%d. %s - %s\n", i+1, sub.Name, sub.URL)
}
}
func addSubscription() {
name := readInput("Имя подписки: ")
if name == "" {
fmt.Printf("%s Имя не может быть пустым\n", red("✗"))
return
}
url := readInput("URL подписки: ")
if url == "" {
fmt.Printf("%s URL не может быть пустым\n", red("✗"))
return
}
subs, err := config.LoadSubscriptions()
if err != nil {
fmt.Printf("%s Ошибка загрузки подписок: %v\n", red("✗"), err)
return
}
subs.Subscriptions = append(subs.Subscriptions, config.Subscription{
Name: name,
URL: url,
})
if err := config.SaveSubscriptions(subs); err != nil {
fmt.Printf("%s Ошибка сохранения подписок: %v\n", red("✗"), err)
return
}
fmt.Printf("%s Подписка '%s' добавлена\n", green("✓"), name)
}
func deleteSubscription() {
subs, err := config.LoadSubscriptions()
if err != nil {
fmt.Printf("%s Ошибка загрузки подписок: %v\n", red("✗"), err)
return
}
if len(subs.Subscriptions) == 0 {
fmt.Println("Нет подписок для удаления")
return
}
fmt.Println("\nПодписки:")
for i, sub := range subs.Subscriptions {
fmt.Printf("%d. %s\n", i+1, sub.Name)
}
numStr := readInput("\nНомер подписки для удаления: ")
num, err := strconv.Atoi(numStr)
if err != nil || num < 1 || num > len(subs.Subscriptions) {
fmt.Printf("%s Неверный номер подписки\n", red("✗"))
return
}
name := subs.Subscriptions[num-1].Name
subs.Subscriptions = append(subs.Subscriptions[:num-1], subs.Subscriptions[num:]...)
if err := config.SaveSubscriptions(subs); err != nil {
fmt.Printf("%s Ошибка сохранения подписок: %v\n", red("✗"), err)
return
}
fmt.Printf("%s Подписка '%s' удалена\n", green("✓"), name)
}
func updateSubscription() {
subs, err := config.LoadSubscriptions()
if err != nil {
fmt.Printf("%s Ошибка загрузки подписок: %v\n", red("✗"), err)
return
}
if len(subs.Subscriptions) == 0 {
fmt.Println("Нет подписок")
return
}
fmt.Println("\nПодписки:")
for i, sub := range subs.Subscriptions {
fmt.Printf("%d. %s\n", i+1, sub.Name)
}
numStr := readInput("\nНомер подписки для обновления: ")
num, err := strconv.Atoi(numStr)
if err != nil || num < 1 || num > len(subs.Subscriptions) {
fmt.Printf("%s Неверный номер подписки\n", red("✗"))
return
}
name := subs.Subscriptions[num-1].Name
if err := subscription.UpdateSubscription(name, config.LogsDir); err != nil {
fmt.Printf("%s Ошибка обновления: %v\n", red("✗"), err)
return
}
}
func showSubscriptionConfigs() {
subs, err := config.LoadSubscriptions()
if err != nil {
fmt.Printf("%s Ошибка загрузки подписок: %v\n", red("✗"), err)
return
}
if len(subs.Subscriptions) == 0 {
fmt.Println("Нет подписок")
return
}
fmt.Println("\nПодписки:")
for i, sub := range subs.Subscriptions {
fmt.Printf("%d. %s\n", i+1, sub.Name)
}
numStr := readInput("\nНомер подписки: ")
num, err := strconv.Atoi(numStr)
if err != nil || num < 1 || num > len(subs.Subscriptions) {
fmt.Printf("%s Неверный номер подписки\n", red("✗"))
return
}
subName := subs.Subscriptions[num-1].Name
configs, err := config.LoadConfigs()
if err != nil {
fmt.Printf("%s Ошибка загрузки конфигураций: %v\n", red("✗"), err)
return
}
fmt.Printf("\n=== Конфиги из подписки '%s' ===\n", subName)
count := 0
for _, cfg := range configs.VLESS {
if cfg.Subscription == subName {
count++
protocol := cfg.Protocol
if protocol == "" {
protocol = "Unknown"
}
displayName := strings.TrimPrefix(cfg.Name, fmt.Sprintf("[%s] ", subName))
fmt.Printf("%d. [%s] %s\n", count, protocol, displayName)
}
}
if count == 0 {
fmt.Println("Нет конфигов из этой подписки")
fmt.Println("Сначала обновите конфиги из подписки (пункт 4)")
}
}
func testSubscriptionConfigs() {
subs, err := config.LoadSubscriptions()
if err != nil {
fmt.Printf("%s Ошибка загрузки подписок: %v\n", red("✗"), err)
return
}
if len(subs.Subscriptions) == 0 {
fmt.Println("Нет подписок")
return
}
fmt.Println("\nПодписки:")
for i, sub := range subs.Subscriptions {
fmt.Printf("%d. %s\n", i+1, sub.Name)
}
numStr := readInput("\nНомер подписки: ")
num, err := strconv.Atoi(numStr)
if err != nil || num < 1 || num > len(subs.Subscriptions) {
fmt.Printf("%s Неверный номер подписки\n", red("✗"))
return
}
subName := subs.Subscriptions[num-1].Name
configs, err := config.LoadConfigs()
if err != nil {
fmt.Printf("%s Ошибка загрузки конфигураций: %v\n", red("✗"), err)
return
}
var subConfigs []config.VLESSConfig
for _, cfg := range configs.VLESS {
if cfg.Subscription == subName {
subConfigs = append(subConfigs, cfg)
}
}
if len(subConfigs) == 0 {
fmt.Println("Нет конфигов из этой подписки")
return
}
fmt.Printf("\n=== Тестирование конфигов из '%s' ===\n", subName)
fmt.Printf("Найдено конфигов: %d\n\n", len(subConfigs))
type result struct {
name string
protocol string
ping float64
success bool
}
var results []result
for i, cfg := range subConfigs {
protocol := cfg.Protocol
if protocol == "" {
protocol = "Unknown"
}
displayName := strings.TrimPrefix(cfg.Name, fmt.Sprintf("[%s] ", subName))
fmt.Printf("%d/%d Тестирование [%s] %s...", i+1, len(subConfigs), protocol, truncate(displayName, 50))
success, ping, _ := vless.PingServer(cfg.URL, 3000000000) // 3 секунды
if success {
fmt.Printf(" %s %.2f мс\n", green("✓"), ping)
results = append(results, result{
name: displayName,
protocol: protocol,
ping: ping,
success: true,
})
} else {
fmt.Printf(" %s Недоступен\n", red("✗"))
results = append(results, result{
name: displayName,
protocol: protocol,
success: false,
})
}
}
// Сортируем по пингу
var successful []result
var failed []result
for _, r := range results {
if r.success {
successful = append(successful, r)
} else {
failed = append(failed, r)
}
}
// Простая сортировка пузырьком
for i := 0; i < len(successful); i++ {
for j := i + 1; j < len(successful); j++ {
if successful[i].ping > successful[j].ping {
successful[i], successful[j] = successful[j], successful[i]
}
}
}
fmt.Println("\n" + strings.Repeat("=", 60))
fmt.Println("РЕЗУЛЬТАТЫ ТЕСТИРОВАНИЯ")
fmt.Println(strings.Repeat("=", 60))
if len(successful) > 0 {
fmt.Printf("\n%s Доступно: %d\n", green("✓"), len(successful))
fmt.Println("\nЛучшие серверы:")
for i, r := range successful {
if i >= 5 {
break
}
fmt.Printf(" %d. [%s] %s - %.2f мс\n", i+1, r.protocol, truncate(r.name, 35), r.ping)
}
}
if len(failed) > 0 {
fmt.Printf("\n%s Недоступно: %d\n", red("✗"), len(failed))
}
fmt.Println(strings.Repeat("=", 60))
}
func clearScreen() {
if runtime.GOOS == "windows" {
cmd := exec.Command("cmd", "/c", "cls")
cmd.Stdout = os.Stdout
cmd.Run()
} else {
cmd := exec.Command("clear")
cmd.Stdout = os.Stdout
cmd.Run()
}
}
func readInput(prompt string) string {
fmt.Print(prompt)
reader := bufio.NewReader(os.Stdin)
input, _ := reader.ReadString('\n')
return strings.TrimSpace(input)
}
func pause() {
fmt.Print("\nНажмите Enter для продолжения...")
bufio.NewReader(os.Stdin).ReadBytes('\n')
}
func truncate(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "..."
}
func wireguardMenu() {
for {
clearScreen()
fmt.Println("\n" + strings.Repeat("=", 50))
fmt.Println(bold("WireGuard"))
fmt.Println(strings.Repeat("=", 50))
fmt.Println("1. Список конфигураций")
fmt.Println("2. Добавить конфиг (вручную)")
fmt.Println("3. Добавить конфиг (из файла)")
fmt.Println("4. Удалить конфиг")
fmt.Println("5. Подключиться")
fmt.Println("0. Назад")
fmt.Println(strings.Repeat("=", 50))
choice := readInput("\nВыберите действие: ")
switch choice {
case "0":
return
case "1":
listWireGuardConfigs()
pause()
case "2":
addWireGuardConfigManual()
pause()
case "3":
addWireGuardConfigFromFile()
pause()
case "4":
deleteWireGuardConfig()
pause()
case "5":
connectWireGuard()
pause()
default:
fmt.Printf("%s Неверный выбор\n", red("✗"))
pause()
}
}
}
func listWireGuardConfigs() {
configs, err := config.LoadConfigs()
if err != nil {
fmt.Printf("%s Ошибка загрузки конфигураций: %v\n", red("✗"), err)
return
}
fmt.Println("\n=== WireGuard конфиги ===")
if len(configs.WireGuard) == 0 {
fmt.Println("Нет конфигов")
return
}
for i, cfg := range configs.WireGuard {
fmt.Printf("%d. %s\n", i+1, cfg.Name)
}
}
func addWireGuardConfigManual() {
name := readInput("Имя конфига: ")
if name == "" {
fmt.Printf("%s Имя не может быть пустым\n", red("✗"))
return
}
fmt.Println("Вставьте конфиг WireGuard (завершите пустой строкой):")
var lines []string
reader := bufio.NewReader(os.Stdin)
for {
line, _ := reader.ReadString('\n')
line = strings.TrimRight(line, "\r\n")
if line == "" {
break
}
lines = append(lines, line)
}
if len(lines) == 0 {
fmt.Printf("%s Конфиг не может быть пустым\n", red("✗"))
return
}
configText := strings.Join(lines, "\n")
if err := wireguard.AddConfig(name, configText); err != nil {
fmt.Printf("%s Ошибка добавления конфига: %v\n", red("✗"), err)
return
}
fmt.Printf("%s WireGuard конфиг '%s' добавлен\n", green("✓"), name)
}
func addWireGuardConfigFromFile() {
name := readInput("Имя конфига: ")
if name == "" {
fmt.Printf("%s Имя не может быть пустым\n", red("✗"))
return
}
filePath := readInput("Путь к файлу конфига: ")
if filePath == "" {
fmt.Printf("%s Путь не может быть пустым\n", red("✗"))
return
}
if err := wireguard.AddConfigFromFile(name, filePath); err != nil {
fmt.Printf("%s Ошибка добавления конфига: %v\n", red("✗"), err)
return
}
fmt.Printf("%s WireGuard конфиг '%s' добавлен из файла\n", green("✓"), name)
}
func deleteWireGuardConfig() {
configs, err := config.LoadConfigs()
if err != nil {
fmt.Printf("%s Ошибка загрузки конфигураций: %v\n", red("✗"), err)
return
}
if len(configs.WireGuard) == 0 {
fmt.Println("Нет конфигов для удаления")
return
}
fmt.Println("\n=== WireGuard конфиги ===")
for i, cfg := range configs.WireGuard {
fmt.Printf("%d. %s\n", i+1, cfg.Name)
}
numStr := readInput("\nНомер конфига для удаления: ")
num, err := strconv.Atoi(numStr)
if err != nil || num < 1 || num > len(configs.WireGuard) {
fmt.Printf("%s Неверный номер конфига\n", red("✗"))
return
}
name := configs.WireGuard[num-1].Name
if err := wireguard.DeleteConfig(name); err != nil {
fmt.Printf("%s Ошибка удаления конфига: %v\n", red("✗"), err)
return
}
fmt.Printf("%s Конфиг '%s' удален\n", green("✓"), name)
}
func connectWireGuard() {
configs, err := config.LoadConfigs()
if err != nil {
fmt.Printf("%s Ошибка загрузки конфигураций: %v\n", red("✗"), err)
return
}
if len(configs.WireGuard) == 0 {
fmt.Println("Нет конфигов для подключения")
return
}
fmt.Println("\n=== WireGuard конфиги ===")
for i, cfg := range configs.WireGuard {
fmt.Printf("%d. %s\n", i+1, cfg.Name)
}
numStr := readInput("\nНомер конфига для подключения: ")
num, err := strconv.Atoi(numStr)
if err != nil || num < 1 || num > len(configs.WireGuard) {
fmt.Printf("%s Неверный номер конфига\n", red("✗"))
return
}
configName := configs.WireGuard[num-1].Name
if err := wireguard.Connect(configName, config.LogsDir); err != nil {
fmt.Printf("%s Ошибка подключения: %v\n", red("✗"), err)
return
}
// Предлагаем запустить мониторинг
fmt.Println("\n" + strings.Repeat("─", 60))
choice := readInput("Запустить мониторинг в реальном времени? (y/n): ")
if strings.ToLower(choice) == "y" || strings.ToLower(choice) == "д" {
if err := MonitorConnection(); err != nil {
fmt.Printf("%s %v\n", red("✗"), err)
}
}
}
func settingsMenu() {
clearScreen()
settings, err := config.LoadSettings()
if err != nil {
fmt.Printf("%s Ошибка загрузки настроек: %v\n", red("✗"), err)
pause()
return
}
currentCore := "Xray"
if settings.CoreType == "v2ray" {
currentCore = "V2Ray"
}
fmt.Println("\n" + strings.Repeat("=", 60))
fmt.Println(bold("Настройки"))
fmt.Println(strings.Repeat("=", 60))
fmt.Printf("Текущее ядро для VLESS: %s\n", cyan(currentCore))
fmt.Println(strings.Repeat("=", 60))
fmt.Println("\nВыберите ядро для подключения к VLESS:")
fmt.Println("1. Xray (рекомендуется)")
fmt.Println("2. V2Ray")
fmt.Println("0. Назад")
fmt.Println(strings.Repeat("=", 60))
choice := readInput("\nВыберите действие: ")
switch choice {
case "1":
settings.CoreType = "xray"
if err := config.SaveSettings(settings); err != nil {
fmt.Printf("%s Ошибка сохранения настроек: %v\n", red("✗"), err)
} else {
fmt.Printf("%s Ядро изменено на Xray\n", green("✓"))
}
pause()
case "2":
settings.CoreType = "v2ray"
if err := config.SaveSettings(settings); err != nil {
fmt.Printf("%s Ошибка сохранения настроек: %v\n", red("✗"), err)
} else {
fmt.Printf("%s Ядро изменено на V2Ray\n", green("✓"))
}
pause()
case "0":
return
default:
fmt.Printf("%s Неверный выбор\n", red("✗"))
pause()
}
}
func showVLESSLogs() {
fmt.Println("\n" + strings.Repeat("=", 60))
fmt.Println(bold("Логи VLESS/Xray/V2Ray"))
fmt.Println(strings.Repeat("=", 60))
errorLog := filepath.Join(config.LogsDir, "vless_error.log")
accessLog := filepath.Join(config.LogsDir, "vless_access.log")
// Показываем лог ошибок
fmt.Println("\n" + yellow("=== Лог ошибок (последние 30 строк) ==="))
if data, err := os.ReadFile(errorLog); err == nil && len(data) > 0 {
lines := strings.Split(string(data), "\n")
start := len(lines) - 30
if start < 0 {
start = 0
}
for i := start; i < len(lines); i++ {
if strings.TrimSpace(lines[i]) != "" {
fmt.Println(lines[i])
}
}
} else {
fmt.Println("Лог пуст или не найден")
}
fmt.Println("\n" + cyan("=== Лог доступа (последние 20 строк) ==="))
if data, err := os.ReadFile(accessLog); err == nil && len(data) > 0 {
lines := strings.Split(string(data), "\n")
start := len(lines) - 20
if start < 0 {
start = 0
}
for i := start; i < len(lines); i++ {
if strings.TrimSpace(lines[i]) != "" {
fmt.Println(lines[i])
}
}
} else {
fmt.Println("Лог пуст или не найден")
}
fmt.Println("\n" + strings.Repeat("=", 60))
fmt.Printf("Полные логи:\n")
fmt.Printf(" Ошибки: %s\n", errorLog)
fmt.Printf(" Доступ: %s\n", accessLog)
}