Release 1.9.0: add VLESS, VMess and Trojan via Xray-core.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Navis
2026-07-29 07:05:44 +03:00
co-authored by Cursor
parent fce99cc393
commit 573a834d1b
25 changed files with 1199 additions and 36 deletions
+4 -1
View File
@@ -1,6 +1,6 @@
# Navis
Клиент [NaiveProxy](https://github.com/klzgrad/naiveproxy) + [Hysteria 2](https://github.com/apernet/hysteria) + [AmneziaWG 2.0](https://docs.amnezia.org/documentation/amnezia-wg/) с профилями, пингом, URL-подпиской и автообновлением.
Клиент [NaiveProxy](https://github.com/klzgrad/naiveproxy) + [Hysteria 2](https://github.com/apernet/hysteria) + [AmneziaWG 2.0](https://docs.amnezia.org/documentation/amnezia-wg/) + [Xray](https://github.com/XTLS/Xray-core) (VLESS / VMess / Trojan) с профилями, пингом, URL-подпиской и автообновлением.
## Платформы
@@ -210,6 +210,9 @@ https://evilfox.win/
Некоторые AV (Bkav, Microsoft Wacapew/Wacatac, Ikarus Trojan.WinGo.Agent, Google, Trapmine) часто помечают **любые** неподписанные Go-бинарники с сетью и сменой системного прокси.
В 1.9.0:
- VLESS / VMess / Trojan через официальный Xray-core (кнопка «Установить cores»).
В 1.8.0:
- автопроверка обновлений при старте и установка с автоперезапуском;
- AmneziaWG 2.0: вставьте полный `.conf` (Jc/Jmin/Jmax, S1S4, H1H4, I1I5) или `awg://…` — встроенный userspace-туннель + локальный SOCKS/HTTP.
+5
View File
@@ -24,6 +24,7 @@ import (
"vpnclient/internal/protocols/awg"
"vpnclient/internal/protocols/hysteria2"
"vpnclient/internal/protocols/naive"
"vpnclient/internal/protocols/xray"
"vpnclient/internal/update"
)
@@ -176,6 +177,10 @@ func (a *app) getState() (uiState, error) {
if path, err := awg.ResolveBinary(a.mgr.BinDir()); err == nil {
corePath, coreReady = path, true
}
case config.ProtocolVLESS, config.ProtocolVMess, config.ProtocolTrojan:
if path, err := xray.ResolveBinary(a.mgr.BinDir()); err == nil {
corePath, coreReady = path, true
}
default:
if path, err := naive.ResolveBinary(a.mgr.BinDir()); err == nil {
corePath, coreReady = path, true
+9 -2
View File
@@ -15,6 +15,7 @@ import (
"vpnclient/internal/core"
"vpnclient/internal/protocols/hysteria2"
"vpnclient/internal/protocols/naive"
"vpnclient/internal/protocols/xray"
"vpnclient/internal/update"
)
@@ -58,7 +59,7 @@ func main() {
}
func printUsage() {
fmt.Fprintf(os.Stderr, `Navis VPN client (NaiveProxy + Hysteria 2) — Windows / macOS
fmt.Fprintf(os.Stderr, `Navis VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan) — Windows / macOS
Usage:
navis init [-config configs/config.json]
@@ -69,7 +70,7 @@ Usage:
navis check-update
navis apply-update
Share links: naive+https://… hysteria2://… hy2://…
Share links: naive+https://… hysteria2://… vless://… vmess://… trojan://… awg .conf
On macOS system proxy uses networksetup (HTTP + SOCKS). Local listens:
socks://127.0.0.1:1080 http://127.0.0.1:1081
@@ -128,6 +129,12 @@ func runInstallCore(args []string) int {
return 1
}
fmt.Printf("hysteria2 core ready: %s\n", pathHy2)
pathXray, err := xray.EnsureBinary(binDir)
if err != nil {
fmt.Fprintf(os.Stderr, "install-core xray: %v\n", err)
return 1
}
fmt.Printf("xray core ready: %s\n", pathXray)
return 0
}
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+6 -6
View File
@@ -1,28 +1,28 @@
{
"version": "1.8.1",
"notes": "Исправлено автообновление: замена текущего Navis.exe на месте без .new",
"version": "1.9.0",
"notes": "VLESS / VMess / Trojan через Xray-core; нажмите «Установить cores»",
"platform": "windows-amd64",
"os": "windows",
"arch": "amd64",
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "7265ace71fdd58b86c0c9acf8ee05d3f0182623201987c9d119d9d1ab328f500",
"sha256": "bd78ead7711b23a25b1a30278ec381bd762b595e91c4c00f7370c62d785740f7",
"mandatory": false,
"platforms": {
"windows-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "7265ace71fdd58b86c0c9acf8ee05d3f0182623201987c9d119d9d1ab328f500",
"sha256": "bd78ead7711b23a25b1a30278ec381bd762b595e91c4c00f7370c62d785740f7",
"os": "windows",
"arch": "amd64"
},
"darwin-arm64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
"sha256": "9a1f9d99b930cde479f9d28ef37b0f1472ab033f6e9f28da5781617639c17d54",
"sha256": "237962c24fb875f3dd2a5ecfad0db5d6837cefb6b691860a3e1d6f0e721f8449",
"os": "darwin",
"arch": "arm64"
},
"darwin-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
"sha256": "9c036f9022761a117021af37778ecd293a3fc2b7438eaf26f804bb01ef4943f1",
"sha256": "91430bb85df68b3816d9cc643b1bdc3d022410473c4f87f4d8573bb99259ba45",
"os": "darwin",
"arch": "amd64"
}
+6 -6
View File
@@ -1,28 +1,28 @@
{
"version": "1.8.1",
"notes": "Исправлено автообновление: замена текущего Navis.exe на месте без .new",
"version": "1.9.0",
"notes": "VLESS / VMess / Trojan через Xray-core; нажмите «Установить cores»",
"platform": "windows-amd64",
"os": "windows",
"arch": "amd64",
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "7265ace71fdd58b86c0c9acf8ee05d3f0182623201987c9d119d9d1ab328f500",
"sha256": "bd78ead7711b23a25b1a30278ec381bd762b595e91c4c00f7370c62d785740f7",
"mandatory": false,
"platforms": {
"windows-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "7265ace71fdd58b86c0c9acf8ee05d3f0182623201987c9d119d9d1ab328f500",
"sha256": "bd78ead7711b23a25b1a30278ec381bd762b595e91c4c00f7370c62d785740f7",
"os": "windows",
"arch": "amd64"
},
"darwin-arm64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
"sha256": "9a1f9d99b930cde479f9d28ef37b0f1472ab033f6e9f28da5781617639c17d54",
"sha256": "237962c24fb875f3dd2a5ecfad0db5d6837cefb6b691860a3e1d6f0e721f8449",
"os": "darwin",
"arch": "arm64"
},
"darwin-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
"sha256": "9c036f9022761a117021af37778ecd293a3fc2b7438eaf26f804bb01ef4943f1",
"sha256": "91430bb85df68b3816d9cc643b1bdc3d022410473c4f87f4d8573bb99259ba45",
"os": "darwin",
"arch": "amd64"
}
+5 -5
View File
@@ -374,7 +374,7 @@
</div>
<div>
<label class="field" for="proxy">Ссылка / конфиг сервера</label>
<textarea id="proxy" rows="4" placeholder="naive+https://… · hysteria2://… · или весь .conf AmneziaWG 2.0 ([Interface]/Jc/I1…)" spellcheck="false"></textarea>
<textarea id="proxy" rows="4" placeholder="naive+https://… · hysteria2://… · vless:// · vmess:// · trojan:// · AWG .conf" spellcheck="false"></textarea>
</div>
<div>
<label class="field" for="subUrl">URL подписки</label>
@@ -456,7 +456,7 @@
<button class="action secondary" id="pingBtn" type="button">Пинг серверов</button>
<button class="action secondary" id="updCheckBtn" type="button">Проверить обновление</button>
</div>
<button class="action secondary" id="coreBtn" type="button">Установить cores (naive + hysteria2)</button>
<button class="action secondary" id="coreBtn" type="button">Установить cores (naive + hy2 + xray)</button>
</div>
<div class="ping-list" id="pingList"></div>
<p class="meta" id="meta">Загрузка…</p>
@@ -480,7 +480,7 @@
</div>
<div>
<label class="field" for="newProxy">Ссылка</label>
<input id="newProxy" type="text" placeholder="hysteria2://… / naive+https://… / awg://… или вставьте .conf" />
<input id="newProxy" type="text" placeholder="vless://… / vmess://… / trojan://… / hy2 / naive / awg" />
</div>
</div>
<div class="actions">
@@ -678,9 +678,9 @@
if (state.socks_proxy) parts.push("SOCKS " + state.socks_proxy);
detail = parts.join(" · ");
} else if (state.core_ready === false) {
detail = "Сначала установите cores (naive / hysteria2)";
detail = "Сначала установите cores (naive / hy2 / xray)";
} else {
detail = "Вставьте ссылку naive или hysteria2://";
detail = "Вставьте ссылку: naive / hy2 / vless / vmess / trojan / awg";
}
if (!busy && Date.now() > metaHoldUntil) {
setMeta(detail, state.core_ready === false ? "err" : "");
+4 -1
View File
@@ -14,6 +14,9 @@ const (
ProtocolNaive Protocol = "naive"
ProtocolHysteria2 Protocol = "hysteria2"
ProtocolAWG Protocol = "awg"
ProtocolVLESS Protocol = "vless"
ProtocolVMess Protocol = "vmess"
ProtocolTrojan Protocol = "trojan"
)
// Config is the top-level client configuration.
@@ -182,7 +185,7 @@ func (c *Config) Validate() error {
return fmt.Errorf("config: profile[%d] missing name", i)
}
switch p.Protocol {
case ProtocolNaive, ProtocolHysteria2, ProtocolAWG:
case ProtocolNaive, ProtocolHysteria2, ProtocolAWG, ProtocolVLESS, ProtocolVMess, ProtocolTrojan:
// Proxy may be empty until the user pastes a share link / conf in the UI.
case "":
c.Profiles[i].Protocol = ProtocolNaive
+6
View File
@@ -8,6 +8,12 @@ func DetectProtocol(raw string) Protocol {
switch {
case strings.HasPrefix(lower, "hysteria2://"), strings.HasPrefix(lower, "hy2://"):
return ProtocolHysteria2
case strings.HasPrefix(lower, "vless://"):
return ProtocolVLESS
case strings.HasPrefix(lower, "vmess://"):
return ProtocolVMess
case strings.HasPrefix(lower, "trojan://"):
return ProtocolTrojan
case strings.HasPrefix(lower, "awg://"), strings.HasPrefix(lower, "amneziawg://"),
strings.HasPrefix(lower, "wireguard://"):
return ProtocolAWG
+12 -2
View File
@@ -18,6 +18,7 @@ import (
"vpnclient/internal/protocols/awg"
"vpnclient/internal/protocols/hysteria2"
"vpnclient/internal/protocols/naive"
"vpnclient/internal/protocols/xray"
"vpnclient/internal/subscription"
"vpnclient/internal/sysproxy"
)
@@ -412,7 +413,7 @@ func (m *Manager) ImportSubscription(rawURL string) (int, error) {
return 0, err
}
if len(items) == 0 {
return 0, fmt.Errorf("в подписке нет поддерживаемых ссылок (naive / hysteria2 / awg)")
return 0, fmt.Errorf("в подписке нет поддерживаемых ссылок (naive / hysteria2 / awg / vless / vmess / trojan)")
}
m.mu.Lock()
@@ -466,6 +467,8 @@ func (m *Manager) newEngine(p config.Protocol) (Engine, error) {
return hysteria2.New(m.stderr), nil
case config.ProtocolAWG:
return awg.New(m.stderr), nil
case config.ProtocolVLESS, config.ProtocolVMess, config.ProtocolTrojan:
return xray.New(m.stderr), nil
default:
return nil, fmt.Errorf("unsupported protocol %q", p)
}
@@ -483,12 +486,14 @@ func (m *Manager) EnsureCore(proto config.Protocol) (string, error) {
return hysteria2.EnsureBinary(m.binDir)
case config.ProtocolAWG:
return awg.EnsureBinary(m.binDir)
case config.ProtocolVLESS, config.ProtocolVMess, config.ProtocolTrojan:
return xray.EnsureBinary(m.binDir)
default:
return naive.EnsureBinary(m.binDir)
}
}
// EnsureAllCores installs naive + hysteria2 cores (AWG is embedded).
// EnsureAllCores installs naive + hysteria2 + xray cores (AWG is embedded).
func (m *Manager) EnsureAllCores() (map[string]string, error) {
out := map[string]string{}
p1, err := naive.EnsureBinary(m.binDir)
@@ -506,5 +511,10 @@ func (m *Manager) EnsureAllCores() (map[string]string, error) {
return out, fmt.Errorf("awg: %w", err)
}
out["awg"] = p3
p4, err := xray.EnsureBinary(m.binDir)
if err != nil {
return out, fmt.Errorf("xray: %w", err)
}
out["xray"] = p4
return out, nil
}
+15
View File
@@ -8,6 +8,7 @@ import (
"vpnclient/internal/protocols/awg"
"vpnclient/internal/protocols/hysteria2"
"vpnclient/internal/protocols/naive"
"vpnclient/internal/protocols/xray"
)
// Normalize normalizes a share/proxy URI for the given protocol (or auto-detect).
@@ -26,11 +27,25 @@ func Normalize(proto config.Protocol, raw string) (normalized string, detected c
case config.ProtocolAWG:
u, rem, err := awg.NormalizeShareLink(raw)
return u, config.ProtocolAWG, rem, err
case config.ProtocolVLESS, config.ProtocolVMess, config.ProtocolTrojan:
u, rem, err := xray.NormalizeShareLink(raw)
if err != nil {
return "", proto, "", err
}
detected = xray.DetectProtocol(u)
if detected == "" {
detected = proto
}
return u, detected, rem, nil
case config.ProtocolNaive, "":
if awg.Detect(raw) {
u, rem, err := awg.NormalizeShareLink(raw)
return u, config.ProtocolAWG, rem, err
}
if xray.Detect(raw) {
u, rem, err := xray.NormalizeShareLink(raw)
return u, xray.DetectProtocol(raw), rem, err
}
u, rem, err := naive.ParseShareLink(raw)
if err != nil {
if hysteria2.Detect(raw) {
+9
View File
@@ -11,6 +11,7 @@ import (
"vpnclient/internal/linknorm"
"vpnclient/internal/protocols/awg"
"vpnclient/internal/protocols/hysteria2"
"vpnclient/internal/protocols/xray"
)
// Result is a reachability measurement to a proxy host.
@@ -39,6 +40,7 @@ func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyU
hy2 := proto == config.ProtocolHysteria2 || hysteria2.Detect(proxyURI)
isAWG := proto == config.ProtocolAWG || awg.Detect(proxyURI)
isXray := proto == config.ProtocolVLESS || proto == config.ProtocolVMess || proto == config.ProtocolTrojan || xray.Detect(proxyURI)
var host, port string
if isAWG {
h, p, err := awg.HostPort(proxyURI)
@@ -47,6 +49,13 @@ func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyU
return res
}
host, port = h, p
} else if isXray {
h, p, err := xray.HostPort(proxyURI)
if err != nil {
res.Error = err.Error()
return res
}
host, port = h, p
} else if hy2 {
h, p, err := hysteria2.HostPort(proxyURI)
if err != nil {
+210
View File
@@ -0,0 +1,210 @@
package xray
import (
"archive/zip"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"time"
)
const githubAPILatest = "https://api.github.com/repos/XTLS/Xray-core/releases/latest"
// ResolveBinary finds xray executable.
func ResolveBinary(binDir string) (string, error) {
for _, name := range candidateNames() {
candidates := []string{
filepath.Join(binDir, name),
filepath.Join(binDir, "xray", name),
}
if exe, err := os.Executable(); err == nil {
dir := filepath.Dir(exe)
candidates = append(candidates, filepath.Join(dir, name), filepath.Join(dir, "bin", name))
}
for _, c := range candidates {
if st, err := os.Stat(c); err == nil && !st.IsDir() {
return c, nil
}
}
}
return "", fmt.Errorf("xray binary not found in %s; run install-core", binDir)
}
func candidateNames() []string {
if runtime.GOOS == "windows" {
return []string{"xray.exe", "Xray.exe"}
}
return []string{"xray", "Xray"}
}
// EnsureBinary downloads official Xray-core release if missing.
func EnsureBinary(binDir string) (string, error) {
if path, err := ResolveBinary(binDir); err == nil {
return path, nil
}
if err := os.MkdirAll(binDir, 0o755); err != nil {
return "", err
}
want, err := releaseZipName()
if err != nil {
return "", err
}
fmt.Fprintf(os.Stderr, "downloading official Xray-core (%s)...\n", want)
name, url, err := findReleaseAsset(want)
if err != nil {
return "", err
}
zipPath := filepath.Join(binDir, name+".download")
_ = os.Remove(zipPath)
if err := downloadFile(zipPath, url); err != nil {
return "", err
}
defer os.Remove(zipPath)
destName := "xray"
if runtime.GOOS == "windows" {
destName = "xray.exe"
}
dest := filepath.Join(binDir, destName)
if err := extractNamedFromZip(zipPath, destName, dest); err != nil {
// Some zips nest the binary; try case-insensitive match.
if err2 := extractXrayFromZip(zipPath, dest); err2 != nil {
return "", fmt.Errorf("%v; %w", err, err2)
}
}
_ = os.Chmod(dest, 0o755)
return ResolveBinary(binDir)
}
func releaseZipName() (string, error) {
switch {
case runtime.GOOS == "windows" && runtime.GOARCH == "amd64":
return "Xray-windows-64.zip", nil
case runtime.GOOS == "windows" && runtime.GOARCH == "arm64":
return "Xray-windows-arm64-v8a.zip", nil
case runtime.GOOS == "darwin" && runtime.GOARCH == "amd64":
return "Xray-macos-64.zip", nil
case runtime.GOOS == "darwin" && runtime.GOARCH == "arm64":
return "Xray-macos-arm64-v8a.zip", nil
case runtime.GOOS == "linux" && runtime.GOARCH == "amd64":
return "Xray-linux-64.zip", nil
case runtime.GOOS == "linux" && runtime.GOARCH == "arm64":
return "Xray-linux-arm64-v8a.zip", nil
default:
return "", fmt.Errorf("unsupported platform %s/%s for xray auto-download", runtime.GOOS, runtime.GOARCH)
}
}
type ghRelease struct {
TagName string `json:"tag_name"`
Assets []struct {
Name string `json:"name"`
BrowserDownloadURL string `json:"browser_download_url"`
} `json:"assets"`
}
func findReleaseAsset(exactName string) (name, downloadURL string, err error) {
client := &http.Client{Timeout: 60 * time.Second}
req, err := http.NewRequest(http.MethodGet, githubAPILatest, nil)
if err != nil {
return "", "", err
}
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("User-Agent", "navis-vpnclient")
resp, err := client.Do(req)
if err != nil {
return "", "", fmt.Errorf("github api: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return "", "", fmt.Errorf("github api: %s: %s", resp.Status, string(body))
}
var rel ghRelease
if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil {
return "", "", err
}
for _, a := range rel.Assets {
if a.Name == exactName {
return a.Name, a.BrowserDownloadURL, nil
}
}
for _, a := range rel.Assets {
if strings.EqualFold(a.Name, exactName) {
return a.Name, a.BrowserDownloadURL, nil
}
}
return "", "", fmt.Errorf("no asset %s in release %s", exactName, rel.TagName)
}
func downloadFile(path, url string) error {
client := &http.Client{Timeout: 15 * time.Minute}
resp, err := client.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("download %s: %s", url, resp.Status)
}
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, io.LimitReader(resp.Body, 120<<20))
return err
}
func extractNamedFromZip(zipPath, wantName, dest string) error {
r, err := zip.OpenReader(zipPath)
if err != nil {
return err
}
defer r.Close()
wantBase := strings.ToLower(filepath.Base(wantName))
for _, f := range r.File {
base := strings.ToLower(filepath.Base(f.Name))
if base != wantBase {
continue
}
return writeZipFile(f, dest)
}
return fmt.Errorf("%s not found in zip", wantName)
}
func extractXrayFromZip(zipPath, dest string) error {
r, err := zip.OpenReader(zipPath)
if err != nil {
return err
}
defer r.Close()
for _, f := range r.File {
base := strings.ToLower(filepath.Base(f.Name))
if base == "xray" || base == "xray.exe" {
return writeZipFile(f, dest)
}
}
return fmt.Errorf("xray binary not found in archive")
}
func writeZipFile(f *zip.File, dest string) error {
rc, err := f.Open()
if err != nil {
return err
}
defer rc.Close()
_ = os.Remove(dest)
out, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, io.LimitReader(rc, 80<<20))
return err
}
+286
View File
@@ -0,0 +1,286 @@
package xray
import (
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"vpnclient/internal/config"
)
// writeRuntimeConfig builds an Xray config.json for SOCKS+HTTP inbounds and one proxy outbound.
func writeRuntimeConfig(path string, profile config.Profile) error {
link, err := Parse(profile.Proxy)
if err != nil {
return err
}
socksHost, socksPort := "127.0.0.1", 1080
httpHost, httpPort := "127.0.0.1", 1081
if hp, ok := profile.SOCKSListenHostPort(); ok {
h, p, err := splitHostPort(hp, 1080)
if err == nil {
socksHost, socksPort = h, p
}
}
if hp, ok := profile.HTTPListenHostPort(); ok {
h, p, err := splitHostPort(hp, 1081)
if err == nil {
httpHost, httpPort = h, p
}
}
outbound, err := buildOutbound(link)
if err != nil {
return err
}
cfg := map[string]any{
"log": map[string]any{"loglevel": "warning"},
"inbounds": []any{
map[string]any{
"tag": "socks-in",
"listen": socksHost,
"port": socksPort,
"protocol": "socks",
"settings": map[string]any{"udp": true, "auth": "noauth"},
},
map[string]any{
"tag": "http-in",
"listen": httpHost,
"port": httpPort,
"protocol": "http",
},
},
"outbounds": []any{
outbound,
map[string]any{"protocol": "freedom", "tag": "direct"},
map[string]any{"protocol": "blackhole", "tag": "block"},
},
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, append(data, '\n'), 0o600)
}
func buildOutbound(link Link) (map[string]any, error) {
stream, err := buildStreamSettings(link)
if err != nil {
return nil, err
}
switch link.Protocol {
case config.ProtocolVLESS:
user := map[string]any{
"id": link.UUID,
"encryption": firstNonEmpty(link.Security, "none"),
}
if link.Flow != "" {
user["flow"] = link.Flow
}
settings := map[string]any{
"vnext": []any{
map[string]any{
"address": link.Address,
"port": link.Port,
"users": []any{user},
},
},
}
if link.PacketEncoding != "" {
settings["packetEncoding"] = link.PacketEncoding
}
return map[string]any{
"tag": "proxy",
"protocol": "vless",
"settings": settings,
"streamSettings": stream,
}, nil
case config.ProtocolVMess:
user := map[string]any{
"id": link.UUID,
"alterId": link.AlterID,
"security": firstNonEmpty(link.Security, "auto"),
}
return map[string]any{
"tag": "proxy",
"protocol": "vmess",
"settings": map[string]any{
"vnext": []any{
map[string]any{
"address": link.Address,
"port": link.Port,
"users": []any{user},
},
},
},
"streamSettings": stream,
}, nil
case config.ProtocolTrojan:
return map[string]any{
"tag": "proxy",
"protocol": "trojan",
"settings": map[string]any{
"servers": []any{
map[string]any{
"address": link.Address,
"port": link.Port,
"password": link.UUID,
},
},
},
"streamSettings": stream,
}, nil
default:
return nil, fmt.Errorf("unsupported xray protocol %q", link.Protocol)
}
}
func buildStreamSettings(link Link) (map[string]any, error) {
network := strings.ToLower(firstNonEmpty(link.Network, "tcp"))
stream := map[string]any{"network": network}
switch network {
case "ws", "websocket":
stream["network"] = "ws"
ws := map[string]any{}
if link.Path != "" {
ws["path"] = link.Path
}
if link.Host != "" {
ws["headers"] = map[string]any{"Host": link.Host}
}
stream["wsSettings"] = ws
case "grpc", "gun":
stream["network"] = "grpc"
grpc := map[string]any{"serviceName": firstNonEmpty(link.ServiceName, link.Path)}
if link.Mode != "" {
grpc["multiMode"] = strings.EqualFold(link.Mode, "multi")
}
stream["grpcSettings"] = grpc
case "h2", "http":
stream["network"] = "h2"
h2 := map[string]any{}
if link.Path != "" {
h2["path"] = link.Path
}
if link.Host != "" {
h2["host"] = strings.Split(link.Host, ",")
}
stream["httpSettings"] = h2
case "httpupgrade":
stream["network"] = "httpupgrade"
hu := map[string]any{}
if link.Path != "" {
hu["path"] = link.Path
}
if link.Host != "" {
hu["host"] = link.Host
}
stream["httpupgradeSettings"] = hu
case "xhttp", "splithttp":
stream["network"] = "xhttp"
xh := map[string]any{}
if link.Path != "" {
xh["path"] = link.Path
}
if link.Host != "" {
xh["host"] = link.Host
}
stream["xhttpSettings"] = xh
default: // tcp
stream["network"] = "tcp"
if link.Type != "" && link.Type != "none" {
stream["tcpSettings"] = map[string]any{
"header": map[string]any{"type": link.Type},
}
}
}
sec := strings.ToLower(firstNonEmpty(link.TLS, "none"))
switch sec {
case "", "none", "0", "false":
stream["security"] = "none"
case "tls":
stream["security"] = "tls"
tls := map[string]any{
"serverName": firstNonEmpty(link.SNI, link.Host, link.Address),
"allowInsecure": link.AllowInsecure,
}
if link.FP != "" {
tls["fingerprint"] = link.FP
}
if link.ALPN != "" {
tls["alpn"] = splitCSV(link.ALPN)
}
stream["tlsSettings"] = tls
case "reality":
stream["security"] = "reality"
reality := map[string]any{
"serverName": firstNonEmpty(link.SNI, link.Host, link.Address),
"fingerprint": firstNonEmpty(link.FP, "chrome"),
"publicKey": link.PBK,
"shortId": link.SID,
"spiderX": firstNonEmpty(link.SPX, ""),
}
if link.PBK == "" {
return nil, fmt.Errorf("reality: нужен pbk (publicKey)")
}
stream["realitySettings"] = reality
default:
stream["security"] = sec
}
return stream, nil
}
func splitHostPort(hp string, defPort int) (string, int, error) {
host, portStr, err := netSplitHostPort(hp)
if err != nil {
return "", 0, err
}
if portStr == "" {
return host, defPort, nil
}
p, err := strconv.Atoi(portStr)
if err != nil {
return "", 0, err
}
return host, p, nil
}
func netSplitHostPort(hp string) (host, port string, err error) {
if strings.HasPrefix(hp, "[") {
return splitBracket(hp)
}
if i := strings.LastIndex(hp, ":"); i >= 0 {
return hp[:i], hp[i+1:], nil
}
return hp, "", nil
}
func splitBracket(hp string) (string, string, error) {
end := strings.Index(hp, "]")
if end < 0 {
return "", "", fmt.Errorf("bad host:port")
}
host := hp[1:end]
rest := hp[end+1:]
if strings.HasPrefix(rest, ":") {
return host, rest[1:], nil
}
return host, "", nil
}
func splitCSV(s string) []string {
parts := strings.Split(s, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
out = append(out, p)
}
}
return out
}
+185
View File
@@ -0,0 +1,185 @@
package xray
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"sync"
"time"
"vpnclient/internal/config"
)
// Engine runs official XTLS/Xray-core for VLESS / VMess / Trojan.
type Engine struct {
mu sync.Mutex
cmd *exec.Cmd
cancel context.CancelFunc
done chan struct{}
workdir string
profile config.Profile
stderr io.Writer
running bool
proto config.Protocol
}
func New(stderr io.Writer) *Engine {
if stderr == nil {
stderr = os.Stderr
}
return &Engine{stderr: stderr}
}
func (e *Engine) Protocol() config.Protocol {
if e.proto != "" {
return e.proto
}
return config.ProtocolVLESS
}
func (e *Engine) Start(ctx context.Context, profile config.Profile, binDir string) error {
e.mu.Lock()
defer e.mu.Unlock()
if e.running {
return fmt.Errorf("xray: already running")
}
link, err := Parse(profile.Proxy)
if err != nil {
return err
}
e.proto = link.Protocol
bin, err := ResolveBinary(binDir)
if err != nil {
return err
}
workdir, err := os.MkdirTemp("", "vpnclient-xray-*")
if err != nil {
return fmt.Errorf("xray: temp dir: %w", err)
}
cfgPath := filepath.Join(workdir, "config.json")
if err := writeRuntimeConfig(cfgPath, profile); err != nil {
os.RemoveAll(workdir)
return err
}
runCtx, cancel := context.WithCancel(context.Background())
cmd := exec.CommandContext(runCtx, bin, "run", "-c", cfgPath)
cmd.Dir = workdir
cmd.Stdout = e.stderr
cmd.Stderr = e.stderr
applySysProcAttr(cmd)
if err := cmd.Start(); err != nil {
cancel()
os.RemoveAll(workdir)
return fmt.Errorf("xray: start %s: %w", bin, err)
}
done := make(chan struct{})
e.cmd = cmd
e.cancel = cancel
e.done = done
e.workdir = workdir
e.profile = profile
e.running = true
go func() {
err := cmd.Wait()
if err != nil {
fmt.Fprintf(e.stderr, "xray: process ended: %v\n", err)
}
e.mu.Lock()
e.cleanupLocked()
e.mu.Unlock()
close(done)
}()
e.mu.Unlock()
timer := time.NewTimer(900 * time.Millisecond)
defer timer.Stop()
var startErr error
select {
case <-done:
startErr = fmt.Errorf("xray: process exited immediately; check link and install-core (xray)")
case <-ctx.Done():
_ = e.Stop()
startErr = ctx.Err()
case <-timer.C:
e.mu.Lock()
alive := e.running
e.mu.Unlock()
if !alive {
startErr = fmt.Errorf("xray: process exited during startup")
}
}
e.mu.Lock()
return startErr
}
func (e *Engine) Stop() error {
e.mu.Lock()
defer e.mu.Unlock()
return e.stopLocked()
}
func (e *Engine) stopLocked() error {
if !e.running || e.cmd == nil {
return nil
}
done := e.done
if e.cancel != nil {
e.cancel()
}
proc := e.cmd.Process
e.mu.Unlock()
select {
case <-done:
case <-time.After(2 * time.Second):
if proc != nil {
_ = proc.Kill()
}
<-done
}
e.mu.Lock()
return nil
}
func (e *Engine) cleanupLocked() {
if e.workdir != "" {
_ = os.RemoveAll(e.workdir)
e.workdir = ""
}
e.cmd = nil
e.cancel = nil
e.running = false
}
func (e *Engine) Running() bool {
e.mu.Lock()
defer e.mu.Unlock()
return e.running
}
func (e *Engine) LocalHTTPProxy() (string, bool) {
e.mu.Lock()
defer e.mu.Unlock()
if !e.running {
return "", false
}
return e.profile.HTTPListenHostPort()
}
func (e *Engine) LocalSOCKSProxy() (string, bool) {
e.mu.Lock()
defer e.mu.Unlock()
if !e.running {
return "", false
}
return e.profile.SOCKSListenHostPort()
}
+7
View File
@@ -0,0 +1,7 @@
//go:build !windows
package xray
import "os/exec"
func applySysProcAttr(cmd *exec.Cmd) {}
+13
View File
@@ -0,0 +1,13 @@
//go:build windows
package xray
import (
"os/exec"
"syscall"
)
func applySysProcAttr(cmd *exec.Cmd) {
const createNoWindow = 0x08000000
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: createNoWindow}
}
+364
View File
@@ -0,0 +1,364 @@
package xray
import (
"encoding/base64"
"encoding/json"
"fmt"
"net"
"net/url"
"strconv"
"strings"
"vpnclient/internal/config"
)
// Link is a normalized VLESS / VMess / Trojan share link.
type Link struct {
Protocol config.Protocol
Raw string
Remark string
Address string
Port int
UUID string // vless/vmess id, or trojan password
AlterID int
Security string // encryption for vmess (auto/aes-128-gcm/…) or vless encryption
Flow string
Network string // tcp/ws/grpc/h2/httpupgrade/xhttp/splithttp
Type string // header type for tcp
Host string // ws/http host header
Path string
TLS string // none/tls/reality
SNI string
ALPN string
FP string
PBK string // reality public key
SID string // reality shortId
SPX string // reality spiderX
ServiceName string // grpc
Mode string // grpc multi/gun
AllowInsecure bool
PacketEncoding string // xudp/packetaddr for vless
}
// Detect reports whether raw is a vless/vmess/trojan share link.
func Detect(raw string) bool {
lower := strings.ToLower(strings.TrimSpace(raw))
return strings.HasPrefix(lower, "vless://") ||
strings.HasPrefix(lower, "vmess://") ||
strings.HasPrefix(lower, "trojan://")
}
// DetectProtocol returns the specific protocol or "".
func DetectProtocol(raw string) config.Protocol {
lower := strings.ToLower(strings.TrimSpace(raw))
switch {
case strings.HasPrefix(lower, "vless://"):
return config.ProtocolVLESS
case strings.HasPrefix(lower, "vmess://"):
return config.ProtocolVMess
case strings.HasPrefix(lower, "trojan://"):
return config.ProtocolTrojan
default:
return ""
}
}
// NormalizeShareLink parses and returns a canonical share URI + remark.
func NormalizeShareLink(raw string) (normalized string, remark string, err error) {
link, err := Parse(raw)
if err != nil {
return "", "", err
}
return link.Raw, link.Remark, nil
}
// Parse accepts vless://, vmess://, trojan:// share links.
func Parse(raw string) (Link, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return Link{}, fmt.Errorf("пустая ссылка")
}
lower := strings.ToLower(raw)
switch {
case strings.HasPrefix(lower, "vless://"):
return parseVLESS(raw)
case strings.HasPrefix(lower, "vmess://"):
return parseVMess(raw)
case strings.HasPrefix(lower, "trojan://"):
return parseTrojan(raw)
default:
return Link{}, fmt.Errorf("ожидалась ссылка vless://, vmess:// или trojan://")
}
}
// HostPort extracts server host/port for ping.
func HostPort(raw string) (host, port string, err error) {
link, err := Parse(raw)
if err != nil {
return "", "", err
}
if link.Address == "" {
return "", "", fmt.Errorf("нет хоста")
}
p := strconv.Itoa(link.Port)
if link.Port <= 0 {
p = "443"
}
return link.Address, p, nil
}
func parseVLESS(raw string) (Link, error) {
remark := ""
body := raw
if i := strings.IndexByte(body, '#'); i >= 0 {
remark, _ = url.QueryUnescape(strings.TrimSpace(body[i+1:]))
body = body[:i]
}
u, err := url.Parse(body)
if err != nil {
return Link{}, fmt.Errorf("parse vless: %w", err)
}
host := u.Hostname()
port := 443
if p := u.Port(); p != "" {
port, _ = strconv.Atoi(p)
}
uuid := ""
if u.User != nil {
uuid = u.User.Username()
}
if host == "" || uuid == "" {
return Link{}, fmt.Errorf("vless: нужен uuid@host:port")
}
q := u.Query()
link := Link{
Protocol: config.ProtocolVLESS,
Raw: rebuildURI("vless", uuid, "", host, port, q, remark),
Remark: remark,
Address: host,
Port: port,
UUID: uuid,
Security: firstNonEmpty(q.Get("encryption"), "none"),
Flow: q.Get("flow"),
Network: firstNonEmpty(q.Get("type"), q.Get("network"), "tcp"),
Type: q.Get("headerType"),
Host: firstNonEmpty(q.Get("host"), q.Get("authority")),
Path: firstNonEmpty(q.Get("path"), q.Get("serviceName")),
TLS: firstNonEmpty(q.Get("security"), "none"),
SNI: firstNonEmpty(q.Get("sni"), q.Get("serverName")),
ALPN: q.Get("alpn"),
FP: firstNonEmpty(q.Get("fp"), q.Get("fingerprint")),
PBK: firstNonEmpty(q.Get("pbk"), q.Get("publicKey")),
SID: firstNonEmpty(q.Get("sid"), q.Get("shortId")),
SPX: firstNonEmpty(q.Get("spx"), q.Get("spiderX")),
ServiceName: firstNonEmpty(q.Get("serviceName"), q.Get("path")),
Mode: q.Get("mode"),
AllowInsecure: truthy(q.Get("allowInsecure")) || truthy(q.Get("insecure")),
PacketEncoding: q.Get("packetEncoding"),
}
if link.Network == "grpc" && link.ServiceName == "" {
link.ServiceName = link.Path
}
return link, nil
}
func parseTrojan(raw string) (Link, error) {
remark := ""
body := raw
if i := strings.IndexByte(body, '#'); i >= 0 {
remark, _ = url.QueryUnescape(strings.TrimSpace(body[i+1:]))
body = body[:i]
}
u, err := url.Parse(body)
if err != nil {
return Link{}, fmt.Errorf("parse trojan: %w", err)
}
host := u.Hostname()
port := 443
if p := u.Port(); p != "" {
port, _ = strconv.Atoi(p)
}
password := ""
if u.User != nil {
password = u.User.Username()
if p, ok := u.User.Password(); ok && p != "" {
password = password + ":" + p
}
}
if host == "" || password == "" {
return Link{}, fmt.Errorf("trojan: нужен password@host:port")
}
q := u.Query()
tls := firstNonEmpty(q.Get("security"), "tls")
if tls == "" || tls == "none" {
tls = "tls"
}
link := Link{
Protocol: config.ProtocolTrojan,
Raw: rebuildURI("trojan", password, "", host, port, q, remark),
Remark: remark,
Address: host,
Port: port,
UUID: password,
Network: firstNonEmpty(q.Get("type"), q.Get("network"), "tcp"),
Type: q.Get("headerType"),
Host: firstNonEmpty(q.Get("host"), q.Get("authority")),
Path: q.Get("path"),
TLS: tls,
SNI: firstNonEmpty(q.Get("sni"), q.Get("peer"), q.Get("serverName")),
ALPN: q.Get("alpn"),
FP: firstNonEmpty(q.Get("fp"), q.Get("fingerprint")),
PBK: firstNonEmpty(q.Get("pbk"), q.Get("publicKey")),
SID: firstNonEmpty(q.Get("sid"), q.Get("shortId")),
SPX: firstNonEmpty(q.Get("spx"), q.Get("spiderX")),
ServiceName: firstNonEmpty(q.Get("serviceName"), q.Get("path")),
Mode: q.Get("mode"),
AllowInsecure: truthy(q.Get("allowInsecure")) || truthy(q.Get("insecure")),
}
return link, nil
}
type vmessShare struct {
V any `json:"v"`
PS string `json:"ps"`
Add string `json:"add"`
Port any `json:"port"`
ID string `json:"id"`
Aid any `json:"aid"`
Scy string `json:"scy"`
Net string `json:"net"`
Type string `json:"type"`
Host string `json:"host"`
Path string `json:"path"`
TLS string `json:"tls"`
SNI string `json:"sni"`
ALPN string `json:"alpn"`
FP string `json:"fp"`
}
func parseVMess(raw string) (Link, error) {
body := strings.TrimSpace(raw)
if i := strings.Index(strings.ToLower(body), "vmess://"); i >= 0 {
body = body[i+len("vmess://"):]
}
if i := strings.IndexByte(body, '#'); i >= 0 {
body = body[:i]
}
body = strings.TrimSpace(body)
decoded, err := decodeBase64Flexible(body)
if err != nil {
return Link{}, fmt.Errorf("vmess base64: %w", err)
}
var m vmessShare
if err := json.Unmarshal([]byte(decoded), &m); err != nil {
return Link{}, fmt.Errorf("vmess json: %w", err)
}
port := anyToInt(m.Port, 443)
aid := anyToInt(m.Aid, 0)
if m.Add == "" || m.ID == "" {
return Link{}, fmt.Errorf("vmess: нет add/id")
}
tls := strings.ToLower(strings.TrimSpace(m.TLS))
if tls == "1" || tls == "true" {
tls = "tls"
}
if tls == "" {
tls = "none"
}
link := Link{
Protocol: config.ProtocolVMess,
Remark: m.PS,
Address: m.Add,
Port: port,
UUID: m.ID,
AlterID: aid,
Security: firstNonEmpty(m.Scy, "auto"),
Network: firstNonEmpty(m.Net, "tcp"),
Type: m.Type,
Host: m.Host,
Path: m.Path,
TLS: tls,
SNI: firstNonEmpty(m.SNI, m.Host),
ALPN: m.ALPN,
FP: m.FP,
}
// Rebuild canonical vmess:// for storage.
out, _ := json.Marshal(map[string]any{
"v": "2", "ps": link.Remark, "add": link.Address, "port": link.Port,
"id": link.UUID, "aid": link.AlterID, "scy": link.Security, "net": link.Network,
"type": link.Type, "host": link.Host, "path": link.Path, "tls": link.TLS,
"sni": link.SNI, "alpn": link.ALPN, "fp": link.FP,
})
link.Raw = "vmess://" + base64.StdEncoding.EncodeToString(out)
return link, nil
}
func rebuildURI(scheme, user, pass, host string, port int, q url.Values, remark string) string {
u := &url.URL{Scheme: scheme, Host: net.JoinHostPort(host, strconv.Itoa(port))}
if pass != "" {
u.User = url.UserPassword(user, pass)
} else {
u.User = url.User(user)
}
if len(q) > 0 {
u.RawQuery = q.Encode()
}
s := u.String()
if remark != "" {
s += "#" + url.PathEscape(remark)
}
return s
}
func decodeBase64Flexible(s string) (string, error) {
s = strings.TrimSpace(s)
s = strings.ReplaceAll(s, "-", "+")
s = strings.ReplaceAll(s, "_", "/")
switch len(s) % 4 {
case 2:
s += "=="
case 3:
s += "="
}
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
b, err = base64.RawStdEncoding.DecodeString(strings.TrimRight(s, "="))
if err != nil {
return "", err
}
}
return string(b), nil
}
func anyToInt(v any, def int) int {
switch t := v.(type) {
case float64:
return int(t)
case int:
return t
case string:
n, err := strconv.Atoi(strings.TrimSpace(t))
if err == nil {
return n
}
}
return def
}
func firstNonEmpty(vals ...string) string {
for _, v := range vals {
if strings.TrimSpace(v) != "" {
return strings.TrimSpace(v)
}
}
return ""
}
func truthy(s string) bool {
switch strings.ToLower(strings.TrimSpace(s)) {
case "1", "true", "yes", "y", "on":
return true
default:
return false
}
}
+40
View File
@@ -0,0 +1,40 @@
package xray
import "testing"
func TestParseVLESS(t *testing.T) {
raw := "vless://11111111-1111-1111-1111-111111111111@example.com:443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=www.cloudflare.com&fp=chrome&pbk=PUBLIC&sid=abcd&type=tcp#EU"
link, err := Parse(raw)
if err != nil {
t.Fatal(err)
}
if link.Protocol != "vless" || link.Address != "example.com" || link.Port != 443 {
t.Fatalf("%+v", link)
}
if link.TLS != "reality" || link.PBK != "PUBLIC" || link.Flow != "xtls-rprx-vision" {
t.Fatalf("%+v", link)
}
}
func TestParseTrojan(t *testing.T) {
raw := "trojan://secret@1.2.3.4:443?security=tls&sni=example.com&type=ws&path=%2Fws&host=example.com#tr"
link, err := Parse(raw)
if err != nil {
t.Fatal(err)
}
if link.UUID != "secret" || link.Network != "ws" || link.Path != "/ws" {
t.Fatalf("%+v", link)
}
}
func TestParseVMess(t *testing.T) {
// {"v":"2","ps":"n","add":"1.2.3.4","port":"443","id":"11111111-1111-1111-1111-111111111111","aid":"0","scy":"auto","net":"tcp","type":"none","host":"","path":"","tls":"tls","sni":"example.com"}
b64 := "eyJ2IjoiMiIsInBzIjoibiIsImFkZCI6IjEuMi4zLjQiLCJwb3J0IjoiNDQzIiwiaWQiOiIxMTExMTExMS0xMTExLTExMTEtMTExMS0xMTExMTExMTExMTEiLCJhaWQiOiIwIiwic2N5IjoiYXV0byIsIm5ldCI6InRjcCIsInR5cGUiOiJub25lIiwiaG9zdCI6IiIsInBhdGgiOiIiLCJ0bHMiOiJ0bHMiLCJzbmkiOiJleGFtcGxlLmNvbSJ9"
link, err := Parse("vmess://" + b64)
if err != nil {
t.Fatal(err)
}
if link.Address != "1.2.3.4" || link.Port != 443 || link.TLS != "tls" {
t.Fatalf("%+v", link)
}
}
+1 -1
View File
@@ -26,7 +26,7 @@ type Item struct {
}
var (
reShareLine = regexp.MustCompile(`(?i)((?:hysteria2|hy2|awg|amneziawg|wireguard|naive\+https|naive\+quic|naive|quic|https|http)://[^\s"'<>]+)`)
reShareLine = regexp.MustCompile(`(?i)((?:hysteria2|hy2|vless|vmess|trojan|awg|amneziawg|wireguard|naive\+https|naive\+quic|naive|quic|https|http)://[^\s"'<>]+)`)
reClashHY2 = regexp.MustCompile(`(?is)type:\s*hysteria2\b.*?server:\s*(\S+).*?port:\s*(\d+).*?(?:password|auth):\s*["']?([^\s"']+)`)
)
+1 -1
View File
@@ -17,7 +17,7 @@ import (
)
// CurrentVersion is the shipped client version.
const CurrentVersion = "1.8.1"
const CurrentVersion = "1.9.0"
// DefaultManifestURL is the update feed (hosted in the project git repo).
const DefaultManifestURL = "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json"
+6 -6
View File
@@ -1,28 +1,28 @@
{
"version": "1.8.1",
"notes": "Исправлено автообновление: замена текущего Navis.exe на месте без .new",
"version": "1.9.0",
"notes": "VLESS / VMess / Trojan через Xray-core; нажмите «Установить cores»",
"platform": "windows-amd64",
"os": "windows",
"arch": "amd64",
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "7265ace71fdd58b86c0c9acf8ee05d3f0182623201987c9d119d9d1ab328f500",
"sha256": "bd78ead7711b23a25b1a30278ec381bd762b595e91c4c00f7370c62d785740f7",
"mandatory": false,
"platforms": {
"windows-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "7265ace71fdd58b86c0c9acf8ee05d3f0182623201987c9d119d9d1ab328f500",
"sha256": "bd78ead7711b23a25b1a30278ec381bd762b595e91c4c00f7370c62d785740f7",
"os": "windows",
"arch": "amd64"
},
"darwin-arm64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
"sha256": "9a1f9d99b930cde479f9d28ef37b0f1472ab033f6e9f28da5781617639c17d54",
"sha256": "237962c24fb875f3dd2a5ecfad0db5d6837cefb6b691860a3e1d6f0e721f8449",
"os": "darwin",
"arch": "arm64"
},
"darwin-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
"sha256": "9c036f9022761a117021af37778ecd293a3fc2b7438eaf26f804bb01ef4943f1",
"sha256": "91430bb85df68b3816d9cc643b1bdc3d022410473c4f87f4d8573bb99259ba45",
"os": "darwin",
"arch": "amd64"
}
+5 -5
View File
@@ -1,7 +1,7 @@
{
"FixedFileInfo": {
"FileVersion": { "Major": 1, "Minor": 8, "Patch": 1, "Build": 0 },
"ProductVersion": { "Major": 1, "Minor": 8, "Patch": 1, "Build": 0 },
"FileVersion": { "Major": 1, "Minor": 9, "Patch": 0, "Build": 0 },
"ProductVersion": { "Major": 1, "Minor": 9, "Patch": 0, "Build": 0 },
"FileFlagsMask": "3f",
"FileFlags": "00",
"FileOS": "40004",
@@ -10,13 +10,13 @@
},
"StringFileInfo": {
"CompanyName": "EvilFox",
"FileDescription": "Navis VPN client (NaiveProxy / Hysteria 2 / AmneziaWG 2.0)",
"FileVersion": "1.8.1.0",
"FileDescription": "Navis VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
"FileVersion": "1.9.0.0",
"InternalName": "Navis",
"LegalCopyright": "Copyright (c) EvilFox",
"OriginalFilename": "Navis.exe",
"ProductName": "Navis",
"ProductVersion": "1.8.1.0",
"ProductVersion": "1.9.0.0",
"Comments": "Open-source VPN/proxy client. https://evilfox.win/"
},
"VarFileInfo": {