Add split tunneling
This commit is contained in:
parent
20f3c0388a
commit
e7658f9859
14 changed files with 422 additions and 104 deletions
|
|
@ -65,6 +65,7 @@ class Awg : Wireguard() {
|
||||||
val configData = parseConfigData(configDataJson.getString("config"))
|
val configData = parseConfigData(configDataJson.getString("config"))
|
||||||
return AwgConfig.build {
|
return AwgConfig.build {
|
||||||
configWireguard(configData)
|
configWireguard(configData)
|
||||||
|
configSplitTunnel(config)
|
||||||
configData["Jc"]?.let { setJc(it.toInt()) }
|
configData["Jc"]?.let { setJc(it.toInt()) }
|
||||||
configData["Jmin"]?.let { setJmin(it.toInt()) }
|
configData["Jmin"]?.let { setJmin(it.toInt()) }
|
||||||
configData["Jmax"]?.let { setJmax(it.toInt()) }
|
configData["Jmax"]?.let { setJmax(it.toInt()) }
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package org.amnezia.vpn.protocol.openvpn
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.VpnService.Builder
|
import android.net.VpnService.Builder
|
||||||
|
import android.os.Build
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import net.openvpn.ovpn3.ClientAPI_Config
|
import net.openvpn.ovpn3.ClientAPI_Config
|
||||||
import org.amnezia.vpn.protocol.BadConfigException
|
import org.amnezia.vpn.protocol.BadConfigException
|
||||||
|
|
@ -10,7 +11,8 @@ import org.amnezia.vpn.protocol.ProtocolState
|
||||||
import org.amnezia.vpn.protocol.Statistics
|
import org.amnezia.vpn.protocol.Statistics
|
||||||
import org.amnezia.vpn.protocol.VpnException
|
import org.amnezia.vpn.protocol.VpnException
|
||||||
import org.amnezia.vpn.protocol.VpnStartException
|
import org.amnezia.vpn.protocol.VpnStartException
|
||||||
import org.amnezia.vpn.util.NetworkUtils
|
import org.amnezia.vpn.util.net.InetNetwork
|
||||||
|
import org.amnezia.vpn.util.net.getLocalNetworks
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -59,7 +61,7 @@ open class OpenVpn : Protocol() {
|
||||||
openVpnClient = OpenVpnClient(
|
openVpnClient = OpenVpnClient(
|
||||||
configBuilder,
|
configBuilder,
|
||||||
state,
|
state,
|
||||||
{ ipv6 -> NetworkUtils.getLocalNetworks(context, ipv6) },
|
{ ipv6 -> getLocalNetworks(context, ipv6) },
|
||||||
makeEstablish(configBuilder, vpnBuilder),
|
makeEstablish(configBuilder, vpnBuilder),
|
||||||
protect
|
protect
|
||||||
)
|
)
|
||||||
|
|
@ -71,6 +73,16 @@ open class OpenVpn : Protocol() {
|
||||||
if (evalConfig.error) {
|
if (evalConfig.error) {
|
||||||
throw BadConfigException("OpenVPN config parse error: ${evalConfig.message}")
|
throw BadConfigException("OpenVPN config parse error: ${evalConfig.message}")
|
||||||
}
|
}
|
||||||
|
configBuilder.apply {
|
||||||
|
// fix for split tunneling
|
||||||
|
// The exclude split tunneling OpenVpn configuration does not contain a default route.
|
||||||
|
// It is required for split tunneling in newer versions of Android.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
addRoute(InetNetwork("0.0.0.0", 0))
|
||||||
|
addRoute(InetNetwork("::", 0))
|
||||||
|
}
|
||||||
|
configSplitTunnel(config)
|
||||||
|
}
|
||||||
|
|
||||||
val status = client.connect()
|
val status = client.connect()
|
||||||
if (status.error) {
|
if (status.error) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package org.amnezia.vpn.protocol.openvpn
|
package org.amnezia.vpn.protocol.openvpn
|
||||||
|
|
||||||
import android.net.ProxyInfo
|
import android.net.ProxyInfo
|
||||||
|
import android.os.Build
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import net.openvpn.ovpn3.ClientAPI_Config
|
import net.openvpn.ovpn3.ClientAPI_Config
|
||||||
import net.openvpn.ovpn3.ClientAPI_EvalConfig
|
import net.openvpn.ovpn3.ClientAPI_EvalConfig
|
||||||
|
|
@ -14,9 +15,9 @@ import org.amnezia.vpn.protocol.ProtocolState
|
||||||
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
|
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
|
||||||
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
|
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
|
||||||
import org.amnezia.vpn.protocol.VpnStartException
|
import org.amnezia.vpn.protocol.VpnStartException
|
||||||
import org.amnezia.vpn.util.InetNetwork
|
|
||||||
import org.amnezia.vpn.util.Log
|
import org.amnezia.vpn.util.Log
|
||||||
import org.amnezia.vpn.util.parseInetAddress
|
import org.amnezia.vpn.util.net.InetNetwork
|
||||||
|
import org.amnezia.vpn.util.net.parseInetAddress
|
||||||
|
|
||||||
private const val TAG = "OpenVpnClient"
|
private const val TAG = "OpenVpnClient"
|
||||||
private const val EMULATED_EXCLUDE_ROUTES = (1 shl 16)
|
private const val EMULATED_EXCLUDE_ROUTES = (1 shl 16)
|
||||||
|
|
@ -87,7 +88,9 @@ class OpenVpnClient(
|
||||||
// metric is optional and should be ignored if < 0
|
// metric is optional and should be ignored if < 0
|
||||||
override fun tun_builder_exclude_route(address: String, prefix_length: Int, metric: Int, ipv6: Boolean): Boolean {
|
override fun tun_builder_exclude_route(address: String, prefix_length: Int, metric: Int, ipv6: Boolean): Boolean {
|
||||||
Log.v(TAG, "tun_builder_exclude_route: $address, $prefix_length, $metric, $ipv6")
|
Log.v(TAG, "tun_builder_exclude_route: $address, $prefix_length, $metric, $ipv6")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
configBuilder.excludeRoute(InetNetwork(address, prefix_length))
|
configBuilder.excludeRoute(InetNetwork(address, prefix_length))
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,12 +182,14 @@ class OpenVpnClient(
|
||||||
// Never called more than once per tun_builder session.
|
// Never called more than once per tun_builder session.
|
||||||
override fun tun_builder_set_proxy_http(host: String, port: Int): Boolean {
|
override fun tun_builder_set_proxy_http(host: String, port: Int): Boolean {
|
||||||
Log.v(TAG, "tun_builder_set_proxy_http: $host, $port")
|
Log.v(TAG, "tun_builder_set_proxy_http: $host, $port")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
try {
|
try {
|
||||||
configBuilder.setHttpProxy(ProxyInfo.buildDirectProxy(host, port))
|
configBuilder.setHttpProxy(ProxyInfo.buildDirectProxy(host, port))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Could not set proxy: ${e.message}")
|
Log.e(TAG, "Could not set proxy: ${e.message}")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,20 @@ import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import org.amnezia.vpn.util.InetNetwork
|
|
||||||
import org.amnezia.vpn.util.Log
|
import org.amnezia.vpn.util.Log
|
||||||
|
import org.amnezia.vpn.util.net.InetNetwork
|
||||||
|
import org.amnezia.vpn.util.net.IpRange
|
||||||
|
import org.amnezia.vpn.util.net.IpRangeSet
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
private const val TAG = "Protocol"
|
private const val TAG = "Protocol"
|
||||||
|
|
||||||
const val VPN_SESSION_NAME = "AmneziaVPN"
|
const val VPN_SESSION_NAME = "AmneziaVPN"
|
||||||
|
|
||||||
|
private const val SPLIT_TUNNEL_DISABLE = 0
|
||||||
|
private const val SPLIT_TUNNEL_INCLUDE = 1
|
||||||
|
private const val SPLIT_TUNNEL_EXCLUDE = 2
|
||||||
|
|
||||||
abstract class Protocol {
|
abstract class Protocol {
|
||||||
|
|
||||||
abstract val statistics: Statistics
|
abstract val statistics: Statistics
|
||||||
|
|
@ -33,6 +39,47 @@ abstract class Protocol {
|
||||||
|
|
||||||
abstract fun stopVpn()
|
abstract fun stopVpn()
|
||||||
|
|
||||||
|
protected fun ProtocolConfig.Builder.configSplitTunnel(config: JSONObject) {
|
||||||
|
val splitTunnelType = config.optInt("splitTunnelType")
|
||||||
|
if (splitTunnelType == SPLIT_TUNNEL_DISABLE) return
|
||||||
|
val splitTunnelSites = config.getJSONArray("splitTunnelSites")
|
||||||
|
when (splitTunnelType) {
|
||||||
|
SPLIT_TUNNEL_INCLUDE -> {
|
||||||
|
// remove default routes, if any
|
||||||
|
removeRoute(InetNetwork("0.0.0.0", 0))
|
||||||
|
removeRoute(InetNetwork("::", 0))
|
||||||
|
// add routes from config
|
||||||
|
for (i in 0 until splitTunnelSites.length()) {
|
||||||
|
val address = InetNetwork.parse(splitTunnelSites.getString(i))
|
||||||
|
addRoute(address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SPLIT_TUNNEL_EXCLUDE -> {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
// exclude routes from config
|
||||||
|
for (i in 0 until splitTunnelSites.length()) {
|
||||||
|
val address = InetNetwork.parse(splitTunnelSites.getString(i))
|
||||||
|
excludeRoute(address)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For older versions of Android, build a list of subnets without excluded addresses
|
||||||
|
val ipRangeSet = IpRangeSet()
|
||||||
|
ipRangeSet.remove(IpRange("127.0.0.0", 8))
|
||||||
|
for (i in 0 until splitTunnelSites.length()) {
|
||||||
|
val address = InetNetwork.parse(splitTunnelSites.getString(i))
|
||||||
|
ipRangeSet.remove(IpRange(address))
|
||||||
|
}
|
||||||
|
// remove default routes, if any
|
||||||
|
removeRoute(InetNetwork("0.0.0.0", 0))
|
||||||
|
removeRoute(InetNetwork("::", 0))
|
||||||
|
ipRangeSet.subnets().forEach(::addRoute)
|
||||||
|
addRoute(InetNetwork("2000::", 3))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected open fun buildVpnInterface(config: ProtocolConfig, vpnBuilder: Builder) {
|
protected open fun buildVpnInterface(config: ProtocolConfig, vpnBuilder: Builder) {
|
||||||
vpnBuilder.setSession(VPN_SESSION_NAME)
|
vpnBuilder.setSession(VPN_SESSION_NAME)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
package org.amnezia.vpn.protocol
|
package org.amnezia.vpn.protocol
|
||||||
|
|
||||||
import android.net.ProxyInfo
|
import android.net.ProxyInfo
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import org.amnezia.vpn.util.InetNetwork
|
import org.amnezia.vpn.util.net.InetNetwork
|
||||||
|
|
||||||
open class ProtocolConfig protected constructor(
|
open class ProtocolConfig protected constructor(
|
||||||
val addresses: Set<InetNetwork>,
|
val addresses: Set<InetNetwork>,
|
||||||
|
|
@ -62,13 +64,19 @@ open class ProtocolConfig protected constructor(
|
||||||
|
|
||||||
fun addRoute(route: InetNetwork) = apply { this.routes += route }
|
fun addRoute(route: InetNetwork) = apply { this.routes += route }
|
||||||
fun addRoutes(routes: List<InetNetwork>) = apply { this.routes += routes }
|
fun addRoutes(routes: List<InetNetwork>) = apply { this.routes += routes }
|
||||||
|
fun removeRoute(route: InetNetwork) = apply { this.routes.remove(route) }
|
||||||
|
fun clearRoutes() = apply { this.routes.clear() }
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||||
fun excludeRoute(route: InetNetwork) = apply { this.excludedRoutes += route }
|
fun excludeRoute(route: InetNetwork) = apply { this.excludedRoutes += route }
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||||
fun excludeRoutes(routes: List<InetNetwork>) = apply { this.excludedRoutes += routes }
|
fun excludeRoutes(routes: List<InetNetwork>) = apply { this.excludedRoutes += routes }
|
||||||
|
|
||||||
fun excludeApplication(application: String) = apply { this.excludedApplications += application }
|
fun excludeApplication(application: String) = apply { this.excludedApplications += application }
|
||||||
fun excludeApplications(applications: List<String>) = apply { this.excludedApplications += applications }
|
fun excludeApplications(applications: List<String>) = apply { this.excludedApplications += applications }
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
fun setHttpProxy(httpProxy: ProxyInfo) = apply { this.httpProxy = httpProxy }
|
fun setHttpProxy(httpProxy: ProxyInfo) = apply { this.httpProxy = httpProxy }
|
||||||
|
|
||||||
fun setAllowAllAF(allowAllAF: Boolean) = apply { this.allowAllAF = allowAllAF }
|
fun setAllowAllAF(allowAllAF: Boolean) = apply { this.allowAllAF = allowAllAF }
|
||||||
|
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
package org.amnezia.vpn.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.net.InetAddresses
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.os.Build
|
|
||||||
import java.net.Inet4Address
|
|
||||||
import java.net.Inet6Address
|
|
||||||
import java.net.InetAddress
|
|
||||||
|
|
||||||
object NetworkUtils {
|
|
||||||
|
|
||||||
fun getLocalNetworks(context: Context, ipv6: Boolean): List<InetNetwork> {
|
|
||||||
val connectivityManager = context.getSystemService(ConnectivityManager::class.java)
|
|
||||||
connectivityManager.activeNetwork?.let { network ->
|
|
||||||
val netCapabilities = connectivityManager.getNetworkCapabilities(network)
|
|
||||||
val linkProperties = connectivityManager.getLinkProperties(network)
|
|
||||||
if (linkProperties == null ||
|
|
||||||
netCapabilities == null ||
|
|
||||||
netCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
|
|
||||||
netCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
|
||||||
) return emptyList()
|
|
||||||
|
|
||||||
val addresses = mutableListOf<InetNetwork>()
|
|
||||||
|
|
||||||
for (linkAddress in linkProperties.linkAddresses) {
|
|
||||||
val address = linkAddress.address
|
|
||||||
if ((!ipv6 && address is Inet4Address) || (ipv6 && address is Inet6Address)) {
|
|
||||||
addresses += InetNetwork(address, linkAddress.prefixLength)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return addresses
|
|
||||||
}
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class InetNetwork(val address: InetAddress, val mask: Int) {
|
|
||||||
|
|
||||||
constructor(address: String, mask: Int) : this(parseInetAddress(address), mask)
|
|
||||||
|
|
||||||
constructor(address: InetAddress) : this(address, address.maxPrefixLength)
|
|
||||||
|
|
||||||
constructor(address: String) : this(parseInetAddress(address))
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val InetAddress.maxPrefixLength: Int
|
|
||||||
get() = if (this is Inet4Address) 32 else 128
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
17
client/android/utils/src/main/kotlin/net/InetEndpoint.kt
Normal file
17
client/android/utils/src/main/kotlin/net/InetEndpoint.kt
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
package org.amnezia.vpn.util.net
|
||||||
|
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
client/android/utils/src/main/kotlin/net/InetNetwork.kt
Normal file
26
client/android/utils/src/main/kotlin/net/InetNetwork.kt
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
package org.amnezia.vpn.util.net
|
||||||
|
|
||||||
|
import java.net.Inet4Address
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
data class InetNetwork(val address: InetAddress, val mask: Int) {
|
||||||
|
|
||||||
|
constructor(address: String, mask: Int) : this(parseInetAddress(address), mask)
|
||||||
|
|
||||||
|
constructor(address: InetAddress) : this(address, address.maxPrefixLength)
|
||||||
|
|
||||||
|
override fun toString(): String = "${address.hostAddress}/$mask"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parse(data: String): InetNetwork {
|
||||||
|
val split = data.split("/")
|
||||||
|
val address = parseInetAddress(split.first())
|
||||||
|
if (split.size == 1) return InetNetwork(address)
|
||||||
|
val mask = split.last().toInt()
|
||||||
|
return InetNetwork(address, mask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val InetAddress.maxPrefixLength: Int
|
||||||
|
get() = if (this is Inet4Address) 32 else 128
|
||||||
85
client/android/utils/src/main/kotlin/net/IpAddress.kt
Normal file
85
client/android/utils/src/main/kotlin/net/IpAddress.kt
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
package org.amnezia.vpn.util.net
|
||||||
|
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
@OptIn(ExperimentalUnsignedTypes::class)
|
||||||
|
class IpAddress private constructor(private val address: UByteArray) : Comparable<IpAddress> {
|
||||||
|
|
||||||
|
val size: Int = address.size
|
||||||
|
val lastIndex: Int = address.lastIndex
|
||||||
|
val maxMask: Int = size * 8
|
||||||
|
|
||||||
|
constructor(inetAddress: InetAddress) : this(inetAddress.address.asUByteArray())
|
||||||
|
|
||||||
|
constructor(ipAddress: String) : this(parseInetAddress(ipAddress))
|
||||||
|
|
||||||
|
operator fun get(i: Int): UByte = address[i]
|
||||||
|
|
||||||
|
operator fun set(i: Int, b: UByte) { address[i] = b }
|
||||||
|
|
||||||
|
fun fill(value: UByte, fromByte: Int) = address.fill(value, fromByte)
|
||||||
|
|
||||||
|
fun copy(): IpAddress = IpAddress(address.copyOf())
|
||||||
|
|
||||||
|
fun inc(): IpAddress {
|
||||||
|
if (address.all { it == 0xffu.toUByte() }) {
|
||||||
|
throw RuntimeException("IP address overflow")
|
||||||
|
}
|
||||||
|
val copy = copy()
|
||||||
|
for (i in copy.lastIndex downTo 0) {
|
||||||
|
if (++copy[i] != 0u.toUByte()) break
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dec(): IpAddress {
|
||||||
|
if (address.all { it == 0u.toUByte() }) {
|
||||||
|
throw RuntimeException("IP address overflow")
|
||||||
|
}
|
||||||
|
val copy = copy()
|
||||||
|
for (i in copy.lastIndex downTo 0) {
|
||||||
|
if (--copy[i] != 0xffu.toUByte()) break
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isMaxIp(): Boolean = address.all { it == 0xffu.toUByte() }
|
||||||
|
|
||||||
|
override fun compareTo(other: IpAddress): Int {
|
||||||
|
if (size != other.size) return size - other.size
|
||||||
|
for (i in address.indices) {
|
||||||
|
val d = (address[i] - other.address[i]).toInt()
|
||||||
|
if (d != 0) return d
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
other as IpAddress
|
||||||
|
return compareTo(other) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return address.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
if (size > 4) return toIpv6String()
|
||||||
|
return address.joinToString(".")
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
private fun toIpv6String(): String {
|
||||||
|
val sb = StringBuilder()
|
||||||
|
var i = 0
|
||||||
|
while (i < size) {
|
||||||
|
sb.append(address[i++].toHexString())
|
||||||
|
sb.append(address[i++].toHexString())
|
||||||
|
sb.append(':')
|
||||||
|
}
|
||||||
|
sb.deleteAt(sb.lastIndex)
|
||||||
|
return sb.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
119
client/android/utils/src/main/kotlin/net/IpRange.kt
Normal file
119
client/android/utils/src/main/kotlin/net/IpRange.kt
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
package org.amnezia.vpn.util.net
|
||||||
|
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
class IpRange(private val start: IpAddress, private val end: IpAddress) : Comparable<IpRange> {
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (start > end) throw IllegalArgumentException("Start IP: $start is greater then end IP: $end")
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(addresses: Pair<IpAddress, IpAddress>) : this(addresses.first, addresses.second)
|
||||||
|
|
||||||
|
constructor(inetAddress: InetAddress, mask: Int) : this(from(inetAddress, mask))
|
||||||
|
|
||||||
|
constructor(address: String, mask: Int) : this(parseInetAddress(address), mask)
|
||||||
|
|
||||||
|
constructor(inetNetwork: InetNetwork) : this(from(inetNetwork))
|
||||||
|
|
||||||
|
private operator fun contains(other: IpRange): Boolean =
|
||||||
|
(start <= other.start) && (end >= other.end)
|
||||||
|
|
||||||
|
private fun isIntersect(other: IpRange): Boolean =
|
||||||
|
(start <= other.end) && (end >= other.start)
|
||||||
|
|
||||||
|
operator fun minus(other: IpRange): List<IpRange>? {
|
||||||
|
if (this in other) return emptyList()
|
||||||
|
if (!isIntersect(other)) return null
|
||||||
|
val resultRanges = mutableListOf<IpRange>()
|
||||||
|
if (start < other.start) resultRanges += IpRange(start, other.start.dec())
|
||||||
|
if (end > other.end) resultRanges += IpRange(other.end.inc(), end)
|
||||||
|
return resultRanges
|
||||||
|
}
|
||||||
|
|
||||||
|
fun subnets(): List<InetNetwork> {
|
||||||
|
var currentIp = start
|
||||||
|
var mask: Int
|
||||||
|
val subnets = mutableListOf<InetNetwork>()
|
||||||
|
while (currentIp <= end) {
|
||||||
|
mask = getPossibleMaxMask(currentIp)
|
||||||
|
var lastIp = getLastIpForMask(currentIp, mask)
|
||||||
|
while (lastIp > end) {
|
||||||
|
lastIp = getLastIpForMask(currentIp, ++mask)
|
||||||
|
}
|
||||||
|
subnets.add(InetNetwork(currentIp.toString(), mask))
|
||||||
|
if (lastIp.isMaxIp()) break
|
||||||
|
currentIp = lastIp.inc()
|
||||||
|
}
|
||||||
|
return subnets
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPossibleMaxMask(ip: IpAddress): Int {
|
||||||
|
var mask = ip.maxMask
|
||||||
|
for (i in ip.lastIndex downTo 0) {
|
||||||
|
val lastZeroBits = ip[i].countTrailingZeroBits()
|
||||||
|
mask -= lastZeroBits
|
||||||
|
if (lastZeroBits != 8) break
|
||||||
|
}
|
||||||
|
return mask
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLastIpForMask(ip: IpAddress, mask: Int): IpAddress {
|
||||||
|
var remainingBits = ip.maxMask - mask
|
||||||
|
if (remainingBits == 0) return ip
|
||||||
|
var i = ip.lastIndex
|
||||||
|
val lastIp = ip.copy()
|
||||||
|
while (remainingBits > 0 && i >= 0) {
|
||||||
|
lastIp[i] =
|
||||||
|
if (remainingBits > 8) {
|
||||||
|
lastIp[i] or 0xffu
|
||||||
|
} else {
|
||||||
|
lastIp[i] or ((0xffu shl remainingBits).toUByte().inv())
|
||||||
|
}
|
||||||
|
remainingBits -= 8
|
||||||
|
--i
|
||||||
|
}
|
||||||
|
return lastIp
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun compareTo(other: IpRange): Int {
|
||||||
|
val d = start.compareTo(other.start)
|
||||||
|
return if (d == 0) end.compareTo(other.end) else d
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
other as IpRange
|
||||||
|
return compareTo(other) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = start.hashCode()
|
||||||
|
result = 31 * result + end.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$start - $end"
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private fun from(inetAddress: InetAddress, mask: Int): Pair<IpAddress, IpAddress> {
|
||||||
|
val start = IpAddress(inetAddress)
|
||||||
|
val end = IpAddress(inetAddress)
|
||||||
|
val lastByte = mask / 8
|
||||||
|
if (lastByte < start.size) {
|
||||||
|
val byteMask = (0xffu shl (8 - mask % 8)).toUByte()
|
||||||
|
start[lastByte] = start[lastByte].and(byteMask)
|
||||||
|
end[lastByte] = end[lastByte].or(byteMask.inv())
|
||||||
|
start.fill(0u, lastByte + 1)
|
||||||
|
end.fill(0xffu, lastByte + 1)
|
||||||
|
}
|
||||||
|
return Pair(start, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun from(inetNetwork: InetNetwork): Pair<IpAddress, IpAddress> =
|
||||||
|
from(inetNetwork.address, inetNetwork.mask)
|
||||||
|
}
|
||||||
|
}
|
||||||
26
client/android/utils/src/main/kotlin/net/IpRangeSet.kt
Normal file
26
client/android/utils/src/main/kotlin/net/IpRangeSet.kt
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
package org.amnezia.vpn.util.net
|
||||||
|
|
||||||
|
class IpRangeSet(ipRange: IpRange = IpRange("0.0.0.0", 0)) {
|
||||||
|
|
||||||
|
private val ranges = sortedSetOf(ipRange)
|
||||||
|
|
||||||
|
fun remove(ipRange: IpRange) {
|
||||||
|
val iterator = ranges.iterator()
|
||||||
|
val splitRanges = mutableListOf<IpRange>()
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val range = iterator.next()
|
||||||
|
(range - ipRange)?.let { resultRanges ->
|
||||||
|
iterator.remove()
|
||||||
|
splitRanges += resultRanges
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ranges += splitRanges
|
||||||
|
}
|
||||||
|
|
||||||
|
fun subnets(): List<InetNetwork> =
|
||||||
|
ranges.map(IpRange::subnets).flatten()
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return ranges.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
46
client/android/utils/src/main/kotlin/net/NetworkUtils.kt
Normal file
46
client/android/utils/src/main/kotlin/net/NetworkUtils.kt
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
package org.amnezia.vpn.util.net
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.InetAddresses
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.os.Build
|
||||||
|
import java.net.Inet4Address
|
||||||
|
import java.net.Inet6Address
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
fun getLocalNetworks(context: Context, ipv6: Boolean): List<InetNetwork> {
|
||||||
|
val connectivityManager = context.getSystemService(ConnectivityManager::class.java)
|
||||||
|
connectivityManager.activeNetwork?.let { network ->
|
||||||
|
val netCapabilities = connectivityManager.getNetworkCapabilities(network)
|
||||||
|
val linkProperties = connectivityManager.getLinkProperties(network)
|
||||||
|
if (linkProperties == null ||
|
||||||
|
netCapabilities == null ||
|
||||||
|
netCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
|
||||||
|
netCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||||
|
) return emptyList()
|
||||||
|
|
||||||
|
val addresses = mutableListOf<InetNetwork>()
|
||||||
|
|
||||||
|
for (linkAddress in linkProperties.linkAddresses) {
|
||||||
|
val address = linkAddress.address
|
||||||
|
if ((!ipv6 && address is Inet4Address) || (ipv6 && address is Inet6Address)) {
|
||||||
|
addresses += InetNetwork(address, linkAddress.prefixLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addresses
|
||||||
|
}
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,10 +11,10 @@ import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
|
||||||
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
|
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
|
||||||
import org.amnezia.vpn.protocol.Statistics
|
import org.amnezia.vpn.protocol.Statistics
|
||||||
import org.amnezia.vpn.protocol.VpnStartException
|
import org.amnezia.vpn.protocol.VpnStartException
|
||||||
import org.amnezia.vpn.util.InetEndpoint
|
|
||||||
import org.amnezia.vpn.util.InetNetwork
|
|
||||||
import org.amnezia.vpn.util.Log
|
import org.amnezia.vpn.util.Log
|
||||||
import org.amnezia.vpn.util.parseInetAddress
|
import org.amnezia.vpn.util.net.InetEndpoint
|
||||||
|
import org.amnezia.vpn.util.net.InetNetwork
|
||||||
|
import org.amnezia.vpn.util.net.parseInetAddress
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -92,7 +92,20 @@ open class Wireguard : Protocol() {
|
||||||
protected open fun parseConfig(config: JSONObject): WireguardConfig {
|
protected open fun parseConfig(config: JSONObject): WireguardConfig {
|
||||||
val configDataJson = config.getJSONObject("wireguard_config_data")
|
val configDataJson = config.getJSONObject("wireguard_config_data")
|
||||||
val configData = parseConfigData(configDataJson.getString("config"))
|
val configData = parseConfigData(configDataJson.getString("config"))
|
||||||
return WireguardConfig.build { configWireguard(configData) }
|
return WireguardConfig.build {
|
||||||
|
configWireguard(configData)
|
||||||
|
// Default Wireguard routes (0.0.0.0/0, ::/0) will be removed,
|
||||||
|
// allowed routes from the Wireguard configuration will be merged
|
||||||
|
// with allowed routes from the split tunneling configuration.
|
||||||
|
//
|
||||||
|
// Excluded routes from the split tunneling configuration can overwrite
|
||||||
|
// allowed routes from the Wireguard configuration (two routes are equal
|
||||||
|
// if they have the same address and prefix).
|
||||||
|
//
|
||||||
|
// If multiple routes match the packet destination,
|
||||||
|
// route with the longest prefix takes precedence
|
||||||
|
configSplitTunnel(config)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun WireguardConfig.Builder.configWireguard(configData: Map<String, String>) {
|
protected fun WireguardConfig.Builder.configWireguard(configData: Map<String, String>) {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ package org.amnezia.vpn.protocol.wireguard
|
||||||
|
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import org.amnezia.vpn.protocol.ProtocolConfig
|
import org.amnezia.vpn.protocol.ProtocolConfig
|
||||||
import org.amnezia.vpn.util.InetEndpoint
|
import org.amnezia.vpn.util.net.InetEndpoint
|
||||||
|
|
||||||
private const val WIREGUARD_DEFAULT_MTU = 1280
|
private const val WIREGUARD_DEFAULT_MTU = 1280
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue