diff --git a/README.md b/README.md index cb0a5d7..a7c3799 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 4ecaead..f96962d 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -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") diff --git a/android/app/src/main/java/win/evilfox/navis/data/ProfileStore.kt b/android/app/src/main/java/win/evilfox/navis/data/ProfileStore.kt index d821a68..452e456 100644 --- a/android/app/src/main/java/win/evilfox/navis/data/ProfileStore.kt +++ b/android/app/src/main/java/win/evilfox/navis/data/ProfileStore.kt @@ -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 = withContext(Dispatchers.IO) { diff --git a/android/app/src/main/java/win/evilfox/navis/ui/MainViewModel.kt b/android/app/src/main/java/win/evilfox/navis/ui/MainViewModel.kt index 6d11ab5..6f58372 100644 --- a/android/app/src/main/java/win/evilfox/navis/ui/MainViewModel.kt +++ b/android/app/src/main/java/win/evilfox/navis/ui/MainViewModel.kt @@ -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, diff --git a/android/app/src/main/java/win/evilfox/navis/ui/NavisScreen.kt b/android/app/src/main/java/win/evilfox/navis/ui/NavisScreen.kt index 4cc8dcb..2b2325c 100644 --- a/android/app/src/main/java/win/evilfox/navis/ui/NavisScreen.kt +++ b/android/app/src/main/java/win/evilfox/navis/ui/NavisScreen.kt @@ -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) diff --git a/build-macos.bat b/build-macos.bat index c1b6069..e42d336 100644 --- a/build-macos.bat +++ b/build-macos.bat @@ -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: diff --git a/dist/navis-release/Navis.exe b/dist/navis-release/Navis.exe index e7a994b..3023e2b 100644 Binary files a/dist/navis-release/Navis.exe and b/dist/navis-release/Navis.exe differ diff --git a/dist/navis-release/darwin-amd64/Navis b/dist/navis-release/darwin-amd64/Navis index 3806f77..100a903 100644 Binary files a/dist/navis-release/darwin-amd64/Navis and b/dist/navis-release/darwin-amd64/Navis differ diff --git a/dist/navis-release/darwin-amd64/Navis.app.zip b/dist/navis-release/darwin-amd64/Navis.app.zip index 1c03fc7..92abc89 100644 Binary files a/dist/navis-release/darwin-amd64/Navis.app.zip and b/dist/navis-release/darwin-amd64/Navis.app.zip differ diff --git a/dist/navis-release/darwin-amd64/Navis.dmg b/dist/navis-release/darwin-amd64/Navis.dmg index 90fb6ac..953f27a 100644 Binary files a/dist/navis-release/darwin-amd64/Navis.dmg and b/dist/navis-release/darwin-amd64/Navis.dmg differ diff --git a/dist/navis-release/darwin-arm64/Navis b/dist/navis-release/darwin-arm64/Navis index 8a36804..4711e8e 100644 Binary files a/dist/navis-release/darwin-arm64/Navis and b/dist/navis-release/darwin-arm64/Navis differ diff --git a/dist/navis-release/darwin-arm64/Navis.app.zip b/dist/navis-release/darwin-arm64/Navis.app.zip index 4708563..6fc6880 100644 Binary files a/dist/navis-release/darwin-arm64/Navis.app.zip and b/dist/navis-release/darwin-arm64/Navis.app.zip differ diff --git a/dist/navis-release/darwin-arm64/Navis.dmg b/dist/navis-release/darwin-arm64/Navis.dmg index c71b32e..5a86492 100644 Binary files a/dist/navis-release/darwin-arm64/Navis.dmg and b/dist/navis-release/darwin-arm64/Navis.dmg differ diff --git a/dist/navis-release/darwin-universal/Navis b/dist/navis-release/darwin-universal/Navis index b4ad106..90e8557 100644 Binary files a/dist/navis-release/darwin-universal/Navis and b/dist/navis-release/darwin-universal/Navis differ diff --git a/dist/navis-release/darwin-universal/Navis.app.zip b/dist/navis-release/darwin-universal/Navis.app.zip index 2dc7608..5c45b37 100644 Binary files a/dist/navis-release/darwin-universal/Navis.app.zip and b/dist/navis-release/darwin-universal/Navis.app.zip differ diff --git a/dist/navis-release/darwin-universal/Navis.dmg b/dist/navis-release/darwin-universal/Navis.dmg index d18157d..e0cb43f 100644 Binary files a/dist/navis-release/darwin-universal/Navis.dmg and b/dist/navis-release/darwin-universal/Navis.dmg differ diff --git a/dist/navis-release/update.json b/dist/navis-release/update.json index e258e40..f7d6f8e 100644 --- a/dist/navis-release/update.json +++ b/dist/navis-release/update.json @@ -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", diff --git a/dist/update.json b/dist/update.json index e258e40..f7d6f8e 100644 --- a/dist/update.json +++ b/dist/update.json @@ -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", diff --git a/internal/appui/index.html b/internal/appui/index.html index 3f55ffc..ab58051 100644 --- a/internal/appui/index.html +++ b/internal/appui/index.html @@ -725,7 +725,7 @@

Navis

- 2.2.1 + 2.6

Быстрый клиент · Naive · Hy2 · AWG · Xray

@@ -756,7 +756,7 @@
@@ -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 (_) {} diff --git a/internal/config/detect.go b/internal/config/detect.go index 5c98907..c14a4c7 100644 --- a/internal/config/detect.go +++ b/internal/config/detect.go @@ -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://"), diff --git a/internal/protocols/awg/conf.go b/internal/protocols/awg/conf.go index cadd08c..f455d5f 100644 --- a/internal/protocols/awg/conf.go +++ b/internal/protocols/awg/conf.go @@ -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 diff --git a/internal/protocols/awg/conf_test.go b/internal/protocols/awg/conf_test.go index 22cd8d8..a5c1202 100644 --- a/internal/protocols/awg/conf_test.go +++ b/internal/protocols/awg/conf_test.go @@ -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 { diff --git a/internal/update/update.go b/internal/update/update.go index d6a7bdf..d7c81dc 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -17,7 +17,7 @@ import ( ) // CurrentVersion is the shipped client version. -const CurrentVersion = "2.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" diff --git a/server/update.json b/server/update.json index e258e40..f7d6f8e 100644 --- a/server/update.json +++ b/server/update.json @@ -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", diff --git a/versioninfo.json b/versioninfo.json index b07185d..cd6312c 100644 --- a/versioninfo.json +++ b/versioninfo.json @@ -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": {