Initial commit

This commit is contained in:
2026-04-05 20:33:30 +06:00
commit 83fbe7afdd
18 changed files with 3038 additions and 0 deletions

493
internal/vless/vless.go Normal file
View File

@@ -0,0 +1,493 @@
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
}