Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b6c13c933 | ||
|
|
bec6c8392d |
@@ -243,12 +243,20 @@ 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-бинарники с сетью и сменой системного прокси.
|
||||||
|
|
||||||
|
В 3.8.1:
|
||||||
|
- меньше нагрузка в простое: лёгкий опрос интерфейса без полного клонирования конфига, кэш путей к ядрам, лог с лимитом размера;
|
||||||
|
- список серверов в UI не пересобирается без изменений; интервал опроса 4 с;
|
||||||
|
- быстрее и точнее пинг (учитывается протокол профиля; лёгкий разбор AWG Endpoint);
|
||||||
|
- AWG-прокси копирует трафик через буферный pool; PingAll и установка cores используют несколько ядер CPU;
|
||||||
|
- 3.8.1+1: первая сборка линейки 3.8.1.
|
||||||
|
|
||||||
В 3.8.0:
|
В 3.8.0:
|
||||||
- единый GUI-контроллер `apphost` для Windows и macOS (без дублирования логики);
|
- единый GUI-контроллер `apphost` для Windows и macOS (без дублирования логики);
|
||||||
- безопасный lifecycle Connect/Disconnect (ожидание Stop, mutex Windows sysproxy);
|
- безопасный lifecycle Connect/Disconnect (ожидание Stop, mutex Windows sysproxy);
|
||||||
- SHA-256 проверка при установке cores (naive / hy2 / xray);
|
- SHA-256 проверка при установке cores (naive / hy2 / xray);
|
||||||
- snapshot конфига в UI (`Config().Clone`);
|
- snapshot конфига в UI (`Config().Clone`);
|
||||||
- 3.8.0+2: обновление Navis.app через zip+SHA; Start вне mutex; getState без секретов; подпись/notarize scripts; UI CSS/JS split.
|
- 3.8.0+2: обновление Navis.app через zip+SHA; Start вне mutex; getState без секретов; подпись/notarize scripts; UI CSS/JS split;
|
||||||
|
- 3.8.0+3: пинг Hy2/AWG по UDP — soft-ok при тишине (без ложных «недоступен»).
|
||||||
|
|
||||||
В 2.7.3:
|
В 2.7.3:
|
||||||
- восстановление системного прокси после аварийного завершения;
|
- восстановление системного прокси после аварийного завершения;
|
||||||
|
|||||||
+3
-3
@@ -34,11 +34,11 @@ 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 3.8.0 -build 3.8.0.2 -arch arm64
|
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 3.8.1 -build 3.8.1.1 -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 3.8.0 -build 3.8.0.2 -arch amd64
|
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 3.8.1 -build 3.8.1.1 -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 3.8.0 -build 3.8.0.2 -arch universal
|
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 3.8.1 -build 3.8.1.1 -arch universal
|
||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
|
|
||||||
echo Built Mac GUI + CLI:
|
echo Built Mac GUI + CLI:
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"vpnclient/internal/apphost"
|
"vpnclient/internal/apphost"
|
||||||
"vpnclient/internal/config"
|
"vpnclient/internal/config"
|
||||||
"vpnclient/internal/core"
|
"vpnclient/internal/core"
|
||||||
|
"vpnclient/internal/logbuf"
|
||||||
"vpnclient/internal/update"
|
"vpnclient/internal/update"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
update.CleanupStaleDownloads()
|
update.CleanupStaleDownloads()
|
||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
|
log.Printf("Navis %s · GOMAXPROCS=%d NumCPU=%d", update.DisplayVersion(), runtime.GOMAXPROCS(0), runtime.NumCPU())
|
||||||
|
|
||||||
cfgPath, err := config.LocateConfig()
|
cfgPath, err := config.LocateConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -41,7 +43,7 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
fatalf("Ошибка конфига %s: %v", cfgPath, err)
|
fatalf("Ошибка конфига %s: %v", cfgPath, err)
|
||||||
}
|
}
|
||||||
logBuf := &bytes.Buffer{}
|
logBuf := logbuf.New(0)
|
||||||
mgr, err := core.NewManager(cfgPath, cfg, logBuf)
|
mgr, err := core.NewManager(cfgPath, cfg, logBuf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalf("%v", err)
|
fatalf("%v", err)
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"vpnclient/internal/appui"
|
"vpnclient/internal/appui"
|
||||||
"vpnclient/internal/config"
|
"vpnclient/internal/config"
|
||||||
"vpnclient/internal/core"
|
"vpnclient/internal/core"
|
||||||
|
"vpnclient/internal/logbuf"
|
||||||
"vpnclient/internal/update"
|
"vpnclient/internal/update"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ func main() {
|
|||||||
update.CleanupStaleDownloads()
|
update.CleanupStaleDownloads()
|
||||||
|
|
||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
|
log.Printf("Navis %s · GOMAXPROCS=%d NumCPU=%d", update.DisplayVersion(), runtime.GOMAXPROCS(0), runtime.NumCPU())
|
||||||
|
|
||||||
cfgPath, err := config.LocateConfig()
|
cfgPath, err := config.LocateConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -44,7 +46,7 @@ func main() {
|
|||||||
fatalDialog("Ошибка конфига %s: %v", cfgPath, err)
|
fatalDialog("Ошибка конфига %s: %v", cfgPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logBuf := &bytes.Buffer{}
|
logBuf := logbuf.New(0)
|
||||||
mgr, err := core.NewManager(cfgPath, cfg, logBuf)
|
mgr, err := core.NewManager(cfgPath, cfg, logBuf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalDialog("%v", err)
|
fatalDialog("%v", err)
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
|||||||
3.8.0+2
|
3.8.1+1
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
3.8.0.2
|
3.8.1.1
|
||||||
|
|||||||
Vendored
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "3.8.0",
|
"version": "3.8.1",
|
||||||
"notes": "Navis 3.8.0+2: Mac .app zip update; EnsureCore lock; getState без секретов; подпись/notarize pipeline; UI split.",
|
"notes": "Navis 3.8.1+1: меньше нагрузки в простое (лёгкий опрос UI, кэш ядер, список серверов не мигает); быстрее пинг; стабильнее логи и AWG-прокси; параллельная работа на нескольких ядрах CPU.",
|
||||||
"platform": "windows-amd64",
|
"platform": "windows-amd64",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64",
|
"arch": "amd64",
|
||||||
@@ -16,13 +16,13 @@
|
|||||||
},
|
},
|
||||||
"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": "6e15ad79e5ec1a1f74c48bc8f189ab8694b8f3f67f4be204420a12cf0fd622fd",
|
"sha256": "42e90ef84c1000b0850e89b30e13b2c15b0ca230a3ede28038fa03b3b493ec2e",
|
||||||
"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",
|
||||||
"zip_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.app.zip",
|
"zip_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.app.zip",
|
||||||
"zip_sha256": "65041e154356a33525b9769f851bcccfc79d0c2a1200fb9a47bc22dd95cdf2ef",
|
"zip_sha256": "e8e95be506542bc1d2cbf259bbddd9f40967d7ef9bf8ac1c9b8d44ddd83f8c05",
|
||||||
"dmg_sha256": "46617b7cbf22e92a5ef85740ed9473cd1bc9219c9de5841ce0ac46b8eb946912"
|
"dmg_sha256": "8ba9a6b18c594800445a344f6955a750f3dd208181e612f07e26330dd1a25fc2"
|
||||||
},
|
},
|
||||||
"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",
|
||||||
|
|||||||
Vendored
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "3.8.0",
|
"version": "3.8.1",
|
||||||
"notes": "Navis 3.8.0+2: Mac .app zip update; EnsureCore lock; getState без секретов; подпись/notarize pipeline; UI split.",
|
"notes": "Navis 3.8.1+1: меньше нагрузки в простое (лёгкий опрос UI, кэш ядер, список серверов не мигает); быстрее пинг; стабильнее логи и AWG-прокси; параллельная работа на нескольких ядрах CPU.",
|
||||||
"platform": "windows-amd64",
|
"platform": "windows-amd64",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64",
|
"arch": "amd64",
|
||||||
@@ -16,13 +16,13 @@
|
|||||||
},
|
},
|
||||||
"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": "6e15ad79e5ec1a1f74c48bc8f189ab8694b8f3f67f4be204420a12cf0fd622fd",
|
"sha256": "42e90ef84c1000b0850e89b30e13b2c15b0ca230a3ede28038fa03b3b493ec2e",
|
||||||
"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",
|
||||||
"zip_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.app.zip",
|
"zip_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.app.zip",
|
||||||
"zip_sha256": "65041e154356a33525b9769f851bcccfc79d0c2a1200fb9a47bc22dd95cdf2ef",
|
"zip_sha256": "e8e95be506542bc1d2cbf259bbddd9f40967d7ef9bf8ac1c9b8d44ddd83f8c05",
|
||||||
"dmg_sha256": "46617b7cbf22e92a5ef85740ed9473cd1bc9219c9de5841ce0ac46b8eb946912"
|
"dmg_sha256": "8ba9a6b18c594800445a344f6955a750f3dd208181e612f07e26330dd1a25fc2"
|
||||||
},
|
},
|
||||||
"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",
|
||||||
|
|||||||
+49
-47
@@ -1,7 +1,6 @@
|
|||||||
package apphost
|
package apphost
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -18,6 +17,8 @@ import (
|
|||||||
"vpnclient/internal/appui"
|
"vpnclient/internal/appui"
|
||||||
"vpnclient/internal/config"
|
"vpnclient/internal/config"
|
||||||
"vpnclient/internal/core"
|
"vpnclient/internal/core"
|
||||||
|
"vpnclient/internal/corebin"
|
||||||
|
"vpnclient/internal/logbuf"
|
||||||
"vpnclient/internal/netcheck"
|
"vpnclient/internal/netcheck"
|
||||||
"vpnclient/internal/protocols/awg"
|
"vpnclient/internal/protocols/awg"
|
||||||
"vpnclient/internal/protocols/hysteria2"
|
"vpnclient/internal/protocols/hysteria2"
|
||||||
@@ -31,7 +32,7 @@ type App struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
Mgr *core.Manager
|
Mgr *core.Manager
|
||||||
CfgPath string
|
CfgPath string
|
||||||
LogBuf *bytes.Buffer
|
LogBuf *logbuf.Buffer
|
||||||
UpdateStatus update.Status
|
UpdateStatus update.Status
|
||||||
Pings []netcheck.Result
|
Pings []netcheck.Result
|
||||||
OpenURL func(string) error
|
OpenURL func(string) error
|
||||||
@@ -67,7 +68,7 @@ type PingBestResult struct {
|
|||||||
Connected bool `json:"connected"`
|
Connected bool `json:"connected"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(mgr *core.Manager, cfgPath string, logBuf *bytes.Buffer) *App {
|
func New(mgr *core.Manager, cfgPath string, logBuf *logbuf.Buffer) *App {
|
||||||
return &App{
|
return &App{
|
||||||
Mgr: mgr,
|
Mgr: mgr,
|
||||||
CfgPath: cfgPath,
|
CfgPath: cfgPath,
|
||||||
@@ -89,71 +90,53 @@ func (a *App) GetEditState() (UIState, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) getState(includeSecrets bool) (UIState, error) {
|
func (a *App) getState(includeSecrets bool) (UIState, error) {
|
||||||
|
// Hold App.mu only for fields mutated by update/ping handlers.
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
defer a.mu.Unlock()
|
upd := a.UpdateStatus
|
||||||
|
pings := append([]netcheck.Result(nil), a.Pings...)
|
||||||
|
cfgPath := a.CfgPath
|
||||||
|
a.mu.Unlock()
|
||||||
|
|
||||||
st := a.Mgr.Status()
|
poll := a.Mgr.PollUI(includeSecrets)
|
||||||
cfg := a.Mgr.Config()
|
st := poll.Status
|
||||||
proxy := ""
|
proxy := poll.ActiveProxy
|
||||||
active := cfg.Active
|
|
||||||
if p, err := cfg.ActiveProfile(); err == nil {
|
|
||||||
proxy = p.Proxy
|
|
||||||
active = p.Name
|
|
||||||
}
|
|
||||||
if !includeSecrets {
|
if !includeSecrets {
|
||||||
proxy = config.RedactProxyURI(proxy)
|
proxy = config.RedactProxyURI(proxy)
|
||||||
}
|
}
|
||||||
corePath := ""
|
hy2 := poll.Hy2
|
||||||
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
|
|
||||||
}
|
|
||||||
hy2 := a.Mgr.ActiveHy2Options()
|
|
||||||
if !includeSecrets {
|
if !includeSecrets {
|
||||||
hy2.ObfsPassword = ""
|
hy2.ObfsPassword = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
corePath, coreReady := resolveCoreCached(poll.BinDir, poll.ActiveProtocol)
|
||||||
|
|
||||||
|
profiles := poll.Profiles
|
||||||
|
if includeSecrets {
|
||||||
|
// Editor needs state.Proxy; list still omits other profiles' URIs.
|
||||||
|
profiles = config.RedactProfileList(profiles)
|
||||||
|
}
|
||||||
|
|
||||||
out := UIState{
|
out := UIState{
|
||||||
Connected: st.Connected,
|
Connected: st.Connected,
|
||||||
Profile: st.Profile,
|
Profile: st.Profile,
|
||||||
ActiveProfile: active,
|
ActiveProfile: poll.Active,
|
||||||
Protocol: string(st.Protocol),
|
Protocol: string(st.Protocol),
|
||||||
HTTPProxy: st.HTTPProxy,
|
HTTPProxy: st.HTTPProxy,
|
||||||
SOCKSProxy: st.SOCKSProxy,
|
SOCKSProxy: st.SOCKSProxy,
|
||||||
SystemProxy: cfg.SystemProxy,
|
SystemProxy: poll.SystemProxy,
|
||||||
Proxy: proxy,
|
Proxy: proxy,
|
||||||
CoreReady: coreReady,
|
CoreReady: coreReady,
|
||||||
CorePath: corePath,
|
CorePath: corePath,
|
||||||
ConfigPath: a.CfgPath,
|
ConfigPath: cfgPath,
|
||||||
Profiles: config.RedactProfileList(cfg.ListProfiles()),
|
Profiles: profiles,
|
||||||
Version: update.DisplayVersion(),
|
Version: update.DisplayVersion(),
|
||||||
Update: a.UpdateStatus,
|
Update: upd,
|
||||||
Pings: append([]netcheck.Result(nil), a.Pings...),
|
Pings: pings,
|
||||||
Subscription: cfg.SubscriptionURL,
|
Subscription: poll.Subscription,
|
||||||
Hy2: hy2,
|
Hy2: hy2,
|
||||||
}
|
}
|
||||||
if out.Protocol == "" {
|
if out.Protocol == "" {
|
||||||
if p, err := cfg.ActiveProfile(); err == nil {
|
out.Protocol = string(poll.ActiveProtocol)
|
||||||
out.Protocol = string(p.Protocol)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if st.Connected {
|
if st.Connected {
|
||||||
out.SystemProxy = st.SystemProxy
|
out.SystemProxy = st.SystemProxy
|
||||||
@@ -161,6 +144,25 @@ func (a *App) getState(includeSecrets bool) (UIState, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveCoreCached(binDir string, proto config.Protocol) (path string, ready bool) {
|
||||||
|
key := corebin.CacheKey(binDir, corebin.ProtoKey(string(proto)))
|
||||||
|
var err error
|
||||||
|
switch proto {
|
||||||
|
case config.ProtocolHysteria2:
|
||||||
|
path, err = corebin.Resolve(key, func() (string, error) { return hysteria2.ResolveBinary(binDir) })
|
||||||
|
case config.ProtocolAWG:
|
||||||
|
path, err = corebin.Resolve(key, func() (string, error) { return awg.ResolveBinary(binDir) })
|
||||||
|
case config.ProtocolVLESS, config.ProtocolVMess, config.ProtocolTrojan:
|
||||||
|
path, err = corebin.Resolve(key, func() (string, error) { return xray.ResolveBinary(binDir) })
|
||||||
|
default:
|
||||||
|
path, err = corebin.Resolve(key, func() (string, error) { return naive.ResolveBinary(binDir) })
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return path, true
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) SaveProfile(name, proxy string, systemProxy bool) error {
|
func (a *App) SaveProfile(name, proxy string, systemProxy bool) error {
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
defer a.mu.Unlock()
|
defer a.mu.Unlock()
|
||||||
|
|||||||
+22
-2
@@ -202,6 +202,22 @@
|
|||||||
(pings || []).forEach((p) => { pingMap[p.name] = p; });
|
(pings || []).forEach((p) => { pingMap[p.name] = p; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let listFP = "";
|
||||||
|
|
||||||
|
function profilesFingerprint(state) {
|
||||||
|
const parts = [
|
||||||
|
state.connected ? "1" : "0",
|
||||||
|
state.active_profile || state.profile || "",
|
||||||
|
];
|
||||||
|
(state.profiles || []).forEach((p) => {
|
||||||
|
parts.push(p.name + "|" + (p.protocol || "") + "|" + (p.host || ""));
|
||||||
|
});
|
||||||
|
(state.pings || []).forEach((p) => {
|
||||||
|
parts.push(p.name + "|" + (p.ok ? 1 : 0) + "|" + (p.ms || 0) + "|" + (p.error || ""));
|
||||||
|
});
|
||||||
|
return parts.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
function fillProfiles(list, active) {
|
function fillProfiles(list, active) {
|
||||||
profiles = list || [];
|
profiles = list || [];
|
||||||
const cur = profile.value;
|
const cur = profile.value;
|
||||||
@@ -361,7 +377,11 @@
|
|||||||
btn.disabled = busy;
|
btn.disabled = busy;
|
||||||
|
|
||||||
rememberPings(state.pings || []);
|
rememberPings(state.pings || []);
|
||||||
fillProfiles(state.profiles || [], state.active_profile || state.profile);
|
const fp = profilesFingerprint(state);
|
||||||
|
if (syncForm || fp !== listFP) {
|
||||||
|
listFP = fp;
|
||||||
|
fillProfiles(state.profiles || [], state.active_profile || state.profile);
|
||||||
|
}
|
||||||
renderUpdate(state.update, state.version);
|
renderUpdate(state.update, state.version);
|
||||||
if (typeof state.subscription_url === "string" && !dirty) {
|
if (typeof state.subscription_url === "string" && !dirty) {
|
||||||
subUrl.value = state.subscription_url;
|
subUrl.value = state.subscription_url;
|
||||||
@@ -641,5 +661,5 @@
|
|||||||
setMeta(String(e), "err");
|
setMeta(String(e), "err");
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
setInterval(() => { if (!busy && !modal.classList.contains("open")) refresh().catch(() => {}); }, 2500);
|
setInterval(() => { if (!busy && !modal.classList.contains("open")) refresh().catch(() => {}); }, 4000);
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,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" id="badgeVer">3.8.0</span>
|
<span class="badge-ver" id="badgeVer">3.8.1</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="tagline">Быстрый клиент · Naive · Hy2 · AWG · Xray</p>
|
<p class="tagline">Быстрый клиент · Naive · Hy2 · AWG · Xray</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,6 +29,20 @@ func (c *Config) ListProfiles() []ProfileInfo {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListProfilesForPoll is like ListProfiles but omits Proxy URIs (cheaper idle polls).
|
||||||
|
func (c *Config) ListProfilesForPoll() []ProfileInfo {
|
||||||
|
out := make([]ProfileInfo, 0, len(c.Profiles))
|
||||||
|
for _, p := range c.Profiles {
|
||||||
|
out = append(out, ProfileInfo{
|
||||||
|
Name: p.Name,
|
||||||
|
Protocol: string(p.Protocol),
|
||||||
|
Host: proxyHost(p.Proxy),
|
||||||
|
Active: p.Name == c.Active,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// RedactProfileList clears Proxy URIs from a profile list (host/protocol remain).
|
// RedactProfileList clears Proxy URIs from a profile list (host/protocol remain).
|
||||||
// Use for periodic UI polls so credentials are not echoed for every profile.
|
// Use for periodic UI polls so credentials are not echoed for every profile.
|
||||||
func RedactProfileList(in []ProfileInfo) []ProfileInfo {
|
func RedactProfileList(in []ProfileInfo) []ProfileInfo {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"vpnclient/internal/config"
|
"vpnclient/internal/config"
|
||||||
|
"vpnclient/internal/corebin"
|
||||||
"vpnclient/internal/linknorm"
|
"vpnclient/internal/linknorm"
|
||||||
"vpnclient/internal/protocols/awg"
|
"vpnclient/internal/protocols/awg"
|
||||||
"vpnclient/internal/protocols/hysteria2"
|
"vpnclient/internal/protocols/hysteria2"
|
||||||
@@ -277,6 +278,76 @@ func (m *Manager) Config() *config.Config {
|
|||||||
return m.cfg.Clone()
|
return m.cfg.Clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UIPoll is a cheap manager snapshot for GUI polling (no JSON deep-clone of all proxies).
|
||||||
|
type UIPoll struct {
|
||||||
|
Status Status
|
||||||
|
Active string
|
||||||
|
SystemProxy bool
|
||||||
|
Subscription string
|
||||||
|
ActiveProxy string
|
||||||
|
ActiveProtocol config.Protocol
|
||||||
|
Profiles []config.ProfileInfo
|
||||||
|
Hy2 Hy2Options
|
||||||
|
BinDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PollUI gathers status + profile list under one lock without Config().Clone().
|
||||||
|
func (m *Manager) PollUI(includeSecrets bool) UIPoll {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
out := UIPoll{
|
||||||
|
Status: Status{SystemProxy: m.sys.Enabled()},
|
||||||
|
BinDir: m.binDir,
|
||||||
|
SystemProxy: false,
|
||||||
|
Subscription: "",
|
||||||
|
Hy2: Hy2Options{Congestion: "bbr", BBRProfile: "standard"},
|
||||||
|
}
|
||||||
|
if m.cfg != nil {
|
||||||
|
out.Active = m.cfg.Active
|
||||||
|
out.SystemProxy = m.cfg.SystemProxy
|
||||||
|
out.Subscription = m.cfg.SubscriptionURL
|
||||||
|
if includeSecrets {
|
||||||
|
out.Profiles = m.cfg.ListProfiles()
|
||||||
|
} else {
|
||||||
|
out.Profiles = m.cfg.ListProfilesForPoll()
|
||||||
|
}
|
||||||
|
if p, err := m.cfg.ActiveProfile(); err == nil {
|
||||||
|
out.Active = p.Name
|
||||||
|
out.ActiveProxy = p.Proxy
|
||||||
|
out.ActiveProtocol = p.Protocol
|
||||||
|
out.Hy2 = Hy2Options{
|
||||||
|
Congestion: p.Hy2Congestion,
|
||||||
|
BBRProfile: p.Hy2BBRProfile,
|
||||||
|
BandwidthUp: p.Hy2BandwidthUp,
|
||||||
|
BandwidthDown: p.Hy2BandwidthDown,
|
||||||
|
Obfs: p.Hy2Obfs,
|
||||||
|
ObfsPassword: p.Hy2ObfsPassword,
|
||||||
|
SNI: p.Hy2SNI,
|
||||||
|
Insecure: p.Hy2Insecure,
|
||||||
|
PinSHA256: p.Hy2PinSHA256,
|
||||||
|
FastOpen: p.Hy2FastOpen,
|
||||||
|
Lazy: p.Hy2Lazy,
|
||||||
|
HopInterval: p.Hy2HopInterval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m.engine != nil && m.engine.Running() {
|
||||||
|
out.Status.Connected = true
|
||||||
|
if m.profile != nil {
|
||||||
|
out.Status.Profile = m.profile.Name
|
||||||
|
out.Status.Protocol = m.profile.Protocol
|
||||||
|
}
|
||||||
|
if hp, ok := m.engine.LocalHTTPProxy(); ok {
|
||||||
|
out.Status.HTTPProxy = hp
|
||||||
|
}
|
||||||
|
if sp, ok := m.engine.LocalSOCKSProxy(); ok {
|
||||||
|
out.Status.SOCKSProxy = sp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) SetSystemProxy(enabled bool) {
|
func (m *Manager) SetSystemProxy(enabled bool) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
@@ -572,24 +643,37 @@ func (m *Manager) EnsureCore(proto config.Protocol) (string, error) {
|
|||||||
}
|
}
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
var (
|
||||||
|
path string
|
||||||
|
err error
|
||||||
|
)
|
||||||
switch proto {
|
switch proto {
|
||||||
case config.ProtocolHysteria2:
|
case config.ProtocolHysteria2:
|
||||||
return hysteria2.EnsureBinary(binDir)
|
path, err = hysteria2.EnsureBinary(binDir)
|
||||||
case config.ProtocolAWG:
|
case config.ProtocolAWG:
|
||||||
return awg.EnsureBinary(binDir)
|
path, err = awg.EnsureBinary(binDir)
|
||||||
case config.ProtocolVLESS, config.ProtocolVMess, config.ProtocolTrojan:
|
case config.ProtocolVLESS, config.ProtocolVMess, config.ProtocolTrojan:
|
||||||
return xray.EnsureBinary(binDir)
|
path, err = xray.EnsureBinary(binDir)
|
||||||
default:
|
default:
|
||||||
return naive.EnsureBinary(binDir)
|
path, err = naive.EnsureBinary(binDir)
|
||||||
|
proto = config.ProtocolNaive
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
corebin.Set(corebin.CacheKey(binDir, corebin.ProtoKey(string(proto))), path)
|
||||||
|
return path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnsureAllCores installs naive + hysteria2 + xray cores (AWG is embedded).
|
// EnsureAllCores installs naive + hysteria2 + xray cores (AWG is embedded).
|
||||||
|
// Downloads run in parallel (one goroutine per core) to use multiple CPU cores / network.
|
||||||
func (m *Manager) EnsureAllCores() (map[string]string, error) {
|
func (m *Manager) EnsureAllCores() (map[string]string, error) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
binDir := m.binDir
|
binDir := m.binDir
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
corebin.Invalidate()
|
||||||
|
|
||||||
type item struct {
|
type item struct {
|
||||||
key string
|
key string
|
||||||
path string
|
path string
|
||||||
@@ -623,6 +707,7 @@ func (m *Manager) EnsureAllCores() (map[string]string, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out[it.key] = it.path
|
out[it.key] = it.path
|
||||||
|
corebin.Set(corebin.CacheKey(binDir, it.key), it.path)
|
||||||
}
|
}
|
||||||
return out, first
|
return out, first
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package corebin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
cache = map[string]string{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// CacheKey identifies a resolved core binary for a binDir + protocol.
|
||||||
|
func CacheKey(binDir, proto string) string {
|
||||||
|
return binDir + "\x00" + proto
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a cached absolute path if present.
|
||||||
|
func Get(key string) (string, bool) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
p, ok := cache[key]
|
||||||
|
return p, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set stores a resolved path.
|
||||||
|
func Set(key, path string) {
|
||||||
|
if key == "" || path == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
cache[key] = path
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate clears all cached paths (call after EnsureCore / binDir change).
|
||||||
|
func Invalidate() {
|
||||||
|
mu.Lock()
|
||||||
|
cache = map[string]string{}
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve caches the result of fn under key. Stale paths (missing on disk) are refreshed.
|
||||||
|
func Resolve(key string, fn func() (string, error)) (string, error) {
|
||||||
|
if path, ok := Get(key); ok {
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
delete(cache, key)
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
path, err := fn()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
Set(key, path)
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProtoKey normalizes protocol to the shared binary cache key (xray covers vless/vmess/trojan).
|
||||||
|
func ProtoKey(proto string) string {
|
||||||
|
switch proto {
|
||||||
|
case "hysteria2":
|
||||||
|
return "hysteria2"
|
||||||
|
case "awg":
|
||||||
|
return "awg"
|
||||||
|
case "vless", "vmess", "trojan", "xray":
|
||||||
|
return "xray"
|
||||||
|
default:
|
||||||
|
return "naive"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package corebin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolveCacheAndStale(t *testing.T) {
|
||||||
|
Invalidate()
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "core")
|
||||||
|
if err := os.WriteFile(path, []byte("x"), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
key := CacheKey(dir, "naive")
|
||||||
|
calls := 0
|
||||||
|
fn := func() (string, error) {
|
||||||
|
calls++
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
if p, err := Resolve(key, fn); err != nil || p != path || calls != 1 {
|
||||||
|
t.Fatalf("first: path=%q calls=%d err=%v", p, calls, err)
|
||||||
|
}
|
||||||
|
if p, err := Resolve(key, fn); err != nil || p != path || calls != 1 {
|
||||||
|
t.Fatalf("cached: path=%q calls=%d err=%v", p, calls, err)
|
||||||
|
}
|
||||||
|
_ = os.Remove(path)
|
||||||
|
if _, err := Resolve(key, fn); err != nil {
|
||||||
|
// fn returns removed path; Resolve still succeeds but next Stat misses
|
||||||
|
t.Fatalf("unexpected err after delete before recreate: %v", err)
|
||||||
|
}
|
||||||
|
if calls != 2 {
|
||||||
|
t.Fatalf("expected refresh call, got %d", calls)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, []byte("y"), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if p, err := Resolve(key, fn); err != nil || p != path {
|
||||||
|
t.Fatalf("after recreate: %q %v", p, err)
|
||||||
|
}
|
||||||
|
if ProtoKey("vless") != "xray" || ProtoKey("naive") != "naive" {
|
||||||
|
t.Fatal("ProtoKey")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package logbuf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultMax = 256 << 10 // 256 KiB
|
||||||
|
|
||||||
|
// Buffer is a concurrency-safe, size-capped bytes.Buffer replacement for engine stderr.
|
||||||
|
type Buffer struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
buf bytes.Buffer
|
||||||
|
max int
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(max int) *Buffer {
|
||||||
|
if max <= 0 {
|
||||||
|
max = DefaultMax
|
||||||
|
}
|
||||||
|
return &Buffer{max: max}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) Write(p []byte) (int, error) {
|
||||||
|
if b == nil {
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
if b.buf.Len()+len(p) > b.max {
|
||||||
|
// Drop oldest half to keep recent diagnostics.
|
||||||
|
keep := b.buf.Bytes()
|
||||||
|
if len(keep) > b.max/2 {
|
||||||
|
keep = keep[len(keep)-b.max/2:]
|
||||||
|
}
|
||||||
|
b.buf.Reset()
|
||||||
|
_, _ = b.buf.Write(keep)
|
||||||
|
_, _ = b.buf.WriteString("\n… log truncated …\n")
|
||||||
|
}
|
||||||
|
return b.buf.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) String() string {
|
||||||
|
if b == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
return b.buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) Len() int {
|
||||||
|
if b == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
return b.buf.Len()
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package logbuf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBufferConcurrentWriteAndCap(t *testing.T) {
|
||||||
|
b := New(64)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < 40; j++ {
|
||||||
|
_, _ = b.Write([]byte("abcdefghij"))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
if b.Len() > 64+40 { // allow truncate marker overhead
|
||||||
|
t.Fatalf("len=%d exceeds soft cap", b.Len())
|
||||||
|
}
|
||||||
|
s := b.String()
|
||||||
|
if !strings.Contains(s, "abcdefghij") && !strings.Contains(s, "truncated") {
|
||||||
|
t.Fatalf("unexpected content %q", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
+64
-23
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -44,7 +45,14 @@ func PingAll(ctx context.Context, targets []Target) []Result {
|
|||||||
if len(targets) == 0 {
|
if len(targets) == 0 {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
workers := 12
|
// Scale with available OS threads (GOMAXPROCS); keep a useful floor/ceiling.
|
||||||
|
workers := runtime.GOMAXPROCS(0) * 2
|
||||||
|
if workers < 8 {
|
||||||
|
workers = 8
|
||||||
|
}
|
||||||
|
if workers > 32 {
|
||||||
|
workers = 32
|
||||||
|
}
|
||||||
if len(targets) < workers {
|
if len(targets) < workers {
|
||||||
workers = len(targets)
|
workers = len(targets)
|
||||||
}
|
}
|
||||||
@@ -99,7 +107,8 @@ func BestOK(results []Result) (Result, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PingProfile pings using protocol hints when available.
|
// PingProfile pings using protocol hints when available.
|
||||||
// Naive uses TCP; Hysteria 2 uses UDP (QUIC) — TCP would always look "refused".
|
// Naive uses TCP; Hysteria 2 / AWG use UDP — TCP would always look "refused".
|
||||||
|
// When proto is set, Detect is skipped (trust stored protocol).
|
||||||
func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyURI string) Result {
|
func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyURI string) Result {
|
||||||
res := Result{Name: name}
|
res := Result{Name: name}
|
||||||
if strings.TrimSpace(proxyURI) == "" {
|
if strings.TrimSpace(proxyURI) == "" {
|
||||||
@@ -107,9 +116,22 @@ func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyU
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
hy2 := proto == config.ProtocolHysteria2 || hysteria2.Detect(proxyURI)
|
var hy2, isAWG, isXray bool
|
||||||
isAWG := proto == config.ProtocolAWG || awg.Detect(proxyURI)
|
switch proto {
|
||||||
isXray := proto == config.ProtocolVLESS || proto == config.ProtocolVMess || proto == config.ProtocolTrojan || xray.Detect(proxyURI)
|
case config.ProtocolHysteria2:
|
||||||
|
hy2 = true
|
||||||
|
case config.ProtocolAWG:
|
||||||
|
isAWG = true
|
||||||
|
case config.ProtocolVLESS, config.ProtocolVMess, config.ProtocolTrojan:
|
||||||
|
isXray = true
|
||||||
|
case config.ProtocolNaive:
|
||||||
|
// TCP host parse below
|
||||||
|
default:
|
||||||
|
hy2 = hysteria2.Detect(proxyURI)
|
||||||
|
isAWG = awg.Detect(proxyURI)
|
||||||
|
isXray = xray.Detect(proxyURI)
|
||||||
|
}
|
||||||
|
|
||||||
var host, port string
|
var host, port string
|
||||||
if isAWG {
|
if isAWG {
|
||||||
h, p, err := awg.HostPort(proxyURI)
|
h, p, err := awg.HostPort(proxyURI)
|
||||||
@@ -163,67 +185,86 @@ func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyU
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
start := time.Now()
|
var (
|
||||||
var err error
|
rtt time.Duration
|
||||||
|
err error
|
||||||
|
)
|
||||||
if hy2 || isAWG {
|
if hy2 || isAWG {
|
||||||
err = probeUDP(ctx, host, port)
|
rtt, err = probeUDP(ctx, host, port)
|
||||||
} else {
|
} else {
|
||||||
err = probeTCP(ctx, host, port)
|
rtt, err = probeTCP(ctx, host, port)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
res.Error = friendlyDialError(err, hy2 || isAWG)
|
res.Error = friendlyDialError(err, hy2 || isAWG)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
res.Ms = time.Since(start).Milliseconds()
|
ms := rtt.Milliseconds()
|
||||||
|
if ms < 1 {
|
||||||
|
ms = 1
|
||||||
|
}
|
||||||
|
res.Ms = ms
|
||||||
res.OK = true
|
res.OK = true
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func probeTCP(ctx context.Context, host, port string) error {
|
func probeTCP(ctx context.Context, host, port string) (time.Duration, error) {
|
||||||
|
start := time.Now()
|
||||||
d := net.Dialer{Timeout: 3 * time.Second}
|
d := net.Dialer{Timeout: 3 * time.Second}
|
||||||
conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(host, port))
|
conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(host, port))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
return nil
|
return time.Since(start), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func probeUDP(ctx context.Context, host, port string) error {
|
// probeUDP checks Hy2/AWG endpoints.
|
||||||
|
//
|
||||||
|
// UDP VPN ports usually ignore a probe datagram (no reply). Treating read-timeout
|
||||||
|
// as failure marked live nodes as down. Semantics:
|
||||||
|
// - ICMP / connection refused → down
|
||||||
|
// - any reply → up (RTT to reply)
|
||||||
|
// - short read-timeout after a successful write → soft-up (RTT ≈ DNS+dial+write)
|
||||||
|
func probeUDP(ctx context.Context, host, port string) (time.Duration, error) {
|
||||||
|
start := time.Now()
|
||||||
d := net.Dialer{Timeout: 3 * time.Second}
|
d := net.Dialer{Timeout: 3 * time.Second}
|
||||||
conn, err := d.DialContext(ctx, "udp", net.JoinHostPort(host, port))
|
conn, err := d.DialContext(ctx, "udp", net.JoinHostPort(host, port))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
deadline := time.Now().Add(1200 * time.Millisecond)
|
// Brief window only to catch ICMP port-unreachable; do not wait for an app reply.
|
||||||
|
icmpWait := 350 * time.Millisecond
|
||||||
|
deadline := time.Now().Add(icmpWait)
|
||||||
if dl, ok := ctx.Deadline(); ok && dl.Before(deadline) {
|
if dl, ok := ctx.Deadline(); ok && dl.Before(deadline) {
|
||||||
deadline = dl
|
deadline = dl
|
||||||
}
|
}
|
||||||
_ = conn.SetDeadline(deadline)
|
_ = conn.SetDeadline(deadline)
|
||||||
|
|
||||||
if _, err := conn.Write([]byte{0}); err != nil {
|
if _, err := conn.Write([]byte{0}); err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
afterWrite := time.Now()
|
||||||
|
|
||||||
buf := make([]byte, 64)
|
buf := make([]byte, 64)
|
||||||
_, err = conn.Read(buf)
|
_, err = conn.Read(buf)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return time.Since(start), nil
|
||||||
}
|
}
|
||||||
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||||||
// UDP is connectionless: no reply does NOT mean the port is open.
|
// No ICMP refuse → endpoint is plausible; latency without the wait pad.
|
||||||
return fmt.Errorf("udp timeout")
|
return afterWrite.Sub(start), nil
|
||||||
}
|
}
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func friendlyDialError(err error, hy2 bool) string {
|
func friendlyDialError(err error, udp bool) string {
|
||||||
msg := err.Error()
|
msg := err.Error()
|
||||||
lower := strings.ToLower(msg)
|
lower := strings.ToLower(msg)
|
||||||
switch {
|
switch {
|
||||||
case strings.Contains(lower, "refused"):
|
case strings.Contains(lower, "refused"):
|
||||||
if hy2 {
|
if udp {
|
||||||
return "UDP порт недоступен (connection refused)"
|
return "UDP порт недоступен (connection refused)"
|
||||||
}
|
}
|
||||||
return "порт закрыт (connection refused)"
|
return "порт закрыт (connection refused)"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package netcheck
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
func TestFriendlyDialError(t *testing.T) {
|
func TestFriendlyDialError(t *testing.T) {
|
||||||
err := &netError{s: "dial tcp 1.2.3.4:22514: connectex: No connection could be made because the target machine actively refused it."}
|
err := &netError{s: "dial tcp 1.2.3.4:22514: connectex: No connection could be made because the target machine actively refused it."}
|
||||||
got := friendlyDialError(err, true)
|
got := friendlyDialError(err, true)
|
||||||
if !strings.Contains(got, "UDP") {
|
if got == "" || got == err.Error() {
|
||||||
t.Fatalf("got %q", got)
|
t.Fatalf("got %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,3 +31,67 @@ func TestPingProfileEmpty(t *testing.T) {
|
|||||||
t.Fatalf("%+v", r)
|
t.Fatalf("%+v", r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProbeUDPSoftOKWhenSilent(t *testing.T) {
|
||||||
|
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer pc.Close()
|
||||||
|
// Intentionally do not Read — mimics Hy2/AWG ignoring garbage probes.
|
||||||
|
_, port, err := net.SplitHostPort(pc.LocalAddr().String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
start := time.Now()
|
||||||
|
rtt, err := probeUDP(ctx, "127.0.0.1", port)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected soft-ok, got err=%v", err)
|
||||||
|
}
|
||||||
|
if rtt <= 0 {
|
||||||
|
t.Fatalf("rtt=%v", rtt)
|
||||||
|
}
|
||||||
|
if time.Since(start) > 900*time.Millisecond {
|
||||||
|
t.Fatalf("soft-ok waited too long: %v (should not use long timeout)", time.Since(start))
|
||||||
|
}
|
||||||
|
|
||||||
|
res := PingProfile(ctx, "silent", config.ProtocolHysteria2, "hysteria2://x@127.0.0.1:"+port+"/")
|
||||||
|
if !res.OK {
|
||||||
|
t.Fatalf("PingProfile soft-ok failed: %+v", res)
|
||||||
|
}
|
||||||
|
if res.Ms < 1 {
|
||||||
|
t.Fatalf("ms=%d", res.Ms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProbeUDPReplyOK(t *testing.T) {
|
||||||
|
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer pc.Close()
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, 64)
|
||||||
|
n, addr, err := pc.ReadFrom(buf)
|
||||||
|
if err != nil || n == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = pc.WriteTo([]byte{1}, addr)
|
||||||
|
}()
|
||||||
|
_, port, err := net.SplitHostPort(pc.LocalAddr().String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
rtt, err := probeUDP(ctx, "127.0.0.1", port)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if rtt <= 0 {
|
||||||
|
t.Fatalf("rtt=%v", rtt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -454,7 +454,11 @@ func (c *Conf) validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HostPort returns UDP endpoint host and port for ping.
|
// HostPort returns UDP endpoint host and port for ping.
|
||||||
|
// Prefers a light Endpoint=/URI extract; falls back to full Parse for JSON/vpn:// blobs.
|
||||||
func HostPort(raw string) (host, port string, err error) {
|
func HostPort(raw string) (host, port string, err error) {
|
||||||
|
if h, p, ok := lightEndpoint(raw); ok {
|
||||||
|
return h, p, nil
|
||||||
|
}
|
||||||
c, err := Parse(raw)
|
c, err := Parse(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
@@ -466,6 +470,67 @@ func HostPort(raw string) (host, port string, err error) {
|
|||||||
return h, p, nil
|
return h, p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func lightEndpoint(raw string) (host, port string, ok bool) {
|
||||||
|
raw = sanitizeShare(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
lower := strings.ToLower(raw)
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(lower, "awg://"), strings.HasPrefix(lower, "amneziawg://"),
|
||||||
|
strings.HasPrefix(lower, "wireguard://"):
|
||||||
|
// URI host:port only — no full config decode.
|
||||||
|
rest := raw
|
||||||
|
if i := strings.Index(rest, "://"); i >= 0 {
|
||||||
|
rest = rest[i+3:]
|
||||||
|
}
|
||||||
|
if at := strings.Index(rest, "@"); at >= 0 {
|
||||||
|
rest = rest[at+1:]
|
||||||
|
}
|
||||||
|
if i := strings.IndexAny(rest, "/?#"); i >= 0 {
|
||||||
|
rest = rest[:i]
|
||||||
|
}
|
||||||
|
rest = strings.TrimSpace(rest)
|
||||||
|
if rest == "" {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
h, p, err := net.SplitHostPort(rest)
|
||||||
|
if err != nil {
|
||||||
|
return rest, "51820", true
|
||||||
|
}
|
||||||
|
return h, p, true
|
||||||
|
}
|
||||||
|
if strings.Contains(lower, "endpoint") {
|
||||||
|
for _, line := range strings.Split(raw, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
low := strings.ToLower(line)
|
||||||
|
if !strings.HasPrefix(low, "endpoint") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, val, cutOK := strings.Cut(line, "=")
|
||||||
|
if !cutOK {
|
||||||
|
_, val, cutOK = strings.Cut(line, ":")
|
||||||
|
}
|
||||||
|
if !cutOK {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val = strings.TrimSpace(val)
|
||||||
|
if val == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
h, p, err := net.SplitHostPort(val)
|
||||||
|
if err != nil {
|
||||||
|
return val, "51820", true
|
||||||
|
}
|
||||||
|
return h, p, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
// lookupIP is overridable in tests.
|
// lookupIP is overridable in tests.
|
||||||
var lookupIP = defaultLookupIP
|
var lookupIP = defaultLookupIP
|
||||||
|
|
||||||
|
|||||||
@@ -236,3 +236,29 @@ func indexOf(s, sub string) int {
|
|||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHostPortLightINI(t *testing.T) {
|
||||||
|
raw := `[Interface]
|
||||||
|
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
Address = 10.8.0.2/32
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
|
||||||
|
Endpoint = 203.0.113.10:51820
|
||||||
|
AllowedIPs = 0.0.0.0/0
|
||||||
|
`
|
||||||
|
h, p, err := HostPort(raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if h != "203.0.113.10" || p != "51820" {
|
||||||
|
t.Fatalf("got %s:%s", h, p)
|
||||||
|
}
|
||||||
|
h2, p2, err := HostPort("awg://AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=@9.9.9.9:41421")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if h2 != "9.9.9.9" || p2 != "41421" {
|
||||||
|
t.Fatalf("uri got %s:%s", h2, p2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -268,17 +268,24 @@ func handleHTTPProxy(client net.Conn, dial DialFunc) error {
|
|||||||
|
|
||||||
func relay(a, b net.Conn) error {
|
func relay(a, b net.Conn) error {
|
||||||
errc := make(chan error, 2)
|
errc := make(chan error, 2)
|
||||||
go func() {
|
copyOne := func(dst, src net.Conn) {
|
||||||
_, err := io.Copy(a, b)
|
bufp := copyBufPool.Get().(*[]byte)
|
||||||
|
defer copyBufPool.Put(bufp)
|
||||||
|
_, err := io.CopyBuffer(dst, src, *bufp)
|
||||||
errc <- err
|
errc <- err
|
||||||
}()
|
}
|
||||||
go func() {
|
go copyOne(a, b)
|
||||||
_, err := io.Copy(b, a)
|
go copyOne(b, a)
|
||||||
errc <- err
|
|
||||||
}()
|
|
||||||
err := <-errc
|
err := <-errc
|
||||||
_ = a.Close()
|
_ = a.Close()
|
||||||
_ = b.Close()
|
_ = b.Close()
|
||||||
<-errc
|
<-errc
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var copyBufPool = sync.Pool{
|
||||||
|
New: func() any {
|
||||||
|
b := make([]byte, 32*1024)
|
||||||
|
return &b
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ import (
|
|||||||
|
|
||||||
// CurrentVersion is the product/semver used for update eligibility (feed "version").
|
// CurrentVersion is the product/semver used for update eligibility (feed "version").
|
||||||
// Keep major.minor.patch only — no build suffix here.
|
// Keep major.minor.patch only — no build suffix here.
|
||||||
const CurrentVersion = "3.8.0"
|
const CurrentVersion = "3.8.1"
|
||||||
|
|
||||||
// BuildNumber is the monotonic build within CurrentVersion (Windows FileVersion 4th part,
|
// BuildNumber is the monotonic build within CurrentVersion (Windows FileVersion 4th part,
|
||||||
// macOS CFBundleVersion suffix, Android versionCode low digits). Bump on every release build.
|
// macOS CFBundleVersion suffix, Android versionCode low digits). Bump on every release build.
|
||||||
const BuildNumber = 2
|
const BuildNumber = 1
|
||||||
|
|
||||||
// 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"
|
||||||
|
|||||||
@@ -65,8 +65,9 @@ h = hashlib.sha256(binp.read_bytes()).hexdigest()
|
|||||||
zh = hashlib.sha256(zipp.read_bytes()).hexdigest() if zipp.exists() else ""
|
zh = hashlib.sha256(zipp.read_bytes()).hexdigest() if zipp.exists() else ""
|
||||||
dh = hashlib.sha256(dmgp.read_bytes()).hexdigest() if dmgp.exists() else ""
|
dh = hashlib.sha256(dmgp.read_bytes()).hexdigest() if dmgp.exists() else ""
|
||||||
notes = (
|
notes = (
|
||||||
f"Navis {ver}+{build}: Mac .app zip update; EnsureCore lock; "
|
f"Navis {ver}+{build}: меньше нагрузки в простое (лёгкий опрос UI, кэш ядер, "
|
||||||
f"getState без секретов; подпись/notarize pipeline; UI split."
|
f"список серверов не мигает); быстрее пинг; стабильнее логи и AWG-прокси; "
|
||||||
|
f"параллельная работа на нескольких ядрах CPU."
|
||||||
)
|
)
|
||||||
paths = [
|
paths = [
|
||||||
Path('dist/update.json'),
|
Path('dist/update.json'),
|
||||||
|
|||||||
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "3.8.0",
|
"version": "3.8.1",
|
||||||
"notes": "Navis 3.8.0+2: Mac .app zip update; EnsureCore lock; getState без секретов; подпись/notarize pipeline; UI split.",
|
"notes": "Navis 3.8.1+1: меньше нагрузки в простое (лёгкий опрос UI, кэш ядер, список серверов не мигает); быстрее пинг; стабильнее логи и AWG-прокси; параллельная работа на нескольких ядрах CPU.",
|
||||||
"platform": "windows-amd64",
|
"platform": "windows-amd64",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64",
|
"arch": "amd64",
|
||||||
@@ -16,13 +16,13 @@
|
|||||||
},
|
},
|
||||||
"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": "6e15ad79e5ec1a1f74c48bc8f189ab8694b8f3f67f4be204420a12cf0fd622fd",
|
"sha256": "42e90ef84c1000b0850e89b30e13b2c15b0ca230a3ede28038fa03b3b493ec2e",
|
||||||
"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",
|
||||||
"zip_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.app.zip",
|
"zip_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.app.zip",
|
||||||
"zip_sha256": "65041e154356a33525b9769f851bcccfc79d0c2a1200fb9a47bc22dd95cdf2ef",
|
"zip_sha256": "e8e95be506542bc1d2cbf259bbddd9f40967d7ef9bf8ac1c9b8d44ddd83f8c05",
|
||||||
"dmg_sha256": "46617b7cbf22e92a5ef85740ed9473cd1bc9219c9de5841ce0ac46b8eb946912"
|
"dmg_sha256": "8ba9a6b18c594800445a344f6955a750f3dd208181e612f07e26330dd1a25fc2"
|
||||||
},
|
},
|
||||||
"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",
|
||||||
|
|||||||
+6
-6
@@ -3,14 +3,14 @@
|
|||||||
"FileVersion": {
|
"FileVersion": {
|
||||||
"Major": 3,
|
"Major": 3,
|
||||||
"Minor": 8,
|
"Minor": 8,
|
||||||
"Patch": 0,
|
"Patch": 1,
|
||||||
"Build": 2
|
"Build": 1
|
||||||
},
|
},
|
||||||
"ProductVersion": {
|
"ProductVersion": {
|
||||||
"Major": 3,
|
"Major": 3,
|
||||||
"Minor": 8,
|
"Minor": 8,
|
||||||
"Patch": 0,
|
"Patch": 1,
|
||||||
"Build": 2
|
"Build": 1
|
||||||
},
|
},
|
||||||
"FileFlagsMask": "3f",
|
"FileFlagsMask": "3f",
|
||||||
"FileFlags": "00",
|
"FileFlags": "00",
|
||||||
@@ -21,12 +21,12 @@
|
|||||||
"StringFileInfo": {
|
"StringFileInfo": {
|
||||||
"CompanyName": "EvilFox",
|
"CompanyName": "EvilFox",
|
||||||
"FileDescription": "Navis — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
|
"FileDescription": "Navis — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
|
||||||
"FileVersion": "3.8.0.2",
|
"FileVersion": "3.8.1.1",
|
||||||
"InternalName": "Navis",
|
"InternalName": "Navis",
|
||||||
"LegalCopyright": "Copyright (c) EvilFox",
|
"LegalCopyright": "Copyright (c) EvilFox",
|
||||||
"OriginalFilename": "Navis.exe",
|
"OriginalFilename": "Navis.exe",
|
||||||
"ProductName": "Navis",
|
"ProductName": "Navis",
|
||||||
"ProductVersion": "3.8.0.2",
|
"ProductVersion": "3.8.1.1",
|
||||||
"Comments": "Open-source VPN/proxy client. https://evilfox.win/"
|
"Comments": "Open-source VPN/proxy client. https://evilfox.win/"
|
||||||
},
|
},
|
||||||
"VarFileInfo": {
|
"VarFileInfo": {
|
||||||
|
|||||||
Reference in New Issue
Block a user