494 lines
14 KiB
Go
494 lines
14 KiB
Go
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
|
||
}
|