Files
Go-VPN-Client/internal/vless/vless.go
2026-04-05 20:33:30 +06:00

494 lines
14 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 vless
import (
"encoding/json"
"fmt"
"net"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"vpn-client/internal/config"
"vpn-client/internal/logger"
)
// 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, xrayDir string) error {
// Загружаем конфигурации
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)
}
// Создаем конфигурацию Xray
xrayConfig, 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, "", " ")
if err != nil {
return fmt.Errorf("ошибка сериализации конфига: %w", err)
}
if err := os.WriteFile(configPath, data, 0644); err != nil {
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)
}
// Создаем лог-файл трафика
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, "Клиент: Xray\n")
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.Stdout = logFile
cmd.Stderr = logFile
if err := cmd.Start(); err != nil {
logFile.Close()
return fmt.Errorf("ошибка запуска xray: %w", err)
}
// Ждем немного для проверки запуска
time.Sleep(3 * time.Second)
// Проверяем, что процесс еще работает
if err := cmd.Process.Signal(os.Signal(nil)); err != nil {
logFile.Close()
return fmt.Errorf("процесс xray завершился с ошибкой")
}
// Сохраняем состояние
state := &config.ConnectionState{
Connected: true,
ConfigName: configName,
ConfigType: "vless",
StartTime: time.Now().Format(time.RFC3339),
Interface: "xray",
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' (PID: %d, Лог: %s)", configName, cmd.Process.Pid, trafficLog))
fmt.Printf("✓ Подключено к '%s'\n", configName)
fmt.Printf("SOCKS5 прокси: 127.0.0.1:10808\n")
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
}