Auto-download Xray for VLESS connections

This commit is contained in:
2026-04-06 09:20:27 +07:00
parent 7268b11e5d
commit 7c27aff3b9
2 changed files with 253 additions and 29 deletions

View File

@@ -375,8 +375,8 @@ func Connect(configName string, logsDir, xrayDir string) error {
} }
xrayPath := filepath.Join(xrayDir, xrayExe) xrayPath := filepath.Join(xrayDir, xrayExe)
if _, err := os.Stat(xrayPath); os.IsNotExist(err) { if xrayPath, err = ensureXrayBinary(xrayDir); err != nil {
return fmt.Errorf("xray не найден в %s", xrayDir) return err
} }
// Создаем лог-файл трафика // Создаем лог-файл трафика

View File

@@ -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
}