Commit ec432103 authored by Daniel Wolf's avatar Daniel Wolf
Browse files

Add a setting to inform the user when the DNS server is performing bad (bad latency or packet loss)

Implements #263
parent dbcbf8ff
......@@ -130,7 +130,7 @@ dependencies {
implementation 'com.frostnerd.utilskt:preferences:1.5.29' // https://git.frostnerd.com/AndroidUtils/preferenceskt
implementation 'com.frostnerd.utilskt:navigationdraweractivity:1.4.0' // https://git.frostnerd.com/AndroidUtils/navigationdraweractivity
implementation 'com.frostnerd.utilskt:encrypteddnstunnelproxy:1.6.4' // https://git.frostnerd.com/AndroidUtils/encrypteddnstunnelproxy
implementation 'com.frostnerd.utilskt:encrypteddnstunnelproxy:1.6.5' // https://git.frostnerd.com/AndroidUtils/encrypteddnstunnelproxy
implementation 'com.frostnerd.utilskt:general:1.0.25' // https://git.frostnerd.com/AndroidUtils/generalkt
implementation 'com.frostnerd.utilskt:adapters:1.2.0' // https://git.frostnerd.com/AndroidUtils/Adapters
......
package com.frostnerd.smokescreen.service
import com.frostnerd.vpntunnelproxy.TrafficStats
import kotlinx.coroutines.*
/*
* Copyright (C) 2020 Daniel Wolf (Ch4t4r)
*
* 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 3 of the License, or
* (at your option) any later version.
* 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.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can contact the developer at daniel.wolf@frostnerd.com.
*/
class ConnectionWatchdog(private val trafficStats: TrafficStats,
val checkIntervalMs:Long,
val debounceCallbackByMs:Long? = null,
val badLatencyThresholdMs:Int = 900,
val badPacketLossThresholdPercent:Int = 30,
private val onBadServerConnection:() -> Unit) {
val supervisor = SupervisorJob()
val scope = CoroutineScope(supervisor + Dispatchers.IO)
private var running = true
private var latencyAtLastCheck:Int? = null
private var packetLossAtLastCheck:Int? = null
private var packetCountAtLastCheck:Int? = null
private var lastCallbackCall:Long? = null
init {
scope.launch {
checkConnection()
}
}
private suspend fun checkConnection() {
delay(checkIntervalMs)
if(trafficStats.packetsReceivedFromDevice >= 15
&& trafficStats.bytesSentToDevice > 0
&& packetCountAtLastCheck?.let { trafficStats.packetsReceivedFromDevice - it > 10 } != false
) { // Not enough data to act on.
val currentLatency = trafficStats.floatingAverageLatency.toInt()
val currentPacketLossPercent = (100*trafficStats.failedAnswers)/(trafficStats.packetsReceivedFromDevice*0.9)
if(latencyAtLastCheck?.let { it > badLatencyThresholdMs } == true && currentLatency > badLatencyThresholdMs) {
callCallback()
} else if(packetLossAtLastCheck?.let { it > badPacketLossThresholdPercent } == true && currentPacketLossPercent > badPacketLossThresholdPercent) {
callCallback()
}
latencyAtLastCheck = trafficStats.floatingAverageLatency.toInt()
packetLossAtLastCheck = currentPacketLossPercent.toInt()
}
if(running) {
scope.launch {
checkConnection()
}
}
}
private fun callCallback() {
if(!running) return
if(debounceCallbackByMs == null || lastCallbackCall == null) onBadServerConnection()
else if(System.currentTimeMillis() - lastCallbackCall!! > debounceCallbackByMs) onBadServerConnection()
lastCallbackCall = System.currentTimeMillis()
}
fun stop() {
supervisor.cancel()
running = false
}
}
\ No newline at end of file
......@@ -99,6 +99,8 @@ class DnsVpnService : VpnService(), Runnable {
private var dnsCache:SimpleDnsCache? = null
private var localResolver:LocalResolver? = null
private var runInNonVpnMode:Boolean = getPreferences().runWithoutVpn
private var connectionWatchDog:ConnectionWatchdog? = null
private var watchdogDisabledForSession = false
private val addressResolveScope:CoroutineScope by lazy {
CoroutineScope(newSingleThreadContext("service-resolve-retry"))
}
......@@ -356,7 +358,8 @@ class DnsVpnService : VpnService(), Runnable {
"simple_notification",
"pin",
"nonvpn_use_iptables",
"nonvpn_iptables_disable_ipv6"
"nonvpn_iptables_disable_ipv6",
"connection_watchdog"
)
settingsSubscription = getPreferences().listenForChanges(
relevantSettings,
......@@ -557,6 +560,49 @@ class DnsVpnService : VpnService(), Runnable {
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).cancel(Notifications.ID_MULTIPLEUSERS_WARNING)
}
private fun showBadConnectionNotification() {
val builder = NotificationCompat.Builder(
this,
Notifications.getBadConnectionChannelId(this)
)
builder.priority = NotificationCompat.PRIORITY_DEFAULT
builder.setOngoing(false)
builder.setSmallIcon(R.drawable.ic_cloud_strikethrough)
builder.setContentTitle(getString(R.string.notification_bad_connection_title))
builder.setContentText(getString(R.string.notification_bad_connection_text))
builder.setStyle(
NotificationCompat.BigTextStyle(
notificationBuilder
).bigText(getString(R.string.notification_bad_connection_text))
)
val ignoreIntent = Intent(this, DnsVpnService::class.java).putExtra(
"command",
Command.IGNORE_BAD_CONNECTION
)
val ignorePendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PendingIntent.getForegroundService(
this@DnsVpnService,
RequestCodes.REQUEST_CODE_IGNORE_BAD_CONNECTION,
ignoreIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getService(
this@DnsVpnService,
RequestCodes.REQUEST_CODE_IGNORE_BAD_CONNECTION,
ignoreIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
builder.addAction(NotificationCompat.Action(null, getString(R.string.notification_service_killed_ignore), ignorePendingIntent))
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(Notifications.ID_BAD_SERVER_CONNECTION, builder.build())
}
private fun hideBadConnectionNotification() {
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).cancel(Notifications.ID_BAD_SERVER_CONNECTION)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
log("Service onStartCommand", intent = intent)
runInNonVpnMode = getPreferences().runWithoutVpn
......@@ -613,6 +659,11 @@ class DnsVpnService : VpnService(), Runnable {
dnsProxy?.dnsCache?.clear()
restartVpn(this, false)
}
Command.IGNORE_BAD_CONNECTION -> {
watchdogDisabledForSession = true
connectionWatchDog?.stop()
hideBadConnectionNotification()
}
}
} else {
log("No command passed, fetching servers and establishing connection if needed")
......@@ -817,6 +868,7 @@ class DnsVpnService : VpnService(), Runnable {
if (!destroyed) {
vpnProxy?.stop()
dnsServerProxy?.stop()
connectionWatchDog?.stop()
if(ipTablesRedirector == null || ipTablesRedirector?.endForward() == IpTablesPacketRedirector.IpTablesMode.SUCCEEDED) {
getPreferences().lastIptablesRedirectAddress = null
getPreferences().lastIptablesRedirectAddressIPv6 = null
......@@ -1244,6 +1296,12 @@ class DnsVpnService : VpnService(), Runnable {
log("VPN proxy started.")
}
currentTrafficStats = vpnProxy?.trafficStats
if(!watchdogDisabledForSession && getPreferences().enableConnectionWatchDog) connectionWatchDog = currentTrafficStats?.let {
ConnectionWatchdog(it, 30*1000, 10*60*10000) {
showBadConnectionNotification()
}
}
hideBadConnectionNotification()
LocalBroadcastManager.getInstance(this).sendBroadcast(Intent(BROADCAST_VPN_ACTIVE))
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).cancel(Notifications.ID_SERVICE_REVOKED)
}
......@@ -1431,7 +1489,7 @@ class DnsVpnService : VpnService(), Runnable {
}
enum class Command : Serializable {
STOP, RESTART, PAUSE_RESUME, IGNORE_SERVICE_KILLED, INVALIDATE_DNS_CACHE
STOP, RESTART, PAUSE_RESUME, IGNORE_SERVICE_KILLED, INVALIDATE_DNS_CACHE, IGNORE_BAD_CONNECTION
}
data class DnsServerConfiguration(
......
......@@ -40,6 +40,7 @@ class Notifications {
const val ID_DNSSERVER_MODE = 11
const val ID_PRIVATEDNS_WARNING = 12
const val ID_MULTIPLEUSERS_WARNING = 13
const val ID_BAD_SERVER_CONNECTION = 14
const val ID_VPN_RESTART = 999
fun servicePersistentNotificationChannel(context: Context):String {
......@@ -175,4 +176,5 @@ object RequestCodes {
const val REQUEST_CODE_IGNORE_SERVICE_KILLED = 10
const val RESTART_AFTER_REVOKE = 11
const val RESTART_WHOLE_APP = 12
const val REQUEST_CODE_IGNORE_BAD_CONNECTION = 13
}
\ No newline at end of file
......@@ -274,6 +274,8 @@ class AppSettingsSharedPreferences(context: Context) : AppSettings, SimpleTypedP
var lastIptablesRedirectAddress:String? by stringPref("nonvpn_iptables_last_address")
var lastIptablesRedirectAddressIPv6:String? by stringPref("nonvpn_iptables_last_address_ipv6")
var iptablesModeDisableIpv6:Boolean by booleanPref("nonvpn_iptables_disable_ipv6", false)
var enableConnectionWatchDog:Boolean by booleanPref("connection_watchdog", true)
}
fun AppSettings.Companion.fromSharedPreferences(context: Context): AppSettingsSharedPreferences {
......
......@@ -76,4 +76,7 @@
<string name="notification_multipleuserswarning_title">Dummy VPN could not be established</string>
<string name="notification_multipleuserswarning_text">Nebulo could not start its dummy VPN because of an Android bug. This happens when there are multiple users on your device.</string>
<string name="notification_bad_connection_title">Bad server connection</string>
<string name="notification_bad_connection_text">Your connection to the DNS server is bad right now. This might impact your browsing. Consider selecting another server.</string>
</resources>
\ No newline at end of file
......@@ -20,9 +20,6 @@
<string name="title_fallback_dns">Internal DNS server</string>
<string name="summary_fallback_dns">Configure the DNS server Nebulo uses internally. This defaults to your service providers DNS server. This configured server is only used for internal purposes and does not handle your normal DNS queries.</string>
<string name="title_inform_bad_connection">Inform me of a bad connection</string>
<string name="summary_inform_bad_connection">Inform me with a notification and in the main menu when the DNS server currently is slow or not reachable</string>
<string name="preference_category_notification">Notification</string>
......@@ -42,11 +39,14 @@
<string name="summary_notification_allow_pause">Allow the app to be paused from the notification</string>
<string name="title_show_noconnection_notification">Notification on no connection</string>
<string name="summary_show_noconnection_notification">Show a notification when the app has no or a bad connection</string>
<string name="summary_show_noconnection_notification">Show a notification when the app currently has no connection to the DNS server</string>
<string name="title_show_revoked_notification">Notification on permission lost</string>
<string name="summary_show_revoked_notification">Show a notification when Nebulo loses permission to the VPN</string>
<string name="title_inform_bad_connection">Inform me of a bad connection</string>
<string name="summary_inform_bad_connection">Inform me when the DNS server currently is slow or otherwise performs badly</string>
<string name="preference_category_pin">PIN protection</string>
......
......@@ -43,5 +43,11 @@
android:key="show_vpn_revoked_notification"
android:summary="@string/summary_show_revoked_notification"
android:title="@string/title_show_revoked_notification" />
<CheckBoxPreference
android:defaultValue="true"
android:key="connection_watchdog"
android:summary="@string/summary_inform_bad_connection"
android:title="@string/title_inform_bad_connection"/>
</androidx.preference.PreferenceCategory>
</androidx.preference.PreferenceScreen>
\ No newline at end of file
Markdown is supported
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