mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-05-28 09:01:22 +02:00
feat: Add VPN tool management functionality
Signed-off-by: luckfox-eng29 <eng29@luckfox.com>
This commit is contained in:
@@ -1649,6 +1649,8 @@ var rpcHandlers = map[string]RPCHandler{
|
|||||||
"getVpnToolStatus": {Func: rpcGetVpnToolStatus, Params: []string{"tool"}},
|
"getVpnToolStatus": {Func: rpcGetVpnToolStatus, Params: []string{"tool"}},
|
||||||
"listVpnToolReleases": {Func: rpcListVpnToolReleases, Params: []string{"tool"}},
|
"listVpnToolReleases": {Func: rpcListVpnToolReleases, Params: []string{"tool"}},
|
||||||
"installVpnTool": {Func: rpcInstallVpnTool, Params: []string{"tool", "version", "assetName", "downloadURL"}},
|
"installVpnTool": {Func: rpcInstallVpnTool, Params: []string{"tool", "version", "assetName", "downloadURL"}},
|
||||||
|
"startVpnToolInstall": {Func: rpcStartVpnToolInstall, Params: []string{"tool", "version", "assetName", "downloadURL"}},
|
||||||
|
"getVpnToolInstallTask": {Func: rpcGetVpnToolInstallTask, Params: []string{"tool"}},
|
||||||
"useVpnToolVersion": {Func: rpcUseVpnToolVersion, Params: []string{"tool", "version"}},
|
"useVpnToolVersion": {Func: rpcUseVpnToolVersion, Params: []string{"tool", "version"}},
|
||||||
"uninstallVpnToolVersion": {Func: rpcUninstallVpnToolVersion, Params: []string{"tool", "version"}},
|
"uninstallVpnToolVersion": {Func: rpcUninstallVpnToolVersion, Params: []string{"tool", "version"}},
|
||||||
"getStreamEncodecType": {Func: rpcGetStreamEncodecType},
|
"getStreamEncodecType": {Func: rpcGetStreamEncodecType},
|
||||||
|
|||||||
906
tools.go
Normal file
906
tools.go
Normal file
@@ -0,0 +1,906 @@
|
|||||||
|
package kvm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"archive/zip"
|
||||||
|
"compress/gzip"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const vpnToolsRoot = "/userdata/vpn-tools"
|
||||||
|
const vntPinnedVersion = "v1.2.16"
|
||||||
|
|
||||||
|
type vpnToolSpec struct {
|
||||||
|
Name string
|
||||||
|
Repo string
|
||||||
|
Binaries []string
|
||||||
|
VersionBinary string
|
||||||
|
VersionFlags [][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
var vpnToolSpecs = map[string]vpnToolSpec{
|
||||||
|
"frpc": {
|
||||||
|
Name: "frpc",
|
||||||
|
Repo: "fatedier/frp",
|
||||||
|
Binaries: []string{"frpc"},
|
||||||
|
VersionBinary: "frpc",
|
||||||
|
VersionFlags: [][]string{{"-v"}, {"--version"}, {"version"}},
|
||||||
|
},
|
||||||
|
"easytier": {
|
||||||
|
Name: "easytier",
|
||||||
|
Repo: "EasyTier/EasyTier",
|
||||||
|
Binaries: []string{"easytier-core", "easytier-cli"},
|
||||||
|
VersionBinary: "easytier-cli",
|
||||||
|
VersionFlags: [][]string{{"--version"}, {"-V"}, {"version"}},
|
||||||
|
},
|
||||||
|
"vnt": {
|
||||||
|
Name: "vnt",
|
||||||
|
Repo: "vnt-dev/vnt",
|
||||||
|
Binaries: []string{"vnt-cli"},
|
||||||
|
VersionBinary: "vnt-cli",
|
||||||
|
VersionFlags: [][]string{{}, {"--version"}, {"-V"}, {"version"}},
|
||||||
|
},
|
||||||
|
"cloudflared": {
|
||||||
|
Name: "cloudflared",
|
||||||
|
Repo: "cloudflare/cloudflared",
|
||||||
|
Binaries: []string{"cloudflared"},
|
||||||
|
VersionBinary: "cloudflared",
|
||||||
|
VersionFlags: [][]string{{"-v"}, {"version"}, {"--version"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type VpnToolSystemInfo struct {
|
||||||
|
GOOS string `json:"goos"`
|
||||||
|
GOARCH string `json:"goarch"`
|
||||||
|
UnameArch string `json:"uname_arch"`
|
||||||
|
ArchLabel string `json:"arch_label"`
|
||||||
|
ArchKeywords []string `json:"arch_keywords"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VpnToolStatus struct {
|
||||||
|
Tool string `json:"tool"`
|
||||||
|
Installed bool `json:"installed"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
CurrentVersion string `json:"current_version"`
|
||||||
|
DetectedVersion string `json:"detected_version"`
|
||||||
|
ManagedVersions []string `json:"managed_versions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VpnToolReleaseAsset struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
ArchMatch bool `json:"arch_match"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VpnToolRelease struct {
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
Assets []VpnToolReleaseAsset `json:"assets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type githubReleaseAsset struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
BrowserDownloadURL string `json:"browser_download_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type githubRelease struct {
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
Draft bool `json:"draft"`
|
||||||
|
Prerelease bool `json:"prerelease"`
|
||||||
|
Assets []githubReleaseAsset `json:"assets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VpnToolInstallTask struct {
|
||||||
|
Tool string `json:"tool"`
|
||||||
|
Running bool `json:"running"`
|
||||||
|
Progress float64 `json:"progress"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Logs []string `json:"logs"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
vpnToolInstallTaskMu sync.Mutex
|
||||||
|
vpnToolInstallTasks = map[string]*VpnToolInstallTask{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func getVpnToolSpec(tool string) (vpnToolSpec, error) {
|
||||||
|
spec, ok := vpnToolSpecs[strings.ToLower(strings.TrimSpace(tool))]
|
||||||
|
if !ok {
|
||||||
|
return vpnToolSpec{}, fmt.Errorf("unsupported vpn tool: %s", tool)
|
||||||
|
}
|
||||||
|
return spec, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeArchLabel(arch string) string {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(arch)) {
|
||||||
|
case "x86_64", "amd64":
|
||||||
|
return "amd64"
|
||||||
|
case "aarch64", "arm64":
|
||||||
|
return "arm64"
|
||||||
|
case "armv7l", "armv7", "armhf":
|
||||||
|
return "armv7"
|
||||||
|
case "armv6l", "armv6":
|
||||||
|
return "armv6"
|
||||||
|
case "i386", "i686", "386", "x86":
|
||||||
|
return "386"
|
||||||
|
default:
|
||||||
|
return strings.ToLower(strings.TrimSpace(arch))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func archKeywords(archLabel string) []string {
|
||||||
|
switch archLabel {
|
||||||
|
case "amd64":
|
||||||
|
return []string{"amd64", "x86_64", "x64"}
|
||||||
|
case "arm64":
|
||||||
|
return []string{"arm64", "aarch64"}
|
||||||
|
case "armv7":
|
||||||
|
return []string{"armv7", "armv7l", "armhf", "arm"}
|
||||||
|
case "armv6":
|
||||||
|
return []string{"armv6", "armv6l", "arm"}
|
||||||
|
case "386":
|
||||||
|
return []string{"386", "i386", "x86"}
|
||||||
|
default:
|
||||||
|
if archLabel == "" {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
return []string{archLabel}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcGetVpnToolSystemInfo() (VpnToolSystemInfo, error) {
|
||||||
|
unameArch := runtime.GOARCH
|
||||||
|
if out, err := exec.Command("uname", "-m").Output(); err == nil {
|
||||||
|
unameArch = strings.TrimSpace(string(out))
|
||||||
|
}
|
||||||
|
archLabel := normalizeArchLabel(unameArch)
|
||||||
|
if archLabel == "" {
|
||||||
|
archLabel = normalizeArchLabel(runtime.GOARCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
return VpnToolSystemInfo{
|
||||||
|
GOOS: runtime.GOOS,
|
||||||
|
GOARCH: runtime.GOARCH,
|
||||||
|
UnameArch: unameArch,
|
||||||
|
ArchLabel: archLabel,
|
||||||
|
ArchKeywords: archKeywords(archLabel),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func vpnToolDir(spec vpnToolSpec) string {
|
||||||
|
return filepath.Join(vpnToolsRoot, spec.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func vpnToolVersionsDir(spec vpnToolSpec) string {
|
||||||
|
return filepath.Join(vpnToolDir(spec), "versions")
|
||||||
|
}
|
||||||
|
|
||||||
|
func vpnToolCurrentDir(spec vpnToolSpec) string {
|
||||||
|
return filepath.Join(vpnToolDir(spec), "current")
|
||||||
|
}
|
||||||
|
|
||||||
|
func managedBinaryPath(spec vpnToolSpec, binaryName string) string {
|
||||||
|
return filepath.Join(vpnToolCurrentDir(spec), binaryName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findExecutablePath(binary string) (string, error) {
|
||||||
|
if strings.Contains(binary, "/") {
|
||||||
|
info, err := os.Stat(binary)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if info.Mode().IsRegular() || (info.Mode()&os.ModeSymlink) != 0 {
|
||||||
|
return binary, nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("not executable file: %s", binary)
|
||||||
|
}
|
||||||
|
return exec.LookPath(binary)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveVpnToolBinary(tool, defaultBinary string) string {
|
||||||
|
spec, err := getVpnToolSpec(tool)
|
||||||
|
if err != nil {
|
||||||
|
return defaultBinary
|
||||||
|
}
|
||||||
|
managed := managedBinaryPath(spec, defaultBinary)
|
||||||
|
if _, err := os.Stat(managed); err == nil {
|
||||||
|
return managed
|
||||||
|
}
|
||||||
|
return defaultBinary
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectCommandVersion(binaryPath string, flags [][]string) string {
|
||||||
|
for _, args := range flags {
|
||||||
|
cmd := exec.Command(binaryPath, args...)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil && len(out) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
line := extractVersionLine(string(out))
|
||||||
|
if line != "" {
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractVersionLine(output string) string {
|
||||||
|
normalized := strings.ReplaceAll(output, "\r\n", "\n")
|
||||||
|
lines := strings.Split(normalized, "\n")
|
||||||
|
|
||||||
|
// Prefer explicit version lines to handle tools like vnt-cli
|
||||||
|
// where the first line is usage text.
|
||||||
|
for _, raw := range lines {
|
||||||
|
line := strings.TrimSpace(raw)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lower := strings.ToLower(line)
|
||||||
|
if strings.Contains(lower, "version:") {
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, raw := range lines {
|
||||||
|
line := strings.TrimSpace(raw)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lower := strings.ToLower(line)
|
||||||
|
if strings.Contains(lower, "version") || strings.HasPrefix(lower, "v") {
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, raw := range lines {
|
||||||
|
line := strings.TrimSpace(raw)
|
||||||
|
if line != "" {
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func listManagedVersions(spec vpnToolSpec) []string {
|
||||||
|
versionsDir := vpnToolVersionsDir(spec)
|
||||||
|
entries, err := os.ReadDir(versionsDir)
|
||||||
|
if err != nil {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
versions := make([]string, 0, len(entries))
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() {
|
||||||
|
versions = append(versions, e.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(versions, func(i, j int) bool { return versions[i] > versions[j] })
|
||||||
|
return versions
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentManagedVersion(spec vpnToolSpec) string {
|
||||||
|
currentDir := vpnToolCurrentDir(spec)
|
||||||
|
for _, binary := range spec.Binaries {
|
||||||
|
p := filepath.Join(currentDir, binary)
|
||||||
|
target, err := os.Readlink(p)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
abs := target
|
||||||
|
if !filepath.IsAbs(target) {
|
||||||
|
abs = filepath.Clean(filepath.Join(filepath.Dir(p), target))
|
||||||
|
}
|
||||||
|
versionsRoot := vpnToolVersionsDir(spec) + string(os.PathSeparator)
|
||||||
|
if strings.HasPrefix(abs, versionsRoot) {
|
||||||
|
rel := strings.TrimPrefix(abs, versionsRoot)
|
||||||
|
parts := strings.Split(rel, string(os.PathSeparator))
|
||||||
|
if len(parts) > 0 && parts[0] != "" {
|
||||||
|
return parts[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func initVpnToolInstallTask(tool, version string) {
|
||||||
|
vpnToolInstallTaskMu.Lock()
|
||||||
|
defer vpnToolInstallTaskMu.Unlock()
|
||||||
|
vpnToolInstallTasks[tool] = &VpnToolInstallTask{
|
||||||
|
Tool: tool,
|
||||||
|
Running: true,
|
||||||
|
Progress: 0,
|
||||||
|
Message: "preparing",
|
||||||
|
Logs: []string{"Preparing install task..."},
|
||||||
|
Error: "",
|
||||||
|
Version: version,
|
||||||
|
UpdatedAt: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateVpnToolInstallTask(tool string, progress float64, message string) {
|
||||||
|
vpnToolInstallTaskMu.Lock()
|
||||||
|
defer vpnToolInstallTaskMu.Unlock()
|
||||||
|
task, ok := vpnToolInstallTasks[tool]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if progress < 0 {
|
||||||
|
progress = 0
|
||||||
|
}
|
||||||
|
if progress > 1 {
|
||||||
|
progress = 1
|
||||||
|
}
|
||||||
|
task.Progress = progress
|
||||||
|
if strings.TrimSpace(message) != "" {
|
||||||
|
task.Message = message
|
||||||
|
}
|
||||||
|
task.UpdatedAt = time.Now().Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendVpnToolInstallLog(tool, line string) {
|
||||||
|
vpnToolInstallTaskMu.Lock()
|
||||||
|
defer vpnToolInstallTaskMu.Unlock()
|
||||||
|
task, ok := vpnToolInstallTasks[tool]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task.Logs = append(task.Logs, line)
|
||||||
|
if len(task.Logs) > 200 {
|
||||||
|
task.Logs = task.Logs[len(task.Logs)-200:]
|
||||||
|
}
|
||||||
|
task.UpdatedAt = time.Now().Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
func finishVpnToolInstallTask(tool string, err error) {
|
||||||
|
vpnToolInstallTaskMu.Lock()
|
||||||
|
defer vpnToolInstallTaskMu.Unlock()
|
||||||
|
task, ok := vpnToolInstallTasks[tool]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task.Running = false
|
||||||
|
task.UpdatedAt = time.Now().Unix()
|
||||||
|
if err != nil {
|
||||||
|
task.Error = err.Error()
|
||||||
|
task.Message = "failed"
|
||||||
|
task.Logs = append(task.Logs, "Install failed: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task.Progress = 1
|
||||||
|
task.Message = "completed"
|
||||||
|
task.Error = ""
|
||||||
|
task.Logs = append(task.Logs, "Install completed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcGetVpnToolInstallTask(tool string) (VpnToolInstallTask, error) {
|
||||||
|
spec, err := getVpnToolSpec(tool)
|
||||||
|
if err != nil {
|
||||||
|
return VpnToolInstallTask{}, err
|
||||||
|
}
|
||||||
|
_ = spec
|
||||||
|
vpnToolInstallTaskMu.Lock()
|
||||||
|
defer vpnToolInstallTaskMu.Unlock()
|
||||||
|
task, ok := vpnToolInstallTasks[tool]
|
||||||
|
if !ok {
|
||||||
|
return VpnToolInstallTask{
|
||||||
|
Tool: tool,
|
||||||
|
Running: false,
|
||||||
|
Progress: 0,
|
||||||
|
Message: "",
|
||||||
|
Logs: []string{},
|
||||||
|
Error: "",
|
||||||
|
Version: "",
|
||||||
|
UpdatedAt: time.Now().Unix(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
cp := *task
|
||||||
|
cp.Logs = append([]string{}, task.Logs...)
|
||||||
|
return cp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcGetVpnToolStatus(tool string) (VpnToolStatus, error) {
|
||||||
|
spec, err := getVpnToolSpec(tool)
|
||||||
|
if err != nil {
|
||||||
|
return VpnToolStatus{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
status := VpnToolStatus{
|
||||||
|
Tool: spec.Name,
|
||||||
|
Installed: false,
|
||||||
|
Source: "none",
|
||||||
|
CurrentVersion: currentManagedVersion(spec),
|
||||||
|
DetectedVersion: "",
|
||||||
|
ManagedVersions: listManagedVersions(spec),
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryBinary := spec.Binaries[0]
|
||||||
|
versionBinary := spec.VersionBinary
|
||||||
|
if strings.TrimSpace(versionBinary) == "" {
|
||||||
|
versionBinary = primaryBinary
|
||||||
|
}
|
||||||
|
|
||||||
|
managedPrimary := managedBinaryPath(spec, primaryBinary)
|
||||||
|
managedVersionBinary := managedBinaryPath(spec, versionBinary)
|
||||||
|
if _, err := os.Stat(managedPrimary); err == nil {
|
||||||
|
status.Installed = true
|
||||||
|
status.Source = "managed"
|
||||||
|
status.DetectedVersion = detectCommandVersion(managedVersionBinary, spec.VersionFlags)
|
||||||
|
if status.CurrentVersion == "" {
|
||||||
|
status.CurrentVersion = "managed"
|
||||||
|
}
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryLookedUp, primaryErr := findExecutablePath(primaryBinary)
|
||||||
|
if primaryErr == nil {
|
||||||
|
status.Installed = true
|
||||||
|
status.Source = "system"
|
||||||
|
_ = primaryLookedUp
|
||||||
|
versionLookedUp, versionErr := findExecutablePath(versionBinary)
|
||||||
|
if versionErr == nil {
|
||||||
|
status.DetectedVersion = detectCommandVersion(versionLookedUp, spec.VersionFlags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcListVpnToolReleases(tool string) ([]VpnToolRelease, error) {
|
||||||
|
spec, err := getVpnToolSpec(tool)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
systemInfo, err := rpcGetVpnToolSystemInfo()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/releases?per_page=20", spec.Repo)
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
context.Background(),
|
||||||
|
http.MethodGet,
|
||||||
|
apiURL,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to build release request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/vnd.github+json")
|
||||||
|
req.Header.Set("User-Agent", "kvm-vpn-tool-manager")
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 20 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch releases: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
||||||
|
return nil, fmt.Errorf("fetch releases failed with status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||||
|
}
|
||||||
|
|
||||||
|
var ghReleases []githubRelease
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&ghReleases); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse release json: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keywords := systemInfo.ArchKeywords
|
||||||
|
results := make([]VpnToolRelease, 0, len(ghReleases))
|
||||||
|
for _, rel := range ghReleases {
|
||||||
|
if rel.Draft {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if spec.Name == "vnt" {
|
||||||
|
tag := strings.TrimSpace(rel.TagName)
|
||||||
|
if tag != vntPinnedVersion && tag != strings.TrimPrefix(vntPinnedVersion, "v") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
releaseItem := VpnToolRelease{
|
||||||
|
TagName: strings.TrimSpace(rel.TagName),
|
||||||
|
Assets: make([]VpnToolReleaseAsset, 0, len(rel.Assets)),
|
||||||
|
}
|
||||||
|
for _, asset := range rel.Assets {
|
||||||
|
nameLower := strings.ToLower(strings.TrimSpace(asset.Name))
|
||||||
|
isLinux := strings.Contains(nameLower, "linux")
|
||||||
|
archMatch := false
|
||||||
|
for _, kw := range keywords {
|
||||||
|
if strings.Contains(nameLower, strings.ToLower(kw)) {
|
||||||
|
archMatch = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isLinux {
|
||||||
|
archMatch = false
|
||||||
|
}
|
||||||
|
releaseItem.Assets = append(releaseItem.Assets, VpnToolReleaseAsset{
|
||||||
|
Name: asset.Name,
|
||||||
|
URL: strings.TrimSpace(asset.BrowserDownloadURL),
|
||||||
|
ArchMatch: archMatch,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if releaseItem.TagName != "" {
|
||||||
|
results = append(results, releaseItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadFileToPath(url, targetPath string, onProgress func(downloaded int64, total int64)) error {
|
||||||
|
if strings.TrimSpace(url) == "" {
|
||||||
|
return fmt.Errorf("empty download url")
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
context.Background(),
|
||||||
|
http.MethodGet,
|
||||||
|
url,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create download request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", "kvm-vpn-tool-manager")
|
||||||
|
client := &http.Client{Timeout: 2 * time.Minute}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to download file: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
||||||
|
return fmt.Errorf("download failed with status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := os.Create(targetPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create target file: %w", err)
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
total := resp.ContentLength
|
||||||
|
var downloaded int64
|
||||||
|
buf := make([]byte, 64*1024)
|
||||||
|
for {
|
||||||
|
n, readErr := resp.Body.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
written, writeErr := out.Write(buf[:n])
|
||||||
|
if writeErr != nil {
|
||||||
|
return fmt.Errorf("failed to write target file: %w", writeErr)
|
||||||
|
}
|
||||||
|
if written != n {
|
||||||
|
return fmt.Errorf("short write while saving downloaded file")
|
||||||
|
}
|
||||||
|
downloaded += int64(n)
|
||||||
|
if onProgress != nil {
|
||||||
|
onProgress(downloaded, total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if readErr == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if readErr != nil {
|
||||||
|
return fmt.Errorf("failed to read download stream: %w", readErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractFromTarGz(archivePath string, targetBinaryNames []string, outDir string) (map[string]bool, error) {
|
||||||
|
f, err := os.Open(archivePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
gzReader, err := gzip.NewReader(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer gzReader.Close()
|
||||||
|
tr := tar.NewReader(gzReader)
|
||||||
|
|
||||||
|
need := make(map[string]bool, len(targetBinaryNames))
|
||||||
|
found := make(map[string]bool, len(targetBinaryNames))
|
||||||
|
for _, b := range targetBinaryNames {
|
||||||
|
need[b] = true
|
||||||
|
found[b] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
hdr, err := tr.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return found, err
|
||||||
|
}
|
||||||
|
if hdr.Typeflag != tar.TypeReg {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
base := filepath.Base(hdr.Name)
|
||||||
|
if !need[base] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dstPath := filepath.Join(outDir, base)
|
||||||
|
dst, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return found, err
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(dst, tr); err != nil {
|
||||||
|
dst.Close()
|
||||||
|
return found, err
|
||||||
|
}
|
||||||
|
if err := dst.Close(); err != nil {
|
||||||
|
return found, err
|
||||||
|
}
|
||||||
|
found[base] = true
|
||||||
|
}
|
||||||
|
return found, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractFromZip(archivePath string, targetBinaryNames []string, outDir string) (map[string]bool, error) {
|
||||||
|
zr, err := zip.OpenReader(archivePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer zr.Close()
|
||||||
|
|
||||||
|
need := make(map[string]bool, len(targetBinaryNames))
|
||||||
|
found := make(map[string]bool, len(targetBinaryNames))
|
||||||
|
for _, b := range targetBinaryNames {
|
||||||
|
need[b] = true
|
||||||
|
found[b] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range zr.File {
|
||||||
|
base := filepath.Base(f.Name)
|
||||||
|
if !need[base] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
src, err := f.Open()
|
||||||
|
if err != nil {
|
||||||
|
return found, err
|
||||||
|
}
|
||||||
|
dstPath := filepath.Join(outDir, base)
|
||||||
|
dst, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
|
||||||
|
if err != nil {
|
||||||
|
src.Close()
|
||||||
|
return found, err
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(dst, src); err != nil {
|
||||||
|
src.Close()
|
||||||
|
dst.Close()
|
||||||
|
return found, err
|
||||||
|
}
|
||||||
|
src.Close()
|
||||||
|
if err := dst.Close(); err != nil {
|
||||||
|
return found, err
|
||||||
|
}
|
||||||
|
found[base] = true
|
||||||
|
}
|
||||||
|
return found, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureBinariesInstalled(found map[string]bool, required []string) error {
|
||||||
|
missing := make([]string, 0)
|
||||||
|
for _, b := range required {
|
||||||
|
if !found[b] {
|
||||||
|
missing = append(missing, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(missing) > 0 {
|
||||||
|
return fmt.Errorf("required binaries are missing in package: %s", strings.Join(missing, ", "))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupCurrentSymlinks(spec vpnToolSpec, version string) error {
|
||||||
|
currentDir := vpnToolCurrentDir(spec)
|
||||||
|
versionDir := filepath.Join(vpnToolVersionsDir(spec), version)
|
||||||
|
if err := os.MkdirAll(currentDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, b := range spec.Binaries {
|
||||||
|
currentLink := filepath.Join(currentDir, b)
|
||||||
|
_ = os.Remove(currentLink)
|
||||||
|
target := filepath.Join(versionDir, b)
|
||||||
|
if err := os.Symlink(target, currentLink); err != nil {
|
||||||
|
return fmt.Errorf("failed to update current binary symlink for %s: %w", b, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcInstallVpnTool(tool, version, assetName, downloadURL string) error {
|
||||||
|
spec, err := getVpnToolSpec(tool)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
version = strings.TrimSpace(version)
|
||||||
|
if version == "" {
|
||||||
|
return fmt.Errorf("version is required")
|
||||||
|
}
|
||||||
|
if spec.Name == "vnt" && version != vntPinnedVersion && version != strings.TrimPrefix(vntPinnedVersion, "v") {
|
||||||
|
return fmt.Errorf("vnt install is temporarily pinned to %s", vntPinnedVersion)
|
||||||
|
}
|
||||||
|
downloadURL = strings.TrimSpace(downloadURL)
|
||||||
|
if downloadURL == "" {
|
||||||
|
return fmt.Errorf("downloadURL is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
versionDir := filepath.Join(vpnToolVersionsDir(spec), version)
|
||||||
|
appendVpnToolInstallLog(tool, "Preparing version directory...")
|
||||||
|
updateVpnToolInstallTask(tool, 0.05, "preparing")
|
||||||
|
if err := os.MkdirAll(versionDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create version directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpFile, err := os.CreateTemp("", "vpn-tool-*")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp file: %w", err)
|
||||||
|
}
|
||||||
|
tmpPath := tmpFile.Name()
|
||||||
|
tmpFile.Close()
|
||||||
|
defer os.Remove(tmpPath)
|
||||||
|
|
||||||
|
appendVpnToolInstallLog(tool, "Downloading release package...")
|
||||||
|
if err := downloadFileToPath(downloadURL, tmpPath, func(downloaded int64, total int64) {
|
||||||
|
if total > 0 {
|
||||||
|
ratio := float64(downloaded) / float64(total)
|
||||||
|
updateVpnToolInstallTask(tool, 0.1+ratio*0.65, "downloading")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Unknown total size, keep showing progress moving slowly.
|
||||||
|
updateVpnToolInstallTask(tool, 0.4, "downloading")
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
appendVpnToolInstallLog(tool, "Download completed.")
|
||||||
|
|
||||||
|
assetLower := strings.ToLower(strings.TrimSpace(assetName))
|
||||||
|
if assetLower == "" {
|
||||||
|
assetLower = strings.ToLower(strings.TrimSpace(downloadURL))
|
||||||
|
}
|
||||||
|
|
||||||
|
found := map[string]bool{}
|
||||||
|
updateVpnToolInstallTask(tool, 0.8, "extracting")
|
||||||
|
appendVpnToolInstallLog(tool, "Extracting package...")
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(assetLower, ".tar.gz") || strings.HasSuffix(assetLower, ".tgz"):
|
||||||
|
found, err = extractFromTarGz(tmpPath, spec.Binaries, versionDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to extract tar.gz: %w", err)
|
||||||
|
}
|
||||||
|
case strings.HasSuffix(assetLower, ".zip"):
|
||||||
|
found, err = extractFromZip(tmpPath, spec.Binaries, versionDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to extract zip: %w", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if len(spec.Binaries) != 1 {
|
||||||
|
return fmt.Errorf("single binary install is not supported for %s", spec.Name)
|
||||||
|
}
|
||||||
|
dstPath := filepath.Join(versionDir, spec.Binaries[0])
|
||||||
|
input, err := os.Open(tmpPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer input.Close()
|
||||||
|
output, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(output, input); err != nil {
|
||||||
|
output.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := output.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
found[spec.Binaries[0]] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
appendVpnToolInstallLog(tool, "Validating installed binaries...")
|
||||||
|
updateVpnToolInstallTask(tool, 0.92, "validating")
|
||||||
|
if err := ensureBinariesInstalled(found, spec.Binaries); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
appendVpnToolInstallLog(tool, "Switching to installed version...")
|
||||||
|
updateVpnToolInstallTask(tool, 0.97, "activating")
|
||||||
|
if err := setupCurrentSymlinks(spec, version); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
updateVpnToolInstallTask(tool, 1, "completed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcStartVpnToolInstall(tool, version, assetName, downloadURL string) error {
|
||||||
|
_, err := getVpnToolSpec(tool)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
vpnToolInstallTaskMu.Lock()
|
||||||
|
if task, ok := vpnToolInstallTasks[tool]; ok && task.Running {
|
||||||
|
vpnToolInstallTaskMu.Unlock()
|
||||||
|
return fmt.Errorf("install task is already running for %s", tool)
|
||||||
|
}
|
||||||
|
vpnToolInstallTaskMu.Unlock()
|
||||||
|
|
||||||
|
initVpnToolInstallTask(tool, version)
|
||||||
|
go func() {
|
||||||
|
err := rpcInstallVpnTool(tool, version, assetName, downloadURL)
|
||||||
|
finishVpnToolInstallTask(tool, err)
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcUseVpnToolVersion(tool, version string) error {
|
||||||
|
spec, err := getVpnToolSpec(tool)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
version = strings.TrimSpace(version)
|
||||||
|
if version == "" {
|
||||||
|
return fmt.Errorf("version is required")
|
||||||
|
}
|
||||||
|
versionDir := filepath.Join(vpnToolVersionsDir(spec), version)
|
||||||
|
info, err := os.Stat(versionDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("version is not installed: %w", err)
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
return fmt.Errorf("invalid version directory")
|
||||||
|
}
|
||||||
|
for _, b := range spec.Binaries {
|
||||||
|
if _, err := os.Stat(filepath.Join(versionDir, b)); err != nil {
|
||||||
|
return fmt.Errorf("binary %s is missing in version %s", b, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return setupCurrentSymlinks(spec, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcUninstallVpnToolVersion(tool, version string) error {
|
||||||
|
spec, err := getVpnToolSpec(tool)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
version = strings.TrimSpace(version)
|
||||||
|
if version == "" {
|
||||||
|
return fmt.Errorf("version is required")
|
||||||
|
}
|
||||||
|
versionDir := filepath.Join(vpnToolVersionsDir(spec), version)
|
||||||
|
if err := os.RemoveAll(versionDir); err != nil {
|
||||||
|
return fmt.Errorf("failed to uninstall version: %w", err)
|
||||||
|
}
|
||||||
|
if currentManagedVersion(spec) == version {
|
||||||
|
currentDir := vpnToolCurrentDir(spec)
|
||||||
|
for _, b := range spec.Binaries {
|
||||||
|
_ = os.Remove(filepath.Join(currentDir, b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining := listManagedVersions(spec)
|
||||||
|
if len(remaining) > 0 {
|
||||||
|
// Automatically switch to latest remaining managed version.
|
||||||
|
if err := setupCurrentSymlinks(spec, remaining[0]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -71,6 +71,47 @@ export interface CloudflaredRunningResponse {
|
|||||||
running: boolean;
|
running: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ManagedVpnTool = "frpc" | "easytier" | "vnt" | "cloudflared";
|
||||||
|
|
||||||
|
export interface VpnToolSystemInfo {
|
||||||
|
goos: string;
|
||||||
|
goarch: string;
|
||||||
|
uname_arch: string;
|
||||||
|
arch_label: string;
|
||||||
|
arch_keywords: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VpnToolStatus {
|
||||||
|
tool: string;
|
||||||
|
installed: boolean;
|
||||||
|
source: string;
|
||||||
|
current_version: string;
|
||||||
|
detected_version: string;
|
||||||
|
managed_versions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VpnToolReleaseAsset {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
arch_match: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VpnToolRelease {
|
||||||
|
tag_name: string;
|
||||||
|
assets: VpnToolReleaseAsset[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VpnToolInstallTask {
|
||||||
|
tool: string;
|
||||||
|
running: boolean;
|
||||||
|
progress: number;
|
||||||
|
message: string;
|
||||||
|
logs: string[];
|
||||||
|
error: string;
|
||||||
|
version: string;
|
||||||
|
updated_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WireguardStatus {
|
export interface WireguardStatus {
|
||||||
running: boolean;
|
running: boolean;
|
||||||
}
|
}
|
||||||
@@ -198,6 +239,20 @@ function AccessContent({ setOpenDialog }: { setOpenDialog: (open: boolean) => vo
|
|||||||
const [cloudflaredLog, setCloudflaredLog] = useState<string>("");
|
const [cloudflaredLog, setCloudflaredLog] = useState<string>("");
|
||||||
const [showCloudflaredLogModal, setShowCloudflaredLogModal] = useState(false);
|
const [showCloudflaredLogModal, setShowCloudflaredLogModal] = useState(false);
|
||||||
|
|
||||||
|
const [vpnToolSystemInfo, setVpnToolSystemInfo] = useState<VpnToolSystemInfo | null>(null);
|
||||||
|
const [vpnToolStatusMap, setVpnToolStatusMap] = useState<Record<string, VpnToolStatus>>({});
|
||||||
|
const [vpnToolReleasesMap, setVpnToolReleasesMap] = useState<Record<string, VpnToolRelease[]>>({});
|
||||||
|
const [vpnToolSelectedVersionMap, setVpnToolSelectedVersionMap] = useState<Record<string, string>>({});
|
||||||
|
const [vpnToolSelectedAssetMap, setVpnToolSelectedAssetMap] = useState<Record<string, string>>({});
|
||||||
|
const [vpnToolBusyMap, setVpnToolBusyMap] = useState<Record<string, boolean>>({});
|
||||||
|
const [vpnToolInstallTaskMap, setVpnToolInstallTaskMap] = useState<Record<string, VpnToolInstallTask>>({});
|
||||||
|
const [vpnToolInstallPanelOpenMap, setVpnToolInstallPanelOpenMap] = useState<Record<string, boolean>>({
|
||||||
|
frpc: false,
|
||||||
|
easytier: false,
|
||||||
|
vnt: false,
|
||||||
|
cloudflared: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
const getTLSState = useCallback(() => {
|
const getTLSState = useCallback(() => {
|
||||||
send("getTLSState", {}, resp => {
|
send("getTLSState", {}, resp => {
|
||||||
@@ -282,10 +337,156 @@ function AccessContent({ setOpenDialog }: { setOpenDialog: (open: boolean) => vo
|
|||||||
});
|
});
|
||||||
}, [send]);
|
}, [send]);
|
||||||
|
|
||||||
|
const managedTools: ManagedVpnTool[] = ["frpc", "easytier", "vnt", "cloudflared"];
|
||||||
|
|
||||||
|
const getVpnToolSystemInfo = useCallback(() => {
|
||||||
|
send("getVpnToolSystemInfo", {}, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(`Failed to get system architecture info: ${resp.error.data || "Unknown error"}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setVpnToolSystemInfo(resp.result as VpnToolSystemInfo);
|
||||||
|
});
|
||||||
|
}, [send]);
|
||||||
|
|
||||||
|
const getVpnToolStatus = useCallback((tool: ManagedVpnTool) => {
|
||||||
|
send("getVpnToolStatus", { tool }, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(`Failed to get ${tool} status: ${resp.error.data || "Unknown error"}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setVpnToolStatusMap(prev => ({ ...prev, [tool]: resp.result as VpnToolStatus }));
|
||||||
|
});
|
||||||
|
}, [send]);
|
||||||
|
|
||||||
|
const listVpnToolReleases = useCallback((tool: ManagedVpnTool) => {
|
||||||
|
send("listVpnToolReleases", { tool }, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(`Failed to list ${tool} releases: ${resp.error.data || "Unknown error"}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const releases = resp.result as VpnToolRelease[];
|
||||||
|
setVpnToolReleasesMap(prev => ({ ...prev, [tool]: releases }));
|
||||||
|
if (!releases.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setVpnToolSelectedVersionMap(prev => {
|
||||||
|
if (prev[tool]) return prev;
|
||||||
|
return { ...prev, [tool]: releases[0].tag_name };
|
||||||
|
});
|
||||||
|
setVpnToolSelectedAssetMap(prev => {
|
||||||
|
if (prev[tool]) return prev;
|
||||||
|
const firstRelease = releases[0];
|
||||||
|
const preferred = firstRelease.assets.find(asset => asset.arch_match) || firstRelease.assets[0];
|
||||||
|
return preferred ? { ...prev, [tool]: preferred.url } : prev;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [send]);
|
||||||
|
|
||||||
|
const refreshVpnToolManager = useCallback((tool: ManagedVpnTool, withReleases: boolean) => {
|
||||||
|
getVpnToolStatus(tool);
|
||||||
|
if (withReleases) {
|
||||||
|
listVpnToolReleases(tool);
|
||||||
|
}
|
||||||
|
}, [getVpnToolStatus, listVpnToolReleases]);
|
||||||
|
|
||||||
|
const getVpnToolInstallTask = useCallback((tool: ManagedVpnTool) => {
|
||||||
|
send("getVpnToolInstallTask", { tool }, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setVpnToolInstallTaskMap(prev => ({ ...prev, [tool]: resp.result as VpnToolInstallTask }));
|
||||||
|
});
|
||||||
|
}, [send]);
|
||||||
|
|
||||||
|
const handleVpnToolVersionChange = useCallback((tool: ManagedVpnTool, version: string) => {
|
||||||
|
setVpnToolSelectedVersionMap(prev => ({ ...prev, [tool]: version }));
|
||||||
|
const releases = vpnToolReleasesMap[tool] || [];
|
||||||
|
const selectedRelease = releases.find(release => release.tag_name === version);
|
||||||
|
if (!selectedRelease || !selectedRelease.assets.length) {
|
||||||
|
setVpnToolSelectedAssetMap(prev => ({ ...prev, [tool]: "" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const preferred = selectedRelease.assets.find(asset => asset.arch_match) || selectedRelease.assets[0];
|
||||||
|
setVpnToolSelectedAssetMap(prev => ({ ...prev, [tool]: preferred?.url || "" }));
|
||||||
|
}, [vpnToolReleasesMap]);
|
||||||
|
|
||||||
|
const handleInstallVpnTool = useCallback((tool: ManagedVpnTool) => {
|
||||||
|
const version = vpnToolSelectedVersionMap[tool];
|
||||||
|
const downloadURL = vpnToolSelectedAssetMap[tool];
|
||||||
|
const releases = vpnToolReleasesMap[tool] || [];
|
||||||
|
const release = releases.find(item => item.tag_name === version);
|
||||||
|
const selectedAsset = release?.assets.find(asset => asset.url === downloadURL);
|
||||||
|
if (!version || !downloadURL || !selectedAsset) {
|
||||||
|
notifications.error(`Please select version and release asset for ${tool}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setVpnToolBusyMap(prev => ({ ...prev, [tool]: true }));
|
||||||
|
send("startVpnToolInstall", { tool, version, assetName: selectedAsset.name, downloadURL }, resp => {
|
||||||
|
setVpnToolBusyMap(prev => ({ ...prev, [tool]: false }));
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(`Failed to install ${tool}: ${resp.error.data || "Unknown error"}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notifications.success(`${tool} install task started (${version})`);
|
||||||
|
getVpnToolInstallTask(tool);
|
||||||
|
});
|
||||||
|
}, [send, vpnToolSelectedVersionMap, vpnToolSelectedAssetMap, vpnToolReleasesMap, getVpnToolInstallTask]);
|
||||||
|
|
||||||
|
const handleUninstallVpnToolVersion = useCallback((tool: ManagedVpnTool, version: string) => {
|
||||||
|
if (!version) {
|
||||||
|
notifications.error("Please select installed version");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setVpnToolBusyMap(prev => ({ ...prev, [tool]: true }));
|
||||||
|
send("uninstallVpnToolVersion", { tool, version }, resp => {
|
||||||
|
setVpnToolBusyMap(prev => ({ ...prev, [tool]: false }));
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(`Failed to uninstall ${tool} version ${version}: ${resp.error.data || "Unknown error"}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notifications.success(`${tool} ${version} uninstalled`);
|
||||||
|
refreshVpnToolManager(tool, true);
|
||||||
|
});
|
||||||
|
}, [send, refreshVpnToolManager]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getCloudflaredStatus();
|
getCloudflaredStatus();
|
||||||
}, [getCloudflaredStatus]);
|
}, [getCloudflaredStatus]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getVpnToolSystemInfo();
|
||||||
|
managedTools.forEach(tool => {
|
||||||
|
refreshVpnToolManager(tool, false);
|
||||||
|
getVpnToolInstallTask(tool);
|
||||||
|
});
|
||||||
|
}, [getVpnToolSystemInfo, refreshVpnToolManager, getVpnToolInstallTask]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
managedTools.forEach(tool => {
|
||||||
|
getVpnToolInstallTask(tool);
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [getVpnToolInstallTask]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const tabToolMap: Partial<Record<string, ManagedVpnTool>> = {
|
||||||
|
frp: "frpc",
|
||||||
|
easytier: "easytier",
|
||||||
|
vnt: "vnt",
|
||||||
|
cloudflared: "cloudflared",
|
||||||
|
};
|
||||||
|
const tool = tabToolMap[activeTab];
|
||||||
|
if (tool) {
|
||||||
|
refreshVpnToolManager(tool, false);
|
||||||
|
}
|
||||||
|
if (activeTab === "cloudflared") {
|
||||||
|
getCloudflaredStatus();
|
||||||
|
}
|
||||||
|
}, [activeTab, refreshVpnToolManager, getCloudflaredStatus]);
|
||||||
|
|
||||||
// Handle TLS mode change
|
// Handle TLS mode change
|
||||||
const handleTlsModeChange = (value: string) => {
|
const handleTlsModeChange = (value: string) => {
|
||||||
setTlsMode(value);
|
setTlsMode(value);
|
||||||
@@ -870,6 +1071,154 @@ function AccessContent({ setOpenDialog }: { setOpenDialog: (open: boolean) => vo
|
|||||||
getVntConfigFile();
|
getVntConfigFile();
|
||||||
}, [getVntStatus, getVntConfig]);
|
}, [getVntStatus, getVntConfig]);
|
||||||
|
|
||||||
|
const renderVpnToolManager = (tool: ManagedVpnTool, label: string) => {
|
||||||
|
const status = vpnToolStatusMap[tool];
|
||||||
|
const releases = vpnToolReleasesMap[tool] || [];
|
||||||
|
const selectedVersion = vpnToolSelectedVersionMap[tool] || "";
|
||||||
|
const selectedRelease = releases.find(release => release.tag_name === selectedVersion);
|
||||||
|
const selectedAsset = vpnToolSelectedAssetMap[tool] || "";
|
||||||
|
const assets = selectedRelease?.assets || [];
|
||||||
|
const busy = vpnToolBusyMap[tool] || false;
|
||||||
|
const installTask = vpnToolInstallTaskMap[tool];
|
||||||
|
const installRunning = installTask?.running === true;
|
||||||
|
const installPanelOpen = vpnToolInstallPanelOpenMap[tool] || false;
|
||||||
|
const isInstalled = status?.installed === true;
|
||||||
|
const uninstallVersion = status?.current_version || status?.managed_versions?.[0] || "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-700">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||||
|
{label} {$at("Version Manager")}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
text={$at("Refresh")}
|
||||||
|
onClick={() => refreshVpnToolManager(tool, installPanelOpen)}
|
||||||
|
disabled={busy || installRunning}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
text={installPanelOpen ? $at("Hide Install Actions") : $at("Show Install Actions")}
|
||||||
|
onClick={() => {
|
||||||
|
const nextOpen = !installPanelOpen;
|
||||||
|
setVpnToolInstallPanelOpenMap(prev => ({ ...prev, [tool]: nextOpen }));
|
||||||
|
if (nextOpen) {
|
||||||
|
listVpnToolReleases(tool);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={busy || installRunning}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
{$at("Install Status")}: {status?.installed ? $at("Installed") : $at("Not Installed")}
|
||||||
|
{status?.source ? ` (${status.source})` : ""}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
{$at("Detected Version")}: {status?.detected_version || "-"}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{installPanelOpen ? (
|
||||||
|
<>
|
||||||
|
<div className="text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
{$at("System Architecture")}: {vpnToolSystemInfo?.arch_label || "unknown"}
|
||||||
|
{vpnToolSystemInfo?.arch_keywords?.length
|
||||||
|
? ` (${vpnToolSystemInfo.arch_keywords.join(", ")})`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<SettingsItem title={$at("Release Version")} description="">
|
||||||
|
<Select
|
||||||
|
className={isMobile ? "!w-full !h-[36px]" : "!w-[28%] !h-[36px]"}
|
||||||
|
value={selectedVersion}
|
||||||
|
onChange={value => handleVpnToolVersionChange(tool, value)}
|
||||||
|
options={releases.map(release => ({
|
||||||
|
value: release.tag_name,
|
||||||
|
label: release.tag_name,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
<SettingsItem title={$at("Release Asset")} description="">
|
||||||
|
<Select
|
||||||
|
className={isMobile ? "!w-full !h-[36px]" : "!w-[60%] !h-[36px]"}
|
||||||
|
value={selectedAsset}
|
||||||
|
onChange={value => setVpnToolSelectedAssetMap(prev => ({ ...prev, [tool]: value }))}
|
||||||
|
options={assets.map(asset => ({
|
||||||
|
value: asset.url,
|
||||||
|
label: `${asset.arch_match ? "[ARCH OK] " : ""}${asset.name}`,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
{isInstalled ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="primary"
|
||||||
|
text={$at("Update")}
|
||||||
|
onClick={() => handleInstallVpnTool(tool)}
|
||||||
|
disabled={busy || installRunning || !selectedVersion || !selectedAsset}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="danger"
|
||||||
|
text={$at("Uninstall")}
|
||||||
|
onClick={() => handleUninstallVpnToolVersion(tool, uninstallVersion)}
|
||||||
|
disabled={busy || installRunning || !uninstallVersion}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="primary"
|
||||||
|
text={$at("Install")}
|
||||||
|
onClick={() => handleInstallVpnTool(tool)}
|
||||||
|
disabled={busy || installRunning || !selectedVersion || !selectedAsset}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{installRunning && installTask ? (
|
||||||
|
<div className="space-y-2 rounded border border-slate-200 p-2 dark:border-slate-700">
|
||||||
|
<div className="text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
{$at("Install Task")}: {installTask.message || "-"}
|
||||||
|
{installTask.error ? ` (${installTask.error})` : ""}
|
||||||
|
</div>
|
||||||
|
<div className="h-2 w-full rounded bg-slate-200 dark:bg-slate-700">
|
||||||
|
<div
|
||||||
|
className="h-2 rounded bg-blue-500 transition-all"
|
||||||
|
style={{ width: `${Math.max(0, Math.min(100, Math.round((installTask.progress || 0) * 100)))}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
{$at("Progress")}: {Math.max(0, Math.min(100, Math.round((installTask.progress || 0) * 100)))}%
|
||||||
|
</div>
|
||||||
|
{!!installTask.logs?.length && (
|
||||||
|
<pre className="max-h-36 overflow-auto rounded bg-slate-50 p-2 text-[11px] text-slate-600 dark:bg-slate-900 dark:text-slate-300">
|
||||||
|
{installTask.logs.join("\n")}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{status?.managed_versions?.length ? (
|
||||||
|
<div className="text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
{$at("Installed Versions")}: {status.managed_versions.join(", ")}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -1307,6 +1656,7 @@ function AccessContent({ setOpenDialog }: { setOpenDialog: (open: boolean) => vo
|
|||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{renderVpnToolManager("easytier", "EasyTier")}
|
||||||
{ easyTierRunningStatus.running ? (
|
{ easyTierRunningStatus.running ? (
|
||||||
<div className="flex-1 space-y-2">
|
<div className="flex-1 space-y-2">
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
@@ -1424,6 +1774,7 @@ function AccessContent({ setOpenDialog }: { setOpenDialog: (open: boolean) => vo
|
|||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{renderVpnToolManager("vnt", "Vnt")}
|
||||||
{ vntRunningStatus.running ? (
|
{ vntRunningStatus.running ? (
|
||||||
|
|
||||||
<div className="flex-1 space-y-2">
|
<div className="flex-1 space-y-2">
|
||||||
@@ -1636,6 +1987,7 @@ function AccessContent({ setOpenDialog }: { setOpenDialog: (open: boolean) => vo
|
|||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{renderVpnToolManager("cloudflared", "Cloudflare")}
|
||||||
{cloudflaredRunningStatus.running ? (
|
{cloudflaredRunningStatus.running ? (
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -1684,6 +2036,7 @@ function AccessContent({ setOpenDialog }: { setOpenDialog: (open: boolean) => vo
|
|||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{renderVpnToolManager("frpc", "frpc")}
|
||||||
<TextAreaWithLabel
|
<TextAreaWithLabel
|
||||||
label={$at("Edit frpc.toml")}
|
label={$at("Edit frpc.toml")}
|
||||||
placeholder={$at("Enter frpc configuration")}
|
placeholder={$at("Enter frpc configuration")}
|
||||||
|
|||||||
16
vpn.go
16
vpn.go
@@ -332,7 +332,7 @@ func rpcStartFrpc(frpcToml string) error {
|
|||||||
if err := os.WriteFile(frpcTomlPath, []byte(frpcToml), 0600); err != nil {
|
if err := os.WriteFile(frpcTomlPath, []byte(frpcToml), 0600); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
cmd := exec.Command("frpc", "-c", frpcTomlPath)
|
cmd := exec.Command(resolveVpnToolBinary("frpc", "frpc"), "-c", frpcTomlPath)
|
||||||
cmd.Stdout = nil
|
cmd.Stdout = nil
|
||||||
cmd.Stderr = nil
|
cmd.Stderr = nil
|
||||||
logFile, err := os.OpenFile(frpcLogPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
logFile, err := os.OpenFile(frpcLogPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||||
@@ -386,7 +386,9 @@ type CloudflaredStatus struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cloudflaredRunning() bool {
|
func cloudflaredRunning() bool {
|
||||||
cmd := exec.Command("pgrep", "-x", "cloudflared")
|
// Only treat long-running tunnel process as running.
|
||||||
|
// This avoids false positives from short-lived version checks like `cloudflared -v`.
|
||||||
|
cmd := exec.Command("pgrep", "-f", `cloudflared.*tunnel.*run`)
|
||||||
return cmd.Run() == nil
|
return cmd.Run() == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,7 +403,7 @@ func rpcStartCloudflared(token string) error {
|
|||||||
if token == "" {
|
if token == "" {
|
||||||
return fmt.Errorf("cloudflared token is empty")
|
return fmt.Errorf("cloudflared token is empty")
|
||||||
}
|
}
|
||||||
cmd := exec.Command("cloudflared", "tunnel", "run", "--token", token)
|
cmd := exec.Command(resolveVpnToolBinary("cloudflared", "cloudflared"), "tunnel", "run", "--token", token)
|
||||||
logFile, err := os.OpenFile(cloudflaredLogPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
logFile, err := os.OpenFile(cloudflaredLogPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -521,7 +523,7 @@ func rpcGetEasyTierLog() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func rpcGetEasyTierNodeInfo() (string, error) {
|
func rpcGetEasyTierNodeInfo() (string, error) {
|
||||||
cmd := exec.Command("easytier-cli", "node")
|
cmd := exec.Command(resolveVpnToolBinary("easytier", "easytier-cli"), "node")
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get easytier node info: %w", err)
|
return "", fmt.Errorf("failed to get easytier node info: %w", err)
|
||||||
@@ -543,7 +545,7 @@ func rpcStartEasyTier(name, secret, node string) error {
|
|||||||
return fmt.Errorf("easytier config is invalid")
|
return fmt.Errorf("easytier config is invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command("easytier-core", "-d", "--network-name", name, "--network-secret", secret, "-p", node)
|
cmd := exec.Command(resolveVpnToolBinary("easytier", "easytier-core"), "-d", "--network-name", name, "--network-secret", secret, "-p", node)
|
||||||
cmd.Stdout = nil
|
cmd.Stdout = nil
|
||||||
cmd.Stderr = nil
|
cmd.Stderr = nil
|
||||||
logFile, err := os.OpenFile(easytierLogPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
logFile, err := os.OpenFile(easytierLogPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||||
@@ -639,7 +641,7 @@ func rpcGetVntLog() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func rpcGetVntInfo() (string, error) {
|
func rpcGetVntInfo() (string, error) {
|
||||||
cmd := exec.Command("vnt-cli", "--info")
|
cmd := exec.Command(resolveVpnToolBinary("vnt", "vnt-cli"), "--info")
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get vnt info: %w", err)
|
return "", fmt.Errorf("failed to get vnt info: %w", err)
|
||||||
@@ -707,7 +709,7 @@ func rpcStartVnt(configMode, token, deviceId, name, serverAddr, configFile strin
|
|||||||
args = append(args, "--compressor", "lz4")
|
args = append(args, "--compressor", "lz4")
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command("vnt-cli", args...)
|
cmd := exec.Command(resolveVpnToolBinary("vnt", "vnt-cli"), args...)
|
||||||
cmd.Stdout = nil
|
cmd.Stdout = nil
|
||||||
cmd.Stderr = nil
|
cmd.Stderr = nil
|
||||||
logFile, err := os.OpenFile(vntLogPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
logFile, err := os.OpenFile(vntLogPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||||
|
|||||||
Reference in New Issue
Block a user