Commit 03f86606 authored by Daniel Wolf's avatar Daniel Wolf
Browse files

Dns queries can now be logged to the database (no UI yet)

parent 87511db5
......@@ -58,7 +58,7 @@ dependencies {
implementation 'com.frostnerd.utilskt:preferences:1.4.6'
implementation 'com.frostnerd.utilskt:navigationdraweractivity:1.3.2'
implementation 'com.frostnerd.utilskt:encrypteddnstunnelproxy:1.4.7'
implementation 'com.frostnerd.utilskt:encrypteddnstunnelproxy:1.4.8'
implementation 'com.frostnerd.utilskt:general:1.0.6'
implementation 'com.frostnerd.utils:materialedittext:1.0.17'
implementation 'com.frostnerd.utils:design:1.0.17'
......
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "c9eefb9b6fc3651fe204ff874fc2bf7d",
"entities": [
{
"tableName": "UserServerConfiguration",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `primaryServerUrl` TEXT NOT NULL, `secondaryServerUrl` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "primaryServerUrl",
"columnName": "primaryServerUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "secondaryServerUrl",
"columnName": "secondaryServerUrl",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "CachedResponse",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`dnsName` TEXT NOT NULL, `type` INTEGER NOT NULL, `records` TEXT NOT NULL, PRIMARY KEY(`dnsName`, `type`))",
"fields": [
{
"fieldPath": "dnsName",
"columnName": "dnsName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "records",
"columnName": "records",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"dnsName",
"type"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DnsQuery",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `askedServer` TEXT, `questionTime` INTEGER NOT NULL, `responseTime` INTEGER NOT NULL, `responses` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "askedServer",
"columnName": "askedServer",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "questionTime",
"columnName": "questionTime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "responseTime",
"columnName": "responseTime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "responses",
"columnName": "responses",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"c9eefb9b6fc3651fe204ff874fc2bf7d\")"
]
}
}
\ No newline at end of file
......@@ -13,8 +13,11 @@ import com.frostnerd.navigationdraweractivity.items.DrawerItem
import com.frostnerd.navigationdraweractivity.items.createMenu
import com.frostnerd.navigationdraweractivity.items.singleInstanceFragment
import com.frostnerd.smokescreen.*
import com.frostnerd.smokescreen.database.getDatabase
import com.frostnerd.smokescreen.fragment.MainFragment
import com.frostnerd.smokescreen.fragment.SettingsFragment
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class MainActivity : NavigationDrawerActivity() {
private var textColor: Int = 0
......@@ -30,6 +33,12 @@ class MainActivity : NavigationDrawerActivity() {
val view = layoutInflater.inflate(R.layout.menu_cardview, viewParent, false)
view
}
GlobalScope.launch {
val all = getDatabase().dnsQueryRepository().getAllAsync(GlobalScope)
for (dnsQuery in all) {
println(dnsQuery)
}
}
}
override fun createDrawerItems(): MutableList<DrawerItem> {
......
......@@ -3,10 +3,13 @@ package com.frostnerd.smokescreen.database
import androidx.room.Database
import androidx.room.RoomDatabase
import com.frostnerd.smokescreen.database.dao.CachedResponseDao
import com.frostnerd.smokescreen.database.dao.DnsQueryDao
import com.frostnerd.smokescreen.database.dao.UserServerConfigurationDao
import com.frostnerd.smokescreen.database.entities.CachedResponse
import com.frostnerd.smokescreen.database.entities.DnsQuery
import com.frostnerd.smokescreen.database.entities.UserServerConfiguration
import com.frostnerd.smokescreen.database.repository.CachedResponseRepository
import com.frostnerd.smokescreen.database.repository.DnsQueryRepository
import com.frostnerd.smokescreen.database.repository.UserServerConfigurationRepository
/**
......@@ -19,11 +22,13 @@ import com.frostnerd.smokescreen.database.repository.UserServerConfigurationRepo
* development@frostnerd.com
*/
@Database(entities = [UserServerConfiguration::class, CachedResponse::class], version = 3)
@Database(entities = [UserServerConfiguration::class, CachedResponse::class, DnsQuery::class], version = 4)
abstract class AppDatabase : RoomDatabase() {
abstract fun userServerConfigurationDao(): UserServerConfigurationDao
abstract fun cachedResponseDao(): CachedResponseDao
abstract fun dnsQueryDao():DnsQueryDao
fun cachedResponseRepository() = CachedResponseRepository(cachedResponseDao())
fun userServerConfigurationRepository() = UserServerConfigurationRepository(userServerConfigurationDao())
fun dnsQueryRepository() = DnsQueryRepository(dnsQueryDao())
}
\ No newline at end of file
package com.frostnerd.smokescreen.database
import android.content.Context
import android.util.Base64
import androidx.room.Room
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import org.minidns.record.Record
import java.io.ByteArrayInputStream
import java.io.DataInputStream
/**
* Copyright Daniel Wolf 2018
......@@ -41,4 +45,9 @@ private fun migration(from: Int, to: Int, migrate: (database: SupportSQLiteDatab
migrate.invoke(database)
}
}
}
fun recordFromBase64(base64:String):Record<*> {
val bytes = Base64.decode(base64, Base64.NO_WRAP)
return Record.parse(DataInputStream(ByteArrayInputStream(bytes)), bytes)
}
\ No newline at end of file
package com.frostnerd.smokescreen.database.converters
import androidx.room.TypeConverter
import com.google.gson.reflect.TypeToken
import com.google.gson.Gson
/**
* Copyright Daniel Wolf 2018
* All rights reserved.
* Code may NOT be used without proper permission, neither in binary nor in source form.
* All redistributions of this software in source code must retain this copyright header
* All redistributions of this software in binary form must visibly inform users about usage of this software
*
* development@frostnerd.com
*/
class StringListConverter {
private val gson = Gson()
private val type = object:TypeToken<MutableList<String>>() {}.type
@TypeConverter
fun stringToList(value: String): MutableList<String> {
return gson.fromJson(value, type)
}
@TypeConverter
fun someObjectListToString(value: MutableList<String>): String {
return gson.toJson(value)
}
}
\ No newline at end of file
package com.frostnerd.smokescreen.database.dao
import androidx.room.*
import com.frostnerd.smokescreen.database.entities.DnsQuery
/**
* Copyright Daniel Wolf 2018
* All rights reserved.
* Code may NOT be used without proper permission, neither in binary nor in source form.
* All redistributions of this software in source code must retain this copyright header
* All redistributions of this software in binary form must visibly inform users about usage of this software
*
* development@frostnerd.com
*/
@Dao
interface DnsQueryDao {
@Query("SELECT * FROM DnsQuery")
fun getAll(): List<DnsQuery>
@Query("SELECT MAX(id) FROM DnsQuery")
fun getLastInsertedId():Long
@Insert
fun insertAll(vararg dnsQueries: DnsQuery)
@Insert
fun insertAll(dnsQueries: List<DnsQuery>)
@Insert
fun insert(dnsQuery: DnsQuery)
@Update
fun update(dnsQuery: DnsQuery)
@Delete
fun delete(dnsQuery: DnsQuery)
@Query("DELETE FROM DnsQuery")
fun deleteAll()
}
\ No newline at end of file
package com.frostnerd.smokescreen.database.entities
import android.util.Base64
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.Relation
import androidx.room.TypeConverters
import com.frostnerd.smokescreen.database.converters.DnsRecordMapConverter
import com.frostnerd.smokescreen.database.converters.DnsTypeConverter
import com.frostnerd.smokescreen.database.converters.StringListConverter
import com.frostnerd.smokescreen.database.recordFromBase64
import org.minidns.record.Record
/**
* Copyright Daniel Wolf 2018
* All rights reserved.
* Code may NOT be used without proper permission, neither in binary nor in source form.
* All redistributions of this software in source code must retain this copyright header
* All redistributions of this software in binary form must visibly inform users about usage of this software
*
* development@frostnerd.com
*/
@Entity(tableName = "DnsQuery")
@TypeConverters(DnsTypeConverter::class, StringListConverter::class)
data class DnsQuery(
@PrimaryKey(autoGenerate = true) var id: Long = 0,
val type: Record.TYPE,
val name: String,
var askedServer: String?,
val questionTime: Long,
var responseTime: Long = 0,
var responses: MutableList<String>
) {
fun getParsedResponses(): List<Record<*>> {
val responses = mutableListOf<Record<*>>()
for (response in this.responses) {
responses.add(recordFromBase64(response))
}
return responses
}
fun addResponse(record: Record<*>) {
responses.add(Base64.encodeToString(record.toByteArray(), Base64.NO_WRAP))
}
override fun toString(): String {
return "DnsQuery(id=$id, type=$type, name='$name', askedServer=$askedServer, questionTime=$questionTime, responseTime=$responseTime, responses={${responses.size}})"
}
}
\ No newline at end of file
package com.frostnerd.smokescreen.database.repository
import androidx.room.Insert
import com.frostnerd.smokescreen.database.dao.DnsQueryDao
import com.frostnerd.smokescreen.database.entities.DnsQuery
import kotlinx.coroutines.*
/**
* Copyright Daniel Wolf 2018
* All rights reserved.
* Code may NOT be used without proper permission, neither in binary nor in source form.
* All redistributions of this software in source code must retain this copyright header
* All redistributions of this software in binary form must visibly inform users about usage of this software
*
* development@frostnerd.com
*/
class DnsQueryRepository(val dnsQueryDao: DnsQueryDao) {
fun updateAsync(dnsQuery: DnsQuery): Job {
return GlobalScope.launch {
dnsQueryDao.update(dnsQuery)
}
}
fun insertAllAsync(dnsQueries:List<DnsQuery>): Job {
return GlobalScope.launch {
dnsQueryDao.insertAll(dnsQueries)
}
}
fun insertAsync(dnsQuery:DnsQuery): Job {
return GlobalScope.launch {
dnsQueryDao.insert(dnsQuery)
}
}
suspend fun getAllAsync(coroutineScope: CoroutineScope): List<DnsQuery> {
return coroutineScope.async(start = CoroutineStart.DEFAULT) {
dnsQueryDao.getAll()
}.await()
}
}
\ No newline at end of file
......@@ -162,7 +162,8 @@ class DnsVpnService : VpnService(), Runnable {
"dnscache_keepacrosslaunches",
"bypass_searchdomains",
"user_bypass_blacklist",
"doh_server_url_primary"
"doh_server_url_primary",
"log_dns_queries"
)
settingsSubscription = getPreferences().listenForChanges(relevantSettings) { key, _, _ ->
log("The Preference $key has changed, restarting the VPN.")
......@@ -542,7 +543,7 @@ class DnsVpnService : VpnService(), Runnable {
)
log("Handle created, creating DNS proxy")
dnsProxy = SmokeProxy(handle!!, createProxyBypassHandlers(),this, createDnsCache())
dnsProxy = SmokeProxy(handle!!, createProxyBypassHandlers(),this, createDnsCache(), createQueryLogger())
log("DnsProxy created, creating VPN proxy")
vpnProxy = VPNTunnelProxy(dnsProxy!!)
......@@ -553,6 +554,13 @@ class DnsVpnService : VpnService(), Runnable {
LocalBroadcastManager.getInstance(this).sendBroadcast(Intent(BROADCAST_VPN_ACTIVE))
}
private fun createQueryLogger(): QueryListener? {
return if(getPreferences().loggingEnabled || getPreferences().queryLoggingEnabled) {
com.frostnerd.smokescreen.util.proxy.QueryListener(this)
} else null
}
/**
* Creates bypass handlers for each network and its associated search domains
* Requests for .*SEARCHDOMAIN won't use doh and are sent to the DNS servers of the network they originated from.
......
......@@ -55,6 +55,9 @@ interface AppSettings {
var forceIpv6: Boolean
var forceIpv4:Boolean
var bypassSearchdomains:Boolean
// Query logging category
var queryLoggingEnabled:Boolean
// ###### End of settings
......@@ -85,6 +88,8 @@ class AppSettingsSharedPreferences(context: Context) : AppSettings, SimpleTypedP
override var forceIpv4: Boolean by booleanPref("force_ipv4", false)
override var bypassSearchdomains: Boolean by booleanPref("bypass_searchdomains", true)
override var queryLoggingEnabled: Boolean by booleanPref("log_dns_queries", false)
override var catchKnownDnsServers: Boolean by booleanPref("catch_known_servers", false)
override var dummyDnsAddressIpv4: String by stringPref("dummy_dns_ipv4", "8.8.8.8")
override var dummyDnsAddressIpv6: String by stringPref("dummy_dns_ipv6", "2001:4860:4860::8888")
......
package com.frostnerd.smokescreen.util.proxy
import android.content.Context
import com.frostnerd.dnstunnelproxy.UpstreamAddress
import com.frostnerd.smokescreen.database.entities.DnsQuery
import com.frostnerd.smokescreen.database.getDatabase
import com.frostnerd.smokescreen.getPreferences
import com.frostnerd.smokescreen.log
import org.minidns.dnsmessage.DnsMessage
/**
* Copyright Daniel Wolf 2018
* All rights reserved.
* Code may NOT be used without proper permission, neither in binary nor in source form.
* All redistributions of this software in source code must retain this copyright header
* All redistributions of this software in binary form must visibly inform users about usage of this software
*
* development@frostnerd.com
*/
class QueryListener(private val context: Context) : com.frostnerd.dnstunnelproxy.QueryListener {
private val writeQueriesToLog = context.getPreferences().loggingEnabled
private val logQueriesToDb = context.getPreferences().queryLoggingEnabled
private val waitingQueryLogs: MutableMap<Int, DnsQuery> = mutableMapOf()
private val askedServer = context.getPreferences().primaryServerConfig.urlCreator.baseUrl
override suspend fun onQueryForwarded(questionMessage: DnsMessage, destination: UpstreamAddress) {
if (logQueriesToDb) {
val query = waitingQueryLogs[questionMessage.id]!!
query.askedServer = askedServer
context.getDatabase().dnsQueryDao().update(query)
}
}
override suspend fun onDeviceQuery(questionMessage: DnsMessage) {
if (writeQueriesToLog) {
context.log("Query from device: $questionMessage")
}
if (logQueriesToDb) {
val query = DnsQuery(
type = questionMessage.question.type,
name = questionMessage.question.name.toString(),
askedServer = null,
questionTime = System.currentTimeMillis(),
responses = mutableListOf()
)
val dao = context.getDatabase().dnsQueryDao()
dao.insert(query)
query.id = dao.getLastInsertedId()
waitingQueryLogs[questionMessage.id] = query
}
}
override suspend fun onQueryResponse(responseMessage: DnsMessage, fromUpstream: Boolean) {
if (writeQueriesToLog) {
if (!fromUpstream) {
context.log("Returned from cache: $responseMessage")
} else {
context.log("Response from upstream: $responseMessage")
}
}
if (fromUpstream && logQueriesToDb) {
val query = waitingQueryLogs[responseMessage.id]
if (query != null) {
query.responseTime = System.currentTimeMillis()
for (answer in responseMessage.answerSection) {
query.addResponse(answer)
}
context.getDatabase().dnsQueryRepository().updateAsync(query)
}
} else if(logQueriesToDb) {
waitingQueryLogs.remove(responseMessage.id)
}
}
}
......@@ -24,7 +24,8 @@ class SmokeProxy(
dnsHandle: ProxyHandler,
proxyBypassHandles: List<ProxyBypassHandler>,
vpnService: DnsVpnService,
val cache: SimpleDnsCache?
val cache: SimpleDnsCache?,
queryListener: QueryListener?
) :
DnsPacketProxy(
(proxyBypassHandles as List<DnsHandle>).toMutableList().let {
......@@ -34,24 +35,5 @@ class SmokeProxy(
null,
vpnService,
cache,
queryListener = if (vpnService.getPreferences().loggingEnabled) object : QueryListener {
override suspend fun onDeviceQuery(questionMessage: DnsMessage) {
vpnService.log("Query from device: $questionMessage")
}
override suspend fun onQueryResponse(responseMessage: DnsMessage, fromUpstream: Boolean) {
if (!fromUpstream) {
vpnService.log("Returned from cache: $responseMessage")
} else {
vpnService.log("Response from upstream: $responseMessage")
}
}
} else null) {
override fun informFailedRequest(request: FutureAnswer) {
super.informFailedRequest(request)
vpnService.log("Query from ${request.time} failed.")
}
}
\ No newline at end of file
queryListener = queryListener
)
\ No newline at end of file
......@@ -15,10 +15,10 @@
<string name="summary_excluded_apps">Exclude apps from the app here, for example if they cannot connect to the Internet anymore - selected apps won\'t use dns-over-https anymore.</string>
<string name="preference_category_cache">Cache control</string>
<string name="title_dnscache_enabled">Use DNS cache</string>
<string name="summary_dnscache_enabled">Enables the apps DNS cache. The cache memorizes old requests to answer future questions faster</string>
<string name="title_dnscache_keepacrosslaunches">Keep DNS cache</string>
<string name="summary_dnscache_keepacrosslaunches">Keep the DNS cache across multiple launches</string>
......@@ -61,4 +61,9 @@
<string name="title_bypass_searchdomains">Searchdomains bypass</string>
<string name="summary_bypass_searchdomains">Send search domains (e.g. MyPC.local or MyPc.fritz.box) to the Dns server of your network instead of the DoH server.\nThis is highly recommended. Those querie