Author SHA1 Message Date
NavisandCursor 8ebbc90ef6 Pack Microsoft Store 4.2.9.0 MSIX (x64+arm64 bundle) and refresh WhatsNew.
Identity Version stays .0 for Partner Center; upload EvilFox_4.2.9.0.msixbundle from dist/microsoft-store.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 15:55:04 +03:00
NavisandCursor 42abbfa8ad Release 4.2.9+1: Windows builds accept AWG 2.0 and Naive pastes in Add config.
Rebuild amd64/arm64 EvilFox.exe and refresh update.json SHA so the Windows feed serves the fixed importer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 15:47:46 +03:00
NavisandCursor 50f7f71393 Add site contacts/FAQ and accept AWG 2.0 / naive pastes in Add config.
ImportSubscription no longer requires only http(s): paste awg://, AWG .conf/JSON, or naive links; subscription parser also reads hosted AWG and Clash wireguard/naive. Site lists support emails and FAQ; Store listing texts/tools updated.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 15:40:13 +03:00
NavisandCursor e416327fbf Add Microsoft Store multi-arch MSIX packaging for EvilFox 4.2.8.1 (x64+Arm64).
Stage Partner Center folder with listing assets and Arm64 binary; MakeAppx not present so README includes pack/bundle commands.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 14:06:24 +03:00
NavisandCursor 6ed698e0a0 Release 4.2.8+1: Store-ready Privacy Policy and Terms of Use on Help (RU/EN) plus docs for website hosting. Windows 4.2.8.1.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 13:36:41 +03:00
NavisandCursor c370e089ef Fix 4.2.7+1: restore UTF-8 in update.go (em-dash/RU strings) and rebuild Windows binaries + feed SHA.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 12:52:05 +03:00
NavisandCursor fa3e11216e Release 4.2.7+1: adaptive HiDPI window sizing (work-area DIPs + min size) and responsive CSS so the UI opens usable without stretch. Windows 4.2.7.1.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 12:00:10 +03:00
NavisandCursor a1756715ef Release 4.2.6+1: fix sidebar RU/EN and theme toggles — applyTheme param shadowed i18n t(), so init threw and click listeners never bound. Windows 4.2.6.1.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 10:21:52 +03:00
NavisandCursor e633b63a9b Release 4.2.5+1: RU/EN language switch in UI (navis.lang, default Russian); translate static and dynamic strings including server titles and country names. Windows 4.2.5.1.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 10:07:12 +03:00
NavisandCursor 6d52913c32 Release 4.2.4+1: group same-host profiles into one server row with protocol chips (VLESS · Trojan · Hysteria2) instead of «сервер 2/3»; оптимальный among distinct hosts. Windows 4.2.4.1.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 09:40:17 +03:00
NavisandCursor 0ba662c650 Release 4.2.3+1: на вкладке «Защита» только карточка EvilFox — убраны NordVPN/ExpressVPN/Surfshark и их openURL-хосты. Windows 4.2.3.1.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 09:15:48 +03:00
NavisandCursor 19711c3a33 Release 4.2.2+1: светлая тема по умолчанию (без flash dark), Telegram поддержки → t.me/evilfox100Bot. Windows 4.2.2.1.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 23:32:09 +03:00
NavisandCursor 497af0e475 Release 4.2.1+1: UI polish — опечатки Румынии→RO+флаг, чистая шапка «Серверы» без +/×, «Полезные ссылки» (сайт+Telegram), убран мусор «СПЕЦИАЛЬНЫЕ СЕРВИСИ», sticky кнопка ПОДКЛЮЧИТЬСЯ, свойства сервера заполнены, без T/V в списке, оффер «3 дня бесплатного доступа». Windows 4.2.1.1.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 23:08:28 +03:00
NavisandCursor 2a57987aea Release 4.2.0+1: store-compliance формулировки и UX — убраны все цены/продажи из приложения (оффер «3 дня бесплатно» без ₽), «Активировать ключ» → «Добавить конфигурацию» (URL-ссылка, placeholder https://), блок «Ключ доступа» заменён статусом «Конфигурация добавлена/не добавлена» (URL не отображается), глобально Подписка→Конфигурация/Ваучер→URL-ссылка/Тариф→Профиль в UI и Go-строках; большая круглая кнопка ПОДКЛЮЧИТЬСЯ/ОТКЛЮЧИТЬСЯ со статусом, убран футер «Готово к подключению», протокол в списке — буквенный бейдж только при отличии от активного, трафик «ДОСТУПНО X ГБ» + «Активен до DD.MM.YYYY» + процент; пинг отдельного сервера кнопкой в строке, экран Статистика (за сегодня: трафик/время в сети/сессии + график 7 дней из localStorage), Kill Switch для VPN-режима (blackhole-маршруты сохраняются при обрыве туннеля, для прокси — системный прокси остаётся на мёртвый порт), тумблер «Авто-лучший сервер» в Настройках. Windows 4.2.0.1.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 22:34:43 +03:00
NavisandCursor 74ffbd87d7 Release 4.1.2+1: настоящие SVG-флаги стран — Windows/WebView2 не отображает эмодзи-флаги (Segoe UI Emoji рисует пары региональных индикаторов буквами «BE», «LV»), поэтому встроен офлайн-генератор SVG-флагов (~75 стран: полосы/кресты + упрощённые сложные флаги GB/US/CH/KR и др., ~6 КБ в index.html); флаги в списке серверов и на карточке активного сервера, буквенный код — только для неизвестных регионов, обе темы. Windows 4.1.2.1.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 22:04:18 +03:00
NavisandCursor 46e28aa73f Release 4.1.1+1: UI polish по чек-листу ревью — трафик «Доступно X ГБ» + процент использования (баг «Осталось 100 из 100 · 0%»), оффер «3 дня бесплатно, затем от 99 ₽/мес», бейдж «Рекомендуем» на карточке EvilFox + подпись «Сторонний сервис» у конкурентов, пилюля «АКТИВЕН» и протокол мелким текстом в списке серверов, «—» вместо пустого пинга, счётчик серверов только на «Мои серверы», 🦊 вместо буквы N (старый логотип Navis), флаги во всех строках, geoflag-алиасы опечаток («Румьиня» → RO), фикс [hidden] vs display:grid. Windows 4.1.1.1.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 21:39:37 +03:00
orohimaru2andCursor 329209fa88 Release 4.1.0+1: Store-compliance UI rework — tabs Защита/Мои серверы/Настройки/Статистика/Помощь; provider chooser (EvilFox highlighted + NordVPN/ExpressVPN/Surfshark, clean links); «Активировать ключ» wording; human-readable server names (host:port in server properties only); proxy ports moved to advanced settings/stats; traffic progress bar on main; account info on Мои серверы; connect spinner; ping test buttons. Windows 4.1.0.1.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 20:55:40 +03:00
NavisandCursor 5f768c7732 Release 4.0.1+1: remove account binding / centralized provisioning (no panel credentials in binary; configs via user's own subscription URL); add recommended-services block on About page; Windows 4.0.1.1.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 20:31:29 +03:00
Navis 1144fab54d 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.
2026-08-01 20:06:25 +03:00
NavisandCursor e76c0a0977 Release 3.10.0+2: switch update feed and asset URLs to git.de4ima.uk/Evilfox/navi (branch Windows); Windows 3.10.0.2 rebuild with new embedded feed URL
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 19:36:19 +03:00
NavisandCursor 01fac376ba Release 3.10.0+1: sidebar UI (Home / Profiles / Settings / Logs / About), remembered account with 50 GB monthly renew + auto-renew, Windows VPN mode (wintun + tun2socks).
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 18:34:25 +03:00
NavisandCursor 77bd7da861 Release 3.9.0+3: subscription-URL routing, emoji flags, Remnawave provisioning.
Route credential-less http(s) URLs pasted into the add-link field to
subscription import (fixes remaining 'proxy URI missing username').
Extend geoflag with RU country names and city hints; live Remnawave
names already carrying emoji flags are kept as-is. Add admin
provisioning via configs/remnawave-api.json (GET by-username / POST
users, 50 GB MONTH plan) and the «Выдать доступ» UI panel.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 17:36:54 +03:00
NavisandCursor e34312ef9c Release 3.9.0+2: tolerant subscription import, full Remnawave server list, subscription info card (expiry / traffic / devices).
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 17:01:37 +03:00
NavisandCursor 015ded9bd5 Release 3.9.0+1: Remnawave API configs, Remnawave-style UI with flags, Windows build.
EOF

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 16:17:10 +03:00
115 changed files with 10637 additions and 1185 deletions
+14
View File
@@ -4,6 +4,12 @@ bin/
!dist/**/*.exe !dist/**/*.exe
!dist/**/ !dist/**/
configs/config.json configs/config.json
configs/remnawave-api.json
configs/account.json
dist/navis-release/windows/configs/
# Runtime-downloaded protocol cores + local packaging artifact (not release sources)
dist/navis-release/windows/bin/
dist/navis-release/navis-release.zip
configs/.navis-webview/ configs/.navis-webview/
cmd/vpnapp/resource.syso cmd/vpnapp/resource.syso
*.syso *.syso
@@ -11,13 +17,21 @@ 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/
packaging/msix/_staging_*/
packaging/msix/Assets/ packaging/msix/Assets/
*.msix *.msix
*.msixbundle
dist/microsoft-store/*.msix
dist/microsoft-store/*.msixbundle
dist/microsoft-store/_bundle_input/
# Android # Android
android/.gradle/ android/.gradle/
+24 -4
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-подпиской и автообновлением. Клиент [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) |
@@ -210,13 +212,26 @@ Navis — Win32 + WebView2 VPN/прокси-клиент (системный п
```powershell ```powershell
build.bat build.bat
.\scripts\pack-msix.ps1 .\scripts\pack-msix.ps1 -Version 3.9.0.1
``` ```
Нужен Windows SDK (`MakeAppx.exe`). Перед публикацией подставьте Identity Name / Publisher из Partner Center (см. `packaging/msix/AppxManifest.xml` и [packaging/msix/README.md](packaging/msix/README.md)). Нужен Windows SDK (`MakeAppx.exe`). Перед публикацией подставьте Identity Name / Publisher из Partner Center (см. `packaging/msix/AppxManifest.xml` и [packaging/msix/README.md](packaging/msix/README.md)).
Сборка из Store **не** самообновляется через in-app updater (детект MSIX package identity) — только обновления Microsoft Store. Сборка из Store **не** самообновляется через in-app updater (детект MSIX package identity) — только обновления Microsoft Store.
### Remnawave
Импорт нод из панели [Remnawave](https://remna.st):
| Способ | Что указать |
|--------|-------------|
| Публичная подписка | `https://<panel>/api/sub/<shortUuid>` в поле «URL подписки» |
| Панель + short UUID | блок Remnawave API → URL панели + short UUID → «Синхронизировать» |
| С API token | токен из панели → API Tokens (для `/api/subscriptions/…`, `/api/users/…`) |
| Caddy auth | опциональный `X-Api-Key` |
Токены хранятся только локально в `configs/config.json` (не коммитьте).
## Сборка macOS (кросс с Windows / Linux) ## Сборка macOS (кросс с Windows / Linux)
```bat ```bat
@@ -270,6 +285,11 @@ 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.9.0:
- выдача конфигов через Remnawave API (`/api/sub/{shortUuid}`, опционально Bearer token);
- UI в стиле Remnawave (тёмная тема по умолчанию, cyan accent), флаги стран у серверов;
- MSIX / Store prep для `3.9.0.1`.
В 2.9.0: В 2.9.0:
- светлая / тёмная тема с переключателем (preference в localStorage, по умолчанию — системная); - светлая / тёмная тема с переключателем (preference в localStorage, по умолчанию — системная);
- подготовка к Microsoft Store: MSIX-манифест, `scripts/pack-msix.ps1`, отключение in-app updater в MSIX-сборке. - подготовка к Microsoft Store: MSIX-манифест, `scripts/pack-msix.ps1`, отключение in-app updater в MSIX-сборке.
+650
View File
@@ -0,0 +1,650 @@
<!DOCTYPE html>
<html lang="ru" data-lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>EvilFox — Apps</title>
<meta name="description" content="EvilFox — приложения для безопасного и удобного интернета. EvilFox VPN и другие продукты." />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@500;700;800&family=Manrope:wght@400;600;700&display=swap" rel="stylesheet" />
<style>
:root {
--bg0: #061018;
--bg1: #0a1c24;
--ink: #e8f4f7;
--muted: #8aa8b3;
--accent: #22d3ee;
--accent-deep: #0e7490;
--glow: rgba(34, 211, 238, 0.35);
--line: rgba(232, 244, 247, 0.12);
--panel: rgba(10, 28, 36, 0.55);
--ease: cubic-bezier(.22, 1, .36, 1);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
min-height: 100vh;
font-family: Manrope, system-ui, sans-serif;
color: var(--ink);
background: var(--bg0);
overflow-x: hidden;
}
/* Full-bleed atmospheric plane */
.stage {
position: relative;
min-height: 100vh;
isolation: isolate;
background:
radial-gradient(1200px 700px at 78% 18%, rgba(14, 116, 144, 0.45) 0%, transparent 58%),
radial-gradient(900px 600px at 12% 80%, rgba(8, 90, 110, 0.35) 0%, transparent 55%),
linear-gradient(165deg, #041015 0%, #0a1c24 48%, #07141a 100%);
}
.stage::before {
content: "";
position: absolute; inset: 0; z-index: -1; pointer-events: none;
background-image:
linear-gradient(rgba(232,244,247,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(232,244,247,0.03) 1px, transparent 1px);
background-size: 64px 64px;
mask-image: radial-gradient(ellipse 80% 70% at 50% 40%, #000 20%, transparent 75%);
animation: gridDrift 28s linear infinite;
}
@keyframes gridDrift {
from { transform: translateY(0); }
to { transform: translateY(64px); }
}
.orb {
position: absolute; border-radius: 50%; filter: blur(40px); pointer-events: none; z-index: -1;
animation: float 12s var(--ease) infinite alternate;
}
.orb-a { width: 380px; height: 380px; top: -8%; right: 8%; background: var(--glow); }
.orb-b { width: 260px; height: 260px; bottom: 12%; left: 6%; background: rgba(14,116,144,.4); animation-delay: -4s; }
@keyframes float {
from { transform: translate3d(0,0,0) scale(1); }
to { transform: translate3d(12px, -22px, 0) scale(1.06); }
}
.top {
display: flex; align-items: center; justify-content: space-between;
gap: 16px; padding: 22px clamp(20px, 4vw, 48px);
max-width: 1200px; margin: 0 auto;
}
.logo {
display: flex; align-items: center; gap: 12px; text-decoration: none; color: inherit;
}
.mark {
width: 42px; height: 42px; border-radius: 13px;
display: grid; place-items: center; font-size: 1.35rem;
background: linear-gradient(145deg, #0e7490, #22d3ee);
box-shadow: 0 12px 28px rgba(8, 145, 178, 0.35);
}
.word {
font-family: Outfit, sans-serif; font-weight: 800; font-size: 1.35rem;
letter-spacing: -0.045em; line-height: 1;
}
.lang {
display: inline-flex; padding: 4px; border-radius: 999px;
border: 1px solid var(--line); background: rgba(0,0,0,.25);
}
.lang button {
border: 0; background: transparent; color: var(--muted);
font: inherit; font-weight: 700; font-size: .8rem;
padding: 8px 14px; border-radius: 999px; cursor: pointer;
transition: background .2s, color .2s;
}
.lang button[aria-pressed="true"] {
background: rgba(34, 211, 238, 0.18); color: var(--accent);
}
/* Hero: brand-first, one composition */
.hero {
max-width: 1200px; margin: 0 auto;
padding: clamp(20px, 4vh, 48px) clamp(20px, 4vw, 48px) clamp(40px, 7vh, 72px);
display: grid;
grid-template-columns: 1.1fr 0.9fr;
gap: clamp(28px, 5vw, 64px);
align-items: center;
}
@media (max-width: 900px) {
.hero { grid-template-columns: 1fr; align-items: start; }
.visual { order: -1; min-height: 280px; }
}
.eyebrow {
display: inline-block; font-size: .75rem; font-weight: 700; letter-spacing: .12em;
text-transform: uppercase; color: var(--accent); margin: 0 0 14px;
opacity: 0; animation: rise .7s var(--ease) .05s forwards;
}
.brand-title {
font-family: Outfit, sans-serif;
font-size: clamp(3.2rem, 8vw, 5.6rem);
font-weight: 800; letter-spacing: -0.06em; line-height: .92;
margin: 0 0 18px;
opacity: 0; animation: rise .8s var(--ease) .12s forwards;
}
.lead {
max-width: 42ch; font-size: clamp(1.05rem, 2vw, 1.22rem);
color: var(--muted); margin: 0 0 14px; font-weight: 500;
opacity: 0; animation: rise .8s var(--ease) .22s forwards;
}
.desc {
max-width: 48ch; margin: 0 0 22px; color: rgba(232, 244, 247, 0.88);
font-size: .98rem; line-height: 1.55;
opacity: 0; animation: rise .8s var(--ease) .26s forwards;
}
.points {
list-style: none; margin: 0 0 26px; padding: 0;
display: grid; gap: 10px;
opacity: 0; animation: rise .8s var(--ease) .3s forwards;
}
.points li {
display: flex; gap: 10px; align-items: flex-start;
color: var(--muted); font-size: .92rem; line-height: 1.45;
}
.points li::before {
content: ""; flex: 0 0 8px; height: 8px; margin-top: .45em;
border-radius: 50%; background: var(--accent);
box-shadow: 0 0 0 4px rgba(34,211,238,.15);
}
.cta-row {
display: flex; flex-wrap: wrap; gap: 12px;
opacity: 0; animation: rise .8s var(--ease) .36s forwards;
}
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
text-decoration: none; font-weight: 700; font-size: .95rem;
padding: 14px 22px; border-radius: 14px; border: 1px solid transparent;
transition: transform .2s var(--ease), box-shadow .2s, background .2s;
}
.btn:hover { transform: translateY(-2px); }
.btn-primary {
color: #042028; background: linear-gradient(135deg, #67e8f9, #22d3ee);
box-shadow: 0 14px 36px rgba(34, 211, 238, 0.28);
}
.btn-ghost {
color: var(--ink); border-color: var(--line); background: rgba(255,255,255,.04);
}
@keyframes rise {
from { opacity: 0; transform: translateY(18px); }
to { opacity: 1; transform: translateY(0); }
}
.visual {
position: relative;
border-radius: 28px;
min-height: 420px;
background:
linear-gradient(160deg, rgba(34,211,238,.12), transparent 45%),
linear-gradient(20deg, rgba(14,116,144,.2), transparent 40%),
var(--panel);
border: 1px solid var(--line);
backdrop-filter: blur(16px);
overflow: hidden;
opacity: 0; animation: rise .9s var(--ease) .18s forwards;
}
.visual-inner {
position: absolute; inset: 0;
display: grid; place-items: center;
padding: 32px;
}
.device {
width: min(100%, 340px);
aspect-ratio: 10 / 14;
border-radius: 22px;
background: linear-gradient(180deg, #0c222c, #07151c);
border: 1px solid rgba(34,211,238,.25);
box-shadow: 0 30px 60px rgba(0,0,0,.45), inset 0 1px 0 rgba(255,255,255,.06);
padding: 22px 18px;
display: flex; flex-direction: column; gap: 16px;
animation: deviceIn 1.1s var(--ease) .35s both;
}
@keyframes deviceIn {
from { opacity: 0; transform: translateY(28px) scale(.96); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.device-top { display: flex; align-items: center; gap: 10px; }
.device-dot {
width: 36px; height: 36px; border-radius: 11px;
background: linear-gradient(145deg, #0e7490, #22d3ee);
display: grid; place-items: center;
}
.device-name { font-family: Outfit, sans-serif; font-weight: 700; letter-spacing: -.02em; }
.device-sub { font-size: .72rem; color: var(--muted); }
.power {
margin: 12px auto;
width: 110px; height: 110px; border-radius: 50%;
background: radial-gradient(circle at 35% 30%, #67e8f9, #0e7490 70%);
box-shadow: 0 0 0 10px rgba(34,211,238,.12), 0 20px 40px rgba(14,116,144,.35);
display: grid; place-items: center;
animation: pulse 2.8s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 10px rgba(34,211,238,.12), 0 20px 40px rgba(14,116,144,.35); }
50% { box-shadow: 0 0 0 16px rgba(34,211,238,.2), 0 24px 48px rgba(14,116,144,.45); }
}
.power span { font-size: 2rem; line-height: 1; filter: drop-shadow(0 2px 4px rgba(0,0,0,.3)); }
.chip-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: auto; }
.chip {
font-size: .7rem; font-weight: 700; color: var(--accent);
border: 1px solid rgba(34,211,238,.25); border-radius: 999px;
padding: 6px 10px; background: rgba(34,211,238,.08);
}
/* Apps catalog — one section, one job */
.apps {
max-width: 1200px; margin: 0 auto;
padding: 0 clamp(20px, 4vw, 48px) clamp(72px, 10vh, 120px);
}
.apps-head {
margin-bottom: 28px;
opacity: 0; animation: rise .7s var(--ease) .4s forwards;
}
.apps-head h2 {
font-family: Outfit, sans-serif; font-size: clamp(1.6rem, 3vw, 2.1rem);
letter-spacing: -.03em; margin: 0 0 8px;
}
.apps-head p { margin: 0; color: var(--muted); max-width: 48ch; }
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 18px;
}
.app-tile {
display: flex; flex-direction: column; gap: 14px;
padding: 22px; border-radius: 22px;
border: 1px solid var(--line);
background: linear-gradient(165deg, rgba(34,211,238,.08), rgba(255,255,255,.02));
text-decoration: none; color: inherit;
transition: transform .25s var(--ease), border-color .25s, box-shadow .25s;
opacity: 0; animation: rise .7s var(--ease) forwards;
}
.app-tile:nth-child(1) { animation-delay: .45s; }
.app-tile:nth-child(2) { animation-delay: .55s; }
.app-tile:nth-child(3) { animation-delay: .65s; }
.app-tile:hover {
transform: translateY(-4px);
border-color: rgba(34,211,238,.35);
box-shadow: 0 20px 40px rgba(0,0,0,.25);
}
.app-tile.coming {
opacity: .55; filter: grayscale(.2);
pointer-events: none;
animation-name: rise; animation-fill-mode: forwards;
}
.app-tile.coming:nth-child(2) { animation-delay: .55s; }
.app-tile.coming:nth-child(3) { animation-delay: .65s; }
.app-ico {
width: 52px; height: 52px; border-radius: 16px;
display: grid; place-items: center; font-size: 1.5rem;
background: linear-gradient(145deg, #0e7490, #22d3ee);
}
.app-ico.muted { background: linear-gradient(145deg, #1e293b, #334155); }
.app-name { font-family: Outfit, sans-serif; font-weight: 700; font-size: 1.2rem; letter-spacing: -.02em; margin: 0; }
.app-desc { margin: 0; color: var(--muted); font-size: .92rem; flex: 1; }
.app-meta {
display: flex; align-items: center; justify-content: space-between; gap: 10px;
font-size: .78rem; font-weight: 700;
}
.tag {
color: var(--accent); background: rgba(34,211,238,.1);
padding: 5px 10px; border-radius: 999px;
}
.tag.soon { color: var(--muted); background: rgba(255,255,255,.06); }
.go { color: var(--accent); }
.faq {
max-width: 1200px; margin: 0 auto 28px;
padding: 0 clamp(20px, 4vw, 48px);
}
.faq-head { margin-bottom: 18px; }
.faq-head h2 {
font-family: Outfit, sans-serif; font-size: clamp(1.45rem, 2.6vw, 1.9rem);
letter-spacing: -.03em; margin: 0 0 8px;
}
.faq-head p { margin: 0; color: var(--muted); max-width: 52ch; }
.faq-list { display: grid; gap: 10px; }
.faq-item {
border: 1px solid var(--line);
border-radius: 16px;
background: rgba(255,255,255,.02);
overflow: hidden;
}
.faq-item summary {
list-style: none; cursor: pointer;
padding: 16px 18px;
font-family: Outfit, sans-serif; font-weight: 700; font-size: 1.02rem;
letter-spacing: -.01em;
display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
content: "+"; color: var(--accent); font-weight: 800; font-size: 1.2rem; line-height: 1;
transition: transform .2s var(--ease);
}
.faq-item[open] summary::after { content: ""; }
.faq-item .faq-a {
margin: 0; padding: 0 18px 16px;
color: var(--muted); font-size: .95rem; line-height: 1.55;
}
.faq-item[open] { border-color: rgba(34,211,238,.28); background: rgba(34,211,238,.05); }
.contact {
max-width: 1200px; margin: 0 auto 28px;
padding: 0 clamp(20px, 4vw, 48px);
}
.contact-panel {
border: 1px solid var(--line);
border-radius: 22px;
padding: 22px 24px;
background: linear-gradient(165deg, rgba(34,211,238,.08), rgba(255,255,255,.02));
}
.contact-panel h2 {
font-family: Outfit, sans-serif; font-size: 1.35rem;
letter-spacing: -.02em; margin: 0 0 8px;
}
.contact-panel > p { margin: 0 0 14px; color: var(--muted); font-size: .95rem; }
.mails { display: flex; flex-wrap: wrap; gap: 10px 18px; }
.mails a {
color: var(--accent); font-weight: 700; text-decoration: none;
border-bottom: 1px solid rgba(34,211,238,.35);
}
.mails a:hover { border-bottom-color: var(--accent); }
.mails .label { color: var(--muted); font-size: .8rem; font-weight: 700; display: block; margin-bottom: 2px; }
.mail-item { min-width: 200px; }
.legal {
max-width: 1200px; margin: 0 auto;
padding: 0 clamp(20px, 4vw, 48px) 48px;
display: flex; flex-wrap: wrap; gap: 14px 22px;
font-size: .85rem; color: var(--muted);
}
.legal a { color: var(--accent); font-weight: 700; text-decoration: none; }
.legal a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="stage">
<div class="orb orb-a" aria-hidden="true"></div>
<div class="orb orb-b" aria-hidden="true"></div>
<header class="top">
<a class="logo" href="index.html">
<div class="mark" aria-hidden="true">🦊</div>
<div class="word">EvilFox</div>
</a>
<div class="lang" role="group" aria-label="Language">
<button type="button" data-set-lang="ru" aria-pressed="true">RU</button>
<button type="button" data-set-lang="en" aria-pressed="false">EN</button>
</div>
</header>
<section class="hero">
<div>
<p class="eyebrow" data-i18n="eyebrow">Студия приложений</p>
<h1 class="brand-title">EvilFox</h1>
<p class="lead" data-i18n="lead">Приложения для безопасного и спокойного интернета. Начните с EvilFox VPN — дальше появятся новые продукты.</p>
<p class="desc" data-i18n="desc">EvilFox — это локальный клиент: вы сами добавляете конфигурации и URL подключения. Шифрование, выбор серверов, Kill Switch, светлая и тёмная темы, русский и английский интерфейс. Сборка для Windows, в том числе Microsoft Store.</p>
<ul class="points">
<li data-i18n="point1">Современные протоколы и группировка серверов по странам</li>
<li data-i18n="point2">Режимы полной защиты и системного прокси</li>
<li data-i18n="point3">Данные профилей хранятся на устройстве — без обязательной регистрации</li>
</ul>
<div class="cta-row">
<a class="btn btn-primary" href="#apps" data-i18n="cta_apps">Смотреть приложения</a>
<a class="btn btn-ghost" href="#faq" data-i18n="cta_faq">FAQ</a>
<a class="btn btn-ghost" href="#contact" data-i18n="cta_contact">Связаться с нами</a>
</div>
</div>
<div class="visual" aria-hidden="true">
<div class="visual-inner">
<div class="device">
<div class="device-top">
<div class="device-dot">🦊</div>
<div>
<div class="device-name">EvilFox VPN</div>
<div class="device-sub" data-i18n="device_sub">Защита · Windows</div>
</div>
</div>
<div class="power"><span></span></div>
<div class="chip-row">
<span class="chip" data-i18n="chip1">Шифрование</span>
<span class="chip" data-i18n="chip2">Kill Switch</span>
<span class="chip" data-i18n="chip3">RU / EN</span>
</div>
</div>
</div>
</div>
</section>
<section class="apps" id="apps">
<div class="apps-head">
<h2 data-i18n="apps_title">Наши приложения</h2>
<p data-i18n="apps_lead">Каталог продуктов EvilFox. Сейчас доступен VPN-клиент; другие приложения — скоро.</p>
</div>
<div class="grid">
<a class="app-tile" href="https://evilfox.win/">
<div class="app-ico">🦊</div>
<h3 class="app-name">EvilFox VPN</h3>
<p class="app-desc" data-i18n="vpn_desc">Клиент для защищённого доступа в интернет на Windows: современные протоколы, флаги серверов, Kill Switch, светлая и тёмная темы, русский и английский.</p>
<div class="app-meta">
<span class="tag" data-i18n="tag_available">Доступно</span>
<span class="go" data-i18n="open">Открыть →</span>
</div>
</a>
<div class="app-tile coming" aria-disabled="true">
<div class="app-ico muted"></div>
<h3 class="app-name" data-i18n="soon1_name">Скоро</h3>
<p class="app-desc" data-i18n="soon1_desc">Новое приложение вне VPN — в разработке. Следите за обновлениями на сайте.</p>
<div class="app-meta">
<span class="tag soon" data-i18n="tag_soon">В планах</span>
</div>
</div>
<div class="app-tile coming" aria-disabled="true">
<div class="app-ico muted"></div>
<h3 class="app-name" data-i18n="soon2_name">Ещё проекты</h3>
<p class="app-desc" data-i18n="soon2_desc">Инструменты для работы и повседневных задач — появятся здесь позже.</p>
<div class="app-meta">
<span class="tag soon" data-i18n="tag_soon">В планах</span>
</div>
</div>
</div>
</section>
<section class="faq" id="faq" aria-labelledby="faq-title">
<div class="faq-head">
<h2 id="faq-title" data-i18n="faq_title">Частые вопросы</h2>
<p data-i18n="faq_lead">Кратко о клиенте, данных и поддержке. Если ответа нет — напишите нам.</p>
</div>
<div class="faq-list">
<details class="faq-item">
<summary data-i18n="faq_q1">Что такое EvilFox?</summary>
<p class="faq-a" data-i18n="faq_a1">EvilFox — Windows-клиент для защищённого доступа в интернет. Вы подключаетесь к серверам по своим конфигурациям; мы публикуем приложение и обновления.</p>
</details>
<details class="faq-item">
<summary data-i18n="faq_q2">Нужна ли регистрация или аккаунт?</summary>
<p class="faq-a" data-i18n="faq_a2">Нет обязательного аккаунта у издателя. Профили и настройки хранятся локально. URL и ключи подключения вы получаете у своего провайдера конфигурации.</p>
</details>
<details class="faq-item">
<summary data-i18n="faq_q3">Собираете ли вы мой трафик?</summary>
<p class="faq-a" data-i18n="faq_a3">Клиент работает на устройстве. EvilFox как издатель не ведёт централизованный сбор содержимого вашего трафика для рекламы. При подключении трафик идёт через серверы из вашей конфигурации — их политика может отличаться.</p>
</details>
<details class="faq-item">
<summary data-i18n="faq_q4">На каких платформах доступно приложение?</summary>
<p class="faq-a" data-i18n="faq_a4">Основная платформа — Windows (включая Microsoft Store). Следите за каталогом на сайте: появятся и другие продукты EvilFox.</p>
</details>
<details class="faq-item">
<summary data-i18n="faq_q5">Что делает Kill Switch?</summary>
<p class="faq-a" data-i18n="faq_a5">Kill Switch помогает ограничить незащищённый доступ в сеть, если защищённое соединение обрывается. Включайте в настройках приложения, если нужна такая защита.</p>
</details>
<details class="faq-item">
<summary data-i18n="faq_q6">Как связаться с поддержкой?</summary>
<p class="faq-a" data-i18n="faq_a6">Напишите на animegold3@gmail.com или резервную admin@evilfox.win. Также есть Telegram: t.me/evilfox100Bot.</p>
</details>
</div>
</section>
<section class="contact" id="contact" aria-labelledby="contact-title">
<div class="contact-panel">
<h2 id="contact-title" data-i18n="contact_title">Связаться с нами</h2>
<p data-i18n="contact_lead">Напишите на почту — ответим по вопросам приложений и поддержки.</p>
<div class="mails">
<div class="mail-item">
<span class="label" data-i18n="mail_main">Основная почта</span>
<a href="mailto:animegold3@gmail.com">animegold3@gmail.com</a>
</div>
<div class="mail-item">
<span class="label" data-i18n="mail_backup">Резервная почта</span>
<a href="mailto:admin@evilfox.win">admin@evilfox.win</a>
</div>
<div class="mail-item">
<span class="label" data-i18n="mail_tg">Telegram</span>
<a href="https://t.me/evilfox100Bot">t.me/evilfox100Bot</a>
</div>
</div>
</div>
</section>
<footer class="legal">
<span>© 2026 EvilFox</span>
<a href="#faq" data-i18n="faq_link">FAQ</a>
<a href="privacy-ru.html" data-i18n-href-ru="privacy-ru.html" data-i18n-href-en="privacy-en.html" data-i18n="privacy">Конфиденциальность</a>
<a href="terms-ru.html" data-i18n-href-ru="terms-ru.html" data-i18n-href-en="terms-en.html" data-i18n="terms">Соглашение</a>
<a href="#contact" data-i18n="support">Связаться</a>
</footer>
</div>
<script>
const I18N = {
ru: {
eyebrow: "Студия приложений",
lead: "Приложения для безопасного и спокойного интернета. Начните с EvilFox VPN — дальше появятся новые продукты.",
desc: "EvilFox — это локальный клиент: вы сами добавляете конфигурации и URL подключения. Шифрование, выбор серверов, Kill Switch, светлая и тёмная темы, русский и английский интерфейс. Сборка для Windows, в том числе Microsoft Store.",
point1: "Современные протоколы и группировка серверов по странам",
point2: "Режимы полной защиты и системного прокси",
point3: "Данные профилей хранятся на устройстве — без обязательной регистрации",
cta_apps: "Смотреть приложения",
cta_faq: "FAQ",
cta_contact: "Связаться с нами",
device_sub: "Защита · Windows",
chip1: "Шифрование",
chip2: "Kill Switch",
chip3: "RU / EN",
apps_title: "Наши приложения",
apps_lead: "Каталог продуктов EvilFox. Сейчас доступен VPN-клиент; другие приложения — скоро.",
vpn_desc: "Клиент для защищённого доступа в интернет на Windows: современные протоколы, флаги серверов, Kill Switch, светлая и тёмная темы, русский и английский.",
tag_available: "Доступно",
open: "Открыть →",
soon1_name: "Скоро",
soon1_desc: "Новое приложение вне VPN — в разработке. Следите за обновлениями на сайте.",
soon2_name: "Ещё проекты",
soon2_desc: "Инструменты для работы и повседневных задач — появятся здесь позже.",
tag_soon: "В планах",
faq_title: "Частые вопросы",
faq_lead: "Кратко о клиенте, данных и поддержке. Если ответа нет — напишите нам.",
faq_link: "FAQ",
faq_q1: "Что такое EvilFox?",
faq_a1: "EvilFox — Windows-клиент для защищённого доступа в интернет. Вы подключаетесь к серверам по своим конфигурациям; мы публикуем приложение и обновления.",
faq_q2: "Нужна ли регистрация или аккаунт?",
faq_a2: "Нет обязательного аккаунта у издателя. Профили и настройки хранятся локально. URL и ключи подключения вы получаете у своего провайдера конфигурации.",
faq_q3: "Собираете ли вы мой трафик?",
faq_a3: "Клиент работает на устройстве. EvilFox как издатель не ведёт централизованный сбор содержимого вашего трафика для рекламы. При подключении трафик идёт через серверы из вашей конфигурации — их политика может отличаться.",
faq_q4: "На каких платформах доступно приложение?",
faq_a4: "Основная платформа — Windows (включая Microsoft Store). Следите за каталогом на сайте: появятся и другие продукты EvilFox.",
faq_q5: "Что делает Kill Switch?",
faq_a5: "Kill Switch помогает ограничить незащищённый доступ в сеть, если защищённое соединение обрывается. Включайте в настройках приложения, если нужна такая защита.",
faq_q6: "Как связаться с поддержкой?",
faq_a6: "Напишите на animegold3@gmail.com или резервную admin@evilfox.win. Также есть Telegram: t.me/evilfox100Bot.",
privacy: "Конфиденциальность",
terms: "Соглашение",
support: "Связаться",
contact_title: "Связаться с нами",
contact_lead: "Напишите на почту — ответим по вопросам приложений и поддержки.",
mail_main: "Основная почта",
mail_backup: "Резервная почта",
mail_tg: "Telegram"
},
en: {
eyebrow: "App studio",
lead: "Apps for a safer, calmer internet. Start with EvilFox VPN — more products are on the way.",
desc: "EvilFox is a local client: you add your own configs and connection URLs. Encryption, server picker, Kill Switch, light and dark themes, Russian and English UI. Built for Windows, including the Microsoft Store.",
point1: "Modern protocols and servers grouped by country",
point2: "Full protection and system proxy modes",
point3: "Profiles stay on your device — no mandatory publisher account",
cta_apps: "Browse apps",
cta_faq: "FAQ",
cta_contact: "Contact us",
device_sub: "Protection · Windows",
chip1: "Encryption",
chip2: "Kill Switch",
chip3: "RU / EN",
apps_title: "Our apps",
apps_lead: "The EvilFox catalog. VPN is available now; more apps are coming.",
vpn_desc: "A Windows client for private internet access: modern protocols, country flags, Kill Switch, light and dark themes, Russian and English.",
tag_available: "Available",
open: "Open →",
soon1_name: "Coming soon",
soon1_desc: "A new non-VPN app is in the works. Watch the site for updates.",
soon2_name: "More projects",
soon2_desc: "Tools for work and everyday tasks will appear here later.",
tag_soon: "Planned",
faq_title: "FAQ",
faq_lead: "Short answers about the client, data, and support. Still stuck? Contact us.",
faq_link: "FAQ",
faq_q1: "What is EvilFox?",
faq_a1: "EvilFox is a Windows client for private internet access. You connect using your own configurations; we publish the app and updates.",
faq_q2: "Do I need to register or create an account?",
faq_a2: "No publisher account is required. Profiles and settings stay local. Connection URLs and keys come from your configuration provider.",
faq_q3: "Do you collect my traffic?",
faq_a3: "The client runs on your device. As publisher, EvilFox does not centrally collect your traffic contents for advertising. When connected, traffic goes through servers in your config — their policies may differ.",
faq_q4: "Which platforms are supported?",
faq_a4: "The main platform is Windows (including Microsoft Store). Watch the site catalog for more EvilFox products.",
faq_q5: "What does Kill Switch do?",
faq_a5: "Kill Switch helps limit unprotected network access if the protected connection drops. Turn it on in app settings when you want that safeguard.",
faq_q6: "How do I contact support?",
faq_a6: "Email animegold3@gmail.com or backup admin@evilfox.win. Telegram: t.me/evilfox100Bot.",
privacy: "Privacy",
terms: "Terms",
support: "Contact",
contact_title: "Contact us",
contact_lead: "Email us about apps and support — we will get back to you.",
mail_main: "Primary email",
mail_backup: "Backup email",
mail_tg: "Telegram"
}
};
function setLang(lang) {
const pack = I18N[lang] || I18N.ru;
document.documentElement.lang = lang;
document.documentElement.dataset.lang = lang;
document.querySelectorAll("[data-i18n]").forEach((el) => {
const key = el.getAttribute("data-i18n");
if (pack[key] != null) el.textContent = pack[key];
});
document.querySelectorAll("[data-i18n-href-ru]").forEach((el) => {
el.setAttribute("href", lang === "en" ? el.getAttribute("data-i18n-href-en") : el.getAttribute("data-i18n-href-ru"));
});
document.querySelectorAll(".lang button").forEach((btn) => {
btn.setAttribute("aria-pressed", btn.getAttribute("data-set-lang") === lang ? "true" : "false");
});
try { localStorage.setItem("evilfox.site.lang", lang); } catch (_) {}
document.title = lang === "en" ? "EvilFox — Apps" : "EvilFox — Приложения";
}
document.querySelectorAll(".lang button").forEach((btn) => {
btn.addEventListener("click", () => setLang(btn.getAttribute("data-set-lang")));
});
let initial = "ru";
try {
const saved = localStorage.getItem("evilfox.site.lang");
if (saved === "en" || saved === "ru") initial = saved;
else if (navigator.language && navigator.language.toLowerCase().startsWith("en")) initial = "en";
} catch (_) {}
setLang(initial);
</script>
</body>
</html>
+260
View File
@@ -0,0 +1,260 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Privacy Policy — EvilFox</title>
<meta name="description" content="Detailed EvilFox Privacy Policy (US, EU, Microsoft Store)." />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700&family=Outfit:wght@700;800&display=swap" rel="stylesheet" />
<style>
:root {
--bg: #f4fafb; --ink: #0f172a; --muted: #64748b; --card: #fff;
--line: rgba(15, 23, 42, 0.1); --accent: #0e7490;
}
* { box-sizing: border-box; }
body {
margin: 0; font-family: Manrope, system-ui, sans-serif; color: var(--ink);
background:
radial-gradient(900px 420px at 10% -10%, rgba(34, 211, 238, 0.18), transparent 55%),
radial-gradient(700px 360px at 100% 0%, rgba(14, 116, 144, 0.12), transparent 50%),
var(--bg);
line-height: 1.65;
}
.wrap { max-width: 860px; margin: 0 auto; padding: 32px 20px 72px; }
header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 28px; }
.brand { display: flex; align-items: center; gap: 12px; text-decoration: none; color: inherit; }
.logo {
width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center;
background: linear-gradient(145deg, #0e7490, #22d3ee);
box-shadow: 0 10px 24px rgba(8, 145, 178, 0.28); font-size: 1.4rem;
}
.brand h1 { font-family: Outfit, sans-serif; font-size: 1.45rem; font-weight: 800; letter-spacing: -0.04em; margin: 0; }
.brand span { display: block; font-size: .78rem; color: var(--muted); font-weight: 600; }
.langs { display: flex; gap: 8px; }
.langs a {
text-decoration: none; font-weight: 700; font-size: .85rem; padding: 8px 14px;
border-radius: 999px; border: 1px solid var(--line); color: var(--muted); background: var(--card);
}
.langs a.active, .langs a:hover { color: #fff; background: var(--accent); border-color: var(--accent); }
.card {
background: var(--card); border: 1px solid var(--line); border-radius: 20px;
padding: 28px 26px; box-shadow: 0 18px 40px rgba(15, 23, 42, 0.06);
}
h2 { font-family: Outfit, sans-serif; font-size: 1.65rem; letter-spacing: -0.03em; margin: 0 0 8px; }
.meta { color: var(--muted); font-size: .92rem; margin: 0 0 18px; }
.toc { background: rgba(14,116,144,.06); border-radius: 14px; padding: 14px 18px; margin: 0 0 24px; font-size: .9rem; }
.toc a { color: var(--accent); font-weight: 600; text-decoration: none; }
.toc ol { margin: 8px 0 0; padding-left: 1.25rem; }
.toc li { margin: 4px 0; }
h3 { font-size: 1.08rem; margin: 26px 0 10px; color: var(--accent); scroll-margin-top: 16px; }
h4 { font-size: .98rem; margin: 16px 0 8px; }
p, li { margin: 0 0 12px; }
ul, ol { padding-left: 1.25rem; margin: 0 0 14px; }
table { width: 100%; border-collapse: collapse; font-size: .9rem; margin: 0 0 16px; }
th, td { border: 1px solid var(--line); padding: 10px 12px; text-align: left; vertical-align: top; }
th { background: rgba(14,116,144,.08); }
.note { font-size: .9rem; color: var(--muted); border-left: 3px solid var(--accent); padding-left: 12px; margin: 12px 0 16px; }
footer { margin-top: 28px; font-size: .88rem; color: var(--muted); display: flex; flex-wrap: wrap; gap: 12px 20px; }
footer a, .card a { color: var(--accent); font-weight: 700; text-decoration: none; }
footer a:hover, .card a:hover { text-decoration: underline; }
.nav-docs { margin-top: 22px; font-size: .9rem; }
</style>
</head>
<body>
<div class="wrap">
<header>
<a class="brand" href="https://evilfox.win/">
<div class="logo" aria-hidden="true">🦊</div>
<div>
<h1>EvilFox</h1>
<span>evilfox.win</span>
</div>
</a>
<nav class="langs" aria-label="Language">
<a href="privacy-ru.html" lang="ru">Русский</a>
<a class="active" href="privacy-en.html" lang="en">English</a>
</nav>
</header>
<article class="card">
<h2>Privacy Policy</h2>
<p class="meta">
Publisher / controller (for on-device data): <strong>EvilFox</strong><br />
Effective date: <strong>August 2, 2026</strong> · Document version: 1.0<br />
Applies to the EvilFox Windows app (including the Microsoft Store build) and related pages on evilfox.win.
</p>
<nav class="toc" aria-label="Table of contents">
<strong>Contents</strong>
<ol>
<li><a href="#s1">Who we are and what this Policy covers</a></li>
<li><a href="#s2">Scope and roles</a></li>
<li><a href="#s3">What data is processed</a></li>
<li><a href="#s4">What we do not collect centrally</a></li>
<li><a href="#s5">Purposes and legal bases</a></li>
<li><a href="#s6">Network traffic and third-party servers</a></li>
<li><a href="#s7">App updates</a></li>
<li><a href="#s8">External links and third-party services</a></li>
<li><a href="#s9">Retention, deletion, and security</a></li>
<li><a href="#s10">Transfers and international aspects</a></li>
<li><a href="#s11">Your rights (EU / GDPR)</a></li>
<li><a href="#s12">California (CCPA/CPRA) and US users</a></li>
<li><a href="#s13">Children</a></li>
<li><a href="#s14">Microsoft Store</a></li>
<li><a href="#s15">Changes to this Policy</a></li>
<li><a href="#s16">Contact</a></li>
</ol>
</nav>
<h3 id="s1">1. Who we are and what this Policy covers</h3>
<p>EvilFox is a local Windows client for protected network connectivity (proxy mode and full-protection / TUN mode, modern protocols). You add your own configurations and connection URLs. The Microsoft Store build has no in-app purchases.</p>
<p>This Privacy Policy explains what data may be processed when you use the App, for what purposes, how long it is kept, what rights you have, and how to contact us. We aim to meet Microsoft Store transparency expectations and the expectations of users in the United States and the European Union / EEA / United Kingdom.</p>
<p class="note">In short: most data stays on your device. We do not sell personal data and do not build an advertising profile from the contents of your traffic.</p>
<h3 id="s2">2. Scope and roles</h3>
<h4>2.1. What is covered</h4>
<ul>
<li>the EvilFox application (Windows, including MSIX / Microsoft Store);</li>
<li>policy and terms pages on evilfox.win;</li>
<li>support requests sent to the contacts listed below.</li>
</ul>
<h4>2.2. Roles</h4>
<ul>
<li><strong>EvilFox (client publisher)</strong> — controller for data the App stores and processes <em>locally on your device</em> to run the client.</li>
<li><strong>Server operator / configuration provider</strong> — a separate party (you or your provider). When traffic goes through servers you choose, processing on those servers is governed by their policies. EvilFox as client publisher is not necessarily the operator of those servers.</li>
<li><strong>Microsoft</strong> — if you install or update from the Microsoft Store, Microsofts policies apply to Store distribution, Microsoft accounts, and Store payments (where applicable).</li>
</ul>
<h3 id="s3">3. What data is processed</h3>
<h4>3.1. Data on your device (local)</h4>
<p>The App may store on your device (config files, app data folders, WebView2 localStorage and similar OS storage):</p>
<table>
<thead><tr><th>Category</th><th>Examples</th><th>Why</th></tr></thead>
<tbody>
<tr><td>Connection configurations</td><td>Configuration URLs, server profiles, protocol parameters you enter or import</td><td>Connecting and choosing a server</td></tr>
<tr><td>App settings</td><td>Theme (light/dark), language (RU/EN), proxy or full-protection mode, Kill Switch, auto best server</td><td>Remembering your preferences</td></tr>
<tr><td>Session technical data</td><td>Connection status, selected profile, ping, local session/traffic stats (when available)</td><td>Status and statistics UI</td></tr>
<tr><td>OS / WebView2 service data</td><td>WebView2 cache, on-device error logs</td><td>Stable UI operation</td></tr>
</tbody>
</table>
<h4>3.2. Data during network use</h4>
<ul>
<li><strong>With servers you choose:</strong> technical parameters needed by the protocol are exchanged (addresses, keys/tokens from your configuration, tunnel control traffic). Your user traffic is processed along the path through the selected server — see Section 6.</li>
<li><strong>Update checks (non-Store builds or when you initiate a check):</strong> a request to an update feed (e.g. hosting on git.de4ima.uk) may include standard HTTP request metadata (IP address as seen by the host, User-Agent, time).</li>
<li><strong>When you open external links:</strong> the browser or OS sends data to that site/messenger under their rules.</li>
</ul>
<h4>3.3. Data you may send to support</h4>
<p>If you contact us (email / Telegram / website), you may voluntarily provide an address, problem description, or screenshots. We use them only to respond and improve the product.</p>
<h3 id="s4">4. What we do not collect centrally</h3>
<p>As the client publisher, EvilFox does <strong>not</strong> operate a mandatory central collection and does not sell:</p>
<ul>
<li>a cloud account registry of EvilFox client users for ordinary app operation;</li>
<li>payment cards or in-app purchase details (the Store build has no in-app purchases; Store purchases, if any, are handled by Microsoft);</li>
<li>browsing history, contacts, microphone/camera by default;</li>
<li>the contents of your internet traffic for advertising or data resale.</li>
</ul>
<p>We do <strong>not sell</strong> personal data.</p>
<h3 id="s5">5. Purposes and legal bases</h3>
<table>
<thead><tr><th>Purpose</th><th>Legal basis (GDPR / equivalents)</th></tr></thead>
<tbody>
<tr><td>Providing client features (connect, settings, UI)</td><td>Performance of a contract / user request for the client service</td></tr>
<tr><td>Security and stability (local logs, preventing failures)</td><td>Legitimate interests / necessity to operate the product</td></tr>
<tr><td>Update checks (user-initiated or build policy)</td><td>Legitimate interests / user action</td></tr>
<tr><td>Responding to support requests</td><td>Legitimate interests / responding to the data subject</td></tr>
<tr><td>Legal compliance and protecting rights</td><td>Legal obligation / legitimate interests</td></tr>
</tbody>
</table>
<h3 id="s6">6. Network traffic and third-party servers</h3>
<p>After you connect, traffic is routed according to your configuration. The node operator may process IP addresses, session metadata, and (depending on protocol and settings) traffic contents. EvilFox does not control every third-party servers privacy practice.</p>
<p><strong>Recommendation:</strong> use only trusted configurations; read your server providers privacy policy and terms.</p>
<p><strong>Full-protection (TUN) mode and Kill Switch:</strong> these features change on-device network routing to protect the connection and reduce leaks if the tunnel drops. They operate locally on your system.</p>
<h3 id="s7">7. App updates</h3>
<ul>
<li><strong>Microsoft Store:</strong> updates are normally delivered by the Store; in-app self-update in the MSIX build is disabled or limited.</li>
<li><strong>Other channels (sideload):</strong> when you choose, the client may fetch an update manifest from a publisher URL (including on git.de4ima.uk). The hosting server may see IP and technical request headers.</li>
</ul>
<h3 id="s8">8. External links and third-party services</h3>
<p>The App may open, after you choose to click:</p>
<ul>
<li><a href="https://evilfox.win/">https://evilfox.win/</a> — publisher website;</li>
<li><a href="https://t.me/evilfox100Bot">https://t.me/evilfox100Bot</a> — Telegram support (Telegrams policies apply).</li>
</ul>
<p>The device also uses OS components and Microsoft Edge WebView2 — Microsofts policies apply to those components.</p>
<h3 id="s9">9. Retention, deletion, and security</h3>
<h4>9.1. Retention</h4>
<p>Local data remains while you use the App, or until you delete profiles, clear app data, or uninstall EvilFox. Support correspondence is kept as long as needed to resolve the request (typically no longer than needed for the conversation and basic support records).</p>
<h4>9.2. How to delete data</h4>
<ul>
<li>delete configurations/profiles in the App;</li>
<li>uninstall EvilFox in Windows;</li>
<li>if needed, manually clear remaining app data folders.</li>
</ul>
<p>There is no central EvilFox cloud account for restoring local profiles — we cannot restore device-deleted data from our servers.</p>
<h4>9.3. Security</h4>
<p>We apply reasonable organizational and technical measures within the client and OS. No method is perfectly secure. You are responsible for keeping configuration URLs/keys confidential on the device and for physical access to the computer.</p>
<h3 id="s10">10. Transfers and international aspects</h3>
<p>The client runs locally. Data may be processed in other countries when:</p>
<ul>
<li>you connect to servers abroad (via your configuration);</li>
<li>you open foreign websites/services;</li>
<li>an update request is handled by hosting infrastructure outside your country.</li>
</ul>
<p>For users in the EU/EEA/UK: such transfers result from your choice of configuration and services. Where required, server operators should provide appropriate safeguards (e.g. Standard Contractual Clauses) — ask your configuration provider.</p>
<h3 id="s11">11. Your rights (EU / GDPR and equivalents)</h3>
<p>Depending on applicable law, you may have rights to:</p>
<ul>
<li>access and obtain a copy of data;</li>
<li>rectify inaccurate data;</li>
<li>erasure (“right to be forgotten”) — including by uninstalling and clearing local data;</li>
<li>restriction of processing and objection to processing based on legitimate interests;</li>
<li>data portability — to the extent data is stored in a structured form on the device (export configurations if available);</li>
<li>lodge a complaint with a supervisory authority where you live or work in the EU/EEA/UK.</li>
</ul>
<p>Because most processing is on-device, most rights are exercised locally. For questions about this Policy, contact us below. We will respond within a reasonable time.</p>
<h3 id="s12">12. California (CCPA/CPRA) and US users</h3>
<p>We do <strong>not sell</strong> and do <strong>not “share”</strong> personal information for cross-context behavioral advertising under CCPA/CPRA. Categories of data are described in Section 3. Sources are mainly you (configuration input) and the device. We do not use sensitive personal information to infer consumer characteristics for advertising.</p>
<p>Rights to know, delete, and correct regarding local client data are exercised on the device (uninstall / clear data). Requests related to support correspondence can be sent via Section 16.</p>
<p>We do not discriminate against you for exercising privacy rights.</p>
<h3 id="s13">13. Children</h3>
<p>The App is not directed to children under <strong>13</strong>. In the EU/EEA, where local law sets a higher digital consent age (often up to <strong>16</strong>), do not use the App below that age without parental/guardian consent where required. We do not knowingly collect childrens data centrally. If you believe a child sent us data via support, contact us — we will delete the request where appropriate.</p>
<h3 id="s14">14. Microsoft Store</h3>
<p>When distributed through the Microsoft Store, Microsoft may process install, update, review, and (for paid products) payment data under its own policies. Capabilities such as runFullTrust / classic desktop may require Store disclosure and approval of restricted capabilities.</p>
<h3 id="s15">15. Changes to this Policy</h3>
<p>We may update this Policy. The current version is published on the website (this page) and/or in the App Help section. Material changes are reflected by the date at the top where practical. Continued use of the App after an update means you have an opportunity to review the new text.</p>
<h3 id="s16">16. Contact</h3>
<p>
<strong>EvilFox</strong><br />
Website: <a href="https://evilfox.win/">https://evilfox.win/</a><br />
Primary email: <a href="mailto:animegold3@gmail.com">animegold3@gmail.com</a><br />
Backup email: <a href="mailto:admin@evilfox.win">admin@evilfox.win</a><br />
Telegram: <a href="https://t.me/evilfox100Bot">https://t.me/evilfox100Bot</a><br />
Terms of Use: <a href="terms-en.html">terms-en.html</a>
</p>
<p class="nav-docs">Русский: <a href="privacy-ru.html">Политика конфиденциальности</a></p>
</article>
<footer>
<span>© 2026 EvilFox</span>
<a href="terms-en.html">Terms of Use</a>
<a href="https://evilfox.win/">Website</a>
</footer>
</div>
</body>
</html>
+260
View File
@@ -0,0 +1,260 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Политика конфиденциальности — EvilFox</title>
<meta name="description" content="Подробная политика конфиденциальности приложения EvilFox (США, ЕС, Microsoft Store)." />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700&family=Outfit:wght@700;800&display=swap" rel="stylesheet" />
<style>
:root {
--bg: #f4fafb; --ink: #0f172a; --muted: #64748b; --card: #fff;
--line: rgba(15, 23, 42, 0.1); --accent: #0e7490;
}
* { box-sizing: border-box; }
body {
margin: 0; font-family: Manrope, system-ui, sans-serif; color: var(--ink);
background:
radial-gradient(900px 420px at 10% -10%, rgba(34, 211, 238, 0.18), transparent 55%),
radial-gradient(700px 360px at 100% 0%, rgba(14, 116, 144, 0.12), transparent 50%),
var(--bg);
line-height: 1.65;
}
.wrap { max-width: 860px; margin: 0 auto; padding: 32px 20px 72px; }
header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 28px; }
.brand { display: flex; align-items: center; gap: 12px; text-decoration: none; color: inherit; }
.logo {
width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center;
background: linear-gradient(145deg, #0e7490, #22d3ee);
box-shadow: 0 10px 24px rgba(8, 145, 178, 0.28); font-size: 1.4rem;
}
.brand h1 { font-family: Outfit, sans-serif; font-size: 1.45rem; font-weight: 800; letter-spacing: -0.04em; margin: 0; }
.brand span { display: block; font-size: .78rem; color: var(--muted); font-weight: 600; }
.langs { display: flex; gap: 8px; }
.langs a {
text-decoration: none; font-weight: 700; font-size: .85rem; padding: 8px 14px;
border-radius: 999px; border: 1px solid var(--line); color: var(--muted); background: var(--card);
}
.langs a.active, .langs a:hover { color: #fff; background: var(--accent); border-color: var(--accent); }
.card {
background: var(--card); border: 1px solid var(--line); border-radius: 20px;
padding: 28px 26px; box-shadow: 0 18px 40px rgba(15, 23, 42, 0.06);
}
h2 { font-family: Outfit, sans-serif; font-size: 1.65rem; letter-spacing: -0.03em; margin: 0 0 8px; }
.meta { color: var(--muted); font-size: .92rem; margin: 0 0 18px; }
.toc { background: rgba(14,116,144,.06); border-radius: 14px; padding: 14px 18px; margin: 0 0 24px; font-size: .9rem; }
.toc a { color: var(--accent); font-weight: 600; text-decoration: none; }
.toc ol { margin: 8px 0 0; padding-left: 1.25rem; }
.toc li { margin: 4px 0; }
h3 { font-size: 1.08rem; margin: 26px 0 10px; color: var(--accent); scroll-margin-top: 16px; }
h4 { font-size: .98rem; margin: 16px 0 8px; }
p, li { margin: 0 0 12px; }
ul, ol { padding-left: 1.25rem; margin: 0 0 14px; }
table { width: 100%; border-collapse: collapse; font-size: .9rem; margin: 0 0 16px; }
th, td { border: 1px solid var(--line); padding: 10px 12px; text-align: left; vertical-align: top; }
th { background: rgba(14,116,144,.08); }
.note { font-size: .9rem; color: var(--muted); border-left: 3px solid var(--accent); padding-left: 12px; margin: 12px 0 16px; }
footer { margin-top: 28px; font-size: .88rem; color: var(--muted); display: flex; flex-wrap: wrap; gap: 12px 20px; }
footer a, .card a { color: var(--accent); font-weight: 700; text-decoration: none; }
footer a:hover, .card a:hover { text-decoration: underline; }
.nav-docs { margin-top: 22px; font-size: .9rem; }
</style>
</head>
<body>
<div class="wrap">
<header>
<a class="brand" href="https://evilfox.win/">
<div class="logo" aria-hidden="true">🦊</div>
<div>
<h1>EvilFox</h1>
<span>evilfox.win</span>
</div>
</a>
<nav class="langs" aria-label="Язык">
<a class="active" href="privacy-ru.html" lang="ru">Русский</a>
<a href="privacy-en.html" lang="en">English</a>
</nav>
</header>
<article class="card">
<h2>Политика конфиденциальности</h2>
<p class="meta">
Издатель / контролёр (для данных на устройстве): <strong>EvilFox</strong><br />
Дата вступления в силу: <strong>2 августа 2026 г.</strong> · Версия документа: 1.0<br />
Применяется к приложению EvilFox для Windows (включая сборку Microsoft Store) и связанным страницам на evilfox.win.
</p>
<nav class="toc" aria-label="Содержание">
<strong>Содержание</strong>
<ol>
<li><a href="#s1">Кто мы и о чём эта политика</a></li>
<li><a href="#s2">Область применения и роли</a></li>
<li><a href="#s3">Какие данные обрабатываются</a></li>
<li><a href="#s4">Что мы не собираем централизованно</a></li>
<li><a href="#s5">Цели и правовые основания</a></li>
<li><a href="#s6">Сетевой трафик и сторонние серверы</a></li>
<li><a href="#s7">Обновления приложения</a></li>
<li><a href="#s8">Внешние ссылки и сторонние сервисы</a></li>
<li><a href="#s9">Хранение, удаление и безопасность</a></li>
<li><a href="#s10">Передачи данных и международные аспекты</a></li>
<li><a href="#s11">Ваши права (ЕС / GDPR)</a></li>
<li><a href="#s12">Калифорния (CCPA/CPRA) и США</a></li>
<li><a href="#s13">Дети</a></li>
<li><a href="#s14">Microsoft Store</a></li>
<li><a href="#s15">Изменения политики</a></li>
<li><a href="#s16">Контакты</a></li>
</ol>
</nav>
<h3 id="s1">1. Кто мы и о чём эта политика</h3>
<p>EvilFox — локальный клиент для защищённого сетевого подключения на Windows (режимы прокси и полной защиты / TUN, поддержка современных протоколов). Вы самостоятельно добавляете конфигурации и URL подключения. В сборке Microsoft Store нет встроенных покупок внутри приложения.</p>
<p>Настоящая Политика объясняет, какие данные могут обрабатываться при использовании Приложения, для каких целей, как долго они хранятся, какие у вас есть права и как с нами связаться. Мы стремимся соответствовать требованиям прозрачности Microsoft Store, а также ожиданиям пользователей в США и Европейском союзе / ЕЭЗ / Великобритании.</p>
<p class="note">Кратко: большинство данных остаётся на вашем устройстве. Мы не продаём персональные данные и не ведём рекламный профиль на основе содержимого вашего трафика.</p>
<h3 id="s2">2. Область применения и роли</h3>
<h4>2.1. Что покрывается</h4>
<ul>
<li>приложение EvilFox (Windows, в том числе MSIX / Microsoft Store);</li>
<li>страницы политики и соглашения на сайте evilfox.win;</li>
<li>обращения в поддержку через указанные контакты.</li>
</ul>
<h4>2.2. Роли</h4>
<ul>
<li><strong>EvilFox (издатель клиента)</strong> — контролёр в отношении данных, которые Приложение хранит и обрабатывает <em>локально на устройстве</em> для работы клиента.</li>
<li><strong>Оператор серверов / провайдер конфигурации</strong> — отдельное лицо (вы сами или ваш поставщик). Когда трафик проходит через выбранные вами серверы, обработка на стороне этих серверов регулируется их политиками. EvilFox как издатель клиента не обязательно является оператором этих серверов.</li>
<li><strong>Microsoft</strong> — при установке и обновлении из Microsoft Store действуют политики Microsoft в части Store, учётной записи Microsoft и платёжных операций Store (если применимо).</li>
</ul>
<h3 id="s3">3. Какие данные обрабатываются</h3>
<h4>3.1. Данные на вашем устройстве (локально)</h4>
<p>Приложение может сохранять на устройстве (файлы конфигурации, каталог данных приложения, localStorage WebView2 и аналогичные хранилища ОС):</p>
<table>
<thead><tr><th>Категория</th><th>Примеры</th><th>Зачем</th></tr></thead>
<tbody>
<tr><td>Конфигурации подключения</td><td>URL конфигурации, профили серверов, параметры протоколов, которые вы вводите или импортируете</td><td>Подключение и выбор сервера</td></tr>
<tr><td>Настройки приложения</td><td>Тема (светлая/тёмная), язык (RU/EN), режим прокси или полной защиты, Kill Switch, автовыбор лучшего сервера</td><td>Сохранение ваших предпочтений</td></tr>
<tr><td>Технические сведения о сессии</td><td>Статус подключения, выбранный профиль, пинг, локальная статистика сессий/трафика (если доступна)</td><td>Отображение статуса и статистики</td></tr>
<tr><td>Служебные файлы ОС/WebView2</td><td>Кэш WebView2, журналы ошибок на устройстве</td><td>Стабильная работа интерфейса</td></tr>
</tbody>
</table>
<h4>3.2. Данные при сетевом взаимодействии</h4>
<ul>
<li><strong>С выбранными вами серверами:</strong> для установления защищённого соединения передаются технические параметры, необходимые протоколу (адреса, ключи/токены из вашей конфигурации, служебный трафик туннеля). Содержимое вашего пользовательского трафика обрабатывается на пути через выбранный сервер — см. раздел 6.</li>
<li><strong>При проверке обновлений (не Store-сборка или по вашей инициативе):</strong> запрос к ленте обновлений (например, хостинг git.de4ima.uk) может включать стандартные технические данные HTTP-запроса (IP-адрес на стороне сервера хостинга, User-Agent, время запроса).</li>
<li><strong>При открытии внешних ссылок:</strong> браузер или система передаёт данные соответствующему сайту/мессенджеру по их правилам.</li>
</ul>
<h4>3.3. Данные, которые вы можете прислать в поддержку</h4>
<p>Если вы пишете нам (почта / Telegram / сайт), вы можете добровольно указать адрес, описание проблемы, скриншоты. Мы используем их только для ответа на обращение и улучшения продукта.</p>
<h3 id="s4">4. Что мы не собираем централизованно</h3>
<p>EvilFox как издатель клиента <strong>не ведёт</strong> обязательный централизованный сбор и не продаёт:</p>
<ul>
<li>реестр аккаунтов пользователей клиента «в облаке EvilFox» для обычной работы приложения;</li>
<li>платёжные карты и данные покупок внутри приложения (в Store-сборке покупок в приложении нет; покупки Store, если есть, обрабатывает Microsoft);</li>
<li>историю браузера, контакты, микрофон/камеру по умолчанию;</li>
<li>содержимое вашего интернет-трафика для рекламы или перепродажи данных.</li>
</ul>
<p>Мы <strong>не продаём</strong> персональные данные.</p>
<h3 id="s5">5. Цели и правовые основания</h3>
<table>
<thead><tr><th>Цель</th><th>Основание (GDPR / аналоги)</th></tr></thead>
<tbody>
<tr><td>Предоставление функций клиента (подключение, настройки, интерфейс)</td><td>Исполнение договора / запрос пользователя на предоставление сервиса клиента</td></tr>
<tr><td>Безопасность и стабильность (локальные логи, предотвращение сбоев)</td><td>Законный интерес / необходимость для работы продукта</td></tr>
<tr><td>Проверка обновлений (по действию пользователя или политике сборки)</td><td>Законный интерес / согласие-действие пользователя</td></tr>
<tr><td>Ответы на обращения в поддержку</td><td>Законный интерес / исполнение запроса субъекта</td></tr>
<tr><td>Соблюдение закона и защита прав</td><td>Правовая обязанность / законный интерес</td></tr>
</tbody>
</table>
<h3 id="s6">6. Сетевой трафик и сторонние серверы</h3>
<p>После подключения ваш трафик направляется согласно выбранной конфигурации. Оператор узла может обрабатывать IP-адреса, метаданные сессии и (в зависимости от протокола и настроек) содержимое трафика. EvilFox не контролирует политику каждого стороннего сервера.</p>
<p><strong>Рекомендация:</strong> используйте только доверенные конфигурации; читайте политику конфиденциальности и условия вашего провайдера серверов.</p>
<p><strong>Режим полной защиты (TUN) и Kill Switch:</strong> эти функции изменяют сетевую маршрутизацию на устройстве для защиты соединения и снижения утечек при обрыве. Они работают локально на вашей системе.</p>
<h3 id="s7">7. Обновления приложения</h3>
<ul>
<li><strong>Microsoft Store:</strong> обновления обычно доставляет Store; встроенное самообновление в MSIX-сборке отключено или ограничено.</li>
<li><strong>Иные каналы (sideload):</strong> по вашей инициативе клиент может запросить манифест обновлений с указанного издателем URL (в т.ч. на git.de4ima.uk). Сервер хостинга может видеть IP и технические заголовки запроса.</li>
</ul>
<h3 id="s8">8. Внешние ссылки и сторонние сервисы</h3>
<p>Приложение может открывать по вашему действию:</p>
<ul>
<li><a href="https://evilfox.win/">https://evilfox.win/</a> — сайт издателя;</li>
<li><a href="https://t.me/evilfox100Bot">https://t.me/evilfox100Bot</a> — поддержка в Telegram (действует политика Telegram).</li>
</ul>
<p>Также на устройстве используются компоненты ОС и Microsoft Edge WebView2 — к ним применяются политики Microsoft.</p>
<h3 id="s9">9. Хранение, удаление и безопасность</h3>
<h4>9.1. Срок хранения</h4>
<p>Локальные данные хранятся, пока вы пользуетесь Приложением, либо до удаления профилей, очистки данных приложения или удаления EvilFox. Данные обращений в поддержку храним столько, сколько нужно для решения вопроса (как правило, не дольше, чем требуется для переписки и учёта обращений).</p>
<h4>9.2. Как удалить данные</h4>
<ul>
<li>удалить конфигурации/профили в приложении;</li>
<li>удалить приложение EvilFox в Windows;</li>
<li>при необходимости очистить остаточные папки данных приложения вручную.</li>
</ul>
<p>Центрального «облачного аккаунта EvilFox» для восстановления локальных профилей нет — удалённое с устройства мы не можем вернуть с наших серверов.</p>
<h4>9.3. Безопасность</h4>
<p>Мы применяем разумные организационные и технические меры в рамках клиентского приложения и ОС. Абсолютной безопасности не существует. Вы отвечаете за сохранность URL, ключей и файлов конфигурации на устройстве и за физический доступ к компьютеру.</p>
<h3 id="s10">10. Передачи данных и международные аспекты</h3>
<p>Клиент работает локально. Передача данных в другие страны может происходить, когда:</p>
<ul>
<li>вы подключаетесь к серверам за рубежом (по вашей конфигурации);</li>
<li>вы открываете зарубежные сайты/сервисы;</li>
<li>запрос обновлений обрабатывается инфраструктурой хостинга за пределами вашей страны.</li>
</ul>
<p>Для пользователей в ЕС/ЕЭЗ/Великобритании: такие передачи связаны с вашим выбором конфигурации и сервисов. При необходимости операторы серверов должны обеспечивать надлежащие гарантии (например, стандартные договорные условия) — уточняйте у провайдера конфигурации.</p>
<h3 id="s11">11. Ваши права (ЕС / GDPR и аналоги)</h3>
<p>В зависимости от применимого права вы можете иметь права на:</p>
<ul>
<li>доступ к данным и получение копии;</li>
<li>исправление неточных данных;</li>
<li>удаление («право на забвение») — в т.ч. путём удаления приложения и локальных данных;</li>
<li>ограничение обработки и возражение против обработки на основе законного интереса;</li>
<li>переносимость данных — в той мере, в какой данные хранятся в структурированном виде на устройстве (экспорт конфигураций, если доступен);</li>
<li>жалобу в надзорный орган по месту жительства/работы в ЕС/ЕЭЗ/Великобритании.</li>
</ul>
<p>Поскольку основная обработка идёт на устройстве, большинство прав реализуется вами локально. По вопросам, связанным с этой Политикой, пишите на контакты ниже. Мы ответим в разумный срок.</p>
<h3 id="s12">12. Калифорния (CCPA/CPRA) и пользователи в США</h3>
<p>Мы <strong>не продаём</strong> и <strong>не «делимся»</strong> персональной информацией для контекстной кросс-рекламы в смысле CCPA/CPRA. Категории данных описаны в разделе 3. Источники — в основном вы (ввод конфигураций) и устройство. Мы не используем чувствительные данные для вывода характеристик потребителя в рекламных целях.</p>
<p>Права на знание, удаление и исправление в отношении локальных данных клиента осуществляются на устройстве (удаление приложения/данных). Запросы, связанные с перепиской поддержки, можно направить через контакты в разделе 16.</p>
<p>Мы не осуществляем дискриминацию за реализацию конфиденциальных прав.</p>
<h3 id="s13">13. Дети</h3>
<p>Приложение не предназначено для детей младше <strong>13 лет</strong>. В ЕС/ЕЭЗ, где местное право устанавливает более высокий возраст цифрового согласия (часто до <strong>16 лет</strong>), не используйте Приложение ниже этого возраста без согласия родителя или опекуна, если оно требуется. Мы сознательно не собираем данные детей централизованно. Если вы считаете, что ребёнок передал нам данные через поддержку, свяжитесь с нами — мы удалим обращение, где это уместно.</p>
<h3 id="s14">14. Microsoft Store</h3>
<p>При распространении через Microsoft Store корпорация Microsoft может обрабатывать данные установки, обновлений, отзывов и (при платных продуктах) платежей согласно своим политикам. Для возможностей вроде runFullTrust / классического рабочего стола могут применяться требования Store к прозрачности и разрешению ограниченных возможностей.</p>
<h3 id="s15">15. Изменения политики</h3>
<p>Мы можем обновлять Политику. Актуальная версия публикуется на сайте (эта страница) и/или в разделе «Помощь» приложения. Существенные изменения по возможности отражаются датой в начале документа. Продолжение использования Приложения после обновления означает, что вы можете ознакомиться с новой редакцией.</p>
<h3 id="s16">16. Контакты</h3>
<p>
<strong>EvilFox</strong><br />
Сайт: <a href="https://evilfox.win/">https://evilfox.win/</a><br />
Основная почта: <a href="mailto:animegold3@gmail.com">animegold3@gmail.com</a><br />
Резервная почта: <a href="mailto:admin@evilfox.win">admin@evilfox.win</a><br />
Telegram: <a href="https://t.me/evilfox100Bot">https://t.me/evilfox100Bot</a><br />
Пользовательское соглашение: <a href="terms-ru.html">terms-ru.html</a>
</p>
<p class="nav-docs">English: <a href="privacy-en.html">Privacy Policy</a></p>
</article>
<footer>
<span>© 2026 EvilFox</span>
<a href="terms-ru.html">Пользовательское соглашение</a>
<a href="https://evilfox.win/">Сайт</a>
</footer>
</div>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta http-equiv="refresh" content="0; url=privacy-ru.html" />
<title>EvilFox — Privacy</title>
<link rel="canonical" href="privacy-ru.html" />
</head>
<body>
<p><a href="privacy-ru.html">Политика конфиденциальности (RU)</a> · <a href="privacy-en.html">Privacy Policy (EN)</a></p>
</body>
</html>
+144
View File
@@ -0,0 +1,144 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Terms of Use — EvilFox</title>
<meta name="description" content="EvilFox Terms of Use (User Agreement)." />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700&family=Outfit:wght@700;800&display=swap" rel="stylesheet" />
<style>
:root {
--bg: #f4fafb;
--ink: #0f172a;
--muted: #64748b;
--card: #ffffff;
--line: rgba(15, 23, 42, 0.1);
--accent: #0e7490;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: Manrope, system-ui, sans-serif;
color: var(--ink);
background:
radial-gradient(900px 420px at 10% -10%, rgba(34, 211, 238, 0.18), transparent 55%),
radial-gradient(700px 360px at 100% 0%, rgba(14, 116, 144, 0.12), transparent 50%),
var(--bg);
line-height: 1.65;
}
.wrap { max-width: 820px; margin: 0 auto; padding: 32px 20px 64px; }
header {
display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
gap: 16px; margin-bottom: 28px;
}
.brand { display: flex; align-items: center; gap: 12px; text-decoration: none; color: inherit; }
.logo {
width: 44px; height: 44px; border-radius: 12px;
display: grid; place-items: center;
background: linear-gradient(145deg, #0e7490, #22d3ee);
box-shadow: 0 10px 24px rgba(8, 145, 178, 0.28);
font-size: 1.4rem;
}
.brand h1 {
font-family: Outfit, sans-serif; font-size: 1.45rem; font-weight: 800;
letter-spacing: -0.04em; margin: 0;
}
.brand span { display: block; font-size: .78rem; color: var(--muted); font-weight: 600; }
.langs { display: flex; gap: 8px; }
.langs a {
text-decoration: none; font-weight: 700; font-size: .85rem;
padding: 8px 14px; border-radius: 999px; border: 1px solid var(--line);
color: var(--muted); background: var(--card);
}
.langs a.active, .langs a:hover {
color: #fff; background: var(--accent); border-color: var(--accent);
}
.card {
background: var(--card); border: 1px solid var(--line); border-radius: 20px;
padding: 28px 26px; box-shadow: 0 18px 40px rgba(15, 23, 42, 0.06);
}
h2 {
font-family: Outfit, sans-serif; font-size: 1.65rem; letter-spacing: -0.03em;
margin: 0 0 8px;
}
.meta { color: var(--muted); font-size: .92rem; margin: 0 0 24px; }
h3 { font-size: 1.05rem; margin: 22px 0 8px; color: var(--accent); }
p { margin: 0 0 12px; }
footer {
margin-top: 28px; font-size: .88rem; color: var(--muted);
display: flex; flex-wrap: wrap; gap: 12px 20px;
}
footer a { color: var(--accent); font-weight: 700; text-decoration: none; }
footer a:hover { text-decoration: underline; }
.nav-docs { margin-top: 18px; font-size: .9rem; }
.nav-docs a { color: var(--accent); font-weight: 700; }
</style>
</head>
<body>
<div class="wrap">
<header>
<a class="brand" href="https://evilfox.win/">
<div class="logo" aria-hidden="true">🦊</div>
<div>
<h1>EvilFox</h1>
<span>evilfox.win</span>
</div>
</a>
<nav class="langs" aria-label="Language">
<a href="terms-ru.html" lang="ru">Русский</a>
<a class="active" href="terms-en.html" lang="en">English</a>
</nav>
</header>
<article class="card">
<h2>Terms of Use (User Agreement)</h2>
<p class="meta">Publisher: EvilFox · Effective date: August 2, 2026</p>
<h3>1. Acceptance</h3>
<p>By installing or using the EvilFox application (the “App”), you confirm that you have read and accept this User Agreement (Terms of Use). If you do not agree, do not install or use the App.</p>
<h3>2. License</h3>
<p>You receive a limited, non-exclusive, non-transferable license to use the EvilFox client on your devices for personal or internal use, subject to these Terms and applicable law, including Microsoft Store rules if the App is installed from the Microsoft Store.</p>
<h3>3. Acceptable use</h3>
<p>Use EvilFox only for lawful purposes. You must not violate the laws of your country or third-party rights, or attempt to disrupt the App or others systems. You are responsible for how you use the client and the servers you choose.</p>
<h3>4. User-supplied configurations</h3>
<p>You provide connection URLs and parameters. EvilFox does not guarantee availability, quality, or legality of third-party servers. Review your configuration providers terms. Configurations are not part of a Microsoft Store purchase (where applicable).</p>
<h3>5. No warranty</h3>
<p>The App is provided “as is” and “as available,” without warranties of any kind (express or implied, including merchantability and fitness for a particular purpose), to the fullest extent permitted by law.</p>
<h3>6. Limitation of liability</h3>
<p>To the maximum extent permitted by law, EvilFox is not liable for indirect, incidental, special, or consequential damages, lost profits, data loss, or connectivity interruptions arising from use of the client or third-party servers. Mandatory consumer rights (including in the EU) remain unaffected.</p>
<h3>7. Updates</h3>
<p>Microsoft Store builds update through the Store. Other (sideload) builds may offer updates via the publishers update feed. Features may change in new versions.</p>
<h3>8. Termination</h3>
<p>You may stop using the App by uninstalling it. We may discontinue support for particular builds. If you breach these Terms, your right to use the App ends.</p>
<h3>9. Governing law</h3>
<p>These Terms are interpreted under the laws of your country of residence unless mandatory rules require otherwise. EU consumers keep mandatory rights under the law of their country of residence.</p>
<h3>10. Contact</h3>
<p>
EvilFox · <a href="https://evilfox.win/">https://evilfox.win/</a><br />
Primary: <a href="mailto:animegold3@gmail.com">animegold3@gmail.com</a> ·
Backup: <a href="mailto:admin@evilfox.win">admin@evilfox.win</a> ·
Telegram: <a href="https://t.me/evilfox100Bot">https://t.me/evilfox100Bot</a>
</p>
<p class="nav-docs">See also: <a href="privacy-en.html">Privacy Policy</a></p>
</article>
<footer>
<span>© 2026 EvilFox</span>
<a href="terms-ru.html">Русская версия</a>
<a href="https://evilfox.win/">Website</a>
</footer>
</div>
</body>
</html>
+146
View File
@@ -0,0 +1,146 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Пользовательское соглашение — EvilFox</title>
<meta name="description" content="Пользовательское соглашение (условия использования) приложения EvilFox." />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700&family=Outfit:wght@700;800&display=swap" rel="stylesheet" />
<style>
:root {
--bg: #f4fafb;
--ink: #0f172a;
--muted: #64748b;
--card: #ffffff;
--line: rgba(15, 23, 42, 0.1);
--accent: #0e7490;
--accent-soft: rgba(14, 116, 144, 0.1);
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: Manrope, system-ui, sans-serif;
color: var(--ink);
background:
radial-gradient(900px 420px at 10% -10%, rgba(34, 211, 238, 0.18), transparent 55%),
radial-gradient(700px 360px at 100% 0%, rgba(14, 116, 144, 0.12), transparent 50%),
var(--bg);
line-height: 1.65;
}
.wrap { max-width: 820px; margin: 0 auto; padding: 32px 20px 64px; }
header {
display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
gap: 16px; margin-bottom: 28px;
}
.brand { display: flex; align-items: center; gap: 12px; text-decoration: none; color: inherit; }
.logo {
width: 44px; height: 44px; border-radius: 12px;
display: grid; place-items: center;
background: linear-gradient(145deg, #0e7490, #22d3ee);
box-shadow: 0 10px 24px rgba(8, 145, 178, 0.28);
font-size: 1.4rem;
}
.brand h1 {
font-family: Outfit, sans-serif; font-size: 1.45rem; font-weight: 800;
letter-spacing: -0.04em; margin: 0;
}
.brand span { display: block; font-size: .78rem; color: var(--muted); font-weight: 600; }
.langs { display: flex; gap: 8px; }
.langs a {
text-decoration: none; font-weight: 700; font-size: .85rem;
padding: 8px 14px; border-radius: 999px; border: 1px solid var(--line);
color: var(--muted); background: var(--card);
}
.langs a.active, .langs a:hover {
color: #fff; background: var(--accent); border-color: var(--accent);
}
.card {
background: var(--card); border: 1px solid var(--line); border-radius: 20px;
padding: 28px 26px; box-shadow: 0 18px 40px rgba(15, 23, 42, 0.06);
}
h2 {
font-family: Outfit, sans-serif; font-size: 1.65rem; letter-spacing: -0.03em;
margin: 0 0 8px;
}
.meta { color: var(--muted); font-size: .92rem; margin: 0 0 24px; }
h3 { font-size: 1.05rem; margin: 22px 0 8px; color: var(--accent); }
p, li { margin: 0 0 12px; }
ul { padding-left: 1.2rem; margin: 0 0 12px; }
footer {
margin-top: 28px; font-size: .88rem; color: var(--muted);
display: flex; flex-wrap: wrap; gap: 12px 20px;
}
footer a { color: var(--accent); font-weight: 700; text-decoration: none; }
footer a:hover { text-decoration: underline; }
.nav-docs { margin-top: 18px; font-size: .9rem; }
.nav-docs a { color: var(--accent); font-weight: 700; }
</style>
</head>
<body>
<div class="wrap">
<header>
<a class="brand" href="https://evilfox.win/">
<div class="logo" aria-hidden="true">🦊</div>
<div>
<h1>EvilFox</h1>
<span>evilfox.win</span>
</div>
</a>
<nav class="langs" aria-label="Language">
<a class="active" href="terms-ru.html" lang="ru">Русский</a>
<a href="terms-en.html" lang="en">English</a>
</nav>
</header>
<article class="card">
<h2>Пользовательское соглашение</h2>
<p class="meta">Издатель: EvilFox · Дата вступления в силу: 2 августа 2026 г.</p>
<h3>1. Принятие условий</h3>
<p>Устанавливая или используя приложение EvilFox («Приложение»), вы подтверждаете, что прочитали и принимаете настоящее Пользовательское соглашение (условия использования). Если вы не согласны с условиями — не устанавливайте и не используйте Приложение.</p>
<h3>2. Лицензия</h3>
<p>Вам предоставляется ограниченная, неисключительная, непередаваемая лицензия на использование клиента EvilFox на ваших устройствах в личных или внутренних целях в соответствии с настоящим Соглашением и применимым правом, включая правила Microsoft Store (если Приложение установлено из Microsoft Store).</p>
<h3>3. Допустимое использование</h3>
<p>Используйте EvilFox только законно. Запрещены действия, нарушающие законы вашей страны или права третьих лиц, а также попытки нарушить работу Приложения или чужих систем. Вы несёте ответственность за то, как используете клиент и выбранные серверы.</p>
<h3>4. Конфигурации пользователя</h3>
<p>URL и параметры подключения задаёте вы. EvilFox не гарантирует доступность, качество или законность сторонних серверов. Ознакомьтесь с условиями вашего провайдера конфигурации. Конфигурации не являются частью покупки в Microsoft Store (если применимо).</p>
<h3>5. Отсутствие гарантий</h3>
<p>Приложение предоставляется «как есть» и «по мере доступности», без явных или подразумеваемых гарантий (включая товарную пригодность и пригодность для конкретной цели), в пределах, допускаемых законом.</p>
<h3>6. Ограничение ответственности</h3>
<p>В максимальной степени, допускаемой законом, EvilFox не несёт ответственности за косвенные, случайные, особые убытки, упущенную выгоду, потерю данных или перебои связи, связанные с использованием клиента или сторонних серверов. Обязательные права потребителей (в том числе в ЕС) сохраняются.</p>
<h3>7. Обновления</h3>
<p>Сборка из Microsoft Store обновляется через Store. Иные (sideload) сборки могут предлагать обновление через ленту обновлений издателя. Функции могут меняться в новых версиях.</p>
<h3>8. Прекращение</h3>
<p>Вы можете прекратить использование, удалив Приложение. Мы можем прекратить поддержку отдельных сборок. При нарушении Соглашения право на использование Приложения прекращается.</p>
<h3>9. Применимое право</h3>
<p>Настоящее Соглашение толкуется по законам страны вашего проживания, если иное не требуется императивными нормами. Для потребителей в ЕС сохраняются обязательные права по праву страны проживания.</p>
<h3>10. Контакты</h3>
<p>
EvilFox · <a href="https://evilfox.win/">https://evilfox.win/</a><br />
Основная: <a href="mailto:animegold3@gmail.com">animegold3@gmail.com</a> ·
Резервная: <a href="mailto:admin@evilfox.win">admin@evilfox.win</a> ·
Telegram: <a href="https://t.me/evilfox100Bot">https://t.me/evilfox100Bot</a>
</p>
<p class="nav-docs">См. также: <a href="privacy-ru.html">Политика конфиденциальности</a></p>
</article>
<footer>
<span>© 2026 EvilFox</span>
<a href="terms-en.html">English version</a>
<a href="https://evilfox.win/">Сайт</a>
</footer>
</div>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta http-equiv="refresh" content="0; url=terms-ru.html" />
<title>EvilFox — Terms</title>
<link rel="canonical" href="terms-ru.html" />
</head>
<body>
<p><a href="terms-ru.html">Пользовательское соглашение (RU)</a> · <a href="terms-en.html">Terms of Use (EN)</a></p>
</body>
</html>
+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_080_201 versionCode = 4_028_001
versionName = "3.8.2+1" versionName = "4.2.8+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>
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity <assemblyIdentity
version="3.8.2.1" version="4.2.8.1"
processorArchitecture="*" processorArchitecture="*"
name="EvilFox.Navis" name="EvilFox.Navis"
type="win32"/> type="win32"/>
+13 -8
View File
@@ -14,22 +14,27 @@ 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... echo Building EvilFox GUI and CLI...
go build -ldflags="-H windowsgui -s -w" -trimpath -o Navis.exe ./cmd/vpnapp 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)
} }
+112 -22
View File
@@ -1,4 +1,4 @@
//go:build windows //go:build windows
package main package main
@@ -17,6 +17,7 @@ import (
"github.com/jchv/go-webview2" "github.com/jchv/go-webview2"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
"vpnclient/internal/apphost"
"vpnclient/internal/appui" "vpnclient/internal/appui"
"vpnclient/internal/config" "vpnclient/internal/config"
"vpnclient/internal/core" "vpnclient/internal/core"
@@ -25,6 +26,7 @@ import (
"vpnclient/internal/protocols/hysteria2" "vpnclient/internal/protocols/hysteria2"
"vpnclient/internal/protocols/naive" "vpnclient/internal/protocols/naive"
"vpnclient/internal/protocols/xray" "vpnclient/internal/protocols/xray"
"vpnclient/internal/remnawave"
"vpnclient/internal/update" "vpnclient/internal/update"
) )
@@ -55,8 +57,17 @@ type uiState struct {
Update update.Status `json:"update"` Update update.Status `json:"update"`
Pings []netcheck.Result `json:"pings"` Pings []netcheck.Result `json:"pings"`
Subscription string `json:"subscription_url"` Subscription string `json:"subscription_url"`
SubInfo *config.SubscriptionInfo `json:"sub_info,omitempty"`
Remnawave remnawave.Settings `json:"remnawave"`
Hy2 core.Hy2Options `json:"hy2"` Hy2 core.Hy2Options `json:"hy2"`
StorePackaged bool `json:"store_packaged,omitempty"` StorePackaged bool `json:"store_packaged,omitempty"`
Mode string `json:"mode"`
VPNSupported bool `json:"vpn_mode_supported"`
VPNActive bool `json:"vpn_active"`
// KillSwitch — persisted setting; KillSwitchActive — tunnel died and
// traffic is intentionally blocked until disconnect / reconnect.
KillSwitch bool `json:"kill_switch"`
KillSwitchActive bool `json:"kill_switch_active,omitempty"`
} }
func main() { func main() {
@@ -65,6 +76,10 @@ func main() {
} }
update.CleanupStaleDownloads() update.CleanupStaleDownloads()
// Before any HWND: declare DPI awareness so CreateWindow/SetSize sizes
// are physical pixels and we can convert DIPs correctly.
enableProcessDPIAware()
log.SetFlags(log.LstdFlags | log.Lshortfile) log.SetFlags(log.LstdFlags | log.Lshortfile)
cfgPath, err := config.LocateConfig() cfgPath, err := config.LocateConfig()
@@ -103,14 +118,17 @@ func main() {
// Do NOT auto-download cores on startup — silent network+write looks like malware to AV. // Do NOT auto-download cores on startup — silent network+write looks like malware to AV.
// Size from monitor work area in DIPs, converted to physical pixels for
// DPI-aware CreateWindow (fixed 920x680 physical collapses CSS <700px on HiDPI).
initW, initH := initialWindowPhysicalSize()
w := webview2.NewWithOptions(webview2.WebViewOptions{ w := webview2.NewWithOptions(webview2.WebViewOptions{
Debug: false, Debug: false,
AutoFocus: true, AutoFocus: true,
DataPath: dataPath, DataPath: dataPath,
WindowOptions: webview2.WindowOptions{ WindowOptions: webview2.WindowOptions{
Title: "Navis 2", Title: "EvilFox",
Width: 500, Width: uint(initW),
Height: 900, Height: uint(initH),
Center: true, Center: true,
IconId: 1, IconId: 1,
}, },
@@ -125,7 +143,7 @@ func main() {
}() }()
applyWindowIcon(w.Window()) applyWindowIcon(w.Window())
w.SetSize(500, 900, webview2.HintNone) applyAdaptiveWindowSize(w)
mustBind(w, "getState", a.getState) mustBind(w, "getState", a.getState)
mustBind(w, "connect", a.connect) mustBind(w, "connect", a.connect)
@@ -138,11 +156,17 @@ func main() {
mustBind(w, "installCore", a.installCore) mustBind(w, "installCore", a.installCore)
mustBind(w, "openURL", openURL) mustBind(w, "openURL", openURL)
mustBind(w, "pingServers", a.pingServers) mustBind(w, "pingServers", a.pingServers)
mustBind(w, "pingServer", a.pingServer)
mustBind(w, "pingBest", a.pingBest) mustBind(w, "pingBest", a.pingBest)
mustBind(w, "setKillSwitch", a.setKillSwitch)
mustBind(w, "checkUpdate", a.checkUpdate) mustBind(w, "checkUpdate", a.checkUpdate)
mustBind(w, "applyUpdate", a.applyUpdate) mustBind(w, "applyUpdate", a.applyUpdate)
mustBind(w, "saveHy2", a.saveHy2) mustBind(w, "saveHy2", a.saveHy2)
mustBind(w, "importSubscription", a.importSubscription) mustBind(w, "importSubscription", a.importSubscription)
mustBind(w, "saveRemnawave", a.saveRemnawave)
mustBind(w, "importRemnawave", a.importRemnawave)
mustBind(w, "setMode", a.setMode)
mustBind(w, "getLogs", a.getLogs)
go a.autoCheckUpdate() go a.autoCheckUpdate()
@@ -209,8 +233,15 @@ func (a *app) getState() (uiState, error) {
Update: a.updateStatus, Update: a.updateStatus,
Pings: append([]netcheck.Result(nil), a.pings...), Pings: append([]netcheck.Result(nil), a.pings...),
Subscription: cfg.SubscriptionURL, Subscription: cfg.SubscriptionURL,
SubInfo: a.mgr.SubscriptionInfo(),
Remnawave: a.mgr.RemnawaveSettings(),
Hy2: a.mgr.ActiveHy2Options(), Hy2: a.mgr.ActiveHy2Options(),
StorePackaged: update.IsStorePackaged(), StorePackaged: update.IsStorePackaged(),
Mode: a.mgr.Mode(),
VPNSupported: a.mgr.VPNModeSupported(),
VPNActive: st.VPNActive,
KillSwitch: cfg.KillSwitch,
KillSwitchActive: st.KillSwitchActive,
} }
if out.Protocol == "" { if out.Protocol == "" {
if p, err := cfg.ActiveProfile(); err == nil { if p, err := cfg.ActiveProfile(); err == nil {
@@ -322,10 +353,34 @@ func (a *app) saveHy2(opts core.Hy2Options) error {
return a.mgr.SaveHy2Options(opts) return a.mgr.SaveHy2Options(opts)
} }
func (a *app) importSubscription(rawURL string) (int, error) { func (a *app) importSubscription(rawURL string) (core.ImportResult, error) {
return a.mgr.ImportSubscription(rawURL) return a.mgr.ImportSubscription(rawURL)
} }
func (a *app) saveRemnawave(s remnawave.Settings) error {
return a.mgr.SaveRemnawaveSettings(s)
}
func (a *app) importRemnawave(s remnawave.Settings) (core.ImportResult, error) {
return a.mgr.ImportRemnawave(s)
}
func (a *app) setMode(mode string) error {
return a.mgr.SetMode(strings.TrimSpace(mode))
}
func (a *app) getLogs() (string, error) {
if a.logBuf == nil {
return "", nil
}
const maxTail = 64 * 1024
b := a.logBuf.Bytes()
if len(b) > maxTail {
b = b[len(b)-maxTail:]
}
return string(b), nil
}
type pingBestResult struct { type pingBestResult struct {
Pings []netcheck.Result `json:"pings"` Pings []netcheck.Result `json:"pings"`
BestName string `json:"best_name,omitempty"` BestName string `json:"best_name,omitempty"`
@@ -339,6 +394,50 @@ func (a *app) pingServers() ([]netcheck.Result, error) {
return res, err return res, err
} }
// pingServer measures latency of one server (📊 button on a server row)
// and merges the result into the cached ping list.
func (a *app) pingServer(name string) (netcheck.Result, error) {
name = strings.TrimSpace(name)
a.mu.Lock()
list := a.mgr.Profiles()
a.mu.Unlock()
var target *netcheck.Target
for _, p := range list {
if p.Name == name {
target = &netcheck.Target{Name: p.Name, Protocol: config.Protocol(p.Protocol), Proxy: p.Proxy}
break
}
}
if target == nil {
return netcheck.Result{}, fmt.Errorf("сервер %q не найден", name)
}
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
defer cancel()
out := netcheck.PingAll(ctx, []netcheck.Target{*target})
if len(out) == 0 {
return netcheck.Result{}, fmt.Errorf("не удалось измерить пинг")
}
res := out[0]
a.mu.Lock()
replaced := false
for i := range a.pings {
if a.pings[i].Name == res.Name {
a.pings[i] = res
replaced = true
break
}
}
if !replaced {
a.pings = append(a.pings, res)
}
a.mu.Unlock()
return res, nil
}
func (a *app) setKillSwitch(enabled bool) error {
return a.mgr.SetKillSwitch(enabled)
}
// pingBest measures all nodes, activates the fastest OK one, and optionally connects. // pingBest measures all nodes, activates the fastest OK one, and optionally connects.
func (a *app) pingBest(autoConnect bool) (pingBestResult, error) { func (a *app) pingBest(autoConnect bool) (pingBestResult, error) {
pings, err := a.runPings() pings, err := a.runPings()
@@ -448,12 +547,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() {
@@ -476,21 +575,12 @@ func (a *app) autoCheckUpdate() {
} }
func openURL(raw string) error { func openURL(raw string) error {
raw = strings.TrimSpace(raw) // Shared allowlist: shop + «Полезные ссылки» hosts only.
if raw == "" { normalized, err := apphost.LinkAllowed(raw)
return fmt.Errorf("пустая ссылка") if err != nil {
return err
} }
lower := strings.ToLower(raw) return shellOpen(normalized)
switch {
case lower == "https://evilfox.win/" || lower == "https://evilfox.win" ||
lower == "http://evilfox.win/" || lower == "http://evilfox.win":
raw = "https://evilfox.win/"
case strings.HasPrefix(lower, "https://evilfox.win/") || strings.HasPrefix(lower, "http://evilfox.win/"):
// allow shop deep-links on evilfox.win only
default:
return fmt.Errorf("разрешена только ссылка evilfox.win")
}
return shellOpen(raw)
} }
func shellOpen(raw string) error { func shellOpen(raw string) error {
+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
} }
+215
View File
@@ -0,0 +1,215 @@
//go:build windows
package main
import (
"unsafe"
"github.com/jchv/go-webview2"
"golang.org/x/sys/windows"
)
// DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = (DPI_AWARENESS_CONTEXT)-4
const dpiAwarenessContextPerMonitorAwareV2 = ^uintptr(3)
const (
spiGetWorkArea = 0x0030
monitorDefaultToNearest = 2
)
type monitorInfo struct {
CbSize uint32
RcMonitor windows.Rect
RcWork windows.Rect
DwFlags uint32
}
// enableProcessDPIAware opts into per-monitor DPI before any HWND is created.
// The embedded app.manifest already declares PerMonitorV2; this is a belt-and-
// suspenders call for builds that lose the manifest resource.
func enableProcessDPIAware() {
user32 := windows.NewLazySystemDLL("user32.dll")
if p := user32.NewProc("SetProcessDpiAwarenessContext"); p.Find() == nil {
_, _, _ = p.Call(dpiAwarenessContextPerMonitorAwareV2)
return
}
if p := user32.NewProc("SetProcessDPIAware"); p.Find() == nil {
_, _, _ = p.Call()
}
}
func systemDPI() int {
user32 := windows.NewLazySystemDLL("user32.dll")
if p := user32.NewProc("GetDpiForSystem"); p.Find() == nil {
if r, _, _ := p.Call(); r >= 72 && r <= 480 {
return int(r)
}
}
return 96
}
func windowDPI(hwnd uintptr) int {
if hwnd != 0 {
user32 := windows.NewLazySystemDLL("user32.dll")
if p := user32.NewProc("GetDpiForWindow"); p.Find() == nil {
if r, _, _ := p.Call(hwnd); r >= 72 && r <= 480 {
return int(r)
}
}
}
return systemDPI()
}
func dipsToPhysical(dips, dpi int) int {
if dpi <= 0 {
dpi = 96
}
return (dips*dpi + 48) / 96
}
func physicalToDIPs(px, dpi int) int {
if dpi <= 0 {
dpi = 96
}
return (px*96 + dpi/2) / dpi
}
func primaryWorkAreaPhysical() (w, h int, ok bool) {
var r windows.Rect
user32 := windows.NewLazySystemDLL("user32.dll")
p := user32.NewProc("SystemParametersInfoW")
ret, _, _ := p.Call(spiGetWorkArea, 0, uintptr(unsafe.Pointer(&r)), 0)
if ret == 0 {
return 0, 0, false
}
return int(r.Right - r.Left), int(r.Bottom - r.Top), true
}
func monitorWorkAreaPhysical(hwnd uintptr) (w, h int, ok bool) {
if hwnd == 0 {
return primaryWorkAreaPhysical()
}
user32 := windows.NewLazySystemDLL("user32.dll")
fromWin := user32.NewProc("MonitorFromWindow")
getInfo := user32.NewProc("GetMonitorInfoW")
if fromWin.Find() != nil || getInfo.Find() != nil {
return primaryWorkAreaPhysical()
}
hmon, _, _ := fromWin.Call(hwnd, monitorDefaultToNearest)
if hmon == 0 {
return primaryWorkAreaPhysical()
}
var mi monitorInfo
mi.CbSize = uint32(unsafe.Sizeof(mi))
ret, _, _ := getInfo.Call(hmon, uintptr(unsafe.Pointer(&mi)))
if ret == 0 {
return primaryWorkAreaPhysical()
}
return int(mi.RcWork.Right - mi.RcWork.Left), int(mi.RcWork.Bottom - mi.RcWork.Top), true
}
// computeClientSizeDIPs picks a comfortable client size in DIPs from the
// monitor work area (also expressed as physical px + dpi).
// Target: ~min(1100, workW*0.72) x min(780, workH*0.78), floored near
// 960x680 when the screen allows, always capped with margins.
func computeClientSizeDIPs(workWPhys, workHPhys, dpi int) (w, h int) {
if dpi <= 0 {
dpi = 96
}
workW := physicalToDIPs(workWPhys, dpi)
workH := physicalToDIPs(workHPhys, dpi)
if workW < 320 {
workW = 320
}
if workH < 240 {
workH = 240
}
const marginX, marginY = 64, 64
maxW := workW - marginX
maxH := workH - marginY
if maxW < 480 {
maxW = workW
}
if maxH < 360 {
maxH = workH
}
if maxW < 480 {
maxW = 480
}
if maxH < 360 {
maxH = 360
}
prefW := int(float64(workW) * 0.72)
if prefW > 1100 {
prefW = 1100
}
prefH := int(float64(workH) * 0.78)
if prefH > 780 {
prefH = 780
}
if prefW < 960 && maxW >= 960 {
prefW = 960
}
if prefH < 680 && maxH >= 680 {
prefH = 680
}
if prefW > maxW {
prefW = maxW
}
if prefH > maxH {
prefH = maxH
}
if prefW < 640 && maxW >= 640 {
prefW = 640
}
if prefH < 480 && maxH >= 480 {
prefH = 480
}
return prefW, prefH
}
// initialWindowPhysicalSize estimates CreateWindow size before an HWND exists
// (primary work area + system DPI). go-webview2 treats Width/Height as physical
// pixels when the process is DPI-aware.
func initialWindowPhysicalSize() (w, h int) {
dpi := systemDPI()
ww, wh, ok := primaryWorkAreaPhysical()
if !ok || ww <= 0 || wh <= 0 {
return dipsToPhysical(960, dpi), dipsToPhysical(680, dpi)
}
dipW, dipH := computeClientSizeDIPs(ww, wh, dpi)
return dipsToPhysical(dipW, dpi), dipsToPhysical(dipH, dpi)
}
// applyAdaptiveWindowSize sets HintMin (~720x520 DIPs) then the preferred
// client size from the window's monitor work area, both in physical pixels.
func applyAdaptiveWindowSize(w webview2.WebView) {
if w == nil {
return
}
hwnd := uintptr(w.Window())
dpi := windowDPI(hwnd)
ww, wh, ok := monitorWorkAreaPhysical(hwnd)
if !ok || ww <= 0 || wh <= 0 {
ww, wh, _ = primaryWorkAreaPhysical()
}
dipW, dipH := computeClientSizeDIPs(ww, wh, dpi)
physW := dipsToPhysical(dipW, dpi)
physH := dipsToPhysical(dipH, dpi)
minW := dipsToPhysical(720, dpi)
minH := dipsToPhysical(520, dpi)
if minW > physW {
minW = physW
}
if minH > physH {
minH = physH
}
w.SetSize(minW, minH, webview2.HintMin)
w.SetSize(physW, physH, webview2.HintNone)
}
+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
+3
View File
@@ -3,6 +3,9 @@
"system_proxy": true, "system_proxy": true,
"bin_dir": "bin", "bin_dir": "bin",
"subscription_url": "", "subscription_url": "",
"remnawave_base_url": "",
"remnawave_short_uuid": "",
"remnawave_token": "",
"profiles": [ "profiles": [
{ {
"name": "naive-main", "name": "naive-main",
+15
View File
@@ -0,0 +1,15 @@
In addition to the standard Microsoft Store / app license terms:
1. EvilFox is a client for protected network connectivity. Server configurations and connection URLs are supplied by you and are not part of any Store purchase (where applicable).
2. You are responsible for using the app and chosen servers lawfully in your jurisdiction.
3. The publisher does not warrant uninterrupted operation of third-party servers, speed, or availability of specific nodes.
4. The app may store settings and configurations locally on your device. The Privacy Policy and full Terms of Use are available in the app Help section and at https://evilfox.win/
5. Updates delivered via the Microsoft Store replace in-app self-update in Store builds.
6. The runFullTrust / classic desktop capability is required for the client to function (WebView2, network modes).
Contact: https://evilfox.win/ · https://t.me/evilfox100Bot
+15
View File
@@ -0,0 +1,15 @@
Дополнительно к стандартным условиям лицензии Microsoft Store / приложения:
1. EvilFox — клиент для защищённого сетевого подключения. Конфигурации серверов и URL подключения предоставляются вами и не являются частью покупки в Store (если применимо).
2. Вы несёте ответственность за законность использования приложения и выбранных серверов в вашей юрисдикции.
3. Издатель не гарантирует бесперебойную работу сторонних серверов, скорость или доступность конкретных узлов.
4. Приложение может хранить настройки и конфигурации локально на устройстве. Политика конфиденциальности и полные условия использования доступны в разделе «Помощь» приложения и на https://evilfox.win/
5. Обновления через Microsoft Store заменяют самообновление приложения в сборке Store.
6. Возможность runFullTrust / классическое настольное приложение требуется для работы клиента (WebView2, сетевые режимы).
Контакты: https://evilfox.win/ · https://t.me/evilfox100Bot
+1
View File
@@ -0,0 +1 @@
© 2026 EvilFox. All rights reserved. EvilFox, the EvilFox logo, and related marks are trademarks or registered trademarks of EvilFox. Other product and company names mentioned in the app or product listing may be trademarks of their respective owners.
+1
View File
@@ -0,0 +1 @@
© 2026 EvilFox. Все права защищены. EvilFox, логотип EvilFox и связанные знаки являются товарными знаками или зарегистрированными товарными знаками EvilFox. Прочие названия продуктов и компаний, упомянутые в приложении или на странице продукта, могут быть товарными знаками соответствующих владельцев.
+57
View File
@@ -0,0 +1,57 @@
EvilFox Microsoft Store package folder
Version: 4.2.9.0
Generated: 2026-08-02 15:53:56
=== Partner Center upload (EN) ===
1. Sign in to Partner Center with the same Microsoft Store developer account
you use for Visual Studio packaging / Store association (same tenant).
2. Open the EvilFox product -> Packages (Windows 10 / 11).
3. Prefer uploading EvilFox_4.2.9.0.msixbundle if present.
Otherwise upload each EvilFox_4.2.9.0_<arch>.msix (x64 + arm64).
4. Device family: check Windows 10 / 11 Desktop ONLY
(do NOT select Xbox, HoloLens, Surface Hub, IoT, Team, etc.).
5. Arm64 package covers Windows on ARM. Do not ship x86 / AArch32;
Microsoft is dropping AArch32 on future WoA.
6. Before final Store signing, replace Identity Name/Publisher with the
exact values from Partner Center (or let Partner Center re-sign).
7. Privacy policy URL on https://evilfox.win is required for Store listing.
=== Partner Center (RU) ===
1. Войдите в Partner Center тем же аккаунтом разработчика Store,
что и в Visual Studio (ассоциация приложения / упаковка).
2. Загрузите EvilFox_4.2.9.0.msixbundle, если есть; иначе оба .msix
(x64 и arm64).
3. Семейство устройств: только Windows 10/11 Desktop
(без Xbox / HoloLens / IoT и т.п.).
4. Arm64 = Windows on ARM; x86/AArch32 не публикуем.
5. Name/Publisher в манифесте должны совпадать с Partner Center
(или Store переподпишет пакет).
6. Нужен URL политики конфиденциальности на evilfox.win.
=== Artifacts ===
Bundle: D:\vpn navi\dist\microsoft-store\EvilFox_4.2.9.0.msixbundle
MSIX: D:\vpn navi\dist\microsoft-store\EvilFox_4.2.9.0_x64.msix
MSIX: D:\vpn navi\dist\microsoft-store\EvilFox_4.2.9.0_arm64.msix
Staging x64: D:\vpn navi\packaging\msix\_staging_x64
Staging arm64: D:\vpn navi\packaging\msix\_staging_arm64
=== MakeAppx ===
Found: C:\Users\orohi\AppData\Local\Microsoft\WinGet\Packages\Microsoft.MSIX-Toolkit_Microsoft.Winget.Source_8wekyb3d8bbwe\MSIX-Toolkit.x64\makeappx.exe
=== wintun.dll ===
wintun.dll was NOT found under assets/ or dist/. Packaging continued.
VPN (TUN) mode needs wintun.dll next to EvilFox.exe.
Store users can still use proxy mode without it.
Add arch-specific wintun.dll later under assets/wintun/<arch>/ if needed.
=== Listing assets ===
listing\store.csv - Partner Center bulk listing fields
listing\screen\ - screenshots for Store listing
=== Arm64 build note ===
Cross-build (CGO_ENABLED=0):
set GOOS=windows
set GOARCH=arm64
rem temporarily move cmd\vpnapp\resource.syso (amd64 from goversioninfo -64)
go build -ldflags="-H windowsgui -s -w" -trimpath -o dist\navis-release\windows\arm64\EvilFox.exe ./cmd/vpnapp
rem restore resource.syso
+10
View File
@@ -0,0 +1,10 @@
EvilFox 4.2.9 — a clear client for private internet access.
What's new:
• Add configuration: AWG / AWG 2.0 (.conf, JSON, awg://) and Naive proxy — not only http(s) subscription URLs
• Clash wireguard / naive bodies when fetched from a config URL
• Support contacts and FAQ on the website
• Modern UI: Protection, My servers, Settings, Statistics, Help
• Light/dark themes, Russian and English
• Country flags, server grouping, Kill Switch, auto-select by ping
• Privacy Policy and Terms in Help
+10
View File
@@ -0,0 +1,10 @@
EvilFox 4.2.9 — удобный клиент для безопасного доступа в интернет.
Что нового:
• Добавление конфигурации: AWG / AWG 2.0 (.conf, JSON, awg://) и Naive-прокси — не только http(s) URL подписки
• Поддержка Clash wireguard / naive в теле конфигурации по URL
• Контакты поддержки и FAQ на сайте
• Современный интерфейс: Защита, Мои серверы, Настройки, Статистика, Помощь
• Светлая/тёмная тема, русский и английский
• Флаги стран, группировка серверов, Kill Switch, автовыбор по пингу
• Политика конфиденциальности и условия в разделе Помощь
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

+548
View File
@@ -0,0 +1,548 @@
Field,ID,Type (Тип),default,en
Description,2,Текст,"EvilFox — приложение для безопасного доступа в интернет на Windows.
Защитите данные в публичных Wi‑Fi сетях: EvilFox шифрует трафик, скрывает ваш реальный IP-адрес и помогает предотвратить утечки DNS. Современный интерфейс на русском и английском, светлая и тёмная темы, удобный выбор серверов и понятный статус подключения.
Возможности:
• Шифрование трафика с поддержкой современных протоколов
• Режим прокси и полный режим защиты (TUN) для всего трафика приложений
• Защита от утечек (Kill Switch) при обрыве соединения
• Автовыбор сервера с наименьшим пингом
• Список серверов с флагами стран и группировкой по хосту
• Добавление собственной конфигурации по URL
• Мониторинг использования трафика и статистика сессий
• Интерфейс на русском и английском языках
EvilFox подходит для повседневной работы, путешествий и защиты приватности. Конфигурацию подключения вы добавляете самостоятельно; покупки внутри приложения не требуются.
Сайт: https://evilfox.win/
Поддержка: https://t.me/evilfox100Bot","EvilFox is a Windows app for private, secure internet access.
Protect your data on public WiFi: EvilFox encrypts your traffic, hides your real IP address, and helps prevent DNS leaks. A modern interface in English and Russian, light and dark themes, an easy server list, and a clear connection status.
Features:
• Traffic encryption with modern protocol support
• Proxy mode and full protection mode (TUN) for all app traffic
• Leak protection (Kill Switch) if the connection drops
• Auto-select the server with the lowest ping
• Server list with country flags and host grouping
• Add your own configuration via URL
• Traffic usage monitoring and session stats
• English and Russian interface
EvilFox is built for everyday browsing, travel, and privacy. You add your own connection configuration; no in-app purchases are required.
Website: https://evilfox.win/
Support: https://t.me/evilfox100Bot"
ReleaseNotes,3,Текст,"EvilFox 4.2 — удобный и понятный клиент для безопасного доступа в интернет.
Что нового для пользователей:
• Современный интерфейс: вкладки Защита, Мои серверы, Настройки, Статистика, Помощь
• Большая кнопка подключения и понятный статус защиты
• Светлая тема по умолчанию и переключение на тёмную
• Языки: русский и английский (RU | EN)
• Флаги стран у серверов (корректно на Windows)
• Серверы с одним IP объединяются в одну строку с доступными протоколами
• Добавление конфигурации по URL
• Режим прокси и полный режим защиты (TUN)
• Kill Switch — защита от утечек при обрыве соединения
• Автовыбор сервера с лучшим пингом
• Статистика трафика и сессий
• Политика конфиденциальности и условия использования в разделе Помощь
• Адаптивное окно под любой экран и масштаб Windows","EvilFox 4.2 — a clearer, more polished client for private internet access.
What's new:
• Modern UI: Protection, My servers, Settings, Statistics, Help
• Large connect button and clear protection status
• Light theme by default, with dark theme toggle
• Russian and English languages (RU | EN)
• Country flags for servers (rendered correctly on Windows)
• Same-IP servers grouped into one row with available protocols
• Add configuration via URL
• Proxy mode and full protection mode (TUN)
• Kill Switch — leak protection if the connection drops
• Auto-select the lowest-ping server
• Traffic and session statistics
• Privacy Policy and Terms of Use in Help
• Adaptive window sizing for any screen and Windows display scale"
Title,4,Текст,EvilFox,EvilFox
ShortTitle,5,Текст,EvilFox,EvilFox
SortTitle,6,Текст,EvilFox,EvilFox
VoiceTitle,7,Текст,Evil Fox,Evil Fox
ShortDescription,8,Текст,"Безопасный доступ в интернет: шифрование трафика, скрытие IP, защита от утечек DNS. Русский и английский. Современные протоколы, Kill Switch, удобный выбор серверов.","Secure internet access: encrypt traffic, hide your IP, help prevent DNS leaks. English & Russian. Modern protocols, Kill Switch, easy server selection."
DevStudio,9,Текст,EvilFox,EvilFox
CopyrightTrademarkInformation,12,Текст,"© 2026 EvilFox. Все права защищены. EvilFox, логотип EvilFox и связанные знаки являются товарными знаками или зарегистрированными товарными знаками EvilFox. Прочие названия продуктов и компаний, упомянутые в приложении или на странице продукта, могут быть товарными знаками соответствующих владельцев.","© 2026 EvilFox. All rights reserved. EvilFox, the EvilFox logo, and related marks are trademarks or registered trademarks of EvilFox. Other product and company names mentioned in the app or product listing may be trademarks of their respective owners."
AdditionalLicenseTerms,13,Текст,"Дополнительно к стандартным условиям лицензии Microsoft Store / приложения:
1. EvilFox — клиент для защищённого сетевого подключения. Конфигурации серверов и URL подключения предоставляются вами и не являются частью покупки в Store (если применимо).
2. Вы несёте ответственность за законность использования приложения и выбранных серверов в вашей юрисдикции.
3. Издатель не гарантирует бесперебойную работу сторонних серверов, скорость или доступность конкретных узлов.
4. Приложение может хранить настройки и конфигурации локально на устройстве. Политика конфиденциальности и полные условия использования доступны в разделе «Помощь» приложения и на https://evilfox.win/
5. Обновления через Microsoft Store заменяют самообновление приложения в сборке Store.
6. Возможность runFullTrust / классическое настольное приложение требуется для работы клиента (WebView2, сетевые режимы).
Контакты: https://evilfox.win/ · https://t.me/evilfox100Bot
","In addition to the standard Microsoft Store / app license terms:
1. EvilFox is a client for protected network connectivity. Server configurations and connection URLs are supplied by you and are not part of any Store purchase (where applicable).
2. You are responsible for using the app and chosen servers lawfully in your jurisdiction.
3. The publisher does not warrant uninterrupted operation of third-party servers, speed, or availability of specific nodes.
4. The app may store settings and configurations locally on your device. The Privacy Policy and full Terms of Use are available in the app Help section and at https://evilfox.win/
5. Updates delivered via the Microsoft Store replace in-app self-update in Store builds.
6. The runFullTrust / classic desktop capability is required for the client to function (WebView2, network modes).
Contact: https://evilfox.win/ · https://t.me/evilfox100Bot
"
DesktopScreenshot1,100,Относительный путь (или URL-адрес файла в Центре партнеров),screen/1.ru.png,screen/7.en.png
DesktopScreenshot2,101,Относительный путь (или URL-адрес файла в Центре партнеров),screen/2.ru.png,screen/8.en.png
DesktopScreenshot3,102,Относительный путь (или URL-адрес файла в Центре партнеров),screen/3.ru.png,screen/9.png
DesktopScreenshot4,103,Относительный путь (или URL-адрес файла в Центре партнеров),screen/4.ru.png,screen/10.en.png
DesktopScreenshot5,104,Относительный путь (или URL-адрес файла в Центре партнеров),screen/5.ru.png,screen/11.en.png
DesktopScreenshot6,105,Относительный путь (или URL-адрес файла в Центре партнеров),screen/6.ru.png,screen/12dark.png
DesktopScreenshot7,106,Относительный путь (или URL-адрес файла в Центре партнеров),screen/12dark.png,screen/13dark.png
DesktopScreenshot8,107,Относительный путь (или URL-адрес файла в Центре партнеров),screen/13dark.png,screen/14dark.png
DesktopScreenshot9,108,Относительный путь (или URL-адрес файла в Центре партнеров),screen/14dark.png,screen/15dark.png
DesktopScreenshot10,109,Относительный путь (или URL-адрес файла в Центре партнеров),screen/15dark.png,screen/16dark.png
DesktopScreenshot11,110,Относительный путь (или URL-адрес файла в Центре партнеров),screen/16dark.png,
DesktopScreenshot12,111,Относительный путь (или URL-адрес файла в Центре партнеров),screen/9.png,
DesktopScreenshot13,112,Относительный путь (или URL-адрес файла в Центре партнеров),,
DesktopScreenshot14,113,Относительный путь (или URL-адрес файла в Центре партнеров),,
DesktopScreenshot15,114,Относительный путь (или URL-адрес файла в Центре партнеров),,
DesktopScreenshot16,115,Относительный путь (или URL-адрес файла в Центре партнеров),,
DesktopScreenshot17,116,Относительный путь (или URL-адрес файла в Центре партнеров),,
DesktopScreenshot18,117,Относительный путь (или URL-адрес файла в Центре партнеров),,
DesktopScreenshot19,118,Относительный путь (или URL-адрес файла в Центре партнеров),,
DesktopScreenshot20,119,Относительный путь (или URL-адрес файла в Центре партнеров),,
DesktopScreenshot21,120,Относительный путь (или URL-адрес файла в Центре партнеров),,
DesktopScreenshot22,121,Относительный путь (или URL-адрес файла в Центре партнеров),,
DesktopScreenshot23,122,Относительный путь (или URL-адрес файла в Центре партнеров),,
DesktopScreenshot24,123,Относительный путь (или URL-адрес файла в Центре партнеров),,
DesktopScreenshot25,124,Относительный путь (или URL-адрес файла в Центре партнеров),,
DesktopScreenshot26,125,Относительный путь (или URL-адрес файла в Центре партнеров),,
DesktopScreenshot27,126,Относительный путь (или URL-адрес файла в Центре партнеров),,
DesktopScreenshot28,127,Относительный путь (или URL-адрес файла в Центре партнеров),,
DesktopScreenshot29,128,Относительный путь (или URL-адрес файла в Центре партнеров),,
DesktopScreenshot30,129,Относительный путь (или URL-адрес файла в Центре партнеров),,
DesktopScreenshotCaption1,150,Текст,Главный экран: подключение и статус защиты,Home: connect and protection status
DesktopScreenshotCaption2,151,Текст,Список серверов с флагами и пингом,Server list with flags and ping
DesktopScreenshotCaption3,152,Текст,"Настройки: тема, язык, Kill Switch","Settings: theme, language, Kill Switch"
DesktopScreenshotCaption4,153,Текст,Статистика использования,Usage statistics
DesktopScreenshotCaption5,154,Текст,Помощь и полезные ссылки,Statistics and help
DesktopScreenshotCaption6,155,Текст,Добавление конфигурации,Dark theme: home
DesktopScreenshotCaption7,156,Текст,Тёмная тема: главный экран,Dark theme: servers
DesktopScreenshotCaption8,157,Текст,Тёмная тема: серверы,Dark theme: settings
DesktopScreenshotCaption9,158,Текст,Тёмная тема: настройки,Dark theme: statistics
DesktopScreenshotCaption10,159,Текст,Тёмная тема: статистика,Dark theme: help
DesktopScreenshotCaption11,160,Текст,Тёмная тема: помощь,Dark theme: help
DesktopScreenshotCaption12,161,Текст,Общий вид приложения,App overview
DesktopScreenshotCaption13,162,Текст,,
DesktopScreenshotCaption14,163,Текст,,
DesktopScreenshotCaption15,164,Текст,,
DesktopScreenshotCaption16,165,Текст,,
DesktopScreenshotCaption17,166,Текст,,
DesktopScreenshotCaption18,167,Текст,,
DesktopScreenshotCaption19,168,Текст,,
DesktopScreenshotCaption20,169,Текст,,
DesktopScreenshotCaption21,170,Текст,,
DesktopScreenshotCaption22,171,Текст,,
DesktopScreenshotCaption23,172,Текст,,
DesktopScreenshotCaption24,173,Текст,,
DesktopScreenshotCaption25,174,Текст,,
DesktopScreenshotCaption26,175,Текст,,
DesktopScreenshotCaption27,176,Текст,,
DesktopScreenshotCaption28,177,Текст,,
DesktopScreenshotCaption29,178,Текст,,
DesktopScreenshotCaption30,179,Текст,,
MobileScreenshot1,200,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot2,201,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot3,202,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot4,203,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot5,204,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot6,205,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot7,206,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot8,207,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot9,208,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot10,209,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot11,210,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot12,211,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot13,212,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot14,213,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot15,214,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot16,215,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot17,216,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot18,217,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot19,218,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot20,219,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot21,220,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot22,221,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot23,222,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot24,223,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot25,224,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot26,225,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot27,226,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot28,227,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot29,228,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshot30,229,Относительный путь (или URL-адрес файла в Центре партнеров),,
MobileScreenshotCaption1,250,Текст,,
MobileScreenshotCaption2,251,Текст,,
MobileScreenshotCaption3,252,Текст,,
MobileScreenshotCaption4,253,Текст,,
MobileScreenshotCaption5,254,Текст,,
MobileScreenshotCaption6,255,Текст,,
MobileScreenshotCaption7,256,Текст,,
MobileScreenshotCaption8,257,Текст,,
MobileScreenshotCaption9,258,Текст,,
MobileScreenshotCaption10,259,Текст,,
MobileScreenshotCaption11,260,Текст,,
MobileScreenshotCaption12,261,Текст,,
MobileScreenshotCaption13,262,Текст,,
MobileScreenshotCaption14,263,Текст,,
MobileScreenshotCaption15,264,Текст,,
MobileScreenshotCaption16,265,Текст,,
MobileScreenshotCaption17,266,Текст,,
MobileScreenshotCaption18,267,Текст,,
MobileScreenshotCaption19,268,Текст,,
MobileScreenshotCaption20,269,Текст,,
MobileScreenshotCaption21,270,Текст,,
MobileScreenshotCaption22,271,Текст,,
MobileScreenshotCaption23,272,Текст,,
MobileScreenshotCaption24,273,Текст,,
MobileScreenshotCaption25,274,Текст,,
MobileScreenshotCaption26,275,Текст,,
MobileScreenshotCaption27,276,Текст,,
MobileScreenshotCaption28,277,Текст,,
MobileScreenshotCaption29,278,Текст,,
MobileScreenshotCaption30,279,Текст,,
XboxScreenshot1,300,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot2,301,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot3,302,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot4,303,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot5,304,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot6,305,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot7,306,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot8,307,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot9,308,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot10,309,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot11,310,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot12,311,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot13,312,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot14,313,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot15,314,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot16,315,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot17,316,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot18,317,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot19,318,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot20,319,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot21,320,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot22,321,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot23,322,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot24,323,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot25,324,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot26,325,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot27,326,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot28,327,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot29,328,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshot30,329,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxScreenshotCaption1,350,Текст,,
XboxScreenshotCaption2,351,Текст,,
XboxScreenshotCaption3,352,Текст,,
XboxScreenshotCaption4,353,Текст,,
XboxScreenshotCaption5,354,Текст,,
XboxScreenshotCaption6,355,Текст,,
XboxScreenshotCaption7,356,Текст,,
XboxScreenshotCaption8,357,Текст,,
XboxScreenshotCaption9,358,Текст,,
XboxScreenshotCaption10,359,Текст,,
XboxScreenshotCaption11,360,Текст,,
XboxScreenshotCaption12,361,Текст,,
XboxScreenshotCaption13,362,Текст,,
XboxScreenshotCaption14,363,Текст,,
XboxScreenshotCaption15,364,Текст,,
XboxScreenshotCaption16,365,Текст,,
XboxScreenshotCaption17,366,Текст,,
XboxScreenshotCaption18,367,Текст,,
XboxScreenshotCaption19,368,Текст,,
XboxScreenshotCaption20,369,Текст,,
XboxScreenshotCaption21,370,Текст,,
XboxScreenshotCaption22,371,Текст,,
XboxScreenshotCaption23,372,Текст,,
XboxScreenshotCaption24,373,Текст,,
XboxScreenshotCaption25,374,Текст,,
XboxScreenshotCaption26,375,Текст,,
XboxScreenshotCaption27,376,Текст,,
XboxScreenshotCaption28,377,Текст,,
XboxScreenshotCaption29,378,Текст,,
XboxScreenshotCaption30,379,Текст,,
HolographicScreenshot1,400,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot2,401,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot3,402,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot4,403,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot5,404,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot6,405,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot7,406,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot8,407,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot9,408,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot10,409,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot11,410,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot12,411,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot13,412,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot14,413,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot15,414,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot16,415,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot17,416,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot18,417,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot19,418,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot20,419,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot21,420,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot22,421,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot23,422,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot24,423,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot25,424,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot26,425,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot27,426,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot28,427,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot29,428,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshot30,429,Относительный путь (или URL-адрес файла в Центре партнеров),,
HolographicScreenshotCaption1,450,Текст,,
HolographicScreenshotCaption2,451,Текст,,
HolographicScreenshotCaption3,452,Текст,,
HolographicScreenshotCaption4,453,Текст,,
HolographicScreenshotCaption5,454,Текст,,
HolographicScreenshotCaption6,455,Текст,,
HolographicScreenshotCaption7,456,Текст,,
HolographicScreenshotCaption8,457,Текст,,
HolographicScreenshotCaption9,458,Текст,,
HolographicScreenshotCaption10,459,Текст,,
HolographicScreenshotCaption11,460,Текст,,
HolographicScreenshotCaption12,461,Текст,,
HolographicScreenshotCaption13,462,Текст,,
HolographicScreenshotCaption14,463,Текст,,
HolographicScreenshotCaption15,464,Текст,,
HolographicScreenshotCaption16,465,Текст,,
HolographicScreenshotCaption17,466,Текст,,
HolographicScreenshotCaption18,467,Текст,,
HolographicScreenshotCaption19,468,Текст,,
HolographicScreenshotCaption20,469,Текст,,
HolographicScreenshotCaption21,470,Текст,,
HolographicScreenshotCaption22,471,Текст,,
HolographicScreenshotCaption23,472,Текст,,
HolographicScreenshotCaption24,473,Текст,,
HolographicScreenshotCaption25,474,Текст,,
HolographicScreenshotCaption26,475,Текст,,
HolographicScreenshotCaption27,476,Текст,,
HolographicScreenshotCaption28,477,Текст,,
HolographicScreenshotCaption29,478,Текст,,
HolographicScreenshotCaption30,479,Текст,,
SurfaceHubScreenshot11,510,Относительный путь (или URL-адрес файла в Центре партнеров),,
SurfaceHubScreenshot12,511,Относительный путь (или URL-адрес файла в Центре партнеров),,
SurfaceHubScreenshot13,512,Относительный путь (или URL-адрес файла в Центре партнеров),,
SurfaceHubScreenshot14,513,Относительный путь (или URL-адрес файла в Центре партнеров),,
SurfaceHubScreenshot15,514,Относительный путь (или URL-адрес файла в Центре партнеров),,
SurfaceHubScreenshot16,515,Относительный путь (или URL-адрес файла в Центре партнеров),,
SurfaceHubScreenshot17,516,Относительный путь (или URL-адрес файла в Центре партнеров),,
SurfaceHubScreenshot18,517,Относительный путь (или URL-адрес файла в Центре партнеров),,
SurfaceHubScreenshot19,518,Относительный путь (или URL-адрес файла в Центре партнеров),,
SurfaceHubScreenshot20,519,Относительный путь (или URL-адрес файла в Центре партнеров),,
SurfaceHubScreenshot21,520,Относительный путь (или URL-адрес файла в Центре партнеров),,
SurfaceHubScreenshot22,521,Относительный путь (или URL-адрес файла в Центре партнеров),,
SurfaceHubScreenshot23,522,Относительный путь (или URL-адрес файла в Центре партнеров),,
SurfaceHubScreenshot24,523,Относительный путь (или URL-адрес файла в Центре партнеров),,
SurfaceHubScreenshot25,524,Относительный путь (или URL-адрес файла в Центре партнеров),,
SurfaceHubScreenshot26,525,Относительный путь (или URL-адрес файла в Центре партнеров),,
SurfaceHubScreenshot27,526,Относительный путь (или URL-адрес файла в Центре партнеров),,
SurfaceHubScreenshot28,527,Относительный путь (или URL-адрес файла в Центре партнеров),,
SurfaceHubScreenshot29,528,Относительный путь (или URL-адрес файла в Центре партнеров),,
SurfaceHubScreenshot30,529,Относительный путь (или URL-адрес файла в Центре партнеров),,
SurfaceHubScreenshotCaption11,560,Текст,,
SurfaceHubScreenshotCaption12,561,Текст,,
SurfaceHubScreenshotCaption13,562,Текст,,
SurfaceHubScreenshotCaption14,563,Текст,,
SurfaceHubScreenshotCaption15,564,Текст,,
SurfaceHubScreenshotCaption16,565,Текст,,
SurfaceHubScreenshotCaption17,566,Текст,,
SurfaceHubScreenshotCaption18,567,Текст,,
SurfaceHubScreenshotCaption19,568,Текст,,
SurfaceHubScreenshotCaption20,569,Текст,,
SurfaceHubScreenshotCaption21,570,Текст,,
SurfaceHubScreenshotCaption22,571,Текст,,
SurfaceHubScreenshotCaption23,572,Текст,,
SurfaceHubScreenshotCaption24,573,Текст,,
SurfaceHubScreenshotCaption25,574,Текст,,
SurfaceHubScreenshotCaption26,575,Текст,,
SurfaceHubScreenshotCaption27,576,Текст,,
SurfaceHubScreenshotCaption28,577,Текст,,
SurfaceHubScreenshotCaption29,578,Текст,,
SurfaceHubScreenshotCaption30,579,Текст,,
StoreLogo720x1080,600,Относительный путь (или URL-адрес файла в Центре партнеров),,
StoreLogo1080x1080,601,Относительный путь (или URL-адрес файла в Центре партнеров),,
StoreLogo300x300,602,Относительный путь (или URL-адрес файла в Центре партнеров),screen/1.ru.300x300.png,screen/1.ru.300x300.png
OverrideLogosForWin10,603,True/False,,False
StoreLogoOverride150x150,604,Относительный путь (или URL-адрес файла в Центре партнеров),,
StoreLogoOverride71x71,605,Относительный путь (или URL-адрес файла в Центре партнеров),,
PromoImage1920x1080,606,Относительный путь (или URL-адрес файла в Центре партнеров),,
PromoImage2400x1200,607,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxBrandedKeyArt584x800,608,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxTitledHero1920x1080,609,Относительный путь (или URL-адрес файла в Центре партнеров),,
XboxFeaturedPromo1080x1080,610,Относительный путь (или URL-адрес файла в Центре партнеров),,
OptionalPromo358x358,611,Относительный путь (или URL-адрес файла в Центре партнеров),,
OptionalPromo1000x800,612,Относительный путь (или URL-адрес файла в Центре партнеров),,
OptionalPromo414x180,613,Относительный путь (или URL-адрес файла в Центре партнеров),,
Feature1,700,Текст,Шифрование трафика и скрытие IP-адреса,Encrypt traffic and hide your IP address
Feature2,701,Текст,Режим прокси и полный режим защиты (TUN),Proxy mode and full protection mode (TUN)
Feature3,702,Текст,Kill Switch — блокировка трафика при обрыве VPN,Kill Switch blocks traffic if VPN drops
Feature4,703,Текст,Автовыбор сервера с лучшим пингом,Auto-select the lowest-ping server
Feature5,704,Текст,Серверы с флагами стран и группировкой протоколов,Servers with country flags and protocol grouping
Feature6,705,Текст,Добавление конфигурации по URL,Add configuration via URL
Feature7,706,Текст,Статистика трафика и сессий,Traffic and session statistics
Feature8,707,Текст,Интерфейс на русском и английском,English and Russian interface
Feature9,708,Текст,Светлая и тёмная темы оформления,Light and dark themes
Feature10,709,Текст,Защита от утечек DNS,DNS leak protection
Feature11,710,Текст,,
Feature12,711,Текст,,
Feature13,712,Текст,,
Feature14,713,Текст,,
Feature15,714,Текст,,
Feature16,715,Текст,,
Feature17,716,Текст,,
Feature18,717,Текст,,
Feature19,718,Текст,,
Feature20,719,Текст,,
MinimumHardwareReq1,800,Текст,Windows 10 version 1809 or later (x64),Windows 10 version 1809 or later (x64)
MinimumHardwareReq2,801,Текст,Internet connection,Internet connection
MinimumHardwareReq3,802,Текст,100 MB free disk space,100 MB free disk space
MinimumHardwareReq4,803,Текст,,
MinimumHardwareReq5,804,Текст,,
MinimumHardwareReq6,805,Текст,,
MinimumHardwareReq7,806,Текст,,
MinimumHardwareReq8,807,Текст,,
MinimumHardwareReq9,808,Текст,,
MinimumHardwareReq10,809,Текст,,
MinimumHardwareReq11,810,Текст,,
RecommendedHardwareReq1,850,Текст,Windows 11 (x64),Windows 11 (x64)
RecommendedHardwareReq2,851,Текст,Stable broadband connection,Stable broadband connection
RecommendedHardwareReq3,852,Текст,,
RecommendedHardwareReq4,853,Текст,,
RecommendedHardwareReq5,854,Текст,,
RecommendedHardwareReq6,855,Текст,,
RecommendedHardwareReq7,856,Текст,,
RecommendedHardwareReq8,857,Текст,,
RecommendedHardwareReq9,858,Текст,,
RecommendedHardwareReq10,859,Текст,,
RecommendedHardwareReq11,860,Текст,,
SearchTerm1,900,Текст,VPN,VPN
SearchTerm2,901,Текст,privacy,privacy
SearchTerm3,902,Текст,secure internet,secure internet
SearchTerm4,903,Текст,encrypt traffic,encrypt traffic
SearchTerm5,904,Текст,EvilFox,EvilFox
SearchTerm6,905,Текст,proxy client,proxy client
SearchTerm7,906,Текст,kill switch,kill switch
TrailerToPlayAtTopOfListing,999,Относительный путь (или URL-адрес файла в Центре партнеров),,
Trailer1,1000,Относительный путь (или URL-адрес файла в Центре партнеров),,
Trailer2,1001,Относительный путь (или URL-адрес файла в Центре партнеров),,
Trailer3,1002,Относительный путь (или URL-адрес файла в Центре партнеров),,
Trailer4,1003,Относительный путь (или URL-адрес файла в Центре партнеров),,
Trailer5,1004,Относительный путь (или URL-адрес файла в Центре партнеров),,
Trailer6,1005,Относительный путь (или URL-адрес файла в Центре партнеров),,
Trailer7,1006,Относительный путь (или URL-адрес файла в Центре партнеров),,
Trailer8,1007,Относительный путь (или URL-адрес файла в Центре партнеров),,
Trailer9,1008,Относительный путь (или URL-адрес файла в Центре партнеров),,
Trailer10,1009,Относительный путь (или URL-адрес файла в Центре партнеров),,
Trailer11,1010,Относительный путь (или URL-адрес файла в Центре партнеров),,
Trailer12,1011,Относительный путь (или URL-адрес файла в Центре партнеров),,
Trailer13,1012,Относительный путь (или URL-адрес файла в Центре партнеров),,
Trailer14,1013,Относительный путь (или URL-адрес файла в Центре партнеров),,
Trailer15,1014,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerTitle1,1020,Текст,,
TrailerTitle2,1021,Текст,,
TrailerTitle3,1022,Текст,,
TrailerTitle4,1023,Текст,,
TrailerTitle5,1024,Текст,,
TrailerTitle6,1025,Текст,,
TrailerTitle7,1026,Текст,,
TrailerTitle8,1027,Текст,,
TrailerTitle9,1028,Текст,,
TrailerTitle10,1029,Текст,,
TrailerTitle11,1030,Текст,,
TrailerTitle12,1031,Текст,,
TrailerTitle13,1032,Текст,,
TrailerTitle14,1033,Текст,,
TrailerTitle15,1034,Текст,,
TrailerThumbnail1,1040,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerThumbnail2,1041,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerThumbnail3,1042,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerThumbnail4,1043,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerThumbnail5,1044,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerThumbnail6,1045,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerThumbnail7,1046,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerThumbnail8,1047,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerThumbnail9,1048,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerThumbnail10,1049,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerThumbnail11,1050,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerThumbnail12,1051,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerThumbnail13,1052,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerThumbnail14,1053,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerThumbnail15,1054,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerClosedCaption1,1055,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerClosedCaption2,1056,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerClosedCaption3,1057,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerClosedCaption4,1058,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerClosedCaption5,1059,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerClosedCaption6,1060,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerClosedCaption7,1061,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerClosedCaption8,1062,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerClosedCaption9,1063,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerClosedCaption10,1064,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerClosedCaption11,1065,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerClosedCaption12,1066,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerClosedCaption13,1067,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerClosedCaption14,1068,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerClosedCaption15,1069,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerAudioDescription1,1070,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerAudioDescription2,1071,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerAudioDescription3,1072,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerAudioDescription4,1073,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerAudioDescription5,1074,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerAudioDescription6,1075,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerAudioDescription7,1076,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerAudioDescription8,1077,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerAudioDescription9,1078,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerAudioDescription10,1079,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerAudioDescription11,1080,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerAudioDescription12,1081,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerAudioDescription13,1082,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerAudioDescription14,1083,Относительный путь (или URL-адрес файла в Центре партнеров),,
TrailerAudioDescription15,1084,Относительный путь (или URL-адрес файла в Центре партнеров),,
,,,,
,,,,
,,,,
,,,,
,,,,
,,,,
,,,,
,,,,
,,,,
,,,,
,,,,
,,,,
,,,,
,,,,
,,,,
,,,,
,,,,
,,,,
,,,,
,,,,
,,,,
,,,,
,,,,
,,,,
1 Field ID Type (Тип) default en
2 Description 2 Текст EvilFox — приложение для безопасного доступа в интернет на Windows. Защитите данные в публичных Wi‑Fi сетях: EvilFox шифрует трафик, скрывает ваш реальный IP-адрес и помогает предотвратить утечки DNS. Современный интерфейс на русском и английском, светлая и тёмная темы, удобный выбор серверов и понятный статус подключения. Возможности: • Шифрование трафика с поддержкой современных протоколов • Режим прокси и полный режим защиты (TUN) для всего трафика приложений • Защита от утечек (Kill Switch) при обрыве соединения • Автовыбор сервера с наименьшим пингом • Список серверов с флагами стран и группировкой по хосту • Добавление собственной конфигурации по URL • Мониторинг использования трафика и статистика сессий • Интерфейс на русском и английском языках EvilFox подходит для повседневной работы, путешествий и защиты приватности. Конфигурацию подключения вы добавляете самостоятельно; покупки внутри приложения не требуются. Сайт: https://evilfox.win/ Поддержка: https://t.me/evilfox100Bot EvilFox is a Windows app for private, secure internet access. Protect your data on public Wi‑Fi: EvilFox encrypts your traffic, hides your real IP address, and helps prevent DNS leaks. A modern interface in English and Russian, light and dark themes, an easy server list, and a clear connection status. Features: • Traffic encryption with modern protocol support • Proxy mode and full protection mode (TUN) for all app traffic • Leak protection (Kill Switch) if the connection drops • Auto-select the server with the lowest ping • Server list with country flags and host grouping • Add your own configuration via URL • Traffic usage monitoring and session stats • English and Russian interface EvilFox is built for everyday browsing, travel, and privacy. You add your own connection configuration; no in-app purchases are required. Website: https://evilfox.win/ Support: https://t.me/evilfox100Bot
3 ReleaseNotes 3 Текст EvilFox 4.2 — удобный и понятный клиент для безопасного доступа в интернет. Что нового для пользователей: • Современный интерфейс: вкладки Защита, Мои серверы, Настройки, Статистика, Помощь • Большая кнопка подключения и понятный статус защиты • Светлая тема по умолчанию и переключение на тёмную • Языки: русский и английский (RU | EN) • Флаги стран у серверов (корректно на Windows) • Серверы с одним IP объединяются в одну строку с доступными протоколами • Добавление конфигурации по URL • Режим прокси и полный режим защиты (TUN) • Kill Switch — защита от утечек при обрыве соединения • Автовыбор сервера с лучшим пингом • Статистика трафика и сессий • Политика конфиденциальности и условия использования в разделе Помощь • Адаптивное окно под любой экран и масштаб Windows EvilFox 4.2 — a clearer, more polished client for private internet access. What's new: • Modern UI: Protection, My servers, Settings, Statistics, Help • Large connect button and clear protection status • Light theme by default, with dark theme toggle • Russian and English languages (RU | EN) • Country flags for servers (rendered correctly on Windows) • Same-IP servers grouped into one row with available protocols • Add configuration via URL • Proxy mode and full protection mode (TUN) • Kill Switch — leak protection if the connection drops • Auto-select the lowest-ping server • Traffic and session statistics • Privacy Policy and Terms of Use in Help • Adaptive window sizing for any screen and Windows display scale
4 Title 4 Текст EvilFox EvilFox
5 ShortTitle 5 Текст EvilFox EvilFox
6 SortTitle 6 Текст EvilFox EvilFox
7 VoiceTitle 7 Текст Evil Fox Evil Fox
8 ShortDescription 8 Текст Безопасный доступ в интернет: шифрование трафика, скрытие IP, защита от утечек DNS. Русский и английский. Современные протоколы, Kill Switch, удобный выбор серверов. Secure internet access: encrypt traffic, hide your IP, help prevent DNS leaks. English & Russian. Modern protocols, Kill Switch, easy server selection.
9 DevStudio 9 Текст EvilFox EvilFox
10 CopyrightTrademarkInformation 12 Текст © 2026 EvilFox. Все права защищены. EvilFox, логотип EvilFox и связанные знаки являются товарными знаками или зарегистрированными товарными знаками EvilFox. Прочие названия продуктов и компаний, упомянутые в приложении или на странице продукта, могут быть товарными знаками соответствующих владельцев. © 2026 EvilFox. All rights reserved. EvilFox, the EvilFox logo, and related marks are trademarks or registered trademarks of EvilFox. Other product and company names mentioned in the app or product listing may be trademarks of their respective owners.
11 AdditionalLicenseTerms 13 Текст Дополнительно к стандартным условиям лицензии Microsoft Store / приложения: 1. EvilFox — клиент для защищённого сетевого подключения. Конфигурации серверов и URL подключения предоставляются вами и не являются частью покупки в Store (если применимо). 2. Вы несёте ответственность за законность использования приложения и выбранных серверов в вашей юрисдикции. 3. Издатель не гарантирует бесперебойную работу сторонних серверов, скорость или доступность конкретных узлов. 4. Приложение может хранить настройки и конфигурации локально на устройстве. Политика конфиденциальности и полные условия использования доступны в разделе «Помощь» приложения и на https://evilfox.win/ 5. Обновления через Microsoft Store заменяют самообновление приложения в сборке Store. 6. Возможность runFullTrust / классическое настольное приложение требуется для работы клиента (WebView2, сетевые режимы). Контакты: https://evilfox.win/ · https://t.me/evilfox100Bot In addition to the standard Microsoft Store / app license terms: 1. EvilFox is a client for protected network connectivity. Server configurations and connection URLs are supplied by you and are not part of any Store purchase (where applicable). 2. You are responsible for using the app and chosen servers lawfully in your jurisdiction. 3. The publisher does not warrant uninterrupted operation of third-party servers, speed, or availability of specific nodes. 4. The app may store settings and configurations locally on your device. The Privacy Policy and full Terms of Use are available in the app Help section and at https://evilfox.win/ 5. Updates delivered via the Microsoft Store replace in-app self-update in Store builds. 6. The runFullTrust / classic desktop capability is required for the client to function (WebView2, network modes). Contact: https://evilfox.win/ · https://t.me/evilfox100Bot
12 DesktopScreenshot1 100 Относительный путь (или URL-адрес файла в Центре партнеров) screen/1.ru.png screen/7.en.png
13 DesktopScreenshot2 101 Относительный путь (или URL-адрес файла в Центре партнеров) screen/2.ru.png screen/8.en.png
14 DesktopScreenshot3 102 Относительный путь (или URL-адрес файла в Центре партнеров) screen/3.ru.png screen/9.png
15 DesktopScreenshot4 103 Относительный путь (или URL-адрес файла в Центре партнеров) screen/4.ru.png screen/10.en.png
16 DesktopScreenshot5 104 Относительный путь (или URL-адрес файла в Центре партнеров) screen/5.ru.png screen/11.en.png
17 DesktopScreenshot6 105 Относительный путь (или URL-адрес файла в Центре партнеров) screen/6.ru.png screen/12dark.png
18 DesktopScreenshot7 106 Относительный путь (или URL-адрес файла в Центре партнеров) screen/12dark.png screen/13dark.png
19 DesktopScreenshot8 107 Относительный путь (или URL-адрес файла в Центре партнеров) screen/13dark.png screen/14dark.png
20 DesktopScreenshot9 108 Относительный путь (или URL-адрес файла в Центре партнеров) screen/14dark.png screen/15dark.png
21 DesktopScreenshot10 109 Относительный путь (или URL-адрес файла в Центре партнеров) screen/15dark.png screen/16dark.png
22 DesktopScreenshot11 110 Относительный путь (или URL-адрес файла в Центре партнеров) screen/16dark.png
23 DesktopScreenshot12 111 Относительный путь (или URL-адрес файла в Центре партнеров) screen/9.png
24 DesktopScreenshot13 112 Относительный путь (или URL-адрес файла в Центре партнеров)
25 DesktopScreenshot14 113 Относительный путь (или URL-адрес файла в Центре партнеров)
26 DesktopScreenshot15 114 Относительный путь (или URL-адрес файла в Центре партнеров)
27 DesktopScreenshot16 115 Относительный путь (или URL-адрес файла в Центре партнеров)
28 DesktopScreenshot17 116 Относительный путь (или URL-адрес файла в Центре партнеров)
29 DesktopScreenshot18 117 Относительный путь (или URL-адрес файла в Центре партнеров)
30 DesktopScreenshot19 118 Относительный путь (или URL-адрес файла в Центре партнеров)
31 DesktopScreenshot20 119 Относительный путь (или URL-адрес файла в Центре партнеров)
32 DesktopScreenshot21 120 Относительный путь (или URL-адрес файла в Центре партнеров)
33 DesktopScreenshot22 121 Относительный путь (или URL-адрес файла в Центре партнеров)
34 DesktopScreenshot23 122 Относительный путь (или URL-адрес файла в Центре партнеров)
35 DesktopScreenshot24 123 Относительный путь (или URL-адрес файла в Центре партнеров)
36 DesktopScreenshot25 124 Относительный путь (или URL-адрес файла в Центре партнеров)
37 DesktopScreenshot26 125 Относительный путь (или URL-адрес файла в Центре партнеров)
38 DesktopScreenshot27 126 Относительный путь (или URL-адрес файла в Центре партнеров)
39 DesktopScreenshot28 127 Относительный путь (или URL-адрес файла в Центре партнеров)
40 DesktopScreenshot29 128 Относительный путь (или URL-адрес файла в Центре партнеров)
41 DesktopScreenshot30 129 Относительный путь (или URL-адрес файла в Центре партнеров)
42 DesktopScreenshotCaption1 150 Текст Главный экран: подключение и статус защиты Home: connect and protection status
43 DesktopScreenshotCaption2 151 Текст Список серверов с флагами и пингом Server list with flags and ping
44 DesktopScreenshotCaption3 152 Текст Настройки: тема, язык, Kill Switch Settings: theme, language, Kill Switch
45 DesktopScreenshotCaption4 153 Текст Статистика использования Usage statistics
46 DesktopScreenshotCaption5 154 Текст Помощь и полезные ссылки Statistics and help
47 DesktopScreenshotCaption6 155 Текст Добавление конфигурации Dark theme: home
48 DesktopScreenshotCaption7 156 Текст Тёмная тема: главный экран Dark theme: servers
49 DesktopScreenshotCaption8 157 Текст Тёмная тема: серверы Dark theme: settings
50 DesktopScreenshotCaption9 158 Текст Тёмная тема: настройки Dark theme: statistics
51 DesktopScreenshotCaption10 159 Текст Тёмная тема: статистика Dark theme: help
52 DesktopScreenshotCaption11 160 Текст Тёмная тема: помощь Dark theme: help
53 DesktopScreenshotCaption12 161 Текст Общий вид приложения App overview
54 DesktopScreenshotCaption13 162 Текст
55 DesktopScreenshotCaption14 163 Текст
56 DesktopScreenshotCaption15 164 Текст
57 DesktopScreenshotCaption16 165 Текст
58 DesktopScreenshotCaption17 166 Текст
59 DesktopScreenshotCaption18 167 Текст
60 DesktopScreenshotCaption19 168 Текст
61 DesktopScreenshotCaption20 169 Текст
62 DesktopScreenshotCaption21 170 Текст
63 DesktopScreenshotCaption22 171 Текст
64 DesktopScreenshotCaption23 172 Текст
65 DesktopScreenshotCaption24 173 Текст
66 DesktopScreenshotCaption25 174 Текст
67 DesktopScreenshotCaption26 175 Текст
68 DesktopScreenshotCaption27 176 Текст
69 DesktopScreenshotCaption28 177 Текст
70 DesktopScreenshotCaption29 178 Текст
71 DesktopScreenshotCaption30 179 Текст
72 MobileScreenshot1 200 Относительный путь (или URL-адрес файла в Центре партнеров)
73 MobileScreenshot2 201 Относительный путь (или URL-адрес файла в Центре партнеров)
74 MobileScreenshot3 202 Относительный путь (или URL-адрес файла в Центре партнеров)
75 MobileScreenshot4 203 Относительный путь (или URL-адрес файла в Центре партнеров)
76 MobileScreenshot5 204 Относительный путь (или URL-адрес файла в Центре партнеров)
77 MobileScreenshot6 205 Относительный путь (или URL-адрес файла в Центре партнеров)
78 MobileScreenshot7 206 Относительный путь (или URL-адрес файла в Центре партнеров)
79 MobileScreenshot8 207 Относительный путь (или URL-адрес файла в Центре партнеров)
80 MobileScreenshot9 208 Относительный путь (или URL-адрес файла в Центре партнеров)
81 MobileScreenshot10 209 Относительный путь (или URL-адрес файла в Центре партнеров)
82 MobileScreenshot11 210 Относительный путь (или URL-адрес файла в Центре партнеров)
83 MobileScreenshot12 211 Относительный путь (или URL-адрес файла в Центре партнеров)
84 MobileScreenshot13 212 Относительный путь (или URL-адрес файла в Центре партнеров)
85 MobileScreenshot14 213 Относительный путь (или URL-адрес файла в Центре партнеров)
86 MobileScreenshot15 214 Относительный путь (или URL-адрес файла в Центре партнеров)
87 MobileScreenshot16 215 Относительный путь (или URL-адрес файла в Центре партнеров)
88 MobileScreenshot17 216 Относительный путь (или URL-адрес файла в Центре партнеров)
89 MobileScreenshot18 217 Относительный путь (или URL-адрес файла в Центре партнеров)
90 MobileScreenshot19 218 Относительный путь (или URL-адрес файла в Центре партнеров)
91 MobileScreenshot20 219 Относительный путь (или URL-адрес файла в Центре партнеров)
92 MobileScreenshot21 220 Относительный путь (или URL-адрес файла в Центре партнеров)
93 MobileScreenshot22 221 Относительный путь (или URL-адрес файла в Центре партнеров)
94 MobileScreenshot23 222 Относительный путь (или URL-адрес файла в Центре партнеров)
95 MobileScreenshot24 223 Относительный путь (или URL-адрес файла в Центре партнеров)
96 MobileScreenshot25 224 Относительный путь (или URL-адрес файла в Центре партнеров)
97 MobileScreenshot26 225 Относительный путь (или URL-адрес файла в Центре партнеров)
98 MobileScreenshot27 226 Относительный путь (или URL-адрес файла в Центре партнеров)
99 MobileScreenshot28 227 Относительный путь (или URL-адрес файла в Центре партнеров)
100 MobileScreenshot29 228 Относительный путь (или URL-адрес файла в Центре партнеров)
101 MobileScreenshot30 229 Относительный путь (или URL-адрес файла в Центре партнеров)
102 MobileScreenshotCaption1 250 Текст
103 MobileScreenshotCaption2 251 Текст
104 MobileScreenshotCaption3 252 Текст
105 MobileScreenshotCaption4 253 Текст
106 MobileScreenshotCaption5 254 Текст
107 MobileScreenshotCaption6 255 Текст
108 MobileScreenshotCaption7 256 Текст
109 MobileScreenshotCaption8 257 Текст
110 MobileScreenshotCaption9 258 Текст
111 MobileScreenshotCaption10 259 Текст
112 MobileScreenshotCaption11 260 Текст
113 MobileScreenshotCaption12 261 Текст
114 MobileScreenshotCaption13 262 Текст
115 MobileScreenshotCaption14 263 Текст
116 MobileScreenshotCaption15 264 Текст
117 MobileScreenshotCaption16 265 Текст
118 MobileScreenshotCaption17 266 Текст
119 MobileScreenshotCaption18 267 Текст
120 MobileScreenshotCaption19 268 Текст
121 MobileScreenshotCaption20 269 Текст
122 MobileScreenshotCaption21 270 Текст
123 MobileScreenshotCaption22 271 Текст
124 MobileScreenshotCaption23 272 Текст
125 MobileScreenshotCaption24 273 Текст
126 MobileScreenshotCaption25 274 Текст
127 MobileScreenshotCaption26 275 Текст
128 MobileScreenshotCaption27 276 Текст
129 MobileScreenshotCaption28 277 Текст
130 MobileScreenshotCaption29 278 Текст
131 MobileScreenshotCaption30 279 Текст
132 XboxScreenshot1 300 Относительный путь (или URL-адрес файла в Центре партнеров)
133 XboxScreenshot2 301 Относительный путь (или URL-адрес файла в Центре партнеров)
134 XboxScreenshot3 302 Относительный путь (или URL-адрес файла в Центре партнеров)
135 XboxScreenshot4 303 Относительный путь (или URL-адрес файла в Центре партнеров)
136 XboxScreenshot5 304 Относительный путь (или URL-адрес файла в Центре партнеров)
137 XboxScreenshot6 305 Относительный путь (или URL-адрес файла в Центре партнеров)
138 XboxScreenshot7 306 Относительный путь (или URL-адрес файла в Центре партнеров)
139 XboxScreenshot8 307 Относительный путь (или URL-адрес файла в Центре партнеров)
140 XboxScreenshot9 308 Относительный путь (или URL-адрес файла в Центре партнеров)
141 XboxScreenshot10 309 Относительный путь (или URL-адрес файла в Центре партнеров)
142 XboxScreenshot11 310 Относительный путь (или URL-адрес файла в Центре партнеров)
143 XboxScreenshot12 311 Относительный путь (или URL-адрес файла в Центре партнеров)
144 XboxScreenshot13 312 Относительный путь (или URL-адрес файла в Центре партнеров)
145 XboxScreenshot14 313 Относительный путь (или URL-адрес файла в Центре партнеров)
146 XboxScreenshot15 314 Относительный путь (или URL-адрес файла в Центре партнеров)
147 XboxScreenshot16 315 Относительный путь (или URL-адрес файла в Центре партнеров)
148 XboxScreenshot17 316 Относительный путь (или URL-адрес файла в Центре партнеров)
149 XboxScreenshot18 317 Относительный путь (или URL-адрес файла в Центре партнеров)
150 XboxScreenshot19 318 Относительный путь (или URL-адрес файла в Центре партнеров)
151 XboxScreenshot20 319 Относительный путь (или URL-адрес файла в Центре партнеров)
152 XboxScreenshot21 320 Относительный путь (или URL-адрес файла в Центре партнеров)
153 XboxScreenshot22 321 Относительный путь (или URL-адрес файла в Центре партнеров)
154 XboxScreenshot23 322 Относительный путь (или URL-адрес файла в Центре партнеров)
155 XboxScreenshot24 323 Относительный путь (или URL-адрес файла в Центре партнеров)
156 XboxScreenshot25 324 Относительный путь (или URL-адрес файла в Центре партнеров)
157 XboxScreenshot26 325 Относительный путь (или URL-адрес файла в Центре партнеров)
158 XboxScreenshot27 326 Относительный путь (или URL-адрес файла в Центре партнеров)
159 XboxScreenshot28 327 Относительный путь (или URL-адрес файла в Центре партнеров)
160 XboxScreenshot29 328 Относительный путь (или URL-адрес файла в Центре партнеров)
161 XboxScreenshot30 329 Относительный путь (или URL-адрес файла в Центре партнеров)
162 XboxScreenshotCaption1 350 Текст
163 XboxScreenshotCaption2 351 Текст
164 XboxScreenshotCaption3 352 Текст
165 XboxScreenshotCaption4 353 Текст
166 XboxScreenshotCaption5 354 Текст
167 XboxScreenshotCaption6 355 Текст
168 XboxScreenshotCaption7 356 Текст
169 XboxScreenshotCaption8 357 Текст
170 XboxScreenshotCaption9 358 Текст
171 XboxScreenshotCaption10 359 Текст
172 XboxScreenshotCaption11 360 Текст
173 XboxScreenshotCaption12 361 Текст
174 XboxScreenshotCaption13 362 Текст
175 XboxScreenshotCaption14 363 Текст
176 XboxScreenshotCaption15 364 Текст
177 XboxScreenshotCaption16 365 Текст
178 XboxScreenshotCaption17 366 Текст
179 XboxScreenshotCaption18 367 Текст
180 XboxScreenshotCaption19 368 Текст
181 XboxScreenshotCaption20 369 Текст
182 XboxScreenshotCaption21 370 Текст
183 XboxScreenshotCaption22 371 Текст
184 XboxScreenshotCaption23 372 Текст
185 XboxScreenshotCaption24 373 Текст
186 XboxScreenshotCaption25 374 Текст
187 XboxScreenshotCaption26 375 Текст
188 XboxScreenshotCaption27 376 Текст
189 XboxScreenshotCaption28 377 Текст
190 XboxScreenshotCaption29 378 Текст
191 XboxScreenshotCaption30 379 Текст
192 HolographicScreenshot1 400 Относительный путь (или URL-адрес файла в Центре партнеров)
193 HolographicScreenshot2 401 Относительный путь (или URL-адрес файла в Центре партнеров)
194 HolographicScreenshot3 402 Относительный путь (или URL-адрес файла в Центре партнеров)
195 HolographicScreenshot4 403 Относительный путь (или URL-адрес файла в Центре партнеров)
196 HolographicScreenshot5 404 Относительный путь (или URL-адрес файла в Центре партнеров)
197 HolographicScreenshot6 405 Относительный путь (или URL-адрес файла в Центре партнеров)
198 HolographicScreenshot7 406 Относительный путь (или URL-адрес файла в Центре партнеров)
199 HolographicScreenshot8 407 Относительный путь (или URL-адрес файла в Центре партнеров)
200 HolographicScreenshot9 408 Относительный путь (или URL-адрес файла в Центре партнеров)
201 HolographicScreenshot10 409 Относительный путь (или URL-адрес файла в Центре партнеров)
202 HolographicScreenshot11 410 Относительный путь (или URL-адрес файла в Центре партнеров)
203 HolographicScreenshot12 411 Относительный путь (или URL-адрес файла в Центре партнеров)
204 HolographicScreenshot13 412 Относительный путь (или URL-адрес файла в Центре партнеров)
205 HolographicScreenshot14 413 Относительный путь (или URL-адрес файла в Центре партнеров)
206 HolographicScreenshot15 414 Относительный путь (или URL-адрес файла в Центре партнеров)
207 HolographicScreenshot16 415 Относительный путь (или URL-адрес файла в Центре партнеров)
208 HolographicScreenshot17 416 Относительный путь (или URL-адрес файла в Центре партнеров)
209 HolographicScreenshot18 417 Относительный путь (или URL-адрес файла в Центре партнеров)
210 HolographicScreenshot19 418 Относительный путь (или URL-адрес файла в Центре партнеров)
211 HolographicScreenshot20 419 Относительный путь (или URL-адрес файла в Центре партнеров)
212 HolographicScreenshot21 420 Относительный путь (или URL-адрес файла в Центре партнеров)
213 HolographicScreenshot22 421 Относительный путь (или URL-адрес файла в Центре партнеров)
214 HolographicScreenshot23 422 Относительный путь (или URL-адрес файла в Центре партнеров)
215 HolographicScreenshot24 423 Относительный путь (или URL-адрес файла в Центре партнеров)
216 HolographicScreenshot25 424 Относительный путь (или URL-адрес файла в Центре партнеров)
217 HolographicScreenshot26 425 Относительный путь (или URL-адрес файла в Центре партнеров)
218 HolographicScreenshot27 426 Относительный путь (или URL-адрес файла в Центре партнеров)
219 HolographicScreenshot28 427 Относительный путь (или URL-адрес файла в Центре партнеров)
220 HolographicScreenshot29 428 Относительный путь (или URL-адрес файла в Центре партнеров)
221 HolographicScreenshot30 429 Относительный путь (или URL-адрес файла в Центре партнеров)
222 HolographicScreenshotCaption1 450 Текст
223 HolographicScreenshotCaption2 451 Текст
224 HolographicScreenshotCaption3 452 Текст
225 HolographicScreenshotCaption4 453 Текст
226 HolographicScreenshotCaption5 454 Текст
227 HolographicScreenshotCaption6 455 Текст
228 HolographicScreenshotCaption7 456 Текст
229 HolographicScreenshotCaption8 457 Текст
230 HolographicScreenshotCaption9 458 Текст
231 HolographicScreenshotCaption10 459 Текст
232 HolographicScreenshotCaption11 460 Текст
233 HolographicScreenshotCaption12 461 Текст
234 HolographicScreenshotCaption13 462 Текст
235 HolographicScreenshotCaption14 463 Текст
236 HolographicScreenshotCaption15 464 Текст
237 HolographicScreenshotCaption16 465 Текст
238 HolographicScreenshotCaption17 466 Текст
239 HolographicScreenshotCaption18 467 Текст
240 HolographicScreenshotCaption19 468 Текст
241 HolographicScreenshotCaption20 469 Текст
242 HolographicScreenshotCaption21 470 Текст
243 HolographicScreenshotCaption22 471 Текст
244 HolographicScreenshotCaption23 472 Текст
245 HolographicScreenshotCaption24 473 Текст
246 HolographicScreenshotCaption25 474 Текст
247 HolographicScreenshotCaption26 475 Текст
248 HolographicScreenshotCaption27 476 Текст
249 HolographicScreenshotCaption28 477 Текст
250 HolographicScreenshotCaption29 478 Текст
251 HolographicScreenshotCaption30 479 Текст
252 SurfaceHubScreenshot11 510 Относительный путь (или URL-адрес файла в Центре партнеров)
253 SurfaceHubScreenshot12 511 Относительный путь (или URL-адрес файла в Центре партнеров)
254 SurfaceHubScreenshot13 512 Относительный путь (или URL-адрес файла в Центре партнеров)
255 SurfaceHubScreenshot14 513 Относительный путь (или URL-адрес файла в Центре партнеров)
256 SurfaceHubScreenshot15 514 Относительный путь (или URL-адрес файла в Центре партнеров)
257 SurfaceHubScreenshot16 515 Относительный путь (или URL-адрес файла в Центре партнеров)
258 SurfaceHubScreenshot17 516 Относительный путь (или URL-адрес файла в Центре партнеров)
259 SurfaceHubScreenshot18 517 Относительный путь (или URL-адрес файла в Центре партнеров)
260 SurfaceHubScreenshot19 518 Относительный путь (или URL-адрес файла в Центре партнеров)
261 SurfaceHubScreenshot20 519 Относительный путь (или URL-адрес файла в Центре партнеров)
262 SurfaceHubScreenshot21 520 Относительный путь (или URL-адрес файла в Центре партнеров)
263 SurfaceHubScreenshot22 521 Относительный путь (или URL-адрес файла в Центре партнеров)
264 SurfaceHubScreenshot23 522 Относительный путь (или URL-адрес файла в Центре партнеров)
265 SurfaceHubScreenshot24 523 Относительный путь (или URL-адрес файла в Центре партнеров)
266 SurfaceHubScreenshot25 524 Относительный путь (или URL-адрес файла в Центре партнеров)
267 SurfaceHubScreenshot26 525 Относительный путь (или URL-адрес файла в Центре партнеров)
268 SurfaceHubScreenshot27 526 Относительный путь (или URL-адрес файла в Центре партнеров)
269 SurfaceHubScreenshot28 527 Относительный путь (или URL-адрес файла в Центре партнеров)
270 SurfaceHubScreenshot29 528 Относительный путь (или URL-адрес файла в Центре партнеров)
271 SurfaceHubScreenshot30 529 Относительный путь (или URL-адрес файла в Центре партнеров)
272 SurfaceHubScreenshotCaption11 560 Текст
273 SurfaceHubScreenshotCaption12 561 Текст
274 SurfaceHubScreenshotCaption13 562 Текст
275 SurfaceHubScreenshotCaption14 563 Текст
276 SurfaceHubScreenshotCaption15 564 Текст
277 SurfaceHubScreenshotCaption16 565 Текст
278 SurfaceHubScreenshotCaption17 566 Текст
279 SurfaceHubScreenshotCaption18 567 Текст
280 SurfaceHubScreenshotCaption19 568 Текст
281 SurfaceHubScreenshotCaption20 569 Текст
282 SurfaceHubScreenshotCaption21 570 Текст
283 SurfaceHubScreenshotCaption22 571 Текст
284 SurfaceHubScreenshotCaption23 572 Текст
285 SurfaceHubScreenshotCaption24 573 Текст
286 SurfaceHubScreenshotCaption25 574 Текст
287 SurfaceHubScreenshotCaption26 575 Текст
288 SurfaceHubScreenshotCaption27 576 Текст
289 SurfaceHubScreenshotCaption28 577 Текст
290 SurfaceHubScreenshotCaption29 578 Текст
291 SurfaceHubScreenshotCaption30 579 Текст
292 StoreLogo720x1080 600 Относительный путь (или URL-адрес файла в Центре партнеров)
293 StoreLogo1080x1080 601 Относительный путь (или URL-адрес файла в Центре партнеров)
294 StoreLogo300x300 602 Относительный путь (или URL-адрес файла в Центре партнеров) screen/1.ru.300x300.png screen/1.ru.300x300.png
295 OverrideLogosForWin10 603 True/False False
296 StoreLogoOverride150x150 604 Относительный путь (или URL-адрес файла в Центре партнеров)
297 StoreLogoOverride71x71 605 Относительный путь (или URL-адрес файла в Центре партнеров)
298 PromoImage1920x1080 606 Относительный путь (или URL-адрес файла в Центре партнеров)
299 PromoImage2400x1200 607 Относительный путь (или URL-адрес файла в Центре партнеров)
300 XboxBrandedKeyArt584x800 608 Относительный путь (или URL-адрес файла в Центре партнеров)
301 XboxTitledHero1920x1080 609 Относительный путь (или URL-адрес файла в Центре партнеров)
302 XboxFeaturedPromo1080x1080 610 Относительный путь (или URL-адрес файла в Центре партнеров)
303 OptionalPromo358x358 611 Относительный путь (или URL-адрес файла в Центре партнеров)
304 OptionalPromo1000x800 612 Относительный путь (или URL-адрес файла в Центре партнеров)
305 OptionalPromo414x180 613 Относительный путь (или URL-адрес файла в Центре партнеров)
306 Feature1 700 Текст Шифрование трафика и скрытие IP-адреса Encrypt traffic and hide your IP address
307 Feature2 701 Текст Режим прокси и полный режим защиты (TUN) Proxy mode and full protection mode (TUN)
308 Feature3 702 Текст Kill Switch — блокировка трафика при обрыве VPN Kill Switch blocks traffic if VPN drops
309 Feature4 703 Текст Автовыбор сервера с лучшим пингом Auto-select the lowest-ping server
310 Feature5 704 Текст Серверы с флагами стран и группировкой протоколов Servers with country flags and protocol grouping
311 Feature6 705 Текст Добавление конфигурации по URL Add configuration via URL
312 Feature7 706 Текст Статистика трафика и сессий Traffic and session statistics
313 Feature8 707 Текст Интерфейс на русском и английском English and Russian interface
314 Feature9 708 Текст Светлая и тёмная темы оформления Light and dark themes
315 Feature10 709 Текст Защита от утечек DNS DNS leak protection
316 Feature11 710 Текст
317 Feature12 711 Текст
318 Feature13 712 Текст
319 Feature14 713 Текст
320 Feature15 714 Текст
321 Feature16 715 Текст
322 Feature17 716 Текст
323 Feature18 717 Текст
324 Feature19 718 Текст
325 Feature20 719 Текст
326 MinimumHardwareReq1 800 Текст Windows 10 version 1809 or later (x64) Windows 10 version 1809 or later (x64)
327 MinimumHardwareReq2 801 Текст Internet connection Internet connection
328 MinimumHardwareReq3 802 Текст 100 MB free disk space 100 MB free disk space
329 MinimumHardwareReq4 803 Текст
330 MinimumHardwareReq5 804 Текст
331 MinimumHardwareReq6 805 Текст
332 MinimumHardwareReq7 806 Текст
333 MinimumHardwareReq8 807 Текст
334 MinimumHardwareReq9 808 Текст
335 MinimumHardwareReq10 809 Текст
336 MinimumHardwareReq11 810 Текст
337 RecommendedHardwareReq1 850 Текст Windows 11 (x64) Windows 11 (x64)
338 RecommendedHardwareReq2 851 Текст Stable broadband connection Stable broadband connection
339 RecommendedHardwareReq3 852 Текст
340 RecommendedHardwareReq4 853 Текст
341 RecommendedHardwareReq5 854 Текст
342 RecommendedHardwareReq6 855 Текст
343 RecommendedHardwareReq7 856 Текст
344 RecommendedHardwareReq8 857 Текст
345 RecommendedHardwareReq9 858 Текст
346 RecommendedHardwareReq10 859 Текст
347 RecommendedHardwareReq11 860 Текст
348 SearchTerm1 900 Текст VPN VPN
349 SearchTerm2 901 Текст privacy privacy
350 SearchTerm3 902 Текст secure internet secure internet
351 SearchTerm4 903 Текст encrypt traffic encrypt traffic
352 SearchTerm5 904 Текст EvilFox EvilFox
353 SearchTerm6 905 Текст proxy client proxy client
354 SearchTerm7 906 Текст kill switch kill switch
355 TrailerToPlayAtTopOfListing 999 Относительный путь (или URL-адрес файла в Центре партнеров)
356 Trailer1 1000 Относительный путь (или URL-адрес файла в Центре партнеров)
357 Trailer2 1001 Относительный путь (или URL-адрес файла в Центре партнеров)
358 Trailer3 1002 Относительный путь (или URL-адрес файла в Центре партнеров)
359 Trailer4 1003 Относительный путь (или URL-адрес файла в Центре партнеров)
360 Trailer5 1004 Относительный путь (или URL-адрес файла в Центре партнеров)
361 Trailer6 1005 Относительный путь (или URL-адрес файла в Центре партнеров)
362 Trailer7 1006 Относительный путь (или URL-адрес файла в Центре партнеров)
363 Trailer8 1007 Относительный путь (или URL-адрес файла в Центре партнеров)
364 Trailer9 1008 Относительный путь (или URL-адрес файла в Центре партнеров)
365 Trailer10 1009 Относительный путь (или URL-адрес файла в Центре партнеров)
366 Trailer11 1010 Относительный путь (или URL-адрес файла в Центре партнеров)
367 Trailer12 1011 Относительный путь (или URL-адрес файла в Центре партнеров)
368 Trailer13 1012 Относительный путь (или URL-адрес файла в Центре партнеров)
369 Trailer14 1013 Относительный путь (или URL-адрес файла в Центре партнеров)
370 Trailer15 1014 Относительный путь (или URL-адрес файла в Центре партнеров)
371 TrailerTitle1 1020 Текст
372 TrailerTitle2 1021 Текст
373 TrailerTitle3 1022 Текст
374 TrailerTitle4 1023 Текст
375 TrailerTitle5 1024 Текст
376 TrailerTitle6 1025 Текст
377 TrailerTitle7 1026 Текст
378 TrailerTitle8 1027 Текст
379 TrailerTitle9 1028 Текст
380 TrailerTitle10 1029 Текст
381 TrailerTitle11 1030 Текст
382 TrailerTitle12 1031 Текст
383 TrailerTitle13 1032 Текст
384 TrailerTitle14 1033 Текст
385 TrailerTitle15 1034 Текст
386 TrailerThumbnail1 1040 Относительный путь (или URL-адрес файла в Центре партнеров)
387 TrailerThumbnail2 1041 Относительный путь (или URL-адрес файла в Центре партнеров)
388 TrailerThumbnail3 1042 Относительный путь (или URL-адрес файла в Центре партнеров)
389 TrailerThumbnail4 1043 Относительный путь (или URL-адрес файла в Центре партнеров)
390 TrailerThumbnail5 1044 Относительный путь (или URL-адрес файла в Центре партнеров)
391 TrailerThumbnail6 1045 Относительный путь (или URL-адрес файла в Центре партнеров)
392 TrailerThumbnail7 1046 Относительный путь (или URL-адрес файла в Центре партнеров)
393 TrailerThumbnail8 1047 Относительный путь (или URL-адрес файла в Центре партнеров)
394 TrailerThumbnail9 1048 Относительный путь (или URL-адрес файла в Центре партнеров)
395 TrailerThumbnail10 1049 Относительный путь (или URL-адрес файла в Центре партнеров)
396 TrailerThumbnail11 1050 Относительный путь (или URL-адрес файла в Центре партнеров)
397 TrailerThumbnail12 1051 Относительный путь (или URL-адрес файла в Центре партнеров)
398 TrailerThumbnail13 1052 Относительный путь (или URL-адрес файла в Центре партнеров)
399 TrailerThumbnail14 1053 Относительный путь (или URL-адрес файла в Центре партнеров)
400 TrailerThumbnail15 1054 Относительный путь (или URL-адрес файла в Центре партнеров)
401 TrailerClosedCaption1 1055 Относительный путь (или URL-адрес файла в Центре партнеров)
402 TrailerClosedCaption2 1056 Относительный путь (или URL-адрес файла в Центре партнеров)
403 TrailerClosedCaption3 1057 Относительный путь (или URL-адрес файла в Центре партнеров)
404 TrailerClosedCaption4 1058 Относительный путь (или URL-адрес файла в Центре партнеров)
405 TrailerClosedCaption5 1059 Относительный путь (или URL-адрес файла в Центре партнеров)
406 TrailerClosedCaption6 1060 Относительный путь (или URL-адрес файла в Центре партнеров)
407 TrailerClosedCaption7 1061 Относительный путь (или URL-адрес файла в Центре партнеров)
408 TrailerClosedCaption8 1062 Относительный путь (или URL-адрес файла в Центре партнеров)
409 TrailerClosedCaption9 1063 Относительный путь (или URL-адрес файла в Центре партнеров)
410 TrailerClosedCaption10 1064 Относительный путь (или URL-адрес файла в Центре партнеров)
411 TrailerClosedCaption11 1065 Относительный путь (или URL-адрес файла в Центре партнеров)
412 TrailerClosedCaption12 1066 Относительный путь (или URL-адрес файла в Центре партнеров)
413 TrailerClosedCaption13 1067 Относительный путь (или URL-адрес файла в Центре партнеров)
414 TrailerClosedCaption14 1068 Относительный путь (или URL-адрес файла в Центре партнеров)
415 TrailerClosedCaption15 1069 Относительный путь (или URL-адрес файла в Центре партнеров)
416 TrailerAudioDescription1 1070 Относительный путь (или URL-адрес файла в Центре партнеров)
417 TrailerAudioDescription2 1071 Относительный путь (или URL-адрес файла в Центре партнеров)
418 TrailerAudioDescription3 1072 Относительный путь (или URL-адрес файла в Центре партнеров)
419 TrailerAudioDescription4 1073 Относительный путь (или URL-адрес файла в Центре партнеров)
420 TrailerAudioDescription5 1074 Относительный путь (или URL-адрес файла в Центре партнеров)
421 TrailerAudioDescription6 1075 Относительный путь (или URL-адрес файла в Центре партнеров)
422 TrailerAudioDescription7 1076 Относительный путь (или URL-адрес файла в Центре партнеров)
423 TrailerAudioDescription8 1077 Относительный путь (или URL-адрес файла в Центре партнеров)
424 TrailerAudioDescription9 1078 Относительный путь (или URL-адрес файла в Центре партнеров)
425 TrailerAudioDescription10 1079 Относительный путь (или URL-адрес файла в Центре партнеров)
426 TrailerAudioDescription11 1080 Относительный путь (или URL-адрес файла в Центре партнеров)
427 TrailerAudioDescription12 1081 Относительный путь (или URL-адрес файла в Центре партнеров)
428 TrailerAudioDescription13 1082 Относительный путь (или URL-адрес файла в Центре партнеров)
429 TrailerAudioDescription14 1083 Относительный путь (или URL-адрес файла в Центре партнеров)
430 TrailerAudioDescription15 1084 Относительный путь (или URL-адрес файла в Центре партнеров)
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
+35 -35
View File
@@ -1,48 +1,48 @@
{ {
"version": "3.8.2", "version": "4.2.8",
"notes": "3.8.2: Windows product version 3.8.2.1 (build 1).", "notes": "4.2.8: Store-ready Privacy Policy and Terms of Use on Help (RU/EN); docs for website hosting",
"platform": "windows-amd64", "platform": "windows-amd64",
"os": "windows", "os": "windows",
"arch": "amd64", "arch": "amd64",
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/windows/Navis.exe", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
"sha256": "8049f973e8e8712b31fccac42c48be5eb436a5a6bf103eb979729e1cbfdec8ec", "sha256": "9b5cc04093561a6a6d7b97baf8f8cc9498dd863dfeb0bc3c92446c3fdb79f7c2",
"mandatory": false, "mandatory": false,
"platforms": { "platforms": {
"windows-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/windows/Navis.exe",
"sha256": "8049f973e8e8712b31fccac42c48be5eb436a5a6bf103eb979729e1cbfdec8ec",
"os": "windows",
"arch": "amd64"
},
"darwin-arm64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/macos/arm64/Navis",
"sha256": "01a40b1cce4e2fb3a38eb80f0815c295a00af11e4288f3b9e5c0b58ae336f28d",
"os": "darwin",
"arch": "arm64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/macos/arm64/Navis.dmg",
"zip_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/macos/arm64/Navis.app.zip"
},
"darwin-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/macos/amd64/Navis",
"sha256": "19a066cfc7f7c3a291a220482b28e66a459d091c19c5351818c401b982bc65b3",
"os": "darwin",
"arch": "amd64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/macos/amd64/Navis.dmg",
"zip_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/macos/amd64/Navis.app.zip"
},
"darwin-universal": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/macos/universal/Navis",
"sha256": "d53dba8256fd6f7b62d1ce9bbad741af087ae44fec942bf2782b9c8f4e36cef2",
"os": "darwin",
"arch": "universal",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/macos/universal/Navis.dmg",
"zip_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/macos/universal/Navis.app.zip"
},
"android": { "android": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/android/Navis.apk", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/android/Navis.apk",
"sha256": "4416770bebe576eba42811990c4e4424f6722dea3a8513e6164dd34fc975b3cf", "sha256": "4416770bebe576eba42811990c4e4424f6722dea3a8513e6164dd34fc975b3cf",
"os": "android", "os": "android",
"arch": "universal" "arch": "universal"
},
"darwin-amd64": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis",
"sha256": "19a066cfc7f7c3a291a220482b28e66a459d091c19c5351818c401b982bc65b3",
"os": "darwin",
"arch": "amd64",
"dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis.dmg",
"zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis.app.zip"
},
"darwin-arm64": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis",
"sha256": "01a40b1cce4e2fb3a38eb80f0815c295a00af11e4288f3b9e5c0b58ae336f28d",
"os": "darwin",
"arch": "arm64",
"dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis.dmg",
"zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis.app.zip"
},
"darwin-universal": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis",
"sha256": "d53dba8256fd6f7b62d1ce9bbad741af087ae44fec942bf2782b9c8f4e36cef2",
"os": "darwin",
"arch": "universal",
"dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis.dmg",
"zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis.app.zip"
},
"windows-amd64": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
"sha256": "9b5cc04093561a6a6d7b97baf8f8cc9498dd863dfeb0bc3c92446c3fdb79f7c2",
"os": "windows",
"arch": "amd64"
} }
} }
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+41 -35
View File
@@ -1,48 +1,54 @@
{ {
"version": "3.8.2", "version": "4.2.9",
"notes": "3.8.2: Windows product version 3.8.2.1 (build 1).", "notes": "4.2.9: Add config accepts AWG 2.0 (.conf/JSON/awg://) and Naive share links (not only http(s) subscription URLs); Clash wireguard/naive; site contacts/FAQ",
"platform": "windows-amd64", "platform": "windows-amd64",
"os": "windows", "os": "windows",
"arch": "amd64", "arch": "amd64",
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/windows/Navis.exe", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
"sha256": "8049f973e8e8712b31fccac42c48be5eb436a5a6bf103eb979729e1cbfdec8ec", "sha256": "4e6de7e107a560cd5c24b83fc1064c50145cdee5c36049e8ff274fa0f8c542ea",
"mandatory": false, "mandatory": false,
"platforms": { "platforms": {
"windows-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/windows/Navis.exe",
"sha256": "8049f973e8e8712b31fccac42c48be5eb436a5a6bf103eb979729e1cbfdec8ec",
"os": "windows",
"arch": "amd64"
},
"darwin-arm64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/macos/arm64/Navis",
"sha256": "01a40b1cce4e2fb3a38eb80f0815c295a00af11e4288f3b9e5c0b58ae336f28d",
"os": "darwin",
"arch": "arm64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/macos/arm64/Navis.dmg",
"zip_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/macos/arm64/Navis.app.zip"
},
"darwin-amd64": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/macos/amd64/Navis",
"sha256": "19a066cfc7f7c3a291a220482b28e66a459d091c19c5351818c401b982bc65b3",
"os": "darwin",
"arch": "amd64",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/macos/amd64/Navis.dmg",
"zip_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/macos/amd64/Navis.app.zip"
},
"darwin-universal": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/macos/universal/Navis",
"sha256": "d53dba8256fd6f7b62d1ce9bbad741af087ae44fec942bf2782b9c8f4e36cef2",
"os": "darwin",
"arch": "universal",
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/macos/universal/Navis.dmg",
"zip_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/macos/universal/Navis.app.zip"
},
"android": { "android": {
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/android/Navis.apk", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/android/Navis.apk",
"sha256": "4416770bebe576eba42811990c4e4424f6722dea3a8513e6164dd34fc975b3cf", "sha256": "4416770bebe576eba42811990c4e4424f6722dea3a8513e6164dd34fc975b3cf",
"os": "android", "os": "android",
"arch": "universal" "arch": "universal"
},
"darwin-amd64": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis",
"sha256": "19a066cfc7f7c3a291a220482b28e66a459d091c19c5351818c401b982bc65b3",
"os": "darwin",
"arch": "amd64",
"dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis.dmg",
"zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis.app.zip"
},
"darwin-arm64": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis",
"sha256": "01a40b1cce4e2fb3a38eb80f0815c295a00af11e4288f3b9e5c0b58ae336f28d",
"os": "darwin",
"arch": "arm64",
"dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis.dmg",
"zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis.app.zip"
},
"darwin-universal": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis",
"sha256": "d53dba8256fd6f7b62d1ce9bbad741af087ae44fec942bf2782b9c8f4e36cef2",
"os": "darwin",
"arch": "universal",
"dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis.dmg",
"zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis.app.zip"
},
"windows-amd64": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
"sha256": "4e6de7e107a560cd5c24b83fc1064c50145cdee5c36049e8ff274fa0f8c542ea",
"os": "windows",
"arch": "amd64"
},
"windows-arm64": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/arm64/EvilFox.exe",
"sha256": "6d09911f4cf49747d0f71e1626eebb4c256884425a6f810f63121ae4ce8ea7e2",
"os": "windows",
"arch": "arm64"
} }
} }
} }
+75
View File
@@ -0,0 +1,75 @@
# EvilFox Privacy Policy
**Publisher:** EvilFox
**Effective date:** August 2, 2026
## 1. Who we are
EvilFox is a local VPN/proxy client for Windows. You supply your own connection configurations and URLs. The Microsoft Store build has no in-app purchases. We do not sell personal data.
## 2. Data stored on your device
On your device the app may store:
- profiles and connection URLs/parameters you enter;
- settings (mode, theme, language, Kill Switch, etc.);
- local traffic/session statistics (localStorage / config files).
This data stays on the device until you delete it or uninstall the app.
## 3. What we do not collect
As the client publisher, EvilFox does not operate a central service that collects accounts, payment details, contacts, browsing history, or your traffic contents for advertising or sale. We do not sell personal data.
## 4. Network traffic and third-party servers
When connected, traffic goes through servers specified in *your* configuration. The operator of those servers is your config provider (you or your provider), not necessarily EvilFox. Their privacy practices may differ — review your providers terms.
## 5. Update checks
When you choose to check/apply updates, the app may contact an update feed such as `https://git.de4ima.uk/…/update.json` (git hosting). Microsoft Store builds are normally updated via the Store; in-app update checks may be limited.
## 6. External links
`https://evilfox.win/` and Telegram `https://t.me/evilfox100Bot` open only after you tap them. Those services own policies then apply.
## 7. How data is used
Local data is used to run the client: connect, pick servers, show stats and settings. Network requests are for connecting to servers you choose and (optionally) checking for updates.
## 8. Retention and deletion
Data remains on-device until you uninstall, clear app data, or delete profiles. We cannot restore deleted local data from our servers because the client has no central user account store.
## 9. Security
We use reasonable measures appropriate for on-device settings within OS capabilities. No method is perfectly secure; you are responsible for keeping configuration URLs/keys confidential.
## 10. Childrens privacy
The app is not directed to children under 13 (in the EU, 16 where local law so requires). Do not use it below that age without parental/guardian consent where required.
## 11. International transfers (EU)
The client runs locally. If you connect to servers or open websites outside your country, data may be processed in other jurisdictions by those operators — as a result of your config and link choices.
## 12. Your rights
You can view and delete local data in the app or by uninstalling EvilFox / clearing app data. For questions about this policy, contact us below.
**California (CCPA):** we do not sell or “share” personal information for cross-context behavioral advertising. Access/deletion is exercised locally on the device.
## 13. EU GDPR summary (EEA/UK)
**Controller** for on-device client data: EvilFox (app publisher).
**Legal bases:** performance of the contract to provide client features, and legitimate interests in operating and updating the app.
Processing on third-party VPN/proxy servers is the responsibility of those server operators.
## 14. Changes
We may update this policy. The current version is in the app (Help) and on the website. Continued use after an update means you have had the chance to review the new text.
## 15. Contact
EvilFox · https://evilfox.win/
Primary: animegold3@gmail.com · Backup: admin@evilfox.win · Telegram: https://t.me/evilfox100Bot
+75
View File
@@ -0,0 +1,75 @@
# Политика конфиденциальности EvilFox
**Издатель:** EvilFox
**Дата вступления в силу:** 2 августа 2026 г.
## 1. Кто мы
EvilFox — локальный VPN/прокси-клиент для Windows. Вы сами добавляете конфигурации и URL подключения. В сборке Microsoft Store нет встроенных покупок. Мы не продаём персональные данные.
## 2. Какие данные хранятся локально
На вашем устройстве приложение может сохранять:
- профили и URL/параметры подключения, которые вы вводите;
- настройки (режим, тема, язык, Kill Switch и т.п.);
- локальную статистику трафика и сессий (localStorage / файлы конфигурации).
Эти данные остаются на устройстве, пока вы их не удалите или не удалите приложение.
## 3. Что мы не собираем
EvilFox как издатель клиента не ведёт централизованный сбор аккаунтов, платёжных данных, контактов, истории браузера или содержимого вашего трафика для рекламы или продажи. Мы не продаём персональные данные.
## 4. Сетевой трафик и сторонние серверы
При подключении трафик идёт через серверы, указанные в *вашей* конфигурации. Оператором этих серверов является поставщик конфигурации (вы или ваш провайдер), а не обязательно EvilFox. Их политика конфиденциальности может отличаться — ознакомьтесь с условиями вашего провайдера.
## 5. Проверка обновлений
По вашей инициативе (кнопка проверки/обновления) приложение может обратиться к ленте обновлений, например `https://git.de4ima.uk/…/update.json` (хостинг git). Сборка из Microsoft Store обычно обновляется через Store; встроенная проверка может быть ограничена.
## 6. Внешние ссылки
Сайт `https://evilfox.win/` и Telegram `https://t.me/evilfox100Bot` открываются только после вашего нажатия. Тогда действуют правила соответствующих сервисов.
## 7. Как используются данные
Локальные данные нужны для работы клиента: подключение, выбор сервера, отображение статистики и настроек. Сетевые обращения — для соединения с выбранными вами серверами и (по желанию) проверки обновлений.
## 8. Хранение и удаление
Данные хранятся на устройстве до удаления приложения, очистки данных приложения или ручного удаления профилей. Удалённые локальные данные мы не можем восстановить с наших серверов, потому что центрального профиля пользователя у клиента нет.
## 9. Безопасность
Мы применяем разумные меры защиты локальных настроек в рамках возможностей ОС. Абсолютной безопасности не существует; ответственность за конфиденциальность URL/ключей в конфигурациях лежит на вас.
## 10. Дети
Приложение не предназначено для детей младше 13 лет (в ЕС — ориентир 16 лет, если применимо местное право). Не используйте приложение, если вам меньше указанного возраста, без согласия родителя/опекуна где это требуется.
## 11. Международная передача (ЕС)
Клиент работает локально. Если вы подключаетесь к серверам или открываете сайты за пределами своей страны, данные могут обрабатываться в других юрисдикциях операторами этих сервисов. Это следует из вашего выбора конфигурации и внешних ссылок.
## 12. Ваши права
Вы можете просмотреть и удалить локальные данные в приложении или удалив EvilFox / очистив данные приложения. Для вопросов по этой политике свяжитесь с нами (ниже).
**Калифорния (CCPA):** мы не продаём и не «делимся» персональными данными в смысле CCPA для перекрёстной рекламы. Права на доступ/удаление реализуются локально на устройстве.
## 13. Кратко о GDPR (ЕЭЗ/Великобритания)
**Контролёр** в отношении локальных данных клиента: EvilFox (издатель приложения).
**Правовые основания:** исполнение договора на предоставление функций клиента и законный интерес в обеспечении работы и обновлений.
Обработка на сторонних VPN/прокси-серверах — ответственность оператора тех серверов.
## 14. Изменения
Мы можем обновлять эту политику. Актуальная версия доступна в приложении (Помощь) и на сайте. Продолжение использования после обновления означает ознакомление с новой редакцией.
## 15. Контакты
EvilFox · https://evilfox.win/
Основная: animegold3@gmail.com · Резервная: admin@evilfox.win · Telegram: https://t.me/evilfox100Bot
+45
View File
@@ -0,0 +1,45 @@
# EvilFox Terms of Use
**Publisher:** EvilFox
**Effective date:** August 2, 2026
## 1. Acceptance
By installing or using EvilFox, you agree to these Terms. If you do not agree, do not use the app.
## 2. License
You receive a limited, non-exclusive, non-transferable license to use the EvilFox client on your devices for personal or internal use, subject to these Terms and applicable law (including Microsoft Store rules if installed from the Store).
## 3. Acceptable use
Use EvilFox only for lawful purposes. You must not violate the laws of your country or third-party rights, or attempt to disrupt the app or others systems. You are responsible for how you use the client and the servers you choose.
## 4. User-supplied configurations
You provide connection URLs and parameters. EvilFox does not guarantee availability, quality, or legality of third-party servers. Review your configuration providers terms.
## 5. No warranty
The app is provided “as is” and “as available,” without warranties of any kind (express or implied, including merchantability and fitness for a particular purpose), to the fullest extent permitted by law.
## 6. Limitation of liability
To the maximum extent permitted by law, EvilFox is not liable for indirect, incidental, special, or consequential damages, lost profits, data loss, or connectivity interruptions arising from use of the client or third-party servers. Mandatory consumer rights (including in the EU) remain unaffected.
## 7. Updates
Microsoft Store builds update through the Store. Other (sideload) builds may offer updates via the feed on git.de4ima.uk. Features may change in new versions.
## 8. Termination
You may stop using the app by uninstalling it. We may discontinue support for particular builds. If you breach these Terms, your right to use the app ends.
## 9. Governing law
These Terms are interpreted under the laws of your country of residence unless mandatory rules require otherwise. EU consumers keep mandatory rights under the law of their country of residence.
## 10. Contact
EvilFox · https://evilfox.win/
Primary: animegold3@gmail.com · Backup: admin@evilfox.win · Telegram: https://t.me/evilfox100Bot
+45
View File
@@ -0,0 +1,45 @@
# Условия использования EvilFox
**Издатель:** EvilFox
**Дата вступления в силу:** 2 августа 2026 г.
## 1. Принятие условий
Устанавливая или используя EvilFox, вы соглашаетесь с этими Условиями. Если не согласны — не используйте приложение.
## 2. Лицензия
Вам предоставляется ограниченная, неисключительная, непередаваемая лицензия на использование клиента EvilFox на ваших устройствах в личных или внутренних целях в соответствии с этими Условиями и применимым правом (включая правила Microsoft Store, если приложение установлено из Store).
## 3. Допустимое использование
Используйте EvilFox только законно. Запрещены действия, нарушающие законы вашей страны или права третьих лиц, а также попытки нарушить работу приложения или чужих систем. Вы несёте ответственность за то, как используете клиент и выбранные серверы.
## 4. Конфигурации пользователя
URL и параметры подключения задаёте вы. EvilFox не гарантирует доступность, качество или законность сторонних серверов. Проверьте условия вашего провайдера конфигурации.
## 5. Нет гарантий
Приложение предоставляется «как есть» и «по мере доступности», без явных или подразумеваемых гарантий (включая товарную пригодность и пригодность для конкретной цели), в пределах, допускаемых законом.
## 6. Ограничение ответственности
В максимальной степени, допускаемой законом, EvilFox не несёт ответственности за косвенные, случайные, особые убытки, упущенную выгоду, потерю данных или перебои связи, связанные с использованием клиента или сторонних серверов. Обязательные права потребителей (в т.ч. в ЕС) сохраняются.
## 7. Обновления
Сборка из Microsoft Store обновляется через Store. Другие (sideload) сборки могут предлагать обновление через ленту на git.de4ima.uk. Мы можем менять функции в новых версиях.
## 8. Прекращение
Вы можете прекратить использование, удалив приложение. Мы можем прекратить поддержку отдельных сборок. При нарушении Условий право на использование прекращается.
## 9. Применимое право
Эти Условия толкуются по законам страны вашего проживания, если иное не требуется императивными нормами. Для потребителей в ЕС сохраняются обязательные права по праву страны проживания.
## 10. Контакты
EvilFox · https://evilfox.win/
Основная: animegold3@gmail.com · Резервная: admin@evilfox.win · Telegram: https://t.me/evilfox100Bot
+12 -6
View File
@@ -1,13 +1,15 @@
module vpnclient module vpnclient
go 1.25.0 go 1.26.3
require ( require (
github.com/amnezia-vpn/amneziawg-go v0.2.19 github.com/amnezia-vpn/amneziawg-go v0.2.19
github.com/diskfs/go-diskfs v1.9.4 github.com/diskfs/go-diskfs v1.9.4
github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808 github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808
github.com/xjasonlyu/tun2socks/v2 v2.7.0
golang.org/x/image v0.44.0 golang.org/x/image v0.44.0
golang.org/x/sys v0.43.0 golang.org/x/sys v0.46.0
gvisor.dev/gvisor v0.0.0-20260701204157-69c2d17aea96
) )
require ( require (
@@ -22,9 +24,13 @@ require (
github.com/pkg/xattr v0.4.12 // indirect github.com/pkg/xattr v0.4.12 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect github.com/sirupsen/logrus v1.9.4 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect github.com/ulikunitz/xz v0.5.15 // indirect
golang.org/x/crypto v0.42.0 // indirect go.uber.org/atomic v1.11.0 // indirect
golang.org/x/net v0.44.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/time v0.9.0 // indirect go.uber.org/zap v1.28.0 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 // indirect golang.zx2c4.com/wireguard v0.0.0-20260522210424-ecfc5a8d5446 // indirect
) )
+28 -14
View File
@@ -2,8 +2,8 @@ github.com/amnezia-vpn/amneziawg-go v0.2.19 h1:l3rOmrA4o5z38kpgnA5iSk1yOm7Cv3Aaf
github.com/amnezia-vpn/amneziawg-go v0.2.19/go.mod h1:aMgOk9MuX0xI7b5TKAYp8pLM54RlXcOPzDvYw3YEO5A= github.com/amnezia-vpn/amneziawg-go v0.2.19/go.mod h1:aMgOk9MuX0xI7b5TKAYp8pLM54RlXcOPzDvYw3YEO5A=
github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs= github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs=
github.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk= github.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/diskfs/go-diskfs v1.9.4 h1:0j2d7eG4IjyxL6+ChWbDPocdBCF6HQ4HBWU2WDYWVnc= github.com/diskfs/go-diskfs v1.9.4 h1:0j2d7eG4IjyxL6+ChWbDPocdBCF6HQ4HBWU2WDYWVnc=
github.com/diskfs/go-diskfs v1.9.4/go.mod h1:TePJORO83Adh5pb2SqsxAwaP0fofFxKLkxctiS/9OQc= github.com/diskfs/go-diskfs v1.9.4/go.mod h1:TePJORO83Adh5pb2SqsxAwaP0fofFxKLkxctiS/9OQc=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
@@ -32,29 +32,43 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xjasonlyu/tun2socks/v2 v2.7.0 h1:fYEN0Q1sSanuoID8xvUTsMODbKLJJH/J5ywYoMRxIPw=
github.com/xjasonlyu/tun2socks/v2 v2.7.0/go.mod h1:XHgJ3wbs63mZxGbJawt6jjNX65aPDl+Qhn2ePPEE4KI=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M=
golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY=
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I= golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY= golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210218145245-beda7e5e158e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210218145245-beda7e5e158e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20260522210424-ecfc5a8d5446 h1:cqHQ3AycTHvM2R7ikgyX57D+XvtcSnGylsLkOVhta/w=
golang.zx2c4.com/wireguard v0.0.0-20260522210424-ecfc5a8d5446/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 h1:ze1vwAdliUAr68RQ5NtufWaXaOg8WUO2OACzEV+TNdE= gvisor.dev/gvisor v0.0.0-20260701204157-69c2d17aea96 h1:LZXOf4NwvAwUy/eI8P+Y3DUdYLyaCEDNPEjsL2OP2ro=
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk= gvisor.dev/gvisor v0.0.0-20260701204157-69c2d17aea96/go.mod h1:8aLQqUBHDH8fY5y60lzmwDpMMbQCcT3EBfoSwhfaGCY=
+160 -13
View File
@@ -8,6 +8,7 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"net/url"
"os" "os"
"strings" "strings"
"sync" "sync"
@@ -21,6 +22,7 @@ import (
"vpnclient/internal/protocols/hysteria2" "vpnclient/internal/protocols/hysteria2"
"vpnclient/internal/protocols/naive" "vpnclient/internal/protocols/naive"
"vpnclient/internal/protocols/xray" "vpnclient/internal/protocols/xray"
"vpnclient/internal/remnawave"
"vpnclient/internal/update" "vpnclient/internal/update"
) )
@@ -53,8 +55,17 @@ type UIState struct {
Update update.Status `json:"update"` Update update.Status `json:"update"`
Pings []netcheck.Result `json:"pings"` Pings []netcheck.Result `json:"pings"`
Subscription string `json:"subscription_url"` Subscription string `json:"subscription_url"`
SubInfo *config.SubscriptionInfo `json:"sub_info,omitempty"`
Remnawave remnawave.Settings `json:"remnawave"`
Hy2 core.Hy2Options `json:"hy2"` Hy2 core.Hy2Options `json:"hy2"`
StorePackaged bool `json:"store_packaged,omitempty"` StorePackaged bool `json:"store_packaged,omitempty"`
Mode string `json:"mode"`
VPNSupported bool `json:"vpn_mode_supported"`
VPNActive bool `json:"vpn_active"`
// KillSwitch is the persisted setting; KillSwitchActive means the tunnel
// died unexpectedly and traffic is blocked until disconnect/reconnect.
KillSwitch bool `json:"kill_switch"`
KillSwitchActive bool `json:"kill_switch_active,omitempty"`
} }
type PingBestResult struct { type PingBestResult struct {
@@ -130,8 +141,15 @@ func (a *App) GetState() (UIState, error) {
Update: a.UpdateStatus, Update: a.UpdateStatus,
Pings: append([]netcheck.Result(nil), a.Pings...), Pings: append([]netcheck.Result(nil), a.Pings...),
Subscription: cfg.SubscriptionURL, Subscription: cfg.SubscriptionURL,
SubInfo: a.Mgr.SubscriptionInfo(),
Remnawave: a.Mgr.RemnawaveSettings(),
Hy2: a.Mgr.ActiveHy2Options(), Hy2: a.Mgr.ActiveHy2Options(),
StorePackaged: update.IsStorePackaged(), StorePackaged: update.IsStorePackaged(),
Mode: a.Mgr.Mode(),
VPNSupported: a.Mgr.VPNModeSupported(),
VPNActive: st.VPNActive,
KillSwitch: cfg.KillSwitch,
KillSwitchActive: st.KillSwitchActive,
} }
if out.Protocol == "" { if out.Protocol == "" {
if p, err := cfg.ActiveProfile(); err == nil { if p, err := cfg.ActiveProfile(); err == nil {
@@ -236,14 +254,84 @@ func (a *App) SaveHy2(opts core.Hy2Options) error {
return a.Mgr.SaveHy2Options(opts) return a.Mgr.SaveHy2Options(opts)
} }
func (a *App) ImportSubscription(rawURL string) (int, error) { func (a *App) ImportSubscription(rawURL string) (core.ImportResult, error) {
return a.Mgr.ImportSubscription(rawURL) return a.Mgr.ImportSubscription(rawURL)
} }
func (a *App) SaveRemnawave(s remnawave.Settings) error {
return a.Mgr.SaveRemnawaveSettings(s)
}
func (a *App) ImportRemnawave(s remnawave.Settings) (core.ImportResult, error) {
return a.Mgr.ImportRemnawave(s)
}
func (a *App) SetMode(mode string) error {
return a.Mgr.SetMode(strings.TrimSpace(mode))
}
// GetLogs returns the tail of the core log buffer for the «Логи» page.
func (a *App) GetLogs() (string, error) {
if a.LogBuf == nil {
return "", nil
}
const maxTail = 64 * 1024
b := a.LogBuf.Bytes()
if len(b) > maxTail {
b = b[len(b)-maxTail:]
}
return string(b), nil
}
func (a *App) PingServers() ([]netcheck.Result, error) { func (a *App) PingServers() ([]netcheck.Result, error) {
return a.runPings() return a.runPings()
} }
// PingServer measures latency of a single server (📊 button on a server row)
// and merges the result into the cached ping list.
func (a *App) PingServer(name string) (netcheck.Result, error) {
name = strings.TrimSpace(name)
a.mu.Lock()
list := a.Mgr.Profiles()
a.mu.Unlock()
var target *netcheck.Target
for _, p := range list {
if p.Name == name {
target = &netcheck.Target{Name: p.Name, Protocol: config.Protocol(p.Protocol), Proxy: p.Proxy}
break
}
}
if target == nil {
return netcheck.Result{}, fmt.Errorf("сервер %q не найден", name)
}
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
defer cancel()
out := netcheck.PingAll(ctx, []netcheck.Target{*target})
if len(out) == 0 {
return netcheck.Result{}, fmt.Errorf("не удалось измерить пинг")
}
res := out[0]
a.mu.Lock()
replaced := false
for i := range a.Pings {
if a.Pings[i].Name == res.Name {
a.Pings[i] = res
replaced = true
break
}
}
if !replaced {
a.Pings = append(a.Pings, res)
}
a.mu.Unlock()
return res, nil
}
// SetKillSwitch persists the leak-protection setting.
func (a *App) SetKillSwitch(enabled bool) error {
return a.Mgr.SetKillSwitch(enabled)
}
func (a *App) PingBest(autoConnect bool) (PingBestResult, error) { func (a *App) PingBest(autoConnect bool) (PingBestResult, error) {
pings, err := a.runPings() pings, err := a.runPings()
out := PingBestResult{Pings: pings} out := PingBestResult{Pings: pings}
@@ -360,7 +448,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() {
@@ -381,22 +469,61 @@ func (a *App) AutoCheckUpdate() {
_, _ = a.CheckUpdate() _, _ = a.CheckUpdate()
} }
func (a *App) OpenShopURL(raw string) error { // allowedLinkHosts are the only hosts the UI may open in the default browser
// (shop / «Полезные ссылки» in index.html).
var allowedLinkHosts = map[string]bool{
"evilfox.win": true,
"t.me": true,
}
// allowedMailto are support addresses the UI may open via mailto:.
var allowedMailto = map[string]bool{
"animegold3@gmail.com": true,
"admin@evilfox.win": true,
}
// LinkAllowed validates that raw is an http(s) URL pointing at one of the
// allowed hosts, or a mailto: address from the allowlist; returns the normalized URL.
func LinkAllowed(raw string) (string, error) {
raw = strings.TrimSpace(raw) raw = strings.TrimSpace(raw)
if raw == "" { if raw == "" {
return fmt.Errorf("пустая ссылка") return "", fmt.Errorf("пустая ссылка")
} }
lower := strings.ToLower(raw) u, err := url.Parse(raw)
switch { if err != nil {
case lower == "https://evilfox.win/" || lower == "https://evilfox.win" || return "", fmt.Errorf("некорректная ссылка")
lower == "http://evilfox.win/" || lower == "http://evilfox.win": }
raw = "https://evilfox.win/" if strings.EqualFold(u.Scheme, "mailto") {
case strings.HasPrefix(lower, "https://evilfox.win/") || strings.HasPrefix(lower, "http://evilfox.win/"): addr := strings.TrimSpace(u.Opaque)
default: if addr == "" {
return fmt.Errorf("разрешена только ссылка evilfox.win") addr = strings.TrimSpace(u.Path)
}
addr = strings.TrimPrefix(addr, "//")
if i := strings.IndexAny(addr, "?#"); i >= 0 {
addr = addr[:i]
}
addr = strings.ToLower(addr)
if !allowedMailto[addr] {
return "", fmt.Errorf("почта %s не разрешена", addr)
}
return "mailto:" + addr, nil
}
if u.Scheme != "https" && u.Scheme != "http" {
return "", fmt.Errorf("разрешены только http(s) и mailto ссылки")
}
if !allowedLinkHosts[strings.ToLower(u.Hostname())] {
return "", fmt.Errorf("ссылка на %s не разрешена", u.Hostname())
}
return u.String(), nil
}
func (a *App) OpenShopURL(raw string) error {
normalized, err := LinkAllowed(raw)
if err != nil {
return err
} }
if a.OpenURL != nil { if a.OpenURL != nil {
return a.OpenURL(raw) return a.OpenURL(normalized)
} }
return fmt.Errorf("openURL не настроен") return fmt.Errorf("openURL не настроен")
} }
@@ -472,6 +599,10 @@ func (a *App) dispatch(name string, args []json.RawMessage) (any, error) {
return nil, a.OpenShopURL(arg(args, 0, "")) return nil, a.OpenShopURL(arg(args, 0, ""))
case "pingServers": case "pingServers":
return a.PingServers() return a.PingServers()
case "pingServer":
return a.PingServer(arg(args, 0, ""))
case "setKillSwitch":
return nil, a.SetKillSwitch(arg(args, 0, false))
case "pingBest": case "pingBest":
return a.PingBest(arg(args, 0, false)) return a.PingBest(arg(args, 0, false))
case "checkUpdate": case "checkUpdate":
@@ -486,6 +617,22 @@ func (a *App) dispatch(name string, args []json.RawMessage) (any, error) {
return nil, a.SaveHy2(opts) return nil, a.SaveHy2(opts)
case "importSubscription": case "importSubscription":
return a.ImportSubscription(arg(args, 0, "")) return a.ImportSubscription(arg(args, 0, ""))
case "saveRemnawave":
var s remnawave.Settings
if len(args) > 0 {
_ = json.Unmarshal(args[0], &s)
}
return nil, a.SaveRemnawave(s)
case "importRemnawave":
var s remnawave.Settings
if len(args) > 0 {
_ = json.Unmarshal(args[0], &s)
}
return a.ImportRemnawave(s)
case "setMode":
return nil, a.SetMode(arg(args, 0, ""))
case "getLogs":
return a.GetLogs()
case "quit": case "quit":
go func() { go func() {
time.Sleep(200 * time.Millisecond) time.Sleep(200 * time.Millisecond)
+2967 -536
View File
File diff suppressed because it is too large Load Diff
+41
View File
@@ -27,15 +27,51 @@ type Config struct {
// SystemProxy enables Windows Internet Settings / WinHTTP proxy on connect. // SystemProxy enables Windows Internet Settings / WinHTTP proxy on connect.
SystemProxy bool `json:"system_proxy"` SystemProxy bool `json:"system_proxy"`
// Mode selects traffic capture: "proxy" (default — local proxy + optional
// system proxy) or "vpn" (Windows: TUN device, ALL traffic incl. apps/games).
Mode string `json:"mode,omitempty"`
// KillSwitch keeps leak protection engaged when the tunnel dies
// unexpectedly: in VPN mode the TUN blackhole routes stay up, in proxy
// mode the system proxy keeps pointing at the dead local port, so traffic
// is blocked until the user disconnects manually or reconnects.
KillSwitch bool `json:"kill_switch,omitempty"`
// BinDir is where protocol binaries (naive.exe / hysteria.exe) are stored. // BinDir is where protocol binaries (naive.exe / hysteria.exe) are stored.
BinDir string `json:"bin_dir,omitempty"` BinDir string `json:"bin_dir,omitempty"`
// SubscriptionURL pulls share links (one per line or base64) and imports profiles. // SubscriptionURL pulls share links (one per line or base64) and imports profiles.
SubscriptionURL string `json:"subscription_url,omitempty"` SubscriptionURL string `json:"subscription_url,omitempty"`
// Remnawave panel integration (local only — do not commit tokens).
RemnawaveBaseURL string `json:"remnawave_base_url,omitempty"`
RemnawaveToken string `json:"remnawave_token,omitempty"`
RemnawaveShortUUID string `json:"remnawave_short_uuid,omitempty"`
RemnawaveCaddyToken string `json:"remnawave_caddy_token,omitempty"`
// SubInfo caches last-known subscription usage so the UI can render it
// after restart; refreshed on subscription import.
SubInfo *SubscriptionInfo `json:"subscription_info,omitempty"`
Profiles []Profile `json:"profiles"` Profiles []Profile `json:"profiles"`
} }
// SubscriptionInfo is last-known subscription usage metadata.
// Traffic values are bytes; Total==0 means unlimited/unknown.
type SubscriptionInfo struct {
Upload int64 `json:"upload,omitempty"`
Download int64 `json:"download,omitempty"`
Total int64 `json:"total,omitempty"`
Expire int64 `json:"expire,omitempty"` // unix seconds; 0 = unknown
// Devices are known only when the Remnawave API token is configured.
DevicesKnown bool `json:"devices_known,omitempty"`
Devices int `json:"devices,omitempty"`
DeviceLimit int `json:"device_limit,omitempty"` // 0 = no limit / unknown
UpdatedAt int64 `json:"updated_at,omitempty"` // unix seconds of last refresh
}
// Profile describes one connection endpoint. // Profile describes one connection endpoint.
type Profile struct { type Profile struct {
Name string `json:"name"` Name string `json:"name"`
@@ -171,6 +207,11 @@ func stripUTF8BOM(data []byte) []byte {
// Validate checks required fields. // Validate checks required fields.
func (c *Config) Validate() error { func (c *Config) Validate() error {
switch c.Mode {
case "", "proxy", "vpn":
default:
c.Mode = "proxy"
}
if len(c.Profiles) == 0 { if len(c.Profiles) == 0 {
return fmt.Errorf("config: no profiles defined") return fmt.Errorf("config: no profiles defined")
} }
+10
View File
@@ -3,6 +3,8 @@ package config
import ( import (
"fmt" "fmt"
"strings" "strings"
"vpnclient/internal/geoflag"
) )
// ProfileInfo is a safe summary for UI lists (no secrets beyond proxy URI the user already owns). // ProfileInfo is a safe summary for UI lists (no secrets beyond proxy URI the user already owns).
@@ -12,18 +14,26 @@ type ProfileInfo struct {
Proxy string `json:"proxy"` Proxy string `json:"proxy"`
Host string `json:"host"` Host string `json:"host"`
Active bool `json:"active"` Active bool `json:"active"`
Country string `json:"country,omitempty"` // ISO 3166-1 alpha-2
Flag string `json:"flag,omitempty"` // emoji flag or empty
} }
// ListProfiles returns UI-friendly profile summaries. // ListProfiles returns UI-friendly profile summaries.
func (c *Config) ListProfiles() []ProfileInfo { func (c *Config) ListProfiles() []ProfileInfo {
out := make([]ProfileInfo, 0, len(c.Profiles)) out := make([]ProfileInfo, 0, len(c.Profiles))
for _, p := range c.Profiles { for _, p := range c.Profiles {
cc, flag := geoflag.FromText(p.Name)
if cc == "" {
cc, flag = geoflag.FromText(proxyHost(p.Proxy))
}
out = append(out, ProfileInfo{ out = append(out, ProfileInfo{
Name: p.Name, Name: p.Name,
Protocol: string(p.Protocol), Protocol: string(p.Protocol),
Proxy: p.Proxy, Proxy: p.Proxy,
Host: proxyHost(p.Proxy), Host: proxyHost(p.Proxy),
Active: p.Name == c.Active, Active: p.Name == c.Active,
Country: cc,
Flag: flag,
}) })
} }
return out return out
+4
View File
@@ -24,5 +24,9 @@ type Status struct {
HTTPProxy string `json:"http_proxy,omitempty"` HTTPProxy string `json:"http_proxy,omitempty"`
SOCKSProxy string `json:"socks_proxy,omitempty"` SOCKSProxy string `json:"socks_proxy,omitempty"`
SystemProxy bool `json:"system_proxy"` SystemProxy bool `json:"system_proxy"`
VPNActive bool `json:"vpn_active,omitempty"`
// KillSwitchActive: the tunnel died unexpectedly and traffic is being
// blocked on purpose until manual disconnect / reconnect.
KillSwitchActive bool `json:"kill_switch_active,omitempty"`
Extra map[string]string `json:"extra,omitempty"` Extra map[string]string `json:"extra,omitempty"`
} }
+366 -12
View File
@@ -15,12 +15,15 @@ import (
"vpnclient/internal/config" "vpnclient/internal/config"
"vpnclient/internal/linknorm" "vpnclient/internal/linknorm"
"vpnclient/internal/netcheck"
"vpnclient/internal/protocols/awg" "vpnclient/internal/protocols/awg"
"vpnclient/internal/protocols/hysteria2" "vpnclient/internal/protocols/hysteria2"
"vpnclient/internal/protocols/naive" "vpnclient/internal/protocols/naive"
"vpnclient/internal/protocols/xray" "vpnclient/internal/protocols/xray"
"vpnclient/internal/remnawave"
"vpnclient/internal/subscription" "vpnclient/internal/subscription"
"vpnclient/internal/sysproxy" "vpnclient/internal/sysproxy"
"vpnclient/internal/vpnmode"
) )
// Manager orchestrates protocol engines and Windows system proxy. // Manager orchestrates protocol engines and Windows system proxy.
@@ -33,6 +36,15 @@ type Manager struct {
profile *config.Profile profile *config.Profile
stderr io.Writer stderr io.Writer
binDir string binDir string
// vpn is the active VPN-mode (TUN) session, nil in proxy mode.
vpn *vpnmode.Session
// watchGen invalidates engine watchdogs from previous connects.
watchGen uint64
// ksEngaged is true after an unexpected engine death with kill switch on:
// blackhole routes / dead system proxy are intentionally left in place.
ksEngaged bool
} }
func NewManager(cfgPath string, cfg *config.Config, stderr io.Writer) (*Manager, error) { func NewManager(cfgPath string, cfg *config.Config, stderr io.Writer) (*Manager, error) {
@@ -60,6 +72,18 @@ func (m *Manager) Connect(ctx context.Context, profileName string) error {
return fmt.Errorf("already connected; disconnect first") return fmt.Errorf("already connected; disconnect first")
} }
// Reconnect releases a previously engaged kill switch (blackhole state).
if m.ksEngaged {
if m.vpn != nil {
_ = m.vpn.Close()
m.vpn = nil
}
if m.sys.Enabled() {
_ = m.sys.Disable()
}
m.ksEngaged = false
}
if profileName != "" { if profileName != "" {
m.cfg.Active = profileName m.cfg.Active = profileName
} }
@@ -77,7 +101,37 @@ func (m *Manager) Connect(ctx context.Context, profileName string) error {
return err return err
} }
if m.cfg.SystemProxy { vpnActive := false
if m.cfg.Mode == "vpn" {
if !vpnmode.Supported() {
_ = engine.Stop()
return fmt.Errorf("режим VPN пока доступен только на Windows — переключитесь на режим прокси")
}
socksAddr, ok := engine.LocalSOCKSProxy()
if !ok {
_ = engine.Stop()
return fmt.Errorf("режим VPN: у протокола нет локального SOCKS-прокси")
}
host, _, err := netcheck.HostPort(profile.Protocol, profile.Proxy)
if err != nil || strings.TrimSpace(host) == "" {
_ = engine.Stop()
return fmt.Errorf("режим VPN: не удалось определить адрес сервера для исключения из туннеля")
}
sess, err := vpnmode.Start(vpnmode.Config{
SOCKSAddr: socksAddr,
ExcludeHosts: []string{host},
Stderr: m.stderr,
})
if err != nil {
_ = engine.Stop()
return err
}
m.vpn = sess
vpnActive = true
}
// System proxy is redundant while the TUN captures everything.
if m.cfg.SystemProxy && !vpnActive {
httpProxy, ok := engine.LocalHTTPProxy() httpProxy, ok := engine.LocalHTTPProxy()
if !ok { if !ok {
_ = engine.Stop() _ = engine.Stop()
@@ -88,6 +142,10 @@ func (m *Manager) Connect(ctx context.Context, profileName string) error {
fmt.Fprintf(m.stderr, "system proxy unsupported on this OS; local SOCKS/HTTP still up\n") fmt.Fprintf(m.stderr, "system proxy unsupported on this OS; local SOCKS/HTTP still up\n")
} else { } else {
_ = engine.Stop() _ = engine.Stop()
if m.vpn != nil {
_ = m.vpn.Close()
m.vpn = nil
}
return fmt.Errorf("enable system proxy: %w", err) return fmt.Errorf("enable system proxy: %w", err)
} }
} }
@@ -95,18 +153,88 @@ func (m *Manager) Connect(ctx context.Context, profileName string) error {
m.engine = engine m.engine = engine
m.profile = profile m.profile = profile
m.watchGen++
go m.watchEngine(m.watchGen, engine)
return nil return nil
} }
// watchEngine polls the engine and reacts to an UNEXPECTED death (crash of the
// core process). A normal Disconnect bumps watchGen first, so the watcher just
// exits. With kill switch enabled the blackhole state is kept, otherwise the
// network is restored (system proxy off, TUN routes removed).
func (m *Manager) watchEngine(gen uint64, engine Engine) {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for range ticker.C {
m.mu.Lock()
if m.watchGen != gen || m.engine != engine {
m.mu.Unlock()
return
}
if engine.Running() {
m.mu.Unlock()
continue
}
// Unexpected death: engine stopped without Disconnect.
m.engine = nil
m.profile = nil
ks := m.cfg.KillSwitch
vpn := m.vpn
sysOn := m.sys.Enabled()
if ks && (vpn != nil || sysOn) {
// Keep TUN blackhole routes / dead system proxy: traffic is
// blocked until the user disconnects manually or reconnects.
m.ksEngaged = true
m.mu.Unlock()
fmt.Fprintf(m.stderr, "kill switch: соединение прервано — трафик заблокирован до ручного отключения или переподключения\n")
return
}
m.vpn = nil
m.mu.Unlock()
if vpn != nil {
_ = vpn.Close()
}
if sysOn {
_ = m.sys.Disable()
}
fmt.Fprintf(m.stderr, "соединение прервано — сетевые настройки восстановлены\n")
return
}
}
// KillSwitchEnabled returns the persisted kill switch setting.
func (m *Manager) KillSwitchEnabled() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.cfg.KillSwitch
}
// SetKillSwitch persists the kill switch setting.
func (m *Manager) SetKillSwitch(enabled bool) error {
m.mu.Lock()
defer m.mu.Unlock()
m.cfg.KillSwitch = enabled
return config.Save(m.cfgPath, *m.cfg)
}
func (m *Manager) Disconnect() error { func (m *Manager) Disconnect() error {
m.mu.Lock() m.mu.Lock()
sys := m.sys sys := m.sys
engine := m.engine engine := m.engine
vpn := m.vpn
m.engine = nil m.engine = nil
m.profile = nil m.profile = nil
m.vpn = nil
m.watchGen++ // cancel the engine watchdog — this stop is intentional
m.ksEngaged = false
m.mu.Unlock() m.mu.Unlock()
var first error var first error
if vpn != nil {
if err := vpn.Close(); err != nil && first == nil {
first = err
}
}
if sys != nil && sys.Enabled() { if sys != nil && sys.Enabled() {
if err := sys.Disable(); err != nil && first == nil { if err := sys.Disable(); err != nil && first == nil {
first = err first = err
@@ -134,7 +262,7 @@ func (m *Manager) Status() Status {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
st := Status{SystemProxy: m.sys.Enabled()} st := Status{SystemProxy: m.sys.Enabled(), VPNActive: m.vpn != nil, KillSwitchActive: m.ksEngaged}
if m.engine == nil || !m.engine.Running() { if m.engine == nil || !m.engine.Running() {
return st return st
} }
@@ -219,7 +347,54 @@ func (m *Manager) SetSystemProxy(enabled bool) {
m.cfg.SystemProxy = enabled m.cfg.SystemProxy = enabled
} }
// Mode returns the persisted traffic mode: "proxy" (default) or "vpn".
func (m *Manager) Mode() string {
m.mu.Lock()
defer m.mu.Unlock()
if m.cfg.Mode == "vpn" {
return "vpn"
}
return "proxy"
}
// SetMode persists the traffic mode; takes effect on next connect.
func (m *Manager) SetMode(mode string) error {
switch mode {
case "proxy", "vpn":
default:
return fmt.Errorf("неизвестный режим %q", mode)
}
if mode == "vpn" {
if !vpnmode.Supported() {
return fmt.Errorf("режим VPN пока доступен только на Windows")
}
if !vpnmode.IsElevated() {
return fmt.Errorf("Запустите EvilFox от имени администратора для режима VPN")
}
if !vpnmode.WintunAvailable() {
return fmt.Errorf("не найден wintun.dll — скачайте с wintun.net и положите рядом с EvilFox.exe")
}
}
m.mu.Lock()
defer m.mu.Unlock()
if m.engine != nil && m.engine.Running() {
return fmt.Errorf("сначала отключитесь, затем меняйте режим")
}
m.cfg.Mode = mode
return config.Save(m.cfgPath, *m.cfg)
}
// VPNModeSupported reports whether the VPN (TUN) switch should be shown.
func (m *Manager) VPNModeSupported() bool { return vpnmode.Supported() }
func (m *Manager) UpdateProxyURI(proxy string) error { func (m *Manager) UpdateProxyURI(proxy string) error {
// A plain http(s) URL without credentials is a subscription URL, not a
// proxy link — run the subscription import instead of failing with
// "proxy URI missing username".
if linknorm.LooksLikeSubscriptionURL(proxy) {
_, err := m.ImportSubscription(proxy)
return err
}
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
active, err := m.cfg.ActiveProfile() active, err := m.cfg.ActiveProfile()
@@ -252,6 +427,11 @@ func (m *Manager) SetActiveProfile(name string) error {
} }
func (m *Manager) SaveProfile(name, proxy string) error { func (m *Manager) SaveProfile(name, proxy string) error {
// Subscription URL pasted into the "add server link" field → import it.
if linknorm.LooksLikeSubscriptionURL(proxy) {
_, err := m.ImportSubscription(proxy)
return err
}
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
if m.engine != nil && m.engine.Running() { if m.engine != nil && m.engine.Running() {
@@ -278,6 +458,11 @@ func (m *Manager) SaveProfile(name, proxy string) error {
// SaveActiveProfile updates the current profile (rename if needed) and proxy URI. // SaveActiveProfile updates the current profile (rename if needed) and proxy URI.
func (m *Manager) SaveActiveProfile(name, proxy string) error { func (m *Manager) SaveActiveProfile(name, proxy string) error {
// Subscription URL pasted into the profile link field → import it.
if linknorm.LooksLikeSubscriptionURL(proxy) {
_, err := m.ImportSubscription(proxy)
return err
}
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
if m.engine != nil && m.engine.Running() { if m.engine != nil && m.engine.Running() {
@@ -417,21 +602,167 @@ func (m *Manager) SubscriptionURL() string {
return m.cfg.SubscriptionURL return m.cfg.SubscriptionURL
} }
func (m *Manager) ImportSubscription(rawURL string) (int, error) { func (m *Manager) RemnawaveSettings() remnawave.Settings {
m.mu.Lock()
defer m.mu.Unlock()
return remnawave.Settings{
BaseURL: m.cfg.RemnawaveBaseURL,
Token: m.cfg.RemnawaveToken,
ShortUUID: m.cfg.RemnawaveShortUUID,
CaddyToken: m.cfg.RemnawaveCaddyToken,
}
}
func (m *Manager) SaveRemnawaveSettings(s remnawave.Settings) error {
m.mu.Lock()
defer m.mu.Unlock()
m.cfg.RemnawaveBaseURL = remnawave.NormalizeBase(s.BaseURL)
m.cfg.RemnawaveToken = strings.TrimSpace(s.Token)
m.cfg.RemnawaveShortUUID = strings.TrimSpace(s.ShortUUID)
m.cfg.RemnawaveCaddyToken = strings.TrimSpace(s.CaddyToken)
return config.Save(m.cfgPath, *m.cfg)
}
// ImportResult reports how a subscription import went.
type ImportResult struct {
Imported int `json:"imported"`
Skipped int `json:"skipped"`
Warnings []string `json:"warnings,omitempty"`
}
func (m *Manager) ImportSubscription(rawURL string) (ImportResult, error) {
rawURL = strings.TrimSpace(rawURL)
if rawURL == "" {
return ImportResult{}, fmt.Errorf("пустой URL или конфигурация")
}
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel() defer cancel()
items, err := subscription.Fetch(ctx, rawURL)
if err != nil { // Remnawave-style URL → persist panel coords and fetch via API helper.
return 0, err if base, short, ok := remnawave.ParseInput(rawURL); ok {
s := remnawave.Settings{
BaseURL: base,
ShortUUID: short,
Token: m.RemnawaveSettings().Token,
CaddyToken: m.RemnawaveSettings().CaddyToken,
} }
if len(items) == 0 { res, used, err := remnawave.Fetch(ctx, s)
return 0, fmt.Errorf("в подписке нет поддерживаемых ссылок (naive / hysteria2 / awg / vless / vmess / trojan)") if err != nil {
return ImportResult{}, err
}
return m.applySubscription(ctx, res, used, &s)
}
// Remote subscription / hosted config file (http(s) without user:pass@).
if linknorm.LooksLikeSubscriptionURL(rawURL) {
res, err := subscription.Fetch(ctx, rawURL)
if err != nil {
return ImportResult{}, err
}
return m.applySubscription(ctx, res, rawURL, nil)
}
// Direct paste into «Добавить конфигурацию»: awg://, AWG 2.0 .conf / JSON,
// naive share links, Clash YAML body, multi-line link lists — no HTTP fetch.
res, err := subscription.ParseBodyDetailed(rawURL)
if err != nil || res == nil || len(res.Items) == 0 {
if err != nil {
return ImportResult{}, fmt.Errorf("%w — либо вставьте http(s) URL конфигурации, либо ссылку/тело AWG, naive, hy2, vless…", err)
}
return ImportResult{}, fmt.Errorf("не удалось распознать конфигурацию — нужен http(s) URL либо ссылка/тело (AWG, naive, hy2, vless…)")
}
return m.applySubscription(ctx, res, "", nil)
}
// ImportRemnawave fetches configs using saved / provided panel settings.
func (m *Manager) ImportRemnawave(s remnawave.Settings) (ImportResult, error) {
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
if strings.TrimSpace(s.BaseURL) == "" || strings.TrimSpace(s.ShortUUID) == "" {
cur := m.RemnawaveSettings()
if strings.TrimSpace(s.BaseURL) == "" {
s.BaseURL = cur.BaseURL
}
if strings.TrimSpace(s.ShortUUID) == "" {
s.ShortUUID = cur.ShortUUID
}
if strings.TrimSpace(s.Token) == "" {
s.Token = cur.Token
}
if strings.TrimSpace(s.CaddyToken) == "" {
s.CaddyToken = cur.CaddyToken
}
}
res, used, err := remnawave.Fetch(ctx, s)
if err != nil {
return ImportResult{}, err
}
return m.applySubscription(ctx, res, used, &s)
}
func (m *Manager) applySubscription(ctx context.Context, res *subscription.Result, usedURL string, rw *remnawave.Settings) (ImportResult, error) {
if res == nil || len(res.Items) == 0 {
if res != nil && res.Skipped > 0 {
return ImportResult{Skipped: res.Skipped, Warnings: res.Warnings},
fmt.Errorf("нет валидных серверов: %d записей пропущено (%s)", res.Skipped, strings.Join(res.Warnings, "; "))
}
return ImportResult{}, fmt.Errorf("в конфигурации нет поддерживаемых ссылок (naive / hysteria2 / awg / vless / vmess / trojan)")
}
// Subscription usage info: header data first, enriched via Remnawave API
// (devices / limits) when an API token is configured. Best-effort.
subInfo := buildSubInfo(res.Info)
{
s := m.RemnawaveSettings()
if rw != nil {
s = *rw
if strings.TrimSpace(s.Token) == "" {
s.Token = m.RemnawaveSettings().Token
}
}
if strings.TrimSpace(s.Token) != "" && strings.TrimSpace(s.BaseURL) != "" && strings.TrimSpace(s.ShortUUID) != "" {
if ui, err := remnawave.FetchUserInfo(ctx, s); err == nil && ui != nil {
if subInfo == nil {
subInfo = &config.SubscriptionInfo{}
}
if subInfo.Upload == 0 && subInfo.Download == 0 && ui.UsedTraffic > 0 {
subInfo.Download = ui.UsedTraffic
}
if subInfo.Total == 0 && ui.TrafficLimit > 0 {
subInfo.Total = ui.TrafficLimit
}
if subInfo.Expire == 0 && ui.ExpireAt > 0 {
subInfo.Expire = ui.ExpireAt
}
subInfo.DevicesKnown = ui.DevicesKnown
subInfo.Devices = ui.Devices
subInfo.DeviceLimit = ui.DeviceLimit
}
}
}
if subInfo != nil {
subInfo.UpdatedAt = time.Now().Unix()
} }
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
m.cfg.SubscriptionURL = strings.TrimSpace(rawURL) if usedURL != "" {
for _, it := range items { m.cfg.SubscriptionURL = strings.TrimSpace(usedURL)
}
if rw != nil {
m.cfg.RemnawaveBaseURL = remnawave.NormalizeBase(rw.BaseURL)
m.cfg.RemnawaveShortUUID = strings.TrimSpace(rw.ShortUUID)
if t := strings.TrimSpace(rw.Token); t != "" {
m.cfg.RemnawaveToken = t
}
if t := strings.TrimSpace(rw.CaddyToken); t != "" {
m.cfg.RemnawaveCaddyToken = t
}
}
if subInfo != nil {
m.cfg.SubInfo = subInfo
}
for _, it := range res.Items {
_ = m.cfg.UpsertProfileWithProtocol(it.Name, it.URI, it.Protocol) _ = m.cfg.UpsertProfileWithProtocol(it.Name, it.URI, it.Protocol)
for i := range m.cfg.Profiles { for i := range m.cfg.Profiles {
if m.cfg.Profiles[i].Name == it.Name { if m.cfg.Profiles[i].Name == it.Name {
@@ -441,9 +772,32 @@ func (m *Manager) ImportSubscription(rawURL string) (int, error) {
} }
} }
if err := config.Save(m.cfgPath, *m.cfg); err != nil { if err := config.Save(m.cfgPath, *m.cfg); err != nil {
return 0, err return ImportResult{}, err
} }
return len(items), nil return ImportResult{Imported: len(res.Items), Skipped: res.Skipped, Warnings: res.Warnings}, nil
}
func buildSubInfo(info *subscription.Info) *config.SubscriptionInfo {
if info == nil {
return nil
}
return &config.SubscriptionInfo{
Upload: info.Upload,
Download: info.Download,
Total: info.Total,
Expire: info.Expire,
}
}
// SubscriptionInfo returns the cached subscription usage info (may be nil).
func (m *Manager) SubscriptionInfo() *config.SubscriptionInfo {
m.mu.Lock()
defer m.mu.Unlock()
if m.cfg.SubInfo == nil {
return nil
}
cp := *m.cfg.SubInfo
return &cp
} }
func (m *Manager) SaveConfig() error { func (m *Manager) SaveConfig() error {
+146
View File
@@ -0,0 +1,146 @@
package core
import (
"io"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"vpnclient/internal/config"
)
func newTestManager(t *testing.T) *Manager {
t.Helper()
dir := t.TempDir()
cfgPath := filepath.Join(dir, "configs", "config.json")
cfg := config.Example()
if err := config.Save(cfgPath, cfg); err != nil {
t.Fatal(err)
}
loaded, err := config.Load(cfgPath)
if err != nil {
t.Fatal(err)
}
m, err := NewManager(cfgPath, loaded, io.Discard)
if err != nil {
t.Fatal(err)
}
return m
}
const testSubBody = "vless://7e1c0fbf-1758-4781-97f6-ae8715ed3f60@de.example.com:443?encryption=none&type=tcp&security=tls&sni=de.example.com#DE-1\n" +
"hysteria2://pass@nl.example.com:9000/?sni=nl.example.com#NL-1\n"
// A credential-less https URL pasted into the "add server link" field must be
// treated as a subscription URL and imported — never fail with
// "proxy URI missing username" (regression for the naive proxy-URI parser).
func TestSaveProfileRoutesSubscriptionURL(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = io.WriteString(w, testSubBody)
}))
defer ts.Close()
m := newTestManager(t)
if err := m.SaveProfile("whatever", ts.URL+"/ZKszcVcC3xbWb8qj"); err != nil {
t.Fatalf("SaveProfile(subscription URL) failed: %v", err)
}
names := map[string]bool{}
for _, p := range m.Profiles() {
names[p.Name] = true
}
if !names["DE-1"] || !names["NL-1"] {
t.Fatalf("subscription profiles not imported, got %v", names)
}
if m.Config().SubscriptionURL == "" {
t.Fatal("subscription URL not persisted")
}
}
func TestSaveActiveProfileRoutesSubscriptionURL(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = io.WriteString(w, testSubBody)
}))
defer ts.Close()
m := newTestManager(t)
if err := m.SaveActiveProfile("naive-main", ts.URL+"/short"); err != nil {
t.Fatalf("SaveActiveProfile(subscription URL) failed: %v", err)
}
if len(m.Profiles()) < 3 { // naive-main + 2 imported
t.Fatalf("expected imported profiles, got %d", len(m.Profiles()))
}
}
// A real naive share link (with credentials) must still be saved as a proxy.
func TestSaveProfileKeepsNaiveLink(t *testing.T) {
m := newTestManager(t)
if err := m.SaveProfile("naive-1", "https://user:pass@proxy.example.com:443"); err != nil {
t.Fatal(err)
}
p, err := m.Config().ActiveProfile()
if err != nil {
t.Fatal(err)
}
if p.Protocol != config.ProtocolNaive || !strings.Contains(p.Proxy, "user:pass@") {
t.Fatalf("naive profile broken: %+v", p)
}
}
// «Добавить конфигурацию» must accept pasted AWG 2.0 .conf / naive links,
// not only http(s) subscription URLs (regression: "нужен http(s) URL").
func TestImportSubscriptionDirectAWGAndNaive(t *testing.T) {
m := newTestManager(t)
awgBody := `[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
Address = 10.8.0.2/32
Jc = 4
Jmin = 40
Jmax = 70
H1 = 1
H2 = 2
H3 = 3
H4 = 4
[Peer]
PublicKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Endpoint = 203.0.113.10:51820
AllowedIPs = 0.0.0.0/0
`
r, err := m.ImportSubscription(awgBody)
if err != nil {
t.Fatalf("AWG paste: %v", err)
}
if r.Imported < 1 {
t.Fatalf("AWG imported=%d", r.Imported)
}
foundAWG := false
for _, p := range m.Profiles() {
if config.Protocol(p.Protocol) == config.ProtocolAWG && strings.Contains(p.Proxy, "203.0.113.10:51820") {
foundAWG = true
break
}
}
if !foundAWG {
t.Fatal("AWG profile missing")
}
r2, err := m.ImportSubscription("https://user:secret@naive.example.com:443#NV")
if err != nil {
t.Fatalf("naive paste: %v", err)
}
if r2.Imported < 1 {
t.Fatalf("naive imported=%d", r2.Imported)
}
foundNaive := false
for _, p := range m.Profiles() {
if config.Protocol(p.Protocol) == config.ProtocolNaive && strings.Contains(p.Proxy, "naive.example.com") {
foundNaive = true
break
}
}
if !foundNaive {
t.Fatal("naive profile missing")
}
}
+182
View File
@@ -0,0 +1,182 @@
// Package geoflag maps server/profile names to ISO 3166-1 alpha-2 and emoji flags.
package geoflag
import (
"regexp"
"strings"
"unicode"
)
var (
reBracketCC = regexp.MustCompile(`(?i)[\[(]([a-z]{2})[\])]`)
rePrefixCC = regexp.MustCompile(`(?i)^([a-z]{2})(?:\s*[-_|/]\s*|\s+)`)
reTokenCC = regexp.MustCompile(`(?i)(?:^|[^a-z])([a-z]{2})(?:\s*[-_|]|$)`)
reEmojiFlag = regexp.MustCompile(`[\x{1F1E6}-\x{1F1FF}]{2}`)
)
// FromText returns ISO2 (upper) and emoji flag, or empty if unknown.
func FromText(name string) (iso2, emoji string) {
name = strings.TrimSpace(name)
if name == "" {
return "", ""
}
if m := reEmojiFlag.FindString(name); m != "" {
if cc := emojiToISO(m); cc != "" {
return cc, m
}
}
lower := strings.ToLower(name)
for _, pair := range countryNames {
if strings.Contains(lower, pair.name) {
return pair.code, ISOToEmoji(pair.code)
}
}
if m := reBracketCC.FindStringSubmatch(name); len(m) == 2 {
cc := strings.ToUpper(m[1])
if Known(cc) {
return cc, ISOToEmoji(cc)
}
}
if m := rePrefixCC.FindStringSubmatch(name); len(m) == 2 {
cc := strings.ToUpper(m[1])
if Known(cc) {
return cc, ISOToEmoji(cc)
}
}
if m := reTokenCC.FindStringSubmatch(name); len(m) == 2 {
cc := strings.ToUpper(m[1])
if Known(cc) {
return cc, ISOToEmoji(cc)
}
}
return "", ""
}
// Known reports whether cc is a mapped ISO2 code.
func Known(cc string) bool {
cc = strings.ToUpper(strings.TrimSpace(cc))
_, ok := isoSet[cc]
return ok
}
// ISOToEmoji converts "DE" → "🇩🇪". Invalid → "".
func ISOToEmoji(cc string) string {
cc = strings.ToUpper(strings.TrimSpace(cc))
if len(cc) != 2 {
return ""
}
a, b := rune(cc[0]), rune(cc[1])
if a < 'A' || a > 'Z' || b < 'A' || b > 'Z' {
return ""
}
return string(rune(0x1F1E6+a-'A')) + string(rune(0x1F1E6+b-'A'))
}
func emojiToISO(flag string) string {
rs := []rune(flag)
if len(rs) != 2 {
return ""
}
a, b := rs[0]-0x1F1E6, rs[1]-0x1F1E6
if a < 0 || a > 25 || b < 0 || b > 25 {
return ""
}
cc := string([]byte{byte('A' + a), byte('A' + b)})
if !Known(cc) {
// Still return regional indicator pair as ISO guess for display.
if unicode.IsLetter(rune('A' + a)) {
return cc
}
return ""
}
return cc
}
type nameCode struct {
name string
code string
}
// Longer / more specific names first where needed.
var countryNames = []nameCode{
{"netherlands", "NL"}, {"deutschland", "DE"}, {"germany", "DE"},
{"united kingdom", "GB"}, {"great britain", "GB"}, {"england", "GB"},
{"united states", "US"}, {"america", "US"}, {"finland", "FI"},
{"sweden", "SE"}, {"norway", "NO"}, {"denmark", "DK"}, {"poland", "PL"},
{"france", "FR"}, {"spain", "ES"}, {"italy", "IT"}, {"portugal", "PT"},
{"switzerland", "CH"}, {"austria", "AT"}, {"belgium", "BE"},
{"czech", "CZ"}, {"slovakia", "SK"}, {"hungary", "HU"}, {"romania", "RO"},
{"bulgaria", "BG"}, {"ukraine", "UA"}, {"russia", "RU"}, {"turkey", "TR"},
{"israel", "IL"}, {"uae", "AE"}, {"emirates", "AE"}, {"dubai", "AE"},
{"singapore", "SG"}, {"hong kong", "HK"}, {"japan", "JP"}, {"korea", "KR"},
{"taiwan", "TW"}, {"india", "IN"}, {"canada", "CA"}, {"mexico", "MX"},
{"brazil", "BR"}, {"argentina", "AR"}, {"australia", "AU"},
{"new zealand", "NZ"}, {"latvia", "LV"}, {"lithuania", "LT"},
{"estonia", "EE"}, {"moldova", "MD"}, {"georgia", "GE"}, {"armenia", "AM"},
{"kazakhstan", "KZ"}, {"uzbekistan", "UZ"}, {"serbia", "RS"},
{"croatia", "HR"}, {"slovenia", "SI"}, {"greece", "GR"}, {"cyprus", "CY"},
{"ireland", "IE"}, {"iceland", "IS"}, {"luxembourg", "LU"},
{"malaysia", "MY"}, {"thailand", "TH"}, {"vietnam", "VN"},
{"indonesia", "ID"}, {"philippines", "PH"}, {"south africa", "ZA"},
// Russian country names (lowercase; longer/more specific first).
{"великобритания", "GB"}, {"нидерланды", "NL"}, {"голландия", "NL"},
{"германия", "DE"}, {"австралия", "AU"}, {"австрия", "AT"},
{"швейцария", "CH"}, {"финляндия", "FI"}, {"швеция", "SE"},
{"норвегия", "NO"}, {"дания", "DK"}, {"польша", "PL"},
{"франция", "FR"}, {"испания", "ES"}, {"италия", "IT"},
{"португалия", "PT"}, {"бельгия", "BE"}, {"чехия", "CZ"},
{"словакия", "SK"}, {"словения", "SI"}, {"венгрия", "HU"},
{"румыния", "RO"}, {"болгария", "BG"}, {"украина", "UA"},
{"россия", "RU"}, {"турция", "TR"}, {"израиль", "IL"},
{"эмираты", "AE"}, {"дубай", "AE"}, {"сингапур", "SG"},
{"гонконг", "HK"}, {"япония", "JP"}, {"корея", "KR"},
{"тайвань", "TW"}, {"индия", "IN"}, {"канада", "CA"},
{"бразилия", "BR"}, {"аргентина", "AR"}, {"латвия", "LV"},
{"литва", "LT"}, {"эстония", "EE"}, {"молдова", "MD"},
{"грузия", "GE"}, {"армения", "AM"}, {"казахстан", "KZ"},
{"узбекистан", "UZ"}, {"сербия", "RS"}, {"хорватия", "HR"},
{"греция", "GR"}, {"кипр", "CY"}, {"ирландия", "IE"},
{"исландия", "IS"}, {"люксембург", "LU"}, {"англия", "GB"},
{"сша", "US"}, {"америка", "US"},
// Частые опечатки в именах серверов на панелях (наблюдались в проде).
// Longer stems first (Contains): «румьиния» before «румьиня».
{"румьиния", "RO"}, {"румяння", "RO"}, {"румьиня", "RO"},
{"румыня", "RO"}, {"руминия", "RO"}, {"румыниа", "RO"},
// City → country hints (EN + RU).
{"frankfurt", "DE"}, {"amsterdam", "NL"}, {"london", "GB"},
{"paris", "FR"}, {"helsinki", "FI"}, {"stockholm", "SE"},
{"oslo", "NO"}, {"copenhagen", "DK"}, {"warsaw", "PL"},
{"prague", "CZ"}, {"vienna", "AT"}, {"zurich", "CH"},
{"madrid", "ES"}, {"milan", "IT"}, {"lisbon", "PT"},
{"moscow", "RU"}, {"istanbul", "TR"}, {"tokyo", "JP"},
{"seoul", "KR"}, {"new york", "US"}, {"los angeles", "US"},
{"dallas", "US"}, {"miami", "US"}, {"chicago", "US"},
{"toronto", "CA"}, {"riga", "LV"}, {"vilnius", "LT"},
{"tallinn", "EE"}, {"bucharest", "RO"}, {"sofia", "BG"},
{"kyiv", "UA"}, {"kiev", "UA"}, {"tbilisi", "GE"},
{"almaty", "KZ"}, {"франкфурт", "DE"}, {"амстердам", "NL"},
{"лондон", "GB"}, {"париж", "FR"}, {"хельсинки", "FI"},
{"стокгольм", "SE"}, {"осло", "NO"}, {"копенгаген", "DK"},
{"варшава", "PL"}, {"прага", "CZ"}, {"вена", "AT"},
{"цюрих", "CH"}, {"мадрид", "ES"}, {"милан", "IT"},
{"москва", "RU"}, {"стамбул", "TR"}, {"токио", "JP"},
{"сеул", "KR"}, {"торонто", "CA"}, {"рига", "LV"},
{"вильнюс", "LT"}, {"таллин", "EE"}, {"бухарест", "RO"},
{"софия", "BG"}, {"киев", "UA"}, {"тбилиси", "GE"},
}
var isoSet = map[string]struct{}{
"AD": {}, "AE": {}, "AF": {}, "AL": {}, "AM": {}, "AR": {}, "AT": {}, "AU": {},
"AZ": {}, "BA": {}, "BD": {}, "BE": {}, "BG": {}, "BH": {}, "BR": {}, "BY": {},
"CA": {}, "CH": {}, "CL": {}, "CN": {}, "CO": {}, "CY": {}, "CZ": {}, "DE": {},
"DK": {}, "EE": {}, "EG": {}, "ES": {}, "FI": {}, "FR": {}, "GB": {}, "GE": {},
"GR": {}, "HK": {}, "HR": {}, "HU": {}, "ID": {}, "IE": {}, "IL": {}, "IN": {},
"IQ": {}, "IR": {}, "IS": {}, "IT": {}, "JP": {}, "KG": {}, "KR": {}, "KZ": {},
"LT": {}, "LU": {}, "LV": {}, "MD": {}, "MK": {}, "MX": {}, "MY": {}, "NL": {},
"NO": {}, "NZ": {}, "PH": {}, "PK": {}, "PL": {}, "PT": {}, "RO": {}, "RS": {},
"RU": {}, "SA": {}, "SE": {}, "SG": {}, "SI": {}, "SK": {}, "TH": {}, "TJ": {},
"TM": {}, "TR": {}, "TW": {}, "UA": {}, "US": {}, "UZ": {}, "VN": {}, "ZA": {},
}
+47
View File
@@ -0,0 +1,47 @@
package geoflag
import "testing"
func TestFromText(t *testing.T) {
cases := []struct {
in, cc string
}{
{"DE-Frankfurt-1", "DE"},
{"[NL] Amsterdam", "NL"},
{"🇺🇸 US West", "US"},
{"Germany Hetzner", "DE"},
{"unknown-node", ""},
// Russian country names.
{"Германия-1", "DE"},
{"Нидерланды Fast", "NL"},
{"Сервер Швейцария", "CH"},
{"США восток", "US"},
{"Великобритания", "GB"},
// City → country hints.
{"Frankfurt-Hetzner", "DE"},
{"Амстердам 2", "NL"},
{"node-helsinki", "FI"},
// Names from live Remnawave subscription (emoji comes first).
{"🇨🇿 Czech Republic Vless", "CZ"},
{"🇳🇴 Norway TR+WS", "NO"},
{"🇩🇪 Germany 3 HY", "DE"},
// Panel-side typos seen in production.
{"Румьиня", "RO"},
{"Румыня-2", "RO"},
{"Румяння", "RO"},
{"Румяння — оптимальный", "RO"},
{"Румьиния", "RO"},
{"Румьиния Fast", "RO"},
{"румыниа", "RO"},
{"Румыния", "RO"},
}
for _, c := range cases {
cc, emoji := FromText(c.in)
if cc != c.cc {
t.Fatalf("%q: got cc %q want %q", c.in, cc, c.cc)
}
if c.cc != "" && emoji == "" {
t.Fatalf("%q: empty emoji", c.in)
}
}
}
+24
View File
@@ -2,6 +2,7 @@ package linknorm
import ( import (
"fmt" "fmt"
"net/url"
"strings" "strings"
"vpnclient/internal/config" "vpnclient/internal/config"
@@ -11,6 +12,23 @@ import (
"vpnclient/internal/protocols/xray" "vpnclient/internal/protocols/xray"
) )
// LooksLikeSubscriptionURL reports whether raw is a plain http(s) URL without
// userinfo — i.e. a subscription URL rather than a naive proxy share link
// (which always carries user:pass@). Callers should route such input into the
// subscription import flow instead of the proxy-URI parser.
func LooksLikeSubscriptionURL(raw string) bool {
raw = strings.TrimSpace(raw)
lower := strings.ToLower(raw)
if !strings.HasPrefix(lower, "http://") && !strings.HasPrefix(lower, "https://") {
return false
}
u, err := url.Parse(raw)
if err != nil || u.Host == "" {
return false
}
return u.User == nil
}
// Normalize normalizes a share/proxy URI for the given protocol (or auto-detect). // Normalize normalizes a share/proxy URI for the given protocol (or auto-detect).
func Normalize(proto config.Protocol, raw string) (normalized string, detected config.Protocol, remark string, err error) { func Normalize(proto config.Protocol, raw string) (normalized string, detected config.Protocol, remark string, err error) {
raw = strings.TrimSpace(raw) raw = strings.TrimSpace(raw)
@@ -42,6 +60,12 @@ func Normalize(proto config.Protocol, raw string) (normalized string, detected c
u, rem, err := awg.NormalizeShareLink(raw) u, rem, err := awg.NormalizeShareLink(raw)
return u, config.ProtocolAWG, rem, err return u, config.ProtocolAWG, rem, err
} }
// Safety net: a bare http(s) URL without user:pass@ is a subscription
// URL, not a naive proxy link — callers should have routed it to the
// subscription import flow (see LooksLikeSubscriptionURL).
if LooksLikeSubscriptionURL(raw) {
return "", config.ProtocolNaive, "", fmt.Errorf("это URL-ссылка конфигурации (нет логина и пароля) — вставьте её в поле «Добавить конфигурацию» или сохраните ещё раз: импорт запустится автоматически")
}
if xray.Detect(raw) { if xray.Detect(raw) {
u, rem, err := xray.NormalizeShareLink(raw) u, rem, err := xray.NormalizeShareLink(raw)
return u, xray.DetectProtocol(raw), rem, err return u, xray.DetectProtocol(raw), rem, err
+44
View File
@@ -0,0 +1,44 @@
package linknorm
import (
"strings"
"testing"
)
func TestLooksLikeSubscriptionURL(t *testing.T) {
cases := []struct {
in string
want bool
}{
{"https://api.evilfox.win/ZKszcVcC3xbWb8qj", true},
{"http://panel.example.com/api/sub/abc", true},
{" https://api.evilfox.win/ZKszcVcC3xbWb8qj ", true},
{"https://user:pass@host:443", false}, // naive proxy link
{"https://user@host:443", false}, // has userinfo
{"quic://user:pass@host", false},
{"vless://uuid@host:443", false},
{"naive+https://user:pass@host:443", false},
{"not a url", false},
{"", false},
}
for _, c := range cases {
if got := LooksLikeSubscriptionURL(c.in); got != c.want {
t.Fatalf("%q: got %v want %v", c.in, got, c.want)
}
}
}
// Direct Normalize of a credential-less https URL must never surface the raw
// "proxy URI missing username" error — it explains this is a subscription URL.
func TestNormalizeSubscriptionURLError(t *testing.T) {
_, _, _, err := Normalize("", "https://api.evilfox.win/ZKszcVcC3xbWb8qj")
if err == nil {
t.Fatal("expected error")
}
if strings.Contains(err.Error(), "missing username") {
t.Fatalf("raw parser error leaked: %v", err)
}
if !strings.Contains(err.Error(), "конфигурац") {
t.Fatalf("error should mention configuration URL: %v", err)
}
}
+30 -36
View File
@@ -98,45 +98,23 @@ func BestOK(results []Result) (Result, bool) {
return best, found return best, found
} }
// PingProfile pings using protocol hints when available. // HostPort extracts the remote server endpoint from a share/proxy URI using
// Naive uses TCP; Hysteria 2 uses UDP (QUIC) — TCP would always look "refused". // protocol hints. Used for pings and for VPN-mode route exclusions.
func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyURI string) Result { func HostPort(proto config.Protocol, proxyURI string) (string, string, error) {
res := Result{Name: name}
if strings.TrimSpace(proxyURI) == "" {
res.Error = "нет ссылки сервера"
return res
}
hy2 := proto == config.ProtocolHysteria2 || hysteria2.Detect(proxyURI) hy2 := proto == config.ProtocolHysteria2 || hysteria2.Detect(proxyURI)
isAWG := proto == config.ProtocolAWG || awg.Detect(proxyURI) isAWG := proto == config.ProtocolAWG || awg.Detect(proxyURI)
isXray := proto == config.ProtocolVLESS || proto == config.ProtocolVMess || proto == config.ProtocolTrojan || xray.Detect(proxyURI) isXray := proto == config.ProtocolVLESS || proto == config.ProtocolVMess || proto == config.ProtocolTrojan || xray.Detect(proxyURI)
var host, port string switch {
if isAWG { case isAWG:
h, p, err := awg.HostPort(proxyURI) return awg.HostPort(proxyURI)
if err != nil { case isXray:
res.Error = err.Error() return xray.HostPort(proxyURI)
return res case hy2:
return hysteria2.HostPort(proxyURI)
} }
host, port = h, p
} else if isXray {
h, p, err := xray.HostPort(proxyURI)
if err != nil {
res.Error = err.Error()
return res
}
host, port = h, p
} else if hy2 {
h, p, err := hysteria2.HostPort(proxyURI)
if err != nil {
res.Error = err.Error()
return res
}
host, port = h, p
} else {
normalized, _, _, err := linknorm.Normalize(proto, proxyURI) normalized, _, _, err := linknorm.Normalize(proto, proxyURI)
if err != nil { if err != nil {
res.Error = err.Error() return "", "", err
return res
} }
rest := normalized rest := normalized
if i := strings.Index(rest, "://"); i >= 0 { if i := strings.Index(rest, "://"); i >= 0 {
@@ -148,12 +126,29 @@ func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyU
if i := strings.IndexAny(rest, "/?#"); i >= 0 { if i := strings.IndexAny(rest, "/?#"); i >= 0 {
rest = rest[:i] rest = rest[:i]
} }
host = rest host, port := rest, "443"
port = "443"
if i := strings.LastIndex(rest, ":"); i >= 0 { if i := strings.LastIndex(rest, ":"); i >= 0 {
host = rest[:i] host = rest[:i]
port = rest[i+1:] port = rest[i+1:]
} }
return host, port, nil
}
// PingProfile pings using protocol hints when available.
// Naive uses TCP; Hysteria 2 uses UDP (QUIC) — TCP would always look "refused".
func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyURI string) Result {
res := Result{Name: name}
if strings.TrimSpace(proxyURI) == "" {
res.Error = "нет ссылки сервера"
return res
}
hy2 := proto == config.ProtocolHysteria2 || hysteria2.Detect(proxyURI)
isAWG := proto == config.ProtocolAWG || awg.Detect(proxyURI)
host, port, err := HostPort(proto, proxyURI)
if err != nil {
res.Error = err.Error()
return res
} }
res.Host = host res.Host = host
@@ -164,7 +159,6 @@ func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyU
} }
start := time.Now() start := time.Now()
var err error
if hy2 || isAWG { if hy2 || isAWG {
err = probeUDP(ctx, host, port) err = probeUDP(ctx, host, port)
} else { } else {
+336
View File
@@ -0,0 +1,336 @@
// Package remnawave fetches VPN share links from a Remnawave panel.
//
// Real panel endpoints (API ≈2.x, see remnawave/backend + rw-sdk):
//
// GET {base}/api/sub/{shortUuid} — public subscription body (base64/YAML/text)
// GET {base}/api/sub/{shortUuid}/{clientType} — typed client subscription
// GET {base}/api/subscriptions/by-short-uuid/{shortUuid} — JSON subscription meta (Bearer API token)
// GET {base}/api/subscriptions/by-short-uuid/{shortUuid}/raw — rich raw payload (Bearer)
// GET {base}/api/users/by-short-uuid/{shortUuid} — user incl. subscriptionUrl (Bearer)
//
// Auth: Authorization: Bearer <API token> from panel → API Tokens.
// Optional: X-Api-Key for Caddy auth frontends.
// Responses are often wrapped as {"response": ...}.
package remnawave
import (
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"time"
"vpnclient/internal/subscription"
)
// Settings are persisted locally (never committed as secrets).
type Settings struct {
BaseURL string `json:"base_url"`
Token string `json:"token,omitempty"`
ShortUUID string `json:"short_uuid"`
CaddyToken string `json:"caddy_token,omitempty"`
}
// NormalizeBase strips trailing slash and optional trailing /api.
func NormalizeBase(raw string) string {
raw = strings.TrimSpace(raw)
raw = strings.TrimRight(raw, "/")
if strings.HasSuffix(strings.ToLower(raw), "/api") {
raw = strings.TrimRight(raw[:len(raw)-4], "/")
}
return raw
}
// PublicSubURL is the standard Remnawave client subscription URL.
func PublicSubURL(base, shortUUID string) string {
base = NormalizeBase(base)
shortUUID = strings.TrimSpace(shortUUID)
if base == "" || shortUUID == "" {
return ""
}
return base + "/api/sub/" + url.PathEscape(shortUUID)
}
// ParseInput extracts panel base + short UUID from a Remnawave subscription URL.
// Accepts:
// - https://panel.example.com/api/sub/{short}
// - https://panel.example.com/api/sub/{short}/clash
// - https://sub.example.com/{short} (custom sub page — returns short only if path is one segment)
func ParseInput(raw string) (base, shortUUID string, ok bool) {
raw = strings.TrimSpace(raw)
u, err := url.Parse(raw)
if err != nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") {
return "", "", false
}
path := strings.Trim(u.Path, "/")
parts := strings.Split(path, "/")
// …/api/sub/{short}[/{clientType}]
for i := 0; i+1 < len(parts); i++ {
if strings.EqualFold(parts[i], "api") && strings.EqualFold(parts[i+1], "sub") && i+2 < len(parts) {
short := parts[i+2]
if short == "" || strings.EqualFold(short, "outline") {
return "", "", false
}
base = u.Scheme + "://" + u.Host
if i > 0 {
base += "/" + strings.Join(parts[:i], "/")
}
return NormalizeBase(base), short, true
}
}
return "", "", false
}
// Fetch downloads configs via Remnawave public sub URL and, if a token is set,
// authenticated subscription/user endpoints. Parsed items reuse subscription.ParseBody.
func Fetch(ctx context.Context, s Settings) (*subscription.Result, string, error) {
s.BaseURL = NormalizeBase(s.BaseURL)
s.ShortUUID = strings.TrimSpace(s.ShortUUID)
s.Token = strings.TrimSpace(s.Token)
s.CaddyToken = strings.TrimSpace(s.CaddyToken)
if s.BaseURL == "" || s.ShortUUID == "" {
return nil, "", fmt.Errorf("укажите URL панели Remnawave и short UUID")
}
if _, err := url.Parse(s.BaseURL); err != nil {
return nil, "", fmt.Errorf("некорректный URL панели")
}
client := &http.Client{Timeout: 45 * time.Second}
pub := PublicSubURL(s.BaseURL, s.ShortUUID)
var lastErr error
// 1) Public subscription (usual end-user path — no token).
if res, err := fetchAndParse(ctx, client, s, pub, false); err == nil && res != nil && len(res.Items) > 0 {
return res, pub, nil
} else if err != nil {
lastErr = err
}
// 2) With API token: resolve subscriptionUrl / raw payload.
if s.Token != "" {
if res, used, err := fetchWithToken(ctx, client, s); err == nil && res != nil && len(res.Items) > 0 {
if used == "" {
used = pub
}
return res, used, nil
} else if err != nil {
lastErr = err
}
}
if lastErr != nil {
return nil, "", lastErr
}
return nil, "", fmt.Errorf("Remnawave: нет поддерживаемых ссылок (проверьте short UUID / токен / правила ответа конфигурации)")
}
func fetchWithToken(ctx context.Context, client *http.Client, s Settings) (*subscription.Result, string, error) {
base := s.BaseURL
short := s.ShortUUID
// Prefer admin raw endpoint (links in JSON); fall back to meta → subscriptionUrl.
candidates := []string{
base + "/api/subscriptions/by-short-uuid/" + url.PathEscape(short) + "/raw",
base + "/api/sub/" + url.PathEscape(short) + "/raw",
base + "/api/subscriptions/by-short-uuid/" + url.PathEscape(short),
base + "/api/users/by-short-uuid/" + url.PathEscape(short),
}
var lastErr error
for _, endpoint := range candidates {
body, _, err := doGET(ctx, client, s, endpoint, true)
if err != nil {
lastErr = err
continue
}
if res, err := parseRemnawaveBody(body); err == nil && res != nil && len(res.Items) > 0 {
return res, endpoint, nil
}
if subURL := extractSubscriptionURL(body); subURL != "" {
res, err := subscription.Fetch(ctx, subURL)
if err == nil && res != nil && len(res.Items) > 0 {
return res, subURL, nil
}
if err != nil {
lastErr = err
}
}
}
if lastErr != nil {
return nil, "", lastErr
}
return nil, "", fmt.Errorf("Remnawave API: не удалось получить конфиги")
}
func fetchAndParse(ctx context.Context, client *http.Client, s Settings, endpoint string, auth bool) (*subscription.Result, error) {
body, header, err := doGET(ctx, client, s, endpoint, auth)
if err != nil {
return nil, err
}
res, err := parseRemnawaveBody(body)
if res != nil && res.Info == nil {
res.Info = subscription.ParseUserInfoHeader(header.Get("Subscription-Userinfo"))
}
return res, err
}
func doGET(ctx context.Context, client *http.Client, s Settings, endpoint string, auth bool) (string, http.Header, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return "", nil, err
}
req.Header.Set("User-Agent", "EvilFox/4.0 (Remnawave)")
req.Header.Set("Accept", "*/*")
if auth && s.Token != "" {
req.Header.Set("Authorization", "Bearer "+s.Token)
}
if s.CaddyToken != "" {
req.Header.Set("X-Api-Key", s.CaddyToken)
}
// Remnawave proxy-check middleware may reset plain HTTP / direct panel access
// without forwarded headers (see remnawave proxy-check.middleware).
if u, err := url.Parse(endpoint); err == nil && shouldSendProxyHeaders(u) {
req.Header.Set("X-Forwarded-Proto", u.Scheme)
req.Header.Set("X-Forwarded-For", "127.0.0.1")
}
resp, err := client.Do(req)
if err != nil {
return "", nil, fmt.Errorf("Remnawave запрос: %w", err)
}
defer resp.Body.Close()
raw, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
if err != nil {
return "", nil, err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
msg := strings.TrimSpace(string(raw))
if len(msg) > 180 {
msg = msg[:180] + "…"
}
return "", nil, fmt.Errorf("Remnawave HTTP %d: %s", resp.StatusCode, msg)
}
return string(raw), resp.Header, nil
}
func shouldSendProxyHeaders(u *url.URL) bool {
if u == nil {
return false
}
if strings.EqualFold(u.Scheme, "http") {
return true
}
host := u.Hostname()
if host == "localhost" || host == "127.0.0.1" || host == "::1" {
return true
}
ip := net.ParseIP(host)
return ip != nil && (ip.IsLoopback() || ip.IsPrivate())
}
func parseRemnawaveBody(body string) (*subscription.Result, error) {
body = strings.TrimSpace(body)
if body == "" {
return nil, fmt.Errorf("пустой ответ Remnawave")
}
// Try JSON envelope / raw DTO first, then classic subscription parse.
if strings.HasPrefix(body, "{") || strings.HasPrefix(body, "[") {
if res, err := subscription.ParseBodyDetailed(flattenJSONLinks(body)); err == nil && len(res.Items) > 0 {
return res, nil
}
// Also try parsing the whole JSON as if links were string values only.
if flat := extractLinkStrings(body); flat != "" {
if res, err := subscription.ParseBodyDetailed(flat); err == nil && len(res.Items) > 0 {
return res, nil
}
}
}
return subscription.ParseBodyDetailed(body)
}
func flattenJSONLinks(body string) string {
links := extractLinkStrings(body)
if links != "" {
return links
}
return body
}
func extractLinkStrings(body string) string {
var root any
if err := json.Unmarshal([]byte(body), &root); err != nil {
return ""
}
var lines []string
walkJSON(root, &lines)
return strings.Join(lines, "\n")
}
func walkJSON(v any, out *[]string) {
switch t := v.(type) {
case string:
s := strings.TrimSpace(t)
lower := strings.ToLower(s)
if strings.Contains(lower, "://") || strings.HasPrefix(lower, "ss://") {
*out = append(*out, s)
}
case []any:
for _, el := range t {
walkJSON(el, out)
}
case map[string]any:
for _, el := range t {
walkJSON(el, out)
}
}
}
func extractSubscriptionURL(body string) string {
var root any
if err := json.Unmarshal([]byte(body), &root); err != nil {
return ""
}
// Unwrap {"response": ...}
if m, ok := root.(map[string]any); ok {
if resp, ok := m["response"]; ok {
root = resp
}
}
return findURLField(root, []string{
"subscriptionUrl", "subscription_url", "subscriptionURL", "url",
})
}
func findURLField(v any, keys []string) string {
switch t := v.(type) {
case map[string]any:
for _, k := range keys {
for mk, mv := range t {
if strings.EqualFold(mk, k) {
if s, ok := mv.(string); ok {
s = strings.TrimSpace(s)
if strings.HasPrefix(strings.ToLower(s), "http") {
return s
}
}
}
}
}
for _, mv := range t {
if u := findURLField(mv, keys); u != "" {
return u
}
}
case []any:
for _, el := range t {
if u := findURLField(el, keys); u != "" {
return u
}
}
}
return ""
}
+30
View File
@@ -0,0 +1,30 @@
package remnawave
import "testing"
func TestPublicSubURL(t *testing.T) {
got := PublicSubURL("https://panel.example.com/api/", "abc12")
want := "https://panel.example.com/api/sub/abc12"
if got != want {
t.Fatalf("got %q want %q", got, want)
}
}
func TestParseInput(t *testing.T) {
base, short, ok := ParseInput("https://vpn.example.com/api/sub/xyz789/clash")
if !ok || base != "https://vpn.example.com" || short != "xyz789" {
t.Fatalf("got base=%q short=%q ok=%v", base, short, ok)
}
_, _, ok = ParseInput("https://example.com/not-a-sub")
if ok {
t.Fatal("expected not ok")
}
}
func TestExtractSubscriptionURL(t *testing.T) {
body := `{"response":{"subscriptionUrl":"https://vpn.example.com/api/sub/abc"}}`
got := extractSubscriptionURL(body)
if got != "https://vpn.example.com/api/sub/abc" {
t.Fatalf("got %q", got)
}
}
+122
View File
@@ -0,0 +1,122 @@
package remnawave
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
// UserInfo is subscription usage metadata from the Remnawave admin API
// (GET /api/users/by-short-uuid/{short} + GET /api/hwid/devices/{uuid}).
type UserInfo struct {
UsedTraffic int64 // bytes
TrafficLimit int64 // bytes; 0 = unlimited
ExpireAt int64 // unix seconds; 0 = unknown
DeviceLimit int // 0 = no limit / unknown
Devices int
DevicesKnown bool
}
// FetchUserInfo loads traffic/expiry and HWID device info via the admin API.
// Requires BaseURL, ShortUUID and Token; degrade gracefully — the caller
// should treat any error as "info unavailable".
func FetchUserInfo(ctx context.Context, s Settings) (*UserInfo, error) {
s.BaseURL = NormalizeBase(s.BaseURL)
s.ShortUUID = strings.TrimSpace(s.ShortUUID)
s.Token = strings.TrimSpace(s.Token)
if s.BaseURL == "" || s.ShortUUID == "" {
return nil, fmt.Errorf("нет URL панели / short UUID")
}
if s.Token == "" {
return nil, fmt.Errorf("нет API токена Remnawave")
}
client := &http.Client{Timeout: 20 * time.Second}
endpoint := s.BaseURL + "/api/users/by-short-uuid/" + url.PathEscape(s.ShortUUID)
body, _, err := doGET(ctx, client, s, endpoint, true)
if err != nil {
return nil, err
}
var payload struct {
Response userDTO `json:"response"`
}
if err := json.Unmarshal([]byte(body), &payload); err != nil {
// Some deployments return the DTO without the {"response": ...} envelope.
var direct userDTO
if err2 := json.Unmarshal([]byte(body), &direct); err2 != nil {
return nil, fmt.Errorf("Remnawave user info: %w", err)
}
payload.Response = direct
}
u := payload.Response
if u.UUID == "" && u.UsedTrafficBytes == 0 && u.ExpireAt == "" {
return nil, fmt.Errorf("Remnawave: пустой ответ user info")
}
info := &UserInfo{
UsedTraffic: u.UsedTrafficBytes,
TrafficLimit: u.TrafficLimitBytes,
DeviceLimit: u.HwidDeviceLimit,
}
if u.ExpireAt != "" {
if t, err := time.Parse(time.RFC3339, u.ExpireAt); err == nil {
info.ExpireAt = t.Unix()
}
}
if u.UUID != "" {
if total, ok := fetchDeviceCount(ctx, client, s, u.UUID); ok {
info.Devices = total
info.DevicesKnown = true
}
}
return info, nil
}
type userDTO struct {
UUID string `json:"uuid"`
UsedTrafficBytes int64 `json:"usedTrafficBytes"`
TrafficLimitBytes int64 `json:"trafficLimitBytes"`
ExpireAt string `json:"expireAt"`
HwidDeviceLimit int `json:"hwidDeviceLimit"`
}
// fetchDeviceCount returns the number of HWID devices attached to a user.
// Handles both response shapes: {"response":{"total":N,"devices":[...]}} and
// the legacy {"response":[...]}.
func fetchDeviceCount(ctx context.Context, client *http.Client, s Settings, userUUID string) (int, bool) {
endpoint := s.BaseURL + "/api/hwid/devices/" + url.PathEscape(userUUID)
body, _, err := doGET(ctx, client, s, endpoint, true)
if err != nil {
return 0, false
}
var wrapped struct {
Response json.RawMessage `json:"response"`
}
raw := json.RawMessage(body)
if err := json.Unmarshal([]byte(body), &wrapped); err == nil && len(wrapped.Response) > 0 {
raw = wrapped.Response
}
var obj struct {
Total *int `json:"total"`
Devices []json.RawMessage `json:"devices"`
}
if err := json.Unmarshal(raw, &obj); err == nil {
if obj.Total != nil {
return *obj.Total, true
}
if obj.Devices != nil {
return len(obj.Devices), true
}
}
var arr []json.RawMessage
if err := json.Unmarshal(raw, &arr); err == nil {
return len(arr), true
}
return 0, false
}
+423 -63
View File
@@ -5,15 +5,18 @@ import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io" "io"
"net"
"net/http" "net/http"
"net/url" "net/url"
"regexp" "regexp"
"strconv"
"strings" "strings"
"time" "time"
"unicode" "unicode"
"vpnclient/internal/config" "vpnclient/internal/config"
"vpnclient/internal/linknorm" "vpnclient/internal/linknorm"
"vpnclient/internal/protocols/awg"
"vpnclient/internal/protocols/hysteria2" "vpnclient/internal/protocols/hysteria2"
"vpnclient/internal/protocols/naive" "vpnclient/internal/protocols/naive"
) )
@@ -25,53 +28,164 @@ type Item struct {
URI string URI string
} }
// Info is subscription usage metadata from the subscription-userinfo header
// (upload/download/total in bytes, expire as unix seconds; 0 = unknown).
type Info struct {
Upload int64 `json:"upload,omitempty"`
Download int64 `json:"download,omitempty"`
Total int64 `json:"total,omitempty"`
Expire int64 `json:"expire,omitempty"`
}
// Result is a parsed subscription: imported items plus per-line skip diagnostics.
type Result struct {
Items []Item
Skipped int
Warnings []string
Info *Info
}
const maxWarnings = 8
var ( var (
reShareLine = regexp.MustCompile(`(?i)((?:hysteria2|hy2|vless|vmess|trojan|awg|amneziawg|wireguard|naive\+https|naive\+quic|naive|quic|https|http)://[^\s"'<>]+)`) reShareLine = regexp.MustCompile(`(?i)((?:hysteria2|hy2|vless|vmess|trojan|awg|amneziawg|wireguard|vpn|naive\+https|naive\+quic|naive|quic|https|http)://[^\s"'<>]+)`)
reClashHY2 = regexp.MustCompile(`(?is)type:\s*hysteria2\b.*?server:\s*(\S+).*?port:\s*(\d+).*?(?:password|auth):\s*["']?([^\s"']+)`) reClashHY2 = regexp.MustCompile(`(?is)type:\s*hysteria2\b.*?server:\s*(\S+).*?port:\s*(\d+).*?(?:password|auth):\s*["']?([^\s"']+)`)
// reURILine matches a standalone "scheme://..." line — only such lines are
// counted as skipped entries (YAML/HTML noise is ignored silently).
reURILine = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9+.-]*://\S`)
) )
// userAgents to try in order. Remnawave and most panels return raw share links
// 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.
var userAgents = []string{
"EvilFox/4.0 (+https://evilfox.win)",
"ClashMeta/1.18.0",
}
// Fetch downloads a subscription body and parses proxy share links. // Fetch downloads a subscription body and parses proxy share links.
func Fetch(ctx context.Context, rawURL string) ([]Item, error) { // Parsing is tolerant: unsupported or broken entries are skipped (with
// warnings), valid ones are imported.
func Fetch(ctx context.Context, rawURL string) (*Result, error) {
rawURL = strings.TrimSpace(rawURL) rawURL = strings.TrimSpace(rawURL)
if rawURL == "" { if rawURL == "" {
return nil, fmt.Errorf("пустой URL подписки") return nil, fmt.Errorf("пустой URL конфигурации")
} }
u, err := url.Parse(rawURL) u, err := url.Parse(rawURL)
if err != nil || (u.Scheme != "http" && u.Scheme != "https") { if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
return nil, fmt.Errorf("нужен http(s) URL подписки") return nil, fmt.Errorf("нужен http(s) URL конфигурации")
} }
var (
best *Result
info *Info
lastErr error
)
for _, ua := range userAgents {
body, header, err := download(ctx, rawURL, ua)
if err != nil {
lastErr = err
continue
}
if info == nil {
info = ParseUserInfoHeader(header.Get("Subscription-Userinfo"))
}
res, err := ParseBodyDetailed(body)
if res != nil && (best == nil || len(res.Items) > 0 || res.Skipped > best.Skipped) {
best = res
}
if err != nil {
lastErr = err
continue
}
if res != nil && len(res.Items) > 0 {
break
}
}
if best == nil || len(best.Items) == 0 {
if lastErr != nil {
return nil, lastErr
}
return nil, fmt.Errorf("в конфигурации нет поддерживаемых ссылок")
}
best.Info = info
return best, nil
}
func download(ctx context.Context, rawURL, userAgent string) (string, http.Header, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
if err != nil { if err != nil {
return nil, err return "", nil, err
} }
req.Header.Set("User-Agent", "ClashMeta/1.18.0") req.Header.Set("User-Agent", userAgent)
req.Header.Set("Accept", "*/*") req.Header.Set("Accept", "*/*")
client := &http.Client{Timeout: 45 * time.Second} client := &http.Client{Timeout: 45 * time.Second}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, fmt.Errorf("не удалось скачать подписку: %w", err) return "", nil, fmt.Errorf("не удалось загрузить конфигурацию: %w", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("подписка HTTP %d", resp.StatusCode) return "", nil, fmt.Errorf("конфигурация недоступна (HTTP %d)", resp.StatusCode)
} }
body, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20)) body, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
if err != nil { if err != nil {
return nil, err return "", nil, err
} }
items, err := ParseBody(string(body)) return string(body), resp.Header, nil
}
// ParseUserInfoHeader parses "upload=..; download=..; total=..; expire=<unix>".
// Returns nil when the header is absent or has no recognized fields.
func ParseUserInfoHeader(v string) *Info {
v = strings.TrimSpace(v)
if v == "" {
return nil
}
info := &Info{}
found := false
for _, part := range strings.Split(v, ";") {
kv := strings.SplitN(part, "=", 2)
if len(kv) != 2 {
continue
}
n, err := strconv.ParseInt(strings.TrimSpace(kv[1]), 10, 64)
if err != nil { if err != nil {
return nil, err continue
} }
return items, nil switch strings.ToLower(strings.TrimSpace(kv[0])) {
case "upload":
info.Upload, found = n, true
case "download":
info.Download, found = n, true
case "total":
info.Total, found = n, true
case "expire":
info.Expire, found = n, true
}
}
if !found {
return nil
}
return info
} }
// ParseBody accepts plain text lines, base64, or Clash-like blobs with share links. // ParseBody accepts plain text lines, base64, or Clash-like blobs with share links.
func ParseBody(body string) ([]Item, error) { func ParseBody(body string) ([]Item, error) {
res, err := ParseBodyDetailed(body)
if err != nil {
return nil, err
}
return res.Items, nil
}
// ParseBodyDetailed parses a subscription body and reports skipped entries.
// The returned Result is non-nil even on error (zero valid entries) so callers
// can surface the collected warnings.
func ParseBodyDetailed(body string) (*Result, error) {
body = strings.TrimSpace(body) body = strings.TrimSpace(body)
if body == "" { if body == "" {
return nil, fmt.Errorf("пустая подписка") return &Result{}, fmt.Errorf("пустая конфигурация")
} }
candidates := []string{body} candidates := []string{body}
@@ -83,62 +197,83 @@ func ParseBody(body string) ([]Item, error) {
} }
} }
var lastErr error var best *Result
for _, text := range candidates { for _, text := range candidates {
items, err := parseText(text) res := parseText(text)
if err == nil && len(items) > 0 { if len(res.Items) > 0 {
return items, nil return res, nil
} }
if err != nil { if best == nil || res.Skipped > best.Skipped {
lastErr = err best = res
} }
} }
if lastErr != nil { if best == nil {
return nil, lastErr best = &Result{}
} }
return nil, fmt.Errorf("в подписке нет поддерживаемых ссылок (naive / hysteria2)") if best.Skipped > 0 {
return best, fmt.Errorf("нет поддерживаемых ссылок: %d записей пропущено (%s)",
best.Skipped, strings.Join(best.Warnings, "; "))
}
return best, fmt.Errorf("в конфигурации нет поддерживаемых ссылок (naive / hysteria2 / awg / vless / vmess / trojan)")
} }
func parseText(text string) ([]Item, error) { func parseText(text string) *Result {
lines := splitLines(text) res := &Result{}
// Also extract share URIs embedded in YAML/HTML/JSON.
extracted := reShareLine.FindAllString(text, -1)
for _, m := range extracted {
lines = append(lines, strings.TrimRight(m, ".,;)]}\"'"))
}
for _, block := range extractClashHY2(text) {
lines = append(lines, block)
}
out := make([]Item, 0, len(lines))
seen := map[string]int{} seen := map[string]int{}
seenURI := map[string]struct{}{} seenURI := map[string]struct{}{}
for _, line := range lines {
line = strings.TrimSpace(line) // countSkips is true only for raw subscription lines; URIs extracted from
if line == "" || strings.HasPrefix(line, "#") { // YAML/HTML/JSON noise are best-effort and never counted as "skipped".
continue process := func(raw string, countSkips bool) {
raw = strings.TrimSpace(raw)
if raw == "" || strings.HasPrefix(raw, "#") {
return
} }
line = strings.Trim(line, `"'`) // Only strip surrounding quotes on single-line share links.
proto := config.DetectProtocol(line) if !strings.Contains(raw, "\n") {
raw = strings.Trim(raw, `"'`)
}
looksURI := reURILine.MatchString(raw)
skip := func(reason string) {
if !countSkips || !looksURI {
return
}
res.Skipped++
if len(res.Warnings) < maxWarnings {
res.Warnings = append(res.Warnings, shortenLink(raw)+" — "+reason)
}
}
proto := config.DetectProtocol(raw)
if proto == "" { if proto == "" {
continue skip("неподдерживаемая схема")
return
} }
// Skip bare https:// without credentials (common noise in HTML/YAML). // Bare https:// without credentials (common noise in HTML/YAML,
if proto == config.ProtocolNaive && !naiveLinkHasAuth(line) && !strings.HasPrefix(strings.ToLower(line), "naive+") { // or a subscription URL pasted among the links) is not a proxy link.
continue if proto == config.ProtocolNaive && !naiveLinkHasAuth(raw) && !strings.HasPrefix(strings.ToLower(raw), "naive+") {
skip("нет логина и пароля в ссылке")
return
} }
normalized, detected, remark, err := linknorm.Normalize(proto, line) normalized, detected, remark, err := linknorm.Normalize(proto, raw)
if err != nil { if err != nil {
continue skip(err.Error())
return
} }
if _, dup := seenURI[normalized]; dup { if _, dup := seenURI[normalized]; dup {
continue return
} }
seenURI[normalized] = struct{}{} seenURI[normalized] = struct{}{}
name := remark name := remark
if name == "" { if name == "" {
name = defaultName(detected, normalized) name = defaultName(detected, normalized)
} }
// Clash wireguard INI may carry "# name: …" — prefer that display name.
if detected == config.ProtocolAWG {
if n := awgCommentName(raw); n != "" {
name = n
}
}
base := name base := name
for n := 2; ; n++ { for n := 2; ; n++ {
if _, ok := seen[name]; !ok { if _, ok := seen[name]; !ok {
@@ -147,12 +282,69 @@ func parseText(text string) ([]Item, error) {
name = fmt.Sprintf("%s-%d", base, n) name = fmt.Sprintf("%s-%d", base, n)
} }
seen[name] = 1 seen[name] = 1
out = append(out, Item{Name: name, Protocol: detected, URI: normalized}) res.Items = append(res.Items, Item{Name: name, Protocol: detected, URI: normalized})
} }
if len(out) == 0 {
return nil, fmt.Errorf("в подписке нет поддерживаемых ссылок (naive / hysteria2)") // Whole-body AWG/WireGuard .conf, Amnezia JSON, vpn:// — hosted as a single
// HTTP(S) config file (line-oriented parsing cannot reconstruct these).
if awg.Detect(text) {
process(text, false)
if len(res.Items) > 0 && !looksLikeShareList(text) {
return res
} }
return out, nil }
for _, line := range splitLines(text) {
process(line, true)
}
// Also extract share URIs embedded in YAML/HTML/JSON.
for _, m := range reShareLine.FindAllString(text, -1) {
process(strings.TrimRight(m, ".,;)]}\"'"), false)
}
for _, block := range extractClashProxies(text) {
process(block, false)
}
return res
}
// looksLikeShareList reports bodies that also contain line-oriented share links
// (mixed subscription + AWG is rare, but do not early-return on those).
func looksLikeShareList(text string) bool {
lower := strings.ToLower(text)
for _, p := range []string{"vless://", "vmess://", "trojan://", "hysteria2://", "hy2://", "naive+", "ss://"} {
if strings.Contains(lower, p) {
return true
}
}
return false
}
func awgCommentName(raw string) string {
for _, line := range splitLines(raw) {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, "#") {
continue
}
rest := strings.TrimSpace(strings.TrimPrefix(line, "#"))
lower := strings.ToLower(rest)
if strings.HasPrefix(lower, "name:") {
return strings.TrimSpace(rest[len("name:"):])
}
}
return ""
}
// shortenLink trims a link for warning messages, hiding query/credentials noise.
func shortenLink(line string) string {
if i := strings.IndexAny(line, "?#"); i >= 0 {
line = line[:i]
}
const max = 48
runes := []rune(line)
if len(runes) > max {
return string(runes[:max]) + "…"
}
return line
} }
func naiveLinkHasAuth(line string) bool { func naiveLinkHasAuth(line string) bool {
@@ -167,20 +359,44 @@ func naiveLinkHasAuth(line string) bool {
return false return false
} }
func extractClashHY2(text string) []string { // extractClashProxies builds share links from Clash YAML proxy blocks when the
// Very small helper for Clash YAML hysteria2 blocks when share URI is absent. // panel does not embed native share URIs (hysteria2, wireguard/AWG, naive).
func extractClashProxies(text string) []string {
lower := strings.ToLower(text) lower := strings.ToLower(text)
if !strings.Contains(lower, "type:") || !strings.Contains(lower, "hysteria2") { if !strings.Contains(lower, "type:") {
return nil
}
need := strings.Contains(lower, "hysteria2") ||
strings.Contains(lower, "wireguard") ||
strings.Contains(lower, "type: naive") ||
strings.Contains(lower, "type:naive")
if !need {
return nil return nil
} }
var out []string var out []string
// Split roughly by list items. for _, chunk := range splitClashItems(text) {
chunks := strings.Split(text, "\n- ")
for _, chunk := range chunks {
cl := strings.ToLower(chunk) cl := strings.ToLower(chunk)
if !strings.Contains(cl, "type:") || !strings.Contains(cl, "hysteria2") { typ := strings.ToLower(yamlField(chunk, "type"))
continue switch {
case typ == "hysteria2" || (typ == "" && strings.Contains(cl, "hysteria2")):
if link := clashHY2Link(chunk); link != "" {
out = append(out, link)
} }
case typ == "wireguard":
if link := clashWireguardLink(chunk); link != "" {
out = append(out, link)
}
case typ == "naive":
if link := clashNaiveLink(chunk); link != "" {
out = append(out, link)
}
}
}
_ = reClashHY2 // kept for possible future tightening
return out
}
func clashHY2Link(chunk string) string {
server := yamlField(chunk, "server") server := yamlField(chunk, "server")
port := yamlField(chunk, "port") port := yamlField(chunk, "port")
pass := yamlField(chunk, "password") pass := yamlField(chunk, "password")
@@ -188,7 +404,7 @@ func extractClashHY2(text string) []string {
pass = yamlField(chunk, "auth") pass = yamlField(chunk, "auth")
} }
if server == "" || port == "" || pass == "" { if server == "" || port == "" || pass == "" {
continue return ""
} }
name := yamlField(chunk, "name") name := yamlField(chunk, "name")
sni := yamlField(chunk, "sni") sni := yamlField(chunk, "sni")
@@ -217,14 +433,147 @@ func extractClashHY2(text string) []string {
if name != "" { if name != "" {
link += "#" + url.PathEscape(name) link += "#" + url.PathEscape(name)
} }
out = append(out, link) return link
} }
_ = reClashHY2 // kept for possible future tightening
func clashWireguardLink(chunk string) string {
priv := firstYAML(chunk, "private-key", "private_key")
pub := firstYAML(chunk, "public-key", "public_key")
server := yamlField(chunk, "server")
port := yamlField(chunk, "port")
ip := firstYAML(chunk, "ip", "address")
if priv == "" || pub == "" || server == "" || port == "" {
return ""
}
name := yamlField(chunk, "name")
psk := firstYAML(chunk, "pre-shared-key", "preshared-key", "presharedkey")
allowed := stripYAMLList(firstYAML(chunk, "allowed-ips", "allowed_ips"))
if allowed == "" {
allowed = "0.0.0.0/0"
}
dns := stripYAMLList(yamlField(chunk, "dns"))
mtu := yamlField(chunk, "mtu")
keepalive := firstYAML(chunk, "persistent-keepalive", "keepalive")
var b strings.Builder
b.WriteString("[Interface]\n")
b.WriteString("PrivateKey = " + priv + "\n")
if ip != "" {
b.WriteString("Address = " + ip + "\n")
}
if dns != "" {
b.WriteString("DNS = " + dns + "\n")
}
if mtu != "" {
b.WriteString("MTU = " + mtu + "\n")
}
// AmneziaWG / AWG 2.0 obfuscation (Clash Meta / Amnezia exports).
for _, k := range []string{"jc", "jmin", "jmax", "s1", "s2", "s3", "s4", "h1", "h2", "h3", "h4", "i1", "i2", "i3", "i4", "i5"} {
v := firstYAML(chunk, k, strings.ToUpper(k), strings.ToUpper(k[:1])+k[1:])
if v == "" {
continue
}
canon := strings.ToUpper(k[:1]) + k[1:]
b.WriteString(canon + " = " + v + "\n")
}
b.WriteString("\n[Peer]\n")
b.WriteString("PublicKey = " + pub + "\n")
if psk != "" {
b.WriteString("PresharedKey = " + psk + "\n")
}
b.WriteString("Endpoint = " + server + ":" + port + "\n")
b.WriteString("AllowedIPs = " + allowed + "\n")
if keepalive != "" {
b.WriteString("PersistentKeepalive = " + keepalive + "\n")
}
if name != "" {
b.WriteString("# name: " + name + "\n")
}
return b.String()
}
func clashNaiveLink(chunk string) string {
server := yamlField(chunk, "server")
port := yamlField(chunk, "port")
user := firstYAML(chunk, "username", "user")
pass := yamlField(chunk, "password")
if server == "" || user == "" || pass == "" {
return ""
}
if port == "" {
port = "443"
}
name := yamlField(chunk, "name")
udp := strings.ToLower(yamlField(chunk, "udp"))
scheme := "https"
if udp == "true" || udp == "yes" || strings.EqualFold(yamlField(chunk, "protocol"), "quic") {
scheme = "quic"
}
u := url.URL{
Scheme: scheme,
User: url.UserPassword(user, pass),
Host: server + ":" + port,
}
link := u.String()
if name != "" {
link += "#" + url.PathEscape(name)
}
return link
}
func firstYAML(block string, keys ...string) string {
for _, k := range keys {
if v := yamlField(block, k); v != "" {
return v
}
}
return ""
}
func stripYAMLList(v string) string {
v = strings.TrimSpace(v)
v = strings.TrimPrefix(v, "[")
v = strings.TrimSuffix(v, "]")
parts := strings.Split(v, ",")
var clean []string
for _, p := range parts {
p = strings.TrimSpace(p)
p = strings.Trim(p, `"'`)
if p != "" {
clean = append(clean, p)
}
}
return strings.Join(clean, ", ")
}
// reClashItemStart matches a mapping list item ("- name: X", " - type: y"),
// but not nested scalar list entries like "- h3" under alpn.
var reClashItemStart = regexp.MustCompile(`^\s*-\s+[^\s:]+\s*:`)
// splitClashItems splits a Clash YAML proxies list into per-proxy chunks,
// tolerating any list-item indentation ("- name:" at column 0 or nested).
func splitClashItems(text string) []string {
lines := splitLines(text)
var out []string
var cur []string
flush := func() {
if len(cur) > 0 {
out = append(out, strings.Join(cur, "\n"))
cur = nil
}
}
for _, line := range lines {
if reClashItemStart.MatchString(line) {
flush()
}
cur = append(cur, line)
}
flush()
return out return out
} }
func yamlField(block, key string) string { func yamlField(block, key string) string {
re := regexp.MustCompile(`(?im)^\s*` + regexp.QuoteMeta(key) + `\s*:\s*["']?([^"'#\n\r]+)["']?`) re := regexp.MustCompile(`(?im)^\s*(?:-\s+)?` + regexp.QuoteMeta(key) + `\s*:\s*["']?([^"'#\n\r]+)["']?`)
m := re.FindStringSubmatch(block) m := re.FindStringSubmatch(block)
if len(m) < 2 { if len(m) < 2 {
return "" return ""
@@ -271,6 +620,17 @@ func defaultName(proto config.Protocol, uri string) string {
if h, _, err := hysteria2.HostPort(uri); err == nil { if h, _, err := hysteria2.HostPort(uri); err == nil {
host = h host = h
} }
case config.ProtocolAWG:
if c, err := awg.Parse(uri); err == nil {
if c.Name != "" {
return c.Name
}
if h, _, e := net.SplitHostPort(c.Endpoint); e == nil {
host = h
} else {
host = c.Endpoint
}
}
default: default:
if u, _, err := naive.ParseShareLink(uri); err == nil { if u, _, err := naive.ParseShareLink(uri); err == nil {
if parsed, err := url.Parse(u); err == nil { if parsed, err := url.Parse(u); err == nil {
+294
View File
@@ -1,7 +1,11 @@
package subscription package subscription
import ( import (
"context"
"encoding/base64" "encoding/base64"
"io"
"net/http"
"net/http/httptest"
"strings" "strings"
"testing" "testing"
) )
@@ -32,6 +36,138 @@ func TestParseBodyBase64(t *testing.T) {
} }
} }
// TestParseBodyTolerant: one broken entry must never abort the import —
// valid links are kept, unsupported/broken lines are skipped with warnings.
func TestParseBodyTolerant(t *testing.T) {
body := `vless://7e1c0fbf-1758-4781-97f6-ae8715ed3f60@cz.example.com:443?encryption=none&security=tls&type=tcp#CZ
hysteria2://secret@de.example.com:443/?sni=de.example.com#DE-HY2
https://api.example.com/ZKszcVcC3xbWb8qj
foobar://what@is.this:1
# comment line
`
res, err := ParseBodyDetailed(body)
if err != nil {
t.Fatal(err)
}
if len(res.Items) != 2 {
t.Fatalf("imported %d items, want 2: %+v", len(res.Items), res.Items)
}
if res.Items[0].Protocol != "vless" || res.Items[1].Protocol != "hysteria2" {
t.Fatalf("protocols: %s, %s", res.Items[0].Protocol, res.Items[1].Protocol)
}
// https:// without userinfo + unknown scheme → skipped; empty/comment lines are not counted.
if res.Skipped != 2 {
t.Fatalf("skipped %d, want 2 (%v)", res.Skipped, res.Warnings)
}
if len(res.Warnings) != 2 {
t.Fatalf("warnings: %v", res.Warnings)
}
}
func TestParseBodyZeroValidListsReasons(t *testing.T) {
body := "https://api.example.com/sub-token-line\nfoobar://x@y:1\n"
res, err := ParseBodyDetailed(body)
if err == nil {
t.Fatal("want error for zero valid entries")
}
if res.Skipped != 2 {
t.Fatalf("skipped %d, want 2", res.Skipped)
}
if !strings.Contains(err.Error(), "2 записей пропущено") {
t.Fatalf("error should list skip count: %v", err)
}
}
func TestParseUserInfoHeader(t *testing.T) {
info := ParseUserInfoHeader("upload=0; download=5679561725; total=53687091200000; expire=1799366585")
if info == nil {
t.Fatal("nil info")
}
if info.Download != 5679561725 || info.Total != 53687091200000 || info.Expire != 1799366585 || info.Upload != 0 {
t.Fatalf("bad parse: %+v", info)
}
if ParseUserInfoHeader("") != nil {
t.Fatal("empty header must give nil")
}
if ParseUserInfoHeader("garbage") != nil {
t.Fatal("garbage header must give nil")
}
}
// Remnawave returns Clash YAML for Clash-like UAs and raw base64 links otherwise.
// Fetch must obtain the full server list (plain UA) and the userinfo header.
func TestFetchPrefersPlainUA(t *testing.T) {
links := "vless://7e1c0fbf-1758-4781-97f6-ae8715ed3f60@cz.example.com:443?encryption=none&security=tls&type=tcp#CZ\n" +
"trojan://pass@no.example.com:20000?security=tls&type=ws#NO\n" +
"hysteria2://secret@de.example.com:443/?sni=de.example.com#DE\n" +
"ss://YWVzLTI1Ni1nY206cGFzcw@ss.example.com:8388#SS\n"
clashYAML := "proxies:\n - name: only-hy2\n type: hysteria2\n server: de.example.com\n port: 443\n password: secret\n"
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Subscription-Userinfo", "upload=1; download=2; total=100; expire=1799366585")
if strings.Contains(strings.ToLower(r.Header.Get("User-Agent")), "clash") {
_, _ = w.Write([]byte(clashYAML))
return
}
_, _ = w.Write([]byte(base64.StdEncoding.EncodeToString([]byte(links))))
}))
defer srv.Close()
res, err := Fetch(context.Background(), srv.URL)
if err != nil {
t.Fatal(err)
}
if len(res.Items) != 3 {
t.Fatalf("imported %d, want 3 (vless+trojan+hy2): %+v", len(res.Items), res.Items)
}
if res.Skipped != 1 {
t.Fatalf("skipped %d, want 1 (ss://)", res.Skipped)
}
if res.Info == nil || res.Info.Expire != 1799366585 || res.Info.Total != 100 {
t.Fatalf("info: %+v", res.Info)
}
}
// Indented Clash proxies with nested lists (alpn) must split into separate blocks.
func TestParseBodyClashYAMLIndented(t *testing.T) {
body := `proxies:
- name: DE HY2
type: hysteria2
server: de.example.com
port: 443
udp: true
password: secret1
sni: de.example.com
alpn:
- h3
- name: NO HY2
type: hysteria2
server: no.example.com
port: 9000
password: secret2
alpn:
- h3
`
items, err := ParseBody(body)
if err != nil {
t.Fatal(err)
}
if len(items) != 2 {
t.Fatalf("got %d items, want 2: %+v", len(items), items)
}
for i, host := range []string{"de.example.com:443", "no.example.com:9000"} {
if !strings.Contains(items[i].URI, host) {
t.Fatalf("item %d uri %s missing %s", i, items[i].URI, host)
}
}
for i, pass := range []string{"secret1", "secret2"} {
if !strings.Contains(items[i].URI, pass) {
t.Fatalf("item %d uri %s missing password %s", i, items[i].URI, pass)
}
}
}
func TestParseBodyClashYAML(t *testing.T) { func TestParseBodyClashYAML(t *testing.T) {
body := ` body := `
proxies: proxies:
@@ -58,3 +194,161 @@ proxies:
t.Fatalf("uri %s", items[0].URI) t.Fatalf("uri %s", items[0].URI)
} }
} }
func TestParseBodyAWGConf(t *testing.T) {
body := `[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
Address = 10.8.0.2/32
DNS = 1.1.1.1
Jc = 4
Jmin = 40
Jmax = 70
H1 = 1
H2 = 2
H3 = 3
H4 = 4
I1 = <r 128>
[Peer]
PublicKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Endpoint = 203.0.113.10:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
`
items, err := ParseBody(body)
if err != nil {
t.Fatal(err)
}
if len(items) != 1 {
t.Fatalf("got %d items: %+v", len(items), items)
}
if items[0].Protocol != "awg" {
t.Fatalf("proto %s", items[0].Protocol)
}
if !strings.Contains(items[0].URI, "Jc = 4") || !strings.Contains(items[0].URI, "203.0.113.10:51820") {
t.Fatalf("uri missing AWG2 fields: %s", items[0].URI)
}
}
func TestParseBodyAmneziaJSON(t *testing.T) {
body := `{
"client_priv_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"server_pub_key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=",
"client_ip": "10.8.0.2/32",
"hostName": "203.0.113.10",
"port": "51820",
"Jc": 4,
"Jmin": 10,
"Jmax": 50,
"H1": "1",
"H2": "2",
"H3": "3",
"H4": "4"
}`
items, err := ParseBody(body)
if err != nil {
t.Fatal(err)
}
if len(items) != 1 || items[0].Protocol != "awg" {
t.Fatalf("got %+v", items)
}
if !strings.Contains(items[0].URI, "203.0.113.10:51820") {
t.Fatalf("uri %s", items[0].URI)
}
}
func TestParseBodyClashWireguardAWG20(t *testing.T) {
body := `proxies:
- name: AWG-EU
type: wireguard
server: 203.0.113.20
port: 51820
ip: 10.8.0.2/32
private-key: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
public-key: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
allowed-ips: ['0.0.0.0/0']
dns: [1.1.1.1]
jc: 4
jmin: 40
jmax: 70
h1: 1
h2: 2
h3: 3
h4: 4
`
items, err := ParseBody(body)
if err != nil {
t.Fatal(err)
}
if len(items) != 1 {
t.Fatalf("got %d: %+v", len(items), items)
}
if items[0].Protocol != "awg" {
t.Fatalf("proto %s", items[0].Protocol)
}
if items[0].Name != "AWG-EU" {
t.Fatalf("name %q", items[0].Name)
}
if !strings.Contains(items[0].URI, "Jc = 4") || !strings.Contains(items[0].URI, "203.0.113.20:51820") {
t.Fatalf("uri %s", items[0].URI)
}
}
func TestParseBodyClashNaive(t *testing.T) {
body := `proxies:
- name: Naive-HTTPS
type: naive
server: naive.example.com
port: 443
username: user
password: secret
- name: Naive-QUIC
type: naive
server: quic.example.com
port: 443
username: u2
password: p2
udp: true
`
items, err := ParseBody(body)
if err != nil {
t.Fatal(err)
}
if len(items) != 2 {
t.Fatalf("got %d: %+v", len(items), items)
}
if items[0].Protocol != "naive" || items[1].Protocol != "naive" {
t.Fatalf("protos %s %s", items[0].Protocol, items[1].Protocol)
}
if !strings.Contains(items[0].URI, "https://") || !strings.Contains(items[0].URI, "user:secret@naive.example.com") {
t.Fatalf("https uri %s", items[0].URI)
}
if !strings.Contains(items[1].URI, "quic://") || !strings.Contains(items[1].URI, "u2:p2@quic.example.com") {
t.Fatalf("quic uri %s", items[1].URI)
}
}
func TestFetchAWGConfOverHTTP(t *testing.T) {
conf := `[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
Address = 10.8.0.2/32
Jc = 3
[Peer]
PublicKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Endpoint = 198.51.100.7:51820
AllowedIPs = 0.0.0.0/0
`
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = io.WriteString(w, conf)
}))
defer srv.Close()
res, err := Fetch(context.Background(), srv.URL+"/awg.conf")
if err != nil {
t.Fatal(err)
}
if len(res.Items) != 1 || res.Items[0].Protocol != "awg" {
t.Fatalf("got %+v", res.Items)
}
}
+7 -6
View File
@@ -18,14 +18,14 @@ 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.2" const CurrentVersion = "4.2.9"
// 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 = 1 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.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/update.json"
// DisplayVersion is shown in the UI (product + build), e.g. "2.8.0+1". // DisplayVersion is shown in the UI (product + build), e.g. "2.8.0+1".
func DisplayVersion() string { func DisplayVersion() string {
@@ -274,7 +274,8 @@ func splitVer(v string) []int {
func allowedDownloadURL(u string) bool { func allowedDownloadURL(u string) bool {
u = strings.ToLower(strings.TrimSpace(u)) u = strings.ToLower(strings.TrimSpace(u))
return strings.HasPrefix(u, "https://git.evilfox.cc/") || return strings.HasPrefix(u, "https://git.de4ima.uk/") ||
strings.HasPrefix(u, "https://git.evilfox.cc/") ||
strings.HasPrefix(u, "https://files.de4ima.uk/") strings.HasPrefix(u, "https://files.de4ima.uk/")
} }
@@ -286,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)
@@ -309,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 {
@@ -355,7 +356,7 @@ func prepareDownload(ctx context.Context, manifestURL string) (latest, url, sha,
return "", "", "", "", "", fmt.Errorf("уже актуальная версия %s (канал %s)", DisplayVersion(), st.Latest) return "", "", "", "", "", fmt.Errorf("уже актуальная версия %s (канал %s)", DisplayVersion(), st.Latest)
} }
if !allowedDownloadURL(st.URL) { if !allowedDownloadURL(st.URL) {
return "", "", "", "", "", fmt.Errorf("URL обновления должен быть на git.evilfox.cc или files.de4ima.uk") return "", "", "", "", "", fmt.Errorf("URL обновления должен быть на git.de4ima.uk, git.evilfox.cc или files.de4ima.uk")
} }
exe, err := os.Executable() exe, err := os.Executable()
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 {
+22
View File
@@ -0,0 +1,22 @@
// Package vpnmode implements "Режим VPN": a wintun TUN device that captures
// ALL system traffic (apps/games included) and feeds it into the local SOCKS
// proxy of the active protocol engine via the embedded tun2socks stack.
//
// Windows-only for now. On other platforms Supported() returns false and the
// UI hides the switch (no fake toggle).
package vpnmode
import "io"
// Config describes one VPN-mode session.
type Config struct {
// SOCKSAddr is the host:port of the running local SOCKS5 proxy.
SOCKSAddr string
// ExcludeHosts are VPN server endpoints (host names or IPs) that must
// bypass the TUN default route, otherwise the tunnel would loop.
ExcludeHosts []string
// DNSServers go through the tunnel (default 1.1.1.1 + 8.8.8.8).
DNSServers []string
// Stderr receives diagnostic messages (may be nil).
Stderr io.Writer
}
+25
View File
@@ -0,0 +1,25 @@
//go:build !windows
package vpnmode
import "fmt"
// Supported: VPN mode ships Windows-first; the UI hides the switch elsewhere.
func Supported() bool { return false }
// IsElevated is meaningless off Windows.
func IsElevated() bool { return false }
// WintunAvailable is Windows-only.
func WintunAvailable() bool { return false }
// Session is a stub off Windows.
type Session struct{}
// Start always fails off Windows.
func Start(cfg Config) (*Session, error) {
return nil, fmt.Errorf("режим VPN пока доступен только на Windows")
}
// Close is a no-op stub.
func (s *Session) Close() error { return nil }
+281
View File
@@ -0,0 +1,281 @@
//go:build windows
package vpnmode
import (
"encoding/binary"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/windows"
"gvisor.dev/gvisor/pkg/tcpip/stack"
t2core "github.com/xjasonlyu/tun2socks/v2/core"
"github.com/xjasonlyu/tun2socks/v2/core/device"
t2tun "github.com/xjasonlyu/tun2socks/v2/core/device/tun"
t2log "github.com/xjasonlyu/tun2socks/v2/log"
"github.com/xjasonlyu/tun2socks/v2/proxy/socks5"
"github.com/xjasonlyu/tun2socks/v2/tunnel"
)
const (
tunName = "NavisTun"
tunAddr = "10.255.99.2"
tunMask = "255.255.255.0"
// On-link gateway inside the TUN subnet; wintun swallows everything anyway.
tunGateway = "10.255.99.1"
)
// Supported reports that VPN mode is available on this OS.
func Supported() bool { return true }
// IsElevated reports whether the process runs with administrator rights.
func IsElevated() bool {
return windows.GetCurrentProcessToken().IsElevated()
}
// WintunAvailable checks that wintun.dll can be found (exe dir or System32).
func WintunAvailable() bool {
if exe, err := os.Executable(); err == nil {
if st, err := os.Stat(filepath.Join(filepath.Dir(exe), "wintun.dll")); err == nil && !st.IsDir() {
return true
}
}
if sysDir, err := windows.GetSystemDirectory(); err == nil {
if st, err := os.Stat(filepath.Join(sysDir, "wintun.dll")); err == nil && !st.IsDir() {
return true
}
}
return false
}
// Session is a running VPN-mode capture (TUN + tun2socks + routes).
type Session struct {
mu sync.Mutex
dev device.Device
stack *stack.Stack
excluded []exclusionRoute // host routes added via route.exe, removed on Close
stderr io.Writer
closed bool
setupDone bool
}
type exclusionRoute struct {
ip string
gateway string
ifIndex uint32
}
// Start opens the wintun adapter, wires it to the local SOCKS proxy and takes
// over the default route (0.0.0.0/1 + 128.0.0.0/1). Requires admin+wintun.dll.
func Start(cfg Config) (*Session, error) {
if cfg.Stderr == nil {
cfg.Stderr = os.Stderr
}
if strings.TrimSpace(cfg.SOCKSAddr) == "" {
return nil, fmt.Errorf("режим VPN: нет локального SOCKS-прокси у текущего протокола")
}
if !IsElevated() {
return nil, fmt.Errorf("Запустите EvilFox от имени администратора для режима VPN")
}
if !WintunAvailable() {
return nil, fmt.Errorf("не найден wintun.dll — скачайте с wintun.net и положите рядом с EvilFox.exe")
}
if len(cfg.DNSServers) == 0 {
cfg.DNSServers = []string{"1.1.1.1", "8.8.8.8"}
}
// Resolve exclusion routes BEFORE the default route changes, otherwise
// DNS + GetBestRoute would already point into the (not yet working) TUN.
var excls []exclusionRoute
for _, h := range cfg.ExcludeHosts {
h = strings.TrimSpace(h)
if h == "" {
continue
}
ips, err := net.LookupIP(h)
if err != nil {
return nil, fmt.Errorf("режим VPN: не удалось определить IP сервера %s: %w", h, err)
}
for _, ip := range ips {
ip4 := ip.To4()
if ip4 == nil || ip4.IsLoopback() || ip4.IsPrivate() {
continue
}
gw, ifIdx, err := bestRoute(ip4)
if err != nil {
fmt.Fprintf(cfg.Stderr, "vpnmode: GetBestRoute %s: %v\n", ip4, err)
continue
}
excls = append(excls, exclusionRoute{ip: ip4.String(), gateway: gw.String(), ifIndex: ifIdx})
}
}
// Keep tun2socks quiet unless something is wrong.
if lvl, err := t2log.ParseLevel("warning"); err == nil {
t2log.SetLogger(t2log.Must(t2log.NewLeveled(lvl)))
}
px, err := socks5.New(cfg.SOCKSAddr, "", "")
if err != nil {
return nil, fmt.Errorf("режим VPN: socks5 %s: %w", cfg.SOCKSAddr, err)
}
tunnel.T().SetProxy(px)
dev, err := t2tun.Open(tunName, 0)
if err != nil {
return nil, fmt.Errorf("режим VPN: не удалось создать TUN-адаптер (wintun): %w", err)
}
st, err := t2core.CreateStack(&t2core.Config{
LinkEndpoint: dev,
TransportHandler: tunnel.T(),
})
if err != nil {
dev.Close()
return nil, fmt.Errorf("режим VPN: сетевой стек: %w", err)
}
s := &Session{dev: dev, stack: st, stderr: cfg.Stderr}
if err := s.setupNetwork(cfg, excls); err != nil {
_ = s.Close()
return nil, err
}
return s, nil
}
func (s *Session) setupNetwork(cfg Config, excls []exclusionRoute) error {
// The adapter needs a moment to register with the IP stack.
var lastErr error
for i := 0; i < 20; i++ {
if _, err := net.InterfaceByName(tunName); err == nil {
lastErr = nil
break
} else {
lastErr = err
}
time.Sleep(150 * time.Millisecond)
}
if lastErr != nil {
return fmt.Errorf("режим VPN: адаптер %s не появился: %w", tunName, lastErr)
}
if out, err := runHidden("netsh", "interface", "ipv4", "set", "address",
"name="+tunName, "source=static", "addr="+tunAddr, "mask="+tunMask); err != nil {
return fmt.Errorf("режим VPN: настройка адреса TUN: %v (%s)", err, out)
}
// DNS goes through the tunnel (queries hit the default route → TUN).
if out, err := runHidden("netsh", "interface", "ipv4", "set", "dnsservers",
"name="+tunName, "source=static", "address="+cfg.DNSServers[0], "register=none", "validate=no"); err != nil {
fmt.Fprintf(s.stderr, "vpnmode: set dns: %v (%s)\n", err, out)
}
for i, d := range cfg.DNSServers[1:] {
if out, err := runHidden("netsh", "interface", "ipv4", "add", "dnsservers",
"name="+tunName, "address="+d, "index="+strconv.Itoa(i+2), "validate=no"); err != nil {
fmt.Fprintf(s.stderr, "vpnmode: add dns: %v (%s)\n", err, out)
}
}
// Exclusions first — the server endpoint must keep using the real uplink.
for _, e := range excls {
if e.gateway == "0.0.0.0" || e.gateway == "" {
continue
}
if out, err := runHidden("route", "add", e.ip, "mask", "255.255.255.255",
e.gateway, "metric", "1", "if", strconv.FormatUint(uint64(e.ifIndex), 10)); err != nil {
return fmt.Errorf("режим VPN: маршрут-исключение для %s: %v (%s)", e.ip, err, out)
}
s.excluded = append(s.excluded, e)
}
// Two half-default routes beat 0.0.0.0/0 without touching the original.
for _, prefix := range []string{"0.0.0.0/1", "128.0.0.0/1"} {
if out, err := runHidden("netsh", "interface", "ipv4", "add", "route",
prefix, tunName, tunGateway, "metric=1", "store=active"); err != nil {
return fmt.Errorf("режим VPN: маршрут %s: %v (%s)", prefix, err, out)
}
}
s.setupDone = true
fmt.Fprintf(s.stderr, "vpnmode: TUN %s активен, весь трафик через %s (исключений: %d)\n",
tunName, cfg.SOCKSAddr, len(s.excluded))
return nil
}
// Close removes exclusion routes and destroys the TUN adapter (its own routes
// disappear together with the adapter).
func (s *Session) Close() error {
s.mu.Lock()
defer s.mu.Unlock()
if s.closed {
return nil
}
s.closed = true
for _, e := range s.excluded {
if out, err := runHidden("route", "delete", e.ip, "mask", "255.255.255.255", e.gateway); err != nil {
fmt.Fprintf(s.stderr, "vpnmode: route delete %s: %v (%s)\n", e.ip, err, out)
}
}
s.excluded = nil
if s.dev != nil {
s.dev.Close()
s.dev = nil
}
if s.stack != nil {
s.stack.Close()
s.stack = nil
}
return nil
}
func runHidden(name string, args ...string) (string, error) {
cmd := exec.Command(name, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: 0x08000000} // CREATE_NO_WINDOW
out, err := cmd.CombinedOutput()
return strings.TrimSpace(string(out)), err
}
var (
modiphlpapi = windows.NewLazySystemDLL("iphlpapi.dll")
procGetBestRoute = modiphlpapi.NewProc("GetBestRoute")
)
// mibIPForwardRow mirrors MIB_IPFORWARDROW (iphlpapi).
type mibIPForwardRow struct {
Dest uint32
Mask uint32
Policy uint32
NextHop uint32
IfIndex uint32
Type uint32
Proto uint32
Age uint32
NextHopAS uint32
Metric1 uint32
Metric2 uint32
Metric3 uint32
Metric4 uint32
Metric5 uint32
}
// bestRoute asks Windows for the current gateway + interface toward ip
// (IPv4, network byte order DWORDs).
func bestRoute(ip4 net.IP) (net.IP, uint32, error) {
dest := binary.LittleEndian.Uint32(ip4.To4())
var row mibIPForwardRow
r, _, _ := procGetBestRoute.Call(uintptr(dest), 0, uintptr(unsafe.Pointer(&row)))
if r != 0 {
return nil, 0, fmt.Errorf("GetBestRoute: код %d", r)
}
gw := make(net.IP, 4)
binary.LittleEndian.PutUint32(gw, row.NextHop)
return gw, row.IfIndex, nil
}
+14 -13
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)
@@ -30,16 +30,17 @@
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap"> IgnorableNamespaces="uap rescap">
<!-- ProcessorArchitecture is substituted by scripts/pack-msix.ps1 (x64 | arm64). Do not ship x86/AArch32. -->
<Identity <Identity
Name="EvilFox.Navis" Name="EvilFox.evilfox"
Publisher="CN=EvilFox" Publisher="CN=9D58DFF7-7918-460E-845B-B3904A30562A"
Version="3.8.2.1" Version="4.2.9.0"
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 +62,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 +75,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>
+57 -20
View File
@@ -1,13 +1,37 @@
# MSIX packaging for Navis (Microsoft Store) # MSIX packaging for EvilFox (Microsoft Store)
Navis ships as a **full-trust Win32 + WebView2** app. Prefer **MSIX** over a PWA: EvilFox ships as a **full-trust Win32 + WebView2** app. Prefer **MSIX** over a PWA:
system proxy and protocol cores cannot be delivered as a Store PWA alone. system proxy and protocol cores cannot be delivered as a Store PWA alone.
## Multi-arch (x64 + Arm64)
- Device family: `Windows.Desktop` (Win10 19041+ / Win11; `MaxVersionTested` 10.0.26100.0).
- Architectures: **x64** and **arm64** only (no x86 / AArch32).
- Pack script writes final artifacts to **`dist\microsoft-store\`**:
- `EvilFox_<ver>_x64.msix`
- `EvilFox_<ver>_arm64.msix`
- `EvilFox_<ver>.msixbundle` (when MakeAppx can bundle both)
- `README.txt` (Partner Center upload notes, RU+EN)
- `listing\` (store.csv + screenshots) — filled by the submission prep step
If MakeAppx is missing, the script still stages under `packaging/msix/_staging_<arch>` and prints exact `makeappx` commands in `README.txt`.
## Prerequisites ## Prerequisites
1. Build Windows binaries: `build.bat``dist\navis-release\windows\Navis.exe` 1. Build Windows x64: `build.bat``dist\navis-release\windows\EvilFox.exe`
2. [Windows SDK](https://developer.microsoft.com/windows/downloads/windows-sdk/) with `MakeAppx.exe` (and `SignTool.exe` for sideload signing) 2. Cross-build Arm64 (`CGO_ENABLED=0`). **Important:** `goversioninfo -64` produces an **amd64** `cmd/vpnapp/resource.syso` — temporarily rename it aside for the arm64 link, then restore:
3. Optional: a code-signing certificate whose **Subject CN** matches `Identity/@Publisher` in `AppxManifest.xml`
```bat
set GOOS=windows
set GOARCH=arm64
set CGO_ENABLED=0
move cmd\vpnapp\resource.syso cmd\vpnapp\resource.syso.amd64.bak
go build -ldflags="-H windowsgui -s -w" -trimpath -o dist\navis-release\windows\arm64\EvilFox.exe ./cmd/vpnapp
move cmd\vpnapp\resource.syso.amd64.bak cmd\vpnapp\resource.syso
```
3. [Windows SDK](https://developer.microsoft.com/windows/downloads/windows-sdk/) with `MakeAppx.exe` (and `SignTool.exe` for sideload signing)
4. Optional: code-signing certificate whose **Subject CN** matches `Identity/@Publisher`
## Pack ## Pack
@@ -15,31 +39,44 @@ From the repo root (PowerShell):
```powershell ```powershell
.\scripts\pack-msix.ps1 .\scripts\pack-msix.ps1
.\scripts\pack-msix.ps1 -Arch x64 -Version 4.2.8.1
.\scripts\pack-msix.ps1 -Arch arm64 -ExePath dist\navis-release\windows\arm64\EvilFox.exe
.\scripts\pack-msix.ps1 -Arch all -OutDir dist\microsoft-store
.\scripts\pack-msix.ps1 -Publisher "CN=Your Store Publisher" -Name "EvilFox.EvilFox"
.\scripts\pack-msix.ps1 -CertPath ".\certs\evilfox-msix.pfx" -CertPassword "..."
``` ```
Options: Parameters: `-Arch x64|arm64|all`, `-OutDir`, `-Version`, `-Name`, `-Publisher`.
```powershell
.\scripts\pack-msix.ps1 -ExePath "dist\navis-release\windows\Navis.exe" -Version "2.9.0.1"
.\scripts\pack-msix.ps1 -Publisher "CN=Your Store Publisher" -Name "EvilFox.Navis"
.\scripts\pack-msix.ps1 -CertPath ".\certs\navis-msix.pfx" -CertPassword "..."
```
Output: `dist\navis-release\windows\Navis_<version>.msix`
## Store submission ## Store submission
1. Create the app in [Partner Center](https://partner.microsoft.com/dashboard). 1. Create the app in [Partner Center](https://partner.microsoft.com/dashboard).
2. Copy **Package/Identity Name** and **Publisher** into `AppxManifest.xml` (or pass `-Name` / `-Publisher` to the script). 2. Copy **Package/Identity Name** and **Publisher** into `AppxManifest.xml` (or pass `-Name` / `-Publisher`).
3. Upload the `.msix` (Partner Center can re-sign with the Store cert). 3. Upload **`.msixbundle`** if available, else both **`.msix`** files. Partner Center can re-sign with the Store cert.
4. Declare **runFullTrust** and network capabilities; answer Store questions about VPN/proxy use. 4. Device family: **Windows 10/11 Desktop** only. Arm64 covers Windows on ARM.
5. Privacy policy URL required (document system-proxy changes and outbound tunnel traffic). 5. Declare **runFullTrust** and network capabilities; answer Store questions about VPN/proxy use.
6. **Do not enable in-app self-update** for Store builds — Navis detects MSIX package identity and hides the updater (Store updates only). 6. Privacy policy URL on [evilfox.win](https://evilfox.win) required.
7. Sign in to Visual Studio / Partner Center with the **same Store developer account** if using VS packaging / Store association.
8. **Do not enable in-app self-update** for Store builds — EvilFox detects MSIX package identity and hides the updater (Store updates only).
Optional: place arch-specific `wintun.dll` under `assets/wintun/<arch>/`. Without it, packaging still succeeds; TUN/VPN mode needs the dll next to the exe (proxy mode works without it).
## Sideload test ## Sideload test
```powershell ```powershell
Add-AppxPackage -Path .\dist\navis-release\windows\Navis_2.9.0.1.msix Add-AppxPackage -Path .\dist\microsoft-store\EvilFox_4.2.8.1_x64.msix
``` ```
Unsigned packages need a trusted cert or developer mode / sideloading enabled. Unsigned packages need a trusted cert or developer mode / sideloading enabled.
## Remnawave notes
EvilFox can pull configs from a Remnawave panel:
1. Public subscription: paste `https://<panel>/api/sub/<shortUuid>` (or use panel URL + short UUID in the Remnawave block).
2. Optional API token (panel → API Tokens) enables authenticated lookups:
- `GET /api/subscriptions/by-short-uuid/{short}` / `…/raw`
- `GET /api/users/by-short-uuid/{short}``subscriptionUrl`
3. Optional Caddy `X-Api-Key` if the panel sits behind Caddy token auth.
Tokens are stored only in local `configs/config.json` (mode 0600) — never commit them.
+50
View File
@@ -0,0 +1,50 @@
EvilFox Microsoft Store package folder
Version: {{VERSION}}
Generated: {{GENERATED}}
=== Partner Center upload (EN) ===
1. Sign in to Partner Center with the same Microsoft Store developer account
you use for Visual Studio packaging / Store association (same tenant).
2. Open the EvilFox product -> Packages (Windows 10 / 11).
3. Prefer uploading EvilFox_{{VERSION}}.msixbundle if present.
Otherwise upload each EvilFox_{{VERSION}}_<arch>.msix (x64 + arm64).
4. Device family: check Windows 10 / 11 Desktop ONLY
(do NOT select Xbox, HoloLens, Surface Hub, IoT, Team, etc.).
5. Arm64 package covers Windows on ARM. Do not ship x86 / AArch32;
Microsoft is dropping AArch32 on future WoA.
6. Before final Store signing, replace Identity Name/Publisher with the
exact values from Partner Center (or let Partner Center re-sign).
7. Privacy policy URL on https://evilfox.win is required for Store listing.
=== Partner Center (RU) ===
1. Войдите в Partner Center тем же аккаунтом разработчика Store,
что и в Visual Studio (ассоциация приложения / упаковка).
2. Загрузите EvilFox_{{VERSION}}.msixbundle, если есть; иначе оба .msix
(x64 и arm64).
3. Семейство устройств: только Windows 10/11 Desktop
(без Xbox / HoloLens / IoT и т.п.).
4. Arm64 = Windows on ARM; x86/AArch32 не публикуем.
5. Name/Publisher в манифесте должны совпадать с Partner Center
(или Store переподпишет пакет).
6. Нужен URL политики конфиденциальности на evilfox.win.
=== Artifacts ===
{{ARTIFACTS}}
=== MakeAppx ===
{{MAKEAPPX}}
=== wintun.dll ===
{{WINTUN}}
=== Listing assets ===
listing\store.csv - Partner Center bulk listing fields
listing\screen\ - screenshots for Store listing
=== Arm64 build note ===
Cross-build (CGO_ENABLED=0):
set GOOS=windows
set GOARCH=arm64
rem temporarily move cmd\vpnapp\resource.syso (amd64 from goversioninfo -64)
go build -ldflags="-H windowsgui -s -w" -trimpath -o dist\navis-release\windows\arm64\EvilFox.exe ./cmd/vpnapp
rem restore resource.syso
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Some files were not shown because too many files have changed in this diff Show More