Files
Go-VPN-Client/internal/vless/vless.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

588 lines
18 KiB
Go
Raw 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 vless
import (
"encoding/json"
"fmt"
"net"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"time"
"vpn-client/internal/config"
"vpn-client/internal/logger"
"vpn-client/internal/proxy"
)
// XrayConfig представляет конфигурацию Xray
type XrayConfig struct {
Log LogConfig `json:"log"`
Inbounds []InboundConfig `json:"inbounds"`
Outbounds []OutboundConfig `json:"outbounds"`
Stats interface{} `json:"stats"`
Policy PolicyConfig `json:"policy"`
}
type LogConfig struct {
Loglevel string `json:"loglevel"`
Access string `json:"access"`
Error string `json:"error"`
DNSLog bool `json:"dnsLog"`
}
type InboundConfig struct {
Port int `json:"port"`
Protocol string `json:"protocol"`
Settings InboundSettings `json:"settings"`
Sniffing SniffingConfig `json:"sniffing"`
Tag string `json:"tag"`
}
type InboundSettings struct {
UDP bool `json:"udp"`
Auth string `json:"auth"`
}
type SniffingConfig struct {
Enabled bool `json:"enabled"`
DestOverride []string `json:"destOverride"`
MetadataOnly bool `json:"metadataOnly"`
}
type OutboundConfig struct {
Protocol string `json:"protocol"`
Tag string `json:"tag"`
Settings OutboundSettings `json:"settings"`
StreamSettings StreamSettings `json:"streamSettings"`
}
type OutboundSettings struct {
Vnext []VnextConfig `json:"vnext"`
}
type VnextConfig struct {
Address string `json:"address"`
Port int `json:"port"`
Users []UserConfig `json:"users"`
}
type UserConfig struct {
ID string `json:"id"`
Encryption string `json:"encryption"`
Flow string `json:"flow,omitempty"`
}
type StreamSettings struct {
Network string `json:"network"`
Security string `json:"security,omitempty"`
TLSSettings *TLSSettings `json:"tlsSettings,omitempty"`
RealitySettings *RealitySettings `json:"realitySettings,omitempty"`
WSSettings *WSSettings `json:"wsSettings,omitempty"`
GRPCSettings *GRPCSettings `json:"grpcSettings,omitempty"`
HTTPSettings *HTTPSettings `json:"httpSettings,omitempty"`
}
type TLSSettings struct {
ServerName string `json:"serverName"`
AllowInsecure bool `json:"allowInsecure"`
Fingerprint string `json:"fingerprint,omitempty"`
}
type RealitySettings struct {
ServerName string `json:"serverName"`
Fingerprint string `json:"fingerprint"`
Show bool `json:"show"`
PublicKey string `json:"publicKey,omitempty"`
ShortID string `json:"shortId,omitempty"`
SpiderX string `json:"spiderX,omitempty"`
}
type WSSettings struct {
Path string `json:"path"`
Headers map[string]string `json:"headers"`
}
type GRPCSettings struct {
ServiceName string `json:"serviceName"`
MultiMode bool `json:"multiMode"`
}
type HTTPSettings struct {
Path []string `json:"path"`
Host []string `json:"host"`
}
type PolicyConfig struct {
Levels map[string]LevelPolicy `json:"levels"`
System SystemPolicy `json:"system"`
}
type LevelPolicy struct {
StatsUserUplink bool `json:"statsUserUplink"`
StatsUserDownlink bool `json:"statsUserDownlink"`
}
type SystemPolicy struct {
StatsInboundUplink bool `json:"statsInboundUplink"`
StatsInboundDownlink bool `json:"statsInboundDownlink"`
StatsOutboundUplink bool `json:"statsOutboundUplink"`
StatsOutboundDownlink bool `json:"statsOutboundDownlink"`
}
// ParseVLESSURL парсит VLESS URL и создает конфигурацию Xray
func ParseVLESSURL(vlessURL, logsDir string) (*XrayConfig, error) {
// Убираем префикс vless://
urlStr := strings.TrimPrefix(vlessURL, "vless://")
// Разделяем на части
var name string
if idx := strings.Index(urlStr, "#"); idx != -1 {
name = urlStr[idx+1:]
urlStr = urlStr[:idx]
}
// Парсим параметры
var paramsStr string
var connection string
if idx := strings.Index(urlStr, "?"); idx != -1 {
connection = urlStr[:idx]
paramsStr = urlStr[idx+1:]
} else {
connection = urlStr
}
// Парсим connection (uuid@server:port)
parts := strings.Split(connection, "@")
if len(parts) != 2 {
return nil, fmt.Errorf("неверный формат VLESS URL")
}
uuid := parts[0]
serverPort := parts[1]
// Парсим server:port
var server string
var port int
if strings.Contains(serverPort, "[") {
// IPv6
endIdx := strings.Index(serverPort, "]")
server = serverPort[1:endIdx]
portStr := strings.TrimPrefix(serverPort[endIdx+1:], ":")
port, _ = strconv.Atoi(portStr)
} else {
lastColon := strings.LastIndex(serverPort, ":")
server = serverPort[:lastColon]
portStr := serverPort[lastColon+1:]
port, _ = strconv.Atoi(portStr)
}
if port == 0 {
port = 443
}
// Парсим параметры
params := make(map[string]string)
if paramsStr != "" {
for _, param := range strings.Split(paramsStr, "&") {
kv := strings.SplitN(param, "=", 2)
if len(kv) == 2 {
key, _ := url.QueryUnescape(kv[0])
value, _ := url.QueryUnescape(kv[1])
params[key] = value
}
}
}
// Создаем базовую конфигурацию
cfg := &XrayConfig{
Log: LogConfig{
Loglevel: "debug",
Access: filepath.Join(logsDir, "vless_access.log"),
Error: filepath.Join(logsDir, "vless_error.log"),
DNSLog: true,
},
Inbounds: []InboundConfig{
{
Port: 10808,
Protocol: "socks",
Settings: InboundSettings{
UDP: true,
Auth: "noauth",
},
Sniffing: SniffingConfig{
Enabled: true,
DestOverride: []string{"http", "tls"},
MetadataOnly: false,
},
Tag: "socks-in",
},
},
Outbounds: []OutboundConfig{
{
Protocol: "vless",
Tag: "proxy",
Settings: OutboundSettings{
Vnext: []VnextConfig{
{
Address: server,
Port: port,
Users: []UserConfig{
{
ID: uuid,
Encryption: getParam(params, "encryption", "none"),
Flow: params["flow"],
},
},
},
},
},
StreamSettings: StreamSettings{
Network: getParam(params, "type", "tcp"),
},
},
},
Stats: struct{}{},
Policy: PolicyConfig{
Levels: map[string]LevelPolicy{
"0": {
StatsUserUplink: true,
StatsUserDownlink: true,
},
},
System: SystemPolicy{
StatsInboundUplink: true,
StatsInboundDownlink: true,
StatsOutboundUplink: true,
StatsOutboundDownlink: true,
},
},
}
// Удаляем пустой flow
if cfg.Outbounds[0].Settings.Vnext[0].Users[0].Flow == "" {
cfg.Outbounds[0].Settings.Vnext[0].Users[0].Flow = ""
}
// Настройки безопасности
security := params["security"]
if security == "tls" {
cfg.Outbounds[0].StreamSettings.Security = "tls"
cfg.Outbounds[0].StreamSettings.TLSSettings = &TLSSettings{
ServerName: getParam(params, "sni", server),
AllowInsecure: params["allowInsecure"] == "1",
Fingerprint: params["fp"],
}
} else if security == "reality" {
cfg.Outbounds[0].StreamSettings.Security = "reality"
cfg.Outbounds[0].StreamSettings.RealitySettings = &RealitySettings{
ServerName: getParam(params, "sni", server),
Fingerprint: getParam(params, "fp", "chrome"),
Show: false,
PublicKey: params["pbk"],
ShortID: params["sid"],
SpiderX: params["spx"],
}
}
// Настройки транспорта
network := getParam(params, "type", "tcp")
if network == "ws" {
headers := make(map[string]string)
if host := params["host"]; host != "" {
headers["Host"] = host
}
cfg.Outbounds[0].StreamSettings.WSSettings = &WSSettings{
Path: getParam(params, "path", "/"),
Headers: headers,
}
} else if network == "grpc" {
cfg.Outbounds[0].StreamSettings.GRPCSettings = &GRPCSettings{
ServiceName: getParam(params, "serviceName", params["path"]),
MultiMode: params["mode"] == "multi",
}
} else if network == "h2" || network == "http" {
cfg.Outbounds[0].StreamSettings.Network = "http"
cfg.Outbounds[0].StreamSettings.HTTPSettings = &HTTPSettings{
Path: []string{getParam(params, "path", "/")},
Host: []string{getParam(params, "host", server)},
}
}
// Логируем информацию
logFile := logger.GetLogPath(logsDir, "vless")
logger.LogMessage(logFile, fmt.Sprintf("Создание конфига для сервера: %s:%d", server, port))
logger.LogMessage(logFile, fmt.Sprintf("UUID: %s", uuid))
logger.LogMessage(logFile, fmt.Sprintf("Транспорт: %s", network))
logger.LogMessage(logFile, fmt.Sprintf("Безопасность: %s", security))
if name != "" {
logger.LogMessage(logFile, fmt.Sprintf("Имя: %s", name))
}
return cfg, nil
}
func getParam(params map[string]string, key, defaultValue string) string {
if val, ok := params[key]; ok && val != "" {
return val
}
return defaultValue
}
// Connect подключается к VLESS серверу
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 {
return fmt.Errorf("ошибка загрузки конфигураций: %w", err)
}
// Ищем конфиг
var vlessConfig *config.VLESSConfig
for i := range configs.VLESS {
if configs.VLESS[i].Name == configName {
vlessConfig = &configs.VLESS[i]
break
}
}
if vlessConfig == nil {
return fmt.Errorf("конфиг '%s' не найден", configName)
}
// Создаем конфигурацию
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(coreConfig, "", " ")
if err != nil {
return fmt.Errorf("ошибка сериализации конфига: %w", err)
}
if err := os.WriteFile(configPath, data, 0644); err != nil {
return fmt.Errorf("ошибка сохранения конфига: %w", err)
}
// Проверяем наличие исполняемого файла
if _, err := os.Stat(corePath); os.IsNotExist(err) {
return fmt.Errorf("%s не найден в %s\n\nПожалуйста, перезапустите приложение для автоматической загрузки %s", coreName, coreDir, coreName)
}
// Создаем лог-файл трафика
trafficLog := logger.GetTrafficLogPath(logsDir)
logFile, err := os.Create(trafficLog)
if err != nil {
return fmt.Errorf("ошибка создания лог-файла: %w", err)
}
// Записываем заголовок
fmt.Fprintf(logFile, "=== VPN Подключение начато: %s ===\n", time.Now().Format("2006-01-02 15:04:05"))
fmt.Fprintf(logFile, "Конфиг: %s\n", configName)
fmt.Fprintf(logFile, "Клиент: %s\n", coreName)
fmt.Fprintf(logFile, "Прокси: 127.0.0.1:10808\n")
fmt.Fprintf(logFile, "%s\n\n", strings.Repeat("=", 60))
// Запускаем ядро
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("ошибка запуска %s: %w", coreName, err)
}
// Ждем немного для проверки запуска
time.Sleep(2 * time.Second)
// Проверяем, что процесс еще работает (для 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()
// Читаем логи для диагностики
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)
}
// Сохраняем состояние
state := &config.ConnectionState{
Connected: true,
ConfigName: configName,
ConfigType: "vless",
StartTime: time.Now().Format(time.RFC3339),
Interface: strings.ToLower(coreName),
ProcessPID: cmd.Process.Pid,
LogFile: trafficLog,
}
if err := config.SaveState(state); err != nil {
cmd.Process.Kill()
logFile.Close()
return fmt.Errorf("ошибка сохранения состояния: %w", err)
}
// Логируем успешное подключение
logPath := logger.GetLogPath(logsDir, "vless")
logger.LogMessage(logPath, fmt.Sprintf("Успешно подключено к '%s' через %s (PID: %d, Лог: %s)", configName, coreName, cmd.Process.Pid, trafficLog))
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"))
fmt.Printf(" - Ошибки: %s\n", filepath.Join(logsDir, "vless_error.log"))
return nil
}
// PingServer проверяет доступность VLESS сервера
func PingServer(vlessURL string, timeout time.Duration) (bool, float64, error) {
// Парсим URL для получения адреса сервера
urlStr := strings.TrimPrefix(vlessURL, "vless://")
if idx := strings.Index(urlStr, "#"); idx != -1 {
urlStr = urlStr[:idx]
}
if idx := strings.Index(urlStr, "?"); idx != -1 {
urlStr = urlStr[:idx]
}
parts := strings.Split(urlStr, "@")
if len(parts) != 2 {
return false, 0, fmt.Errorf("неверный формат URL")
}
serverPort := parts[1]
var server string
var port string
if strings.Contains(serverPort, "[") {
endIdx := strings.Index(serverPort, "]")
server = serverPort[1:endIdx]
port = strings.TrimPrefix(serverPort[endIdx+1:], ":")
} else {
lastColon := strings.LastIndex(serverPort, ":")
server = serverPort[:lastColon]
port = serverPort[lastColon+1:]
}
if port == "" {
port = "443"
}
// Измеряем время подключения
start := time.Now()
conn, err := net.DialTimeout("tcp", net.JoinHostPort(server, port), timeout)
elapsed := time.Since(start)
if err != nil {
return false, 0, err
}
conn.Close()
return true, float64(elapsed.Milliseconds()), nil
}