Initial commit
This commit is contained in:
493
internal/vless/vless.go
Normal file
493
internal/vless/vless.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user