Release 2.7.0: macOS GUI app with the same UI as Windows.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Navis
2026-07-29 16:00:55 +03:00
co-authored by Cursor
parent 6255a0d39b
commit 2d4a797eb1
28 changed files with 703 additions and 59 deletions
+5 -2
View File
@@ -7,8 +7,8 @@
| OS | Артефакты | UI | | OS | Артефакты | UI |
|----|----------|----| |----|----------|----|
| Windows amd64 | `Navis.exe` | GUI (WebView2) | | Windows amd64 | `Navis.exe` | GUI (WebView2) |
| macOS universal | `Navis.dmg` · `Navis.app.zip` | CLI / .app — **рекомендуется** | | macOS universal | `Navis.dmg` · `Navis.app.zip` | **GUI** (как Windows) |
| macOS arm64 | `Navis` · `Navis.dmg` · `Navis.app.zip` | CLI / .app | | macOS arm64 / amd64 | `Navis` · DMG · `Navis-cli` | GUI + CLI |
| Android | `Navis.apk` | GUI (VpnService) | | Android | `Navis.apk` | GUI (VpnService) |
## Скачать macOS прямо с git ## Скачать macOS прямо с git
@@ -243,6 +243,9 @@ https://evilfox.win/
Некоторые AV (Bkav, Microsoft Wacapew/Wacatac, Ikarus Trojan.WinGo.Agent, Google, Trapmine) часто помечают **любые** неподписанные Go-бинарники с сетью и сменой системного прокси. Некоторые AV (Bkav, Microsoft Wacapew/Wacatac, Ikarus Trojan.WinGo.Agent, Google, Trapmine) часто помечают **любые** неподписанные Go-бинарники с сетью и сменой системного прокси.
В 2.7.0:
- GUI на macOS (тот же интерфейс, что на Windows): DMG → Navis.app открывает окно клиента; CLI лежит как `Navis-cli`.
В 2.6.2: В 2.6.2:
- быстрое отключение AWG: принудительно закрываются прокси-соединения, UI больше не зависает. - быстрое отключение AWG: принудительно закрываются прокси-соединения, UI больше не зависает.
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "win.evilfox.navis" applicationId = "win.evilfox.navis"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 261 versionCode = 270
versionName = "2.6.1" versionName = "2.7.0"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
ndk { ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64") abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
+12 -6
View File
@@ -9,11 +9,15 @@ mkdir "dist\navis-release\darwin-universal" 2>nul
set CGO_ENABLED=0 set CGO_ENABLED=0
set GOOS=darwin set GOOS=darwin
set GOARCH=arm64 set GOARCH=arm64
go build -ldflags="-s -w" -o "dist\navis-release\darwin-arm64\Navis" .\cmd\vpnclient go build -ldflags="-s -w" -o "dist\navis-release\darwin-arm64\Navis" .\cmd\vpnapp
if errorlevel 1 exit /b 1
go build -ldflags="-s -w" -o "dist\navis-release\darwin-arm64\Navis-cli" .\cmd\vpnclient
if errorlevel 1 exit /b 1 if errorlevel 1 exit /b 1
set GOARCH=amd64 set GOARCH=amd64
go build -ldflags="-s -w" -o "dist\navis-release\darwin-amd64\Navis" .\cmd\vpnclient go build -ldflags="-s -w" -o "dist\navis-release\darwin-amd64\Navis" .\cmd\vpnapp
if errorlevel 1 exit /b 1
go build -ldflags="-s -w" -o "dist\navis-release\darwin-amd64\Navis-cli" .\cmd\vpnclient
if errorlevel 1 exit /b 1 if errorlevel 1 exit /b 1
set GOOS= set GOOS=
@@ -24,18 +28,20 @@ go build -o "tools\mklipo\mklipo.exe" .\tools\mklipo
if errorlevel 1 exit /b 1 if errorlevel 1 exit /b 1
tools\mklipo\mklipo.exe "dist\navis-release\darwin-amd64\Navis" "dist\navis-release\darwin-arm64\Navis" "dist\navis-release\darwin-universal\Navis" tools\mklipo\mklipo.exe "dist\navis-release\darwin-amd64\Navis" "dist\navis-release\darwin-arm64\Navis" "dist\navis-release\darwin-universal\Navis"
if errorlevel 1 exit /b 1 if errorlevel 1 exit /b 1
tools\mklipo\mklipo.exe "dist\navis-release\darwin-amd64\Navis-cli" "dist\navis-release\darwin-arm64\Navis-cli" "dist\navis-release\darwin-universal\Navis-cli"
if errorlevel 1 exit /b 1
go build -o "tools\packmac\packmac.exe" .\tools\packmac go build -o "tools\packmac\packmac.exe" .\tools\packmac
if errorlevel 1 exit /b 1 if errorlevel 1 exit /b 1
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 2.6.2 -arch arm64 tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 2.7.0 -arch arm64
if errorlevel 1 exit /b 1 if errorlevel 1 exit /b 1
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 2.6.2 -arch amd64 tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 2.7.0 -arch amd64
if errorlevel 1 exit /b 1 if errorlevel 1 exit /b 1
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 2.6.2 -arch universal tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 2.7.0 -arch universal
if errorlevel 1 exit /b 1 if errorlevel 1 exit /b 1
echo Built: echo Built Mac GUI + CLI:
dir "dist\navis-release\darwin-universal" dir "dist\navis-release\darwin-universal"
dir "dist\navis-release\darwin-arm64" dir "dist\navis-release\darwin-arm64"
dir "dist\navis-release\darwin-amd64" dir "dist\navis-release\darwin-amd64"
+104
View File
@@ -0,0 +1,104 @@
//go:build darwin
package main
import (
"bytes"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"os/signal"
"path/filepath"
"syscall"
"time"
"vpnclient/internal/apphost"
"vpnclient/internal/config"
"vpnclient/internal/core"
"vpnclient/internal/update"
)
func main() {
if update.MaybeFinishUpdate(os.Args[1:]) {
return
}
update.CleanupStaleDownloads()
log.SetFlags(log.LstdFlags | log.Lshortfile)
cfgPath, err := config.LocateConfig()
if err != nil {
fatalf("Не удалось найти путь конфига: %v", err)
}
if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
if err := config.WriteExample(cfgPath); err != nil {
fatalf("Не удалось создать конфиг: %v", err)
}
}
cfg, err := config.Load(cfgPath)
if err != nil {
fatalf("Ошибка конфига %s: %v", cfgPath, err)
}
logBuf := &bytes.Buffer{}
mgr, err := core.NewManager(cfgPath, cfg, logBuf)
if err != nil {
fatalf("%v", err)
}
a := apphost.New(mgr, cfgPath, logBuf)
a.OpenURL = func(u string) error {
return exec.Command("open", u).Start()
}
a.OnAfterUpdate = func() {
go func() {
time.Sleep(300 * time.Millisecond)
os.Exit(0)
}()
}
ln, uiURL, err := apphost.ListenLocal()
if err != nil {
fatalf("listen: %v", err)
}
srv := &http.Server{Handler: a.Handler()}
go func() {
_ = srv.Serve(ln)
}()
go a.AutoCheckUpdate()
if err := openAppWindow(uiURL); err != nil {
log.Printf("open UI: %v — откройте вручную: %s", err, uiURL)
fmt.Println(uiURL)
} else {
log.Printf("Navis UI: %s", uiURL)
}
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
_ = mgr.Disconnect()
_ = srv.Close()
}
func openAppWindow(uiURL string) error {
// Prefer Chromium-based --app window (looks like a desktop client).
for _, app := range []string{"Google Chrome", "Chromium", "Microsoft Edge", "Brave Browser", "Arc"} {
cmd := exec.Command("open", "-na", app, "--args", "--app="+uiURL, "--new-window")
if err := cmd.Start(); err == nil {
return nil
}
}
// Fallback: default browser
return exec.Command("open", uiURL).Start()
}
func fatalf(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
log.Println(msg)
_ = exec.Command("osascript", "-e", fmt.Sprintf(`display alert "Navis" message %q`, msg)).Run()
os.Exit(1)
}
// Ensure we keep a reference for potential future dock icon path helpers.
var _ = filepath.Separator
+3 -3
View File
@@ -1,4 +1,4 @@
//go:build !windows //go:build !windows && !darwin
package main package main
@@ -8,7 +8,7 @@ import (
) )
func main() { func main() {
fmt.Fprintln(os.Stderr, "Navis GUI (WebView2) is Windows-only.") fmt.Fprintln(os.Stderr, "Navis GUI supports Windows (WebView2) and macOS.")
fmt.Fprintln(os.Stderr, "On macOS use the CLI binary: ./Navis connect | install-core | check-update") fmt.Fprintln(os.Stderr, "On this OS use the CLI: ./Navis connect | install-core | check-update")
os.Exit(1) os.Exit(1)
} }
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+7 -7
View File
@@ -1,22 +1,22 @@
{ {
"version": "2.6.2", "version": "2.7.0",
"notes": "Быстрое отключение AWG без зависания UI («приложение не отвечает»)", "notes": "GUI на macOS (как на Windows). Откройте Navis.app — окно клиента. CLI: Navis-cli",
"platform": "windows-amd64", "platform": "windows-amd64",
"os": "windows", "os": "windows",
"arch": "amd64", "arch": "amd64",
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe", "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072", "sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
"mandatory": false, "mandatory": false,
"platforms": { "platforms": {
"windows-amd64": { "windows-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe", "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072", "sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
"os": "windows", "os": "windows",
"arch": "amd64" "arch": "amd64"
}, },
"darwin-arm64": { "darwin-arm64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis", "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
"sha256": "497cdf66b74e28061622debbbc6fa922ae64ae532e867dec876188af15e85278", "sha256": "dd15b856bed836b82f2892e64db4db71771a947f3a392ce7d9f9e7f1b59edf1d",
"os": "darwin", "os": "darwin",
"arch": "arm64", "arch": "arm64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg", "dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
@@ -24,7 +24,7 @@
}, },
"darwin-amd64": { "darwin-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis", "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
"sha256": "95ccb649116bd15cbcdddccdd57df77651bf5f41ec19a4c6a0e91f8139621df7", "sha256": "e71d7c0080e9acdcaa2355e4dd8fa3ccec8d4ed24646e27dbc1a814b0a4019cc",
"os": "darwin", "os": "darwin",
"arch": "amd64", "arch": "amd64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg", "dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg",
@@ -32,7 +32,7 @@
}, },
"darwin-universal": { "darwin-universal": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis", "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis",
"sha256": "abdb4d5807145dfab7afa1fc9fd4d0382a4f5d9b4ce7621128b2b393c7f0c928", "sha256": "6b58907bb81583cf1730b34012cce97e7f758cf3eb12e630c971d0232cce5262",
"os": "darwin", "os": "darwin",
"arch": "universal", "arch": "universal",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis.dmg", "dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis.dmg",
+7 -7
View File
@@ -1,22 +1,22 @@
{ {
"version": "2.6.2", "version": "2.7.0",
"notes": "Быстрое отключение AWG без зависания UI («приложение не отвечает»)", "notes": "GUI на macOS (как на Windows). Откройте Navis.app — окно клиента. CLI: Navis-cli",
"platform": "windows-amd64", "platform": "windows-amd64",
"os": "windows", "os": "windows",
"arch": "amd64", "arch": "amd64",
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe", "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072", "sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
"mandatory": false, "mandatory": false,
"platforms": { "platforms": {
"windows-amd64": { "windows-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe", "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072", "sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
"os": "windows", "os": "windows",
"arch": "amd64" "arch": "amd64"
}, },
"darwin-arm64": { "darwin-arm64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis", "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
"sha256": "497cdf66b74e28061622debbbc6fa922ae64ae532e867dec876188af15e85278", "sha256": "dd15b856bed836b82f2892e64db4db71771a947f3a392ce7d9f9e7f1b59edf1d",
"os": "darwin", "os": "darwin",
"arch": "arm64", "arch": "arm64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg", "dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
@@ -24,7 +24,7 @@
}, },
"darwin-amd64": { "darwin-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis", "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
"sha256": "95ccb649116bd15cbcdddccdd57df77651bf5f41ec19a4c6a0e91f8139621df7", "sha256": "e71d7c0080e9acdcaa2355e4dd8fa3ccec8d4ed24646e27dbc1a814b0a4019cc",
"os": "darwin", "os": "darwin",
"arch": "amd64", "arch": "amd64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg", "dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg",
@@ -32,7 +32,7 @@
}, },
"darwin-universal": { "darwin-universal": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis", "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis",
"sha256": "abdb4d5807145dfab7afa1fc9fd4d0382a4f5d9b4ce7621128b2b393c7f0c928", "sha256": "6b58907bb81583cf1730b34012cce97e7f758cf3eb12e630c971d0232cce5262",
"os": "darwin", "os": "darwin",
"arch": "universal", "arch": "universal",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis.dmg", "dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis.dmg",
+488
View File
@@ -0,0 +1,488 @@
package apphost
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"strings"
"sync"
"time"
"vpnclient/internal/appui"
"vpnclient/internal/config"
"vpnclient/internal/core"
"vpnclient/internal/netcheck"
"vpnclient/internal/protocols/awg"
"vpnclient/internal/protocols/hysteria2"
"vpnclient/internal/protocols/naive"
"vpnclient/internal/protocols/xray"
"vpnclient/internal/update"
)
// App is the GUI controller shared by Windows WebView and macOS HTTP UI.
type App struct {
mu sync.Mutex
Mgr *core.Manager
CfgPath string
LogBuf *bytes.Buffer
UpdateStatus update.Status
Pings []netcheck.Result
OpenURL func(string) error
OnAfterUpdate func()
}
type UIState struct {
Connected bool `json:"connected"`
Profile string `json:"profile,omitempty"`
ActiveProfile string `json:"active_profile,omitempty"`
Protocol string `json:"protocol,omitempty"`
HTTPProxy string `json:"http_proxy,omitempty"`
SOCKSProxy string `json:"socks_proxy,omitempty"`
SystemProxy bool `json:"system_proxy"`
Proxy string `json:"proxy"`
CoreReady bool `json:"core_ready"`
CorePath string `json:"core_path,omitempty"`
ConfigPath string `json:"config_path"`
Profiles []config.ProfileInfo `json:"profiles"`
Version string `json:"version"`
Update update.Status `json:"update"`
Pings []netcheck.Result `json:"pings"`
Subscription string `json:"subscription_url"`
Hy2 core.Hy2Options `json:"hy2"`
}
type PingBestResult struct {
Pings []netcheck.Result `json:"pings"`
BestName string `json:"best_name,omitempty"`
BestMs int64 `json:"best_ms,omitempty"`
Selected bool `json:"selected"`
Connected bool `json:"connected"`
}
func New(mgr *core.Manager, cfgPath string, logBuf *bytes.Buffer) *App {
return &App{
Mgr: mgr,
CfgPath: cfgPath,
LogBuf: logBuf,
UpdateStatus: update.Status{
Current: update.CurrentVersion,
ManifestURL: update.DefaultManifestURL,
},
}
}
func (a *App) GetState() (UIState, error) {
a.mu.Lock()
defer a.mu.Unlock()
st := a.Mgr.Status()
cfg := a.Mgr.Config()
proxy := ""
active := cfg.Active
if p, err := cfg.ActiveProfile(); err == nil {
proxy = p.Proxy
active = p.Name
}
corePath := ""
coreReady := false
if p, err := cfg.ActiveProfile(); err == nil {
switch p.Protocol {
case config.ProtocolHysteria2:
if path, err := hysteria2.ResolveBinary(a.Mgr.BinDir()); err == nil {
corePath, coreReady = path, true
}
case config.ProtocolAWG:
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
}
}
} else if path, err := naive.ResolveBinary(a.Mgr.BinDir()); err == nil {
corePath, coreReady = path, true
}
out := UIState{
Connected: st.Connected,
Profile: st.Profile,
ActiveProfile: active,
Protocol: string(st.Protocol),
HTTPProxy: st.HTTPProxy,
SOCKSProxy: st.SOCKSProxy,
SystemProxy: cfg.SystemProxy,
Proxy: proxy,
CoreReady: coreReady,
CorePath: corePath,
ConfigPath: a.CfgPath,
Profiles: cfg.ListProfiles(),
Version: update.CurrentVersion,
Update: a.UpdateStatus,
Pings: append([]netcheck.Result(nil), a.Pings...),
Subscription: cfg.SubscriptionURL,
Hy2: a.Mgr.ActiveHy2Options(),
}
if out.Protocol == "" {
if p, err := cfg.ActiveProfile(); err == nil {
out.Protocol = string(p.Protocol)
}
}
if st.Connected {
out.SystemProxy = st.SystemProxy
}
return out, nil
}
func (a *App) SaveProfile(name, proxy string, systemProxy bool) error {
a.mu.Lock()
defer a.mu.Unlock()
name = strings.TrimSpace(name)
if name == "" {
return fmt.Errorf("укажите название профиля")
}
a.Mgr.SetSystemProxy(systemProxy)
return a.Mgr.SaveActiveProfile(name, proxy)
}
func (a *App) CreateProfile(name, proxy string, systemProxy bool) error {
a.mu.Lock()
defer a.mu.Unlock()
name = strings.TrimSpace(name)
if name == "" {
return fmt.Errorf("укажите название профиля")
}
a.Mgr.SetSystemProxy(systemProxy)
return a.Mgr.SaveProfile(name, proxy)
}
func (a *App) SelectProfile(name string) error {
a.mu.Lock()
defer a.mu.Unlock()
return a.Mgr.SetActiveProfile(name)
}
func (a *App) DeleteProfile(name string) error {
a.mu.Lock()
defer a.mu.Unlock()
return a.Mgr.DeleteProfile(name)
}
func (a *App) Connect() error { return a.ConnectProfile("") }
func (a *App) ConnectProfile(name string) error {
a.mu.Lock()
defer a.mu.Unlock()
name = strings.TrimSpace(name)
st := a.Mgr.Status()
if st.Connected {
if name == "" || st.Profile == name {
return nil
}
if err := a.Mgr.Disconnect(); err != nil {
return err
}
}
if name != "" {
if err := a.Mgr.SetActiveProfile(name); err != nil {
return err
}
}
if p, err := a.Mgr.Config().ActiveProfile(); err != nil {
return err
} else if strings.TrimSpace(p.Proxy) == "" {
return fmt.Errorf("сначала вставьте ссылку сервера")
}
if _, err := a.Mgr.EnsureCore(""); err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
return a.Mgr.Connect(ctx, "")
}
func (a *App) Disconnect() error {
return a.Mgr.Disconnect()
}
func (a *App) InstallCore() (string, error) {
a.mu.Lock()
defer a.mu.Unlock()
paths, err := a.Mgr.EnsureAllCores()
if err != nil {
return "", err
}
parts := make([]string, 0, len(paths))
for k, v := range paths {
parts = append(parts, k+": "+v)
}
return strings.Join(parts, " | "), nil
}
func (a *App) SaveHy2(opts core.Hy2Options) error {
a.mu.Lock()
defer a.mu.Unlock()
return a.Mgr.SaveHy2Options(opts)
}
func (a *App) ImportSubscription(rawURL string) (int, error) {
return a.Mgr.ImportSubscription(rawURL)
}
func (a *App) PingServers() ([]netcheck.Result, error) {
return a.runPings()
}
func (a *App) PingBest(autoConnect bool) (PingBestResult, error) {
pings, err := a.runPings()
out := PingBestResult{Pings: pings}
if err != nil {
return out, err
}
best, ok := netcheck.BestOK(pings)
if !ok {
return out, fmt.Errorf("нет доступных серверов")
}
out.BestName = best.Name
out.BestMs = best.Ms
a.mu.Lock()
defer a.mu.Unlock()
st := a.Mgr.Status()
alreadyBest := st.Connected && st.Profile == best.Name
if st.Connected && !alreadyBest {
if err := a.Mgr.Disconnect(); err != nil {
return out, err
}
}
if !alreadyBest {
if err := a.Mgr.SetActiveProfile(best.Name); err != nil {
return out, err
}
}
out.Selected = true
if !autoConnect {
return out, nil
}
if alreadyBest {
out.Connected = true
return out, nil
}
if p, err := a.Mgr.Config().ActiveProfile(); err != nil {
return out, err
} else if strings.TrimSpace(p.Proxy) == "" {
return out, fmt.Errorf("пустая ссылка у лучшего сервера")
}
if _, err := a.Mgr.EnsureCore(""); err != nil {
return out, err
}
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
if err := a.Mgr.Connect(ctx, ""); err != nil {
return out, err
}
out.Connected = true
return out, nil
}
func (a *App) runPings() ([]netcheck.Result, error) {
a.mu.Lock()
list := a.Mgr.Profiles()
a.mu.Unlock()
targets := make([]netcheck.Target, 0, len(list))
for _, p := range list {
targets = append(targets, netcheck.Target{
Name: p.Name,
Protocol: config.Protocol(p.Protocol),
Proxy: p.Proxy,
})
}
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
out := netcheck.PingAll(ctx, targets)
a.mu.Lock()
a.Pings = out
a.mu.Unlock()
return out, nil
}
func (a *App) CheckUpdate() (update.Status, error) {
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
st, err := update.Check(ctx, update.DefaultManifestURL)
a.mu.Lock()
if err != nil {
st.Current = update.CurrentVersion
st.ManifestURL = update.DefaultManifestURL
st.Error = err.Error()
}
a.UpdateStatus = st
a.mu.Unlock()
return st, nil
}
func (a *App) ApplyUpdate() (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
latest, err := update.Apply(ctx, update.DefaultManifestURL)
if err != nil {
return "", err
}
_ = a.Mgr.Disconnect()
if a.OnAfterUpdate != nil {
a.OnAfterUpdate()
} else {
go func() {
time.Sleep(300 * time.Millisecond)
os.Exit(0)
}()
}
return "Устанавливается v" + latest + "… Navis перезапустится.", nil
}
func (a *App) AutoCheckUpdate() {
time.Sleep(3 * time.Second)
st, err := a.CheckUpdate()
if err != nil || !st.Available {
return
}
time.Sleep(1500 * time.Millisecond)
_, _ = a.ApplyUpdate()
}
func (a *App) OpenShopURL(raw string) error {
raw = strings.TrimSpace(raw)
if raw == "" {
return fmt.Errorf("пустая ссылка")
}
lower := strings.ToLower(raw)
switch {
case lower == "https://evilfox.win/" || lower == "https://evilfox.win" ||
lower == "http://evilfox.win/" || lower == "http://evilfox.win":
raw = "https://evilfox.win/"
case strings.HasPrefix(lower, "https://evilfox.win/") || strings.HasPrefix(lower, "http://evilfox.win/"):
default:
return fmt.Errorf("разрешена только ссылка evilfox.win")
}
if a.OpenURL != nil {
return a.OpenURL(raw)
}
return fmt.Errorf("openURL не настроен")
}
// Handler serves embedded UI and JSON-RPC style /api/<method> endpoints.
func (a *App) Handler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = io.WriteString(w, appui.IndexHTML)
})
mux.HandleFunc("/api/", a.handleAPI)
return mux
}
func (a *App) handleAPI(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "POST only", http.StatusMethodNotAllowed)
return
}
name := strings.TrimPrefix(r.URL.Path, "/api/")
body, _ := io.ReadAll(io.LimitReader(r.Body, 1<<20))
var req struct {
Args []json.RawMessage `json:"args"`
}
_ = json.Unmarshal(body, &req)
result, err := a.dispatch(name, req.Args)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if err != nil {
_ = json.NewEncoder(w).Encode(map[string]any{"error": err.Error()})
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"result": result})
}
func arg[T any](args []json.RawMessage, i int, def T) T {
if i >= len(args) {
return def
}
var v T
if err := json.Unmarshal(args[i], &v); err != nil {
return def
}
return v
}
func (a *App) dispatch(name string, args []json.RawMessage) (any, error) {
switch name {
case "getState":
return a.GetState()
case "connect":
return nil, a.Connect()
case "disconnect":
return nil, a.Disconnect()
case "connectProfile":
return nil, a.ConnectProfile(arg(args, 0, ""))
case "saveProfile":
return nil, a.SaveProfile(arg(args, 0, ""), arg(args, 1, ""), arg(args, 2, true))
case "createProfile":
return nil, a.CreateProfile(arg(args, 0, ""), arg(args, 1, ""), arg(args, 2, true))
case "selectProfile":
return nil, a.SelectProfile(arg(args, 0, ""))
case "deleteProfile":
return nil, a.DeleteProfile(arg(args, 0, ""))
case "installCore":
return a.InstallCore()
case "openURL":
return nil, a.OpenShopURL(arg(args, 0, ""))
case "pingServers":
return a.PingServers()
case "pingBest":
return a.PingBest(arg(args, 0, false))
case "checkUpdate":
return a.CheckUpdate()
case "applyUpdate":
return a.ApplyUpdate()
case "saveHy2":
var opts core.Hy2Options
if len(args) > 0 {
_ = json.Unmarshal(args[0], &opts)
}
return nil, a.SaveHy2(opts)
case "importSubscription":
return a.ImportSubscription(arg(args, 0, ""))
case "quit":
go func() {
time.Sleep(200 * time.Millisecond)
_ = a.Mgr.Disconnect()
os.Exit(0)
}()
return "ok", nil
default:
return nil, fmt.Errorf("unknown method %s", name)
}
}
// ListenLocal binds 127.0.0.1:0 and returns listener + URL.
func ListenLocal() (net.Listener, string, error) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, "", err
}
url := fmt.Sprintf("http://%s/", ln.Addr().String())
return ln, url, nil
}
+38 -1
View File
@@ -725,7 +725,7 @@
<div class="brand-wrap"> <div class="brand-wrap">
<div class="brand-row"> <div class="brand-row">
<h1 class="brand">Navis</h1> <h1 class="brand">Navis</h1>
<span class="badge-ver">2.6.2</span> <span class="badge-ver">2.7</span>
</div> </div>
<p class="tagline">Быстрый клиент · Naive · Hy2 · AWG · Xray</p> <p class="tagline">Быстрый клиент · Naive · Hy2 · AWG · Xray</p>
</div> </div>
@@ -866,6 +866,7 @@
<button class="shop-link" id="shopLink" type="button">https://evilfox.win/</button> <button class="shop-link" id="shopLink" type="button">https://evilfox.win/</button>
</section> </section>
<p class="ver" id="verLabel">Navis</p> <p class="ver" id="verLabel">Navis</p>
<button class="shop-link" id="quitBtn" type="button" hidden style="display:none;margin:8px auto 0;width:auto">Выйти</button>
</main> </main>
<div class="modal-backdrop" id="modal"> <div class="modal-backdrop" id="modal">
@@ -889,6 +890,31 @@
</div> </div>
<script> <script>
// HTTP bridge for macOS GUI (Windows WebView2 already binds these as natives).
(function () {
const methods = [
"getState","connect","disconnect","connectProfile","saveProfile","createProfile",
"selectProfile","deleteProfile","installCore","openURL","pingServers","pingBest",
"checkUpdate","applyUpdate","saveHy2","importSubscription","quit"
];
if (typeof window.getState === "function") return;
window.__navisHttp = true;
async function call(name, args) {
const r = await fetch("/api/" + name, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ args: args || [] })
});
const j = await r.json();
if (j && j.error) throw j.error;
return j ? j.result : null;
}
methods.forEach((name) => {
window[name] = function () {
return call(name, Array.prototype.slice.call(arguments));
};
});
})();
const $ = (id) => document.getElementById(id); const $ = (id) => document.getElementById(id);
const meta = $("meta"); const meta = $("meta");
const btn = $("toggleBtn"); const btn = $("toggleBtn");
@@ -913,6 +939,17 @@
const updateBanner = $("updateBanner"); const updateBanner = $("updateBanner");
const updateBtn = $("updateBtn"); const updateBtn = $("updateBtn");
const verLabel = $("verLabel"); const verLabel = $("verLabel");
const quitBtn = $("quitBtn");
if (window.__navisHttp && quitBtn) {
quitBtn.hidden = false;
quitBtn.style.display = "block";
quitBtn.addEventListener("click", () => {
withBusy(async () => {
setMeta("Выход…");
try { await quit(); } catch (_) {}
});
});
}
const subUrl = $("subUrl"); const subUrl = $("subUrl");
const subBtn = $("subBtn"); const subBtn = $("subBtn");
const hero = $("hero"); const hero = $("hero");
+1 -1
View File
@@ -17,7 +17,7 @@ import (
) )
// CurrentVersion is the shipped client version. // CurrentVersion is the shipped client version.
const CurrentVersion = "2.6.2" const CurrentVersion = "2.7.0"
// DefaultManifestURL is the update feed (hosted in the project git repo). // 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" const DefaultManifestURL = "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json"
+8
View File
@@ -0,0 +1,8 @@
//go:build !windows
package update
// MaybeFinishUpdate is Windows-only (pending exe swap). No-op elsewhere.
func MaybeFinishUpdate(args []string) bool {
return false
}
+12 -13
View File
@@ -5,12 +5,8 @@ BASE="https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release"
ARCH="$(uname -m)" ARCH="$(uname -m)"
case "$ARCH" in case "$ARCH" in
arm64) arm64) SUBDIR="darwin-arm64" ;;
SUBDIR="darwin-arm64" x86_64) SUBDIR="darwin-amd64" ;;
;;
x86_64)
SUBDIR="darwin-amd64"
;;
*) *)
echo "Неподдерживаемая архитектура: $ARCH" >&2 echo "Неподдерживаемая архитектура: $ARCH" >&2
exit 1 exit 1
@@ -21,14 +17,17 @@ DEST="${1:-$HOME/Downloads/Navis.dmg}"
UNIVERSAL="$BASE/darwin-universal/Navis.dmg" UNIVERSAL="$BASE/darwin-universal/Navis.dmg"
SPECIFIC="$BASE/$SUBDIR/Navis.dmg" SPECIFIC="$BASE/$SUBDIR/Navis.dmg"
echo "Mac: $ARCH" echo "Mac: $ARCH · ставим Navis GUI 2.7"
echo "Рекомендуется универсальный DMG (Intel + Apple Silicon)…" echo "Скачиваю универсальный DMG…"
if curl -fL -o "$DEST" "$UNIVERSAL"; then if ! curl -fL -o "$DEST" "$UNIVERSAL"; then
echo "Скачан: $DEST" echo "Universal недоступен, скачиваю $SUBDIR"
else
echo "Универсальный DMG недоступен, скачиваю $SUBDIR"
curl -fL -o "$DEST" "$SPECIFIC" curl -fL -o "$DEST" "$SPECIFIC"
fi fi
open "$DEST" open "$DEST"
echo "Откройте диск и перетащите Navis.app в Программы." echo ""
echo "1) Перетащите Navis.app в Программы"
echo "2) При блоке Gatekeeper:"
echo " xattr -dr com.apple.quarantine /Applications/Navis.app"
echo "3) Запуск: откройте Navis из Программ (откроется окно UI)"
echo " CLI рядом: Navis-cli (install-core / connect / check-update)"
+7 -7
View File
@@ -1,22 +1,22 @@
{ {
"version": "2.6.2", "version": "2.7.0",
"notes": "Быстрое отключение AWG без зависания UI («приложение не отвечает»)", "notes": "GUI на macOS (как на Windows). Откройте Navis.app — окно клиента. CLI: Navis-cli",
"platform": "windows-amd64", "platform": "windows-amd64",
"os": "windows", "os": "windows",
"arch": "amd64", "arch": "amd64",
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe", "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072", "sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
"mandatory": false, "mandatory": false,
"platforms": { "platforms": {
"windows-amd64": { "windows-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe", "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072", "sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
"os": "windows", "os": "windows",
"arch": "amd64" "arch": "amd64"
}, },
"darwin-arm64": { "darwin-arm64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis", "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
"sha256": "497cdf66b74e28061622debbbc6fa922ae64ae532e867dec876188af15e85278", "sha256": "dd15b856bed836b82f2892e64db4db71771a947f3a392ce7d9f9e7f1b59edf1d",
"os": "darwin", "os": "darwin",
"arch": "arm64", "arch": "arm64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg", "dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
@@ -24,7 +24,7 @@
}, },
"darwin-amd64": { "darwin-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis", "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
"sha256": "95ccb649116bd15cbcdddccdd57df77651bf5f41ec19a4c6a0e91f8139621df7", "sha256": "e71d7c0080e9acdcaa2355e4dd8fa3ccec8d4ed24646e27dbc1a814b0a4019cc",
"os": "darwin", "os": "darwin",
"arch": "amd64", "arch": "amd64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg", "dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg",
@@ -32,7 +32,7 @@
}, },
"darwin-universal": { "darwin-universal": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis", "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis",
"sha256": "abdb4d5807145dfab7afa1fc9fd4d0382a4f5d9b4ce7621128b2b393c7f0c928", "sha256": "6b58907bb81583cf1730b34012cce97e7f758cf3eb12e630c971d0232cce5262",
"os": "darwin", "os": "darwin",
"arch": "universal", "arch": "universal",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis.dmg", "dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis.dmg",
+5 -6
View File
@@ -368,17 +368,16 @@ func sanitizeVol(s string) string {
func readmeText(version, arch string) string { func readmeText(version, arch string) string {
switch arch { switch arch {
case "universal": case "universal":
return fmt.Sprintf(`Navis %s for macOS (универсальная сборка) return fmt.Sprintf(`Navis %s for macOS (универсальная GUI-сборка)
Подходит для Apple Silicon (M1/M2/M3/M4) и Intel Mac. Подходит для Apple Silicon (M1/M2/M3/M4) и Intel Mac.
1) Перетащите Navis.app в Программы (Applications) 1) Перетащите Navis.app в Программы (Applications)
2) При первом запуске, если macOS блокирует: 2) При блокировке:
xattr -dr com.apple.quarantine /Applications/Navis.app xattr -dr com.apple.quarantine /Applications/Navis.app
3) В Терминале: 3) Запустите Navis — откроется окно как на Windows
/Applications/Navis.app/Contents/MacOS/Navis init (Chrome/Edge/Brave в режиме приложения, иначе браузер)
/Applications/Navis.app/Contents/MacOS/Navis install-core 4) CLI (ядро/connect из терминала): рядом на git файл Navis-cli
/Applications/Navis.app/Contents/MacOS/Navis connect
Магазин: https://evilfox.win/ Магазин: https://evilfox.win/
`, version) `, version)
+4 -4
View File
@@ -1,7 +1,7 @@
{ {
"FixedFileInfo": { "FixedFileInfo": {
"FileVersion": { "Major": 2, "Minor": 6, "Patch": 2, "Build": 0 }, "FileVersion": { "Major": 2, "Minor": 7, "Patch": 0, "Build": 0 },
"ProductVersion": { "Major": 2, "Minor": 6, "Patch": 2, "Build": 0 }, "ProductVersion": { "Major": 2, "Minor": 7, "Patch": 0, "Build": 0 },
"FileFlagsMask": "3f", "FileFlagsMask": "3f",
"FileFlags": "00", "FileFlags": "00",
"FileOS": "40004", "FileOS": "40004",
@@ -11,12 +11,12 @@
"StringFileInfo": { "StringFileInfo": {
"CompanyName": "EvilFox", "CompanyName": "EvilFox",
"FileDescription": "Navis 2 — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)", "FileDescription": "Navis 2 — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
"FileVersion": "2.6.2.0", "FileVersion": "2.7.0.0",
"InternalName": "Navis", "InternalName": "Navis",
"LegalCopyright": "Copyright (c) EvilFox", "LegalCopyright": "Copyright (c) EvilFox",
"OriginalFilename": "Navis.exe", "OriginalFilename": "Navis.exe",
"ProductName": "Navis", "ProductName": "Navis",
"ProductVersion": "2.6.2.0", "ProductVersion": "2.7.0.0",
"Comments": "Open-source VPN/proxy client. https://evilfox.win/" "Comments": "Open-source VPN/proxy client. https://evilfox.win/"
}, },
"VarFileInfo": { "VarFileInfo": {