From 269cbf1349087e6447ca8d65cc30f075fee3f681 Mon Sep 17 00:00:00 2001 From: nerpa Date: Sat, 12 Sep 2026 06:46:58 +0300 Subject: [PATCH] feat: add AmneziaWG support and Russian localization - Add EConfigType.AMNEZIA with dedicated parsing (AmneziaFmt) and outbound generation (settings: Jc, Jmin, Jmax, S1-S4, H1-H4, etc.) - Detect Amnezia vs plain WireGuard configs by inspecting [Interface] parameters during .conf import - Add Russian (ru) translations for `Add` section --- V2rayNG/app/src/main/AndroidManifest.xml | 4 + .../src/main/java/com/v2ray/ang/AppConfig.kt | 1 + .../com/v2ray/ang/core/CoreOutboundBuilder.kt | 41 +++- .../java/com/v2ray/ang/dto/V2rayConfig.kt | 19 +- .../com/v2ray/ang/dto/entities/ProfileItem.kt | 17 +- .../java/com/v2ray/ang/enums/EConfigType.kt | 1 + .../main/java/com/v2ray/ang/fmt/AmneziaFmt.kt | 203 ++++++++++++++++++ .../com/v2ray/ang/handler/AngConfigManager.kt | 44 +++- .../ang/service/RealPingWorkerService.kt | 2 + .../com/v2ray/ang/ui/main/MainActivity.kt | 3 + .../com/v2ray/ang/ui/main/MainImportMenu.kt | 1 + .../ang/ui/server/ServerAmneziaActivity.kt | 174 +++++++++++++++ .../com/v2ray/ang/ui/server/ServerUiState.kt | 77 ++++++- .../app/src/main/res/values-ru/strings.xml | 1 + V2rayNG/app/src/main/res/values/strings.xml | 17 ++ 15 files changed, 591 insertions(+), 14 deletions(-) create mode 100644 V2rayNG/app/src/main/java/com/v2ray/ang/fmt/AmneziaFmt.kt create mode 100644 V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerAmneziaActivity.kt diff --git a/V2rayNG/app/src/main/AndroidManifest.xml b/V2rayNG/app/src/main/AndroidManifest.xml index c8304c4c..a25d41d7 100644 --- a/V2rayNG/app/src/main/AndroidManifest.xml +++ b/V2rayNG/app/src/main/AndroidManifest.xml @@ -107,6 +107,10 @@ android:name=".ui.server.ServerWireguardActivity" android:exported="false" android:windowSoftInputMode="stateUnchanged|adjustResize" /> + toOutboundVless(profileItem) EConfigType.TROJAN -> toOutboundTrojan(profileItem) EConfigType.WIREGUARD -> toOutboundWireguard(profileItem) + EConfigType.AMNEZIA -> toOutboundAmnezia(profileItem) EConfigType.HYSTERIA2 -> toOutboundHysteria2(profileItem) EConfigType.HTTP -> toOutboundHttp(profileItem) else -> null @@ -93,6 +94,7 @@ object CoreOutboundBuilder { streamSettings = OutboundBean.StreamSettingsBean() ) + EConfigType.AMNEZIA, EConfigType.WIREGUARD -> OutboundBean( protocol = configType.name.lowercase(), settings = OutboundBean.OutSettingsBean( @@ -235,9 +237,8 @@ object CoreOutboundBuilder { return outboundBean } - private fun toOutboundWireguard(profileItem: ProfileItem): OutboundBean? { - val outboundBean = createInitOutbound(EConfigType.WIREGUARD) - + // Help function to not make double-init for Wireguard and Amnenzia same fields + private fun fillWireguardSettings(outboundBean: OutboundBean?, profileItem: ProfileItem): OutboundBean? { val rawAddresses = profileItem.localAddress ?.split(",") ?.map { it.trim() } @@ -261,7 +262,8 @@ object CoreOutboundBuilder { peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}" } wireguard.mtu = profileItem.mtu - wireguard.reserved = profileItem.reserved?.takeIf { it.isNotBlank() }?.split(",")?.filter { it.isNotBlank() }?.map { it.trim().toInt() } + wireguard.reserved = profileItem.reserved?.takeIf { it.isNotBlank() } + ?.split(",")?.filter { it.isNotBlank() }?.map { it.trim().toInt() } } if (!profileItem.finalMask.isNullOrBlank()) { @@ -274,6 +276,37 @@ object CoreOutboundBuilder { return outboundBean } + private fun toOutboundWireguard(profileItem: ProfileItem): OutboundBean? { + val outboundBean = createInitOutbound(EConfigType.WIREGUARD) + return fillWireguardSettings(outboundBean, profileItem) + } + + private fun toOutboundAmnezia(profileItem: ProfileItem): OutboundBean? { + val wireguardPart = createInitOutbound(EConfigType.AMNEZIA) + val outboundBean = fillWireguardSettings(wireguardPart, profileItem) + + outboundBean?.settings?.let { amnezia -> + amnezia.jc = profileItem.jc + amnezia.jMin = profileItem.jMin + amnezia.jMax = profileItem.jMax + amnezia.s1 = profileItem.s1 + amnezia.s2 = profileItem.s2 + amnezia.s3 = profileItem.s3 + amnezia.s4 = profileItem.s4 + amnezia.h1 = profileItem.h1 + amnezia.h2 = profileItem.h2 + amnezia.h3 = profileItem.h3 + amnezia.h4 = profileItem.h4 + amnezia.i1 = profileItem.i1 + amnezia.i2 = profileItem.i2 + amnezia.i3 = profileItem.i3 + amnezia.i4 = profileItem.i4 + amnezia.i5 = profileItem.i5 + } + return outboundBean + } + + private fun toOutboundHysteria2(profileItem: ProfileItem): OutboundBean? { val outboundBean = createInitOutbound(EConfigType.HYSTERIA2) ?: return null profileItem.network = NetworkType.HYSTERIA.type diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt index f4876afb..28616830 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt @@ -93,12 +93,29 @@ data class V2rayConfig( var method: String? = null, /*Hysteria/Hysteria2*/ var version: Int? = null, - /*Wireguard*/ + /*Wireguard/Amnezia*/ var secretKey: String? = null, val peers: List? = null, var reserved: List? = null, var mtu: Int? = null, var domainStrategy: String? = null, + /*Amnezia*/ + var jc: Int? = null, + var jMin: Int? = null, + var jMax: Int? = null, + var s1: Int? = null, + var s2: Int? = null, + var s3: Int? = null, + var s4: Int? = null, + var h1: String? = null, + var h2: String? = null, + var h3: String? = null, + var h4: String? = null, + var i1: String? = null, + var i2: String? = null, + var i3: String? = null, + var i4: String? = null, + var i5: String? = null, ) { data class WireGuardBean( var publicKey: String = "", diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/entities/ProfileItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/entities/ProfileItem.kt index 9b2af45e..78c609f2 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/entities/ProfileItem.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/entities/ProfileItem.kt @@ -56,7 +56,22 @@ data class ProfileItem( var localAddress: String? = null, var reserved: String? = null, var mtu: Int? = null, - + var jc: Int? = null, + var jMin: Int? = null, + var jMax: Int? = null, + var s1: Int? = null, + var s2: Int? = null, + var s3: Int? = null, + var s4: Int? = null, + var h1: String? = null, + var h2: String? = null, + var h3: String? = null, + var h4: String? = null, + var i1: String? = null, + var i2: String? = null, + var i3: String? = null, + var i4: String? = null, + var i5: String? = null, var obfsPassword: String? = null, var portHopping: String? = null, var portHoppingInterval: String? = null, diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/enums/EConfigType.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/enums/EConfigType.kt index c390f8d2..8a3787ba 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/enums/EConfigType.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/enums/EConfigType.kt @@ -10,6 +10,7 @@ enum class EConfigType(val value: Int, val protocolScheme: String) { VLESS(5, AppConfig.VLESS), TROJAN(6, AppConfig.TROJAN), WIREGUARD(7, AppConfig.WIREGUARD), + AMNEZIA(11, AppConfig.AMNEZIA), // TUIC(8, AppConfig.TUIC), HYSTERIA2(9, AppConfig.HYSTERIA2), diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/AmneziaFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/AmneziaFmt.kt new file mode 100644 index 00000000..4c94e5bb --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/AmneziaFmt.kt @@ -0,0 +1,203 @@ +package com.v2ray.ang.fmt + +import com.v2ray.ang.AppConfig +import com.v2ray.ang.dto.entities.ProfileItem +import com.v2ray.ang.enums.EConfigType +import com.v2ray.ang.extension.idnHost +import com.v2ray.ang.extension.nullIfBlank +import com.v2ray.ang.extension.removeWhiteSpace +import com.v2ray.ang.util.Utils +import java.net.URI + +object AmneziaFmt : FmtBase() { + /** + * Parses a URI string into a ProfileItem object. + * + * @param str the URI string to parse + * @return the parsed ProfileItem object, or null if parsing fails + */ + fun parse(str: String): ProfileItem? { + val config = ProfileItem.create(EConfigType.AMNEZIA) + + val uri = URI(Utils.fixIllegalUrl(str)) + if (uri.rawQuery.isNullOrEmpty()) return null + val queryParam = getQueryParam(uri) + + config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } + config.server = uri.idnHost + config.serverPort = uri.port.toString() + + config.secretKey = uri.userInfo.orEmpty() + config.localAddress = queryParam["address"] ?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4 + config.publicKey = queryParam["publickey"].orEmpty() + config.preSharedKey = queryParam["presharedkey"]?.nullIfBlank() + config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU) + config.reserved = queryParam["reserved"] ?: "0,0,0" + config.jc = Utils.parseInt(queryParam["jc"].orEmpty()) + config.jMin = Utils.parseInt(queryParam["jmin"].orEmpty()) + config.jMax = Utils.parseInt(queryParam["jmax"].orEmpty()) + config.s1 = Utils.parseInt(queryParam["s1"].orEmpty()) + config.s2 = Utils.parseInt(queryParam["s2"].orEmpty()) + config.s3 = Utils.parseInt(queryParam["s3"].orEmpty()) + config.s4 = Utils.parseInt(queryParam["s4"].orEmpty()) + config.h1 = queryParam["h1"].orEmpty() + config.h2 = queryParam["h2"].orEmpty() + config.h3 = queryParam["h3"].orEmpty() + config.h4 = queryParam["h4"].orEmpty() + config.i1 = queryParam["i1"].orEmpty() + config.i2 = queryParam["i2"].orEmpty() + config.i3 = queryParam["i3"].orEmpty() + config.i4 = queryParam["i4"].orEmpty() + config.i5 = queryParam["i5"].orEmpty() + + return config + } + + /** + * Parses an Amnezia configuration file string into a ProfileItem object. + * + * @param str the Amnezia configuration file string to parse + * @return the parsed ProfileItem object, or null if parsing fails + */ + fun parseAmneziaConfFile(str: String): ProfileItem { + val config = ProfileItem.create(EConfigType.AMNEZIA) + + val interfaceParams: MutableMap = mutableMapOf() + val peerParams: MutableMap = mutableMapOf() + + var currentSection: String? = null + + str.lines().forEach { line -> + val trimmedLine = line.trim() + + if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) { + return@forEach + } + + when { + trimmedLine.startsWith("[Interface]", ignoreCase = true) -> currentSection = "Interface" + trimmedLine.startsWith("[Peer]", ignoreCase = true) -> currentSection = "Peer" + else -> { + if (currentSection != null) { + val parts = trimmedLine.split("=", limit = 2).map { it.trim() } + if (parts.size == 2) { + val key = parts[0].lowercase() + val value = parts[1] + when (currentSection) { + "Interface" -> interfaceParams[key] = value + "Peer" -> peerParams[key] = value + } + } + } + } + } + } + + config.secretKey = interfaceParams["privatekey"].orEmpty() + config.remarks = System.currentTimeMillis().toString() + config.localAddress = interfaceParams["address"] ?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4 + config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU) + config.jc = Utils.parseInt(interfaceParams["jc"].orEmpty()) + config.jMin = Utils.parseInt(interfaceParams["jmin"].orEmpty()) + config.jMax = Utils.parseInt(interfaceParams["jmax"].orEmpty()) + config.s1 = Utils.parseInt(interfaceParams["s1"].orEmpty()) + config.s2 = Utils.parseInt(interfaceParams["s2"].orEmpty()) + config.s3 = Utils.parseInt(interfaceParams["s3"].orEmpty()) + config.s4 = Utils.parseInt(interfaceParams["s4"].orEmpty()) + config.h1 = interfaceParams["h1"].orEmpty() + config.h2 = interfaceParams["h2"].orEmpty() + config.h3 = interfaceParams["h3"].orEmpty() + config.h4 = interfaceParams["h4"].orEmpty() + config.i1 = interfaceParams["i1"].orEmpty() + config.i2 = interfaceParams["i2"].orEmpty() + config.i3 = interfaceParams["i3"].orEmpty() + config.i4 = interfaceParams["i4"].orEmpty() + config.i5 = interfaceParams["i5"].orEmpty() + config.publicKey = peerParams["publickey"].orEmpty() + config.preSharedKey = peerParams["presharedkey"]?.nullIfBlank() + val endpoint = peerParams["endpoint"].orEmpty() + val endpointParts = endpoint.split(":", limit = 2) + if (endpointParts.size == 2) { + config.server = endpointParts[0] + config.serverPort = endpointParts[1] + } else { + config.server = endpoint + config.serverPort = "" + } + config.reserved = peerParams["reserved"] ?: "0,0,0" + + return config + } + + + /** + * Converts a ProfileItem object to a URI string. + * + * @param config the ProfileItem object to convert + * @return the converted URI string + */ + fun toUri(config: ProfileItem): String { + val dicQuery = HashMap() + + dicQuery["publickey"] = config.publicKey.orEmpty() + if (config.reserved != null) { + dicQuery["reserved"] = config.reserved.removeWhiteSpace().orEmpty() + } + dicQuery["address"] = config.localAddress.removeWhiteSpace().orEmpty() + if (config.mtu != null) { + dicQuery["mtu"] = config.mtu.toString() + } + if (config.preSharedKey != null) { + dicQuery["presharedkey"] = config.preSharedKey.removeWhiteSpace().orEmpty() + } + if (config.jc != null) { + dicQuery["jc"] = config.jc.toString() + } + if (config.jMin != null) { + dicQuery["jmin"] = config.jMin.toString() + } + if (config.jMax != null) { + dicQuery["jmax"] = config.jMax.toString() + } + if (config.s1 != null) { + dicQuery["s1"] = config.s1.toString() + } + if (config.s2 != null) { + dicQuery["s2"] = config.s2.toString() + } + if (config.s3 != null) { + dicQuery["s3"] = config.s3.toString() + } + if (config.s4 != null) { + dicQuery["s4"] = config.s4.toString() + } + if (config.h1 != null) { + dicQuery["h1"] = config.h1.removeWhiteSpace().orEmpty() + } + if (config.h2 != null) { + dicQuery["h2"] = config.h2.removeWhiteSpace().orEmpty() + } + if (config.h3 != null) { + dicQuery["h3"] = config.h3.removeWhiteSpace().orEmpty() + } + if (config.h4 != null) { + dicQuery["h4"] = config.h4.removeWhiteSpace().orEmpty() + } + if (config.i1 != null) { + dicQuery["i1"] = config.i1.removeWhiteSpace().orEmpty() + } + if (config.i2 != null) { + dicQuery["i2"] = config.i2.removeWhiteSpace().orEmpty() + } + if (config.i3 != null) { + dicQuery["i3"] = config.i3.removeWhiteSpace().orEmpty() + } + if (config.i4 != null) { + dicQuery["i4"] = config.i4.removeWhiteSpace().orEmpty() + } + if (config.i5 != null) { + dicQuery["i5"] = config.i5.removeWhiteSpace().orEmpty() + } + return toUri(config, config.secretKey, dicQuery) + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt index e790e595..b088f119 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt @@ -12,6 +12,7 @@ import com.v2ray.ang.dto.entities.SubscriptionCache import com.v2ray.ang.dto.entities.SubscriptionItem import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.extension.isNotNullEmpty +import com.v2ray.ang.fmt.AmneziaFmt import com.v2ray.ang.fmt.CustomFmt import com.v2ray.ang.fmt.Hysteria2Fmt import com.v2ray.ang.fmt.ShadowsocksFmt @@ -28,6 +29,39 @@ import com.v2ray.ang.util.QRCodeDecoder import com.v2ray.ang.util.Utils import java.net.URI +object WireguardConfigDetector { + + private val AMNEZIA_MARKER_KEYS = setOf( + "jc", "jmin", "jmax", + "s1", "s2", "s3", "s4", + "h1", "h2", "h3", "h4", + "i1", "i2", "i3", "i4", "i5", + "j1", "j2", "j3" + ) + + /** + * check type of configuration: Amnezia/Wireguard + */ + fun isAmneziaConfig(content: String): Boolean { + val interfaceKeys = mutableSetOf() + var insideInterface = false + + content.lines().forEach { rawLine -> + val line = rawLine.trim() + when { + line.equals("[Interface]", ignoreCase = true) -> insideInterface = true + line.startsWith("[") -> insideInterface = false + insideInterface && line.contains("=") -> { + val key = line.substringBefore("=").trim().lowercase() + interfaceKeys.add(key) + } + } + } + + return interfaceKeys.any { it in AMNEZIA_MARKER_KEYS } + } +} + object AngConfigManager { private data class ParsedProfile( @@ -46,6 +80,7 @@ object AngConfigManager { EConfigType.TROJAN.protocolScheme to TrojanFmt::parse, EConfigType.VLESS.protocolScheme to VlessFmt::parse, EConfigType.WIREGUARD.protocolScheme to WireguardFmt::parse, + EConfigType.AMNEZIA.protocolScheme to AmneziaFmt::parse, EConfigType.HYSTERIA2.protocolScheme to Hysteria2Fmt::parse, AppConfig.HY2 to Hysteria2Fmt::parse, ) @@ -162,6 +197,7 @@ object AngConfigManager { EConfigType.VLESS -> VlessFmt.toUri(config) EConfigType.TROJAN -> TrojanFmt.toUri(config) EConfigType.WIREGUARD -> WireguardFmt.toUri(config) + EConfigType.AMNEZIA -> AmneziaFmt.toUri(config) EConfigType.HYSTERIA2 -> Hysteria2Fmt.toUri(config) else -> {} } @@ -371,7 +407,11 @@ object AngConfigManager { return 0 } else if (server.startsWith("[Interface]") && server.contains("[Peer]")) { try { - val config = WireguardFmt.parseWireguardConfFile(server) + val config = if (WireguardConfigDetector.isAmneziaConfig(server)) { + AmneziaFmt.parseAmneziaConfFile(server) + } else { + WireguardFmt.parseWireguardConfFile(server) + } config.subscriptionId = subid config.description = generateDescription(config) commitProfiles( @@ -383,7 +423,7 @@ object AngConfigManager { } catch (e: ProfileStorageException) { throw e } catch (e: Exception) { - LogUtil.e(AppConfig.TAG, "Failed to parse WireGuard config file", e) + LogUtil.e(AppConfig.TAG, "Failed to parse WireGuard/Amnezia config file", e) } return 0 } else { diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/RealPingWorkerService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/RealPingWorkerService.kt index 65b5a6a4..621304aa 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/RealPingWorkerService.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/RealPingWorkerService.kt @@ -111,6 +111,7 @@ class RealPingWorkerService( if (!config.configType.isComplexType() && config.configType != EConfigType.HYSTERIA2 && config.configType != EConfigType.WIREGUARD + && config.configType != EConfigType.AMNEZIA && config.alpn?.startsWith("h3") != true && config.server.isNotNullEmpty() && config.serverPort?.toIntOrNull() != null @@ -139,6 +140,7 @@ class RealPingWorkerService( if (!config.configType.isComplexType() && config.configType != EConfigType.HYSTERIA2 && config.configType != EConfigType.WIREGUARD + && config.configType != EConfigType.AMNEZIA && config.alpn?.split(',')?.all { it.trim().startsWith("h3") } != true && config.server.isNotNullEmpty() && config.serverPort?.toIntOrNull() != null diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainActivity.kt index f8a178e9..77aab44c 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainActivity.kt @@ -32,6 +32,7 @@ import com.v2ray.ang.ui.logcat.LogcatActivity import com.v2ray.ang.ui.perappproxy.PerAppProxyActivity import com.v2ray.ang.ui.routing.RoutingSettingActivity import com.v2ray.ang.ui.server.ProfileEditorResult +import com.v2ray.ang.ui.server.ServerAmneziaActivity import com.v2ray.ang.ui.server.ServerCustomConfigActivity import com.v2ray.ang.ui.server.ServerGroupActivity import com.v2ray.ang.ui.server.ServerHttpActivity @@ -205,6 +206,7 @@ class MainActivity : HelperBaseComponentActivity() { EConfigType.HTTP.value -> Intent(this, ServerHttpActivity::class.java) EConfigType.TROJAN.value -> Intent(this, ServerTrojanActivity::class.java) EConfigType.WIREGUARD.value -> Intent(this, ServerWireguardActivity::class.java) + EConfigType.AMNEZIA.value -> Intent(this, ServerAmneziaActivity::class.java) EConfigType.HYSTERIA2.value -> Intent(this, ServerHysteria2Activity::class.java) else -> Intent(this, ServerHttpActivity::class.java).apply { putExtra("createConfigType", createConfigType) @@ -257,6 +259,7 @@ class MainActivity : HelperBaseComponentActivity() { EConfigType.HTTP -> ServerHttpActivity::class.java EConfigType.TROJAN -> ServerTrojanActivity::class.java EConfigType.WIREGUARD -> ServerWireguardActivity::class.java + EConfigType.AMNEZIA -> ServerAmneziaActivity::class.java EConfigType.HYSTERIA2 -> ServerHysteria2Activity::class.java else -> ServerHttpActivity::class.java } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainImportMenu.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainImportMenu.kt index 92ff21de..eb290b6c 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainImportMenu.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainImportMenu.kt @@ -23,6 +23,7 @@ private enum class ImportMenuAction(@StringRes val labelRes: Int, val action: Ma Http(R.string.menu_item_import_config_manually_http, MainAction.ImportManually(EConfigType.HTTP.value)), Trojan(R.string.menu_item_import_config_manually_trojan, MainAction.ImportManually(EConfigType.TROJAN.value)), WireGuard(R.string.menu_item_import_config_manually_wireguard, MainAction.ImportManually(EConfigType.WIREGUARD.value)), + Amnezia(R.string.menu_item_import_config_manually_amnezia, MainAction.ImportManually(EConfigType.AMNEZIA.value)), Hysteria2(R.string.menu_item_import_config_manually_hysteria2, MainAction.ImportManually(EConfigType.HYSTERIA2.value)) } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerAmneziaActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerAmneziaActivity.kt new file mode 100644 index 00000000..ffb4f4e8 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerAmneziaActivity.kt @@ -0,0 +1,174 @@ +package com.v2ray.ang.ui.server + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import com.v2ray.ang.R +import com.v2ray.ang.enums.EConfigType +import com.v2ray.ang.ui.compose.FormTextField + +class ServerAmneziaActivity : BaseServerActivity() { + + override val serverConfigType: EConfigType = EConfigType.AMNEZIA + + @Composable + override fun ScreenContent() { + val scope = rememberCoroutineScope() + val uiState = rememberSaveable(saver = ServerUiState.Saver) { + ServerUiState.from( + initialConfig = initialConfig + ) + }.apply { + configType = EConfigType.AMNEZIA + } + + ServerEditorScaffold( + title = serverConfigType.toString(), + onSaveClick = { saveServer(uiState) } + ) { + CommonBasicFields(uiState) + AmneziaProtocolFields(uiState) + + } + } + + @Composable + private fun AmneziaProtocolFields(state: ServerUiState) { + FormTextField( + stringResource(R.string.server_lab_secret_key), + state.secretKey, + { state.secretKey = it } + ) + FormTextField( + stringResource(R.string.server_lab_public_key), + state.publicKey, + { state.publicKey = it } + ) + FormTextField( + stringResource(R.string.server_lab_preshared_key), + state.preSharedKey, + { state.preSharedKey = it } + ) + FormTextField( + stringResource(R.string.server_lab_reserved), + state.reserved, + { state.reserved = it } + ) + FormTextField( + stringResource(R.string.server_lab_local_address), + state.localAddress, + { state.localAddress = it } + ) + FormTextField( + stringResource(R.string.server_lab_local_mtu), + state.mtu, + { state.mtu = it }, + keyboardType = KeyboardType.Number + ) + + FormTextField( + stringResource(R.string.server_lab_final_mask), + state.finalMask, + { state.finalMask = it } + ) + + FormTextField( + stringResource(R.string.server_lab_local_jc), + state.jc, + { state.jc = it } + ) + + FormTextField( + stringResource(R.string.server_lab_local_jmin), + state.jMin, + { state.jMin = it } + ) + + FormTextField( + stringResource(R.string.server_lab_local_jmax), + state.jMax, + { state.jMax = it } + ) + + FormTextField( + stringResource(R.string.server_lab_local_s1), + state.s1, + { state.s1 = it } + ) + + FormTextField( + stringResource(R.string.server_lab_local_s2), + state.s2, + { state.s2 = it } + ) + + FormTextField( + stringResource(R.string.server_lab_local_s3), + state.s3, + { state.s3 = it } + ) + + FormTextField( + stringResource(R.string.server_lab_local_s4), + state.s4, + { state.s4 = it } + ) + + FormTextField( + stringResource(R.string.server_lab_local_h1), + state.h1, + { state.h1 = it } + ) + + FormTextField( + stringResource(R.string.server_lab_local_h2), + state.h2, + { state.h2 = it } + ) + + FormTextField( + stringResource(R.string.server_lab_local_h3), + state.h3, + { state.h3 = it } + ) + + FormTextField( + stringResource(R.string.server_lab_local_h4), + state.h4, + { state.h4 = it } + ) + + FormTextField( + stringResource(R.string.server_lab_local_i1), + state.i1, + { state.i1 = it } + ) + + FormTextField( + stringResource(R.string.server_lab_local_i2), + state.i2, + { state.i2 = it } + ) + + FormTextField( + stringResource(R.string.server_lab_local_i3), + state.i3, + { state.i3 = it } + ) + + FormTextField( + stringResource(R.string.server_lab_local_i4), + state.i4, + { state.i4 = it } + ) + + FormTextField( + stringResource(R.string.server_lab_local_i5), + state.i5, + { state.i5 = it } + ) + } +} + diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerUiState.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerUiState.kt index 3961bc3b..2fce4a59 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerUiState.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerUiState.kt @@ -30,6 +30,22 @@ class ServerUiState( reserved: String = "0,0,0", localAddress: String = WIREGUARD_LOCAL_ADDRESS_V4, mtu: String = WIREGUARD_LOCAL_MTU, + jc: String = "0", + jMin: String = "0", + jMax: String = "0", + s1: String = "0", + s2: String = "0", + s3: String = "0", + s4: String = "0", + h1: String = "", + h2: String = "", + h3: String = "", + h4: String = "", + i1: String = "", + i2: String = "", + i3: String = "", + i4: String = "", + i5: String = "", obfsPassword: String = "", portHopping: String = "", portHoppingInterval: String = "", @@ -78,6 +94,22 @@ class ServerUiState( var reserved by mutableStateOf(reserved) var localAddress by mutableStateOf(localAddress) var mtu by mutableStateOf(mtu) + var jc by mutableStateOf(jc) + var jMin by mutableStateOf(jMin) + var jMax by mutableStateOf(jMax) + var s1 by mutableStateOf(s1) + var s2 by mutableStateOf(s2) + var s3 by mutableStateOf(s3) + var s4 by mutableStateOf(s4) + var h1 by mutableStateOf(h1) + var h2 by mutableStateOf(h2) + var h3 by mutableStateOf(h3) + var h4 by mutableStateOf(h4) + var i1 by mutableStateOf(i1) + var i2 by mutableStateOf(i2) + var i3 by mutableStateOf(i3) + var i4 by mutableStateOf(i4) + var i5 by mutableStateOf(i5) var obfsPassword by mutableStateOf(obfsPassword) var portHopping by mutableStateOf(portHopping) var portHoppingInterval by mutableStateOf(portHoppingInterval) @@ -117,6 +149,7 @@ class ServerUiState( val isShadowsocks = configType == EConfigType.SHADOWSOCKS val isSocksOrHttp = configType == EConfigType.SOCKS || configType == EConfigType.HTTP val isWireguard = configType == EConfigType.WIREGUARD + val isAmnezia = configType == EConfigType.AMNEZIA val isHysteria2 = configType == EConfigType.HYSTERIA2 return initialConfig.copy( @@ -132,16 +165,32 @@ class ServerUiState( }, flow = if (isVless) flow else null, username = if (isSocksOrHttp) username else null, - secretKey = if (isWireguard) secretKey else null, + secretKey = if (isWireguard or isAmnezia) secretKey else null, publicKey = when { - isWireguard -> publicKey + isWireguard or isAmnezia -> publicKey streamSecurity == REALITY -> publicKeyReality else -> null }, - preSharedKey = if (isWireguard) preSharedKey else null, - reserved = if (isWireguard) reserved else null, - localAddress = if (isWireguard) localAddress else null, - mtu = if (isWireguard) mtu.toIntOrNull() else null, + preSharedKey = if (isWireguard or isAmnezia) preSharedKey else null, + reserved = if (isWireguard or isAmnezia) reserved else null, + localAddress = if (isWireguard or isAmnezia) localAddress else null, + mtu = if (isWireguard or isAmnezia) mtu.toIntOrNull() else null, + jc = if (isAmnezia) jc.toInt() else null, + jMin = if (isAmnezia) jMin.toInt() else null, + jMax = if (isAmnezia) jMax.toInt() else null, + s1 = if (isAmnezia) s1.toInt() else null, + s2 = if (isAmnezia) s2.toInt() else null, + s3 = if (isAmnezia) s3.toInt() else null, + s4 = if (isAmnezia) s4.toInt() else null, + h1 = if (isAmnezia) h1 else null, + h2 = if (isAmnezia) h2 else null, + h3 = if (isAmnezia) h3 else null, + h4 = if (isAmnezia) h4 else null, + i1 = if (isAmnezia) i1 else null, + i2 = if (isAmnezia) i2 else null, + i3 = if (isAmnezia) i3 else null, + i4 = if (isAmnezia) i4 else null, + i5 = if (isAmnezia) i5 else null, obfsPassword = if (isHysteria2) obfsPassword else null, portHopping = if (isHysteria2) portHopping else null, portHoppingInterval = if (isHysteria2) portHoppingInterval else null, @@ -199,6 +248,22 @@ class ServerUiState( reserved = initialConfig.reserved ?: "0,0,0", localAddress = initialConfig.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4, mtu = initialConfig.mtu?.toString() ?: WIREGUARD_LOCAL_MTU, + jc = initialConfig.jc?.toString() ?: "0", + jMin = initialConfig.jMin?.toString() ?: "0", + jMax = initialConfig.jMax?.toString() ?: "0", + s1 = initialConfig.s1?.toString() ?: "0", + s2 = initialConfig.s2?.toString() ?: "0", + s3 = initialConfig.s3?.toString() ?: "0", + s4 = initialConfig.s4?.toString() ?: "0", + h1 = initialConfig.h1 ?: "", + h2 = initialConfig.h2 ?: "", + h3 = initialConfig.h3 ?: "", + h4 = initialConfig.h4 ?: "", + i1 = initialConfig.i1 ?: "", + i2 = initialConfig.i2 ?: "", + i3 = initialConfig.i3 ?: "", + i4 = initialConfig.i4 ?: "", + i5 = initialConfig.i5 ?: "", obfsPassword = initialConfig.obfsPassword ?: "", portHopping = initialConfig.portHopping ?: "", portHoppingInterval = initialConfig.portHoppingInterval ?: "", diff --git a/V2rayNG/app/src/main/res/values-ru/strings.xml b/V2rayNG/app/src/main/res/values-ru/strings.xml index 3f3d9007..09d506a4 100644 --- a/V2rayNG/app/src/main/res/values-ru/strings.xml +++ b/V2rayNG/app/src/main/res/values-ru/strings.xml @@ -41,6 +41,7 @@ Добавить HTTP Добавить Trojan Добавить WireGuard + Добавить Amnezia Добавить Hysteria2 Удалить все отображаемые профили? Удалить все отображаемые дубликаты профилей? diff --git a/V2rayNG/app/src/main/res/values/strings.xml b/V2rayNG/app/src/main/res/values/strings.xml index 0abb4e03..eb861092 100644 --- a/V2rayNG/app/src/main/res/values/strings.xml +++ b/V2rayNG/app/src/main/res/values/strings.xml @@ -41,6 +41,7 @@ Add [HTTP] Add [Trojan] Add [WireGuard] + Add [Amnezia] Add [Hysteria2] Are you sure you want to delete all profiles currently shown? Are you sure you want to delete all duplicate profiles currently shown? @@ -100,6 +101,22 @@ Reserved (optional, separated by commas) Local address (optional IPv4/IPv6, separated by commas) MTU (optional, default 1420) + jc (optional, junk packet count) + jmin (optional, min junk packet size) + jmax (optional, max junk packet size) + s1 (optional, handshake init padding) + s2 (optional, handshake response padding) + s3 (optional, cookie reply padding) + s4 (optional, transport packet padding) + h1 (optional, handshake init header) + h2 (optional, handshake response header) + h3 (optional, cookie reply header) + h4 (optional, transport packet header) + i1 (optional, custom signature packet #1) + i2 (optional, custom signature packet #2) + i3 (optional, custom signature packet #3) + i4 (optional, custom signature packet #4) + i5 (optional, custom signature packet #5) mKCP MTU (optional, default 1350) mKCP TTI (optional, default 50 ms) Success