Skip to content

Commit d3f121e

Browse files
committed
Merge VPN provider preview
2 parents fe6995a + 1ed4982 commit d3f121e

91 files changed

Lines changed: 6387 additions & 1039 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.env
2+
13
.claude/
24
fdroidserver/
35

README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
TrackerControl is an Android app that allows users to monitor and control the widespread,
1010
ongoing, hidden data collection in mobile apps about user behaviour ('tracking').
1111

12+
TrackerControl can also route filtered traffic through a remote VPN endpoint using
13+
its experimental WireGuard support, with built-in setup for Mullvad and IVPN and
14+
support for custom WireGuard profiles.
15+
1216
To detect tracking, TrackerControl combines the power of the *Disconnect blocklist*,
1317
used by Firefox, the *DuckDuckGo Tracker Radar* for mobile apps, and of our in-house blocklist, created *from analysing ~2 000 000 apps*! **To protect your privacy from your ISP, you can also optionally encrypt your DNS traffic using DNS-over-HTTPS (DoH).**
1418
Additionally, TrackerControl supports custom blocklists and uses the signatures from [ClassyShark3xodus](https://f-droid.org/en/packages/com.oF2pks.classyshark3xodus/)/[Exodus Privacy](https://exodus-privacy.eu.org/) for the analysis of tracker libraries within app code.
@@ -24,13 +28,14 @@ Under the hood, TrackerControl uses Android's VPN functionality,
2428
to analyse apps' network communications *locally on the Android device*.
2529
This is accomplished through a local VPN server, to enable network traffic analysis by TrackerControl.
2630

27-
No root is required. Other VPNs or Android's "Private DNS" feature are not supported (due to Android limitations), but TrackerControl provides its own **Secure DNS (DNS-over-HTTPS / DoH)** feature to protect your DNS traffic. For users who want to combine tracker analysis with a remote VPN, TrackerControl also offers **experimental WireGuard support**, allowing filtered traffic to be tunnelled through a WireGuard endpoint of your choice.
31+
No root is required. Other VPN apps or Android's "Private DNS" feature are not supported alongside TrackerControl due to Android limitations, but TrackerControl provides its own **Secure DNS (DNS-over-HTTPS / DoH)** feature and optional **WireGuard tunnelling** for users who want remote VPN routing.
2832
By default, no external VPN server is used, to keep your data safe! TrackerControl even protects you
2933
against *DNS cloaking*, a popular technique to hide trackers in websites and apps.
3034

3135
TrackerControl will always be free and open source, being a research project.
3236

3337
## Contents
38+
- [VPN Support](#vpn-support)
3439
- [Download / Installation](#download--installation)
3540
- [Example Use](#example-use)
3641
- [Contributing](#contributing)
@@ -44,6 +49,22 @@ TrackerControl will always be free and open source, being a research project.
4449
- [License](#license)
4550
- [Citation](#citation)
4651

52+
## VPN Support
53+
54+
TrackerControl's built-in VPN remains local by default: it analyses and filters traffic on your device without sending traffic to an external VPN provider. The experimental WireGuard support adds an optional second step for users who also want remote VPN tunnelling after TrackerControl has applied its local tracker analysis and blocking.
55+
56+
The VPN tab supports three modes:
57+
58+
| Mode | What it does |
59+
| :--- | :--- |
60+
| **Mullvad** | Creates WireGuard profiles from a Mullvad account number, lets you choose a relay country, and stores only the account number and generated WireGuard profile data locally. |
61+
| **IVPN** | Creates WireGuard profiles from an IVPN account ID, including CAPTCHA handling when IVPN requires it, and lets you choose a relay country. |
62+
| **WireGuard** | Imports and manages custom WireGuard configurations from another VPN provider, your own server, or a workplace endpoint. |
63+
64+
When WireGuard tunnelling is enabled, TrackerControl still uses Android's VPN service for local filtering, then routes allowed traffic through the selected WireGuard endpoint. Secure DNS (DoH) is automatically paused when the active WireGuard profile provides DNS, because DNS queries are then handled through the WireGuard tunnel instead. Provider-generated WireGuard keys can be rotated from advanced settings.
65+
66+
This feature is experimental. Android only allows one active VPN service at a time, so TrackerControl cannot run alongside a separate VPN app.
67+
4768
## Download / Installation
4869
*Disclaimer: The usage of this app is at your own risk. No app can offer 100% protection against tracking. Analysis results shown within the app might be inaccurate.*
4970

@@ -100,7 +121,7 @@ TrackerControl is mainly designed to help you investigate the tracking practices
100121

101122
Mobile trackers rely on the sending of personal data over the internet. This is why tracking can be detected and analysed from apps' network traffic. This is the core functionality of TrackerControl. The advantage of this approach over tracker library analysis is that actual evidence of data sharing is gathered; by contrast, when analysing solely the presence of tracking libraries in apps, some of these libraries may never be activated by an app at run-time.
102123

103-
TrackerControl analyses network traffic locally on the device using DNS-based detection. TLS Server Name Indication (SNI) extraction is disabled by default because it requires connecting to tracker servers, leaking the user's IP address. SNI can be re-enabled from the advanced settings for research purposes.
124+
TrackerControl analyses network traffic locally on the device using DNS-based detection. TLS Server Name Indication (SNI) extraction is disabled by default because it requires connecting to tracker servers, leaking the user's IP address. SNI is enabled only when Research mode is turned on for measurement purposes.
104125

105126
You analyse apps network traffic by following the steps within the app to enable the VPN. Consequently, TrackerControl keeps track of any contacted tracking domain. Note that you need to interact with apps of interest in order to make these apps share data with tracking companies over the internet.
106127

TODO.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# TODO
22

3+
## Secure DNS battery and simple protection health
4+
5+
Secure DNS is currently Java-based and can make the phone warm while the screen is off. Do **not** make DoH a stronger default until its idle behavior is profiled and fixed.
6+
7+
Investigate:
8+
- whether the local DNS proxy / DoH client stays active when there is no DNS traffic
9+
- whether retries, circuit-breaker checks, network-change handling, or idle HTTPS connections cause wakeups while the screen is off
10+
- whether DNS caching is effective enough to avoid repeated upstream DoH queries
11+
- whether DoH duplicates work or conflicts with WireGuard-provided DNS
12+
- whether system-app routing through TC/DoH is contributing to wakeups
13+
14+
Desired product direction after the battery issue is fixed:
15+
- add a simple protection health screen showing tracker blocking, Secure DNS, WireGuard, Android Private DNS conflict, and battery/background permission status
16+
- keep recommended defaults simple: low-battery tracker blocking by default; Secure DNS as a clearly explained stronger privacy option until its screen-off cost is low
17+
- avoid exposing Rethink-style expert configuration unless it directly helps users recover from breakage
18+
319
## ParcelFileDescriptor Race Fix
420

521
The VPN file descriptor can be closed by `stopVPN()` while native code in `jni_run()` is still using it, causing EBADF errors and VPN tunnel failures — typically triggered by network transitions (WiFi/mobile).

app/build.gradle

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
apply plugin: 'com.android.application'
2-
apply plugin: 'org.jetbrains.kotlin.android'
32

43
// --- WireGuard bridge (built from source via gomobile) ---
54
// Produces app/build/wgbridge/wgbridge.aar from wgbridge/*.go on demand.
@@ -61,12 +60,9 @@ tasks.register('wgbridgeBind') {
6160

6261
// Ask Go for GOPATH so `gomobile` resolves; also add Go's own dir to
6362
// PATH for downstream tools the binding might invoke (cgo, etc).
64-
def gopathStream = new ByteArrayOutputStream()
65-
exec {
63+
def gopath = providers.exec {
6664
commandLine goBin, 'env', 'GOPATH'
67-
standardOutput = gopathStream
68-
}
69-
def gopath = gopathStream.toString().trim()
65+
}.standardOutput.asText.get().trim()
7066
def goDir = file(goBin).parent
7167

7268
def env = new HashMap<String, String>(System.getenv())
@@ -80,18 +76,18 @@ tasks.register('wgbridgeBind') {
8076
// does the actual Java <-> Go interface generation. `gomobile
8177
// init` used to install gobind for you but on recent versions
8278
// we install it explicitly so the failure mode is clearer.
83-
exec {
79+
providers.exec {
8480
environment env
8581
commandLine goBin, 'install',
8682
"golang.org/x/mobile/cmd/gomobile@${gomobileVersion}",
8783
"golang.org/x/mobile/cmd/gobind@${gomobileVersion}"
88-
}
84+
}.result.get().assertNormalExitValue()
8985
if (!file(gomobileBin).canExecute() || !file(gobindBin).canExecute()) {
9086
throw new GradleException("Installed gomobile/gobind but ${gopath}/bin still missing one of them.")
9187
}
9288
}
9389

94-
exec {
90+
providers.exec {
9591
workingDir wgbridgeSrcDir
9692
environment env
9793
commandLine gomobileBin, 'bind',
@@ -101,7 +97,7 @@ tasks.register('wgbridgeBind') {
10197
'-ldflags', '-extldflags "-Wl,-z,max-page-size=16384 -Wl,-z,common-page-size=16384"',
10298
'-o', wgbridgeAar.absolutePath,
10399
'.'
104-
}
100+
}.result.get().assertNormalExitValue()
105101
}
106102
}
107103

@@ -112,7 +108,7 @@ afterEvaluate {
112108
}
113109

114110
android {
115-
compileSdk 36
111+
compileSdk = 36
116112

117113
defaultConfig {
118114
applicationId "net.kollnig.missioncontrol"
@@ -137,7 +133,7 @@ android {
137133
}
138134
}
139135

140-
ndkVersion "27.2.12479018"
136+
ndkVersion = "27.2.12479018"
141137

142138
ndk {
143139
// https://developer.android.com/ndk/guides/abis.html#sa
@@ -199,12 +195,6 @@ android {
199195
}
200196
}
201197

202-
applicationVariants.configureEach { variant ->
203-
variant.outputs.configureEach { output ->
204-
outputFileName = "TrackerControl-${variant.name}-latest.apk"
205-
}
206-
}
207-
208198
signingConfigs {
209199
release {
210200
enableV1Signing = true
@@ -237,21 +227,27 @@ android {
237227
androidResources {
238228
generateLocaleConfig = true
239229
}
240-
kotlinOptions {
241-
jvmTarget = '17'
230+
}
231+
232+
androidComponents {
233+
onVariants(selector().all()) { variant ->
234+
variant.outputs.forEach { output ->
235+
output.outputFileName.set("TrackerControl-${variant.name}-latest.apk")
236+
}
242237
}
243238
}
244239

245240
dependencies {
246241
implementation 'androidx.core:core-ktx:1.18.0'
247242
testImplementation 'junit:junit:4.13.2'
243+
testImplementation 'org.robolectric:robolectric:4.16.1'
248244

249245
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
250246
implementation fileTree(dir: 'libs', include: ['*.jar'])
251247
implementation files(wgbridgeAar)
252248

253249
// https://developer.android.com/jetpack/androidx/releases/
254-
implementation 'androidx.activity:activity:1.9.3'
250+
implementation 'androidx.activity:activity:1.13.0'
255251
implementation 'androidx.appcompat:appcompat:1.7.1'
256252
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0'
257253
implementation 'androidx.recyclerview:recyclerview:1.4.0'
@@ -260,16 +256,16 @@ dependencies {
260256
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
261257
implementation 'com.google.android.material:material:1.13.0'
262258
implementation 'androidx.work:work-runtime:2.11.2'
263-
implementation 'com.google.guava:guava:33.5.0-android'
264-
annotationProcessor 'androidx.annotation:annotation:1.9.1'
259+
implementation 'com.google.guava:guava:33.6.0-android'
260+
annotationProcessor 'androidx.annotation:annotation:1.10.0'
265261

266262
// fix errors with libraries
267263
def lifecycle_version = '2.10.0'
268264
//implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
269265
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
270266

271267
// https://bumptech.github.io/glide/
272-
def glide_version = "5.0.5"
268+
def glide_version = "5.0.7"
273269
implementation("com.github.bumptech.glide:glide:$glide_version") {
274270
exclude group: "com.android.support"
275271
}

app/src/main/AndroidManifest.xml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
1616

1717
<!-- https://developer.android.com/preview/privacy/package-visibility -->
18-
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
18+
<uses-permission
19+
android:name="android.permission.QUERY_ALL_PACKAGES"
20+
tools:ignore="QueryAllPackagesPermission" />
1921
<!--queries>
2022
<intent>
2123
<action android:name="android.intent.action.MAIN" />
@@ -131,6 +133,7 @@
131133
android:configChanges="orientation|screenSize"
132134
android:exported="true"
133135
android:label="@string/app_name"
136+
android:permission="${applicationId}.permission.ADMIN"
134137
android:theme="@style/AppDialog">
135138
<intent-filter>
136139
<action android:name="eu.faircode.netguard.START_PORT_FORWARD" />
@@ -169,6 +172,16 @@
169172
android:value="eu.faircode.netguard.ActivitySettings" />
170173
</activity>
171174

175+
<activity
176+
android:name="net.kollnig.missioncontrol.ActivityWireGuardProfiles"
177+
android:configChanges="orientation|screenSize"
178+
android:label="@string/setting_wg_profile_manage"
179+
android:parentActivityName="eu.faircode.netguard.ActivitySettings">
180+
<meta-data
181+
android:name="android.support.PARENT_ACTIVITY"
182+
android:value="eu.faircode.netguard.ActivitySettings" />
183+
</activity>
184+
172185

173186
<activity
174187
android:name="net.kollnig.missioncontrol.DetailsActivity"

app/src/main/java/eu/faircode/netguard/ActivityForwarding.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,10 @@ protected void onCreate(Bundle savedInstanceState) {
8686
@Override
8787
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
8888
Cursor cursor = (Cursor) adapter.getItem(position);
89-
final int protocol = cursor.getInt(cursor.getColumnIndex("protocol"));
90-
final int dport = cursor.getInt(cursor.getColumnIndex("dport"));
91-
final String raddr = cursor.getString(cursor.getColumnIndex("raddr"));
92-
final int rport = cursor.getInt(cursor.getColumnIndex("rport"));
89+
final int protocol = cursor.getInt(cursor.getColumnIndexOrThrow("protocol"));
90+
final int dport = cursor.getInt(cursor.getColumnIndexOrThrow("dport"));
91+
final String raddr = cursor.getString(cursor.getColumnIndexOrThrow("raddr"));
92+
final int rport = cursor.getInt(cursor.getColumnIndexOrThrow("rport"));
9393

9494
PopupMenu popup = new PopupMenu(ActivityForwarding.this, view);
9595
popup.inflate(R.menu.forward);
@@ -150,8 +150,7 @@ public boolean onCreateOptionsMenu(Menu menu) {
150150

151151
@Override
152152
public boolean onOptionsItemSelected(MenuItem item) {
153-
switch (item.getItemId()) {
154-
case R.id.menu_add:
153+
if (item.getItemId() == R.id.menu_add) {
155154
LayoutInflater inflater = LayoutInflater.from(this);
156155
View view = inflater.inflate(R.layout.forwardadd, null, false);
157156
final Spinner spProtocol = view.findViewById(R.id.spProtocol);
@@ -249,8 +248,7 @@ public void onDismiss(DialogInterface dialogInterface) {
249248
.create();
250249
dialog.show();
251250
return true;
252-
default:
253-
return super.onOptionsItemSelected(item);
254251
}
252+
return super.onOptionsItemSelected(item);
255253
}
256254
}

app/src/main/java/eu/faircode/netguard/ActivityLog.java

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -151,16 +151,20 @@ protected void onCreate(Bundle savedInstanceState) {
151151
lvLog.setOnItemClickListener((parent, view, position, id) -> {
152152
PackageManager pm = getPackageManager();
153153
Cursor cursor = (Cursor) adapter.getItem(position);
154-
long time = cursor.getLong(cursor.getColumnIndex("time"));
155-
int version = cursor.getInt(cursor.getColumnIndex("version"));
156-
int protocol = cursor.getInt(cursor.getColumnIndex("protocol"));
157-
final String saddr = cursor.getString(cursor.getColumnIndex("saddr"));
158-
final int sport = (cursor.isNull(cursor.getColumnIndex("sport")) ? -1 : cursor.getInt(cursor.getColumnIndex("sport")));
159-
final String daddr = cursor.getString(cursor.getColumnIndex("daddr"));
160-
final int dport = (cursor.isNull(cursor.getColumnIndex("dport")) ? -1 : cursor.getInt(cursor.getColumnIndex("dport")));
161-
final String dname = cursor.getString(cursor.getColumnIndex("dname"));
162-
final int uid = (cursor.isNull(cursor.getColumnIndex("uid")) ? -1 : cursor.getInt(cursor.getColumnIndex("uid")));
163-
int allowed1 = (cursor.isNull(cursor.getColumnIndex("allowed")) ? -1 : cursor.getInt(cursor.getColumnIndex("allowed")));
154+
long time = cursor.getLong(cursor.getColumnIndexOrThrow("time"));
155+
int version = cursor.getInt(cursor.getColumnIndexOrThrow("version"));
156+
int protocol = cursor.getInt(cursor.getColumnIndexOrThrow("protocol"));
157+
final String saddr = cursor.getString(cursor.getColumnIndexOrThrow("saddr"));
158+
int colSport = cursor.getColumnIndexOrThrow("sport");
159+
final int sport = (cursor.isNull(colSport) ? -1 : cursor.getInt(colSport));
160+
final String daddr = cursor.getString(cursor.getColumnIndexOrThrow("daddr"));
161+
int colDPort = cursor.getColumnIndexOrThrow("dport");
162+
final int dport = (cursor.isNull(colDPort) ? -1 : cursor.getInt(colDPort));
163+
final String dname = cursor.getString(cursor.getColumnIndexOrThrow("dname"));
164+
int colUid = cursor.getColumnIndexOrThrow("uid");
165+
final int uid = (cursor.isNull(colUid) ? -1 : cursor.getInt(colUid));
166+
int colAllowed = cursor.getColumnIndexOrThrow("allowed");
167+
int allowed1 = (cursor.isNull(colAllowed) ? -1 : cursor.getInt(colAllowed));
164168

165169
// Get external address
166170
InetAddress addr = null;
@@ -207,12 +211,7 @@ protected void onCreate(Bundle savedInstanceState) {
207211
else
208212
popup.getMenu().findItem(R.id.menu_port).setTitle(getString(R.string.title_log_port, port));
209213

210-
if (prefs.getBoolean("filter", true)) {
211-
if (uid <= 0) {
212-
popup.getMenu().removeItem(R.id.menu_allow);
213-
popup.getMenu().removeItem(R.id.menu_block);
214-
}
215-
} else {
214+
if (uid <= 0) {
216215
popup.getMenu().removeItem(R.id.menu_allow);
217216
popup.getMenu().removeItem(R.id.menu_block);
218217
}
@@ -346,7 +345,6 @@ public boolean onPrepareOptionsMenu(Menu menu) {
346345
menu.findItem(R.id.menu_protocol_udp).setChecked(prefs.getBoolean("proto_udp", true));
347346
menu.findItem(R.id.menu_protocol_tcp).setChecked(prefs.getBoolean("proto_tcp", true));
348347
menu.findItem(R.id.menu_protocol_other).setChecked(prefs.getBoolean("proto_other", true));
349-
menu.findItem(R.id.menu_traffic_allowed).setEnabled(prefs.getBoolean("filter", true));
350348
menu.findItem(R.id.menu_traffic_allowed).setChecked(prefs.getBoolean("traffic_allowed", true));
351349
menu.findItem(R.id.menu_traffic_blocked).setChecked(prefs.getBoolean("traffic_blocked", true));
352350

0 commit comments

Comments
 (0)