Release 4.0.0+1: rebrand Navis → EvilFox; centralized Remnawave auto-provisioning (panel credentials baked into binary); remove Remnawave API UI; Windows 4.0.0.1.

Keep dist/navis-release/ path and ship Navis.exe as a same-hash alias of EvilFox.exe so 3.x clients can still update from the Windows branch feed.
This commit is contained in:
Navis
2026-08-01 20:06:25 +03:00
parent e76c0a0977
commit 1144fab54d
32 changed files with 280 additions and 243 deletions
+5
View File
@@ -6,6 +6,8 @@ bin/
configs/config.json configs/config.json
configs/remnawave-api.json configs/remnawave-api.json
configs/account.json configs/account.json
# Baked panel credentials (copied by build.bat; never commit)
internal/remnawave/embeddedcfg/remnawave-api.json
dist/navis-release/windows/configs/ dist/navis-release/windows/configs/
# Runtime-downloaded protocol cores + local packaging artifact (not release sources) # Runtime-downloaded protocol cores + local packaging artifact (not release sources)
dist/navis-release/windows/bin/ dist/navis-release/windows/bin/
@@ -17,8 +19,11 @@ cmd/vpnapp/resource.syso
navis-update.bat navis-update.bat
Navis.exe.new Navis.exe.new
Navis-pending.exe Navis-pending.exe
EvilFox.exe.new
EvilFox-pending.exe
*.bak *.bak
Navis.exe.bak Navis.exe.bak
EvilFox.exe.bak
# MSIX staging / local packages # MSIX staging / local packages
packaging/msix/_staging/ packaging/msix/_staging/
+5 -3
View File
@@ -1,12 +1,14 @@
# Navis # EvilFox
Клиент [NaiveProxy](https://github.com/klzgrad/naiveproxy) + [Hysteria 2](https://github.com/apernet/hysteria) + [AmneziaWG 2.0](https://docs.amnezia.org/documentation/amnezia-wg/) + [Xray](https://github.com/XTLS/Xray-core) (VLESS / VMess / Trojan) с профилями, пингом, URL-подпиской / **Remnawave API** и автообновлением. Клиент [NaiveProxy](https://github.com/klzgrad/naiveproxy) + [Hysteria 2](https://github.com/apernet/hysteria) + [AmneziaWG 2.0](https://docs.amnezia.org/documentation/amnezia-wg/) + [Xray](https://github.com/XTLS/Xray-core) (VLESS / VMess / Trojan) с профилями, пингом, централизованной выдачей конфигов (Remnawave) и автообновлением.
> Ранее приложение называлось **Navis**. С 4.0.0 бинарник — `EvilFox.exe`. Каталог релиза `dist/navis-release/` и URL-путь сохранены, чтобы клиенты 3.x продолжали обновляться; в той же папке лежит и `Navis.exe` (копия `EvilFox.exe`) как совместимый алиас.
## Платформы ## Платформы
| OS | Артефакты | UI | | OS | Артефакты | UI |
|----|----------|----| |----|----------|----|
| Windows amd64 | `Navis.exe` | GUI (WebView2) | | Windows amd64 | `EvilFox.exe` (+ `Navis.exe` alias) | GUI (WebView2) |
| macOS universal | `Navis.dmg` · `Navis.app.zip` | **GUI** (как Windows) | | macOS universal | `Navis.dmg` · `Navis.app.zip` | **GUI** (как Windows) |
| macOS arm64 / amd64 | `Navis` · DMG · `Navis-cli` | GUI + CLI | | macOS arm64 / amd64 | `Navis` · DMG · `Navis-cli` | GUI + CLI |
| Android | `Navis.apk` | GUI (VpnService) | | Android | `Navis.apk` | GUI (VpnService) |
+2 -2
View File
@@ -13,8 +13,8 @@ android {
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
// versionCode = major*1_000_000 + minor*10_000 + patch*100 + build // versionCode = major*1_000_000 + minor*10_000 + patch*100 + build
versionCode = 3_100_001 versionCode = 4_000_001
versionName = "3.10.0+1" versionName = "4.0.0+1"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
ndk { ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64") abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
@@ -94,7 +94,7 @@ fun NavisScreen(
.padding(18.dp) .padding(18.dp)
) { ) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text("Navis", fontSize = 28.sp, fontWeight = FontWeight.Black, color = Ink) Text("EvilFox", fontSize = 28.sp, fontWeight = FontWeight.Black, color = Ink)
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Surface(color = Soft, shape = RoundedCornerShape(999.dp)) { Surface(color = Soft, shape = RoundedCornerShape(999.dp)) {
Text("2.6.1", Modifier.padding(horizontal = 8.dp, vertical = 2.dp), fontSize = 12.sp, fontWeight = FontWeight.Bold, color = AccentDeep) Text("2.6.1", Modifier.padding(horizontal = 8.dp, vertical = 2.dp), fontSize = 12.sp, fontWeight = FontWeight.Bold, color = AccentDeep)
+2 -2
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">Navis</string> <string name="app_name">EvilFox</string>
<string name="vpn_notification_title">Navis VPN</string> <string name="vpn_notification_title">EvilFox VPN</string>
<string name="vpn_notification_connected">Подключено</string> <string name="vpn_notification_connected">Подключено</string>
<string name="vpn_notification_channel">VPN</string> <string name="vpn_notification_channel">VPN</string>
</resources> </resources>
+23 -8
View File
@@ -14,22 +14,37 @@ if errorlevel 1 (
goversioninfo -64 -icon assets\navis.ico -manifest assets\app.manifest -o cmd\vpnapp\resource.syso versioninfo.json goversioninfo -64 -icon assets\navis.ico -manifest assets\app.manifest -o cmd\vpnapp\resource.syso versioninfo.json
if errorlevel 1 exit /b 1 if errorlevel 1 exit /b 1
echo Building Navis GUI and CLI... REM Bake Remnawave panel credentials into the binary (NordVPN-style centralized provisioning).
go build -ldflags="-H windowsgui -s -w" -trimpath -o Navis.exe ./cmd/vpnapp REM Source of truth: configs\remnawave-api.json (gitignored). Never commit the embedded copy.
if exist "configs\remnawave-api.json" (
mkdir "internal\remnawave\embeddedcfg" 2>nul
copy /Y "configs\remnawave-api.json" "internal\remnawave\embeddedcfg\remnawave-api.json" >nul
echo Embedded panel credentials from configs\remnawave-api.json
) else (
echo WARNING: configs\remnawave-api.json missing — build will use external file / no auto-provision
)
echo Building EvilFox GUI and CLI...
go build -ldflags="-H windowsgui -s -w" -trimpath -o EvilFox.exe ./cmd/vpnapp
if errorlevel 1 exit /b 1 if errorlevel 1 exit /b 1
go build -ldflags="-s -w" -trimpath -o vpnclient.exe ./cmd/vpnclient go build -ldflags="-s -w" -trimpath -o evilfox-cli.exe ./cmd/vpnclient
if errorlevel 1 exit /b 1 if errorlevel 1 exit /b 1
mkdir "dist\navis-release\windows" 2>nul mkdir "dist\navis-release\windows" 2>nul
copy /Y Navis.exe "dist\navis-release\windows\Navis.exe" >nul copy /Y EvilFox.exe "dist\navis-release\windows\EvilFox.exe" >nul
copy /Y vpnclient.exe "dist\navis-release\windows\vpnclient.exe" >nul REM Keep Navis.exe path for existing 3.x clients that fetch the old feed URL.
copy /Y EvilFox.exe "dist\navis-release\windows\Navis.exe" >nul
copy /Y evilfox-cli.exe "dist\navis-release\windows\evilfox-cli.exe" >nul
copy /Y evilfox-cli.exe "dist\navis-release\windows\vpnclient.exe" >nul
echo. echo.
echo Done: echo Done:
echo Navis.exe echo EvilFox.exe
echo vpnclient.exe echo evilfox-cli.exe
echo dist\navis-release\windows\Navis.exe echo dist\navis-release\windows\EvilFox.exe
echo dist\navis-release\windows\Navis.exe (compat alias for 3.x updater)
echo dist\navis-release\windows\evilfox-cli.exe
echo dist\navis-release\windows\vpnclient.exe echo dist\navis-release\windows\vpnclient.exe
echo assets\navis.ico echo assets\navis.ico
endlocal endlocal
+2 -2
View File
@@ -71,7 +71,7 @@ func main() {
log.Printf("open UI: %v — откройте вручную: %s", err, uiURL) log.Printf("open UI: %v — откройте вручную: %s", err, uiURL)
fmt.Println(uiURL) fmt.Println(uiURL)
} else { } else {
log.Printf("Navis UI: %s", uiURL) log.Printf("EvilFox UI: %s", uiURL)
} }
sig := make(chan os.Signal, 1) sig := make(chan os.Signal, 1)
@@ -96,7 +96,7 @@ func openAppWindow(uiURL string) error {
func fatalf(format string, args ...any) { func fatalf(format string, args ...any) {
msg := fmt.Sprintf(format, args...) msg := fmt.Sprintf(format, args...)
log.Println(msg) log.Println(msg)
_ = exec.Command("osascript", "-e", fmt.Sprintf(`display alert "Navis" message %q`, msg)).Run() _ = exec.Command("osascript", "-e", fmt.Sprintf(`display alert "EvilFox" message %q`, msg)).Run()
os.Exit(1) os.Exit(1)
} }
+2 -2
View File
@@ -8,7 +8,7 @@ import (
) )
func main() { func main() {
fmt.Fprintln(os.Stderr, "Navis GUI supports Windows (WebView2) and macOS.") fmt.Fprintln(os.Stderr, "EvilFox GUI supports Windows (WebView2) and macOS.")
fmt.Fprintln(os.Stderr, "On this OS use the CLI: ./Navis connect | install-core | check-update") fmt.Fprintln(os.Stderr, "On this OS use the CLI: ./evilfox-cli connect | install-core | check-update")
os.Exit(1) os.Exit(1)
} }
+23 -3
View File
@@ -117,7 +117,7 @@ func main() {
AutoFocus: true, AutoFocus: true,
DataPath: dataPath, DataPath: dataPath,
WindowOptions: webview2.WindowOptions{ WindowOptions: webview2.WindowOptions{
Title: "Navis 2", Title: "EvilFox",
Width: 920, Width: 920,
Height: 680, Height: 680,
Center: true, Center: true,
@@ -160,12 +160,32 @@ func main() {
mustBind(w, "getLogs", a.getLogs) mustBind(w, "getLogs", a.getLogs)
go a.autoCheckUpdate() go a.autoCheckUpdate()
go a.autoProvision()
mgr.StartAutoRenew(6 * time.Hour) mgr.StartAutoRenew(6 * time.Hour)
w.SetHtml(appui.IndexHTML) w.SetHtml(appui.IndexHTML)
w.Run() w.Run()
} }
// autoProvision quietly issues a panel account + imports configs on first run
// (NordVPN-style centralized provisioning — users never edit the panel).
func (a *app) autoProvision() {
if !a.mgr.ProvisionConfigured() {
return
}
time.Sleep(2 * time.Second)
out, err := a.mgr.EnsureAutoProvision()
if err != nil {
fmt.Fprintf(a.logBuf, "auto-provision: %v\n", err)
return
}
if out.Created {
fmt.Fprintf(a.logBuf, "auto-provision: создан %s · серверов %d\n", out.Username, out.Imported)
} else if out.Imported > 0 {
fmt.Fprintf(a.logBuf, "auto-provision: обновлены конфиги %s · серверов %d\n", out.Username, out.Imported)
}
}
func mustBind(w webview2.WebView, name string, fn interface{}) { func mustBind(w webview2.WebView, name string, fn interface{}) {
if err := w.Bind(name, fn); err != nil { if err := w.Bind(name, fn); err != nil {
fatalDialog("bind %s: %v", name, err) fatalDialog("bind %s: %v", name, err)
@@ -503,12 +523,12 @@ func (a *app) applyUpdate() (string, error) {
return "", err return "", err
} }
_ = a.mgr.Disconnect() _ = a.mgr.Disconnect()
// Hard-exit so Windows unlocks Navis.exe; do not wait on webview.Terminate (can hang). // Hard-exit so Windows unlocks EvilFox.exe; do not wait on webview.Terminate (can hang).
go func() { go func() {
time.Sleep(300 * time.Millisecond) time.Sleep(300 * time.Millisecond)
os.Exit(0) os.Exit(0)
}() }()
return "Устанавливается v" + latest + "… Navis перезапустится.", nil return "Устанавливается v" + latest + "… EvilFox перезапустится.", nil
} }
func (a *app) autoCheckUpdate() { func (a *app) autoCheckUpdate() {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
func messageBox(text string) { func messageBox(text string) {
user32 := syscall.NewLazyDLL("user32.dll") user32 := syscall.NewLazyDLL("user32.dll")
proc := user32.NewProc("MessageBoxW") proc := user32.NewProc("MessageBoxW")
title, _ := syscall.UTF16PtrFromString("Navis") title, _ := syscall.UTF16PtrFromString("EvilFox")
body, _ := syscall.UTF16PtrFromString(text) body, _ := syscall.UTF16PtrFromString(text)
proc.Call(0, uintptr(unsafe.Pointer(body)), uintptr(unsafe.Pointer(title)), 0x10) // MB_ICONERROR proc.Call(0, uintptr(unsafe.Pointer(body)), uintptr(unsafe.Pointer(title)), 0x10) // MB_ICONERROR
} }
+8 -8
View File
@@ -59,16 +59,16 @@ func main() {
} }
func printUsage() { func printUsage() {
fmt.Fprintf(os.Stderr, `Navis VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan) Windows / macOS fmt.Fprintf(os.Stderr, `EvilFox VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan) Windows / macOS
Usage: Usage:
navis init [-config configs/config.json] evilfox-cli init [-config configs/config.json]
navis install-core [-config configs/config.json] evilfox-cli install-core [-config configs/config.json]
navis set-proxy [-config configs/config.json] <share-or-proxy-uri> evilfox-cli set-proxy [-config configs/config.json] <share-or-proxy-uri>
navis connect [-config configs/config.json] [-profile name] [-no-sysproxy] [-probe] evilfox-cli connect [-config configs/config.json] [-profile name] [-no-sysproxy] [-probe]
navis probe [-config configs/config.json] [-profile name] [-url URL] evilfox-cli probe [-config configs/config.json] [-profile name] [-url URL]
navis check-update evilfox-cli check-update
navis apply-update evilfox-cli apply-update
Share links: naive+https://… hysteria2://… vless://… vmess://… trojan://… awg .conf Share links: naive+https://… hysteria2://… vless://… vmess://… trojan://… awg .conf
+6 -6
View File
@@ -1,16 +1,16 @@
{ {
"version": "3.10.0", "version": "4.0.0",
"notes": "3.10.0+2: новый интерфейс с боковым меню (Главная / Профили / Настройки / Логи / О программе), аккаунт с продлением 50 ГБ / +30 дней и автопродлением, режим VPN на Windows (TUN, весь трафик, нужны права администратора и wintun.dll). Обновления теперь с git.de4ima.uk. Windows 3.10.0.2.", "notes": "4.0.0+1: ребрендинг Navis → EvilFox; централизованная автовыдача конфигов (как NordVPN — панель вшита, пользователь только выбирает сервер и продлевает); из интерфейса убраны настройки Remnawave API. Windows 4.0.0.1. Клиенты 3.x обновляются по этому фиду; Navis.exe в релизе — совместимый алиас EvilFox.exe.",
"platform": "windows-amd64", "platform": "windows-amd64",
"os": "windows", "os": "windows",
"arch": "amd64", "arch": "amd64",
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/Navis.exe", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
"sha256": "55ba57898ffc23ce1cb14172643513f70a2ae972c00181b2d3d889430bce1eb3", "sha256": "869b37310526003143c5db8ea3ad6d4468173e9053cf585f06c162a861d5c49c",
"mandatory": false, "mandatory": false,
"platforms": { "platforms": {
"windows-amd64": { "windows-amd64": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/Navis.exe", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
"sha256": "55ba57898ffc23ce1cb14172643513f70a2ae972c00181b2d3d889430bce1eb3", "sha256": "869b37310526003143c5db8ea3ad6d4468173e9053cf585f06c162a861d5c49c",
"os": "windows", "os": "windows",
"arch": "amd64" "arch": "amd64"
}, },
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+6 -6
View File
@@ -1,16 +1,16 @@
{ {
"version": "3.10.0", "version": "4.0.0",
"notes": "3.10.0+2: новый интерфейс с боковым меню (Главная / Профили / Настройки / Логи / О программе), аккаунт с продлением 50 ГБ / +30 дней и автопродлением, режим VPN на Windows (TUN, весь трафик, нужны права администратора и wintun.dll). Обновления теперь с git.de4ima.uk. Windows 3.10.0.2.", "notes": "4.0.0+1: ребрендинг Navis → EvilFox; централизованная автовыдача конфигов (как NordVPN — панель вшита, пользователь только выбирает сервер и продлевает); из интерфейса убраны настройки Remnawave API. Windows 4.0.0.1. Клиенты 3.x обновляются по этому фиду; Navis.exe в релизе — совместимый алиас EvilFox.exe.",
"platform": "windows-amd64", "platform": "windows-amd64",
"os": "windows", "os": "windows",
"arch": "amd64", "arch": "amd64",
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/Navis.exe", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
"sha256": "55ba57898ffc23ce1cb14172643513f70a2ae972c00181b2d3d889430bce1eb3", "sha256": "869b37310526003143c5db8ea3ad6d4468173e9053cf585f06c162a861d5c49c",
"mandatory": false, "mandatory": false,
"platforms": { "platforms": {
"windows-amd64": { "windows-amd64": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/Navis.exe", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
"sha256": "55ba57898ffc23ce1cb14172643513f70a2ae972c00181b2d3d889430bce1eb3", "sha256": "869b37310526003143c5db8ea3ad6d4468173e9053cf585f06c162a861d5c49c",
"os": "windows", "os": "windows",
"arch": "amd64" "arch": "amd64"
}, },
+13 -1
View File
@@ -76,6 +76,18 @@ type PingBestResult struct {
func New(mgr *core.Manager, cfgPath string, logBuf *bytes.Buffer) *App { func New(mgr *core.Manager, cfgPath string, logBuf *bytes.Buffer) *App {
// Monthly plan upkeep: check at start and then periodically. // Monthly plan upkeep: check at start and then periodically.
mgr.StartAutoRenew(6 * time.Hour) mgr.StartAutoRenew(6 * time.Hour)
// NordVPN-style: silently issue a panel account + import configs.
go func() {
time.Sleep(2 * time.Second)
if !mgr.ProvisionConfigured() {
return
}
if out, err := mgr.EnsureAutoProvision(); err != nil {
fmt.Fprintf(logBuf, "auto-provision: %v\n", err)
} else if out.Created {
fmt.Fprintf(logBuf, "auto-provision: создан %s · серверов %d\n", out.Username, out.Imported)
}
}()
return &App{ return &App{
Mgr: mgr, Mgr: mgr,
CfgPath: cfgPath, CfgPath: cfgPath,
@@ -410,7 +422,7 @@ func (a *App) ApplyUpdate() (string, error) {
os.Exit(0) os.Exit(0)
}() }()
} }
return "Устанавливается v" + latest + "… Navis перезапустится.", nil return "Устанавливается v" + latest + "… EvilFox перезапустится.", nil
} }
func (a *App) AutoCheckUpdate() { func (a *App) AutoCheckUpdate() {
+11 -139
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Navis</title> <title>EvilFox</title>
<script> <script>
(function () { (function () {
try { try {
@@ -993,8 +993,8 @@
<path d="M22.5 8.2l3.2-1.1-1.1 3.2-5.6 5.6 1.6 1.6 5.6-5.6 3.2-1.1-1.1 3.2" fill="#b8f0d8" opacity=".9"/> <path d="M22.5 8.2l3.2-1.1-1.1 3.2-5.6 5.6 1.6 1.6 5.6-5.6 3.2-1.1-1.1 3.2" fill="#b8f0d8" opacity=".9"/>
</svg> </svg>
</div> </div>
<h1 class="brand">Navis</h1> <h1 class="brand">EvilFox</h1>
<span class="badge-ver" id="badgeVer">3.10.0</span> <span class="badge-ver" id="badgeVer">4.0.0</span>
</div> </div>
<button class="nav-btn active" type="button" data-page="home"> <button class="nav-btn active" type="button" data-page="home">
@@ -1020,7 +1020,7 @@
<div class="spacer"></div> <div class="spacer"></div>
<div class="side-foot"> <div class="side-foot">
<span class="ver" id="verLabel">Navis</span> <span class="ver" id="verLabel">EvilFox</span>
<button type="button" class="theme-toggle" id="themeToggle" title="Тема" aria-label="Переключить тему"> <button type="button" class="theme-toggle" id="themeToggle" title="Тема" aria-label="Переключить тему">
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" aria-hidden="true"> <svg class="icon-moon" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M20.5 14.2A8.2 8.2 0 0 1 9.8 3.5 8.5 8.5 0 1 0 20.5 14.2z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/> <path d="M20.5 14.2A8.2 8.2 0 0 1 9.8 3.5 8.5 8.5 0 1 0 20.5 14.2z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
@@ -1150,17 +1150,6 @@
</div> </div>
</details> </details>
<!-- Выдача доступа: скрыта, когда аккаунт уже привязан
(создание второго конфига отключено — платная подписка появится позже) -->
<div class="remna-box" id="provisionBox" hidden>
<div class="remna-title">Выдать доступ</div>
<p class="remna-hint">Создаёт пользователя в панели Remnawave (тариф из configs/remnawave-api.json, по умолчанию 50 ГБ / 30 дней), запоминает его и сразу импортирует конфиги. Повторно создать аккаунт нельзя — только продлевать.</p>
<div>
<label class="field" for="provUser">Пользователь</label>
<input id="provUser" type="text" placeholder="username / @telegram / email" spellcheck="false" autocomplete="off" />
</div>
<button class="action primary" id="provBtn" type="button">Выдать доступ</button>
</div>
</section> </section>
<!-- ============ Настройки ============ --> <!-- ============ Настройки ============ -->
@@ -1173,7 +1162,7 @@
<button type="button" id="modeProxyBtn">Режим прокси</button> <button type="button" id="modeProxyBtn">Режим прокси</button>
<button type="button" id="modeVpnBtn">Режим VPN</button> <button type="button" id="modeVpnBtn">Режим VPN</button>
</div> </div>
<p class="remna-hint" id="modeHint">Прокси — трафик идёт через локальный прокси (и системный прокси Windows). VPN — весь трафик, включая игры и приложения, через TUN-адаптер; нужны права администратора и wintun.dll рядом с Navis.exe. Меняется в отключённом состоянии.</p> <p class="remna-hint" id="modeHint">Прокси — трафик идёт через локальный прокси (и системный прокси Windows). VPN — весь трафик, включая игры и приложения, через TUN-адаптер; нужны права администратора и wintun.dll рядом с EvilFox.exe. Меняется в отключённом состоянии.</p>
</div> </div>
<div class="row"> <div class="row">
@@ -1260,33 +1249,6 @@
<label class="check"><input id="hy2Lazy" type="checkbox" /> Lazy connect</label> <label class="check"><input id="hy2Lazy" type="checkbox" /> Lazy connect</label>
</details> </details>
<details class="panel" id="rwPanel">
<summary>Remnawave API</summary>
<div class="remna-box" style="margin-top:8px;border:0;padding:0;background:transparent">
<p class="remna-hint">Панель → short UUID пользователя. Токен API — опционально (для /api/subscriptions/… и /users/…).</p>
<div>
<label class="field" for="rwBase">URL панели</label>
<input id="rwBase" type="text" placeholder="https://panel.example.com" spellcheck="false" />
</div>
<div>
<label class="field" for="rwShort">Short UUID</label>
<input id="rwShort" type="text" placeholder="abc12xyz" spellcheck="false" />
</div>
<div>
<label class="field" for="rwToken">API token</label>
<input id="rwToken" type="password" placeholder="Bearer из панели → API Tokens" spellcheck="false" autocomplete="off" />
</div>
<div>
<label class="field" for="rwCaddy">Caddy X-Api-Key</label>
<input id="rwCaddy" type="password" placeholder="если стоит auth у Caddy" spellcheck="false" autocomplete="off" />
</div>
<div class="sub-bar" style="margin-top:8px">
<button class="action secondary" id="rwSaveBtn" type="button" style="flex:1">Сохранить</button>
<button class="action primary" id="rwSyncBtn" type="button" style="flex:1">Синхронизировать</button>
</div>
</div>
</details>
<details class="panel" id="toolsBox"> <details class="panel" id="toolsBox">
<summary>Сервис · cores, обновления</summary> <summary>Сервис · cores, обновления</summary>
<div class="tools"> <div class="tools">
@@ -1317,9 +1279,9 @@
<path d="M22.5 8.2l3.2-1.1-1.1 3.2-5.6 5.6 1.6 1.6 5.6-5.6 3.2-1.1-1.1 3.2" fill="#b8f0d8" opacity=".9"/> <path d="M22.5 8.2l3.2-1.1-1.1 3.2-5.6 5.6 1.6 1.6 5.6-5.6 3.2-1.1-1.1 3.2" fill="#b8f0d8" opacity=".9"/>
</svg> </svg>
</div> </div>
<h2>Navis</h2> <h2>EvilFox</h2>
<p id="aboutVer">версия…</p> <p id="aboutVer">версия…</p>
<p style="margin-top:6px">VPN/прокси-клиент: Remnawave · Naive · Hysteria 2 · AWG · Xray</p> <p style="margin-top:6px">VPN/прокси-клиент: Naive · Hysteria 2 · AWG · Xray</p>
</div> </div>
<div class="tools" style="margin-top:0"> <div class="tools" style="margin-top:0">
<button class="action secondary" id="aboutUpdBtn" type="button">Проверить обновление</button> <button class="action secondary" id="aboutUpdBtn" type="button">Проверить обновление</button>
@@ -1498,15 +1460,6 @@
} }
const subUrl = $("subUrl"); const subUrl = $("subUrl");
const subBtn = $("subBtn"); const subBtn = $("subBtn");
const rwBase = $("rwBase");
const rwShort = $("rwShort");
const rwToken = $("rwToken");
const rwCaddy = $("rwCaddy");
const rwSaveBtn = $("rwSaveBtn");
const rwSyncBtn = $("rwSyncBtn");
const provisionBox = $("provisionBox");
const provUser = $("provUser");
const provBtn = $("provBtn");
const hero = $("hero"); const hero = $("hero");
const protoChip = $("protoChip"); const protoChip = $("protoChip");
const heroHint = $("heroHint"); const heroHint = $("heroHint");
@@ -1572,7 +1525,7 @@
function markDirty() { dirty = true; } function markDirty() { dirty = true; }
[ [
proxy, nameInput, sysproxy, subUrl, rwBase, rwShort, rwToken, rwCaddy, proxy, nameInput, sysproxy, subUrl,
$("hy2Congestion"), $("hy2Bbr"), $("hy2Up"), $("hy2Down"), $("hy2Congestion"), $("hy2Bbr"), $("hy2Up"), $("hy2Down"),
$("hy2Obfs"), $("hy2ObfsPass"), $("hy2Sni"), $("hy2Pin"), $("hy2Hop"), $("hy2Obfs"), $("hy2ObfsPass"), $("hy2Sni"), $("hy2Pin"), $("hy2Hop"),
$("hy2Insecure"), $("hy2Fast"), $("hy2Lazy") $("hy2Insecure"), $("hy2Fast"), $("hy2Lazy")
@@ -1681,7 +1634,7 @@
if (!list.length) { if (!list.length) {
const empty = document.createElement("div"); const empty = document.createElement("div");
empty.className = "server-empty"; empty.className = "server-empty";
empty.textContent = "Нет серверов — добавьте профиль или подписку"; empty.textContent = "Нет серверов — подождите автовыдачу или добавьте подписку";
serverList.appendChild(empty); serverList.appendChild(empty);
return; return;
} }
@@ -1876,7 +1829,7 @@
} }
function renderUpdate(u, version) { function renderUpdate(u, version) {
const label = "Navis v" + (version || "?"); const label = "EvilFox v" + (version || "?");
verLabel.textContent = label; verLabel.textContent = label;
if (aboutVer) aboutVer.textContent = "Версия " + (version || "?"); if (aboutVer) aboutVer.textContent = "Версия " + (version || "?");
if (badgeVer && version) { if (badgeVer && version) {
@@ -1941,16 +1894,6 @@
updateBtn.disabled = busy; updateBtn.disabled = busy;
subBtn.disabled = busy; subBtn.disabled = busy;
subUrl.disabled = busy; subUrl.disabled = busy;
if (rwBase) rwBase.disabled = busy;
if (rwShort) rwShort.disabled = busy;
if (rwToken) rwToken.disabled = busy;
if (rwCaddy) rwCaddy.disabled = busy;
if (rwSaveBtn) rwSaveBtn.disabled = busy;
if (rwSyncBtn) rwSyncBtn.disabled = busy;
// Выдать доступ — только пока аккаунт НЕ привязан (второй не создаём).
if (provisionBox) provisionBox.hidden = !state.provision_ready || !!state.account;
if (provUser) provUser.disabled = busy;
if (provBtn) provBtn.disabled = busy;
if (renewBtn) renewBtn.disabled = busy || !state.provision_ready; if (renewBtn) renewBtn.disabled = busy || !state.provision_ready;
if (modeProxyBtn) modeProxyBtn.disabled = busy || connected; if (modeProxyBtn) modeProxyBtn.disabled = busy || connected;
if (modeVpnBtn) modeVpnBtn.disabled = busy || connected; if (modeVpnBtn) modeVpnBtn.disabled = busy || connected;
@@ -1965,12 +1908,6 @@
if (typeof state.subscription_url === "string" && !dirty) { if (typeof state.subscription_url === "string" && !dirty) {
subUrl.value = state.subscription_url; subUrl.value = state.subscription_url;
} }
if (state.remnawave && !dirty) {
rwBase.value = state.remnawave.base_url || "";
rwShort.value = state.remnawave.short_uuid || "";
if (typeof state.remnawave.token === "string") rwToken.value = state.remnawave.token;
if (typeof state.remnawave.caddy_token === "string") rwCaddy.value = state.remnawave.caddy_token;
}
if (syncForm || (!formHydrated && !dirty)) { if (syncForm || (!formHydrated && !dirty)) {
const active = (state.profiles || []).find((p) => p.name === profile.value) || {}; const active = (state.profiles || []).find((p) => p.name === profile.value) || {};
@@ -2038,8 +1975,7 @@
function paintButtonsLocked(locked) { function paintButtonsLocked(locked) {
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, bestBtn, updCheckBtn, aboutUpdBtn, [btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, bestBtn, updCheckBtn, aboutUpdBtn,
updateBtn, skipUpdateBtn, subBtn, subUrl, rwBase, rwShort, rwToken, rwCaddy, rwSaveBtn, rwSyncBtn, updateBtn, skipUpdateBtn, subBtn, subUrl, renewBtn, modeProxyBtn, modeVpnBtn, logsRefreshBtn].forEach((b) => {
provUser, provBtn, renewBtn, modeProxyBtn, modeVpnBtn, logsRefreshBtn].forEach((b) => {
if (b) b.disabled = locked; if (b) b.disabled = locked;
}); });
} }
@@ -2166,15 +2102,6 @@
return "Импортировано: " + r; return "Импортировано: " + r;
} }
function readRemnawave() {
return {
base_url: (rwBase && rwBase.value || "").trim(),
short_uuid: (rwShort && rwShort.value || "").trim(),
token: (rwToken && rwToken.value || "").trim(),
caddy_token: (rwCaddy && rwCaddy.value || "").trim()
};
}
subBtn.addEventListener("click", () => withBusy(async () => { subBtn.addEventListener("click", () => withBusy(async () => {
try { try {
await runImportSubscription(); await runImportSubscription();
@@ -2187,61 +2114,6 @@
} }
}); });
if (rwSaveBtn) {
rwSaveBtn.addEventListener("click", () => withBusy(async () => {
try {
await saveRemnawave(readRemnawave());
setMeta("Настройки Remnawave сохранены", "ok");
} catch (e) { setMeta(String(e), "err"); }
}));
}
if (rwSyncBtn) {
rwSyncBtn.addEventListener("click", () => withBusy(async () => {
try {
const s = readRemnawave();
if (!s.base_url || !s.short_uuid) {
setMeta("Укажите URL панели и short UUID", "err");
return;
}
setMeta("Remnawave: загрузка конфигов…");
const r = await importRemnawave(s);
formHydrated = false;
dirty = false;
setMeta("Remnawave: " + importSummary(r).toLowerCase() + " · пинг…", "ok");
await runBest(!!autoBest.checked);
} catch (e) { setMeta(String(e), "err"); }
}));
}
if (provBtn) {
provBtn.addEventListener("click", () => withBusy(async () => {
try {
const u = (provUser.value || "").trim();
if (!u) {
setMeta("Укажите имя пользователя (username / @telegram / email)", "err");
return;
}
setMeta("Выдача доступа через панель…");
const r = await provisionUser(u);
formHydrated = false;
dirty = false;
let msg = (r.created ? "Создан пользователь " : "Найден пользователь ") + r.username;
if (r.traffic_limit_bytes > 0) msg += " · " + fmtTraffic(r.traffic_limit_bytes);
if (r.expire_at) {
try { msg += " · до " + new Date(r.expire_at * 1000).toLocaleDateString("ru-RU"); } catch (_) {}
}
msg += " · импортировано серверов: " + (r.imported || 0);
setMeta(msg, "ok");
} catch (e) { setMeta(String(e), "err"); }
}));
provUser.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
provBtn.click();
}
});
}
if (renewBtn) { if (renewBtn) {
renewBtn.addEventListener("click", () => withBusy(async () => { renewBtn.addEventListener("click", () => withBusy(async () => {
try { try {
+45 -6
View File
@@ -298,10 +298,10 @@ func (m *Manager) SetMode(mode string) error {
return fmt.Errorf("режим VPN пока доступен только на Windows") return fmt.Errorf("режим VPN пока доступен только на Windows")
} }
if !vpnmode.IsElevated() { if !vpnmode.IsElevated() {
return fmt.Errorf("Запустите Navis от имени администратора для режима VPN") return fmt.Errorf("Запустите EvilFox от имени администратора для режима VPN")
} }
if !vpnmode.WintunAvailable() { if !vpnmode.WintunAvailable() {
return fmt.Errorf("не найден wintun.dll — скачайте с wintun.net и положите рядом с Navis.exe") return fmt.Errorf("не найден wintun.dll — скачайте с wintun.net и положите рядом с EvilFox.exe")
} }
} }
m.mu.Lock() m.mu.Lock()
@@ -700,9 +700,12 @@ type ProvisionOutcome struct {
Skipped int `json:"skipped"` Skipped int `json:"skipped"`
} }
// ProvisionConfigured reports whether configs/remnawave-api.json exists — // ProvisionConfigured reports whether the Remnawave admin API is available
// the admin "Выдать доступ" panel is shown only in that case. // (embedded credentials or configs/remnawave-api.json). Used for renew UI.
func (m *Manager) ProvisionConfigured() bool { func (m *Manager) ProvisionConfigured() bool {
if remnawave.EmbeddedAdminConfig() != nil {
return true
}
st, err := os.Stat(remnawave.AdminConfigPath(m.cfgPath)) st, err := os.Stat(remnawave.AdminConfigPath(m.cfgPath))
return err == nil && !st.IsDir() return err == nil && !st.IsDir()
} }
@@ -735,10 +738,46 @@ func (m *Manager) ProvisionAccess(username string) (ProvisionOutcome, error) {
if acc := m.Account(); acc != nil { if acc := m.Account(); acc != nil {
return ProvisionOutcome{}, fmt.Errorf("аккаунт уже привязан (%s) — новый создавать нельзя, используйте «Продлить»", acc.Username) return ProvisionOutcome{}, fmt.Errorf("аккаунт уже привязан (%s) — новый создавать нельзя, используйте «Продлить»", acc.Username)
} }
cfg, err := remnawave.LoadAdminConfig(remnawave.AdminConfigPath(m.cfgPath)) cfg, err := remnawave.ResolveAdminConfig(m.cfgPath)
if err != nil { if err != nil {
return ProvisionOutcome{}, err return ProvisionOutcome{}, err
} }
return m.provisionWithConfig(cfg, username)
}
// EnsureAutoProvision silently creates a panel user (if none remembered yet)
// and imports its subscription configs. NordVPN-style: panel address / API
// token are embedded; the user never edits them. Safe to call on every start.
func (m *Manager) EnsureAutoProvision() (ProvisionOutcome, error) {
if acc := m.Account(); acc != nil {
// Already provisioned — refresh configs in the background if empty.
if len(m.Profiles()) == 0 && acc.SubscriptionURL != "" {
if imp, err := m.ImportSubscription(acc.SubscriptionURL); err == nil {
return ProvisionOutcome{
Username: acc.Username,
SubscriptionURL: acc.SubscriptionURL,
TrafficLimitBytes: acc.TrafficLimitBytes,
ExpireAt: acc.ExpireAt,
Imported: imp.Imported,
Skipped: imp.Skipped,
}, nil
}
}
return ProvisionOutcome{
Username: acc.Username,
SubscriptionURL: acc.SubscriptionURL,
TrafficLimitBytes: acc.TrafficLimitBytes,
ExpireAt: acc.ExpireAt,
}, nil
}
cfg, err := remnawave.ResolveAdminConfig(m.cfgPath)
if err != nil {
return ProvisionOutcome{}, err
}
return m.provisionWithConfig(cfg, remnawave.GenerateUsername())
}
func (m *Manager) provisionWithConfig(cfg *remnawave.AdminConfig, username string) (ProvisionOutcome, error) {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel() defer cancel()
res, err := remnawave.ProvisionUser(ctx, cfg, username) res, err := remnawave.ProvisionUser(ctx, cfg, username)
@@ -791,7 +830,7 @@ func (m *Manager) RenewAccount() (ProvisionOutcome, error) {
if acc == nil { if acc == nil {
return ProvisionOutcome{}, fmt.Errorf("нет привязанного аккаунта — сначала выдайте доступ") return ProvisionOutcome{}, fmt.Errorf("нет привязанного аккаунта — сначала выдайте доступ")
} }
cfg, err := remnawave.LoadAdminConfig(remnawave.AdminConfigPath(m.cfgPath)) cfg, err := remnawave.ResolveAdminConfig(m.cfgPath)
if err != nil { if err != nil {
return ProvisionOutcome{}, err return ProvisionOutcome{}, err
} }
+1 -1
View File
@@ -183,7 +183,7 @@ func doGET(ctx context.Context, client *http.Client, s Settings, endpoint string
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
req.Header.Set("User-Agent", "Navis/3.9 (Remnawave)") req.Header.Set("User-Agent", "EvilFox/4.0 (Remnawave)")
req.Header.Set("Accept", "*/*") req.Header.Set("Accept", "*/*")
if auth && s.Token != "" { if auth && s.Token != "" {
req.Header.Set("Authorization", "Bearer "+s.Token) req.Header.Set("Authorization", "Bearer "+s.Token)
+51
View File
@@ -0,0 +1,51 @@
package remnawave
// Centralized (NordVPN-style) provisioning: the panel address and API token
// are baked into the binary at build time, so every user gets configs
// automatically and cannot change the panel.
//
// build.bat copies configs/remnawave-api.json into embeddedcfg/ before
// `go build`; the copy is gitignored so credentials never land in the repo.
// Without the embedded file the app falls back to the external
// configs/remnawave-api.json (developer mode).
import (
"crypto/rand"
"embed"
"encoding/hex"
"fmt"
"os"
"time"
)
//go:embed embeddedcfg
var embeddedCfgFS embed.FS
// EmbeddedAdminConfig returns the admin config baked into the binary,
// or nil when this build has no embedded credentials.
func EmbeddedAdminConfig() *AdminConfig {
data, err := embeddedCfgFS.ReadFile("embeddedcfg/remnawave-api.json")
if err != nil {
return nil
}
cfg, err := parseAdminConfig(data)
if err != nil {
return nil
}
return cfg
}
// GenerateUsername makes a unique panel username for silent auto-provisioning
// (users never type anything): "fox_" + 10 hex chars, e.g. fox_3fa9c02b71.
func GenerateUsername() string {
var b [5]byte
if _, err := rand.Read(b[:]); err != nil {
// Keep within Remnawave's 634 char username limit.
s := fmt.Sprintf("fox_%x%x", os.Getpid()&0xffff, time.Now().UnixNano()&0xffffffff)
if len(s) > 34 {
s = s[:34]
}
return s
}
return "fox_" + hex.EncodeToString(b[:])
}
@@ -0,0 +1,3 @@
{
"_comment": "build.bat overwrites remnawave-api.json here from configs/ before go build. This placeholder keeps the embed directory non-empty for go build without credentials."
}
+15 -1
View File
@@ -47,12 +47,26 @@ func AdminConfigPath(cfgPath string) string {
return filepath.Join(filepath.Dir(cfgPath), "remnawave-api.json") return filepath.Join(filepath.Dir(cfgPath), "remnawave-api.json")
} }
// ResolveAdminConfig prefers the baked-in (embedded) panel credentials;
// falls back to configs/remnawave-api.json for local/developer builds.
func ResolveAdminConfig(cfgPath string) (*AdminConfig, error) {
if cfg := EmbeddedAdminConfig(); cfg != nil {
return cfg, nil
}
return LoadAdminConfig(AdminConfigPath(cfgPath))
}
// LoadAdminConfig reads and validates the admin API config file. // LoadAdminConfig reads and validates the admin API config file.
func LoadAdminConfig(path string) (*AdminConfig, error) { func LoadAdminConfig(path string) (*AdminConfig, error) {
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
return nil, fmt.Errorf("нет файла %s (скопируйте remnawave-api.example.json и вставьте ключ)", filepath.Base(path)) return nil, fmt.Errorf("нет файла %s (скопируйте remnawave-api.example.json и вставьте ключ)", filepath.Base(path))
} }
return parseAdminConfig(data)
}
// parseAdminConfig validates raw remnawave-api.json bytes (file or embedded).
func parseAdminConfig(data []byte) (*AdminConfig, error) {
var cfg AdminConfig var cfg AdminConfig
if err := json.Unmarshal(bytes.TrimPrefix(data, []byte{0xEF, 0xBB, 0xBF}), &cfg); err != nil { if err := json.Unmarshal(bytes.TrimPrefix(data, []byte{0xEF, 0xBB, 0xBF}), &cfg); err != nil {
return nil, fmt.Errorf("remnawave-api.json: некорректный JSON: %w", err) return nil, fmt.Errorf("remnawave-api.json: некорректный JSON: %w", err)
@@ -252,7 +266,7 @@ func adminDo(ctx context.Context, client *http.Client, cfg *AdminConfig, method,
if err != nil { if err != nil {
return 0, "", err return 0, "", err
} }
req.Header.Set("User-Agent", "Navis/3.10 (Remnawave provision)") req.Header.Set("User-Agent", "EvilFox/4.0 (Remnawave provision)")
req.Header.Set("Accept", "application/json") req.Header.Set("Accept", "application/json")
if body != nil { if body != nil {
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
+1 -1
View File
@@ -57,7 +57,7 @@ var (
// for a generic UA, but return a Clash YAML config for Clash-like UAs — that // for a generic UA, but return a Clash YAML config for Clash-like UAs — that
// YAML loses vless/trojan entries for us, so try the plain UA first. // YAML loses vless/trojan entries for us, so try the plain UA first.
var userAgents = []string{ var userAgents = []string{
"Navis/3.9 (+https://evilfox.win)", "EvilFox/4.0 (+https://evilfox.win)",
"ClashMeta/1.18.0", "ClashMeta/1.18.0",
} }
+4 -4
View File
@@ -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.10.0" const CurrentVersion = "4.0.0"
// 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.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/update.json" const DefaultManifestURL = "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/update.json"
@@ -287,7 +287,7 @@ func fetchManifest(ctx context.Context, manifestURL string) (Manifest, error) {
if err != nil { if err != nil {
return Manifest{}, err return Manifest{}, err
} }
req.Header.Set("User-Agent", "Navis/"+DisplayVersion()) req.Header.Set("User-Agent", "EvilFox/"+DisplayVersion())
req.Header.Set("Accept", "application/json") req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 20 * time.Second} client := &http.Client{Timeout: 20 * time.Second}
resp, err := client.Do(req) resp, err := client.Do(req)
@@ -310,7 +310,7 @@ func downloadFile(ctx context.Context, url, dest string) error {
if err != nil { if err != nil {
return err return err
} }
req.Header.Set("User-Agent", "Navis/"+DisplayVersion()) req.Header.Set("User-Agent", "EvilFox/"+DisplayVersion())
client := &http.Client{Timeout: 10 * time.Minute} client := &http.Client{Timeout: 10 * time.Minute}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
+7 -3
View File
@@ -16,9 +16,12 @@ import (
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
// finishUpdateFlag must stay "--navis-finish-update" forever: older installed
// builds (3.x) launch the freshly downloaded binary with exactly this flag,
// so renaming it would break their update handoff.
const finishUpdateFlag = "--navis-finish-update" const finishUpdateFlag = "--navis-finish-update"
// Apply downloads the new build and replaces the running Navis.exe after this process exits. // Apply downloads the new build and replaces the running EvilFox.exe after this process exits.
func Apply(ctx context.Context, manifestURL string) (string, error) { func Apply(ctx context.Context, manifestURL string) (string, error) {
if IsStorePackaged() { if IsStorePackaged() {
return "", fmt.Errorf("обновления устанавливаются через Microsoft Store") return "", fmt.Errorf("обновления устанавливаются через Microsoft Store")
@@ -27,7 +30,7 @@ func Apply(ctx context.Context, manifestURL string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
pending := filepath.Join(filepath.Dir(exe), "Navis-pending.exe") pending := filepath.Join(filepath.Dir(exe), "EvilFox-pending.exe")
_ = os.Remove(pending) _ = os.Remove(pending)
if err := os.Rename(tmp, pending); err != nil { if err := os.Rename(tmp, pending); err != nil {
if err2 := copyFile(tmp, pending); err2 != nil { if err2 := copyFile(tmp, pending); err2 != nil {
@@ -58,7 +61,7 @@ func Apply(ctx context.Context, manifestURL string) (string, error) {
return latest, nil return latest, nil
} }
// MaybeFinishUpdate handles: Navis-pending.exe --navis-finish-update <pid> <targetExe> // MaybeFinishUpdate handles: EvilFox-pending.exe --navis-finish-update <pid> <targetExe>
// Replaces targetExe in-place with this binary, then relaunches it. // Replaces targetExe in-place with this binary, then relaunches it.
func MaybeFinishUpdate(args []string) bool { func MaybeFinishUpdate(args []string) bool {
if len(args) < 3 || args[0] != finishUpdateFlag { if len(args) < 3 || args[0] != finishUpdateFlag {
@@ -109,6 +112,7 @@ func CleanupStaleDownloads() {
_ = os.Remove(filepath.Join(dir, base+".new")) _ = os.Remove(filepath.Join(dir, base+".new"))
_ = os.Remove(filepath.Join(dir, "Navis-update.part")) _ = os.Remove(filepath.Join(dir, "Navis-update.part"))
_ = os.Remove(filepath.Join(dir, ".navis-update-download")) _ = os.Remove(filepath.Join(dir, ".navis-update-download"))
_ = os.Remove(filepath.Join(dir, "Navis-pending.exe"))
} }
func replaceExecutable(src, dst string) error { func replaceExecutable(src, dst string) error {
+2 -2
View File
@@ -86,10 +86,10 @@ func Start(cfg Config) (*Session, error) {
return nil, fmt.Errorf("режим VPN: нет локального SOCKS-прокси у текущего протокола") return nil, fmt.Errorf("режим VPN: нет локального SOCKS-прокси у текущего протокола")
} }
if !IsElevated() { if !IsElevated() {
return nil, fmt.Errorf("Запустите Navis от имени администратора для режима VPN") return nil, fmt.Errorf("Запустите EvilFox от имени администратора для режима VPN")
} }
if !WintunAvailable() { if !WintunAvailable() {
return nil, fmt.Errorf("не найден wintun.dll — скачайте с wintun.net и положите рядом с Navis.exe") return nil, fmt.Errorf("не найден wintun.dll — скачайте с wintun.net и положите рядом с EvilFox.exe")
} }
if len(cfg.DNSServers) == 0 { if len(cfg.DNSServers) == 0 {
cfg.DNSServers = []string{"1.1.1.1", "8.8.8.8"} cfg.DNSServers = []string{"1.1.1.1", "8.8.8.8"}
+12 -12
View File
@@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
Navis MSIX package manifest (Microsoft Store / sideload). EvilFox MSIX package manifest (Microsoft Store / sideload).
WHY MSIX (not PWA): WHY MSIX (not PWA):
Navis is a Win32 + WebView2 desktop VPN/proxy client (system proxy, local cores, EvilFox is a Win32 + WebView2 desktop VPN/proxy client (system proxy, local cores,
Naive/Hy2/AWG/Xray). A PWA cannot replace those capabilities. Package the existing Naive/Hy2/AWG/Xray). A PWA cannot replace those capabilities. Package the existing
GUI binary with Desktop Bridge / full-trust MSIX for Store distribution. GUI binary with Desktop Bridge / full-trust MSIX for Store distribution.
IDENTITY PLACEHOLDERS — replace before Store submission: IDENTITY PLACEHOLDERS — replace before Store submission:
Identity/@Name → Partner Center "Package/Identity/Name" (e.g. EvilFox.Navis) Identity/@Name → Partner Center "Package/Identity/Name" (e.g. EvilFox.EvilFox)
Identity/@Publisher → exact Publisher CN from your Store certificate Identity/@Publisher → exact Publisher CN from your Store certificate
(e.g. CN=EvilFox LLC or CN=A1B2C3D4-…) (e.g. CN=EvilFox LLC or CN=A1B2C3D4-…)
Identity/@Version → Major.Minor.Build.Revision (all numeric; Revision often 0) Identity/@Version → Major.Minor.Build.Revision (all numeric; Revision often 0)
@@ -31,15 +31,15 @@
IgnorableNamespaces="uap rescap"> IgnorableNamespaces="uap rescap">
<Identity <Identity
Name="EvilFox.Navis" Name="EvilFox.EvilFox"
Publisher="CN=EvilFox" Publisher="CN=EvilFox"
Version="3.10.0.2" Version="4.0.0.1"
ProcessorArchitecture="x64" /> ProcessorArchitecture="x64" />
<Properties> <Properties>
<DisplayName>Navis</DisplayName> <DisplayName>EvilFox</DisplayName>
<PublisherDisplayName>EvilFox</PublisherDisplayName> <PublisherDisplayName>EvilFox</PublisherDisplayName>
<Description>Navis — VPN/proxy client (Naive, Hysteria 2, AmneziaWG, Xray)</Description> <Description>EvilFox — VPN/proxy client (Naive, Hysteria 2, AmneziaWG, Xray)</Description>
<Logo>Assets\StoreLogo.png</Logo> <Logo>Assets\StoreLogo.png</Logo>
</Properties> </Properties>
@@ -61,12 +61,12 @@
<Applications> <Applications>
<Application <Application
Id="Navis" Id="EvilFox"
Executable="Navis.exe" Executable="EvilFox.exe"
EntryPoint="Windows.FullTrustApplication"> EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements <uap:VisualElements
DisplayName="Navis" DisplayName="EvilFox"
Description="Navis VPN client" Description="EvilFox VPN client"
BackgroundColor="transparent" BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png" Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png"> Square44x44Logo="Assets\Square44x44Logo.png">
@@ -74,7 +74,7 @@
Wide310x150Logo="Assets\Wide310x150Logo.png" Wide310x150Logo="Assets\Wide310x150Logo.png"
Square71x71Logo="Assets\Square71x71Logo.png" Square71x71Logo="Assets\Square71x71Logo.png"
Square310x310Logo="Assets\Square310x310Logo.png" Square310x310Logo="Assets\Square310x310Logo.png"
ShortName="Navis" /> ShortName="EvilFox" />
<uap:SplashScreen Image="Assets\SplashScreen.png" BackgroundColor="#0c1613" /> <uap:SplashScreen Image="Assets\SplashScreen.png" BackgroundColor="#0c1613" />
</uap:VisualElements> </uap:VisualElements>
</Application> </Application>
+15 -15
View File
@@ -1,24 +1,24 @@
#Requires -Version 5.1 #Requires -Version 5.1
<# <#
.SYNOPSIS .SYNOPSIS
Pack Navis.exe into an MSIX for Microsoft Store / sideload. Pack EvilFox.exe into an MSIX for Microsoft Store / sideload.
.DESCRIPTION .DESCRIPTION
Builds Store assets, stages AppxManifest + Navis.exe, runs MakeAppx. Builds Store assets, stages AppxManifest + EvilFox.exe, runs MakeAppx.
Prefers MSIX over PWA: Navis needs Win32/WebView2 VPN and system-proxy access. Prefers MSIX over PWA: EvilFox needs Win32/WebView2 VPN and system-proxy access.
.EXAMPLE .EXAMPLE
.\scripts\pack-msix.ps1 .\scripts\pack-msix.ps1
.\scripts\pack-msix.ps1 -Version 3.9.0.1 -Publisher "CN=EvilFox" .\scripts\pack-msix.ps1 -Version 4.0.0.1 -Publisher "CN=EvilFox"
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(
[string]$ExePath = "", [string]$ExePath = "",
[string]$OutDir = "", [string]$OutDir = "",
[string]$Version = "3.10.0.2", [string]$Version = "4.0.0.1",
[string]$Name = "EvilFox.Navis", [string]$Name = "EvilFox.EvilFox",
[string]$Publisher = "CN=EvilFox", [string]$Publisher = "CN=EvilFox",
[string]$DisplayName = "Navis", [string]$DisplayName = "EvilFox",
[string]$PublisherDisplayName = "EvilFox", [string]$PublisherDisplayName = "EvilFox",
[string]$CertPath = "", [string]$CertPath = "",
[string]$CertPassword = "", [string]$CertPassword = "",
@@ -30,14 +30,14 @@ $Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
Set-Location $Root Set-Location $Root
if (-not $ExePath) { if (-not $ExePath) {
$ExePath = Join-Path $Root "dist\navis-release\windows\Navis.exe" $ExePath = Join-Path $Root "dist\navis-release\windows\EvilFox.exe"
} }
if (-not $OutDir) { if (-not $OutDir) {
$OutDir = Join-Path $Root "dist\navis-release\windows" $OutDir = Join-Path $Root "dist\navis-release\windows"
} }
if (-not (Test-Path -LiteralPath $ExePath)) { if (-not (Test-Path -LiteralPath $ExePath)) {
throw "Navis.exe not found at '$ExePath'. Run build.bat first." throw "EvilFox.exe not found at '$ExePath'. Run build.bat first."
} }
function Find-SdkTool([string]$name) { function Find-SdkTool([string]$name) {
@@ -74,22 +74,22 @@ New-Item -ItemType Directory -Path (Join-Path $Staging "Assets") | Out-Null
$AssetsSrc = Join-Path $Root "packaging\msix\Assets" $AssetsSrc = Join-Path $Root "packaging\msix\Assets"
Copy-Item -Path (Join-Path $AssetsSrc "*") -Destination (Join-Path $Staging "Assets") -Force Copy-Item -Path (Join-Path $AssetsSrc "*") -Destination (Join-Path $Staging "Assets") -Force
Copy-Item -LiteralPath $ExePath -Destination (Join-Path $Staging "Navis.exe") -Force Copy-Item -LiteralPath $ExePath -Destination (Join-Path $Staging "EvilFox.exe") -Force
$ManifestSrc = Join-Path $Root "packaging\msix\AppxManifest.xml" $ManifestSrc = Join-Path $Root "packaging\msix\AppxManifest.xml"
$ManifestDst = Join-Path $Staging "AppxManifest.xml" $ManifestDst = Join-Path $Staging "AppxManifest.xml"
$manifestText = Get-Content -LiteralPath $ManifestSrc -Raw -Encoding UTF8 $manifestText = Get-Content -LiteralPath $ManifestSrc -Raw -Encoding UTF8
# Prefer exact placeholder swaps so we never touch <?xml version=...?>. # Prefer exact placeholder swaps so we never touch <?xml version=...?>.
$manifestText = $manifestText.Replace('Name="EvilFox.Navis"', "Name=`"$Name`"") $manifestText = $manifestText.Replace('Name="EvilFox.EvilFox"', "Name=`"$Name`"")
$manifestText = $manifestText.Replace('Publisher="CN=EvilFox"', "Publisher=`"$Publisher`"") $manifestText = $manifestText.Replace('Publisher="CN=EvilFox"', "Publisher=`"$Publisher`"")
$manifestText = $manifestText.Replace('Version="3.10.0.2"', "Version=`"$Version`"") $manifestText = $manifestText.Replace('Version="4.0.0.1"', "Version=`"$Version`"")
$manifestText = $manifestText.Replace('<DisplayName>Navis</DisplayName>', "<DisplayName>$DisplayName</DisplayName>") $manifestText = $manifestText.Replace('<DisplayName>EvilFox</DisplayName>', "<DisplayName>$DisplayName</DisplayName>")
$manifestText = $manifestText.Replace('<PublisherDisplayName>EvilFox</PublisherDisplayName>', "<PublisherDisplayName>$PublisherDisplayName</PublisherDisplayName>") $manifestText = $manifestText.Replace('<PublisherDisplayName>EvilFox</PublisherDisplayName>', "<PublisherDisplayName>$PublisherDisplayName</PublisherDisplayName>")
$manifestText = $manifestText.Replace('DisplayName="Navis"', "DisplayName=`"$DisplayName`"") $manifestText = $manifestText.Replace('DisplayName="EvilFox"', "DisplayName=`"$DisplayName`"")
[System.IO.File]::WriteAllText($ManifestDst, $manifestText, (New-Object System.Text.UTF8Encoding $false)) [System.IO.File]::WriteAllText($ManifestDst, $manifestText, (New-Object System.Text.UTF8Encoding $false))
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
$MsixPath = Join-Path $OutDir ("Navis_" + $Version + ".msix") $MsixPath = Join-Path $OutDir ("EvilFox_" + $Version + ".msix")
if (Test-Path $MsixPath) { Remove-Item -Force $MsixPath } if (Test-Path $MsixPath) { Remove-Item -Force $MsixPath }
if (-not $MakeAppx) { if (-not $MakeAppx) {
+6 -6
View File
@@ -1,16 +1,16 @@
{ {
"version": "3.10.0", "version": "4.0.0",
"notes": "3.10.0+2: новый интерфейс с боковым меню (Главная / Профили / Настройки / Логи / О программе), аккаунт с продлением 50 ГБ / +30 дней и автопродлением, режим VPN на Windows (TUN, весь трафик, нужны права администратора и wintun.dll). Обновления теперь с git.de4ima.uk. Windows 3.10.0.2.", "notes": "4.0.0+1: ребрендинг Navis → EvilFox; централизованная автовыдача конфигов (как NordVPN — панель вшита, пользователь только выбирает сервер и продлевает); из интерфейса убраны настройки Remnawave API. Windows 4.0.0.1. Клиенты 3.x обновляются по этому фиду; Navis.exe в релизе — совместимый алиас EvilFox.exe.",
"platform": "windows-amd64", "platform": "windows-amd64",
"os": "windows", "os": "windows",
"arch": "amd64", "arch": "amd64",
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/Navis.exe", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
"sha256": "55ba57898ffc23ce1cb14172643513f70a2ae972c00181b2d3d889430bce1eb3", "sha256": "869b37310526003143c5db8ea3ad6d4468173e9053cf585f06c162a861d5c49c",
"mandatory": false, "mandatory": false,
"platforms": { "platforms": {
"windows-amd64": { "windows-amd64": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/Navis.exe", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
"sha256": "55ba57898ffc23ce1cb14172643513f70a2ae972c00181b2d3d889430bce1eb3", "sha256": "869b37310526003143c5db8ea3ad6d4468173e9053cf585f06c162a861d5c49c",
"os": "windows", "os": "windows",
"arch": "amd64" "arch": "amd64"
}, },
+8 -8
View File
@@ -1,7 +1,7 @@
{ {
"FixedFileInfo": { "FixedFileInfo": {
"FileVersion": { "Major": 3, "Minor": 10, "Patch": 0, "Build": 2 }, "FileVersion": { "Major": 4, "Minor": 0, "Patch": 0, "Build": 1 },
"ProductVersion": { "Major": 3, "Minor": 10, "Patch": 0, "Build": 2 }, "ProductVersion": { "Major": 4, "Minor": 0, "Patch": 0, "Build": 1 },
"FileFlagsMask": "3f", "FileFlagsMask": "3f",
"FileFlags": "00", "FileFlags": "00",
"FileOS": "40004", "FileOS": "40004",
@@ -10,13 +10,13 @@
}, },
"StringFileInfo": { "StringFileInfo": {
"CompanyName": "EvilFox", "CompanyName": "EvilFox",
"FileDescription": "Navis 2 — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)", "FileDescription": "EvilFox — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
"FileVersion": "3.10.0.2", "FileVersion": "4.0.0.1",
"InternalName": "Navis", "InternalName": "EvilFox",
"LegalCopyright": "Copyright (c) EvilFox", "LegalCopyright": "Copyright (c) EvilFox",
"OriginalFilename": "Navis.exe", "OriginalFilename": "EvilFox.exe",
"ProductName": "Navis", "ProductName": "EvilFox",
"ProductVersion": "3.10.0.2", "ProductVersion": "4.0.0.1",
"Comments": "Open-source VPN/proxy client. https://evilfox.win/" "Comments": "Open-source VPN/proxy client. https://evilfox.win/"
}, },
"VarFileInfo": { "VarFileInfo": {