Add ProtocolApi module
This commit is contained in:
parent
6d6710db4a
commit
de65a03998
10 changed files with 359 additions and 0 deletions
|
@ -85,6 +85,7 @@ dependencies {
|
||||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar", "*.aar"))))
|
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar", "*.aar"))))
|
||||||
implementation(project(":qt"))
|
implementation(project(":qt"))
|
||||||
implementation(project(":utils"))
|
implementation(project(":utils"))
|
||||||
|
implementation(project(":protocolApi"))
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
implementation(libs.androidx.activity)
|
implementation(libs.androidx.activity)
|
||||||
implementation(libs.androidx.security.crypto)
|
implementation(libs.androidx.security.crypto)
|
||||||
|
|
|
@ -3,6 +3,7 @@ agp = "8.1.3"
|
||||||
kotlin = "1.9.20"
|
kotlin = "1.9.20"
|
||||||
androidx-core = "1.12.0"
|
androidx-core = "1.12.0"
|
||||||
androidx-activity = "1.8.1"
|
androidx-activity = "1.8.1"
|
||||||
|
androidx-annotation = "1.7.0"
|
||||||
androidx-camera = "1.3.0"
|
androidx-camera = "1.3.0"
|
||||||
androidx-security-crypto = "1.1.0-alpha06"
|
androidx-security-crypto = "1.1.0-alpha06"
|
||||||
kotlinx-coroutines = "1.7.3"
|
kotlinx-coroutines = "1.7.3"
|
||||||
|
@ -11,6 +12,7 @@ google-mlkit = "17.2.0"
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
||||||
androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" }
|
androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" }
|
||||||
|
androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" }
|
||||||
androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "androidx-camera" }
|
androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "androidx-camera" }
|
||||||
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidx-camera" }
|
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidx-camera" }
|
||||||
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidx-camera" }
|
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidx-camera" }
|
||||||
|
|
17
client/android/protocolApi/build.gradle.kts
Normal file
17
client/android/protocolApi/build.gradle.kts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
plugins {
|
||||||
|
id(libs.plugins.android.library.get().pluginId)
|
||||||
|
id(libs.plugins.kotlin.android.get().pluginId)
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "org.amnezia.vpn.protocol"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(project(":utils"))
|
||||||
|
implementation(libs.androidx.annotation)
|
||||||
|
}
|
9
client/android/protocolApi/src/main/kotlin/Exceptions.kt
Normal file
9
client/android/protocolApi/src/main/kotlin/Exceptions.kt
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package org.amnezia.vpn.protocol
|
||||||
|
|
||||||
|
sealed class ProtocolException(message: String? = null, cause: Throwable? = null) : Exception(message, cause)
|
||||||
|
|
||||||
|
class LoadLibraryException(message: String? = null, cause: Throwable? = null) : ProtocolException(message, cause)
|
||||||
|
class VpnNotAuthorizedException(message: String? = null, cause: Throwable? = null) : ProtocolException(message, cause)
|
||||||
|
class BadConfigException(message: String? = null, cause: Throwable? = null) : ProtocolException(message, cause)
|
||||||
|
|
||||||
|
class VpnStartException(message: String? = null, cause: Throwable? = null) : ProtocolException(message, cause)
|
115
client/android/protocolApi/src/main/kotlin/Protocol.kt
Normal file
115
client/android/protocolApi/src/main/kotlin/Protocol.kt
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
package org.amnezia.vpn.protocol
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.IpPrefix
|
||||||
|
import android.net.VpnService
|
||||||
|
import android.net.VpnService.Builder
|
||||||
|
import android.os.Build
|
||||||
|
import android.system.OsConstants
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
import org.amnezia.vpn.Log
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
private const val TAG = "Protocol"
|
||||||
|
|
||||||
|
const val VPN_SESSION_NAME = "AmneziaVPN"
|
||||||
|
|
||||||
|
abstract class Protocol(protected val context: Context) {
|
||||||
|
|
||||||
|
open lateinit var config: ProtocolConfig
|
||||||
|
|
||||||
|
abstract val statistics: Statistics
|
||||||
|
|
||||||
|
abstract fun initialize()
|
||||||
|
|
||||||
|
abstract fun parseConfig(config: JSONObject)
|
||||||
|
|
||||||
|
protected open fun buildVpnInterface(vpnBuilder: Builder) {
|
||||||
|
vpnBuilder.setSession(VPN_SESSION_NAME)
|
||||||
|
vpnBuilder.allowFamily(OsConstants.AF_INET)
|
||||||
|
vpnBuilder.allowFamily(OsConstants.AF_INET6)
|
||||||
|
|
||||||
|
for (addr in config.addresses) vpnBuilder.addAddress(addr)
|
||||||
|
for (addr in config.dnsServers) vpnBuilder.addDnsServer(addr)
|
||||||
|
for (addr in config.routes) vpnBuilder.addRoute(addr)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||||
|
for (addr in config.excludedRoutes) vpnBuilder.excludeRoute(addr)
|
||||||
|
|
||||||
|
for (app in config.excludedApplications) vpnBuilder.addDisallowedApplication(app)
|
||||||
|
|
||||||
|
vpnBuilder.setMtu(config.mtu)
|
||||||
|
vpnBuilder.setBlocking(config.blockingMode)
|
||||||
|
vpnBuilder.setUnderlyingNetworks(null)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||||
|
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")
|
||||||
|
val apks = hashSetOf<String>()
|
||||||
|
context.applicationInfo.run {
|
||||||
|
sourceDir?.let { apks += it }
|
||||||
|
splitSourceDirs?.let { apks += it }
|
||||||
|
}
|
||||||
|
for (abi in Build.SUPPORTED_ABIS) {
|
||||||
|
for (apk in apks) {
|
||||||
|
ZipFile(File(apk), ZipFile.OPEN_READ).use { zipFile ->
|
||||||
|
val mappedName = System.mapLibraryName(libraryName)
|
||||||
|
val libraryZipPath = listOf("lib", abi, mappedName).joinToString(File.separator)
|
||||||
|
val zipEntry = zipFile.getEntry(libraryZipPath)
|
||||||
|
zipEntry?.let {
|
||||||
|
Log.d(TAG, "Extracting apk:/$libraryZipPath to ${destination.absolutePath}")
|
||||||
|
FileOutputStream(destination).use { outStream ->
|
||||||
|
zipFile.getInputStream(zipEntry).use { inStream ->
|
||||||
|
inStream.copyTo(outStream, 32 * 1024)
|
||||||
|
outStream.fd.sync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UnsafeDynamicallyLoadedCode")
|
||||||
|
fun loadSharedLibrary(context: Context, libraryName: String) {
|
||||||
|
Log.d(TAG, "Loading library: $libraryName")
|
||||||
|
try {
|
||||||
|
System.loadLibrary(libraryName)
|
||||||
|
return
|
||||||
|
} catch (_: UnsatisfiedLinkError) {
|
||||||
|
Log.d(TAG, "Failed to load library, try to extract it from apk")
|
||||||
|
}
|
||||||
|
var tempFile: File? = null
|
||||||
|
try {
|
||||||
|
tempFile = File.createTempFile("lib", ".so", context.codeCacheDir)
|
||||||
|
if (extractLibrary(context, libraryName, tempFile)) {
|
||||||
|
System.load(tempFile.absolutePath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw LoadLibraryException("Failed to load library apk: $libraryName", e)
|
||||||
|
} finally {
|
||||||
|
tempFile?.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun VpnService.Builder.addAddress(addr: InetNetwork) = addAddress(addr.address, addr.mask)
|
||||||
|
private fun VpnService.Builder.addRoute(addr: InetNetwork) = addRoute(addr.address, addr.mask)
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||||
|
private fun VpnService.Builder.excludeRoute(addr: InetNetwork) = excludeRoute(IpPrefix(addr.address, addr.mask))
|
121
client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt
Normal file
121
client/android/protocolApi/src/main/kotlin/ProtocolConfig.kt
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
package org.amnezia.vpn.protocol
|
||||||
|
|
||||||
|
import android.net.InetAddresses
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
data class ProtocolConfig(
|
||||||
|
val addresses: Set<InetNetwork>,
|
||||||
|
val dnsServers: Set<InetAddress>,
|
||||||
|
val routes: Set<InetNetwork>,
|
||||||
|
val excludedRoutes: Set<InetNetwork>,
|
||||||
|
val excludedApplications: Set<String>,
|
||||||
|
val blockingMode: Boolean,
|
||||||
|
val mtu: Int
|
||||||
|
) {
|
||||||
|
|
||||||
|
private constructor(builder: Builder) : this(
|
||||||
|
builder.addresses,
|
||||||
|
builder.dnsServers,
|
||||||
|
builder.routes,
|
||||||
|
builder.excludedRoutes,
|
||||||
|
builder.excludedApplications,
|
||||||
|
builder.blockingMode,
|
||||||
|
builder.mtu
|
||||||
|
)
|
||||||
|
|
||||||
|
class Builder(blockingMode: Boolean) {
|
||||||
|
internal val addresses: MutableSet<InetNetwork> = hashSetOf()
|
||||||
|
internal val dnsServers: MutableSet<InetAddress> = hashSetOf()
|
||||||
|
internal val routes: MutableSet<InetNetwork> = hashSetOf()
|
||||||
|
internal val excludedRoutes: MutableSet<InetNetwork> = hashSetOf()
|
||||||
|
internal val excludedApplications: MutableSet<String> = hashSetOf()
|
||||||
|
|
||||||
|
internal var blockingMode: Boolean = blockingMode
|
||||||
|
private set
|
||||||
|
|
||||||
|
internal var mtu: Int = 0
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun addAddress(addr: InetNetwork) = apply { this.addresses += addr }
|
||||||
|
fun addAddresses(addresses: List<InetNetwork>) = apply { this.addresses += addresses }
|
||||||
|
|
||||||
|
fun addDnsServer(dnsServer: InetAddress) = apply { this.dnsServers += dnsServer }
|
||||||
|
fun addDnsServers(dnsServers: List<InetAddress>) = apply { this.dnsServers += dnsServers }
|
||||||
|
|
||||||
|
fun addRoute(route: InetNetwork) = apply { this.routes += route }
|
||||||
|
fun addRoutes(routes: List<InetNetwork>) = apply { this.routes += routes }
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||||
|
fun excludeRoute(route: InetNetwork) = apply { this.excludedRoutes += route }
|
||||||
|
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||||
|
fun excludeRoutes(routes: List<InetNetwork>) = apply { this.excludedRoutes += routes }
|
||||||
|
|
||||||
|
fun excludeApplication(application: String) = apply { this.excludedApplications += application }
|
||||||
|
fun excludeApplications(applications: List<String>) = apply { this.excludedApplications += applications }
|
||||||
|
|
||||||
|
fun setBlockingMode(blockingMode: Boolean) = apply { this.blockingMode = blockingMode }
|
||||||
|
|
||||||
|
fun setMtu(mtu: Int) = apply { this.mtu = mtu }
|
||||||
|
|
||||||
|
private fun validate() {
|
||||||
|
val errorMessage = StringBuilder()
|
||||||
|
|
||||||
|
with(errorMessage) {
|
||||||
|
if (addresses.isEmpty()) appendLine("VPN interface network address not specified.")
|
||||||
|
if (routes.isEmpty()) appendLine("VPN interface route not specified.")
|
||||||
|
if (mtu == 0) appendLine("MTU not set.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMessage.isNotEmpty()) throw BadConfigException(errorMessage.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(): ProtocolConfig = validate().run { ProtocolConfig(this@Builder) }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
inline fun build(blockingMode: Boolean, block: Builder.() -> Unit): ProtocolConfig =
|
||||||
|
Builder(blockingMode).apply(block).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class InetNetwork(val address: InetAddress, val mask: Int) {
|
||||||
|
|
||||||
|
override fun toString(): String = "${address.hostAddress}/$mask"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parse(data: String): InetNetwork {
|
||||||
|
val split = data.split("/")
|
||||||
|
val address = parseInetAddress(split.first())
|
||||||
|
val mask = split.last().toInt()
|
||||||
|
return InetNetwork(address, mask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class InetEndpoint(val address: InetAddress, val port: Int) {
|
||||||
|
|
||||||
|
override fun toString(): String = "${address.hostAddress}:$port"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parse(data: String): InetEndpoint {
|
||||||
|
val split = data.split(":")
|
||||||
|
val address = parseInetAddress(split.first())
|
||||||
|
val port = split.last().toInt()
|
||||||
|
return InetEndpoint(address, port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseInetAddress(address: String): InetAddress = parseNumericAddressCompat(address)
|
||||||
|
|
||||||
|
private val parseNumericAddressCompat: (String) -> InetAddress =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
InetAddresses::parseNumericAddress
|
||||||
|
} else {
|
||||||
|
val m = InetAddress::class.java.getMethod("parseNumericAddress", String::class.java)
|
||||||
|
fun(address: String): InetAddress {
|
||||||
|
return m.invoke(null, address) as InetAddress
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.amnezia.vpn.protocol
|
||||||
|
|
||||||
|
enum class ProtocolState {
|
||||||
|
CONNECTED,
|
||||||
|
CONNECTING,
|
||||||
|
DISCONNECTED,
|
||||||
|
DISCONNECTING,
|
||||||
|
UNKNOWN
|
||||||
|
}
|
50
client/android/protocolApi/src/main/kotlin/Statistics.kt
Normal file
50
client/android/protocolApi/src/main/kotlin/Statistics.kt
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package org.amnezia.vpn.protocol
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
|
||||||
|
private const val RX_BYTES_KEY = "rxBytes"
|
||||||
|
private const val TX_BYTES_KEY = "txBytes"
|
||||||
|
|
||||||
|
@Suppress("DataClassPrivateConstructor")
|
||||||
|
data class Statistics private constructor(
|
||||||
|
val rxBytes: Long = 0L,
|
||||||
|
val txBytes: Long = 0L
|
||||||
|
) {
|
||||||
|
|
||||||
|
private constructor(builder: Builder) : this(builder.rxBytes, builder.txBytes)
|
||||||
|
|
||||||
|
@Suppress("SuspiciousEqualsCombination")
|
||||||
|
fun isEmpty(): Boolean = this === EMPTY_STATISTICS || this == EMPTY_STATISTICS
|
||||||
|
|
||||||
|
class Builder {
|
||||||
|
var rxBytes: Long = 0L
|
||||||
|
private set
|
||||||
|
|
||||||
|
var txBytes: Long = 0L
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun setRxBytes(rxBytes: Long) = apply { this.rxBytes = rxBytes }
|
||||||
|
fun setTxBytes(txBytes: Long) = apply { this.txBytes = txBytes }
|
||||||
|
|
||||||
|
fun build(): Statistics =
|
||||||
|
if (rxBytes + txBytes == 0L) EMPTY_STATISTICS
|
||||||
|
else Statistics(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val EMPTY_STATISTICS: Statistics = Statistics()
|
||||||
|
|
||||||
|
inline fun build(block: Builder.() -> Unit): Statistics = Builder().apply(block).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Bundle.putStatistics(statistics: Statistics) {
|
||||||
|
putLong(RX_BYTES_KEY, statistics.rxBytes)
|
||||||
|
putLong(TX_BYTES_KEY, statistics.txBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Bundle.getStatistics(): Statistics =
|
||||||
|
Statistics.build {
|
||||||
|
setRxBytes(getLong(RX_BYTES_KEY))
|
||||||
|
setTxBytes(getLong(TX_BYTES_KEY))
|
||||||
|
}
|
34
client/android/protocolApi/src/main/kotlin/Status.kt
Normal file
34
client/android/protocolApi/src/main/kotlin/Status.kt
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package org.amnezia.vpn.protocol
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
|
||||||
|
private const val IS_CONNECTED_KEY = "isConnected"
|
||||||
|
|
||||||
|
@Suppress("DataClassPrivateConstructor")
|
||||||
|
data class Status private constructor(
|
||||||
|
val isConnected: Boolean = false
|
||||||
|
) {
|
||||||
|
private constructor(builder: Builder) : this(builder.isConnected)
|
||||||
|
|
||||||
|
class Builder {
|
||||||
|
var isConnected: Boolean = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun setConnected(isConnected: Boolean) = apply { this.isConnected = isConnected }
|
||||||
|
|
||||||
|
fun build(): Status = Status(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
inline fun build(block: Builder.() -> Unit): Status = Builder().apply(block).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Bundle.putStatus(statistics: Status) {
|
||||||
|
putBoolean(IS_CONNECTED_KEY, statistics.isConnected)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Bundle.getStatus(): Status =
|
||||||
|
Status.build {
|
||||||
|
setConnected(getBoolean(IS_CONNECTED_KEY))
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ rootProject.buildFileName = "build.gradle.kts"
|
||||||
|
|
||||||
include(":qt")
|
include(":qt")
|
||||||
include(":utils")
|
include(":utils")
|
||||||
|
include(":protocolApi")
|
||||||
|
|
||||||
// get values from gradle or local properties
|
// get values from gradle or local properties
|
||||||
val androidBuildToolsVersion: String by gradleProperties
|
val androidBuildToolsVersion: String by gradleProperties
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue