Auto-download Xray for VLESS connections
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
// Создаем лог-файл трафика
|
||||
|
||||
224
internal/vless/xray_bootstrap.go
Normal file
224
internal/vless/xray_bootstrap.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user