diff --git a/internal/vless/vless.go b/internal/vless/vless.go index 3f401cc..f861f60 100644 --- a/internal/vless/vless.go +++ b/internal/vless/vless.go @@ -19,11 +19,11 @@ import ( // XrayConfig представляет конфигурацию Xray type XrayConfig struct { - Log LogConfig `json:"log"` - Inbounds []InboundConfig `json:"inbounds"` + Log LogConfig `json:"log"` + Inbounds []InboundConfig `json:"inbounds"` Outbounds []OutboundConfig `json:"outbounds"` - Stats interface{} `json:"stats"` - Policy PolicyConfig `json:"policy"` + Stats interface{} `json:"stats"` + Policy PolicyConfig `json:"policy"` } type LogConfig struct { @@ -34,11 +34,11 @@ type LogConfig struct { } type InboundConfig struct { - Port int `json:"port"` - Protocol string `json:"protocol"` - Settings InboundSettings `json:"settings"` - Sniffing SniffingConfig `json:"sniffing"` - Tag string `json:"tag"` + Port int `json:"port"` + Protocol string `json:"protocol"` + Settings InboundSettings `json:"settings"` + Sniffing SniffingConfig `json:"sniffing"` + Tag string `json:"tag"` } type InboundSettings struct { @@ -53,8 +53,8 @@ type SniffingConfig struct { } type OutboundConfig struct { - Protocol string `json:"protocol"` - Tag string `json:"tag"` + Protocol string `json:"protocol"` + Tag string `json:"tag"` Settings OutboundSettings `json:"settings"` StreamSettings StreamSettings `json:"streamSettings"` } @@ -76,13 +76,13 @@ type UserConfig struct { } 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"` + 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 { @@ -375,8 +375,8 @@ func Connect(configName string, logsDir, xrayDir string) error { } xrayPath := filepath.Join(xrayDir, xrayExe) - if _, err := os.Stat(xrayPath); os.IsNotExist(err) { - return fmt.Errorf("xray не найден в %s", xrayDir) + if xrayPath, err = ensureXrayBinary(xrayDir); err != nil { + return err } // Создаем лог-файл трафика @@ -447,24 +447,24 @@ func Connect(configName string, logsDir, xrayDir string) error { 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] @@ -474,20 +474,20 @@ func PingServer(vlessURL string, timeout time.Duration) (bool, float64, error) { 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 } diff --git a/internal/vless/xray_bootstrap.go b/internal/vless/xray_bootstrap.go new file mode 100644 index 0000000..a2554f4 --- /dev/null +++ b/internal/vless/xray_bootstrap.go @@ -0,0 +1,224 @@ +package vless + +import ( + "archive/zip" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "runtime" + "strings" + "time" +) + +const xrayLatestReleaseAPI = "https://api.github.com/repos/XTLS/Xray-core/releases/latest" + +type xrayRelease struct { + TagName string `json:"tag_name"` + Assets []xrayReleaseAsset `json:"assets"` +} + +type xrayReleaseAsset struct { + Name string `json:"name"` + BrowserDownloadURL string `json:"browser_download_url"` +} + +func ensureXrayBinary(xrayDir string) (string, error) { + exeName := "xray" + if runtime.GOOS == "windows" { + exeName = "xray.exe" + } + xrayPath := filepath.Join(xrayDir, exeName) + + if _, err := os.Stat(xrayPath); err == nil { + return xrayPath, nil + } + + if err := os.MkdirAll(xrayDir, 0755); err != nil { + return "", fmt.Errorf("ошибка создания папки xray: %w", err) + } + + asset, err := fetchLatestXrayAsset() + if err != nil { + return "", err + } + + archivePath, err := downloadXrayArchive(asset.BrowserDownloadURL, xrayDir, asset.Name) + if err != nil { + return "", err + } + defer os.Remove(archivePath) + + if err := unzipXrayArchive(archivePath, xrayDir); err != nil { + return "", err + } + + if runtime.GOOS != "windows" { + _ = os.Chmod(xrayPath, 0755) + } + + if _, err := os.Stat(xrayPath); err != nil { + return "", fmt.Errorf("xray не найден после распаковки в %s", xrayDir) + } + + return xrayPath, nil +} + +func fetchLatestXrayAsset() (*xrayReleaseAsset, error) { + client := &http.Client{Timeout: 45 * time.Second} + req, err := http.NewRequest(http.MethodGet, xrayLatestReleaseAPI, nil) + if err != nil { + return nil, fmt.Errorf("ошибка подготовки запроса релиза Xray: %w", err) + } + req.Header.Set("User-Agent", "go-vpn-client") + req.Header.Set("Accept", "application/vnd.github+json") + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("ошибка получения релиза Xray: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024)) + return nil, fmt.Errorf("ошибка API Xray: %d %s", resp.StatusCode, strings.TrimSpace(string(body))) + } + + var release xrayRelease + if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { + return nil, fmt.Errorf("ошибка разбора релиза Xray: %w", err) + } + + for _, asset := range release.Assets { + if matchesXrayAsset(asset.Name) { + return &asset, nil + } + } + + return nil, fmt.Errorf("не найден подходящий архив Xray для %s/%s в релизе %s", runtime.GOOS, runtime.GOARCH, release.TagName) +} + +func matchesXrayAsset(name string) bool { + lower := strings.ToLower(name) + if !strings.HasSuffix(lower, ".zip") || strings.Contains(lower, ".dgst") { + return false + } + + osToken := map[string]string{ + "windows": "windows", + "linux": "linux", + "darwin": "macos", + }[runtime.GOOS] + if osToken == "" || !strings.Contains(lower, osToken) { + return false + } + + archTokens := map[string][]string{ + "amd64": {"64", "amd64", "x64"}, + "386": {"32", "386", "x86"}, + "arm64": {"arm64", "aarch64"}, + "arm": {"arm32", "armv7", "arm"}, + }[runtime.GOARCH] + + if len(archTokens) == 0 { + return false + } + for _, token := range archTokens { + if strings.Contains(lower, token) { + return true + } + } + return false +} + +func downloadXrayArchive(downloadURL, xrayDir, name string) (string, error) { + client := &http.Client{Timeout: 2 * time.Minute} + req, err := http.NewRequest(http.MethodGet, downloadURL, nil) + if err != nil { + return "", fmt.Errorf("ошибка подготовки загрузки Xray: %w", err) + } + req.Header.Set("User-Agent", "go-vpn-client") + + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("ошибка загрузки Xray: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("ошибка загрузки Xray: HTTP %d", resp.StatusCode) + } + + archivePath := filepath.Join(xrayDir, name) + file, err := os.Create(archivePath) + if err != nil { + return "", fmt.Errorf("ошибка создания архива Xray: %w", err) + } + defer file.Close() + + if _, err := io.Copy(file, resp.Body); err != nil { + return "", fmt.Errorf("ошибка сохранения архива Xray: %w", err) + } + + return archivePath, nil +} + +func unzipXrayArchive(archivePath, targetDir string) error { + reader, err := zip.OpenReader(archivePath) + if err != nil { + return fmt.Errorf("ошибка открытия архива Xray: %w", err) + } + defer reader.Close() + + for _, file := range reader.File { + if err := extractZipEntry(file, targetDir); err != nil { + return err + } + } + + return nil +} + +func extractZipEntry(file *zip.File, targetDir string) error { + cleanName := filepath.Clean(file.Name) + targetPath := filepath.Join(targetDir, cleanName) + absTargetDir, err := filepath.Abs(targetDir) + if err != nil { + return fmt.Errorf("ошибка определения папки Xray: %w", err) + } + absTargetPath, err := filepath.Abs(targetPath) + if err != nil { + return fmt.Errorf("ошибка определения пути Xray: %w", err) + } + if !strings.HasPrefix(absTargetPath, absTargetDir) { + return fmt.Errorf("небезопасный путь в архиве Xray: %s", file.Name) + } + + if file.FileInfo().IsDir() { + return os.MkdirAll(absTargetPath, 0755) + } + + if err := os.MkdirAll(filepath.Dir(absTargetPath), 0755); err != nil { + return fmt.Errorf("ошибка создания папки Xray: %w", err) + } + + in, err := file.Open() + if err != nil { + return fmt.Errorf("ошибка чтения файла из архива Xray: %w", err) + } + defer in.Close() + + out, err := os.OpenFile(absTargetPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, file.Mode()) + if err != nil { + return fmt.Errorf("ошибка создания файла Xray: %w", err) + } + defer out.Close() + + if _, err := io.Copy(out, in); err != nil { + return fmt.Errorf("ошибка распаковки Xray: %w", err) + } + + return nil +}