Android notification and routing (#797)
Android notification and routing
This commit is contained in:
parent
ff348a348c
commit
abb3c918e3
40 changed files with 1108 additions and 543 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!!)
|
||||
}
|
||||
|
|
|
|||
93
client/android/utils/src/main/kotlin/net/TrafficStats.kt
Normal file
93
client/android/utils/src/main/kotlin/net/TrafficStats.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue