Files
Go-VPN-Client/internal/cli/cli.go
arkonsadter 20d24a3639 feat(cli): add settings menu and VLESS log viewer with core selection
- Add settings menu to switch between Xray and V2Ray cores for VLESS connections
- Implement core type persistence in configuration with LoadSettings/SaveSettings
- Add VLESS error and access log viewer showing last 30 and 20 lines respectively
- Display current core type and system time in main menu
- Update VLESS connection to use selected core dynamically
- Refactor monitor.go to accept 'q' key input for graceful exit instead of signal handling
- Add proxy platform-specific implementations (proxy_unix.go, proxy_windows.go)
- Add downloader module for managing binary resources
- Include V2Ray and Xray configuration files and geodata (geoip.dat, geosite.dat)
- Update CLI imports to include path/filepath and time packages
- Improve user experience with core selection visibility and log diagnostics
2026-04-06 20:06:35 +06:00

1045 lines
27 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 {
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("Настройки (выбор ядра)"))
} else {
fmt.Println("5. Отключиться от VPN")
fmt.Println("6. " + yellow("Настройки (выбор ядра)"))
}
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)
}