Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import kotlinx.coroutines.runBlocking
import okio.Path
import okio.Path.Companion.toPath
import org.ooni.engine.AndroidNetworkTypeFinder
import org.ooni.engine.AndroidPrivateDnsFinder
import org.ooni.engine.AndroidOonimkallBridge
import org.ooni.engine.AndroidSecureStorage
import org.ooni.passport.AndroidPassportBridge
Expand Down Expand Up @@ -62,6 +63,7 @@ class AndroidApplication : Application() {
cacheDir = cacheDir.absolutePath,
databaseDriverFactory = ::buildDatabaseDriver,
networkTypeFinder = AndroidNetworkTypeFinder(connectivityManager),
privateDnsFinder = AndroidPrivateDnsFinder(connectivityManager),
secureStorage = AndroidSecureStorage(this, OrganizationConfig.baseSoftwareName),
buildDataStore = ::buildDataStore,
getBatteryState = ::getBatteryState,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.ooni.engine

import android.net.ConnectivityManager
import android.net.LinkProperties
import android.os.Build
import androidx.annotation.RequiresApi
import co.touchlab.kermit.Logger
import org.ooni.engine.models.PrivateDnsStatus

/**
* Reads the "Private DNS" (DNS-over-TLS) state of the active network.
*
* [LinkProperties.isPrivateDnsActive] is API 28+, so older devices report
* [PrivateDnsStatus.Unsupported] even though they may be running the app.
*/
class AndroidPrivateDnsFinder(
private val connectivityManager: ConnectivityManager?,
) : PrivateDnsFinder {
override fun invoke(): PrivateDnsStatus {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return PrivateDnsStatus.Unsupported
val manager = connectivityManager ?: return PrivateDnsStatus.Unknown
return try {
// No active network means there is nothing to describe, not that private DNS is off.
val network = manager.activeNetwork ?: return PrivateDnsStatus.Unknown
val linkProperties = manager.getLinkProperties(network) ?: return PrivateDnsStatus.Unknown
statusOf(linkProperties)
} catch (e: Throwable) {
// An annotation lookup must never take down a test run.
Logger.w("Error reading private DNS state: ${e.message}")
PrivateDnsStatus.Unknown
}
}

@RequiresApi(Build.VERSION_CODES.P)
private fun statusOf(linkProperties: LinkProperties) =
if (linkProperties.isPrivateDnsActive) {
PrivateDnsStatus.HasPrivateDns
} else {
PrivateDnsStatus.NoPrivateDns
}
}
2 changes: 2 additions & 0 deletions composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class Engine(
private val cacheDir: String,
private val taskEventMapper: TaskEventMapper,
private val networkTypeFinder: NetworkTypeFinder,
private val privateDnsFinder: PrivateDnsFinder,
private val platformInfo: PlatformInfo,
private val getEnginePreferences: suspend () -> EnginePreferences,
private val addRunCancelListener: (() -> Unit) -> CancelListenerCallback,
Expand Down Expand Up @@ -150,6 +151,7 @@ class Engine(
),
annotations = TaskSettings.Annotations(
networkType = networkTypeFinder(),
privateDns = privateDnsFinder(),
flavor = platformInfo.buildSoftwareName(taskOrigin),
origin = taskOrigin,
osVersion = platformInfo.osVersion,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.ooni.engine

import org.ooni.engine.models.PrivateDnsStatus

fun interface PrivateDnsFinder {
operator fun invoke(): PrivateDnsStatus
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.ooni.engine.models

import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

/**
* Whether the device resolves DNS through a private (encrypted) resolver, e.g. Android's
* "Private DNS" (DNS-over-TLS) setting.
*/
@Serializable(with = PrivateDnsStatusSerializer::class)
sealed interface PrivateDnsStatus {
val value: String

data object HasPrivateDns : PrivateDnsStatus {
override val value = "true"
}

data object NoPrivateDns : PrivateDnsStatus {
override val value = "false"
}

data object Unknown : PrivateDnsStatus {
override val value = "unknown"
}

data object Unsupported : PrivateDnsStatus {
override val value = "unsupported"
}

companion object {
fun fromValue(value: String) =
when (value) {
HasPrivateDns.value -> HasPrivateDns
NoPrivateDns.value -> NoPrivateDns
Unsupported.value -> Unsupported
else -> Unknown
}
}
}

object PrivateDnsStatusSerializer : KSerializer<PrivateDnsStatus> {
override val descriptor = PrimitiveSerialDescriptor("PrivateDnsStatus", PrimitiveKind.STRING)

override fun serialize(
encoder: Encoder,
value: PrivateDnsStatus,
) {
encoder.encodeString(value.value)
}

override fun deserialize(decoder: Decoder): PrivateDnsStatus = PrivateDnsStatus.fromValue(decoder.decodeString())
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ data class TaskSettings(
@Serializable
data class Annotations(
@SerialName("network_type") val networkType: NetworkType,
@SerialName("private_dns") val privateDns: PrivateDnsStatus,
// OONI or DW
@SerialName("flavor") val flavor: String,
// "autorun" or "ooni-run"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import okio.SYSTEM
import org.ooni.engine.Engine
import org.ooni.engine.NetworkTypeFinder
import org.ooni.engine.OonimkallBridge
import org.ooni.engine.PrivateDnsFinder
import org.ooni.engine.SecureStorage
import org.ooni.engine.TaskEventMapper
import org.ooni.engine.models.PrivateDnsStatus
import org.ooni.passport.PassportBridge
import org.ooni.passport.PassportGet
import org.ooni.passport.PassportHttpClient
Expand Down Expand Up @@ -171,6 +173,7 @@ class Dependencies(
val cacheDir: String,
private val databaseDriverFactory: () -> SqlDriver,
private val networkTypeFinder: NetworkTypeFinder,
private val privateDnsFinder: PrivateDnsFinder = PrivateDnsFinder { PrivateDnsStatus.Unsupported },
val secureStorage: SecureStorage,
@get:VisibleForTesting
val buildDataStore: () -> DataStore<Preferences>,
Expand Down Expand Up @@ -309,6 +312,7 @@ class Dependencies(
cacheDir = cacheDir,
taskEventMapper = taskEventMapper,
networkTypeFinder = networkTypeFinder,
privateDnsFinder = privateDnsFinder,
platformInfo = platformInfo,
getEnginePreferences = getEnginePreferences::invoke,
addRunCancelListener = runBackgroundStateManager::addCancelListener,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import kotlinx.coroutines.test.runTest
import org.ooni.engine.models.EnginePreferences
import org.ooni.engine.models.Failure
import org.ooni.engine.models.NetworkType
import org.ooni.engine.models.PrivateDnsStatus
import org.ooni.engine.models.TaskEvent
import org.ooni.engine.models.TaskLogLevel
import org.ooni.engine.models.TaskOrigin
Expand All @@ -25,6 +26,7 @@ import kotlin.test.assertTrue
class EngineTest {
private val json = Dependencies.buildJson()
private val networkTypeFinder = NetworkTypeFinder { NetworkType.NoInternet }
private val privateDnsFinder = PrivateDnsFinder { PrivateDnsStatus.HasPrivateDns }

@Test
fun startTaskAndGetEvents() =
Expand All @@ -51,6 +53,10 @@ class EngineTest {
assertEquals("web_connectivity", settings.name)
assertEquals(listOf("https://ooni.org"), settings.inputs)
assertEquals(NetworkType.NoInternet, settings.annotations.networkType)
assertEquals(PrivateDnsStatus.HasPrivateDns, settings.annotations.privateDns)
assertTrue(
bridge.lastStartTaskSettingsSerialized!!.contains("\"private_dns\":\"true\""),
)
}

@Test
Expand All @@ -75,6 +81,7 @@ class EngineTest {
cacheDir = "",
taskEventMapper = TaskEventMapper(networkTypeFinder, json),
networkTypeFinder = networkTypeFinder,
privateDnsFinder = privateDnsFinder,
platformInfo = PlatformInfo(
buildName = "1",
buildNumber = "1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.ooni.engine.models

import org.ooni.probe.di.Dependencies
import kotlin.test.Test
import kotlin.test.assertEquals

class PrivateDnsStatusTest {
private val json = Dependencies.buildJson()

@Test
fun serializesToTheAnnotationStringValues() {
val expected = mapOf(
PrivateDnsStatus.HasPrivateDns to "\"true\"",
PrivateDnsStatus.NoPrivateDns to "\"false\"",
PrivateDnsStatus.Unknown to "\"unknown\"",
PrivateDnsStatus.Unsupported to "\"unsupported\"",
)

expected.forEach { (status, encoded) ->
assertEquals(encoded, json.encodeToString(PrivateDnsStatusSerializer, status))
}
}

@Test
fun deserializesKnownValues() {
listOf(
PrivateDnsStatus.HasPrivateDns,
PrivateDnsStatus.NoPrivateDns,
PrivateDnsStatus.Unknown,
PrivateDnsStatus.Unsupported,
).forEach { status ->
assertEquals(status, PrivateDnsStatus.fromValue(status.value))
}
}

@Test
fun deserializesUnrecognizedValuesAsUnknown() {
assertEquals(PrivateDnsStatus.Unknown, PrivateDnsStatus.fromValue("something_else"))
assertEquals(PrivateDnsStatus.Unknown, PrivateDnsStatus.fromValue(""))
}
}
Loading