Refactor split-tunneling: separate site addresses from routes

This commit is contained in:
albexk 2024-01-27 16:55:05 +03:00
parent b68c9cc145
commit 3afbc248b1
6 changed files with 76 additions and 54 deletions

View file

@ -99,7 +99,7 @@ class AwgConfig private constructor(
fun setH3(h3: Long) = apply { this.h3 = h3 }
fun setH4(h4: Long) = apply { this.h4 = h4 }
override fun build(): AwgConfig = AwgConfig(this)
override fun build(): AwgConfig = configBuild().run { AwgConfig(this@Builder) }
}
companion object {

View file

@ -2,7 +2,6 @@ package org.amnezia.vpn.protocol.openvpn
import android.content.Context
import android.net.VpnService.Builder
import android.os.Build
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@ -14,7 +13,6 @@ import org.amnezia.vpn.protocol.ProtocolState
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
import org.amnezia.vpn.protocol.Statistics
import org.amnezia.vpn.protocol.VpnStartException
import org.amnezia.vpn.util.net.InetNetwork
import org.amnezia.vpn.util.net.getLocalNetworks
import org.json.JSONObject
@ -79,16 +77,7 @@ open class OpenVpn : Protocol() {
if (evalConfig.error) {
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))
}
configSplitTunneling(config)
}
configBuilder.configSplitTunneling(config)
scope.launch {
val status = client.connect()

View file

@ -11,7 +11,7 @@ class OpenVpnConfig private constructor(
class Builder : ProtocolConfig.Builder(false) {
override var mtu: Int = OPENVPN_DEFAULT_MTU
override fun build(): OpenVpnConfig = OpenVpnConfig(this)
override fun build(): OpenVpnConfig = configBuild().run { OpenVpnConfig(this@Builder) }
}
companion object {

View file

@ -14,8 +14,6 @@ import java.util.zip.ZipFile
import kotlinx.coroutines.flow.MutableStateFlow
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
private const val TAG = "Protocol"
@ -53,40 +51,16 @@ abstract class Protocol {
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)
}
}
val addressHandlerFunc = when (splitTunnelType) {
SPLIT_TUNNEL_INCLUDE -> ::includeAddress
SPLIT_TUNNEL_EXCLUDE -> ::excludeAddress
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))
}
}
else -> throw BadConfigException("Unexpected value of the 'splitTunnelType' parameter: $splitTunnelType")
}
for (i in 0 until splitTunnelSites.length()) {
val address = InetNetwork.parse(splitTunnelSites.getString(i))
addressHandlerFunc(address)
}
}

View file

@ -5,6 +5,8 @@ import android.os.Build
import androidx.annotation.RequiresApi
import java.net.InetAddress
import org.amnezia.vpn.util.net.InetNetwork
import org.amnezia.vpn.util.net.IpRange
import org.amnezia.vpn.util.net.IpRangeSet
open class ProtocolConfig protected constructor(
val addresses: Set<InetNetwork>,
@ -12,6 +14,8 @@ open class ProtocolConfig protected constructor(
val searchDomain: String?,
val routes: Set<InetNetwork>,
val excludedRoutes: Set<InetNetwork>,
val includedAddresses: Set<InetNetwork>,
val excludedAddresses: Set<InetNetwork>,
val excludedApplications: Set<String>,
val httpProxy: ProxyInfo?,
val allowAllAF: Boolean,
@ -25,6 +29,8 @@ open class ProtocolConfig protected constructor(
builder.searchDomain,
builder.routes,
builder.excludedRoutes,
builder.includedAddresses,
builder.excludedAddresses,
builder.excludedApplications,
builder.httpProxy,
builder.allowAllAF,
@ -37,6 +43,8 @@ open class ProtocolConfig protected constructor(
internal val dnsServers: MutableSet<InetAddress> = hashSetOf()
internal val routes: MutableSet<InetNetwork> = hashSetOf()
internal val excludedRoutes: MutableSet<InetNetwork> = hashSetOf()
internal val includedAddresses: MutableSet<InetNetwork> = hashSetOf()
internal val excludedAddresses: MutableSet<InetNetwork> = hashSetOf()
internal val excludedApplications: MutableSet<String> = hashSetOf()
internal var searchDomain: String? = null
@ -71,12 +79,15 @@ open class ProtocolConfig protected constructor(
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 }
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
fun excludeRoutes(routes: Collection<InetNetwork>) = apply { this.excludedRoutes += routes }
fun includeAddress(addr: InetNetwork) = apply { this.includedAddresses += addr }
fun includeAddresses(addresses: Collection<InetNetwork>) = apply { this.includedAddresses += addresses }
fun excludeAddress(addr: InetNetwork) = apply { this.excludedAddresses += addr }
fun excludeAddresses(addresses: Collection<InetNetwork>) = apply { this.excludedAddresses += addresses }
fun excludeApplication(application: String) = apply { this.excludedApplications += application }
fun excludeApplications(applications: Collection<String>) = apply { this.excludedApplications += applications }
@ -91,6 +102,48 @@ open class ProtocolConfig protected constructor(
fun setMtu(mtu: Int) = apply { this.mtu = mtu }
private fun processSplitTunneling() {
if (includedAddresses.isNotEmpty() && excludedAddresses.isNotEmpty()) {
throw BadConfigException("Config contains addresses for inclusive and exclusive split tunneling at the same time")
}
if (includedAddresses.isNotEmpty()) {
// remove default routes, if any
removeRoute(InetNetwork("0.0.0.0", 0))
removeRoute(InetNetwork("::", 0))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
// for older versions of Android, add the default route to the excluded routes
// to correctly build the excluded subnets list later
excludeRoute(InetNetwork("0.0.0.0", 0))
}
addRoutes(includedAddresses)
} else if (excludedAddresses.isNotEmpty()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// default routes are required for split tunneling in newer versions of Android
addRoute(InetNetwork("0.0.0.0", 0))
addRoute(InetNetwork("::", 0))
}
excludeRoutes(excludedAddresses)
}
}
private fun processExcludedRoutes() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
// for older versions of Android, build a list of subnets without excluded routes
// and add them to routes
val ipRangeSet = IpRangeSet()
ipRangeSet.remove(IpRange("127.0.0.0", 8))
excludedRoutes.forEach {
ipRangeSet.remove(IpRange(it))
}
// remove default routes, if any
removeRoute(InetNetwork("0.0.0.0", 0))
removeRoute(InetNetwork("::", 0))
ipRangeSet.subnets().forEach(::addRoute)
addRoute(InetNetwork("2000::", 3))
}
}
private fun validate() {
val errorMessage = StringBuilder()
@ -103,7 +156,13 @@ open class ProtocolConfig protected constructor(
if (errorMessage.isNotEmpty()) throw BadConfigException(errorMessage.toString())
}
open fun build(): ProtocolConfig = validate().run { ProtocolConfig(this@Builder) }
protected fun configBuild() {
processSplitTunneling()
processExcludedRoutes()
validate()
}
open fun build(): ProtocolConfig = configBuild().run { ProtocolConfig(this@Builder) }
}
companion object {

View file

@ -75,7 +75,7 @@ open class WireguardConfig protected constructor(
fun setPrivateKeyHex(privateKeyHex: String) = apply { this.privateKeyHex = privateKeyHex }
override fun build(): WireguardConfig = WireguardConfig(this)
override fun build(): WireguardConfig = configBuild().run { WireguardConfig(this@Builder) }
}
companion object {