diff --git a/build-macos.bat b/build-macos.bat
index 6f65961..b5ca5e0 100644
--- a/build-macos.bat
+++ b/build-macos.bat
@@ -34,11 +34,11 @@ 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.7.0 -arch arm64
+tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 2.7.1 -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.7.0 -arch amd64
+tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 2.7.1 -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.7.0 -arch universal
+tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 2.7.1 -arch universal
if errorlevel 1 exit /b 1
echo Built Mac GUI + CLI:
diff --git a/dist/navis-release/darwin-arm64/Navis b/dist/navis-release/darwin-arm64/Navis
old mode 100644
new mode 100755
index 555d959..57cd02a
Binary files a/dist/navis-release/darwin-arm64/Navis and b/dist/navis-release/darwin-arm64/Navis differ
diff --git a/dist/navis-release/darwin-arm64/Navis-cli b/dist/navis-release/darwin-arm64/Navis-cli
old mode 100644
new mode 100755
index e5a6a05..67ee051
Binary files a/dist/navis-release/darwin-arm64/Navis-cli and b/dist/navis-release/darwin-arm64/Navis-cli differ
diff --git a/dist/navis-release/darwin-arm64/Navis.app.zip b/dist/navis-release/darwin-arm64/Navis.app.zip
index 2ab4972..5ad25a4 100644
Binary files a/dist/navis-release/darwin-arm64/Navis.app.zip and b/dist/navis-release/darwin-arm64/Navis.app.zip differ
diff --git a/dist/navis-release/darwin-arm64/Navis.dmg b/dist/navis-release/darwin-arm64/Navis.dmg
index cda76ee..8e0bce2 100644
Binary files a/dist/navis-release/darwin-arm64/Navis.dmg and b/dist/navis-release/darwin-arm64/Navis.dmg differ
diff --git a/dist/navis-release/update.json b/dist/navis-release/update.json
index fe04ef1..38c187e 100644
--- a/dist/navis-release/update.json
+++ b/dist/navis-release/update.json
@@ -1,6 +1,6 @@
{
- "version": "2.7.0",
- "notes": "GUI на macOS (как на Windows). Откройте Navis.app — окно клиента. CLI: Navis-cli",
+ "version": "2.7.1",
+ "notes": "macOS: UDZO DMG, Application Support cores path, naiveproxy asset name fix",
"platform": "windows-amd64",
"os": "windows",
"arch": "amd64",
@@ -16,7 +16,7 @@
},
"darwin-arm64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
- "sha256": "dd15b856bed836b82f2892e64db4db71771a947f3a392ce7d9f9e7f1b59edf1d",
+ "sha256": "8951c351b224d8a13e2772b6ee080d7bee3285ac162dd20c0855a8653a569ec5",
"os": "darwin",
"arch": "arm64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
diff --git a/dist/update.json b/dist/update.json
index fe04ef1..38c187e 100644
--- a/dist/update.json
+++ b/dist/update.json
@@ -1,6 +1,6 @@
{
- "version": "2.7.0",
- "notes": "GUI на macOS (как на Windows). Откройте Navis.app — окно клиента. CLI: Navis-cli",
+ "version": "2.7.1",
+ "notes": "macOS: UDZO DMG, Application Support cores path, naiveproxy asset name fix",
"platform": "windows-amd64",
"os": "windows",
"arch": "amd64",
@@ -16,7 +16,7 @@
},
"darwin-arm64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
- "sha256": "dd15b856bed836b82f2892e64db4db71771a947f3a392ce7d9f9e7f1b59edf1d",
+ "sha256": "8951c351b224d8a13e2772b6ee080d7bee3285ac162dd20c0855a8653a569ec5",
"os": "darwin",
"arch": "arm64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
diff --git a/internal/config/config.go b/internal/config/config.go
index a9e88c8..507fe84 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -207,11 +207,15 @@ func (c *Config) ActiveProfile() (*Profile, error) {
}
// ResolveBinDir returns an absolute bin directory.
-// Relative paths are resolved next to the executable (portable install layout).
+// Relative paths are resolved next to the executable (portable install layout),
+// or under ~/Library/Application Support/Navis on macOS (.app bundles are read-only).
func ResolveBinDir(cfgPath, binDir string) (string, error) {
if filepath.IsAbs(binDir) {
return binDir, nil
}
+ if dir, ok := platformNavisDir(); ok {
+ return filepath.Abs(filepath.Join(dir, binDir))
+ }
if exe, err := os.Executable(); err == nil {
return filepath.Abs(filepath.Join(filepath.Dir(exe), binDir))
}
@@ -283,9 +287,16 @@ func (c *Config) SetActiveProxy(proxy string) error {
return fmt.Errorf("active profile %q not found", c.Active)
}
-// LocateConfig finds configs/config.json next to the executable or in cwd.
+// LocateConfig finds configs/config.json next to the executable, in cwd,
+// or under ~/Library/Application Support/Navis on macOS.
func LocateConfig() (string, error) {
candidates := []string{}
+ if dir, ok := platformNavisDir(); ok {
+ candidates = append(candidates,
+ filepath.Join(dir, "configs", "config.json"),
+ filepath.Join(dir, "config.json"),
+ )
+ }
if exe, err := os.Executable(); err == nil {
dir := filepath.Dir(exe)
candidates = append(candidates,
@@ -301,6 +312,9 @@ func LocateConfig() (string, error) {
return p, nil
}
}
+ if dir, ok := platformNavisDir(); ok {
+ return filepath.Join(dir, "configs", "config.json"), nil
+ }
// Default path next to executable for first-run create.
if exe, err := os.Executable(); err == nil {
return filepath.Join(filepath.Dir(exe), "configs", "config.json"), nil
diff --git a/internal/config/paths_darwin.go b/internal/config/paths_darwin.go
new file mode 100644
index 0000000..3f633f8
--- /dev/null
+++ b/internal/config/paths_darwin.go
@@ -0,0 +1,16 @@
+//go:build darwin
+
+package config
+
+import (
+ "os"
+ "path/filepath"
+)
+
+func platformNavisDir() (string, bool) {
+ base, err := os.UserConfigDir()
+ if err != nil {
+ return "", false
+ }
+ return filepath.Join(base, "Navis"), true
+}
diff --git a/internal/config/paths_default.go b/internal/config/paths_default.go
new file mode 100644
index 0000000..73e5c97
--- /dev/null
+++ b/internal/config/paths_default.go
@@ -0,0 +1,7 @@
+//go:build !darwin
+
+package config
+
+func platformNavisDir() (string, bool) {
+ return "", false
+}
diff --git a/internal/protocols/hysteria2/binary.go b/internal/protocols/hysteria2/binary.go
index b5dd288..da32488 100644
--- a/internal/protocols/hysteria2/binary.go
+++ b/internal/protocols/hysteria2/binary.go
@@ -6,6 +6,7 @@ import (
"io"
"net/http"
"os"
+ "os/exec"
"path/filepath"
"runtime"
"strings"
@@ -90,6 +91,9 @@ func EnsureBinary(binDir string) (string, error) {
return "", err
}
_ = os.Chmod(dest, 0o755)
+ if runtime.GOOS == "darwin" {
+ _ = exec.Command("xattr", "-dr", "com.apple.quarantine", dest).Run()
+ }
return ResolveBinary(binDir)
}
diff --git a/internal/protocols/naive/binary.go b/internal/protocols/naive/binary.go
index c7290ae..f28340b 100644
--- a/internal/protocols/naive/binary.go
+++ b/internal/protocols/naive/binary.go
@@ -64,7 +64,19 @@ func EnsureBinary(binDir string) (string, error) {
if err := extractNaiveArchive(archivePath, binDir); err != nil {
return "", err
}
- return ResolveBinary(binDir)
+ path, err := ResolveBinary(binDir)
+ if err != nil {
+ return "", err
+ }
+ clearQuarantine(path)
+ return path, nil
+}
+
+func clearQuarantine(path string) {
+ if runtime.GOOS != "darwin" {
+ return
+ }
+ _ = exec.Command("xattr", "-dr", "com.apple.quarantine", path).Run()
}
func assetNameForPlatform() (string, error) {
@@ -76,9 +88,11 @@ func assetNameForPlatform() (string, error) {
case runtime.GOOS == "linux" && runtime.GOARCH == "amd64":
return "naiveproxy-*-linux-x64.zip", nil
case runtime.GOOS == "darwin" && runtime.GOARCH == "amd64":
- return "naiveproxy-*-mac-x64.tar.xz", nil
+ // Newer releases use mac-x64-x64; older used mac-x64.
+ return "naiveproxy-*-mac-x64", nil
case runtime.GOOS == "darwin" && runtime.GOARCH == "arm64":
- return "naiveproxy-*-mac-arm64.tar.xz", nil
+ // Newer releases use mac-arm64-arm64; older used mac-arm64.
+ return "naiveproxy-*-mac-arm64", nil
default:
return "", fmt.Errorf("unsupported platform %s/%s for auto-download; place naive binary in bin/", runtime.GOOS, runtime.GOARCH)
}
@@ -114,14 +128,32 @@ func findReleaseAsset(pattern string) (name, downloadURL string, err error) {
return "", "", err
}
prefix := strings.Split(pattern, "*")[0]
- suffix := ""
- if parts := strings.Split(pattern, "*"); len(parts) == 2 {
- suffix = parts[1]
+ needle := ""
+ if parts := strings.SplitN(pattern, "*", 2); len(parts) == 2 {
+ needle = parts[1]
}
+ var bestName, bestURL string
for _, a := range rel.Assets {
- if strings.HasPrefix(a.Name, prefix) && strings.HasSuffix(a.Name, suffix) {
- return a.Name, a.BrowserDownloadURL, nil
+ name := a.Name
+ if strings.Contains(name, "plugin") {
+ continue
}
+ if !strings.HasPrefix(name, prefix) {
+ continue
+ }
+ if needle != "" && !strings.Contains(name, needle) {
+ continue
+ }
+ // Prefer desktop archives over openwrt/apk.
+ if strings.HasSuffix(name, ".zip") || strings.HasSuffix(name, ".tar.xz") || strings.HasSuffix(name, ".txz") {
+ return name, a.BrowserDownloadURL, nil
+ }
+ if bestName == "" {
+ bestName, bestURL = name, a.BrowserDownloadURL
+ }
+ }
+ if bestName != "" {
+ return bestName, bestURL, nil
}
return "", "", fmt.Errorf("no asset matching %s in release %s", pattern, rel.TagName)
}
diff --git a/internal/protocols/xray/binary.go b/internal/protocols/xray/binary.go
index 9a87857..1124552 100644
--- a/internal/protocols/xray/binary.go
+++ b/internal/protocols/xray/binary.go
@@ -7,6 +7,7 @@ import (
"io"
"net/http"
"os"
+ "os/exec"
"path/filepath"
"runtime"
"strings"
@@ -78,6 +79,9 @@ func EnsureBinary(binDir string) (string, error) {
}
}
_ = os.Chmod(dest, 0o755)
+ if runtime.GOOS == "darwin" {
+ _ = exec.Command("xattr", "-dr", "com.apple.quarantine", dest).Run()
+ }
return ResolveBinary(binDir)
}
diff --git a/internal/update/update.go b/internal/update/update.go
index 924a72d..d278be1 100644
--- a/internal/update/update.go
+++ b/internal/update/update.go
@@ -17,7 +17,7 @@ import (
)
// CurrentVersion is the shipped client version.
-const CurrentVersion = "2.7.0"
+const CurrentVersion = "2.7.1"
// 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"
diff --git a/tools/packmac/main.go b/tools/packmac/main.go
index 3e93f29..e1a0dd6 100644
--- a/tools/packmac/main.go
+++ b/tools/packmac/main.go
@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"os"
+ "os/exec"
"path/filepath"
"strings"
@@ -45,6 +46,9 @@ func run(binPath, outDir, version, arch string) error {
if err := writeAppBundle(appRoot, binPath, version, arch); err != nil {
return err
}
+ if err := signAppBundle(appRoot); err != nil {
+ return err
+ }
readme := filepath.Join(stage, "README.txt")
if err := os.WriteFile(readme, []byte(readmeText(version, arch)), 0o644); err != nil {
return err
@@ -67,18 +71,58 @@ func run(binPath, outDir, version, arch string) error {
fmt.Println("wrote", zipPath)
dmgPath := filepath.Join(outDir, "Navis.dmg")
- dmgFiles := []dmgEntry{{appRoot, "/Navis.app"}}
- if warnName != "" {
- dmgFiles = append(dmgFiles, dmgEntry{filepath.Join(stage, warnName), "/" + warnName})
- }
- dmgFiles = append(dmgFiles, dmgEntry{readme, "/README.txt"})
- if err := writeDMG(dmgPath, dmgFiles, "Navis-"+arch); err != nil {
+ if err := writeNativeOrISODmg(dmgPath, stage, "Navis-"+arch); err != nil {
return fmt.Errorf("dmg: %w", err)
}
fmt.Println("wrote", dmgPath)
return nil
}
+// writeNativeOrISODmg prefers hdiutil (real UDIF DMG) on macOS; ISO9660 breaks .app launch.
+func writeNativeOrISODmg(dmgPath, stageDir, volume string) error {
+ if _, err := exec.LookPath("hdiutil"); err == nil {
+ return writeHdiutilDMG(dmgPath, stageDir, volume)
+ }
+ entries := []dmgEntry{}
+ _ = filepath.Walk(stageDir, func(path string, info os.FileInfo, err error) error {
+ if err != nil || info == nil {
+ return err
+ }
+ rel, err := filepath.Rel(stageDir, path)
+ if err != nil || rel == "." {
+ return err
+ }
+ // Only top-level items for ISO packer.
+ if strings.Contains(rel, string(os.PathSeparator)) {
+ return nil
+ }
+ dst := "/" + filepath.ToSlash(rel)
+ entries = append(entries, dmgEntry{path, dst})
+ return nil
+ })
+ return writeDMG(dmgPath, entries, volume)
+}
+
+func writeHdiutilDMG(dmgPath, stageDir, volume string) error {
+ _ = os.Remove(dmgPath)
+ vol := sanitizeVol(volume)
+ cmd := exec.Command("hdiutil", "create",
+ "-volname", vol,
+ "-srcfolder", stageDir,
+ "-ov",
+ "-format", "UDZO",
+ dmgPath,
+ )
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("hdiutil: %w: %s", err, strings.TrimSpace(string(out)))
+ }
+ // Re-sign after packaging is unnecessary for UDZO copy of already-signed .app,
+ // but clear quarantine on the dmg itself if present.
+ _ = exec.Command("xattr", "-cr", dmgPath).Run()
+ return nil
+}
+
func writeAppBundle(appRoot, binPath, version, arch string) error {
macOSDir := filepath.Join(appRoot, "Contents", "MacOS")
if err := os.MkdirAll(macOSDir, 0o755); err != nil {
@@ -92,10 +136,12 @@ func writeAppBundle(appRoot, binPath, version, arch string) error {
CFBundleExecutableNavis
CFBundleIdentifierwin.evilfox.navis
CFBundleNameNavis
+ CFBundleDisplayNameNavis
CFBundlePackageTypeAPPL
CFBundleShortVersionString%s
CFBundleVersion%s
LSMinimumSystemVersion11.0
+ NSHighResolutionCapable
CFBundleInfoDictionaryVersion6.0%s
@@ -103,6 +149,9 @@ func writeAppBundle(appRoot, binPath, version, arch string) error {
if err := os.WriteFile(filepath.Join(appRoot, "Contents", "Info.plist"), []byte(plist), 0o644); err != nil {
return err
}
+ if err := os.WriteFile(filepath.Join(appRoot, "Contents", "PkgInfo"), []byte("APPL????"), 0o644); err != nil {
+ return err
+ }
dest := filepath.Join(macOSDir, "Navis")
if err := copyFile(binPath, dest, 0o755); err != nil {
return err
@@ -110,6 +159,25 @@ func writeAppBundle(appRoot, binPath, version, arch string) error {
return nil
}
+func signAppBundle(appRoot string) error {
+ if _, err := exec.LookPath("codesign"); err != nil {
+ return nil
+ }
+ // Fresh ad-hoc signature; entitlements not required for local/dev builds.
+ cmd := exec.Command("codesign", "-s", "-", "--force", "--deep", "--options", "runtime", appRoot)
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ // --options runtime can fail without a real Developer ID; retry plain ad-hoc.
+ cmd = exec.Command("codesign", "-s", "-", "--force", "--deep", appRoot)
+ out, err = cmd.CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("codesign %s: %w: %s", appRoot, err, strings.TrimSpace(string(out)))
+ }
+ }
+ _ = exec.Command("xattr", "-cr", appRoot).Run()
+ return nil
+}
+
type dmgEntry struct {
srcPath string
dstPath string
@@ -368,16 +436,17 @@ func sanitizeVol(s string) string {
func readmeText(version, arch string) string {
switch arch {
case "universal":
- return fmt.Sprintf(`Navis %s for macOS (универсальная GUI-сборка)
+ return fmt.Sprintf(`Navis %s for macOS (универсальная сборка)
Подходит для Apple Silicon (M1/M2/M3/M4) и Intel Mac.
1) Перетащите Navis.app в Программы (Applications)
-2) При блокировке:
+2) При первом запуске, если macOS блокирует:
xattr -dr com.apple.quarantine /Applications/Navis.app
-3) Запустите Navis — откроется окно как на Windows
- (Chrome/Edge/Brave в режиме приложения, иначе браузер)
-4) CLI (ядро/connect из терминала): рядом на git файл Navis-cli
+3) В Терминале:
+ /Applications/Navis.app/Contents/MacOS/Navis init
+ /Applications/Navis.app/Contents/MacOS/Navis install-core
+ /Applications/Navis.app/Contents/MacOS/Navis connect
Магазин: https://evilfox.win/
`, version)
@@ -467,7 +536,9 @@ func architecturePriorityXML(arch string) string {
case "arm64":
return `
LSArchitecturePriority
- arm64`
+ arm64
+ LSRequiresNativeExecution
+ `
case "amd64":
return `
LSArchitecturePriority
diff --git a/versioninfo.json b/versioninfo.json
index 2084ac2..844ecaf 100644
--- a/versioninfo.json
+++ b/versioninfo.json
@@ -11,12 +11,12 @@
"StringFileInfo": {
"CompanyName": "EvilFox",
"FileDescription": "Navis 2 — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
- "FileVersion": "2.7.0.0",
+ "FileVersion": "2.7.1.0",
"InternalName": "Navis",
"LegalCopyright": "Copyright (c) EvilFox",
"OriginalFilename": "Navis.exe",
"ProductName": "Navis",
- "ProductVersion": "2.7.0.0",
+ "ProductVersion": "2.7.1.0",
"Comments": "Open-source VPN/proxy client. https://evilfox.win/"
},
"VarFileInfo": {