Add ProtocolApi module

This commit is contained in:
albexk 2023-11-23 15:45:55 +03:00
parent 6d6710db4a
commit de65a03998
10 changed files with 359 additions and 0 deletions

View file

@ -85,6 +85,7 @@ dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar", "*.aar"))))
implementation(project(":qt"))
implementation(project(":utils"))
implementation(project(":protocolApi"))
implementation(libs.androidx.core)
implementation(libs.androidx.activity)
implementation(libs.androidx.security.crypto)

View file

@ -3,6 +3,7 @@ agp = "8.1.3"
kotlin = "1.9.20"
androidx-core = "1.12.0"
androidx-activity = "1.8.1"
androidx-annotation = "1.7.0"
androidx-camera = "1.3.0"
androidx-security-crypto = "1.1.0-alpha06"
kotlinx-coroutines = "1.7.3"
@ -11,6 +12,7 @@ google-mlkit = "17.2.0"
[libraries]
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
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-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidx-camera" }
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidx-camera" }

View 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)
}

View 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)

View 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))

View 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
}
}

View file

@ -0,0 +1,9 @@
package org.amnezia.vpn.protocol
enum class ProtocolState {
CONNECTED,
CONNECTING,
DISCONNECTED,
DISCONNECTING,
UNKNOWN
}

View 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))
}

View 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))
}

View file

@ -31,6 +31,7 @@ rootProject.buildFileName = "build.gradle.kts"
include(":qt")
include(":utils")
include(":protocolApi")
// get values from gradle or local properties
val androidBuildToolsVersion: String by gradleProperties