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
This commit is contained in:
2026-04-06 20:06:35 +06:00
parent d88139af1b
commit 20d24a3639
19 changed files with 45913 additions and 45 deletions

View File

@@ -5,9 +5,11 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"vpn-client/internal/config"
"vpn-client/internal/subscription"
@@ -69,6 +71,14 @@ func Run() error {
fmt.Printf("%s %v\n", red("✗"), err)
}
pause()
} else {
// Настройки
settingsMenu()
}
case "7":
if isConnected {
// Настройки
settingsMenu()
} else {
fmt.Printf("%s Неверный выбор\n", red("✗"))
pause()
@@ -86,12 +96,25 @@ func Run() error {
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")
@@ -101,8 +124,10 @@ func showMainMenu() {
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. Выход")
@@ -120,6 +145,7 @@ func vlessMenu() {
fmt.Println("3. Удалить конфиг")
fmt.Println("4. Подключиться")
fmt.Println("5. Тестировать конфиг (пинг)")
fmt.Println("6. " + yellow("Показать логи ошибок"))
fmt.Println("0. Назад")
fmt.Println(strings.Repeat("=", 50))
@@ -143,6 +169,9 @@ func vlessMenu() {
case "5":
testVLESSConfig()
pause()
case "6":
showVLESSLogs()
pause()
default:
fmt.Printf("%s Неверный выбор\n", red("✗"))
pause()
@@ -270,9 +299,16 @@ func connectVLESS() {
configName := configs.VLESS[num-1].Name
fmt.Printf("\nПодключение к '%s' через Xray...\n", configName)
// Показываем текущее ядро
settings, _ := config.LoadSettings()
coreName := "Xray"
if settings != nil && settings.CoreType == "v2ray" {
coreName = "V2Ray"
}
if err := vless.Connect(configName, config.LogsDir, config.XrayDir); err != nil {
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
}
@@ -904,3 +940,105 @@ func connectWireGuard() {
}
}
}
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)
}

View File

@@ -3,7 +3,8 @@ package cli
import (
"fmt"
"os"
"os/signal"
"os/exec"
"runtime"
"strings"
"syscall"
"time"
@@ -25,20 +26,22 @@ func MonitorConnection() error {
return nil
}
fmt.Println("Нажмите Ctrl+C для выхода из мониторинга\n")
fmt.Println("Нажмите 'q' или Ctrl+C для выхода из мониторинга\n")
time.Sleep(1 * time.Second)
// Создаем канал для обработки сигналов
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
// Создаем канал для остановки мониторинга
stopChan := make(chan bool, 1)
// Запускаем горутину для обработки сигналов
// Запускаем горутину для чтения клавиш
go func() {
<-sigChan
stopChan <- true
for {
var input string
fmt.Scanln(&input)
if strings.ToLower(input) == "q" || strings.ToLower(input) == "й" {
stopChan <- true
return
}
}
}()
// Запускаем мониторинг
@@ -50,9 +53,8 @@ func MonitorConnection() error {
case <-ticker.C:
clearScreen()
displayRealtimeStatus(state)
fmt.Printf("\n%s Нажмите 'q' и Enter для выхода\n", cyan(""))
case <-stopChan:
// Восстанавливаем обработку сигналов по умолчанию
signal.Reset(os.Interrupt, syscall.SIGTERM)
fmt.Println("\n\nВыход из мониторинга...")
time.Sleep(500 * time.Millisecond)
return nil
@@ -104,7 +106,7 @@ func displayRealtimeStatus(state *config.ConnectionState) {
}
fmt.Println(strings.Repeat("═", 70))
fmt.Printf("\n%s Обновление каждую секунду | Нажмите Ctrl+C для выхода\n",
fmt.Printf("\n%s Нажмите 'q' и Enter для выхода\n",
cyan(""))
}
@@ -126,17 +128,39 @@ func displayVLESSStats(state *config.ConnectionState) {
}
}
// Проверяем, что процесс еще работает
// Проверяем, что процесс еще работает (улучшенная проверка для Windows)
if state.ProcessPID > 0 {
process, err := os.FindProcess(state.ProcessPID)
if err == nil {
if err := process.Signal(os.Signal(nil)); err != nil {
fmt.Printf("\n%s Процесс Xray не отвечает!\n", red("⚠"))
}
processRunning := checkProcessRunning(state.ProcessPID)
if !processRunning {
fmt.Printf("\n%s Процесс Xray не отвечает!\n", red("⚠"))
}
}
}
// checkProcessRunning проверяет, работает ли процесс
func checkProcessRunning(pid int) bool {
process, err := os.FindProcess(pid)
if err != nil {
return false
}
// На Windows FindProcess всегда успешен, нужна дополнительная проверка
// Пытаемся получить информацию о процессе через tasklist
if runtime.GOOS == "windows" {
cmd := exec.Command("tasklist", "/FI", fmt.Sprintf("PID eq %d", pid), "/NH")
output, err := cmd.Output()
if err != nil {
return false
}
// Если процесс существует, в выводе будет его PID
return strings.Contains(string(output), fmt.Sprintf("%d", pid))
}
// На Unix используем сигнал 0
err = process.Signal(syscall.Signal(0))
return err == nil
}
func displayWireGuardStats(state *config.ConnectionState) {
fmt.Println("\n" + bold("WireGuard Туннель"))
fmt.Printf("Интерфейс: %s\n", state.Interface)
@@ -198,7 +222,8 @@ func ShowQuickStatus() string {
duration := time.Since(startTime)
hours := int(duration.Hours())
minutes := int(duration.Minutes()) % 60
timeStr = fmt.Sprintf(" [%02d:%02d]", hours, minutes)
seconds := int(duration.Seconds()) % 60
timeStr = fmt.Sprintf(" [%02d:%02d:%02d]", hours, minutes, seconds)
}
}