Auto-download Xray for VLESS connections
This commit is contained in:
@@ -19,11 +19,11 @@ import (
|
|||||||
|
|
||||||
// XrayConfig представляет конфигурацию Xray
|
// XrayConfig представляет конфигурацию Xray
|
||||||
type XrayConfig struct {
|
type XrayConfig struct {
|
||||||
Log LogConfig `json:"log"`
|
Log LogConfig `json:"log"`
|
||||||
Inbounds []InboundConfig `json:"inbounds"`
|
Inbounds []InboundConfig `json:"inbounds"`
|
||||||
Outbounds []OutboundConfig `json:"outbounds"`
|
Outbounds []OutboundConfig `json:"outbounds"`
|
||||||
Stats interface{} `json:"stats"`
|
Stats interface{} `json:"stats"`
|
||||||
Policy PolicyConfig `json:"policy"`
|
Policy PolicyConfig `json:"policy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogConfig struct {
|
type LogConfig struct {
|
||||||
@@ -34,11 +34,11 @@ type LogConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type InboundConfig struct {
|
type InboundConfig struct {
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Protocol string `json:"protocol"`
|
Protocol string `json:"protocol"`
|
||||||
Settings InboundSettings `json:"settings"`
|
Settings InboundSettings `json:"settings"`
|
||||||
Sniffing SniffingConfig `json:"sniffing"`
|
Sniffing SniffingConfig `json:"sniffing"`
|
||||||
Tag string `json:"tag"`
|
Tag string `json:"tag"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type InboundSettings struct {
|
type InboundSettings struct {
|
||||||
@@ -53,8 +53,8 @@ type SniffingConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OutboundConfig struct {
|
type OutboundConfig struct {
|
||||||
Protocol string `json:"protocol"`
|
Protocol string `json:"protocol"`
|
||||||
Tag string `json:"tag"`
|
Tag string `json:"tag"`
|
||||||
Settings OutboundSettings `json:"settings"`
|
Settings OutboundSettings `json:"settings"`
|
||||||
StreamSettings StreamSettings `json:"streamSettings"`
|
StreamSettings StreamSettings `json:"streamSettings"`
|
||||||
}
|
}
|
||||||
@@ -76,13 +76,13 @@ type UserConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type StreamSettings struct {
|
type StreamSettings struct {
|
||||||
Network string `json:"network"`
|
Network string `json:"network"`
|
||||||
Security string `json:"security,omitempty"`
|
Security string `json:"security,omitempty"`
|
||||||
TLSSettings *TLSSettings `json:"tlsSettings,omitempty"`
|
TLSSettings *TLSSettings `json:"tlsSettings,omitempty"`
|
||||||
RealitySettings *RealitySettings `json:"realitySettings,omitempty"`
|
RealitySettings *RealitySettings `json:"realitySettings,omitempty"`
|
||||||
WSSettings *WSSettings `json:"wsSettings,omitempty"`
|
WSSettings *WSSettings `json:"wsSettings,omitempty"`
|
||||||
GRPCSettings *GRPCSettings `json:"grpcSettings,omitempty"`
|
GRPCSettings *GRPCSettings `json:"grpcSettings,omitempty"`
|
||||||
HTTPSettings *HTTPSettings `json:"httpSettings,omitempty"`
|
HTTPSettings *HTTPSettings `json:"httpSettings,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TLSSettings struct {
|
type TLSSettings struct {
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем лог-файл трафика
|
// Создаем лог-файл трафика
|
||||||
@@ -447,24 +447,24 @@ func Connect(configName string, logsDir, xrayDir string) error {
|
|||||||
func PingServer(vlessURL string, timeout time.Duration) (bool, float64, error) {
|
func PingServer(vlessURL string, timeout time.Duration) (bool, float64, error) {
|
||||||
// Парсим URL для получения адреса сервера
|
// Парсим URL для получения адреса сервера
|
||||||
urlStr := strings.TrimPrefix(vlessURL, "vless://")
|
urlStr := strings.TrimPrefix(vlessURL, "vless://")
|
||||||
|
|
||||||
if idx := strings.Index(urlStr, "#"); idx != -1 {
|
if idx := strings.Index(urlStr, "#"); idx != -1 {
|
||||||
urlStr = urlStr[:idx]
|
urlStr = urlStr[:idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
if idx := strings.Index(urlStr, "?"); idx != -1 {
|
if idx := strings.Index(urlStr, "?"); idx != -1 {
|
||||||
urlStr = urlStr[:idx]
|
urlStr = urlStr[:idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(urlStr, "@")
|
parts := strings.Split(urlStr, "@")
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
return false, 0, fmt.Errorf("неверный формат URL")
|
return false, 0, fmt.Errorf("неверный формат URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
serverPort := parts[1]
|
serverPort := parts[1]
|
||||||
var server string
|
var server string
|
||||||
var port string
|
var port string
|
||||||
|
|
||||||
if strings.Contains(serverPort, "[") {
|
if strings.Contains(serverPort, "[") {
|
||||||
endIdx := strings.Index(serverPort, "]")
|
endIdx := strings.Index(serverPort, "]")
|
||||||
server = serverPort[1:endIdx]
|
server = serverPort[1:endIdx]
|
||||||
@@ -474,20 +474,20 @@ func PingServer(vlessURL string, timeout time.Duration) (bool, float64, error) {
|
|||||||
server = serverPort[:lastColon]
|
server = serverPort[:lastColon]
|
||||||
port = serverPort[lastColon+1:]
|
port = serverPort[lastColon+1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
if port == "" {
|
if port == "" {
|
||||||
port = "443"
|
port = "443"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Измеряем время подключения
|
// Измеряем время подключения
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
conn, err := net.DialTimeout("tcp", net.JoinHostPort(server, port), timeout)
|
conn, err := net.DialTimeout("tcp", net.JoinHostPort(server, port), timeout)
|
||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, 0, err
|
return false, 0, err
|
||||||
}
|
}
|
||||||
conn.Close()
|
conn.Close()
|
||||||
|
|
||||||
return true, float64(elapsed.Milliseconds()), nil
|
return true, float64(elapsed.Milliseconds()), nil
|
||||||
}
|
}
|
||||||
|
|||||||
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