Commit 3229cf98 authored by Daniel Wolf's avatar Daniel Wolf
Browse files

Merge branch '138-add-support-for-wildcard-dns-rules' into 'master'

Resolve "Add support for wildcard dns rules"

Closes #138

See merge request PublicAndroidApps/smokescreen!34
parents 76ac5caa abfd26df
{
"formatVersion": 1,
"database": {
"version": 10,
"identityHash": "daac9efbbf9d89b9aa10f7ad54ac736a",
"entities": [
{
"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, `fromCache` INTEGER NOT NULL, `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": "fromCache",
"columnName": "fromCache",
"affinity": "INTEGER",
"notNull": true
},
{
"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": []
},
{
"tableName": "DnsRule",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `stagingType` INTEGER NOT NULL, `type` INTEGER NOT NULL, `host` TEXT NOT NULL, `target` TEXT NOT NULL, `ipv6Target` TEXT, `importedFrom` INTEGER, `isWildcard` INTEGER NOT NULL, FOREIGN KEY(`importedFrom`) REFERENCES `HostSource`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "stagingType",
"columnName": "stagingType",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "target",
"columnName": "target",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ipv6Target",
"columnName": "ipv6Target",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "importedFrom",
"columnName": "importedFrom",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "isWildcard",
"columnName": "isWildcard",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_DnsRule_importedFrom",
"unique": false,
"columnNames": [
"importedFrom"
],
"createSql": "CREATE INDEX `index_DnsRule_importedFrom` ON `${TABLE_NAME}` (`importedFrom`)"
},
{
"name": "index_DnsRule_host_type_stagingType",
"unique": true,
"columnNames": [
"host",
"type",
"stagingType"
],
"createSql": "CREATE UNIQUE INDEX `index_DnsRule_host_type_stagingType` ON `${TABLE_NAME}` (`host`, `type`, `stagingType`)"
}
],
"foreignKeys": [
{
"table": "HostSource",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"importedFrom"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "HostSource",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `enabled` INTEGER NOT NULL, `ruleCount` INTEGER, `name` TEXT NOT NULL, `source` TEXT NOT NULL, `whitelistSource` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enabled",
"columnName": "enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ruleCount",
"columnName": "ruleCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "source",
"columnName": "source",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "whitelistSource",
"columnName": "whitelistSource",
"affinity": "INTEGER",
"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, 'daac9efbbf9d89b9aa10f7ad54ac736a')"
]
}
}
\ No newline at end of file
...@@ -37,7 +37,7 @@ import com.frostnerd.smokescreen.database.repository.HostSourceRepository ...@@ -37,7 +37,7 @@ import com.frostnerd.smokescreen.database.repository.HostSourceRepository
@Database(entities = [CachedResponse::class, DnsQuery::class, DnsRule::class, HostSource::class], version = AppDatabase.currentVersion) @Database(entities = [CachedResponse::class, DnsQuery::class, DnsRule::class, HostSource::class], version = AppDatabase.currentVersion)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
companion object { companion object {
const val currentVersion:Int = 9 const val currentVersion:Int = 10
} }
abstract fun cachedResponseDao(): CachedResponseDao abstract fun cachedResponseDao(): CachedResponseDao
......
...@@ -89,13 +89,19 @@ val MIGRATION_8_9 = migration(8, 9) { ...@@ -89,13 +89,19 @@ val MIGRATION_8_9 = migration(8, 9) {
it.execSQL("ALTER TABLE `HostSource` ADD COLUMN `ruleCount` INTEGER") it.execSQL("ALTER TABLE `HostSource` ADD COLUMN `ruleCount` INTEGER")
Logger.logIfOpen("DB_MIGRATION", "Migration from 8 to 9 completed") Logger.logIfOpen("DB_MIGRATION", "Migration from 8 to 9 completed")
} }
@VisibleForTesting
val MIGRATION_9_10 = migration(9, 10) {
Logger.logIfOpen("DB_MIGRATION", "Migrating from 9 to 10")
it.execSQL("ALTER TABLE `DnsRule` ADD COLUMN `isWildcard` INTEGER NOT NULL DEFAULT 0")
Logger.logIfOpen("DB_MIGRATION", "Migration from 9 to 10 completed")
}
fun Context.getDatabase(): AppDatabase { fun Context.getDatabase(): AppDatabase {
if (INSTANCE == null) { if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "data") INSTANCE = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "data")
.allowMainThreadQueries() .allowMainThreadQueries()
.addMigrations(MIGRATION_2_X, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9) .addMigrations(MIGRATION_2_X, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10)
.build() .build()
} }
return INSTANCE!! return INSTANCE!!
......
...@@ -75,9 +75,12 @@ interface DnsRuleDao { ...@@ -75,9 +75,12 @@ interface DnsRuleDao {
@Query("SELECT COUNT(*) FROM DnsRule WHERE importedFrom IS NOT NULL") @Query("SELECT COUNT(*) FROM DnsRule WHERE importedFrom IS NOT NULL")
fun getNonUserCount(): Long fun getNonUserCount(): Long
@Query("SELECT CASE WHEN :type=28 THEN IFNULL(ipv6Target, target) ELSE target END FROM DnsRule WHERE host=:host AND target != '' AND (type = :type OR type=255) AND (importedFrom is NULL OR IFNULL((SELECT enabled FROM HostSource h WHERE h.id=importedFrom),0) = 1) AND (importedFrom IS NOT NULL OR :useUserRules=1) AND (SELECT COUNT(*) FROM DnsRule WHERE target='' AND host=:host)=0 LIMIT 1") @Query("SELECT CASE WHEN :type=28 THEN IFNULL(ipv6Target, target) ELSE target END FROM DnsRule d1 WHERE d1.host=:host AND d1.target != '' AND (d1.type = :type OR d1.type=255) AND (d1.importedFrom is NULL OR IFNULL((SELECT enabled FROM HostSource h WHERE h.id=d1.importedFrom),0) = 1) AND (d1.importedFrom IS NOT NULL OR :useUserRules=1) AND (SELECT COUNT(*) FROM DnsRule d2 WHERE d2.target='' AND d2.host=:host AND (d2.type = :type OR d2.type=255) AND (d2.importedFrom IS NOT NULL OR :useUserRules=1) AND (importedFrom is NULL OR IFNULL((SELECT enabled FROM HostSource h WHERE h.id=importedFrom),0)))=0 AND isWildcard=0 LIMIT 1")
fun findRuleTarget(host: String, type: Record.TYPE, useUserRules:Boolean): String? fun findRuleTarget(host: String, type: Record.TYPE, useUserRules:Boolean): String?
@Query("SELECT * FROM DnsRule d1 WHERE ((:includeWhitelistEntries=1 AND d1.target == '') OR (:includeNonWhitelistEntries=1 AND d1.target!='')) AND (d1.type = :type OR d1.type=255) AND (d1.importedFrom is NULL OR IFNULL((SELECT enabled FROM HostSource h WHERE h.id=d1.importedFrom),0) = 1) AND (d1.importedFrom IS NOT NULL OR :useUserRules=1) AND :host LIKE host AND isWildcard=1")
fun findPossibleWildcardRuleTarget(host: String, type: Record.TYPE, useUserRules:Boolean, includeWhitelistEntries:Boolean, includeNonWhitelistEntries:Boolean):List<DnsRule>
@Query("DELETE FROM DnsRule WHERE importedFrom=:sourceId") @Query("DELETE FROM DnsRule WHERE importedFrom=:sourceId")
fun deleteAllFromSource(sourceId: Long) fun deleteAllFromSource(sourceId: Long)
......
...@@ -38,7 +38,8 @@ data class DnsRule( ...@@ -38,7 +38,8 @@ data class DnsRule(
val host: String, val host: String,
val target: String, val target: String,
val ipv6Target:String? = null, val ipv6Target:String? = null,
val importedFrom: Long? = null val importedFrom: Long? = null,
var isWildcard:Boolean = false
) { ) {
@PrimaryKey(autoGenerate = true) var id: Long = 0 @PrimaryKey(autoGenerate = true) var id: Long = 0
......
...@@ -11,6 +11,7 @@ import kotlinx.android.synthetic.main.dialog_create_dnsrule.view.* ...@@ -11,6 +11,7 @@ import kotlinx.android.synthetic.main.dialog_create_dnsrule.view.*
import org.minidns.record.Record import org.minidns.record.Record
import java.net.Inet4Address import java.net.Inet4Address
import java.net.Inet6Address import java.net.Inet6Address
import java.util.regex.Matcher
/* /*
* Copyright (C) 2019 Daniel Wolf (Ch4t4r) * Copyright (C) 2019 Daniel Wolf (Ch4t4r)
...@@ -33,6 +34,19 @@ import java.net.Inet6Address ...@@ -33,6 +34,19 @@ import java.net.Inet6Address
class DnsRuleDialog(context: Context, dnsRule: DnsRule? = null, onRuleCreated: (DnsRule) -> Unit) : class DnsRuleDialog(context: Context, dnsRule: DnsRule? = null, onRuleCreated: (DnsRule) -> Unit) :
AlertDialog(context, context.getPreferences().theme.dialogStyle) { AlertDialog(context, context.getPreferences().theme.dialogStyle) {
private var isWhitelist = false private var isWhitelist = false
companion object {
private val matchers = mutableMapOf<String, Matcher>()
fun printableHost(host:String): String {
return host.replace("%%", "**").replace("%", "*")
}
fun databaseHostToMatcher(host:String):Matcher {
return matchers.getOrPut(host, {
host.replace("%%", ".*").replace("%", "[^.]*").toPattern().matcher("")
})
}
}
init { init {
val view = layoutInflater.inflate(R.layout.dialog_create_dnsrule, null, false) val view = layoutInflater.inflate(R.layout.dialog_create_dnsrule, null, false)
...@@ -91,12 +105,21 @@ class DnsRuleDialog(context: Context, dnsRule: DnsRule? = null, onRuleCreated: ( ...@@ -91,12 +105,21 @@ class DnsRuleDialog(context: Context, dnsRule: DnsRule? = null, onRuleCreated: (
Record.TYPE.AAAA, Record.TYPE.ANY -> view.ipv6Address.text.toString() Record.TYPE.AAAA, Record.TYPE.ANY -> view.ipv6Address.text.toString()
else -> null else -> null
} }
var isWildcard = false
val host = view.host.text.toString().let {
if(it.contains("*")) {
isWildcard = true
it.replace("**", "%%").replace("*", "%")
} else it
}
val newRule = dnsRule?.copy( val newRule = dnsRule?.copy(
type = type, type = type,
host = view.host.text.toString(), host = host,
target = primaryTarget, target = primaryTarget,
ipv6Target = secondaryTarget ipv6Target = secondaryTarget,
) ?: DnsRule(type, view.host.text.toString(), primaryTarget, secondaryTarget) isWildcard = isWildcard
) ?: DnsRule(type, host, primaryTarget, secondaryTarget, isWildcard = isWildcard)
onRuleCreated( onRuleCreated(
newRule newRule
) )
...@@ -115,12 +138,13 @@ class DnsRuleDialog(context: Context, dnsRule: DnsRule? = null, onRuleCreated: ( ...@@ -115,12 +138,13 @@ class DnsRuleDialog(context: Context, dnsRule: DnsRule? = null, onRuleCreated: (
if (dnsRule != null) { if (dnsRule != null) {
if (dnsRule.isWhitelistRule()) { if (dnsRule.isWhitelistRule()) {
isWhitelist = true isWhitelist = true
view.host.setText(printableHost(dnsRule.host))
getButton(DialogInterface.BUTTON_NEUTRAL).text = getButton(DialogInterface.BUTTON_NEUTRAL).text =
context.getString(R.string.dialog_newdnsrule_specify_address) context.getString(R.string.dialog_newdnsrule_specify_address)
view.ipv4Til.visibility = View.GONE view.ipv4Til.visibility = View.GONE
view.ipv6Til.visibility = View.GONE view.ipv6Til.visibility = View.GONE
} else { } else {
view.host.setText(dnsRule.host) view.host.setText(printableHost(dnsRule.host))
when { when {
dnsRule.type == Record.TYPE.A -> { dnsRule.type == Record.TYPE.A -> {
view.ipv4Address.setText(dnsRule.target) view.ipv4Address.setText(dnsRule.target)
......
...@@ -552,7 +552,7 @@ class DnsRuleFragment : Fragment() { ...@@ -552,7 +552,7 @@ class DnsRuleFragment : Fragment() {
fun display(rule:DnsRule) { fun display(rule:DnsRule) {
dnsRule = rule dnsRule = rule
text.text = rule.host text.text = DnsRuleDialog.printableHost(rule.host)
whitelistIndicator.visibility = if(rule.isWhitelistRule()) View.VISIBLE else View.GONE whitelistIndicator.visibility = if(rule.isWhitelistRule()) View.VISIBLE else View.GONE
} }
override fun destroy() {} override fun destroy() {}
......
...@@ -28,6 +28,7 @@ import com.frostnerd.smokescreen.activity.PinActivity ...@@ -28,6 +28,7 @@ import com.frostnerd.smokescreen.activity.PinActivity
import com.frostnerd.smokescreen.activity.PinType import com.frostnerd.smokescreen.activity.PinType
import com.frostnerd.smokescreen.database.entities.CachedResponse import com.frostnerd.smokescreen.database.entities.CachedResponse
import com.frostnerd.smokescreen.database.getDatabase import com.frostnerd.smokescreen.database.getDatabase
import com.frostnerd.smokescreen.dialog.DnsRuleDialog
import com.frostnerd.smokescreen.util.DeepActionState import com.frostnerd.smokescreen.util.DeepActionState
import com.frostnerd.smokescreen.util.Notifications import com.frostnerd.smokescreen.util.Notifications
import com.frostnerd.smokescreen.util.preferences.VpnServiceState import com.frostnerd.smokescreen.util.preferences.VpnServiceState
...@@ -963,9 +964,37 @@ class DnsVpnService : VpnService(), Runnable { ...@@ -963,9 +964,37 @@ class DnsVpnService : VpnService(), Runnable {
} }
} }
if (resolveResult != null) { if (resolveResult != null) {
resolveResults[question] = resolveResult val isWildcardWhitelisted = dao.findPossibleWildcardRuleTarget(uniformQuestion, question.type, useUserRules, true, false).any {
true DnsRuleDialog.databaseHostToMatcher(it.host).reset(uniformQuestion).matches()
} else false }
if(!isWildcardWhitelisted) {
resolveResults[question] = resolveResult
true
} else false
} else {
val wildcardResolveResults = dao.findPossibleWildcardRuleTarget(uniformQuestion, question.type, useUserRules, true, true).filter {
DnsRuleDialog.databaseHostToMatcher(it.host).reset(uniformQuestion).matches()
}
if(wildcardResolveResults.isEmpty() || wildcardResolveResults.any {
it.isWhitelistRule()
}) false
else {
resolveResults[question] = wildcardResolveResults.first().let {
if(question.type == Record.TYPE.AAAA) it.ipv6Target ?: it.target
else it.target
}.let {
when (it) {
"0" -> "0.0.0.0"
"1" -> {
if (question.type == Record.TYPE.AAAA) "::1"
else "127.0.0.1"
}
else -> it
}
}
true
}
}
} }
} }
......
...@@ -57,4 +57,10 @@ ...@@ -57,4 +57,10 @@
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="wrap_content"
android:alpha="0.8"
android:text="@string/dialog_newdnsrule_wildcard_info"
android:layout_height="wrap_content"/>
</LinearLayout> </LinearLayout>
\ No newline at end of file
...@@ -141,6 +141,7 @@ ...@@ -141,6 +141,7 @@
<string name="dialog_newdnsrule_host_invalid">Please provide a valid host</string> <string name="dialog_newdnsrule_host_invalid">Please provide a valid host</string>
<string name="dialog_newdnsrule_ipv4_invalid">Please provide a valid IPv4 address</string> <string name="dialog_newdnsrule_ipv4_invalid">Please provide a valid IPv4 address</string>
<string name="dialog_newdnsrule_ipv6_invalid">Please provide a valid IPv6 address</string> <string name="dialog_newdnsrule_ipv6_invalid">Please provide a valid IPv6 address</string>
<string name="dialog_newdnsrule_wildcard_info">The host supports wildcards. Use star (*) for any amount of characters, numbers and allowed special characters except period (.). Use two stars (**) to include the period (.).</string>
<string name="dialog_exportdnsrules_title">Export dns rules</string> <string name="dialog_exportdnsrules_title">Export dns rules</string>
<string name="dialog_exportdnsrules_export_user_rules">Export custom host dns rules</string> <string name="dialog_exportdnsrules_export_user_rules">Export custom host dns rules</string>
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment