Android notification and routing (#797)

Android notification and routing
This commit is contained in:
albexk 2024-05-12 18:04:14 +03:00 committed by GitHub
parent ff348a348c
commit abb3c918e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 1108 additions and 543 deletions

View file

@ -1,16 +1,21 @@
package org.amnezia.vpn.util.net
import java.net.Inet4Address
import java.net.InetAddress
data class InetEndpoint(val address: InetAddress, val port: Int) {
override fun toString(): String = "${address.hostAddress}:$port"
override fun toString(): String = if (address is Inet4Address) {
"${address.ip}:$port"
} else {
"[${address.ip}]:$port"
}
companion object {
fun parse(data: String): InetEndpoint {
val split = data.split(":")
val address = parseInetAddress(split.first())
val port = split.last().toInt()
val i = data.lastIndexOf(':')
val address = parseInetAddress(data.substring(0, i))
val port = data.substring(i + 1).toInt()
return InetEndpoint(address, port)
}
}

View file

@ -9,7 +9,11 @@ data class InetNetwork(val address: InetAddress, val mask: Int) {
constructor(address: InetAddress) : this(address, address.maxPrefixLength)
override fun toString(): String = "${address.hostAddress}/$mask"
val isIpv4: Boolean = address is Inet4Address
val isIpv6: Boolean
get() = !isIpv4
override fun toString(): String = "${address.ip}/$mask"
companion object {
fun parse(data: String): InetNetwork {

View file

@ -3,12 +3,17 @@ package org.amnezia.vpn.util.net
import java.net.InetAddress
@OptIn(ExperimentalUnsignedTypes::class)
class IpAddress private constructor(private val address: UByteArray) : Comparable<IpAddress> {
internal 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
@OptIn(ExperimentalStdlibApi::class)
val hexFormat: HexFormat by lazy {
HexFormat { number.removeLeadingZeros = true }
}
constructor(inetAddress: InetAddress) : this(inetAddress.address.asUByteArray())
constructor(ipAddress: String) : this(parseInetAddress(ipAddress))
@ -43,6 +48,8 @@ class IpAddress private constructor(private val address: UByteArray) : Comparabl
return copy
}
fun isMinIp(): Boolean = address.all { it == 0x00u.toUByte() }
fun isMaxIp(): Boolean = address.all { it == 0xffu.toUByte() }
override fun compareTo(other: IpAddress): Int {
@ -74,12 +81,14 @@ class IpAddress private constructor(private val address: UByteArray) : Comparabl
private fun toIpv6String(): String {
val sb = StringBuilder()
var i = 0
var block: Int
while (i < size) {
sb.append(address[i++].toHexString())
sb.append(address[i++].toHexString())
block = address[i++].toInt() shl 8
block += address[i++].toInt()
sb.append(block.toHexString(hexFormat))
sb.append(':')
}
sb.deleteAt(sb.lastIndex)
return sb.toString()
return convertIpv6ToCanonicalForm(sb.toString())
}
}

View file

@ -2,14 +2,24 @@ package org.amnezia.vpn.util.net
import java.net.InetAddress
class IpRange(private val start: IpAddress, private val end: IpAddress) : Comparable<IpRange> {
class IpRange internal constructor(
internal val start: IpAddress,
internal val end: IpAddress
) : Comparable<IpRange> {
init {
if (start > end) throw IllegalArgumentException("Start IP: $start is greater then end IP: $end")
if (start.size != end.size) {
throw IllegalArgumentException(
"Unable to create a range between IPv4 and IPv6 addresses (start IP: [$start], end IP: [$end])"
)
}
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)
internal constructor(ipAddress: IpAddress) : this(ipAddress, ipAddress)
constructor(inetAddress: InetAddress, mask: Int) : this(from(inetAddress, mask))
constructor(address: String, mask: Int) : this(parseInetAddress(address), mask)
@ -22,6 +32,13 @@ class IpRange(private val start: IpAddress, private val end: IpAddress) : Compar
private fun isIntersect(other: IpRange): Boolean =
(start <= other.end) && (end >= other.start)
operator fun plus(other: IpRange): IpRange? {
if (start > other.end && !start.isMinIp() && start.dec() == other.end) return IpRange(other.start, end)
if (end < other.start && !end.isMaxIp() && end.inc() == other.start) return IpRange(start, other.end)
if (!isIntersect(other)) return null
return IpRange(minOf(start, other.start), maxOf(end, other.end))
}
operator fun minus(other: IpRange): List<IpRange>? {
if (this in other) return emptyList()
if (!isIntersect(other)) return null
@ -94,9 +111,7 @@ class IpRange(private val start: IpAddress, private val end: IpAddress) : Compar
return result
}
override fun toString(): String {
return "$start - $end"
}
override fun toString(): String = if (start == end) "<$start>" else "<$start - $end>"
companion object {
private fun from(inetAddress: InetAddress, mask: Int): Pair<IpAddress, IpAddress> {

View file

@ -1,15 +1,35 @@
package org.amnezia.vpn.util.net
class IpRangeSet(ipRange: IpRange = IpRange("0.0.0.0", 0)) {
class IpRangeSet {
private val ranges = sortedSetOf(ipRange)
private val ranges = sortedSetOf<IpRange>()
fun add(ipRange: IpRange) {
val iterator = ranges.iterator()
var rangeToAdd = ipRange
run {
while (iterator.hasNext()) {
val curRange = iterator.next()
if (rangeToAdd.end < curRange.start &&
!rangeToAdd.end.isMaxIp() &&
rangeToAdd.end.inc() != curRange.start) break
(curRange + rangeToAdd)?.let { resultRange ->
if (resultRange == curRange) return@run
iterator.remove()
rangeToAdd = resultRange
}
}
ranges += rangeToAdd
}
}
fun remove(ipRange: IpRange) {
val iterator = ranges.iterator()
val splitRanges = mutableListOf<IpRange>()
while (iterator.hasNext()) {
val range = iterator.next()
(range - ipRange)?.let { resultRanges ->
val curRange = iterator.next()
if (ipRange.end < curRange.start) break
(curRange - ipRange)?.let { resultRanges ->
iterator.remove()
splitRanges += resultRanges
}
@ -17,10 +37,7 @@ class IpRangeSet(ipRange: IpRange = IpRange("0.0.0.0", 0)) {
ranges += splitRanges
}
fun subnets(): List<InetNetwork> =
ranges.map(IpRange::subnets).flatten()
fun subnets(): List<InetNetwork> = ranges.map(IpRange::subnets).flatten()
override fun toString(): String {
return ranges.toString()
}
override fun toString(): String = ranges.toString()
}

View file

@ -10,7 +10,9 @@ import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
import android.net.NetworkRequest
import android.os.Build
import android.os.Handler
import androidx.core.content.getSystemService
import kotlin.LazyThreadSafetyMode.NONE
import kotlinx.coroutines.delay
import org.amnezia.vpn.util.Log
private const val TAG = "NetworkState"
@ -28,7 +30,7 @@ class NetworkState(
}
private val connectivityManager: ConnectivityManager by lazy(NONE) {
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
context.getSystemService<ConnectivityManager>()!!
}
private val networkRequest: NetworkRequest by lazy(NONE) {
@ -80,13 +82,24 @@ class NetworkState(
}
}
fun bindNetworkListener() {
suspend fun bindNetworkListener() {
if (isListenerBound) return
Log.d(TAG, "Bind network listener")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
connectivityManager.registerBestMatchingNetworkCallback(networkRequest, networkCallback, handler)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
connectivityManager.requestNetwork(networkRequest, networkCallback, handler)
try {
connectivityManager.requestNetwork(networkRequest, networkCallback, handler)
} catch (e: SecurityException) {
Log.e(TAG, "Failed to bind network listener: $e")
// Android 11 bug: https://issuetracker.google.com/issues/175055271
if (e.message?.startsWith("Package android does not belong to") == true) {
delay(1000)
connectivityManager.requestNetwork(networkRequest, networkCallback, handler)
} else {
throw e
}
}
} else {
connectivityManager.requestNetwork(networkRequest, networkCallback)
}

View file

@ -5,12 +5,14 @@ import android.net.ConnectivityManager
import android.net.InetAddresses
import android.net.NetworkCapabilities
import android.os.Build
import androidx.core.content.getSystemService
import java.lang.reflect.InvocationTargetException
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)
val connectivityManager = context.getSystemService<ConnectivityManager>()!!
connectivityManager.activeNetwork?.let { network ->
val netCapabilities = connectivityManager.getNetworkCapabilities(network)
val linkProperties = connectivityManager.getLinkProperties(network)
@ -39,8 +41,28 @@ 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
try {
val m = InetAddress::class.java.getMethod("parseNumericAddress", String::class.java)
fun(address: String): InetAddress {
try {
return m.invoke(null, address) as InetAddress
} catch (e: InvocationTargetException) {
throw e.cause ?: e
}
}
} catch (_: NoSuchMethodException) {
fun(address: String): InetAddress {
return InetAddress.getByName(address)
}
}
}
internal fun convertIpv6ToCanonicalForm(ipv6: String): String = ipv6
.replace("((?:(?:^|:)0+\\b){2,}):?(?!\\S*\\b\\1:0+\\b)(\\S*)".toRegex(), "::$2")
internal val InetAddress.ip: String
get() = if (this is Inet4Address) {
hostAddress!!
} else {
convertIpv6ToCanonicalForm(hostAddress!!)
}

View file

@ -0,0 +1,93 @@
package org.amnezia.vpn.util.net
import android.net.TrafficStats
import android.os.Build
import android.os.Process
import android.os.SystemClock
import kotlin.math.roundToLong
private const val BYTE = 1L
private const val KiB = BYTE shl 10
private const val MiB = KiB shl 10
private const val GiB = MiB shl 10
private const val TiB = GiB shl 10
class TrafficStats {
private var lastTrafficData = TrafficData.ZERO
private var lastTimestamp = 0L
private val getTrafficDataCompat: () -> TrafficData =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val iface = "tun0"
fun(): TrafficData {
return TrafficData(TrafficStats.getRxBytes(iface), TrafficStats.getTxBytes(iface))
}
} else {
val uid = Process.myUid()
fun(): TrafficData {
return TrafficData(TrafficStats.getUidRxBytes(uid), TrafficStats.getUidTxBytes(uid))
}
}
fun reset() {
lastTrafficData = getTrafficDataCompat()
lastTimestamp = SystemClock.elapsedRealtime()
}
fun isSupported(): Boolean =
lastTrafficData.rx != TrafficStats.UNSUPPORTED.toLong() && lastTrafficData.tx != TrafficStats.UNSUPPORTED.toLong()
fun getSpeed(): TrafficData {
val timestamp = SystemClock.elapsedRealtime()
val elapsedSeconds = (timestamp - lastTimestamp) / 1000.0
val trafficData = getTrafficDataCompat()
val speed = trafficData.diff(lastTrafficData, elapsedSeconds)
lastTrafficData = trafficData
lastTimestamp = timestamp
return speed
}
class TrafficData(val rx: Long, val tx: Long) {
private var _rxString: String? = null
val rxString: String
get() {
if (_rxString == null) _rxString = rx.speedToString()
return _rxString ?: throw AssertionError("Set to null by another thread")
}
private var _txString: String? = null
val txString: String
get() {
if (_txString == null) _txString = tx.speedToString()
return _txString ?: throw AssertionError("Set to null by another thread")
}
fun diff(other: TrafficData, elapsedSeconds: Double): TrafficData {
val rx = ((this.rx - other.rx) / elapsedSeconds).round()
val tx = ((this.tx - other.tx) / elapsedSeconds).round()
return if (rx == 0L && tx == 0L) ZERO else TrafficData(rx, tx)
}
private fun Double.round() = if (isNaN()) 0L else roundToLong()
private fun Long.speedToString() =
when {
this < KiB -> formatSize(this, BYTE, "B/s")
this < MiB -> formatSize(this, KiB, "KiB/s")
this < GiB -> formatSize(this, MiB, "MiB/s")
this < TiB -> formatSize(this, GiB, "GiB/s")
else -> formatSize(this, TiB, "TiB/s")
}
private fun formatSize(bytes: Long, divider: Long, unit: String): String {
val s = (bytes.toDouble() / divider * 100).roundToLong() / 100.0
return "${s.toString().removeSuffix(".0")} $unit"
}
companion object {
val ZERO: TrafficData = TrafficData(0L, 0L)
}
}
}