package cli import ( "bufio" "fmt" "os" "os/exec" "runtime" "strconv" "strings" "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("Выберите действие: ") 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 err := vpn.Disconnect(config.LogsDir); err != nil { fmt.Printf("%s %v\n", red("✗"), err) } pause() case "0": fmt.Println("До свидания!") return nil default: fmt.Printf("%s Неверный выбор\n", red("✗")) pause() } } } func showMainMenu() { state, _ := vpn.GetStatus() statusIcon := "○" statusText := "Не подключено" if state != nil && state.Connected { statusIcon = "✓" statusText = fmt.Sprintf("Подключено: %s", state.ConfigName) } fmt.Println("\n" + strings.Repeat("=", 50)) fmt.Println(bold("VPN Клиент (Go)")) fmt.Println(strings.Repeat("=", 50)) fmt.Printf("Статус: %s %s\n", statusIcon, statusText) fmt.Println(strings.Repeat("=", 50)) fmt.Println("1. WireGuard") fmt.Println("2. VLESS") fmt.Println("3. Управление подписками") fmt.Println("4. Показать статус подключения") fmt.Println("5. Отключиться от VPN") fmt.Println("0. Выход") fmt.Println(strings.Repeat("=", 50)) } 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("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() 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 fmt.Printf("\nПодключение к '%s' через Xray...\n", configName) if err := vless.Connect(configName, config.LogsDir, config.XrayDir); err != nil { fmt.Printf("%s Ошибка подключения: %v\n", red("✗"), err) return } } 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 } }