summaryrefslogtreecommitdiffhomepage
path: root/android/lib/talpid/src
diff options
context:
space:
mode:
authorAlbin <albin@mullvad.net>2023-07-25 11:55:44 +0200
committerAlbin <albin@mullvad.net>2023-07-25 14:08:48 +0200
commitde4e1d85837d3790eccb6ac0d3a7ed172e68b36c (patch)
treed567dcd3b61ca659cbd63d09a200a32f4b207237 /android/lib/talpid/src
parent38248fa9a0cbeb1414d1db07719c40b7b0992e93 (diff)
downloadmullvadvpn-de4e1d85837d3790eccb6ac0d3a7ed172e68b36c.tar.xz
mullvadvpn-de4e1d85837d3790eccb6ac0d3a7ed172e68b36c.zip
Move talpid classes to talpid module
Diffstat (limited to 'android/lib/talpid/src')
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/ConnectivityListener.kt69
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/CreateTunResult.kt23
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt150
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/Endpoint.kt8
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationEndpoint.kt8
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationType.kt9
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/TransportProtocol.kt10
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/TunnelEndpoint.kt11
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tun_provider/InetNetwork.kt8
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tun_provider/TunConfig.kt10
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ActionAfterDisconnect.kt11
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorState.kt6
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorStateCause.kt32
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ParameterGenerationError.kt8
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifier.kt76
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifierExtensions.kt9
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/InetAddressExt.kt10
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/TalpidSdkUtils.kt12
18 files changed, 470 insertions, 0 deletions
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/ConnectivityListener.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/ConnectivityListener.kt
new file mode 100644
index 0000000000..de56ebb878
--- /dev/null
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/ConnectivityListener.kt
@@ -0,0 +1,69 @@
+package net.mullvad.talpid
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import kotlin.properties.Delegates.observable
+import net.mullvad.talpid.util.EventNotifier
+
+class ConnectivityListener {
+ private val availableNetworks = HashSet<Network>()
+
+ private val callback =
+ object : NetworkCallback() {
+ override fun onAvailable(network: Network) {
+ availableNetworks.add(network)
+ isConnected = true
+ }
+
+ override fun onLost(network: Network) {
+ availableNetworks.remove(network)
+ isConnected = !availableNetworks.isEmpty()
+ }
+ }
+
+ private lateinit var connectivityManager: ConnectivityManager
+
+ val connectivityNotifier = EventNotifier(false)
+
+ var isConnected by
+ observable(false) { _, oldValue, newValue ->
+ if (newValue != oldValue) {
+ if (senderAddress != 0L) {
+ notifyConnectivityChange(newValue, senderAddress)
+ }
+
+ connectivityNotifier.notify(newValue)
+ }
+ }
+
+ var senderAddress = 0L
+
+ fun register(context: Context) {
+ val request =
+ NetworkRequest.Builder()
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+ .build()
+
+ connectivityManager =
+ context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+
+ connectivityManager.registerNetworkCallback(request, callback)
+ }
+
+ fun unregister() {
+ connectivityManager.unregisterNetworkCallback(callback)
+ }
+
+ private fun finalize() {
+ destroySender(senderAddress)
+ senderAddress = 0L
+ }
+
+ private external fun notifyConnectivityChange(isConnected: Boolean, senderAddress: Long)
+ private external fun destroySender(senderAddress: Long)
+}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/CreateTunResult.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/CreateTunResult.kt
new file mode 100644
index 0000000000..33f62026d6
--- /dev/null
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/CreateTunResult.kt
@@ -0,0 +1,23 @@
+package net.mullvad.talpid
+
+import java.net.InetAddress
+
+sealed class CreateTunResult {
+ open val isOpen
+ get() = false
+
+ class Success(val tunFd: Int) : CreateTunResult() {
+ override val isOpen
+ get() = true
+ }
+
+ class InvalidDnsServers(val addresses: ArrayList<InetAddress>, val tunFd: Int) :
+ CreateTunResult() {
+ override val isOpen
+ get() = true
+ }
+
+ object PermissionDenied : CreateTunResult()
+
+ object TunnelDeviceError : CreateTunResult()
+}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt
new file mode 100644
index 0000000000..a94bfac428
--- /dev/null
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt
@@ -0,0 +1,150 @@
+package net.mullvad.talpid
+
+import android.net.VpnService
+import android.os.ParcelFileDescriptor
+import java.net.Inet4Address
+import java.net.Inet6Address
+import java.net.InetAddress
+import kotlin.properties.Delegates.observable
+import net.mullvad.talpid.tun_provider.TunConfig
+import net.mullvad.talpid.util.TalpidSdkUtils.setMeteredIfSupported
+
+open class TalpidVpnService : VpnService() {
+ private var activeTunStatus by
+ observable<CreateTunResult?>(null) { _, oldTunStatus, _ ->
+ val oldTunFd =
+ when (oldTunStatus) {
+ is CreateTunResult.Success -> oldTunStatus.tunFd
+ is CreateTunResult.InvalidDnsServers -> oldTunStatus.tunFd
+ else -> null
+ }
+
+ if (oldTunFd != null) {
+ ParcelFileDescriptor.adoptFd(oldTunFd).close()
+ }
+ }
+
+ private val tunIsOpen
+ get() = activeTunStatus?.isOpen ?: false
+
+ private var currentTunConfig = defaultTunConfig()
+ private var tunIsStale = false
+
+ protected var disallowedApps: List<String>? = null
+
+ val connectivityListener = ConnectivityListener()
+
+ override fun onCreate() {
+ connectivityListener.register(this)
+ }
+
+ override fun onDestroy() {
+ connectivityListener.unregister()
+ }
+
+ fun getTun(config: TunConfig): CreateTunResult {
+ synchronized(this) {
+ val tunStatus = activeTunStatus
+
+ if (config == currentTunConfig && tunIsOpen && !tunIsStale) {
+ return tunStatus!!
+ } else {
+ val newTunStatus = createTun(config)
+
+ currentTunConfig = config
+ activeTunStatus = newTunStatus
+ tunIsStale = false
+
+ return newTunStatus
+ }
+ }
+ }
+
+ fun createTun() {
+ synchronized(this) { activeTunStatus = createTun(currentTunConfig) }
+ }
+
+ fun recreateTunIfOpen(config: TunConfig) {
+ synchronized(this) {
+ if (tunIsOpen) {
+ currentTunConfig = config
+ activeTunStatus = createTun(config)
+ }
+ }
+ }
+
+ fun closeTun() {
+ synchronized(this) { activeTunStatus = null }
+ }
+
+ fun markTunAsStale() {
+ synchronized(this) { tunIsStale = true }
+ }
+
+ private fun createTun(config: TunConfig): CreateTunResult {
+ if (VpnService.prepare(this) != null) {
+ // VPN permission wasn't granted
+ return CreateTunResult.PermissionDenied
+ }
+
+ var invalidDnsServerAddresses = ArrayList<InetAddress>()
+
+ val builder =
+ Builder().apply {
+ for (address in config.addresses) {
+ addAddress(address, prefixForAddress(address))
+ }
+
+ for (dnsServer in config.dnsServers) {
+ try {
+ addDnsServer(dnsServer)
+ } catch (exception: IllegalArgumentException) {
+ invalidDnsServerAddresses.add(dnsServer)
+ }
+ }
+
+ for (route in config.routes) {
+ addRoute(route.address, route.prefixLength.toInt())
+ }
+
+ disallowedApps?.let { apps ->
+ for (app in apps) {
+ addDisallowedApplication(app)
+ }
+ }
+ setMtu(config.mtu)
+ setBlocking(false)
+ setMeteredIfSupported(false)
+ }
+
+ val vpnInterface = builder.establish()
+ val tunFd = vpnInterface?.detachFd()
+
+ if (tunFd == null) {
+ return CreateTunResult.TunnelDeviceError
+ }
+
+ waitForTunnelUp(tunFd, config.routes.any { route -> route.isIpv6 })
+
+ if (!invalidDnsServerAddresses.isEmpty()) {
+ return CreateTunResult.InvalidDnsServers(invalidDnsServerAddresses, tunFd)
+ }
+
+ return CreateTunResult.Success(tunFd)
+ }
+
+ fun bypass(socket: Int): Boolean {
+ return protect(socket)
+ }
+
+ private fun prefixForAddress(address: InetAddress): Int {
+ when (address) {
+ is Inet4Address -> return 32
+ is Inet6Address -> return 128
+ else -> throw RuntimeException("Invalid IP address (not IPv4 nor IPv6)")
+ }
+ }
+
+ private external fun defaultTunConfig(): TunConfig
+ private external fun waitForTunnelUp(tunFd: Int, isIpv6Enabled: Boolean)
+}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/Endpoint.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/Endpoint.kt
new file mode 100644
index 0000000000..8937bd0122
--- /dev/null
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/Endpoint.kt
@@ -0,0 +1,8 @@
+package net.mullvad.talpid.net
+
+import android.os.Parcelable
+import java.net.InetSocketAddress
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class Endpoint(val address: InetSocketAddress, val protocol: TransportProtocol) : Parcelable
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationEndpoint.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationEndpoint.kt
new file mode 100644
index 0000000000..9ec96b1494
--- /dev/null
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationEndpoint.kt
@@ -0,0 +1,8 @@
+package net.mullvad.talpid.net
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class ObfuscationEndpoint(val endpoint: Endpoint, val obfuscationType: ObfuscationType) :
+ Parcelable
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationType.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationType.kt
new file mode 100644
index 0000000000..72409d9026
--- /dev/null
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationType.kt
@@ -0,0 +1,9 @@
+package net.mullvad.talpid.net
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+enum class ObfuscationType : Parcelable {
+ Udp2Tcp
+}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/TransportProtocol.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/TransportProtocol.kt
new file mode 100644
index 0000000000..89fdedaba1
--- /dev/null
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/TransportProtocol.kt
@@ -0,0 +1,10 @@
+package net.mullvad.talpid.net
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+enum class TransportProtocol : Parcelable {
+ Tcp,
+ Udp
+}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/TunnelEndpoint.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/TunnelEndpoint.kt
new file mode 100644
index 0000000000..9c45833eb2
--- /dev/null
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/TunnelEndpoint.kt
@@ -0,0 +1,11 @@
+package net.mullvad.talpid.net
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class TunnelEndpoint(
+ val endpoint: Endpoint,
+ val quantumResistant: Boolean,
+ val obfuscation: ObfuscationEndpoint?
+) : Parcelable
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tun_provider/InetNetwork.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tun_provider/InetNetwork.kt
new file mode 100644
index 0000000000..a8490b48bf
--- /dev/null
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tun_provider/InetNetwork.kt
@@ -0,0 +1,8 @@
+package net.mullvad.talpid.tun_provider
+
+import java.net.Inet6Address
+import java.net.InetAddress
+
+data class InetNetwork(val address: InetAddress, val prefixLength: Short) {
+ val isIpv6 = address is Inet6Address
+}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tun_provider/TunConfig.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tun_provider/TunConfig.kt
new file mode 100644
index 0000000000..7efd3f7763
--- /dev/null
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tun_provider/TunConfig.kt
@@ -0,0 +1,10 @@
+package net.mullvad.talpid.tun_provider
+
+import java.net.InetAddress
+
+data class TunConfig(
+ val addresses: ArrayList<InetAddress>,
+ val dnsServers: ArrayList<InetAddress>,
+ val routes: ArrayList<InetNetwork>,
+ val mtu: Int
+)
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ActionAfterDisconnect.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ActionAfterDisconnect.kt
new file mode 100644
index 0000000000..a62abaacd0
--- /dev/null
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ActionAfterDisconnect.kt
@@ -0,0 +1,11 @@
+package net.mullvad.talpid.tunnel
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+enum class ActionAfterDisconnect : Parcelable {
+ Nothing,
+ Block,
+ Reconnect
+}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorState.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorState.kt
new file mode 100644
index 0000000000..070d190beb
--- /dev/null
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorState.kt
@@ -0,0 +1,6 @@
+package net.mullvad.talpid.tunnel
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize data class ErrorState(val cause: ErrorStateCause, val isBlocking: Boolean) : Parcelable
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorStateCause.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorStateCause.kt
new file mode 100644
index 0000000000..5096e5c693
--- /dev/null
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorStateCause.kt
@@ -0,0 +1,32 @@
+package net.mullvad.talpid.tunnel
+
+import android.os.Parcelable
+import java.net.InetAddress
+import kotlinx.parcelize.Parcelize
+
+private const val AUTH_FAILED_REASON_EXPIRED_ACCOUNT = "[EXPIRED_ACCOUNT]"
+
+sealed class ErrorStateCause : Parcelable {
+ @Parcelize
+ class AuthFailed(private val reason: String?) : ErrorStateCause() {
+ fun isCausedByExpiredAccount(): Boolean {
+ return reason == AUTH_FAILED_REASON_EXPIRED_ACCOUNT
+ }
+ }
+
+ @Parcelize object Ipv6Unavailable : ErrorStateCause()
+
+ @Parcelize object SetFirewallPolicyError : ErrorStateCause()
+
+ @Parcelize object SetDnsError : ErrorStateCause()
+
+ @Parcelize class InvalidDnsServers(val addresses: ArrayList<InetAddress>) : ErrorStateCause()
+
+ @Parcelize object StartTunnelError : ErrorStateCause()
+
+ @Parcelize class TunnelParameterError(val error: ParameterGenerationError) : ErrorStateCause()
+
+ @Parcelize object IsOffline : ErrorStateCause()
+
+ @Parcelize object VpnPermissionDenied : ErrorStateCause()
+}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ParameterGenerationError.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ParameterGenerationError.kt
new file mode 100644
index 0000000000..b1504c676f
--- /dev/null
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ParameterGenerationError.kt
@@ -0,0 +1,8 @@
+package net.mullvad.talpid.tunnel
+
+enum class ParameterGenerationError {
+ NoMatchingRelay,
+ NoMatchingBridgeRelay,
+ NoWireguardKey,
+ CustomTunnelHostResultionError
+}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifier.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifier.kt
new file mode 100644
index 0000000000..148b56eb45
--- /dev/null
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifier.kt
@@ -0,0 +1,76 @@
+package net.mullvad.talpid.util
+
+import kotlin.properties.Delegates.observable
+
+// Manages listeners interested in receiving events of type T
+//
+// The listeners subscribe using an ID object. This ID is used later on for unsubscribing. The only
+// requirement is that the object uses the default implementation of the `hashCode` and `equals`
+// methods inherited from `Any` (or `Object` in Java).
+//
+// If the ID object class (or any of its super-classes) overrides `hashCode` or `equals`,
+// unsubscribe might not work correctly.
+class EventNotifier<T>(private val initialValue: T) {
+ private val listeners = LinkedHashMap<Any, (T) -> Unit>()
+
+ var latestEvent = initialValue
+ private set
+
+ fun notify(event: T) {
+ synchronized(this) {
+ latestEvent = event
+
+ for (listener in listeners.values) {
+ listener(event)
+ }
+ }
+ }
+
+ fun notifyIfChanged(event: T) {
+ synchronized(this) {
+ if (latestEvent != event) {
+ notify(event)
+ }
+ }
+ }
+
+ fun subscribe(id: Any, listener: (T) -> Unit) {
+ subscribe(id, true, listener)
+ }
+
+ fun subscribe(id: Any, startWithLatestEvent: Boolean, listener: (T) -> Unit) {
+ synchronized(this) {
+ listeners.put(id, listener)
+ if (startWithLatestEvent) listener(latestEvent)
+ }
+ }
+
+ fun hasListeners(): Boolean {
+ synchronized(this) {
+ return !listeners.isEmpty()
+ }
+ }
+
+ fun unsubscribe(id: Any) {
+ synchronized(this) { listeners.remove(id) }
+ }
+
+ fun unsubscribeAll() {
+ synchronized(this) { listeners.clear() }
+ }
+
+ fun notifiable() = observable(latestEvent) { _, _, newValue -> notify(newValue) }
+}
+
+fun <T> autoSubscribable(id: Any, fallback: T, listener: (T) -> Unit) =
+ observable<EventNotifier<T>?>(null) { _, old, new ->
+ if (old != new) {
+ old?.unsubscribe(id)
+
+ if (new == null) {
+ listener.invoke(fallback)
+ } else {
+ new.subscribe(id, listener)
+ }
+ }
+ }
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifierExtensions.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifierExtensions.kt
new file mode 100644
index 0000000000..add362fcb1
--- /dev/null
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifierExtensions.kt
@@ -0,0 +1,9 @@
+package net.mullvad.talpid.util
+
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
+
+fun <T> EventNotifier<T>.callbackFlowFromSubscription(id: Any) = callbackFlow {
+ this@callbackFlowFromSubscription.subscribe(id) { this.trySend(it) }
+ awaitClose { this@callbackFlowFromSubscription.unsubscribe(id) }
+}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/InetAddressExt.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/InetAddressExt.kt
new file mode 100644
index 0000000000..d310deb884
--- /dev/null
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/InetAddressExt.kt
@@ -0,0 +1,10 @@
+package net.mullvad.talpid.util
+
+import java.net.InetAddress
+
+fun InetAddress.addressString(): String {
+ val hostNameAndAddress = this.toString().split('/', limit = 2)
+ val address = hostNameAndAddress[1]
+
+ return address
+}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/TalpidSdkUtils.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/TalpidSdkUtils.kt
new file mode 100644
index 0000000000..9eaecb2e8a
--- /dev/null
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/TalpidSdkUtils.kt
@@ -0,0 +1,12 @@
+package net.mullvad.talpid.util
+
+import android.net.VpnService
+import android.os.Build
+
+object TalpidSdkUtils {
+ fun VpnService.Builder.setMeteredIfSupported(isMetered: Boolean) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
+ this.setMetered(isMetered)
+ }
+ }
+}