Skip to content

Commit

Permalink
Block malware in internal builds (#4979)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/1203137811378537/1208212374550781/f

### Description
Block malware for internal builds.

### Steps to test this PR

_Test_
- [x] filter logcat with `Adding DNS`
- [x] install from this branch, launch the app and enable VPN
- [x] verify DNS added is 10.11.12.1
- [x] go to VPN settings and enable "block malware" toggle
- [x] go back to VPN main screen
- [x] verify DNS added is 10.11.12.2
- [x] verify network works as expected
- [x] Set a custom DNS (eg. 1.1.1.1) in VPN settings -> DNS server
- [x] verify DNS added is the custom DNS (eg. 1.1.1.1)
- [x] configure back to DDG DNS
- [x] verify DNS added is 10.11.12.2
- [x] verify network works as expected
  • Loading branch information
aitorvs authored Sep 4, 2024
1 parent fe0574d commit 9874850
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason.SEL
import com.duckduckgo.networkprotection.impl.config.NetPDefaultConfigProvider
import com.duckduckgo.networkprotection.impl.configuration.WgTunnel
import com.duckduckgo.networkprotection.impl.configuration.WgTunnelConfig
import com.duckduckgo.networkprotection.impl.configuration.computeBlockMalwareDnsOrSame
import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixels
import com.duckduckgo.networkprotection.impl.settings.NetPSettingsLocalConfig
import com.duckduckgo.networkprotection.impl.store.NetworkProtectionRepository
import com.squareup.anvil.annotations.ContributesMultibinding
import com.wireguard.config.Config
Expand All @@ -55,6 +57,7 @@ class WgVpnNetworkStack @Inject constructor(
private val netpPixels: Lazy<NetworkProtectionPixels>,
private val dnsProvider: DnsProvider,
private val crashLogger: CrashLogger,
private val netPSettingsLocalConfig: NetPSettingsLocalConfig,
) : VpnNetworkStack {
private var wgConfig: Config? = null

Expand All @@ -72,14 +75,20 @@ class WgVpnNetworkStack @Inject constructor(
logcat { "Wireguard configuration:\n$wgConfig" }

val privateDns = dnsProvider.getPrivateDns()
val dns = if (netPSettingsLocalConfig.blockMalware().isEnabled()) {
// if the user has configured "block malware" we calculate the malware DNS from the DDG default DNS(s)
wgConfig!!.`interface`.dnsServers.map { it.computeBlockMalwareDnsOrSame() }.toSet()
} else {
wgConfig!!.`interface`.dnsServers
}
Result.success(
VpnTunnelConfig(
mtu = wgConfig?.`interface`?.mtu ?: 1280,
addresses = wgConfig!!.`interface`.addresses.associate { Pair(it.address, it.mask) },
// when Android private DNS are set, we return DO NOT configure any DNS.
// why? no use intercepting encrypted DNS traffic, plus we can't configure any DNS that doesn't support DoT, otherwise Android
// will enforce DoT and will stop passing any DNS traffic, resulting in no DNS resolution == connectivity is killed
dns = if (privateDns.isEmpty()) wgConfig!!.`interface`.dnsServers else emptySet(),
dns = if (privateDns.isEmpty()) dns else emptySet(),
customDns = if (privateDns.isEmpty()) netPDefaultConfigProvider.fallbackDns() else emptySet(),
routes = wgConfig!!.`interface`.routes.associate { it.address.hostAddress!! to it.mask },
appExclusionList = wgConfig!!.`interface`.excludedApplications,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright (c) 2024 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.networkprotection.impl.configuration

import java.net.InetAddress

/**
* The block malware DNS IP address is a <<1 bit-wise operations on the last octet based on the default DNS
* This method assumes the [InetAddress] passed in as parameter is the default DNS.
*
* You should only
*/
internal fun InetAddress.computeBlockMalwareDnsOrSame(): InetAddress {
return kotlin.runCatching {
// Perform <<1 operation on the last octet
// Since byte is signed in Kotlin/Java, we mask it with 0xFF to treat it as unsigned
val newLastOctet = (address.last().toInt() and 0xFF) shl 1

val newIPAddress = address
// Update the last octet in the byte array
newIPAddress[newIPAddress.size - 1] = (newLastOctet and 0xFF).toByte() // Ensure it stays within byte range

InetAddress.getByAddress(newIPAddress)
}.getOrNull() ?: this
}
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,6 @@ class RealWgTunnel @Inject constructor(
.addAddress(InetNetwork.parse(serverData.address))
.apply {
addDnsServer(InetAddress.getByName(serverData.gateway))
addDnsServers(netPDefaultConfigProvider.fallbackDns())
}
.excludeApplications(netPDefaultConfigProvider.exclusionList())
.setMtu(netPDefaultConfigProvider.mtu())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ interface NetPSettingsLocalConfig {

@Toggle.DefaultValue(false)
fun excludeSystemAppsOthers(): Toggle

@Toggle.DefaultValue(false)
fun blockMalware(): Toggle
}

@ContributesBinding(AppScope::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.duckduckgo.networkprotection.impl

import com.duckduckgo.data.store.api.FakeSharedPreferencesProvider
import com.duckduckgo.feature.toggles.api.Toggle
import com.duckduckgo.mobile.android.vpn.network.FakeDnsProvider
import com.duckduckgo.mobile.android.vpn.network.VpnNetworkStack.VpnTunnelConfig
import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason.RESTART
Expand All @@ -25,7 +26,10 @@ import com.duckduckgo.networkprotection.impl.config.NetPDefaultConfigProvider
import com.duckduckgo.networkprotection.impl.configuration.ServerDetails
import com.duckduckgo.networkprotection.impl.configuration.WgTunnel
import com.duckduckgo.networkprotection.impl.configuration.WgTunnelConfig
import com.duckduckgo.networkprotection.impl.configuration.computeBlockMalwareDnsOrSame
import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixels
import com.duckduckgo.networkprotection.impl.settings.FakeNetPSettingsLocalConfigFactory
import com.duckduckgo.networkprotection.impl.settings.NetPSettingsLocalConfig
import com.duckduckgo.networkprotection.impl.store.NetworkProtectionRepository
import com.duckduckgo.networkprotection.impl.store.RealNetworkProtectionRepository
import com.duckduckgo.networkprotection.store.RealNetworkProtectionPrefs
Expand Down Expand Up @@ -97,10 +101,12 @@ class WgVpnNetworkStackTest {
private lateinit var wgConfig: Config

private lateinit var wgVpnNetworkStack: WgVpnNetworkStack
private lateinit var netPSettingsLocalConfig: NetPSettingsLocalConfig

@Before
fun setUp() {
MockitoAnnotations.openMocks(this)
netPSettingsLocalConfig = FakeNetPSettingsLocalConfigFactory.create()

privateDnsProvider = FakeDnsProvider()
networkProtectionRepository = RealNetworkProtectionRepository(
Expand All @@ -119,6 +125,7 @@ class WgVpnNetworkStackTest {
{ netpPixels },
privateDnsProvider,
mock(),
netPSettingsLocalConfig,
)
}

Expand All @@ -139,6 +146,21 @@ class WgVpnNetworkStackTest {
verify(netpPixels).reportEnableAttempt()
}

@Test
fun whenBlockMalwareIsConfigureDNSIsConputed() = runTest {
whenever(wgTunnel.createAndSetWgConfig()).thenReturn(wgConfig.success())
netPSettingsLocalConfig.blockMalware().setEnabled(Toggle.State(enable = true))

val actual = wgVpnNetworkStack.onPrepareVpn().getOrNull()
val expected = wgConfig.toTunnelConfig().copy(
dns = wgConfig.toTunnelConfig().dns.map { it.computeBlockMalwareDnsOrSame() }.toSet(),
)
assertNotNull(actual)
assertEquals(expected, actual)

verify(netpPixels).reportEnableAttempt()
}

@Test
fun whenOnPrepareVpnAndPrivateDnsConfiguredThenReturnEmptyDnsList() = runTest {
whenever(wgTunnel.createAndSetWgConfig()).thenReturn(wgConfig.success())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright (c) 2024 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.networkprotection.internal.feature

import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.CompoundButton.OnCheckedChangeListener
import android.widget.FrameLayout
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.anvil.annotations.PriorityKey
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.common.ui.viewbinding.viewBinding
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.di.scopes.ViewScope
import com.duckduckgo.feature.toggles.api.Toggle
import com.duckduckgo.mobile.android.vpn.VpnFeature
import com.duckduckgo.mobile.android.vpn.VpnFeaturesRegistry
import com.duckduckgo.networkprotection.impl.configuration.WgTunnelConfig
import com.duckduckgo.networkprotection.impl.settings.NetPSettingsLocalConfig
import com.duckduckgo.networkprotection.impl.settings.VpnSettingPlugin
import com.duckduckgo.networkprotection.internal.databinding.VpnViewSettingsBlockMalwareBinding
import com.squareup.anvil.annotations.ContributesMultibinding
import dagger.android.support.AndroidSupportInjection
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

@InjectWith(ViewScope::class)
class BlockMalwareVpnSettingView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
) : FrameLayout(context, attrs, defStyle) {

@Inject
lateinit var dispatcherProvider: DispatcherProvider

@Inject
lateinit var netPSettingsLocalConfig: NetPSettingsLocalConfig

@Inject
lateinit var vpnFeaturesRegistry: VpnFeaturesRegistry

@Inject
@AppCoroutineScope
lateinit var appCoroutineScope: CoroutineScope

@Inject
lateinit var wgTunnelConfig: WgTunnelConfig

private var mainCoroutineScope: CoroutineScope? = null

private val binding: VpnViewSettingsBlockMalwareBinding by viewBinding()

private var didToggleSetting = false

private val toggleListener = OnCheckedChangeListener { _, value ->
mainCoroutineScope?.launch(dispatcherProvider.io()) {
didToggleSetting = !didToggleSetting
netPSettingsLocalConfig.blockMalware().setEnabled(Toggle.State(enable = value))
}
}

@OptIn(ExperimentalCoroutinesApi::class)
override fun onAttachedToWindow() {
AndroidSupportInjection.inject(this)
super.onAttachedToWindow()

@SuppressLint("NoHardcodedCoroutineDispatcher")
mainCoroutineScope = CoroutineScope(SupervisorJob() + dispatcherProvider.main())

mainCoroutineScope?.launch(dispatcherProvider.io()) {
val isEnabled = netPSettingsLocalConfig.blockMalware().isEnabled()
withContext(dispatcherProvider.main()) {
binding.blockMalware.quietlySetIsChecked(isEnabled, toggleListener)
}
}
}

override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
if (didToggleSetting) {
// appCoroutineScope to make sure it's not cancelled
appCoroutineScope.launch(dispatcherProvider.io()) {
// wgTunnelConfig.clearWgConfig() // force config re-fetch
// VpnFeature hardcoded here as eventually we'll move this inside the netp-impl module
vpnFeaturesRegistry.refreshFeature(VpnFeature { "NETP_VPN" })
}
}
mainCoroutineScope?.cancel()
mainCoroutineScope = null
}
}

@ContributesMultibinding(ActivityScope::class)
@PriorityKey(BLOCK_MALWARE_PRIORITY)
class BlockMalwareSettingViewPlugin @Inject constructor() : VpnSettingPlugin {
override fun getView(context: Context): View? {
return BlockMalwareVpnSettingView(context)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ private const val INTERNAL_SETTING_BASE = 10000
internal const val INTERNAL_SETTING_SEPARATOR = INTERNAL_SETTING_BASE + 10
internal const val INTERNAL_SETTING_HEADING = INTERNAL_SETTING_BASE + 20
internal const val UNSAFE_WIFI_DETECTION_PRIORITY = INTERNAL_SETTING_BASE + 30
internal const val BLOCK_MALWARE_PRIORITY = INTERNAL_SETTING_BASE + 40
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2024 DuckDuckGo
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->

<com.duckduckgo.common.ui.view.listitem.TwoLineListItem
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/block_malware"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:primaryText="@string/netpBlockMalwarePrimary"
app:secondaryText="@string/netpBlockMalwareByline"
app:showSwitch="true"
/>
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,6 @@
<string name="netpForceRekey">Force Rekey</string>
<string name="netpUnsafeWifiDetectionPrimary">Unsafe Wi-Fi detection</string>
<string name="netpUnsafeWifiDetectionByline">Get notified when connected to unsafe Wi-Fi without a VPN.</string>
<string name="netpBlockMalwarePrimary">Block Malware</string>
<string name="netpBlockMalwareByline">The Default DuckDuckGo DNS will block malware when enabled.</string>
</resources>

0 comments on commit 9874850

Please sign in to comment.