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

@@ -11,10 +11,12 @@ import (
"runtime"
"strconv"
"strings"
"syscall"
"time"
"vpn-client/internal/config"
"vpn-client/internal/logger"
"vpn-client/internal/proxy"
)
// XrayConfig представляет конфигурацию Xray
@@ -331,7 +333,35 @@ func getParam(params map[string]string, key, defaultValue string) string {
}
// Connect подключается к VLESS серверу
func Connect(configName string, logsDir, xrayDir string) error {
func Connect(configName string, logsDir string) error {
// Загружаем настройки
settings, err := config.LoadSettings()
if err != nil {
return fmt.Errorf("ошибка загрузки настроек: %w", err)
}
// Определяем директорию и имя исполняемого файла
var coreDir, coreExe, coreName string
if settings.CoreType == "v2ray" {
coreDir = config.V2RayDir
coreName = "V2Ray"
if runtime.GOOS == "windows" {
coreExe = "v2ray.exe"
} else {
coreExe = "v2ray"
}
} else {
coreDir = config.XrayDir
coreName = "Xray"
if runtime.GOOS == "windows" {
coreExe = "xray.exe"
} else {
coreExe = "xray"
}
}
corePath := filepath.Join(coreDir, coreExe)
// Загружаем конфигурации
configs, err := config.LoadConfigs()
if err != nil {
@@ -351,15 +381,15 @@ func Connect(configName string, logsDir, xrayDir string) error {
return fmt.Errorf("конфиг '%s' не найден", configName)
}
// Создаем конфигурацию Xray
xrayConfig, err := ParseVLESSURL(vlessConfig.URL, logsDir)
// Создаем конфигурацию
coreConfig, err := ParseVLESSURL(vlessConfig.URL, logsDir)
if err != nil {
return fmt.Errorf("ошибка парсинга VLESS URL: %w", err)
}
// Сохраняем конфигурацию
configPath := filepath.Join(config.ConfigDir, "xray_config.json")
data, err := json.MarshalIndent(xrayConfig, "", " ")
data, err := json.MarshalIndent(coreConfig, "", " ")
if err != nil {
return fmt.Errorf("ошибка сериализации конфига: %w", err)
}
@@ -368,15 +398,9 @@ func Connect(configName string, logsDir, xrayDir string) error {
return fmt.Errorf("ошибка сохранения конфига: %w", err)
}
// Путь к xray
xrayExe := "xray"
if runtime.GOOS == "windows" {
xrayExe = "xray.exe"
}
xrayPath := filepath.Join(xrayDir, xrayExe)
if _, err := os.Stat(xrayPath); os.IsNotExist(err) {
return fmt.Errorf("xray не найден в %s", xrayDir)
// Проверяем наличие исполняемого файла
if _, err := os.Stat(corePath); os.IsNotExist(err) {
return fmt.Errorf("%s не найден в %s\n\nПожалуйста, перезапустите приложение для автоматической загрузки %s", coreName, coreDir, coreName)
}
// Создаем лог-файл трафика
@@ -389,27 +413,70 @@ func Connect(configName string, logsDir, xrayDir string) error {
// Записываем заголовок
fmt.Fprintf(logFile, "=== VPN Подключение начато: %s ===\n", time.Now().Format("2006-01-02 15:04:05"))
fmt.Fprintf(logFile, "Конфиг: %s\n", configName)
fmt.Fprintf(logFile, "Клиент: Xray\n")
fmt.Fprintf(logFile, "Клиент: %s\n", coreName)
fmt.Fprintf(logFile, "Прокси: 127.0.0.1:10808\n")
fmt.Fprintf(logFile, "%s\n\n", strings.Repeat("=", 60))
// Запускаем xray
cmd := exec.Command(xrayPath, "run", "-c", configPath)
// Запускаем ядро
cmd := exec.Command(corePath, "run", "-c", configPath)
cmd.Stdout = logFile
cmd.Stderr = logFile
// На Windows создаем процесс в новой группе, чтобы Ctrl+C не убивал его
if runtime.GOOS == "windows" {
cmd.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
}
}
if err := cmd.Start(); err != nil {
logFile.Close()
return fmt.Errorf("ошибка запуска xray: %w", err)
return fmt.Errorf("ошибка запуска %s: %w", coreName, err)
}
// Ждем немного для проверки запуска
time.Sleep(3 * time.Second)
time.Sleep(2 * time.Second)
// Проверяем, что процесс еще работает
if err := cmd.Process.Signal(os.Signal(nil)); err != nil {
// Проверяем, что процесс еще работает (для Windows используем другой метод)
processRunning := true
if runtime.GOOS == "windows" {
// На Windows просто проверяем, что процесс существует
process, err := os.FindProcess(cmd.Process.Pid)
if err != nil {
processRunning = false
} else {
// Пытаемся отправить сигнал 0 (проверка существования)
err = process.Signal(syscall.Signal(0))
// На Windows это всегда возвращает ошибку, поэтому просто считаем что процесс работает
processRunning = true
}
} else {
// На Unix используем стандартную проверку
err := cmd.Process.Signal(syscall.Signal(0))
processRunning = (err == nil)
}
if !processRunning {
logFile.Close()
return fmt.Errorf("процесс xray завершился с ошибкой")
// Читаем логи для диагностики
errorLog := filepath.Join(logsDir, "vless_error.log")
if errorData, readErr := os.ReadFile(errorLog); readErr == nil && len(errorData) > 0 {
lines := strings.Split(string(errorData), "\n")
errorLines := []string{}
for i := len(lines) - 1; i >= 0 && len(errorLines) < 10; i-- {
if strings.TrimSpace(lines[i]) != "" {
errorLines = append([]string{lines[i]}, errorLines...)
}
}
if len(errorLines) > 0 {
return fmt.Errorf("процесс %s завершился с ошибкой.\n\nПоследние строки из лога ошибок:\n%s\n\nПолный лог: %s",
coreName, strings.Join(errorLines, "\n"), errorLog)
}
}
return fmt.Errorf("процесс %s завершился с ошибкой.\nПроверьте логи в: %s", coreName, errorLog)
}
// Сохраняем состояние
@@ -418,7 +485,7 @@ func Connect(configName string, logsDir, xrayDir string) error {
ConfigName: configName,
ConfigType: "vless",
StartTime: time.Now().Format(time.RFC3339),
Interface: "xray",
Interface: strings.ToLower(coreName),
ProcessPID: cmd.Process.Pid,
LogFile: trafficLog,
}
@@ -431,10 +498,37 @@ func Connect(configName string, logsDir, xrayDir string) error {
// Логируем успешное подключение
logPath := logger.GetLogPath(logsDir, "vless")
logger.LogMessage(logPath, fmt.Sprintf("Успешно подключено к '%s' (PID: %d, Лог: %s)", configName, cmd.Process.Pid, trafficLog))
logger.LogMessage(logPath, fmt.Sprintf("Успешно подключено к '%s' через %s (PID: %d, Лог: %s)", configName, coreName, cmd.Process.Pid, trafficLog))
fmt.Printf("✓ Подключено к '%s'\n", configName)
fmt.Printf("✓ Подключено к '%s' через %s\n", configName, coreName)
fmt.Printf("SOCKS5 прокси: 127.0.0.1:10808\n")
// Предлагаем настроить системный прокси
fmt.Println("\n" + strings.Repeat("─", 60))
fmt.Println("Настроить системный прокси Windows?")
fmt.Println("Это позволит всем приложениям использовать VPN.")
fmt.Print("(y/n): ")
var response string
fmt.Scanln(&response)
if strings.ToLower(response) == "y" || strings.ToLower(response) == "д" {
if err := proxy.EnableSystemProxy("127.0.0.1:10808"); err != nil {
fmt.Printf("⚠ Не удалось настроить системный прокси: %v\n", err)
fmt.Println("Вы можете настроить его вручную в настройках Windows")
} else {
fmt.Println("✓ Системный прокси настроен")
}
} else {
fmt.Println("\nДля использования VPN настройте прокси вручную:")
fmt.Println(" 1. Откройте Настройки Windows → Сеть и Интернет → Прокси")
fmt.Println(" 2. Включите 'Использовать прокси-сервер'")
fmt.Println(" 3. Адрес: 127.0.0.1, Порт: 10808")
fmt.Println("\nИли настройте SOCKS5 прокси в браузере:")
fmt.Println(" Firefox: Настройки → Основные → Параметры сети")
fmt.Println(" Chrome: Настройки → Система → Открыть настройки прокси")
}
fmt.Printf("\nЛоги трафика:\n")
fmt.Printf(" - Основной: %s\n", trafficLog)
fmt.Printf(" - Доступ (IP): %s\n", filepath.Join(logsDir, "vless_access.log"))