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 |
|----|----------|----|
| Windows amd64 | `Navis.exe` | GUI (WebView2) |
| macOS universal | `Navis.dmg` · `Navis.app.zip` | CLI / .app — **рекомендуется** |
| macOS arm64 | `Navis` · `Navis.dmg` · `Navis.app.zip` | CLI / .app |
| macOS universal | `Navis.dmg` · `Navis.app.zip` | **GUI** (как Windows) |
| macOS arm64 / amd64 | `Navis` · DMG · `Navis-cli` | GUI + CLI |
| Android | `Navis.apk` | GUI (VpnService) |
## Скачать macOS прямо с git
@@ -243,6 +243,9 @@ https://evilfox.win/
Некоторые 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:
- быстрое отключение AWG: принудительно закрываются прокси-соединения, UI больше не зависает.
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "win.evilfox.navis"
minSdk = 26
targetSdk = 35
versionCode = 261
versionName = "2.6.1"
versionCode = 270
versionName = "2.7.0"
vectorDrawables.useSupportLibrary = true
ndk {
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 GOOS=darwin
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
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
set GOOS=
@@ -24,18 +28,20 @@ go build -o "tools\mklipo\mklipo.exe" .\tools\mklipo
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"
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
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
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
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
echo Built:
echo Built Mac GUI + CLI:
dir "dist\navis-release\darwin-universal"
dir "dist\navis-release\darwin-arm64"
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
@@ -8,7 +8,7 @@ import (
)
func main() {
fmt.Fprintln(os.Stderr, "Navis GUI (WebView2) is Windows-only.")
fmt.Fprintln(os.Stderr, "On macOS use the CLI binary: ./Navis connect | install-core | check-update")
fmt.Fprintln(os.Stderr, "Navis GUI supports Windows (WebView2) and macOS.")
fmt.Fprintln(os.Stderr, "On this OS use the CLI: ./Navis connect | install-core | check-update")
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",
"notes": "Быстрое отключение AWG без зависания UI («приложение не отвечает»)",
"version": "2.7.0",
"notes": "GUI на macOS (как на Windows). Откройте Navis.app — окно клиента. CLI: Navis-cli",
"platform": "windows-amd64",
"os": "windows",
"arch": "amd64",
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072",
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
"mandatory": false,
"platforms": {
"windows-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072",
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
"os": "windows",
"arch": "amd64"
},
"darwin-arm64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
"sha256": "497cdf66b74e28061622debbbc6fa922ae64ae532e867dec876188af15e85278",
"sha256": "dd15b856bed836b82f2892e64db4db71771a947f3a392ce7d9f9e7f1b59edf1d",
"os": "darwin",
"arch": "arm64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
@@ -24,7 +24,7 @@
},
"darwin-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
"sha256": "95ccb649116bd15cbcdddccdd57df77651bf5f41ec19a4c6a0e91f8139621df7",
"sha256": "e71d7c0080e9acdcaa2355e4dd8fa3ccec8d4ed24646e27dbc1a814b0a4019cc",
"os": "darwin",
"arch": "amd64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg",
@@ -32,7 +32,7 @@
},
"darwin-universal": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis",
"sha256": "abdb4d5807145dfab7afa1fc9fd4d0382a4f5d9b4ce7621128b2b393c7f0c928",
"sha256": "6b58907bb81583cf1730b34012cce97e7f758cf3eb12e630c971d0232cce5262",
"os": "darwin",
"arch": "universal",
"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",
"notes": "Быстрое отключение AWG без зависания UI («приложение не отвечает»)",
"version": "2.7.0",
"notes": "GUI на macOS (как на Windows). Откройте Navis.app — окно клиента. CLI: Navis-cli",
"platform": "windows-amd64",
"os": "windows",
"arch": "amd64",
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072",
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
"mandatory": false,
"platforms": {
"windows-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072",
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
"os": "windows",
"arch": "amd64"
},
"darwin-arm64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
"sha256": "497cdf66b74e28061622debbbc6fa922ae64ae532e867dec876188af15e85278",
"sha256": "dd15b856bed836b82f2892e64db4db71771a947f3a392ce7d9f9e7f1b59edf1d",
"os": "darwin",
"arch": "arm64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
@@ -24,7 +24,7 @@
},
"darwin-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
"sha256": "95ccb649116bd15cbcdddccdd57df77651bf5f41ec19a4c6a0e91f8139621df7",
"sha256": "e71d7c0080e9acdcaa2355e4dd8fa3ccec8d4ed24646e27dbc1a814b0a4019cc",
"os": "darwin",
"arch": "amd64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg",
@@ -32,7 +32,7 @@
},
"darwin-universal": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis",
"sha256": "abdb4d5807145dfab7afa1fc9fd4d0382a4f5d9b4ce7621128b2b393c7f0c928",
"sha256": "6b58907bb81583cf1730b34012cce97e7f758cf3eb12e630c971d0232cce5262",
"os": "darwin",
"arch": "universal",
"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-row">
<h1 class="brand">Navis</h1>
<span class="badge-ver">2.6.2</span>
<span class="badge-ver">2.7</span>
</div>
<p class="tagline">Быстрый клиент · Naive · Hy2 · AWG · Xray</p>
</div>
@@ -866,6 +866,7 @@
<button class="shop-link" id="shopLink" type="button">https://evilfox.win/</button>
</section>
<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>
<div class="modal-backdrop" id="modal">
@@ -889,6 +890,31 @@
</div>
<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 meta = $("meta");
const btn = $("toggleBtn");
@@ -913,6 +939,17 @@
const updateBanner = $("updateBanner");
const updateBtn = $("updateBtn");
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 subBtn = $("subBtn");
const hero = $("hero");
+1 -1
View File
@@ -17,7 +17,7 @@ import (
)
// 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).
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)"
case "$ARCH" in
arm64)
SUBDIR="darwin-arm64"
;;
x86_64)
SUBDIR="darwin-amd64"
;;
arm64) SUBDIR="darwin-arm64" ;;
x86_64) SUBDIR="darwin-amd64" ;;
*)
echo "Неподдерживаемая архитектура: $ARCH" >&2
exit 1
@@ -21,14 +17,17 @@ DEST="${1:-$HOME/Downloads/Navis.dmg}"
UNIVERSAL="$BASE/darwin-universal/Navis.dmg"
SPECIFIC="$BASE/$SUBDIR/Navis.dmg"
echo "Mac: $ARCH"
echo "Рекомендуется универсальный DMG (Intel + Apple Silicon)…"
if curl -fL -o "$DEST" "$UNIVERSAL"; then
echo "Скачан: $DEST"
else
echo "Универсальный DMG недоступен, скачиваю $SUBDIR"
echo "Mac: $ARCH · ставим Navis GUI 2.7"
echo "Скачиваю универсальный DMG…"
if ! curl -fL -o "$DEST" "$UNIVERSAL"; then
echo "Universal недоступен, скачиваю $SUBDIR"
curl -fL -o "$DEST" "$SPECIFIC"
fi
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",
"notes": "Быстрое отключение AWG без зависания UI («приложение не отвечает»)",
"version": "2.7.0",
"notes": "GUI на macOS (как на Windows). Откройте Navis.app — окно клиента. CLI: Navis-cli",
"platform": "windows-amd64",
"os": "windows",
"arch": "amd64",
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072",
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
"mandatory": false,
"platforms": {
"windows-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072",
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
"os": "windows",
"arch": "amd64"
},
"darwin-arm64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
"sha256": "497cdf66b74e28061622debbbc6fa922ae64ae532e867dec876188af15e85278",
"sha256": "dd15b856bed836b82f2892e64db4db71771a947f3a392ce7d9f9e7f1b59edf1d",
"os": "darwin",
"arch": "arm64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
@@ -24,7 +24,7 @@
},
"darwin-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
"sha256": "95ccb649116bd15cbcdddccdd57df77651bf5f41ec19a4c6a0e91f8139621df7",
"sha256": "e71d7c0080e9acdcaa2355e4dd8fa3ccec8d4ed24646e27dbc1a814b0a4019cc",
"os": "darwin",
"arch": "amd64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg",
@@ -32,7 +32,7 @@
},
"darwin-universal": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis",
"sha256": "abdb4d5807145dfab7afa1fc9fd4d0382a4f5d9b4ce7621128b2b393c7f0c928",
"sha256": "6b58907bb81583cf1730b34012cce97e7f758cf3eb12e630c971d0232cce5262",
"os": "darwin",
"arch": "universal",
"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 {
switch arch {
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.
1) Перетащите Navis.app в Программы (Applications)
2) При первом запуске, если macOS блокирует:
2) При блокировке:
xattr -dr com.apple.quarantine /Applications/Navis.app
3) В Терминале:
/Applications/Navis.app/Contents/MacOS/Navis init
/Applications/Navis.app/Contents/MacOS/Navis install-core
/Applications/Navis.app/Contents/MacOS/Navis connect
3) Запустите Navis — откроется окно как на Windows
(Chrome/Edge/Brave в режиме приложения, иначе браузер)
4) CLI (ядро/connect из терминала): рядом на git файл Navis-cli
Магазин: https://evilfox.win/
`, version)
+4 -4
View File
@@ -1,7 +1,7 @@
{
"FixedFileInfo": {
"FileVersion": { "Major": 2, "Minor": 6, "Patch": 2, "Build": 0 },
"ProductVersion": { "Major": 2, "Minor": 6, "Patch": 2, "Build": 0 },
"FileVersion": { "Major": 2, "Minor": 7, "Patch": 0, "Build": 0 },
"ProductVersion": { "Major": 2, "Minor": 7, "Patch": 0, "Build": 0 },
"FileFlagsMask": "3f",
"FileFlags": "00",
"FileOS": "40004",
@@ -11,12 +11,12 @@
"StringFileInfo": {
"CompanyName": "EvilFox",
"FileDescription": "Navis 2 — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
"FileVersion": "2.6.2.0",
"FileVersion": "2.7.0.0",
"InternalName": "Navis",
"LegalCopyright": "Copyright (c) EvilFox",
"OriginalFilename": "Navis.exe",
"ProductName": "Navis",
"ProductVersion": "2.6.2.0",
"ProductVersion": "2.7.0.0",
"Comments": "Open-source VPN/proxy client. https://evilfox.win/"
},
"VarFileInfo": {