diff --git a/client/android/awg/build.gradle.kts b/client/android/awg/build.gradle.kts new file mode 100644 index 00000000..8d280c7f --- /dev/null +++ b/client/android/awg/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id(libs.plugins.android.library.get().pluginId) + id(libs.plugins.kotlin.android.get().pluginId) +} + +kotlin { + jvmToolchain(17) +} + +android { + namespace = "org.amnezia.vpn.protocol.awg" +} + +dependencies { + compileOnly(project(":utils")) + compileOnly(project(":protocolApi")) + implementation(project(":wireguard")) +} diff --git a/client/android/awg/src/main/kotlin/Awg.kt b/client/android/awg/src/main/kotlin/Awg.kt new file mode 100644 index 00000000..2bc06af5 --- /dev/null +++ b/client/android/awg/src/main/kotlin/Awg.kt @@ -0,0 +1,79 @@ +package org.amnezia.vpn.protocol.awg + +import org.amnezia.vpn.protocol.wireguard.Wireguard +import org.json.JSONObject + +/** + * Config example: + * { + * "protocol": "awg", + * "description": "Server 1", + * "dns1": "1.1.1.1", + * "dns2": "1.0.0.1", + * "hostName": "100.100.100.0", + * "splitTunnelSites": [ + * ], + * "splitTunnelType": 0, + * "awg_config_data": { + * "H1": "969537490", + * "H2": "481688153", + * "H3": "2049399200", + * "H4": "52029755", + * "Jc": "3", + * "Jmax": "1000", + * "Jmin": "50", + * "S1": "49", + * "S2": "60", + * "client_ip": "10.8.1.1", + * "hostName": "100.100.100.0", + * "port": 12345, + * "client_pub_key": "clientPublicKeyBase64", + * "client_priv_key": "privateKeyBase64", + * "psk_key": "presharedKeyBase64", + * "server_pub_key": "publicKeyBase64", + * "config": "[Interface] + * Address = 10.8.1.1/32 + * DNS = 1.1.1.1, 1.0.0.1 + * PrivateKey = privateKeyBase64 + * Jc = 3 + * Jmin = 50 + * Jmax = 1000 + * S1 = 49 + * S2 = 60 + * H1 = 969537490 + * H2 = 481688153 + * H3 = 2049399200 + * H4 = 52029755 + * + * [Peer] + * PublicKey = publicKeyBase64 + * PresharedKey = presharedKeyBase64 + * AllowedIPs = 0.0.0.0/0, ::/0 + * Endpoint = 100.100.100.0:12345 + * PersistentKeepalive = 25 + * " + * } + * } + */ + +class Awg : Wireguard() { + + override val ifName: String = "awg0" + + override fun parseConfig(config: JSONObject): AwgConfig { + val configDataJson = config.getJSONObject("awg_config_data") + val configData = parseConfigData(configDataJson.getString("config")) + return AwgConfig.build { + configureWireguard(wireguardConfigBuilder(configData)) + configData["Jc"]?.let { setJc(it.toInt()) } + configData["Jmin"]?.let { setJmin(it.toInt()) } + configData["Jmax"]?.let { setJmax(it.toInt()) } + configData["S1"]?.let { setS1(it.toInt()) } + configData["S2"]?.let { setS2(it.toInt()) } + configData["H1"]?.let { setH1(it.toLong()) } + configData["H2"]?.let { setH2(it.toLong()) } + configData["H3"]?.let { setH3(it.toLong()) } + configData["H4"]?.let { setH4(it.toLong()) } + } + } +} diff --git a/client/android/awg/src/main/kotlin/AwgConfig.kt b/client/android/awg/src/main/kotlin/AwgConfig.kt new file mode 100644 index 00000000..d70bbf9c --- /dev/null +++ b/client/android/awg/src/main/kotlin/AwgConfig.kt @@ -0,0 +1,115 @@ +package org.amnezia.vpn.protocol.awg + +import org.amnezia.vpn.protocol.BadConfigException +import org.amnezia.vpn.protocol.wireguard.WireguardConfig + +class AwgConfig private constructor( + wireguardConfigBuilder: WireguardConfig.Builder, + val jc: Int, + val jmin: Int, + val jmax: Int, + val s1: Int, + val s2: Int, + val h1: Long, + val h2: Long, + val h3: Long, + val h4: Long +) : WireguardConfig(wireguardConfigBuilder) { + + private constructor(builder: Builder) : this( + builder.wireguardConfigBuilder, + builder.jc, + builder.jmin, + builder.jmax, + builder.s1, + builder.s2, + builder.h1, + builder.h2, + builder.h3, + builder.h4 + ) + + override fun toWgUserspaceString(): String = with(StringBuilder()) { + append(super.toWgUserspaceString()) + appendLine("jc=$jc") + appendLine("jmin=$jmin") + appendLine("jmax=$jmax") + appendLine("s1=$s1") + appendLine("s2=$s2") + appendLine("h1=$h1") + appendLine("h2=$h2") + appendLine("h3=$h3") + appendLine("h4=$h4") + return this.toString() + } + + class Builder { + internal lateinit var wireguardConfigBuilder: WireguardConfig.Builder + private set + + private var _jc: Int? = null + internal var jc: Int + get() = _jc ?: throw BadConfigException("AWG: parameter jc is undefined") + private set(value) { _jc = value} + + private var _jmin: Int? = null + internal var jmin: Int + get() = _jmin ?: throw BadConfigException("AWG: parameter jmin is undefined") + private set(value) { _jmin = value} + + private var _jmax: Int? = null + internal var jmax: Int + get() = _jmax ?: throw BadConfigException("AWG: parameter jmax is undefined") + private set(value) { _jmax = value} + + private var _s1: Int? = null + internal var s1: Int + get() = _s1 ?: throw BadConfigException("AWG: parameter s1 is undefined") + private set(value) { _s1 = value} + + private var _s2: Int? = null + internal var s2: Int + get() = _s2 ?: throw BadConfigException("AWG: parameter s2 is undefined") + private set(value) { _s2 = value} + + private var _h1: Long? = null + internal var h1: Long + get() = _h1 ?: throw BadConfigException("AWG: parameter h1 is undefined") + private set(value) { _h1 = value} + + private var _h2: Long? = null + internal var h2: Long + get() = _h2 ?: throw BadConfigException("AWG: parameter h2 is undefined") + private set(value) { _h2 = value} + + private var _h3: Long? = null + internal var h3: Long + get() = _h3 ?: throw BadConfigException("AWG: parameter h3 is undefined") + private set(value) { _h3 = value} + + private var _h4: Long? = null + internal var h4: Long + get() = _h4 ?: throw BadConfigException("AWG: parameter h4 is undefined") + private set(value) { _h4 = value} + + fun configureWireguard(block: WireguardConfig.Builder.() -> Unit) = apply { + wireguardConfigBuilder = WireguardConfig.Builder().apply(block) + } + + fun setJc(jc: Int) = apply { this.jc = jc } + fun setJmin(jmin: Int) = apply { this.jmin = jmin } + fun setJmax(jmax: Int) = apply { this.jmax = jmax } + fun setS1(s1: Int) = apply { this.s1 = s1 } + fun setS2(s2: Int) = apply { this.s2 = s2 } + fun setH1(h1: Long) = apply { this.h1 = h1 } + fun setH2(h2: Long) = apply { this.h2 = h2 } + fun setH3(h3: Long) = apply { this.h3 = h3 } + fun setH4(h4: Long) = apply { this.h4 = h4 } + + fun build(): AwgConfig = AwgConfig(this) + } + + companion object { + inline fun build(block: Builder.() -> Unit): AwgConfig = Builder().apply(block).build() + } +} diff --git a/client/android/build.gradle.kts b/client/android/build.gradle.kts index 39736074..6dbf606f 100644 --- a/client/android/build.gradle.kts +++ b/client/android/build.gradle.kts @@ -87,6 +87,7 @@ dependencies { implementation(project(":utils")) implementation(project(":protocolApi")) implementation(project(":wireguard")) + implementation(project(":awg")) implementation(libs.androidx.core) implementation(libs.androidx.activity) implementation(libs.androidx.security.crypto) diff --git a/client/android/protocolApi/build.gradle.kts b/client/android/protocolApi/build.gradle.kts index 9125f7f0..6d47e065 100644 --- a/client/android/protocolApi/build.gradle.kts +++ b/client/android/protocolApi/build.gradle.kts @@ -14,4 +14,4 @@ android { dependencies { compileOnly(project(":utils")) implementation(libs.androidx.annotation) -} \ No newline at end of file +} diff --git a/client/android/protocolApi/src/main/kotlin/Protocol.kt b/client/android/protocolApi/src/main/kotlin/Protocol.kt index c4d549f7..b3a902b5 100644 --- a/client/android/protocolApi/src/main/kotlin/Protocol.kt +++ b/client/android/protocolApi/src/main/kotlin/Protocol.kt @@ -18,17 +18,17 @@ private const val TAG = "Protocol" const val VPN_SESSION_NAME = "AmneziaVPN" -abstract class Protocol(protected val context: Context) { - - open lateinit var config: ProtocolConfig +abstract class Protocol { abstract val statistics: Statistics - abstract fun initialize() + abstract fun initialize(context: Context) - abstract fun parseConfig(config: JSONObject) + abstract fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) - protected open fun buildVpnInterface(vpnBuilder: Builder) { + abstract fun stopVpn() + + protected open fun buildVpnInterface(config: ProtocolConfig, vpnBuilder: Builder) { vpnBuilder.setSession(VPN_SESSION_NAME) vpnBuilder.allowFamily(OsConstants.AF_INET) vpnBuilder.allowFamily(OsConstants.AF_INET6) @@ -49,10 +49,6 @@ abstract class Protocol(protected val context: Context) { vpnBuilder.setMetered(false) } - abstract fun startVpn(vpnBuilder: Builder, protect: (Int) -> Boolean) - - abstract fun stopVpn() - companion object { private fun extractLibrary(context: Context, libraryName: String, destination: File): Boolean { Log.d(TAG, "Extracting library: $libraryName") diff --git a/client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt b/client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt index f6e5df7d..546a7478 100644 --- a/client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt +++ b/client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt @@ -5,7 +5,7 @@ import android.os.Build import androidx.annotation.RequiresApi import java.net.InetAddress -data class ProtocolConfig( +open class ProtocolConfig protected constructor( val addresses: Set, val dnsServers: Set, val routes: Set, @@ -15,7 +15,7 @@ data class ProtocolConfig( val mtu: Int ) { - private constructor(builder: Builder) : this( + protected constructor(builder: Builder) : this( builder.addresses, builder.dnsServers, builder.routes, @@ -49,6 +49,7 @@ data class ProtocolConfig( @RequiresApi(Build.VERSION_CODES.TIRAMISU) fun excludeRoute(route: InetNetwork) = apply { this.excludedRoutes += route } + @RequiresApi(Build.VERSION_CODES.TIRAMISU) fun excludeRoutes(routes: List) = apply { this.excludedRoutes += routes } diff --git a/client/android/settings.gradle.kts b/client/android/settings.gradle.kts index 9b8d3c9d..7f22efa2 100644 --- a/client/android/settings.gradle.kts +++ b/client/android/settings.gradle.kts @@ -33,6 +33,7 @@ include(":qt") include(":utils") include(":protocolApi") include(":wireguard") +include(":awg") // get values from gradle or local properties val androidBuildToolsVersion: String by gradleProperties diff --git a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt index 58f10719..d5185092 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt @@ -35,6 +35,7 @@ import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTING import org.amnezia.vpn.protocol.Statistics import org.amnezia.vpn.protocol.Status import org.amnezia.vpn.protocol.VpnStartException +import org.amnezia.vpn.protocol.awg.Awg import org.amnezia.vpn.protocol.putStatistics import org.amnezia.vpn.protocol.putStatus import org.amnezia.vpn.protocol.wireguard.Wireguard @@ -54,6 +55,7 @@ class AmneziaVpnService : VpnService() { private lateinit var mainScope: CoroutineScope private var isServiceBound = false private var protocol: Protocol? = null + private val protocolCache = mutableMapOf() private var protocolState = MutableStateFlow(DISCONNECTED) private val isConnected @@ -272,11 +274,7 @@ class AmneziaVpnService : VpnService() { disconnectionJob = null protocol = getProtocol(config.getString("protocol")) - protocol?.let { protocol -> - protocol.initialize() - protocol.parseConfig(config) - protocol.startVpn(Builder(), ::protect) - } + protocol?.startVpn(config, Builder(), ::protect) protocolState.value = CONNECTED } } @@ -302,10 +300,12 @@ class AmneziaVpnService : VpnService() { } private fun getProtocol(protocolName: String): Protocol = - when (protocolName) { - "wireguard" -> Wireguard(applicationContext) - else -> throw IllegalArgumentException("Failed to load $protocolName protocol") - } + protocolCache[protocolName] + ?: when (protocolName) { + "wireguard" -> Wireguard() + "awg" -> Awg() + else -> throw IllegalArgumentException("Failed to load $protocolName protocol") + }.apply { initialize(applicationContext) } /** * Utils methods diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt index 0125198e..c24facc0 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt @@ -9,17 +9,51 @@ import org.amnezia.vpn.protocol.InetEndpoint import org.amnezia.vpn.protocol.InetNetwork import org.amnezia.vpn.protocol.Protocol import org.amnezia.vpn.protocol.Statistics -import org.amnezia.vpn.protocol.VPN_SESSION_NAME import org.amnezia.vpn.protocol.VpnStartException import org.amnezia.vpn.protocol.parseInetAddress import org.json.JSONObject +/** + * Config example: + * { + * "protocol": "wireguard", + * "description": "Server 1", + * "dns1": "1.1.1.1", + * "dns2": "1.0.0.1", + * "hostName": "100.100.100.0", + * "splitTunnelSites": [ + * ], + * "splitTunnelType": 0, + * "wireguard_config_data": { + * "client_ip": "10.8.1.1", + * "hostName": "100.100.100.0", + * "port": 12345, + * "client_pub_key": "clientPublicKeyBase64", + * "client_priv_key": "privateKeyBase64", + * "psk_key": "presharedKeyBase64", + * "server_pub_key": "publicKeyBase64", + * "config": "[Interface] + * Address = 10.8.1.1/32 + * DNS = 1.1.1.1, 1.0.0.1 + * PrivateKey = privateKeyBase64 + * + * [Peer] + * PublicKey = publicKeyBase64 + * PresharedKey = presharedKeyBase64 + * AllowedIPs = 0.0.0.0/0, ::/0 + * Endpoint = 100.100.100.0:12345 + * PersistentKeepalive = 25 + * " + * } + * } + */ + private const val TAG = "Wireguard" -class Wireguard(context: Context) : Protocol(context) { +open class Wireguard : Protocol() { private var tunnelHandle: Int = -1 - private lateinit var wireguardConfig: WireguardConfig + protected open val ifName: String = "amn0" override val statistics: Statistics get() { @@ -40,14 +74,23 @@ class Wireguard(context: Context) : Protocol(context) { } } - override fun initialize() { + override fun initialize(context: Context) { loadSharedLibrary(context, "wg-go") } - override fun parseConfig(config: JSONObject) { + override fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { + val wireguardConfig = parseConfig(config) + start(wireguardConfig, vpnBuilder, protect) + } + + protected open fun parseConfig(config: JSONObject): WireguardConfig { val configDataJson = config.getJSONObject("wireguard_config_data") val configData = parseConfigData(configDataJson.getString("config")) - wireguardConfig = WireguardConfig.build { + return WireguardConfig.build(wireguardConfigBuilder(configData)) + } + + protected fun wireguardConfigBuilder(configData: Map): WireguardConfig.Builder.() -> Unit = + { configureBaseProtocol(true) { configData["Address"]?.let { addAddress(InetNetwork.parse(it)) } configData["DNS"]?.split(",")?.map { dns -> @@ -64,10 +107,8 @@ class Wireguard(context: Context) : Protocol(context) { configData["PublicKey"]?.let { setPublicKeyHex(it.base64ToHex()) } configData["PresharedKey"]?.let { setPreSharedKeyHex(it.base64ToHex()) } } - this.config = wireguardConfig.baseProtocolConfig - } - private fun parseConfigData(data: String): Map { + protected fun parseConfigData(data: String): Map { val parsedData = TreeMap(String.CASE_INSENSITIVE_ORDER) data.lineSequence() .filter { it.isNotEmpty() && !it.startsWith('[') } @@ -78,20 +119,20 @@ class Wireguard(context: Context) : Protocol(context) { return parsedData } - override fun startVpn(vpnBuilder: Builder, protect: (Int) -> Boolean) { + private fun start(config: WireguardConfig, vpnBuilder: Builder, protect: (Int) -> Boolean) { if (tunnelHandle != -1) { Log.w(TAG, "Tunnel already up") return } - buildVpnInterface(vpnBuilder) + buildVpnInterface(config, vpnBuilder) vpnBuilder.establish().use { tunFd -> if (tunFd == null) { throw VpnStartException("Create VPN interface: permission not granted or revoked") } Log.v(TAG, "Wg-go backend ${GoBackend.wgVersion()}") - tunnelHandle = GoBackend.wgTurnOn(VPN_SESSION_NAME, tunFd.detachFd(), wireguardConfig.toWgUserspaceString()) + tunnelHandle = GoBackend.wgTurnOn(ifName, tunFd.detachFd(), config.toWgUserspaceString()) } if (tunnelHandle < 0) { diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt index 740d1ab5..bdb341b8 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/WireguardConfig.kt @@ -6,17 +6,17 @@ import org.amnezia.vpn.protocol.ProtocolConfig internal const val WIREGUARD_DEFAULT_MTU = 1280 -data class WireguardConfig( - val baseProtocolConfig: ProtocolConfig, +open class WireguardConfig protected constructor( + protocolConfigBuilder: ProtocolConfig.Builder, val endpoint: InetEndpoint, val persistentKeepalive: Int, val publicKeyHex: String, val preSharedKeyHex: String, val privateKeyHex: String -) { +) : ProtocolConfig(protocolConfigBuilder) { - private constructor(builder: Builder) : this( - builder.baseProtocolConfig, + protected constructor(builder: Builder) : this( + builder.protocolConfigBuilder, builder.endpoint, builder.persistentKeepalive, builder.publicKeyHex, @@ -24,11 +24,11 @@ data class WireguardConfig( builder.privateKeyHex ) - fun toWgUserspaceString(): String = with(StringBuilder()) { + open fun toWgUserspaceString(): String = with(StringBuilder()) { appendLine("private_key=$privateKeyHex") appendLine("replace_peers=true") appendLine("public_key=$publicKeyHex") - baseProtocolConfig.routes.forEach { route -> + routes.forEach { route -> appendLine("allowed_ip=$route") } appendLine("endpoint=$endpoint") @@ -39,7 +39,7 @@ data class WireguardConfig( } class Builder { - internal lateinit var baseProtocolConfig: ProtocolConfig + internal lateinit var protocolConfigBuilder: ProtocolConfig.Builder private set internal lateinit var endpoint: InetEndpoint @@ -58,7 +58,7 @@ data class WireguardConfig( private set fun configureBaseProtocol(blockingMode: Boolean, block: ProtocolConfig.Builder.() -> Unit) = apply { - baseProtocolConfig = ProtocolConfig.Builder(blockingMode).apply(block).build() + protocolConfigBuilder = ProtocolConfig.Builder(blockingMode).apply(block) } fun setEndpoint(endpoint: InetEndpoint) = apply { this.endpoint = endpoint }