Release 2.6.0: fix AmneziaWG key parsing for awg://, JSON and vpn://.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -243,6 +243,9 @@ https://evilfox.win/
|
||||
|
||||
Некоторые AV (Bkav, Microsoft Wacapew/Wacatac, Ikarus Trojan.WinGo.Agent, Google, Trapmine) часто помечают **любые** неподписанные Go-бинарники с сетью и сменой системного прокси.
|
||||
|
||||
В 2.6.0:
|
||||
- исправлен разбор AmneziaWG: ключ в `awg://KEY@host`, JSON/`vpn://`, пробелы в именах полей, URL-safe base64.
|
||||
|
||||
В 2.3.0:
|
||||
- Android APK: подписка, список нод, пинг, автолучший; VLESS/VMess/Trojan (Xray) и Hysteria 2 через VpnService.
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ android {
|
||||
applicationId = "win.evilfox.navis"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 230
|
||||
versionName = "2.3.0"
|
||||
versionCode = 260
|
||||
versionName = "2.6.0"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
ndk {
|
||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
|
||||
|
||||
@@ -61,7 +61,7 @@ class ProfileStore(context: Context) {
|
||||
fun getSubUrl(): String = prefs.getString("sub_url", "") ?: ""
|
||||
fun setSubUrl(url: String) = prefs.edit().putString("sub_url", url).apply()
|
||||
|
||||
fun autoBest(): Boolean = prefs.getBoolean("auto_best", true)
|
||||
fun autoBest(): Boolean = prefs.getBoolean("auto_best", false)
|
||||
fun setAutoBest(v: Boolean) = prefs.edit().putBoolean("auto_best", v).apply()
|
||||
|
||||
suspend fun importSubscription(url: String): List<Profile> = withContext(Dispatchers.IO) {
|
||||
|
||||
@@ -22,7 +22,7 @@ data class UiState(
|
||||
val connected: Boolean = false,
|
||||
val connectedName: String? = null,
|
||||
val subUrl: String = "",
|
||||
val autoBest: Boolean = true,
|
||||
val autoBest: Boolean = false,
|
||||
val busy: Boolean = false,
|
||||
val meta: String = "Готово",
|
||||
val metaErr: Boolean = false,
|
||||
|
||||
@@ -97,7 +97,7 @@ fun NavisScreen(
|
||||
Text("Navis", fontSize = 28.sp, fontWeight = FontWeight.Black, color = Ink)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Surface(color = Soft, shape = RoundedCornerShape(999.dp)) {
|
||||
Text("2.3", Modifier.padding(horizontal = 8.dp, vertical = 2.dp), fontSize = 12.sp, fontWeight = FontWeight.Bold, color = AccentDeep)
|
||||
Text("2.6", Modifier.padding(horizontal = 8.dp, vertical = 2.dp), fontSize = 12.sp, fontWeight = FontWeight.Bold, color = AccentDeep)
|
||||
}
|
||||
}
|
||||
Text("Android · Naive · Hy2 · AWG · Xray", color = Muted, fontSize = 13.sp)
|
||||
|
||||
+3
-3
@@ -28,11 +28,11 @@ if errorlevel 1 exit /b 1
|
||||
go build -o "tools\packmac\packmac.exe" .\tools\packmac
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 2.2.1 -arch arm64
|
||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 2.6.0 -arch arm64
|
||||
if errorlevel 1 exit /b 1
|
||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 2.2.1 -arch amd64
|
||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 2.6.0 -arch amd64
|
||||
if errorlevel 1 exit /b 1
|
||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 2.2.1 -arch universal
|
||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 2.6.0 -arch universal
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
echo Built:
|
||||
|
||||
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Vendored
+7
-7
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"version": "2.2.1",
|
||||
"notes": "Универсальный macOS DMG (Intel + Apple Silicon), скрипт автоустановки, предупреждения при неверной архитектуре",
|
||||
"version": "2.6.0",
|
||||
"notes": "Исправление AmneziaWG: ключи в awg://KEY@host, JSON/vpn://, пробелы в полях; без автоподключения по умолчанию",
|
||||
"platform": "windows-amd64",
|
||||
"os": "windows",
|
||||
"arch": "amd64",
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "993e17d6390c8a4378e51016012006f18db1f6bd7c25957a4bf82fe31ab63473",
|
||||
"sha256": "5d7d262c7804779aff8ccc4ef48ed3b280eb460ead3cc95386added909fb3fb2",
|
||||
"mandatory": false,
|
||||
"platforms": {
|
||||
"windows-amd64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "993e17d6390c8a4378e51016012006f18db1f6bd7c25957a4bf82fe31ab63473",
|
||||
"sha256": "5d7d262c7804779aff8ccc4ef48ed3b280eb460ead3cc95386added909fb3fb2",
|
||||
"os": "windows",
|
||||
"arch": "amd64"
|
||||
},
|
||||
"darwin-arm64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
|
||||
"sha256": "b1589bffebb7cbeee17ca5a40cedb6ca614a3580be22488505a1403e1021c2b6",
|
||||
"sha256": "31ae3cca6cdfd9239b1812251a2c55e7c8ea879ae543020d1ca7f4138585a0b8",
|
||||
"os": "darwin",
|
||||
"arch": "arm64",
|
||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"darwin-amd64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
|
||||
"sha256": "149258fbad816aa2e73384da492bc91e3bf6700d4d01d55c9f35a3f68998232c",
|
||||
"sha256": "d4ed12f647c2ce87a266999f73c66a54873eb9bd77f75dc49d5cb4f36f94aff1",
|
||||
"os": "darwin",
|
||||
"arch": "amd64",
|
||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg",
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"darwin-universal": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis",
|
||||
"sha256": "37ceb0e47cd542dadcd406cedecc760d9aa71a40f0b109f81d560de3a4eba79",
|
||||
"sha256": "e1dd86693e9b002afa7fa2921010e2d9909b94492dcd0d957165e46304c5bcdb",
|
||||
"os": "darwin",
|
||||
"arch": "universal",
|
||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis.dmg",
|
||||
|
||||
Vendored
+7
-7
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"version": "2.2.1",
|
||||
"notes": "Универсальный macOS DMG (Intel + Apple Silicon), скрипт автоустановки, предупреждения при неверной архитектуре",
|
||||
"version": "2.6.0",
|
||||
"notes": "Исправление AmneziaWG: ключи в awg://KEY@host, JSON/vpn://, пробелы в полях; без автоподключения по умолчанию",
|
||||
"platform": "windows-amd64",
|
||||
"os": "windows",
|
||||
"arch": "amd64",
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "993e17d6390c8a4378e51016012006f18db1f6bd7c25957a4bf82fe31ab63473",
|
||||
"sha256": "5d7d262c7804779aff8ccc4ef48ed3b280eb460ead3cc95386added909fb3fb2",
|
||||
"mandatory": false,
|
||||
"platforms": {
|
||||
"windows-amd64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "993e17d6390c8a4378e51016012006f18db1f6bd7c25957a4bf82fe31ab63473",
|
||||
"sha256": "5d7d262c7804779aff8ccc4ef48ed3b280eb460ead3cc95386added909fb3fb2",
|
||||
"os": "windows",
|
||||
"arch": "amd64"
|
||||
},
|
||||
"darwin-arm64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
|
||||
"sha256": "b1589bffebb7cbeee17ca5a40cedb6ca614a3580be22488505a1403e1021c2b6",
|
||||
"sha256": "31ae3cca6cdfd9239b1812251a2c55e7c8ea879ae543020d1ca7f4138585a0b8",
|
||||
"os": "darwin",
|
||||
"arch": "arm64",
|
||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"darwin-amd64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
|
||||
"sha256": "149258fbad816aa2e73384da492bc91e3bf6700d4d01d55c9f35a3f68998232c",
|
||||
"sha256": "d4ed12f647c2ce87a266999f73c66a54873eb9bd77f75dc49d5cb4f36f94aff1",
|
||||
"os": "darwin",
|
||||
"arch": "amd64",
|
||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg",
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"darwin-universal": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis",
|
||||
"sha256": "37ceb0e47cd542dadcd406cedecc760d9aa71a40f0b109f81d560de3a4eba79",
|
||||
"sha256": "e1dd86693e9b002afa7fa2921010e2d9909b94492dcd0d957165e46304c5bcdb",
|
||||
"os": "darwin",
|
||||
"arch": "universal",
|
||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis.dmg",
|
||||
|
||||
@@ -725,7 +725,7 @@
|
||||
<div class="brand-wrap">
|
||||
<div class="brand-row">
|
||||
<h1 class="brand">Navis</h1>
|
||||
<span class="badge-ver">2.2.1</span>
|
||||
<span class="badge-ver">2.6</span>
|
||||
</div>
|
||||
<p class="tagline">Быстрый клиент · Naive · Hy2 · AWG · Xray</p>
|
||||
</div>
|
||||
@@ -756,7 +756,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<label class="auto-best">
|
||||
<input id="autoBest" type="checkbox" checked />
|
||||
<input id="autoBest" type="checkbox" />
|
||||
Авто: пинг + подключение к лучшему
|
||||
</label>
|
||||
<div class="server-list" id="serverList"></div>
|
||||
@@ -923,8 +923,7 @@
|
||||
|
||||
try {
|
||||
const saved = localStorage.getItem(AUTO_BEST_KEY);
|
||||
if (saved === "0") autoBest.checked = false;
|
||||
if (saved === "1") autoBest.checked = true;
|
||||
autoBest.checked = saved === "1";
|
||||
} catch (_) {}
|
||||
autoBest.addEventListener("change", () => {
|
||||
try { localStorage.setItem(AUTO_BEST_KEY, autoBest.checked ? "1" : "0"); } catch (_) {}
|
||||
|
||||
@@ -15,9 +15,11 @@ func DetectProtocol(raw string) Protocol {
|
||||
case strings.HasPrefix(lower, "trojan://"):
|
||||
return ProtocolTrojan
|
||||
case strings.HasPrefix(lower, "awg://"), strings.HasPrefix(lower, "amneziawg://"),
|
||||
strings.HasPrefix(lower, "wireguard://"):
|
||||
strings.HasPrefix(lower, "wireguard://"), strings.HasPrefix(lower, "vpn://"):
|
||||
return ProtocolAWG
|
||||
case strings.Contains(lower, "[interface]") && strings.Contains(lower, "privatekey"):
|
||||
case strings.Contains(lower, "[interface]") && (strings.Contains(lower, "privatekey") || strings.Contains(lower, "private_key") || strings.Contains(lower, "private key")):
|
||||
return ProtocolAWG
|
||||
case strings.HasPrefix(strings.TrimSpace(raw), "{") && (strings.Contains(lower, "client_priv_key") || strings.Contains(lower, "server_pub_key")):
|
||||
return ProtocolAWG
|
||||
case strings.HasPrefix(lower, "naive+"), strings.HasPrefix(lower, "naive://"),
|
||||
strings.HasPrefix(lower, "https://"), strings.HasPrefix(lower, "quic://"),
|
||||
|
||||
+345
-92
@@ -3,6 +3,7 @@ package awg
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
@@ -12,22 +13,22 @@ import (
|
||||
|
||||
// Conf is a parsed AmneziaWG / WireGuard client configuration (AWG 2.0 fields included).
|
||||
type Conf struct {
|
||||
PrivateKey string
|
||||
Address []string
|
||||
DNS []string
|
||||
MTU int
|
||||
Jc int
|
||||
Jmin int
|
||||
Jmax int
|
||||
PrivateKey string
|
||||
Address []string
|
||||
DNS []string
|
||||
MTU int
|
||||
Jc int
|
||||
Jmin int
|
||||
Jmax int
|
||||
S1, S2, S3, S4 int
|
||||
H1, H2, H3, H4 string
|
||||
I1, I2, I3, I4, I5 string
|
||||
PublicKey string
|
||||
PublicKey string
|
||||
PresharedKey string
|
||||
Endpoint string
|
||||
AllowedIPs []string
|
||||
Keepalive int
|
||||
Name string
|
||||
Endpoint string
|
||||
AllowedIPs []string
|
||||
Keepalive int
|
||||
Name string
|
||||
}
|
||||
|
||||
// Detect reports whether raw looks like an AWG/WireGuard config or share link.
|
||||
@@ -38,26 +39,43 @@ func Detect(raw string) bool {
|
||||
}
|
||||
lower := strings.ToLower(raw)
|
||||
if strings.HasPrefix(lower, "awg://") || strings.HasPrefix(lower, "amneziawg://") ||
|
||||
strings.HasPrefix(lower, "wireguard://") {
|
||||
strings.HasPrefix(lower, "wireguard://") || strings.HasPrefix(lower, "vpn://") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(lower, "[interface]") && strings.Contains(lower, "privatekey") {
|
||||
if strings.Contains(lower, "[interface]") &&
|
||||
(strings.Contains(lower, "privatekey") || strings.Contains(lower, "private_key") || strings.Contains(lower, "private key")) {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(strings.TrimSpace(raw), "{") &&
|
||||
(strings.Contains(lower, "client_priv_key") || strings.Contains(lower, "privatekey") ||
|
||||
strings.Contains(lower, "server_pub_key")) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse accepts a WireGuard/AWG .conf body or awg:// URI and returns Conf.
|
||||
// Parse accepts a WireGuard/AWG .conf body, awg:// URI, vpn:// blob, or Amnezia JSON.
|
||||
func Parse(raw string) (Conf, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return Conf{}, fmt.Errorf("пустой AWG конфиг")
|
||||
}
|
||||
// Strip UTF-8 BOM
|
||||
raw = strings.TrimPrefix(raw, "\ufeff")
|
||||
|
||||
lower := strings.ToLower(raw)
|
||||
switch {
|
||||
case strings.HasPrefix(lower, "vpn://"):
|
||||
return parseVPNScheme(raw)
|
||||
case strings.HasPrefix(lower, "awg://"), strings.HasPrefix(lower, "amneziawg://"),
|
||||
strings.HasPrefix(lower, "wireguard://"):
|
||||
return parseURI(raw)
|
||||
case strings.HasPrefix(raw, "{") || strings.HasPrefix(raw, "["):
|
||||
if c, err := parseAmneziaJSON(raw); err == nil {
|
||||
return c, nil
|
||||
}
|
||||
// fall through to INI if JSON didn't work (rare)
|
||||
return parseINI(raw)
|
||||
default:
|
||||
return parseINI(raw)
|
||||
}
|
||||
@@ -65,78 +83,27 @@ func Parse(raw string) (Conf, error) {
|
||||
|
||||
func parseINI(raw string) (Conf, error) {
|
||||
var c Conf
|
||||
section := ""
|
||||
section := "interface" // treat leading keys before [Interface] as Interface
|
||||
for _, line := range strings.Split(raw, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
section = strings.ToLower(strings.TrimSpace(line[1 : len(line)-1]))
|
||||
section = normalizeKey(line[1 : len(line)-1])
|
||||
continue
|
||||
}
|
||||
key, val, ok := splitKV(line)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
k := strings.ToLower(key)
|
||||
k := normalizeKey(key)
|
||||
val = unwrapQuotes(val)
|
||||
switch section {
|
||||
case "interface":
|
||||
switch k {
|
||||
case "privatekey":
|
||||
c.PrivateKey = val
|
||||
case "address":
|
||||
c.Address = appendCSV(c.Address, val)
|
||||
case "dns":
|
||||
c.DNS = appendCSV(c.DNS, val)
|
||||
case "mtu":
|
||||
c.MTU, _ = strconv.Atoi(val)
|
||||
case "jc":
|
||||
c.Jc, _ = strconv.Atoi(val)
|
||||
case "jmin":
|
||||
c.Jmin, _ = strconv.Atoi(val)
|
||||
case "jmax":
|
||||
c.Jmax, _ = strconv.Atoi(val)
|
||||
case "s1":
|
||||
c.S1, _ = strconv.Atoi(val)
|
||||
case "s2":
|
||||
c.S2, _ = strconv.Atoi(val)
|
||||
case "s3":
|
||||
c.S3, _ = strconv.Atoi(val)
|
||||
case "s4":
|
||||
c.S4, _ = strconv.Atoi(val)
|
||||
case "h1":
|
||||
c.H1 = val
|
||||
case "h2":
|
||||
c.H2 = val
|
||||
case "h3":
|
||||
c.H3 = val
|
||||
case "h4":
|
||||
c.H4 = val
|
||||
case "i1":
|
||||
c.I1 = val
|
||||
case "i2":
|
||||
c.I2 = val
|
||||
case "i3":
|
||||
c.I3 = val
|
||||
case "i4":
|
||||
c.I4 = val
|
||||
case "i5":
|
||||
c.I5 = val
|
||||
}
|
||||
case "interface", "":
|
||||
applyInterfaceField(&c, k, val)
|
||||
case "peer":
|
||||
switch k {
|
||||
case "publickey":
|
||||
c.PublicKey = val
|
||||
case "presharedkey":
|
||||
c.PresharedKey = val
|
||||
case "endpoint":
|
||||
c.Endpoint = val
|
||||
case "allowedips":
|
||||
c.AllowedIPs = appendCSV(c.AllowedIPs, val)
|
||||
case "persistentkeepalive":
|
||||
c.Keepalive, _ = strconv.Atoi(val)
|
||||
}
|
||||
applyPeerField(&c, k, val)
|
||||
}
|
||||
}
|
||||
if err := c.validate(); err != nil {
|
||||
@@ -145,9 +112,77 @@ func parseINI(raw string) (Conf, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func applyInterfaceField(c *Conf, k, val string) {
|
||||
switch k {
|
||||
case "privatekey", "clientprivkey", "clientprivatekey":
|
||||
c.PrivateKey = val
|
||||
case "address", "addr", "clientip":
|
||||
c.Address = appendCSV(c.Address, val)
|
||||
case "dns":
|
||||
c.DNS = appendCSV(c.DNS, val)
|
||||
case "mtu":
|
||||
c.MTU, _ = strconv.Atoi(val)
|
||||
case "jc":
|
||||
c.Jc, _ = strconv.Atoi(val)
|
||||
case "jmin":
|
||||
c.Jmin, _ = strconv.Atoi(val)
|
||||
case "jmax":
|
||||
c.Jmax, _ = strconv.Atoi(val)
|
||||
case "s1":
|
||||
c.S1, _ = strconv.Atoi(val)
|
||||
case "s2":
|
||||
c.S2, _ = strconv.Atoi(val)
|
||||
case "s3":
|
||||
c.S3, _ = strconv.Atoi(val)
|
||||
case "s4":
|
||||
c.S4, _ = strconv.Atoi(val)
|
||||
case "h1":
|
||||
c.H1 = val
|
||||
case "h2":
|
||||
c.H2 = val
|
||||
case "h3":
|
||||
c.H3 = val
|
||||
case "h4":
|
||||
c.H4 = val
|
||||
case "i1":
|
||||
c.I1 = val
|
||||
case "i2":
|
||||
c.I2 = val
|
||||
case "i3":
|
||||
c.I3 = val
|
||||
case "i4":
|
||||
c.I4 = val
|
||||
case "i5":
|
||||
c.I5 = val
|
||||
}
|
||||
}
|
||||
|
||||
func applyPeerField(c *Conf, k, val string) {
|
||||
switch k {
|
||||
case "publickey", "serverpubkey", "serverpublickey", "peerpublickey", "peerkey":
|
||||
c.PublicKey = val
|
||||
case "presharedkey", "psk", "pskkey":
|
||||
c.PresharedKey = val
|
||||
case "endpoint", "host", "hostname":
|
||||
c.Endpoint = val
|
||||
case "allowedips", "allowedip":
|
||||
c.AllowedIPs = appendCSV(c.AllowedIPs, val)
|
||||
case "persistentkeepalive", "keepalive":
|
||||
c.Keepalive, _ = strconv.Atoi(val)
|
||||
}
|
||||
}
|
||||
|
||||
func parseURI(raw string) (Conf, error) {
|
||||
// awg://host:port/?public_key=...&private_key=...&address=10.8.0.2/32&jc=4&...
|
||||
// Formats:
|
||||
// awg://host:port/?private_key=...&public_key=...&address=10.8.0.2/32
|
||||
// awg://PRIVATE_KEY@host:port/?public_key=...&address=...
|
||||
// awg://host:port/#name (query keys)
|
||||
u := raw
|
||||
fragment := ""
|
||||
if i := strings.Index(u, "#"); i >= 0 {
|
||||
fragment = u[i+1:]
|
||||
u = u[:i]
|
||||
}
|
||||
schemeIdx := strings.Index(u, "://")
|
||||
if schemeIdx < 0 {
|
||||
return Conf{}, fmt.Errorf("bad awg uri")
|
||||
@@ -160,7 +195,22 @@ func parseURI(raw string) (Conf, error) {
|
||||
query = rest[i+1:]
|
||||
}
|
||||
hostPort = strings.TrimSuffix(hostPort, "/")
|
||||
c := Conf{Endpoint: hostPort}
|
||||
|
||||
c := Conf{}
|
||||
if frag, err := urlDecode(fragment); err == nil && frag != "" {
|
||||
c.Name = frag
|
||||
}
|
||||
|
||||
// userinfo = private key (WireGuard share-link style)
|
||||
if at := strings.LastIndex(hostPort, "@"); at >= 0 {
|
||||
pk, _ := urlDecode(hostPort[:at])
|
||||
c.PrivateKey = unwrapQuotes(pk)
|
||||
hostPort = hostPort[at+1:]
|
||||
}
|
||||
if hostPort != "" {
|
||||
c.Endpoint = hostPort
|
||||
}
|
||||
|
||||
for _, part := range strings.Split(query, "&") {
|
||||
if part == "" {
|
||||
continue
|
||||
@@ -169,21 +219,21 @@ func parseURI(raw string) (Conf, error) {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
k = strings.ToLower(strings.TrimSpace(k))
|
||||
v = strings.TrimSpace(v)
|
||||
k = normalizeKey(k)
|
||||
v = unwrapQuotes(strings.TrimSpace(v))
|
||||
v, _ = urlDecode(v)
|
||||
switch k {
|
||||
case "private_key", "privatekey":
|
||||
case "privatekey", "clientprivkey", "clientprivatekey":
|
||||
c.PrivateKey = v
|
||||
case "public_key", "publickey":
|
||||
case "publickey", "serverpubkey", "serverpublickey", "peerpublickey", "peerkey":
|
||||
c.PublicKey = v
|
||||
case "preshared_key", "presharedkey", "psk":
|
||||
case "presharedkey", "psk", "pskkey":
|
||||
c.PresharedKey = v
|
||||
case "address", "addr":
|
||||
case "address", "addr", "clientip":
|
||||
c.Address = appendCSV(c.Address, v)
|
||||
case "dns":
|
||||
c.DNS = appendCSV(c.DNS, v)
|
||||
case "allowed_ips", "allowedips":
|
||||
case "allowedips", "allowedip":
|
||||
c.AllowedIPs = appendCSV(c.AllowedIPs, v)
|
||||
case "mtu":
|
||||
c.MTU, _ = strconv.Atoi(v)
|
||||
@@ -223,8 +273,12 @@ func parseURI(raw string) (Conf, error) {
|
||||
c.I5 = v
|
||||
case "name", "remark":
|
||||
c.Name = v
|
||||
case "endpoint":
|
||||
case "endpoint", "host", "hostname":
|
||||
c.Endpoint = v
|
||||
case "port":
|
||||
if c.Endpoint != "" && !strings.Contains(c.Endpoint, ":") {
|
||||
c.Endpoint = c.Endpoint + ":" + v
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := c.validate(); err != nil {
|
||||
@@ -233,12 +287,137 @@ func parseURI(raw string) (Conf, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func parseVPNScheme(raw string) (Conf, error) {
|
||||
payload := strings.TrimSpace(raw[len("vpn://"):])
|
||||
payload = strings.TrimSuffix(payload, "/")
|
||||
decoded, err := decodeMaybeBase64(payload)
|
||||
if err != nil {
|
||||
return Conf{}, fmt.Errorf("awg vpn://: %w", err)
|
||||
}
|
||||
if c, err := parseAmneziaJSON(decoded); err == nil {
|
||||
return c, nil
|
||||
}
|
||||
// nested: sometimes JSON has last_config string with INI or JSON
|
||||
var wrap map[string]any
|
||||
if err := json.Unmarshal([]byte(decoded), &wrap); err == nil {
|
||||
for _, key := range []string{"last_config", "config", "cfg", "awg_config", "wireguard"} {
|
||||
if s, ok := wrap[key].(string); ok && strings.TrimSpace(s) != "" {
|
||||
if c, err := Parse(s); err == nil {
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Parse(decoded)
|
||||
}
|
||||
|
||||
func parseAmneziaJSON(raw string) (Conf, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal([]byte(raw), &m); err != nil {
|
||||
return Conf{}, err
|
||||
}
|
||||
// Flatten nested last_config JSON string.
|
||||
if lc, ok := m["last_config"].(string); ok && strings.TrimSpace(lc) != "" {
|
||||
var nested map[string]any
|
||||
if json.Unmarshal([]byte(lc), &nested) == nil {
|
||||
for k, v := range nested {
|
||||
if _, exists := m[k]; !exists {
|
||||
m[k] = v
|
||||
}
|
||||
}
|
||||
} else if c, err := Parse(lc); err == nil {
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
|
||||
str := func(keys ...string) string {
|
||||
for _, k := range keys {
|
||||
for mk, mv := range m {
|
||||
if normalizeKey(mk) != normalizeKey(k) {
|
||||
continue
|
||||
}
|
||||
switch t := mv.(type) {
|
||||
case string:
|
||||
return unwrapQuotes(strings.TrimSpace(t))
|
||||
case float64:
|
||||
return strconv.FormatInt(int64(t), 10)
|
||||
case json.Number:
|
||||
return t.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
atoi := func(keys ...string) int {
|
||||
s := str(keys...)
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
c := Conf{
|
||||
PrivateKey: str("client_priv_key", "privateKey", "private_key", "PrivateKey"),
|
||||
PublicKey: str("server_pub_key", "publicKey", "public_key", "PublicKey", "peer_public_key"),
|
||||
PresharedKey: str("psk_key", "presharedKey", "preshared_key", "PresharedKey"),
|
||||
Jc: atoi("Jc", "jc"),
|
||||
Jmin: atoi("Jmin", "jmin"),
|
||||
Jmax: atoi("Jmax", "jmax"),
|
||||
S1: atoi("S1", "s1"),
|
||||
S2: atoi("S2", "s2"),
|
||||
S3: atoi("S3", "s3"),
|
||||
S4: atoi("S4", "s4"),
|
||||
H1: str("H1", "h1"),
|
||||
H2: str("H2", "h2"),
|
||||
H3: str("H3", "h3"),
|
||||
H4: str("H4", "h4"),
|
||||
I1: str("I1", "i1"),
|
||||
I2: str("I2", "i2"),
|
||||
I3: str("I3", "i3"),
|
||||
I4: str("I4", "i4"),
|
||||
I5: str("I5", "i5"),
|
||||
MTU: atoi("mtu", "MTU"),
|
||||
Keepalive: atoi("persistent_keep_alive", "PersistentKeepalive", "keepalive"),
|
||||
Name: str("name", "description", "hostName"),
|
||||
}
|
||||
if addr := str("client_ip", "address", "Address"); addr != "" {
|
||||
c.Address = appendCSV(nil, addr)
|
||||
}
|
||||
if dns := str("dns1", "dns", "DNS"); dns != "" {
|
||||
c.DNS = appendCSV(nil, dns)
|
||||
}
|
||||
if dns2 := str("dns2"); dns2 != "" {
|
||||
c.DNS = appendCSV(c.DNS, dns2)
|
||||
}
|
||||
host := str("hostName", "host", "endpoint_host", "server")
|
||||
port := str("port", "endpoint_port")
|
||||
endpoint := str("endpoint", "Endpoint")
|
||||
switch {
|
||||
case endpoint != "":
|
||||
c.Endpoint = endpoint
|
||||
case host != "" && port != "":
|
||||
c.Endpoint = net.JoinHostPort(trimBrackets(host), port)
|
||||
case host != "":
|
||||
c.Endpoint = host
|
||||
}
|
||||
if allowed := str("allowed_ips", "AllowedIPs"); allowed != "" {
|
||||
c.AllowedIPs = appendCSV(nil, allowed)
|
||||
}
|
||||
if err := c.validate(); err != nil {
|
||||
return Conf{}, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Conf) validate() error {
|
||||
c.PrivateKey = unwrapQuotes(strings.TrimSpace(c.PrivateKey))
|
||||
c.PublicKey = unwrapQuotes(strings.TrimSpace(c.PublicKey))
|
||||
c.PresharedKey = unwrapQuotes(strings.TrimSpace(c.PresharedKey))
|
||||
c.Endpoint = strings.TrimSpace(c.Endpoint)
|
||||
if c.PrivateKey == "" {
|
||||
return fmt.Errorf("awg: нет PrivateKey")
|
||||
return fmt.Errorf("awg: нет PrivateKey (приватный ключ клиента)")
|
||||
}
|
||||
if c.PublicKey == "" {
|
||||
return fmt.Errorf("awg: нет PublicKey")
|
||||
return fmt.Errorf("awg: нет PublicKey (публичный ключ сервера)")
|
||||
}
|
||||
if c.Endpoint == "" {
|
||||
return fmt.Errorf("awg: нет Endpoint")
|
||||
@@ -260,7 +439,6 @@ func HostPort(raw string) (host, port string, err error) {
|
||||
}
|
||||
h, p, err := net.SplitHostPort(c.Endpoint)
|
||||
if err != nil {
|
||||
// bare host without port
|
||||
return c.Endpoint, "51820", nil
|
||||
}
|
||||
return h, p, nil
|
||||
@@ -368,14 +546,37 @@ func (c Conf) EffectiveMTU() int {
|
||||
}
|
||||
|
||||
func decodeKey(s string) ([]byte, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if b, err := base64.StdEncoding.DecodeString(s); err == nil && len(b) == 32 {
|
||||
return b, nil
|
||||
s = unwrapQuotes(strings.TrimSpace(s))
|
||||
if s == "" {
|
||||
return nil, fmt.Errorf("пустой ключ")
|
||||
}
|
||||
encodings := []*base64.Encoding{
|
||||
base64.StdEncoding,
|
||||
base64.RawStdEncoding,
|
||||
base64.URLEncoding,
|
||||
base64.RawURLEncoding,
|
||||
}
|
||||
for _, enc := range encodings {
|
||||
if b, err := enc.DecodeString(s); err == nil && len(b) == 32 {
|
||||
return b, nil
|
||||
}
|
||||
}
|
||||
// pad std / url
|
||||
pad := func(in string) string {
|
||||
if m := len(in) % 4; m != 0 {
|
||||
return in + strings.Repeat("=", 4-m)
|
||||
}
|
||||
return in
|
||||
}
|
||||
for _, enc := range []*base64.Encoding{base64.StdEncoding, base64.URLEncoding} {
|
||||
if b, err := enc.DecodeString(pad(s)); err == nil && len(b) == 32 {
|
||||
return b, nil
|
||||
}
|
||||
}
|
||||
if b, err := hex.DecodeString(s); err == nil && len(b) == 32 {
|
||||
return b, nil
|
||||
}
|
||||
return nil, fmt.Errorf("expected 32-byte base64/hex key")
|
||||
return nil, fmt.Errorf("ожидался ключ WireGuard (32 байта base64/hex)")
|
||||
}
|
||||
|
||||
func writeInt(b *strings.Builder, key string, v int) {
|
||||
@@ -417,6 +618,58 @@ func appendCSV(dst []string, raw string) []string {
|
||||
return dst
|
||||
}
|
||||
|
||||
func normalizeKey(s string) string {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
var b strings.Builder
|
||||
for _, r := range s {
|
||||
if r == '_' || r == '-' || r == ' ' || r == '\t' {
|
||||
continue
|
||||
}
|
||||
b.WriteRune(r)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func unwrapQuotes(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if len(s) >= 2 {
|
||||
if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') {
|
||||
return strings.TrimSpace(s[1 : len(s)-1])
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func trimBrackets(host string) string {
|
||||
host = strings.TrimSpace(host)
|
||||
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
|
||||
return host[1 : len(host)-1]
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
func decodeMaybeBase64(s string) (string, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
encodings := []*base64.Encoding{
|
||||
base64.StdEncoding,
|
||||
base64.RawStdEncoding,
|
||||
base64.URLEncoding,
|
||||
base64.RawURLEncoding,
|
||||
}
|
||||
pad := func(in string) string {
|
||||
if m := len(in) % 4; m != 0 {
|
||||
return in + strings.Repeat("=", 4-m)
|
||||
}
|
||||
return in
|
||||
}
|
||||
for _, enc := range encodings {
|
||||
if b, err := enc.DecodeString(pad(s)); err == nil && len(b) > 0 {
|
||||
return string(b), nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("не base64")
|
||||
}
|
||||
|
||||
func urlDecode(s string) (string, error) {
|
||||
var b strings.Builder
|
||||
for i := 0; i < len(s); {
|
||||
@@ -465,10 +718,10 @@ func NormalizeShareLink(raw string) (normalized string, remark string, err error
|
||||
} else {
|
||||
remark = c.Endpoint
|
||||
}
|
||||
// Prefer keeping original INI when pasted; re-serialize only for URI imports.
|
||||
lower := strings.ToLower(strings.TrimSpace(raw))
|
||||
if strings.HasPrefix(lower, "awg://") || strings.HasPrefix(lower, "amneziawg://") ||
|
||||
strings.HasPrefix(lower, "wireguard://") {
|
||||
strings.HasPrefix(lower, "wireguard://") || strings.HasPrefix(lower, "vpn://") ||
|
||||
strings.HasPrefix(strings.TrimSpace(raw), "{") {
|
||||
return serializeINI(c), remark, nil
|
||||
}
|
||||
return strings.TrimSpace(raw), remark, nil
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package awg
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseINI_AWG20(t *testing.T) {
|
||||
raw := `[Interface]
|
||||
@@ -41,6 +44,80 @@ PersistentKeepalive = 25
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseURI_UserInfoPrivateKey(t *testing.T) {
|
||||
priv := "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||
pub := "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE="
|
||||
raw := "awg://" + priv + "@203.0.113.10:51820/?public_key=" + pub + "&address=10.8.0.2/32&jc=4"
|
||||
c, err := Parse(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.PrivateKey != priv {
|
||||
t.Fatalf("private key: %q", c.PrivateKey)
|
||||
}
|
||||
if c.PublicKey != pub {
|
||||
t.Fatalf("public key: %q", c.PublicKey)
|
||||
}
|
||||
if c.Endpoint != "203.0.113.10:51820" {
|
||||
t.Fatalf("endpoint: %q", c.Endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseINI_SpacedKeysAndQuotes(t *testing.T) {
|
||||
raw := `Private Key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||
Address = 10.8.0.2/32
|
||||
[Peer]
|
||||
Public Key = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE='
|
||||
Endpoint = 1.2.3.4:51820
|
||||
`
|
||||
c, err := Parse(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.PrivateKey == "" || c.PublicKey == "" {
|
||||
t.Fatalf("keys missing: %+v", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAmneziaJSON(t *testing.T) {
|
||||
raw := `{
|
||||
"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"
|
||||
}`
|
||||
c, err := Parse(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.Endpoint != "203.0.113.10:51820" {
|
||||
t.Fatalf("endpoint %q", c.Endpoint)
|
||||
}
|
||||
if c.Jc != 4 {
|
||||
t.Fatalf("jc %d", c.Jc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVPNScheme(t *testing.T) {
|
||||
inner := `{"client_priv_key":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","server_pub_key":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=","client_ip":"10.8.0.2/32","hostName":"9.9.9.9","port":"51820"}`
|
||||
raw := "vpn://" + base64.StdEncoding.EncodeToString([]byte(inner))
|
||||
c, err := Parse(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.Endpoint != "9.9.9.9:51820" {
|
||||
t.Fatalf("endpoint %q", c.Endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetect(t *testing.T) {
|
||||
if !Detect("[Interface]\nPrivateKey = x") {
|
||||
t.Fatal("expected detect")
|
||||
@@ -48,6 +125,18 @@ func TestDetect(t *testing.T) {
|
||||
if !Detect("awg://1.2.3.4:51820/?private_key=a&public_key=b&address=10.0.0.2/32") {
|
||||
t.Fatal("expected awg uri detect")
|
||||
}
|
||||
if !Detect(`{"client_priv_key":"x","server_pub_key":"y"}`) {
|
||||
t.Fatal("expected json detect")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeKeyURLSafe(t *testing.T) {
|
||||
// 32 zero bytes, URL-safe without padding
|
||||
raw := base64.RawURLEncoding.EncodeToString(make([]byte, 32))
|
||||
b, err := decodeKey(raw)
|
||||
if err != nil || len(b) != 32 {
|
||||
t.Fatalf("url-safe: %v %d", err, len(b))
|
||||
}
|
||||
}
|
||||
|
||||
func containsAll(s string, parts ...string) bool {
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
// CurrentVersion is the shipped client version.
|
||||
const CurrentVersion = "2.2.1"
|
||||
const CurrentVersion = "2.6.0"
|
||||
|
||||
// DefaultManifestURL is the update feed (hosted in the project git repo).
|
||||
const DefaultManifestURL = "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json"
|
||||
|
||||
+7
-7
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"version": "2.2.1",
|
||||
"notes": "Универсальный macOS DMG (Intel + Apple Silicon), скрипт автоустановки, предупреждения при неверной архитектуре",
|
||||
"version": "2.6.0",
|
||||
"notes": "Исправление AmneziaWG: ключи в awg://KEY@host, JSON/vpn://, пробелы в полях; без автоподключения по умолчанию",
|
||||
"platform": "windows-amd64",
|
||||
"os": "windows",
|
||||
"arch": "amd64",
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "993e17d6390c8a4378e51016012006f18db1f6bd7c25957a4bf82fe31ab63473",
|
||||
"sha256": "5d7d262c7804779aff8ccc4ef48ed3b280eb460ead3cc95386added909fb3fb2",
|
||||
"mandatory": false,
|
||||
"platforms": {
|
||||
"windows-amd64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "993e17d6390c8a4378e51016012006f18db1f6bd7c25957a4bf82fe31ab63473",
|
||||
"sha256": "5d7d262c7804779aff8ccc4ef48ed3b280eb460ead3cc95386added909fb3fb2",
|
||||
"os": "windows",
|
||||
"arch": "amd64"
|
||||
},
|
||||
"darwin-arm64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
|
||||
"sha256": "b1589bffebb7cbeee17ca5a40cedb6ca614a3580be22488505a1403e1021c2b6",
|
||||
"sha256": "31ae3cca6cdfd9239b1812251a2c55e7c8ea879ae543020d1ca7f4138585a0b8",
|
||||
"os": "darwin",
|
||||
"arch": "arm64",
|
||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"darwin-amd64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
|
||||
"sha256": "149258fbad816aa2e73384da492bc91e3bf6700d4d01d55c9f35a3f68998232c",
|
||||
"sha256": "d4ed12f647c2ce87a266999f73c66a54873eb9bd77f75dc49d5cb4f36f94aff1",
|
||||
"os": "darwin",
|
||||
"arch": "amd64",
|
||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg",
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"darwin-universal": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis",
|
||||
"sha256": "37ceb0e47cd542dadcd406cedecc760d9aa71a40f0b109f81d560de3a4eba79",
|
||||
"sha256": "e1dd86693e9b002afa7fa2921010e2d9909b94492dcd0d957165e46304c5bcdb",
|
||||
"os": "darwin",
|
||||
"arch": "universal",
|
||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis.dmg",
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"FixedFileInfo": {
|
||||
"FileVersion": { "Major": 2, "Minor": 2, "Patch": 1, "Build": 0 },
|
||||
"ProductVersion": { "Major": 2, "Minor": 2, "Patch": 1, "Build": 0 },
|
||||
"FileVersion": { "Major": 2, "Minor": 6, "Patch": 0, "Build": 0 },
|
||||
"ProductVersion": { "Major": 2, "Minor": 6, "Patch": 0, "Build": 0 },
|
||||
"FileFlagsMask": "3f",
|
||||
"FileFlags": "00",
|
||||
"FileOS": "40004",
|
||||
@@ -11,12 +11,12 @@
|
||||
"StringFileInfo": {
|
||||
"CompanyName": "EvilFox",
|
||||
"FileDescription": "Navis 2 — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
|
||||
"FileVersion": "2.2.1.0",
|
||||
"FileVersion": "2.6.0.0",
|
||||
"InternalName": "Navis",
|
||||
"LegalCopyright": "Copyright (c) EvilFox",
|
||||
"OriginalFilename": "Navis.exe",
|
||||
"ProductName": "Navis",
|
||||
"ProductVersion": "2.2.1.0",
|
||||
"ProductVersion": "2.6.0.0",
|
||||
"Comments": "Open-source VPN/proxy client. https://evilfox.win/"
|
||||
},
|
||||
"VarFileInfo": {
|
||||
|
||||
Reference in New Issue
Block a user