package org.amnezia.vpn.protocol import android.net.ProxyInfo 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, val dnsServers: Set, val searchDomain: String?, val routes: Set, val includedAddresses: Set, val excludedAddresses: Set, val includedApplications: Set, val excludedApplications: Set, val httpProxy: ProxyInfo?, val allowAllAF: Boolean, val blockingMode: Boolean, val mtu: Int ) { protected constructor(builder: Builder) : this( builder.addresses, builder.dnsServers, builder.searchDomain, builder.routes, builder.includedAddresses, builder.excludedAddresses, builder.includedApplications, builder.excludedApplications, builder.httpProxy, builder.allowAllAF, builder.blockingMode, builder.mtu ) open class Builder(blockingMode: Boolean) { internal val addresses: MutableSet = hashSetOf() internal val dnsServers: MutableSet = hashSetOf() internal val routes: MutableSet = mutableSetOf() internal val includedAddresses: MutableSet = hashSetOf() internal val excludedAddresses: MutableSet = hashSetOf() internal val includedApplications: MutableSet = hashSetOf() internal val excludedApplications: MutableSet = hashSetOf() internal var searchDomain: String? = null private set internal var httpProxy: ProxyInfo? = null private set internal var allowAllAF: Boolean = false private set internal var blockingMode: Boolean = blockingMode private set internal var allowSplitTunneling: Boolean = true private set open var mtu: Int = 0 protected set fun addAddress(addr: InetNetwork) = apply { this.addresses += addr } fun addAddresses(addresses: Collection) = apply { this.addresses += addresses } fun clearAddresses() = apply { this.addresses.clear() } fun addDnsServer(dnsServer: InetAddress) = apply { this.dnsServers += dnsServer } fun addDnsServers(dnsServers: Collection) = apply { this.dnsServers += dnsServers } fun setSearchDomain(domain: String) = apply { this.searchDomain = domain } fun addRoute(route: InetNetwork) = apply { this.routes += Route(route, true) } fun addRoutes(routes: Collection) = apply { this.routes += routes.map { Route(it, true) } } fun excludeRoute(route: InetNetwork) = apply { this.routes += Route(route, false) } fun excludeRoutes(routes: Collection) = apply { this.routes += routes.map { Route(it, false) } } fun removeRoute(route: InetNetwork) = apply { this.routes.removeIf { it.inetNetwork == route } } fun clearRoutes() = apply { this.routes.clear() } fun prependRoutes(block: Builder.() -> Unit) = apply { val savedRoutes = mutableListOf().apply { addAll(routes) } routes.clear() block() routes.addAll(savedRoutes) } fun includeAddress(addr: InetNetwork) = apply { this.includedAddresses += addr } fun includeAddresses(addresses: Collection) = apply { this.includedAddresses += addresses } fun excludeAddress(addr: InetNetwork) = apply { this.excludedAddresses += addr } fun excludeAddresses(addresses: Collection) = apply { this.excludedAddresses += addresses } fun includeApplication(application: String) = apply { this.includedApplications += application } fun includeApplications(applications: Collection) = apply { this.includedApplications += applications } fun excludeApplication(application: String) = apply { this.excludedApplications += application } fun excludeApplications(applications: Collection) = apply { this.excludedApplications += applications } @RequiresApi(Build.VERSION_CODES.Q) fun setHttpProxy(httpProxy: ProxyInfo) = apply { this.httpProxy = httpProxy } fun setAllowAllAF(allowAllAF: Boolean) = apply { this.allowAllAF = allowAllAF } fun setBlockingMode(blockingMode: Boolean) = apply { this.blockingMode = blockingMode } fun disableSplitTunneling() = apply { this.allowSplitTunneling = false } 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)) removeRoute(InetNetwork("2000::", 3)) prependRoutes { addRoutes(includedAddresses) } } else if (excludedAddresses.isNotEmpty()) { prependRoutes { addRoute(InetNetwork("0.0.0.0", 0)) addRoute(InetNetwork("2000::", 3)) excludeRoutes(excludedAddresses) } } } private fun processRoutes() { // replace ::/0 as it may cause LAN connection issues val ipv6DefaultRoute = InetNetwork("::", 0) if (routes.removeIf { it.include && it.inetNetwork == ipv6DefaultRoute }) { prependRoutes { addRoute(InetNetwork("2000::", 3)) } } // for older versions of Android, build a list of subnets without excluded routes // and add them to routes if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && routes.any { !it.include }) { val ipRangeSet = IpRangeSet() routes.forEach { if (it.include) ipRangeSet.add(IpRange(it.inetNetwork)) else ipRangeSet.remove(IpRange(it.inetNetwork)) } ipRangeSet.remove(IpRange("127.0.0.0", 8)) ipRangeSet.remove(IpRange("::1", 128)) routes.clear() ipRangeSet.subnets().forEach(::addRoute) } // filter ipv4 and ipv6 loopback addresses val ipv6Loopback = InetNetwork("::1", 128) routes.removeIf { it.include && if (it.inetNetwork.isIpv4) it.inetNetwork.address.address[0] == 127.toByte() else it.inetNetwork == ipv6Loopback } } 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()) } protected fun configBuild() { processSplitTunneling() processRoutes() validate() } open fun build(): ProtocolConfig = configBuild().run { ProtocolConfig(this@Builder) } } companion object { inline fun build(blockingMode: Boolean, block: Builder.() -> Unit): ProtocolConfig = Builder(blockingMode).apply(block).build() } } data class Route(val inetNetwork: InetNetwork, val include: Boolean)