- System-wide proxy: automatic Windows proxy configuration for all apps - DNS leak protection: force all DNS queries through VPN - Config encryption: AES-256-GCM encryption for all config files - File protection: strict access permissions for config directory - Leak detection: built-in security check system - Kill Switch: temporarily disabled (will be improved in next version) Security features: ✓ Automatic system proxy setup ✓ DNS leak protection (optional) ✓ AES-256-GCM config encryption ✓ File and directory protection ✓ Security leak checker ⚠ Kill Switch disabled (caused internet blocking issues) Emergency recovery scripts included: - ОТКЛЮЧИТЬ_KILLSWITCH.bat - EMERGENCY_FIX_INTERNET.bat - ЕСЛИ_СЛОМАЛСЯ_ИНТЕРНЕТ.txt Documentation: - Markdown/SECURITY_GUIDE.md - full security guide - БЕЗОПАСНОСТЬ_БЫСТРЫЙ_СТАРТ.md - quick start guide - CHANGELOG_SECURITY.md - detailed changelog
641 lines
19 KiB
Go
641 lines
19 KiB
Go
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"
|
||
"vpn-client/internal/security"
|
||
)
|
||
|
||
// 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("Настройка системного прокси для работы VPN по всему ПК...")
|
||
|
||
if err := proxy.EnableSystemProxy("127.0.0.1:10808"); err != nil {
|
||
fmt.Printf("⚠ Не удалось настроить системный прокси: %v\n", err)
|
||
fmt.Println("Вы можете настроить его вручную в настройках Windows")
|
||
} else {
|
||
fmt.Println("✓ Системный прокси настроен - VPN работает для всех приложений")
|
||
}
|
||
|
||
// Включаем защитные механизмы (БЕЗ Kill Switch!)
|
||
fmt.Println("\nНастройка защитных механизмов...")
|
||
secManager := security.NewSecurityManager(config.ConfigDir, "default-password")
|
||
|
||
secConfig, _ := secManager.LoadSecurityConfig()
|
||
|
||
// KILL SWITCH ПОЛНОСТЬЮ ОТКЛЮЧЕН!
|
||
// Включаем только защиту DNS если она включена
|
||
if secConfig != nil && secConfig.DNSProtectionEnabled {
|
||
dnsProtection := security.NewDNSProtection()
|
||
if err := dnsProtection.Enable(secConfig.VPNDNSServers); err != nil {
|
||
fmt.Printf("⚠ Предупреждение: не удалось включить защиту DNS: %v\n", err)
|
||
} else {
|
||
fmt.Println("✓ Защита DNS включена")
|
||
}
|
||
} else {
|
||
fmt.Println("ℹ Защита DNS отключена")
|
||
}
|
||
|
||
// Защищаем директорию конфигураций
|
||
if err := secManager.ProtectConfigDirectory(); err != nil {
|
||
fmt.Printf("⚠ Предупреждение: не удалось защитить директорию конфигураций: %v\n", err)
|
||
}
|
||
|
||
// Проверяем утечки
|
||
fmt.Println("\nПроверка безопасности...")
|
||
leaks, err := secManager.CheckForLeaks()
|
||
if err == nil && len(leaks) == 0 {
|
||
fmt.Println("✓ Утечек не обнаружено - соединение безопасно")
|
||
} else if len(leaks) > 0 {
|
||
fmt.Println("⚠ Обнаружены потенциальные утечки:")
|
||
for _, leak := range leaks {
|
||
fmt.Printf(" - %s\n", leak)
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
// extractServerIP извлекает IP адрес сервера из VLESS URL
|
||
func extractServerIP(vlessURL string) string {
|
||
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 ""
|
||
}
|
||
|
||
serverPort := parts[1]
|
||
var server string
|
||
|
||
if strings.Contains(serverPort, "[") {
|
||
endIdx := strings.Index(serverPort, "]")
|
||
server = serverPort[1:endIdx]
|
||
} else {
|
||
lastColon := strings.LastIndex(serverPort, ":")
|
||
server = serverPort[:lastColon]
|
||
}
|
||
|
||
return server
|
||
}
|
||
|
||
// 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
|
||
}
|