Skip to content

Commit

Permalink
Implement NP (#3108)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/488551667048375/1204478072055111/f

### Description
See task description

### Steps to test this PR
Follow the tests in the [asana task](https://app.asana.com/0/488551667048375/1204478072055111/f)

Co-authored-by: Aitor Viana <[email protected]>
Co-authored-by: Karl Len Mae Dimla <[email protected]>
Co-authored-by: Anastasia Shuba <[email protected]>
Co-authored-by: Anastasia Shuba <[email protected]>
  • Loading branch information
5 people authored May 2, 2023
1 parent baeb7a1 commit 78b0c4e
Show file tree
Hide file tree
Showing 167 changed files with 14,125 additions and 22 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/build-debug-apk.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ jobs:
java-version: '11'
distribution: 'adopt'

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: '1.18.3'

- name: Build
uses: gradle/gradle-build-action@v2
with:
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ jobs:
java-version: '11'
distribution: 'adopt'

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: '1.18.3'

- name: Lint
uses: gradle/gradle-build-action@v2
with:
Expand Down Expand Up @@ -119,6 +124,11 @@ jobs:
java-version: '11'
distribution: 'adopt'

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: '1.18.3'

- name: Decode secret
env:
FLANK: ${{ secrets.FLANK }}
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ jobs:
java-version: '11'
distribution: 'adopt'

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: '1.18.3'

- name: Lint
uses: gradle/gradle-build-action@v2
with:
Expand Down Expand Up @@ -109,6 +114,11 @@ jobs:
java-version: '11'
distribution: 'adopt'

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: '1.18.3'

- name: Decode secret
env:
FLANK: ${{ secrets.FLANK }}
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/privacy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ jobs:
java-version: '11'
distribution: 'adopt'

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: '1.18.3'

- name: Decode secret
env:
FLANK: ${{ secrets.FLANK }}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2022 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.mobile.android.vpn.service

interface VpnSocketProtector {
/**
* Call this method to protect the socket from VPN.
*
* @param socket The file descriptor of the socket to protect.
* @#return true if the socket is protected, false otherwise.
*/
fun protect(socket: Int): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* Copyright (c) 2022 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.mobile.android.app.tracking

import android.content.pm.PackageManager
import android.util.LruCache
import com.duckduckgo.di.scopes.VpnScope
import com.duckduckgo.mobile.android.vpn.AppTpVpnFeature
import com.duckduckgo.mobile.android.vpn.VpnFeaturesRegistry
import com.duckduckgo.mobile.android.vpn.apps.VpnExclusionList
import com.duckduckgo.mobile.android.vpn.apps.isSystemApp
import com.duckduckgo.mobile.android.vpn.dao.VpnAppTrackerBlockingDao
import com.duckduckgo.mobile.android.vpn.model.TrackingApp
import com.duckduckgo.mobile.android.vpn.model.VpnTracker
import com.duckduckgo.mobile.android.vpn.processor.requestingapp.AppNameResolver
import com.duckduckgo.mobile.android.vpn.processor.tcp.tracker.AppTrackerRecorder
import com.duckduckgo.mobile.android.vpn.store.VpnDatabase
import com.duckduckgo.mobile.android.vpn.trackers.AppTrackerRepository
import com.duckduckgo.mobile.android.vpn.trackers.AppTrackerType
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import logcat.logcat

class NetpRealAppTrackerDetector constructor(
private val appTrackerRepository: AppTrackerRepository,
private val appNameResolver: AppNameResolver,
private val appTrackerRecorder: AppTrackerRecorder,
private val vpnAppTrackerBlockingDao: VpnAppTrackerBlockingDao,
private val packageManager: PackageManager,
private val vpnFeaturesRegistry: VpnFeaturesRegistry,
) : AppTrackerDetector {

private val isAppTpDisabled by lazy {
!vpnFeaturesRegistry.isFeatureRegistered(AppTpVpnFeature.APPTP_VPN)
}

// cache packageId -> app name
private val appNamesCache = LruCache<String, AppNameResolver.OriginatingApp>(100)

override fun evaluate(domain: String, uid: Int): AppTrackerDetector.AppTracker? {
// Check if AppTP is enabled first
if (isAppTpDisabled) {
return null
}

// `null` package ID means unknown app, return null to not block
val packageId = appNameResolver.getPackageIdForUid(uid) ?: return null

if (VpnExclusionList.isDdgApp(packageId) || packageId.isInExclusionList()) {
logcat { "shouldAllowDomain: $packageId is excluded, allowing $domain" }
return null
}

when (val type = appTrackerRepository.findTracker(domain, packageId)) {
AppTrackerType.NotTracker -> return null
is AppTrackerType.FirstParty -> return null
is AppTrackerType.ThirdParty -> {
if (isTrackerInExceptionRules(packageId = packageId, hostname = domain)) return null

val trackingApp = appNamesCache[packageId] ?: appNameResolver.getAppNameForPackageId(packageId)
appNamesCache.put(packageId, trackingApp)

// if the app name is unknown, do not block
if (trackingApp.isUnknown()) return null

VpnTracker(
trackerCompanyId = type.tracker.trackerCompanyId,
company = type.tracker.owner.name,
companyDisplayName = type.tracker.owner.displayName,
domain = type.tracker.hostname,
trackingApp = TrackingApp(trackingApp.packageId, trackingApp.appName),
).run {
appTrackerRecorder.insertTracker(this)
}
return AppTrackerDetector.AppTracker(
domain = type.tracker.hostname,
uid = uid,
trackerCompanyDisplayName = type.tracker.owner.displayName,
trackingAppId = trackingApp.packageId,
trackingAppName = trackingApp.appName,
)
}
}
}

private fun isTrackerInExceptionRules(
packageId: String,
hostname: String,
): Boolean {
return vpnAppTrackerBlockingDao.getRuleByTrackerDomain(hostname)?.let { rule ->
logcat { "isTrackerInExceptionRules: found rule $rule for $hostname" }
return rule.packageNames.contains(packageId)
} ?: false
}

private fun String.isInExclusionList(): Boolean {
if (packageManager.getApplicationInfo(this, 0).isSystemApp()) {
// if system app is NOT overridden, it means it's in the exclusion list
if (!appTrackerRepository.getSystemAppOverrideList().map { it.packageId }.contains(this)) {
return true
}
}

return appTrackerRepository.getManualAppExclusionList().firstOrNull { it.packageId == this }?.let {
// if app is defined as "unprotected" by the user, then it is in exclusion list
return !it.isProtected
// else, app is in the exclusion list
} ?: appTrackerRepository.getAppExclusionList().map { it.packageId }.contains(this)
}
}

// TODO - this is a temporary solution to unblock NetP. We need to spend more time thinking about how to properly share and unify the AppTrackerDetector impl
// Also this file should not be in this module, it's temporarily here until we settle on how exclusion lists should work for NetP
// decide what needs to be extracted to API modules and how AppTrackerDetector should work / be shared
@ContributesTo(
scope = VpnScope::class,
replaces = [AppTrackerDetectorModule::class],
)
@Module
object NetpAppTrackerDetectorModule {
@Provides
fun provideNetpAppTrackerDetectorModule(
appTrackerRepository: AppTrackerRepository,
appNameResolver: AppNameResolver,
appTrackerRecorder: AppTrackerRecorder,
vpnDatabase: VpnDatabase,
packageManager: PackageManager,
vpnFeaturesRegistry: VpnFeaturesRegistry,
): AppTrackerDetector {
return NetpRealAppTrackerDetector(
appTrackerRepository = appTrackerRepository,
appNameResolver = appNameResolver,
appTrackerRecorder = appTrackerRecorder,
vpnAppTrackerBlockingDao = vpnDatabase.vpnAppTrackerBlockingDao(),
packageManager = packageManager,
vpnFeaturesRegistry = vpnFeaturesRegistry,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.duckduckgo.mobile.android.vpn.health

import android.content.Context
import android.net.ConnectivityManager
import android.os.PowerManager
import com.duckduckgo.app.global.extensions.isAirplaneModeOn
import com.duckduckgo.app.global.plugins.PluginPoint
import com.duckduckgo.app.utils.ConflatedJob
Expand Down Expand Up @@ -51,13 +52,14 @@ class NetworkConnectivityHealthHandler @Inject constructor(
private val vpnConnectivityLossListenerPluginPoint: PluginPoint<VpnConnectivityLossListenerPlugin>,
) : VpnServiceCallbacks {
private val connectivityManager = context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private val powerManager = context.applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
private val job = ConflatedJob()

override fun onVpnStarted(coroutineScope: CoroutineScope) {
job += coroutineScope.launch {
while (isActive) {
delay(15_000)
if (!hasVpnConnectivity() && !context.isAirplaneModeOn()) {
if (powerManager.isInteractive && !context.isAirplaneModeOn() && !hasVpnConnectivity()) {
if (hasDeviceConnectivity()) {
vpnConnectivityLossListenerPluginPoint.getPlugins().forEach {
logcat { "Calling onVpnConnectivityLoss on $it" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.di.scopes.ReceiverScope
import com.duckduckgo.mobile.android.vpn.dao.HeartBeatEntity
import com.duckduckgo.mobile.android.vpn.dao.VpnHeartBeatDao
import com.duckduckgo.mobile.android.vpn.dao.VpnServiceStateStatsDao
import com.duckduckgo.mobile.android.vpn.model.VpnServiceState
import com.duckduckgo.mobile.android.vpn.pixels.DeviceShieldPixels
import com.duckduckgo.mobile.android.vpn.service.TrackerBlockingVpnService
import com.duckduckgo.mobile.android.vpn.store.VpnDatabase
Expand Down Expand Up @@ -98,13 +100,22 @@ class VpnServiceHeartbeatMonitorWorker(
@Inject
lateinit var dispatcherProvider: DispatcherProvider

@Inject
lateinit var vpnServiceStateStatsDao: VpnServiceStateStatsDao

override suspend fun doWork(): Result = withContext(dispatcherProvider.io()) {
val lastHeartBeat = vpnHeartBeatDao.hearBeats().maxByOrNull { it.timestamp }

logcat { "HB monitor checking last HB: $lastHeartBeat" }
if (lastHeartBeat?.isAlive() == true && !TrackerBlockingVpnService.isServiceRunning(context)) {
logcat(LogPriority.WARN) { "HB monitor: VPN stopped, restarting it" }

// TODO this is for now just in NetP, move it to public repo once tested
// Just make sure the internal state is consistent with the actual state before we re-start the VPN service
vpnServiceStateStatsDao.getLastStateStats()?.let {
vpnServiceStateStatsDao.insert(it.copy(state = VpnServiceState.DISABLED, timestamp = System.currentTimeMillis()))
}

deviceShieldPixels.suddenKillBySystem()
deviceShieldPixels.automaticRestart()
TrackerBlockingVpnService.startService(context)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 DuckDuckGo
* Copyright (c) 2023 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,21 +17,22 @@
package com.duckduckgo.mobile.android.vpn.integration

import com.duckduckgo.di.scopes.VpnScope
import com.duckduckgo.mobile.android.vpn.AppTpVpnFeature
import com.duckduckgo.mobile.android.vpn.VpnFeaturesRegistry
import com.duckduckgo.mobile.android.vpn.state.VpnStateCollectorPlugin
import com.squareup.anvil.annotations.ContributesMultibinding
import javax.inject.Inject
import org.json.JSONObject

@ContributesMultibinding(VpnScope::class)
class VpnNetworkLayerStateCollector @Inject constructor(
private val vpnNetworkStackProvider: VpnNetworkStackProvider,
class AppTPStateCollector @Inject constructor(
private val vpnFeaturesRegistry: VpnFeaturesRegistry,
) : VpnStateCollectorPlugin {

override suspend fun collectVpnRelatedState(appPackageId: String?): JSONObject {
return JSONObject().apply {
put("name", vpnNetworkStackProvider.provideNetworkStack().name)
put("enabled", vpnFeaturesRegistry.isFeatureRegistered(AppTpVpnFeature.APPTP_VPN))
}
}

override val collectorName: String = "networkLayer"
override val collectorName: String = "appTpState"
}
Loading

0 comments on commit 78b0c4e

Please sign in to comment.