android: DNS and ads as in the Final 2026 mockup: filtering resolvers (AdGuard, Cloudflare 1.1.1.2), own DNS, applied to the tunnel
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
dcf8ace6e1
commit
8880601ef6
7 files changed
+358
-2
No files matched your search
+5
-2
@@ -2402,9 +2402,12 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
|
||||
sAppliedGeoExclusionsV6 = mGeoExcludedV6;
|
||||
|
||||
|
||||
if (profile.getDnsServers() != null)
|
||||
/* F_Protection: a chosen resolver (filtering or not) wins over the profile's */
|
||||
java.util.List<String> chosenDns = org.strongswan.android.logic.protection.Protection.dnsServers(CharonVpnService.this);
|
||||
String dnsServers = chosenDns != null ? android.text.TextUtils.join(" ", chosenDns) : profile.getDnsServers();
|
||||
if (dnsServers != null)
|
||||
{
|
||||
for (String server : profile.getDnsServers().split("\\s+"))
|
||||
for (String server : dnsServers.split("\\s+"))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright (C) 2026 SHX VPN
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
package org.strongswan.android.logic.protection
|
||||
|
||||
import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/** The DNS server of the tunnel (F_Protection «DNS server»). */
|
||||
enum class DnsChoice { PROFILE, CLOUDFLARE, ADGUARD, CUSTOM }
|
||||
|
||||
/**
|
||||
* DNS and blocking (F_Protection). The blocking is done by the resolver
|
||||
* itself, so it follows from the choice: AdGuard DNS blocks ads, trackers
|
||||
* and phishing together, Cloudflare's 1.1.1.2 blocks malware and phishing
|
||||
* only. The servers replace the profile's ones on the next connect.
|
||||
*/
|
||||
data class ProtectionState(
|
||||
val choice: DnsChoice = DnsChoice.PROFILE,
|
||||
/** with CLOUDFLARE: the malware / phishing filtering resolver */
|
||||
val phishingFilter: Boolean = false,
|
||||
val custom: String = "",
|
||||
)
|
||||
{
|
||||
val blocksAds: Boolean get() = choice == DnsChoice.ADGUARD
|
||||
val blocksTrackers: Boolean get() = choice == DnsChoice.ADGUARD
|
||||
val blocksPhishing: Boolean get() = choice == DnsChoice.ADGUARD || (choice == DnsChoice.CLOUDFLARE && phishingFilter)
|
||||
|
||||
/** null: keep the profile's (or the server's) DNS */
|
||||
fun servers(): List<String>? = when (choice)
|
||||
{
|
||||
DnsChoice.PROFILE -> null
|
||||
DnsChoice.ADGUARD -> listOf("94.140.14.14", "94.140.15.15", "2a10:50c0::ad1:ff", "2a10:50c0::ad2:ff")
|
||||
DnsChoice.CLOUDFLARE -> if (phishingFilter) listOf("1.1.1.2", "1.0.0.2", "2606:4700:4700::1112", "2606:4700:4700::1002")
|
||||
else listOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
|
||||
DnsChoice.CUSTOM -> parseCustom(custom).takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
fun withAds(on: Boolean): ProtectionState = when
|
||||
{
|
||||
on -> copy(choice = DnsChoice.ADGUARD)
|
||||
choice == DnsChoice.ADGUARD -> copy(choice = DnsChoice.CLOUDFLARE, phishingFilter = true)
|
||||
else -> this
|
||||
}
|
||||
|
||||
fun withPhishing(on: Boolean): ProtectionState = when
|
||||
{
|
||||
on && choice == DnsChoice.ADGUARD -> this
|
||||
on -> copy(choice = DnsChoice.CLOUDFLARE, phishingFilter = true)
|
||||
choice == DnsChoice.ADGUARD -> copy(choice = DnsChoice.CLOUDFLARE, phishingFilter = false)
|
||||
else -> copy(phishingFilter = false)
|
||||
}
|
||||
|
||||
companion object
|
||||
{
|
||||
/** «1.2.3.4, 2001:db8::1» → the addresses that parse as IPs */
|
||||
fun parseCustom(text: String): List<String> = text.split(',', ' ', ';').map { it.trim() }.filter { it.isNotEmpty() }.filter { isIp(it) }
|
||||
|
||||
private fun isIp(s: String): Boolean =
|
||||
Regex("""^(\d{1,3})(\.\d{1,3}){3}$""").matches(s) && s.split('.').all { it.toInt() in 0..255 } ||
|
||||
(s.contains(':') && Regex("""^[0-9a-fA-F:]+$""").matches(s))
|
||||
}
|
||||
}
|
||||
|
||||
object Protection
|
||||
{
|
||||
private const val PREF_CHOICE = "pref_dns_choice"
|
||||
private const val PREF_PHISHING = "pref_dns_phishing"
|
||||
private const val PREF_CUSTOM = "pref_dns_custom"
|
||||
|
||||
private val _state = MutableStateFlow<ProtectionState?>(null)
|
||||
|
||||
@JvmStatic
|
||||
fun get(context: Context): ProtectionState
|
||||
{
|
||||
_state.value?.let { return it }
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val state = ProtectionState(
|
||||
choice = runCatching { DnsChoice.valueOf(prefs.getString(PREF_CHOICE, DnsChoice.PROFILE.name)!!) }.getOrDefault(DnsChoice.PROFILE),
|
||||
phishingFilter = prefs.getBoolean(PREF_PHISHING, false),
|
||||
custom = prefs.getString(PREF_CUSTOM, "") ?: "",
|
||||
)
|
||||
_state.value = state
|
||||
return state
|
||||
}
|
||||
|
||||
fun flow(context: Context): StateFlow<ProtectionState?>
|
||||
{
|
||||
get(context)
|
||||
return _state
|
||||
}
|
||||
|
||||
fun set(context: Context, state: ProtectionState)
|
||||
{
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit()
|
||||
.putString(PREF_CHOICE, state.choice.name).putBoolean(PREF_PHISHING, state.phishingFilter).putString(PREF_CUSTOM, state.custom).apply()
|
||||
_state.value = state
|
||||
}
|
||||
|
||||
/** For CharonVpnService: the servers replacing the profile's ones, or null. */
|
||||
@JvmStatic
|
||||
fun dnsServers(context: Context): List<String>? = get(context).servers()
|
||||
}
|
||||
+117
@@ -580,3 +580,120 @@ internal fun InstallPermissionSheet(onOpen: () -> Unit, onDismiss: () -> Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun dnsName(choice: org.strongswan.android.logic.protection.DnsChoice): String = when (choice)
|
||||
{
|
||||
org.strongswan.android.logic.protection.DnsChoice.PROFILE -> stringResource(R.string.prot_profile)
|
||||
org.strongswan.android.logic.protection.DnsChoice.CLOUDFLARE -> "Cloudflare"
|
||||
org.strongswan.android.logic.protection.DnsChoice.ADGUARD -> "AdGuard DNS"
|
||||
org.strongswan.android.logic.protection.DnsChoice.CUSTOM -> stringResource(R.string.prot_custom)
|
||||
}
|
||||
|
||||
/** «Cloudflare · blocking on» line of the settings list. */
|
||||
@Composable
|
||||
internal fun protectionSub(state: org.strongswan.android.logic.protection.ProtectionState?): String
|
||||
{
|
||||
state ?: return ""
|
||||
val blocking = state.blocksAds || state.blocksPhishing
|
||||
return dnsName(state.choice) + " · " + stringResource(if (blocking) R.string.prot_on else R.string.prot_off).lowercase()
|
||||
}
|
||||
|
||||
/** «DNS and ads» (F_Protection): what is blocked, the blocking switches, the DNS server. */
|
||||
@Composable
|
||||
internal fun ProtectionPage()
|
||||
{
|
||||
val c = LocalShxColors.current
|
||||
val context = LocalContext.current
|
||||
val stateNullable by remember { org.strongswan.android.logic.protection.Protection.flow(context) }.collectAsStateWithLifecycle()
|
||||
val state = stateNullable ?: return
|
||||
var custom by remember { mutableStateOf(state.custom) }
|
||||
val applies = stringResource(R.string.prot_applies)
|
||||
fun update(next: org.strongswan.android.logic.protection.ProtectionState)
|
||||
{
|
||||
if (next == state) return
|
||||
org.strongswan.android.logic.protection.Protection.set(context, next)
|
||||
/* the TUN device gets its DNS servers when it is built */
|
||||
val service = org.strongswan.android.logic.VpnStateService.getLiveInstance()
|
||||
if (service != null && service.state == org.strongswan.android.logic.VpnStateService.State.CONNECTED) service.reconnect()
|
||||
else android.widget.Toast.makeText(context, applies, android.widget.Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
Column(Modifier.padding(horizontal = 16.dp).padding(top = 4.dp), verticalArrangement = Arrangement.spacedBy(12.dp))
|
||||
{
|
||||
/* what the resolver blocks (the app cannot count the blocked queries: the resolver answers them) */
|
||||
val blocked = listOfNotNull(
|
||||
if (state.blocksAds) stringResource(R.string.prot_list_ads) else null,
|
||||
if (state.blocksTrackers) stringResource(R.string.prot_list_trackers) else null,
|
||||
if (state.blocksPhishing) stringResource(R.string.prot_list_phishing) else null,
|
||||
)
|
||||
val on = blocked.isNotEmpty()
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(24.dp)).background(if (on) c.successContainer else c.tile).padding(18.dp),
|
||||
)
|
||||
{
|
||||
Text(stringResource(if (on) R.string.prot_on else R.string.prot_off), style = ShxType.chip, color = if (on) c.onSuccess else c.textSoft)
|
||||
Text(dnsName(state.choice), style = ShxType.number.copy(fontSize = 30.sp, lineHeight = 34.sp), color = c.text)
|
||||
Text(
|
||||
if (on) stringResource(R.string.prot_what, blocked.joinToString(" · ")) else stringResource(R.string.prot_none),
|
||||
style = ShxType.chip.copy(fontWeight = FontWeight.SemiBold), color = if (on) c.onSuccess else c.textSub,
|
||||
)
|
||||
}
|
||||
ShxSectionHeader(stringResource(R.string.prot_block).uppercase(), Modifier.padding(start = 6.dp, end = 6.dp, top = 6.dp))
|
||||
ShxPanel()
|
||||
{
|
||||
ShxToggleRow(stringResource(R.string.prot_ads), state.blocksAds, { update(state.withAds(it)) }, subtitle = stringResource(R.string.prot_ads_sub), divider = true)
|
||||
ShxToggleRow(stringResource(R.string.prot_trackers), state.blocksTrackers, { update(state.withAds(it)) }, subtitle = stringResource(R.string.prot_trackers_sub), divider = true)
|
||||
ShxToggleRow(stringResource(R.string.prot_phishing), state.blocksPhishing, { update(state.withPhishing(it)) }, subtitle = stringResource(R.string.prot_phishing_sub))
|
||||
}
|
||||
Text(stringResource(R.string.prot_together), style = ShxType.meta.copy(fontWeight = FontWeight.Normal, lineHeight = 17.sp), color = c.textSub, modifier = Modifier.padding(horizontal = 6.dp))
|
||||
ShxSectionHeader(stringResource(R.string.prot_dns).uppercase(), Modifier.padding(start = 6.dp, end = 6.dp, top = 6.dp))
|
||||
ShxPanel()
|
||||
{
|
||||
val rows = listOf(
|
||||
org.strongswan.android.logic.protection.DnsChoice.PROFILE to (stringResource(R.string.prot_profile) to stringResource(R.string.prot_profile_sub)),
|
||||
org.strongswan.android.logic.protection.DnsChoice.CLOUDFLARE to ("Cloudflare" to stringResource(R.string.prot_cloudflare_sub)),
|
||||
org.strongswan.android.logic.protection.DnsChoice.ADGUARD to ("AdGuard DNS" to stringResource(R.string.prot_adguard_sub)),
|
||||
org.strongswan.android.logic.protection.DnsChoice.CUSTOM to (stringResource(R.string.prot_custom) to stringResource(R.string.prot_custom_sub)),
|
||||
)
|
||||
rows.forEachIndexed { i, (choice, texts) ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth().height(62.dp).selectable(selected = state.choice == choice, role = Role.RadioButton) {
|
||||
if (choice == org.strongswan.android.logic.protection.DnsChoice.CUSTOM && org.strongswan.android.logic.protection.ProtectionState.parseCustom(custom).isEmpty())
|
||||
{ /* the field below takes the address first */
|
||||
org.strongswan.android.logic.protection.Protection.set(context, state.copy(choice = choice))
|
||||
}
|
||||
else update(state.copy(choice = choice, phishingFilter = if (choice == org.strongswan.android.logic.protection.DnsChoice.CLOUDFLARE) state.phishingFilter else false))
|
||||
}.padding(horizontal = 16.dp),
|
||||
)
|
||||
{
|
||||
androidx.compose.material3.RadioButton(selected = state.choice == choice, onClick = null, colors = androidx.compose.material3.RadioButtonDefaults.colors(selectedColor = c.accent))
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp))
|
||||
{
|
||||
Text(texts.first, style = ShxType.rowTitle.copy(fontSize = 15.sp), color = c.text)
|
||||
Text(texts.second, style = ShxType.meta.copy(fontWeight = FontWeight.Normal), color = c.textSub)
|
||||
}
|
||||
}
|
||||
if (i < rows.lastIndex)
|
||||
{
|
||||
Box(Modifier.fillMaxWidth().height(1.dp).background(c.tile))
|
||||
}
|
||||
}
|
||||
if (state.choice == org.strongswan.android.logic.protection.DnsChoice.CUSTOM)
|
||||
{
|
||||
Column(Modifier.padding(start = 16.dp, end = 16.dp, bottom = 14.dp), verticalArrangement = Arrangement.spacedBy(8.dp))
|
||||
{
|
||||
org.strongswan.android.ui.compose.design.ShxFormField(label = stringResource(R.string.prot_custom), value = custom, onValueChange = { custom = it }, placeholder = "9.9.9.9")
|
||||
ShxButton(
|
||||
stringResource(R.string.server_save), { update(state.copy(custom = custom)) },
|
||||
style = ShxButtonStyle.TONAL, height = 44.dp, modifier = Modifier.fillMaxWidth(),
|
||||
enabled = org.strongswan.android.logic.protection.ProtectionState.parseCustom(custom).isNotEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(stringResource(R.string.prot_note), style = ShxType.meta.copy(fontWeight = FontWeight.Normal, lineHeight = 17.sp), color = c.textSub, modifier = Modifier.padding(horizontal = 6.dp))
|
||||
}
|
||||
}
|
||||
+12
@@ -400,6 +400,11 @@ fun SettingsScreen(profiles: List<VpnProfile>, showTitle: Boolean = true)
|
||||
}
|
||||
}
|
||||
|
||||
if (page == SettingsPage.PROTECTION)
|
||||
{
|
||||
ProtectionPage()
|
||||
}
|
||||
|
||||
if (page == SettingsPage.NOTIFICATIONS)
|
||||
{
|
||||
NotifSettingsPage()
|
||||
@@ -724,6 +729,7 @@ enum class SettingsPage(val titleRes: Int)
|
||||
KILLSWITCH(R.string.settings_killswitch),
|
||||
HELP(R.string.settings_help),
|
||||
NOTIFICATIONS(R.string.settings_notifications),
|
||||
PROTECTION(R.string.prot_title),
|
||||
}
|
||||
|
||||
/** One row of the settings list (F_Settings): 62dp, 36dp grey icon well, 15/700 title, 12 sub. */
|
||||
@@ -758,6 +764,7 @@ private fun SettingsRoot(onOpen: (SettingsPage) -> Unit, updateState: UpdateStat
|
||||
val certs = remember { runCatching { TrustedCertificateManager.getInstance().load().getAllCACertificates().size }.getOrDefault(0) }
|
||||
val scenarioList by remember { org.strongswan.android.logic.scenarios.ScenarioEngine.flow(context) }.collectAsStateWithLifecycle()
|
||||
val scenarioCount = scenarioList.count { it.enabled }
|
||||
val protection by remember { org.strongswan.android.logic.protection.Protection.flow(context) }.collectAsStateWithLifecycle()
|
||||
/* «2 from profiles · 1 expires soon» */
|
||||
val certSummary by produceState(0 to 0) {
|
||||
value = withContext(Dispatchers.IO) { runCatching { org.strongswan.android.ui.certificateSummary(context) }.getOrDefault(0 to 0) }
|
||||
@@ -783,6 +790,11 @@ private fun SettingsRoot(onOpen: (SettingsPage) -> Unit, updateState: UpdateStat
|
||||
) { context.startActivity(Intent(context, ConnectivityCheckActivity::class.java)) },
|
||||
),
|
||||
stringResource(R.string.settings_protection) to listOf(
|
||||
SettingsEntry(
|
||||
stringResource(R.string.prot_title),
|
||||
protectionSub(protection),
|
||||
ShxIcons.Shield,
|
||||
) { onOpen(SettingsPage.PROTECTION) },
|
||||
SettingsEntry(
|
||||
stringResource(R.string.settings_killswitch),
|
||||
stringResource(if (lockdown) R.string.settings_killswitch_on else R.string.settings_killswitch_off),
|
||||
|
||||
@@ -1292,4 +1292,30 @@
|
||||
<string name="suggest_no">Не надо</string>
|
||||
<string name="home_problem_update_profile">Обновить профиль</string>
|
||||
<string name="home_stop_vpn">Отключить VPN</string>
|
||||
<string name="prot_title">DNS и реклама</string>
|
||||
<string name="prot_on">Блокировка включена</string>
|
||||
<string name="prot_off">Блокировка выключена</string>
|
||||
<string name="prot_by">через %1$s</string>
|
||||
<string name="prot_what">Блокируются: %1$s</string>
|
||||
<string name="prot_none">Сайты открываются без фильтрации</string>
|
||||
<string name="prot_block">Блокировать</string>
|
||||
<string name="prot_ads">Рекламу</string>
|
||||
<string name="prot_ads_sub">Баннеры в браузере и приложениях</string>
|
||||
<string name="prot_trackers">Трекеры</string>
|
||||
<string name="prot_trackers_sub">Аналитика и сбор данных</string>
|
||||
<string name="prot_phishing">Фишинг</string>
|
||||
<string name="prot_phishing_sub">Поддельные сайты банков и сервисов</string>
|
||||
<string name="prot_together">Реклама и трекеры блокируются вместе — через AdGuard DNS; только фишинг — через Cloudflare 1.1.1.2.</string>
|
||||
<string name="prot_dns">DNS-сервер</string>
|
||||
<string name="prot_profile">Из профиля сервера</string>
|
||||
<string name="prot_profile_sub">Как настроил администратор</string>
|
||||
<string name="prot_cloudflare_sub">1.1.1.1 · через туннель</string>
|
||||
<string name="prot_adguard_sub">Со встроенной фильтрацией</string>
|
||||
<string name="prot_custom">Свой адрес</string>
|
||||
<string name="prot_custom_sub">IPv4 или IPv6 через запятую</string>
|
||||
<string name="prot_note">Запросы к этому DNS идут через туннель — провайдер не сможет подменить ответ. Приложения, которые идут мимо VPN, используют DNS сети.</string>
|
||||
<string name="prot_applies">Применится при переподключении</string>
|
||||
<string name="prot_list_ads">реклама</string>
|
||||
<string name="prot_list_trackers">трекеры</string>
|
||||
<string name="prot_list_phishing">фишинг</string>
|
||||
</resources>
|
||||
@@ -1278,4 +1278,30 @@
|
||||
<string name="suggest_no">No thanks</string>
|
||||
<string name="home_problem_update_profile">Update profile</string>
|
||||
<string name="home_stop_vpn">Turn VPN off</string>
|
||||
<string name="prot_title">DNS and ads</string>
|
||||
<string name="prot_on">Blocking is on</string>
|
||||
<string name="prot_off">Blocking is off</string>
|
||||
<string name="prot_by">by %1$s</string>
|
||||
<string name="prot_what">Blocked: %1$s</string>
|
||||
<string name="prot_none">Sites resolve without filtering</string>
|
||||
<string name="prot_block">Block</string>
|
||||
<string name="prot_ads">Ads</string>
|
||||
<string name="prot_ads_sub">Banners in the browser and apps</string>
|
||||
<string name="prot_trackers">Trackers</string>
|
||||
<string name="prot_trackers_sub">Analytics and data collection</string>
|
||||
<string name="prot_phishing">Phishing</string>
|
||||
<string name="prot_phishing_sub">Fake bank and service sites</string>
|
||||
<string name="prot_together">Ads and trackers are blocked together by AdGuard DNS; phishing alone by Cloudflare 1.1.1.2.</string>
|
||||
<string name="prot_dns">DNS server</string>
|
||||
<string name="prot_profile">From the server profile</string>
|
||||
<string name="prot_profile_sub">As the admin set it up</string>
|
||||
<string name="prot_cloudflare_sub">1.1.1.1 · through the tunnel</string>
|
||||
<string name="prot_adguard_sub">With built-in filtering</string>
|
||||
<string name="prot_custom">Own address</string>
|
||||
<string name="prot_custom_sub">IPv4 or IPv6, comma separated</string>
|
||||
<string name="prot_note">Queries to this DNS go through the tunnel — the provider cannot change the answer. Apps that bypass the VPN use the network\'s DNS.</string>
|
||||
<string name="prot_applies">Applies on reconnect</string>
|
||||
<string name="prot_list_ads">ads</string>
|
||||
<string name="prot_list_trackers">trackers</string>
|
||||
<string name="prot_list_phishing">phishing</string>
|
||||
</resources>
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2026 SHX VPN
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*/
|
||||
|
||||
package org.strongswan.android.logic.protection
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
|
||||
class ProtectionTest
|
||||
{
|
||||
@Test
|
||||
fun profileKeepsItsDns()
|
||||
{
|
||||
assertThat(ProtectionState().servers()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun adsMeanAdGuard()
|
||||
{
|
||||
val s = ProtectionState().withAds(true)
|
||||
assertThat(s.choice).isEqualTo(DnsChoice.ADGUARD)
|
||||
assertThat(s.blocksTrackers && s.blocksPhishing).isTrue()
|
||||
assertThat(s.servers()).contains("94.140.14.14")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun adsOffKeepsPhishing()
|
||||
{
|
||||
val s = ProtectionState().withAds(true).withAds(false)
|
||||
assertThat(s.choice).isEqualTo(DnsChoice.CLOUDFLARE)
|
||||
assertThat(s.blocksAds).isFalse()
|
||||
assertThat(s.blocksPhishing).isTrue()
|
||||
assertThat(s.servers()).contains("1.1.1.2")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun phishingAloneIsCloudflareFiltering()
|
||||
{
|
||||
val s = ProtectionState().withPhishing(true)
|
||||
assertThat(s.servers()).containsExactly("1.1.1.2", "1.0.0.2", "2606:4700:4700::1112", "2606:4700:4700::1002")
|
||||
assertThat(s.withPhishing(false).servers()).contains("1.1.1.1")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun customAddresses()
|
||||
{
|
||||
assertThat(ProtectionState.parseCustom("9.9.9.9, 2620:fe::fe, bad, 300.1.1.1")).containsExactly("9.9.9.9", "2620:fe::fe")
|
||||
assertThat(ProtectionState(choice = DnsChoice.CUSTOM, custom = "x").servers()).isNull()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user