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)
}
}

View File

@@ -11,8 +11,10 @@ var (
SubscriptionsFile string
ConfigsFile string
StateFile string
SettingsFile string
LogsDir string
XrayDir string
V2RayDir string
)
type WireGuardConfig struct {
@@ -51,6 +53,10 @@ type ConnectionState struct {
LogFile string `json:"log_file"`
}
type Settings struct {
CoreType string `json:"core_type"` // "xray" или "v2ray"
}
// Init инициализирует конфигурационные директории и файлы
func Init() error {
// Получаем рабочую директорию
@@ -63,8 +69,10 @@ func Init() error {
SubscriptionsFile = filepath.Join(ConfigDir, "subscriptions.json")
ConfigsFile = filepath.Join(ConfigDir, "configs.json")
StateFile = filepath.Join(ConfigDir, "state.json")
SettingsFile = filepath.Join(ConfigDir, "settings.json")
LogsDir = filepath.Join(workDir, "logs")
XrayDir = filepath.Join(workDir, "xray")
V2RayDir = filepath.Join(workDir, "v2ray")
// Создаем директории
if err := os.MkdirAll(ConfigDir, 0755); err != nil {
@@ -84,6 +92,9 @@ func Init() error {
if err := initFileIfNotExists(StateFile, ConnectionState{}); err != nil {
return err
}
if err := initFileIfNotExists(SettingsFile, Settings{CoreType: "xray"}); err != nil {
return err
}
return nil
}
@@ -170,3 +181,32 @@ func SaveState(state *ConnectionState) error {
}
return os.WriteFile(StateFile, data, 0644)
}
// LoadSettings загружает настройки
func LoadSettings() (*Settings, error) {
data, err := os.ReadFile(SettingsFile)
if err != nil {
return nil, err
}
var settings Settings
if err := json.Unmarshal(data, &settings); err != nil {
return nil, err
}
// По умолчанию xray
if settings.CoreType == "" {
settings.CoreType = "xray"
}
return &settings, nil
}
// SaveSettings сохраняет настройки
func SaveSettings(settings *Settings) error {
data, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
}
return os.WriteFile(SettingsFile, data, 0644)
}

View File

@@ -0,0 +1,273 @@
package downloader
import (
"archive/zip"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/fatih/color"
)
const (
XrayVersion = "24.12.18"
V2RayVersion = "5.22.0"
XrayDownloadURL = "https://github.com/XTLS/Xray-core/releases/download/v%s/Xray-windows-64.zip"
V2RayDownloadURL = "https://github.com/v2fly/v2ray-core/releases/download/v%s/v2ray-windows-64.zip"
)
var (
green = color.New(color.FgGreen).SprintFunc()
yellow = color.New(color.FgYellow).SprintFunc()
cyan = color.New(color.FgCyan).SprintFunc()
)
// DownloadCore загружает Xray или V2Ray
func DownloadCore(coreDir, coreName string) error {
var downloadURL, version, exeName string
if coreName == "xray" {
downloadURL = fmt.Sprintf(XrayDownloadURL, XrayVersion)
version = XrayVersion
exeName = "xray.exe"
} else if coreName == "v2ray" {
downloadURL = fmt.Sprintf(V2RayDownloadURL, V2RayVersion)
version = V2RayVersion
exeName = "v2ray.exe"
} else {
return fmt.Errorf("неизвестный тип ядра: %s", coreName)
}
// Проверяем, существует ли уже
coreExe := filepath.Join(coreDir, exeName)
if _, err := os.Stat(coreExe); err == nil {
return nil
}
fmt.Println("\n" + strings.Repeat("=", 60))
fmt.Printf("%s %s не найден. Начинаю загрузку...\n", yellow("⚠"), strings.ToUpper(coreName))
fmt.Println(strings.Repeat("=", 60))
// Создаем директорию
if err := os.MkdirAll(coreDir, 0755); err != nil {
return fmt.Errorf("ошибка создания директории: %w", err)
}
fmt.Printf("\nВерсия: %s\n", cyan(version))
fmt.Printf("URL: %s\n\n", downloadURL)
// Загружаем файл
zipPath := filepath.Join(coreDir, coreName+".zip")
if err := downloadFile(zipPath, downloadURL); err != nil {
return fmt.Errorf("ошибка загрузки: %w", err)
}
fmt.Printf("\n%s Загрузка завершена\n", green("✓"))
fmt.Printf("%s Распаковка архива...\n", yellow("⚙"))
// Распаковываем
if err := unzip(zipPath, coreDir); err != nil {
os.Remove(zipPath)
return fmt.Errorf("ошибка распаковки: %w", err)
}
os.Remove(zipPath)
fmt.Printf("%s %s успешно установлен в %s\n", green("✓"), strings.ToUpper(coreName), coreDir)
fmt.Println(strings.Repeat("=", 60))
return nil
}
// DownloadXray загружает Xray
func DownloadXray(xrayDir string) error {
return DownloadCore(xrayDir, "xray")
}
// DownloadV2Ray загружает V2Ray
func DownloadV2Ray(v2rayDir string) error {
return DownloadCore(v2rayDir, "v2ray")
}
// downloadFile загружает файл с прогресс-баром
func downloadFile(filepath string, url string) error {
out, err := os.Create(filepath)
if err != nil {
return err
}
defer out.Close()
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("ошибка загрузки: HTTP %d", resp.StatusCode)
}
size := resp.ContentLength
counter := &WriteCounter{Total: size}
_, err = io.Copy(out, io.TeeReader(resp.Body, counter))
if err != nil {
return err
}
fmt.Println()
return nil
}
// WriteCounter считает загруженные байты
type WriteCounter struct {
Total int64
Downloaded int64
}
func (wc *WriteCounter) Write(p []byte) (int, error) {
n := len(p)
wc.Downloaded += int64(n)
wc.PrintProgress()
return n, nil
}
func (wc *WriteCounter) PrintProgress() {
fmt.Printf("\r%s", strings.Repeat(" ", 60))
percent := float64(wc.Downloaded) / float64(wc.Total) * 100
downloaded := formatBytes(wc.Downloaded)
total := formatBytes(wc.Total)
fmt.Printf("\rЗагрузка: %s / %s (%.1f%%)", downloaded, total, percent)
}
func formatBytes(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}
// unzip распаковывает zip архив
func unzip(src, dest string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer r.Close()
for _, f := range r.File {
fpath := filepath.Join(dest, f.Name)
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
return fmt.Errorf("недопустимый путь файла: %s", fpath)
}
if f.FileInfo().IsDir() {
os.MkdirAll(fpath, os.ModePerm)
continue
}
if err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return err
}
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
rc, err := f.Open()
if err != nil {
outFile.Close()
return err
}
_, err = io.Copy(outFile, rc)
outFile.Close()
rc.Close()
if err != nil {
return err
}
}
return nil
}
// CheckAndDownloadXray проверяет и загружает Xray
func CheckAndDownloadXray(xrayDir string) error {
if runtime.GOOS != "windows" {
return nil
}
xrayExe := filepath.Join(xrayDir, "xray.exe")
if _, err := os.Stat(xrayExe); os.IsNotExist(err) {
return DownloadXray(xrayDir)
}
return nil
}
// CheckAndDownloadV2Ray проверяет и загружает V2Ray
func CheckAndDownloadV2Ray(v2rayDir string) error {
if runtime.GOOS != "windows" {
return nil
}
v2rayExe := filepath.Join(v2rayDir, "v2ray.exe")
if _, err := os.Stat(v2rayExe); os.IsNotExist(err) {
return DownloadV2Ray(v2rayDir)
}
return nil
}
// CheckAndDownloadBoth проверяет и загружает оба ядра
func CheckAndDownloadBoth(xrayDir, v2rayDir string) error {
if runtime.GOOS != "windows" {
return nil
}
// Проверяем Xray
xrayExe := filepath.Join(xrayDir, "xray.exe")
xrayExists := true
if _, err := os.Stat(xrayExe); os.IsNotExist(err) {
xrayExists = false
}
// Проверяем V2Ray
v2rayExe := filepath.Join(v2rayDir, "v2ray.exe")
v2rayExists := true
if _, err := os.Stat(v2rayExe); os.IsNotExist(err) {
v2rayExists = false
}
// Если оба есть, ничего не делаем
if xrayExists && v2rayExists {
return nil
}
// Загружаем недостающие
if !xrayExists {
if err := DownloadXray(xrayDir); err != nil {
return err
}
}
if !v2rayExists {
if err := DownloadV2Ray(v2rayDir); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,22 @@
// +build !windows
package proxy
import (
"fmt"
)
// EnableSystemProxy включает системный прокси (заглушка для Unix)
func EnableSystemProxy(proxyAddr string) error {
return fmt.Errorf("автоматическая настройка системного прокси не поддерживается на этой платформе")
}
// DisableSystemProxy отключает системный прокси (заглушка для Unix)
func DisableSystemProxy() error {
return nil
}
// GetSystemProxyStatus проверяет статус системного прокси (заглушка для Unix)
func GetSystemProxyStatus() (bool, string, error) {
return false, "", nil
}

View File

@@ -0,0 +1,111 @@
// +build windows
package proxy
import (
"fmt"
"os/exec"
"strings"
)
// EnableSystemProxy включает системный прокси в Windows
func EnableSystemProxy(proxyAddr string) error {
// Включаем прокси через реестр
cmd := exec.Command("reg", "add",
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings",
"/v", "ProxyEnable",
"/t", "REG_DWORD",
"/d", "1",
"/f")
if err := cmd.Run(); err != nil {
return fmt.Errorf("ошибка включения прокси: %w", err)
}
// Устанавливаем адрес прокси
cmd = exec.Command("reg", "add",
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings",
"/v", "ProxyServer",
"/t", "REG_SZ",
"/d", fmt.Sprintf("socks=%s", proxyAddr),
"/f")
if err := cmd.Run(); err != nil {
return fmt.Errorf("ошибка установки адреса прокси: %w", err)
}
// Обновляем настройки Internet Explorer (применяет изменения)
refreshIESettings()
return nil
}
// DisableSystemProxy отключает системный прокси в Windows
func DisableSystemProxy() error {
// Отключаем прокси
cmd := exec.Command("reg", "add",
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings",
"/v", "ProxyEnable",
"/t", "REG_DWORD",
"/d", "0",
"/f")
if err := cmd.Run(); err != nil {
return fmt.Errorf("ошибка отключения прокси: %w", err)
}
// Очищаем адрес прокси
cmd = exec.Command("reg", "delete",
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings",
"/v", "ProxyServer",
"/f")
cmd.Run() // Игнорируем ошибку, если ключ не существует
// Обновляем настройки
refreshIESettings()
return nil
}
// GetSystemProxyStatus проверяет статус системного прокси
func GetSystemProxyStatus() (bool, string, error) {
// Проверяем, включен ли прокси
cmd := exec.Command("reg", "query",
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings",
"/v", "ProxyEnable")
output, err := cmd.Output()
if err != nil {
return false, "", nil
}
enabled := strings.Contains(string(output), "0x1")
// Получаем адрес прокси
cmd = exec.Command("reg", "query",
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings",
"/v", "ProxyServer")
output, err = cmd.Output()
proxyAddr := ""
if err == nil {
lines := strings.Split(string(output), "\n")
for _, line := range lines {
if strings.Contains(line, "ProxyServer") {
parts := strings.Fields(line)
if len(parts) >= 3 {
proxyAddr = parts[len(parts)-1]
}
}
}
}
return enabled, proxyAddr, nil
}
func refreshIESettings() {
// Используем rundll32 для обновления настроек Internet Explorer
cmd := exec.Command("rundll32.exe", "inetcpl.cpl,ClearMyTracksByProcess", "8")
cmd.Run()
}

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"))

View File

@@ -9,6 +9,7 @@ import (
"vpn-client/internal/config"
"vpn-client/internal/logger"
"vpn-client/internal/proxy"
"vpn-client/internal/wireguard"
)
@@ -84,6 +85,16 @@ func Disconnect(logsDir string) error {
return fmt.Errorf("ошибка сохранения состояния: %w", err)
}
// Отключаем системный прокси если был VLESS
if state.ConfigType == "vless" {
if err := proxy.DisableSystemProxy(); err != nil {
fmt.Printf("%s Не удалось отключить системный прокси: %v\n", "⚠", err)
fmt.Println("Отключите его вручную в настройках Windows")
} else {
fmt.Println("✓ Системный прокси отключен")
}
}
fmt.Println("✓ Отключено от VPN")
return nil
}