TrackerControl

trackercontrol.org
TrackerControl

Monitor and control hidden data collection in mobile apps about user behavior/ tracking. Get from F-Droid

Open Source

TrackerControl Source Code

Author

TrackerControl

Description

TrackerControl Android: monitor and control trackers and ads.

#android#privacy#tracking-protection

Homepage

https://trackercontrol.org/

Repository

  • LicenseGPL-3.0
  • Created10 Nov 19
  • Primary languageJava
  • Size39,070 KB
  • Stars2,602
  • Forks129
  • Watchers2,602

Language Usage

Language Usage

Project Health

Recent Commits

  • dependabot[bot](10 Aug 26)

    deps(rust): bump base64 in /wgbridge-rs in the cargo-minor-patch group (#716) Bumps the cargo-minor-patch group in /wgbridge-rs with 1 update: [base64](https://github.com/marshallpierce/rust-base64). Updates `base64` from 0.23.0 to 0.23.1 - [Changelog](https://github.com/marshallpierce/rust-base64/blob/master/RELEASE-NOTES.md) - [Commits](https://github.com/marshallpierce/rust-base64/compare/v0.23.0...v0.23.1) --- updated-dependencies: - dependency-name: base64 dependency-version: 0.23.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: cargo-minor-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

  • dependabot[bot](10 Aug 26)

    deps: bump gradle-wrapper in the gradle-minor-patch group (#717) Bumps the gradle-minor-patch group with 1 update: [gradle-wrapper](https://github.com/gradle/gradle). Updates `gradle-wrapper` from 9.6.1 to 9.7.0 - [Release notes](https://github.com/gradle/gradle/releases) - [Commits](https://github.com/gradle/gradle/compare/v9.6.1...v9.7.0) --- updated-dependencies: - dependency-name: gradle-wrapper dependency-version: 9.7.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gradle-minor-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

  • Konrad Kollnig(05 Aug 26)

    Say what is broken in the Private DNS row, not just what to change The row inherited msg_private_dns, written for the onboarding slide it replaced: an instruction to turn a setting off, with no symptom attached. On the device that reads as a nag, in the one state where the user's actual problem is that nothing loads at all — and for a user who declined notifications the row is the only warning there is. Give it its own string, symptom first, matching the notification. Reusing the old one bought translations in 8 of 37 locales; every neighbouring string on this screen, including the notification this row backs up, is already English-only pending Crowdin. Also escape the quotes in msg_private_dns, which aapt was stripping. It has no caller again now, but every translation of it escapes them. Co-Authored-By: Claude Opus 5 <[email protected]>

  • Konrad Kollnig(05 Aug 26)

    Notice a pinned Private DNS when it happens, and warn in the app too (#713) * Notice a pinned Private DNS when it happens, and say so in the app The warning only re-evaluated when the tunnel was built, so the common sequence — VPN already running, user pins a resolver in Android settings — broke name resolution and stayed silent until some unrelated network change rebuilt it. onLinkPropertiesChanged already fires for this and LinkProperties already carries the resolver name; the reload policy simply discarded it by comparing DNS servers alone, which a pinned resolver leaves untouched. Comparing the name too costs no new registration, and the reason is deliberately absent from shouldRestartWireGuard: the tunnel is fine, only the warning is stale. The notification is also the sole surface, and Util.notify() is silent without POST_NOTIFICATIONS — so a user who declined it saw nothing at all in the one state where nothing loads. A row on the main screen, alongside the local-network one and re-evaluated in the same onResume, reaches them, and covers merely reopening the app (which reloads nothing). It is gated on the VPN actually running, since port 853 is only blocked while it is, and it revives msg_private_dns, which lost its last caller when the onboarding slide went. The condition both surfaces share now lives in Util.isPrivateDnsBlocked and is covered by PrivateDnsBlockedTest, including that an unreadable specifier must not silence the warning. * Keep the WireGuard rebind when a burst ends on a private DNS change The debounce collapses a burst of connectivity callbacks to its last reason, which was safe only while every reason restarted WireGuard. The new private-DNS reason does not, so a burst that ends on it now drops the rebind an earlier reason in the same 1500ms window required. Accumulate the need for a rebind across the burst instead of reading it off the surviving reason. Co-Authored-By: Claude Opus 5 <[email protected]> --------- Co-authored-by: Claude Opus 5 <[email protected]>

  • Konrad Kollnig(05 Aug 26)

    Stop asking about Private DNS during onboarding, notify when it breaks (#711) * Stop asking about Private DNS during onboarding, notify when it breaks Blocking DoT on port 853 is on by default, so Private DNS on "automatic" now resolves itself: Android falls back to plaintext and tracker detection carries on. The onboarding slide asks the user to go turn a setting off that no longer costs them anything, in the middle of the one flow where attention is scarcest. "Hostname" mode is the case that still breaks, and it breaks harder than the slide ever conveyed: Android does not fall back to plaintext for a resolver the user pinned by name, so DNS simply fails and nothing loads. Onboarding is also the wrong place to say so — it fires once, before the user has any traffic to lose, and says nothing to whoever turns Private DNS on next week. Notify from where the tunnel is built instead, so the reason arrives when the failure does, naming the configured resolver and opening the network settings that hold the switch. Clear that notification and the local network one on VPN stop, but not on a temporary one: re-posting a cancelled notification alerts again, and these two describe configuration that has not changed, so cancelling on every incoming call buzzes the user each time the VPN returns. The WireGuard error notification wants the opposite, once it can retract itself. stopInternal() never cleared lastError, and the state listener checks lastError before isRunning — deliberately, so a start that fails without ever producing a tunnel still reports — so a stopped tunnel went on reporting its final error and the listener's isRunning branch could never run. Clear it there, and the listener retracts the notification as the tunnel goes down, which is more accurate than holding a tunnel error over a tunnel that no longer exists. The call in stop() stays for the case the listener cannot see, a failed start with no tunnel to tear down. Util.isPrivateDns() had no callers left once the slide went; the notification uses getPrivateDnsSpecifier(), which is non-null only in the mode that actually breaks. Co-Authored-By: Claude Opus 5 <[email protected]> * Gate the Private DNS notification on mode, not the specifier isPrivateDnsHostnameMode() checks private_dns_mode directly, so a resolver that reads back null for any reason no longer silently suppresses the warning in exactly the configuration that is broken; the specifier is now read once and passed through instead of a second Settings lookup, and a missing specifier falls back to generic text instead of interpolating "null". Also drops the onboarding_privatedns_* strings left behind in seven locale files after the onboarding slide was removed. --------- Co-authored-by: Claude Opus 5 <[email protected]>

  • Konrad Kollnig(05 Aug 26)

    Correct the onboarding tap sequence against a measured run The previous sequence was written from a run that showed only four slides. Re-running it on a cleared install shows seven without pre-granting (welcome, blocking mode, VPN, lockdown, notifications, Private DNS, timeline) and five with, so the committed taps would have desynced immediately and driven the wrong controls. Three slides raise an 'Are you sure?' dialog on Next, and its OK button moves with the message length — 1439 on the lockdown warning, 1565 on the longer Private DNS one — so resolve the button from the view hierarchy instead of hardcoding a y. Adds a where() helper to re-sync when a run does not match. Also records what the walk-through surfaced: the lockdown slide warns with always_on_vpn_lockdown=0, and the Private DNS slide fires on opportunistic mode, which is Android's default and not DoT. Co-Authored-By: Claude Opus 5 <[email protected]>

  • Konrad Kollnig(05 Aug 26)

    Hardcode the onboarding tap sequence in the release smoke test Four slides on a fresh install, so give the exact taps rather than telling the next person to drive it by hand. Records the screen size the coordinates assume and how to check it, and keeps the two conditional slides (notifications, VPN lockdown) as a short note on what would lengthen the sequence. Drops the Private DNS slide from the description: #711 removes it, since DoT is blocked by default and Android falls back to plaintext DNS on its own. Co-Authored-By: Claude Opus 5 <[email protected]>

  • Konrad Kollnig(05 Aug 26)

    Stop claiming a fixed onboarding sequence in the release smoke test The checklist hardcoded a four-screen tap-through, which was just what one Pixel 8 happened to show. setupSlides() builds the list from device state — VPN consent, P+, notification permission, private_dns_mode — so a scripted tap sequence silently taps the wrong control elsewhere. Also records the discrepancy that prompted this: that run showed neither the lockdown slide nor the Private DNS slide, although the code adds the first unconditionally on P+ and the second for any non-off private DNS mode, and the device was on opportunistic. Unexplained, so it is written down as something to raise rather than left implied as normal. Co-Authored-By: Claude Opus 5 <[email protected]>

  • Konrad Kollnig(05 Aug 26)

    Document the release smoke test so it is not re-invented each time The 2026080501 release was tested by working out from scratch which package the signed APK installs as, that run-as is refused on it, which onboarding taps are needed, and what actually proves the packet engine still works. None of that was written down. Records the checklist and the two findings that cost the most time: the release build is not debuggable, so the AGENTS.md popup-skipping recipe only half applies (appops and pm grant work, run-as and prefs seeding do not), and gh release view has no isLatest field, so publication has to be confirmed against the API's releases/latest. Also carries over the standing rules that matter here — announce that enabling the VPN revokes whoever holds consent, and do not uninstall afterwards without asking. Co-Authored-By: Claude Opus 5 <[email protected]>

  • Konrad Kollnig(05 Aug 26)

    Version 2026080501 Co-Authored-By: Claude Opus 5 <[email protected]>

  • Konrad Kollnig(05 Aug 26)

    Notify about missing local network access, and stop misdiagnosing it (#709) The banner only reaches someone who opens the app, and the settings prompt only someone already changing that setting. The symptom is "nothing resolves", which nobody attributes to a permission — the likely responses are to turn the VPN off or uninstall, neither of which the app ever hears about. Add a notification from where the tunnel is built, so the reason meets the user where the failure appears. It alerts once, clears as soon as the permission is granted, and opens the main screen, where the warning row requests it. The existing notifications actively misdiagnose this case. A LAN resolver we may not talk to raises the DoH one — "Secure DNS cannot be reached ... consider disabling it in the Network settings" — which blames the wrong thing and recommends turning off a feature that works fine; a WireGuard peer on the LAN sends the user into the WireGuard settings for the same non-reason. Both now check isMissing() first and defer to the local network notification, which names the actual cause and fixes it in one tap. Co-authored-by: Claude Opus 5 <[email protected]>

  • Konrad Kollnig(05 Aug 26)

    Warn about local destinations named by host, not just by address (#707) isConfigured() classifies the stored configuration, so it only sees addresses: a DoH endpoint at https://pi.hole/dns-query, or a WireGuard peer on a dynamic DNS name, resolves to the LAN but reads as remote. Those users get no banner and no prompt — just a resolver that stopped answering. Classifying them up front would mean resolving a name on whichever thread asked, including the main one, which is exactly what the address-only rule exists to avoid. Take the address from the code that already resolved it instead. OkHttp resolves the DoH endpoint anyway, so hang a Dns off the client and read what it got; WgEgress.resolveEndpoint() likewise has the peer's address in hand. Both hand it to LocalNetworkAccess.reportDestination(), which latches if it is local, and isMissing() ors that in. The destination is what matters, not whether the connection failed: if it is local and the permission is missing, that traffic is blocked. The latch is in-memory only — a stale observation dies with the process, and anything still pointing at the LAN re-reports on next use. Changing a relevant setting clears it, since the new value may point elsewhere. Co-authored-by: Claude Opus 5 <[email protected]>

  • Konrad Kollnig(05 Aug 26)

    Support Android 17 local network protections (#701) (#704) * Support Android 17 local network protections (#701) Android 17 gates traffic to local network addresses behind the ACCESS_LOCAL_NETWORK runtime permission for apps targeting API 37, which TrackerControl does: TCP connections time out and UDP fails with EPERM. Traffic other apps send to the LAN is unaffected — those are their own sockets, and RFC 1918 ranges stay out of the VPN routes, so that traffic never enters the tun. What breaks is traffic TrackerControl itself sends to the local network: a custom VPN DNS server on the LAN gets a host route into the tun and is re-sent from our own socket, so pointing the VPN DNS at a Pi-hole or the router leaves the device with no working name resolution at all. The same applies to Secure DNS aimed at a local resolver, to tethering compatibility mode (whose full-tunnel route puts LAN traffic back inside the tun), and to a WireGuard peer hosted at home. Declare the permission and ask for it where the configuration actually needs it: immediately when such a setting changes, and from a tappable warning on the main screen while it is missing. The system's own resolvers are deliberately left out — Android exempts port 53 traffic to the network's DNS servers, so the common "router is the DNS server" setup keeps working, and users who never point TrackerControl at the LAN are never prompted. Below Android 17 nothing is checked or shown. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MsAkkinA9Jfjt9khTKvBma * Update whois lookup link (NetGuard upstream) Merges NetGuard 552ef140: dnslytics moved its lookup from /whois-lookup/<ip> to search.dnslytics.com/ip/<ip>. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MsAkkinA9Jfjt9khTKvBma * Cover SOCKS5 proxies, never resolve names while parsing addresses A SOCKS5 proxy on the LAN breaks the same way a local WireGuard peer does: the native engine dials it from our own socket (jni_socks5), so it needs ACCESS_LOCAL_NETWORK too. Add socks5_enabled/socks5_addr to the configuration check. parseNumeric() handed anything containing a colon to InetAddress.getByName(), which falls back to a name lookup for whatever it cannot parse numerically — a blocking resolve on the caller's thread, including the thread that builds the tunnel. Gate the call on the string being made of characters an IPv6 literal can contain. Tests: SOCKS5 (local, remote, gated on the toggle), the 100.64.0.0/10 boundaries, IPv4 and IPv6 link-local, the 172.16/12 edges, hostnames staying unresolved, and a WireGuard endpoint without a port. Co-Authored-By: Claude Opus 5 <[email protected]> * Name the permission the way Android asks for it The system dialog reads "find, connect to and determine the relative position of nearby devices" — the NEARBY_DEVICES group wording. Coming from a privacy app with no warning, that reads alarmingly. Say up front that this is what Android will ask about. Co-Authored-By: Claude Opus 5 <[email protected]> * Mention proxies in the warning, tighten LocalNetworkAccess The warning row still listed only the DNS server, Secure DNS resolver and WireGuard peer, but a SOCKS5 proxy on the LAN now triggers it too. Derive the row's visibility in onRequestPermissionsResult from isMissing() instead of the grant result, so it agrees with onResume by construction rather than by coincidence. Give the utility class a private constructor, and drop isConfigured() from the public surface — isMissing() is the entry point, and the SharedPreferences overload only needs to be visible to the tests. Co-Authored-By: Claude Opus 5 <[email protected]> --------- Co-authored-by: Claude <[email protected]>

  • Konrad Kollnig(05 Aug 26)

    Document how to drive the app on a device without popups (#710) * Document how to drive the app on a device without popups Agent sessions kept burning turns tapping through onboarding, the VPN consent dialog and permission prompts — and re-entering onboarding after overwriting the preferences file, which drops onboarding_version. All of it can be pre-satisfied from adb, with no code changes and nothing debug-only to maintain: `install -g` for runtime permissions, `appops set ACTIVATE_VPN allow` so VpnService.prepare() returns null (which skips the system dialog and our own explainer before it), and a seeded onboarding_version, which run-as can write before the first launch. Also records the traps behind the churn: force-stop before editing preferences or the running process overwrites them, merge rather than replace so onboarding_version survives, and uninstall/pm clear wipe the seeded state along with the appop and the grants. Two notes for someone else's device: VpnService.prepare() revokes whatever VPN app currently holds consent, and the github debug package is also what a maintainer's own dev build installs as, so seeding preferences into it overwrites their real configuration. Verified end to end on a Pixel 8 (Android 17). Co-Authored-By: Claude Opus 5 <[email protected]> * Default device testing to fdroid debug, and require asking before destroying state github debug installs as net.kollnig.missioncontrol.test, which is also what a maintainer's own day-to-day dev build installs as. Following the previous advice meant testing on top of their real configuration, and enabling its VPN takes the consent slot from whatever they were running. The flavours differ only in the update-check API, so fdroid debug costs nothing and installs alongside. Also states the rule the previous wording only implied: uninstall, pm clear, overwriting preferences and revoking permissions are irreversible, so ask first and name what will be lost — including when the target looks like a throwaway you installed yourself, since that is one mistaken package name away from the real install. Co-Authored-By: Claude Opus 5 <[email protected]> --------- Co-authored-by: Claude Opus 5 <[email protected]>

  • Konrad Kollnig(05 Aug 26)

    Show the notification permission prompt on every tab (#706) The prompt row sits inside llApps, which is hidden whenever the Timeline or VPN tab is selected — and ActivityMain deliberately lands users on Timeline, both on first run and on return. A user who has not granted notification permissions therefore never sees the prompt unless they happen to open the Apps tab. Move it out of llApps, directly below the app bar, and let onCreate and onResume drive its visibility as they already do. The XML default goes from visible to gone to match: onCreate hides it unconditionally anyway, and a row this prominent should not flash before that runs. Co-authored-by: Claude Opus 5 <[email protected]>

  • Claude(04 Aug 26)

    ci: resolve runtime classpaths before the offline test run and build Runtime-scoped transitive dependencies never appear on a compile classpath, so compiling or assembling online does not put them into the Gradle cache. The prefetch step was entirely compile/assemble oriented, leaving them missing for the later --offline steps. Master stayed green only because its read-write cache had accumulated those artifacts from earlier runs. Bumping constraintlayout 2.2.1 -> 2.2.2 pulled in constraintlayout-core 1.1.2 (scope=runtime), a coordinate never cached before, which exposed the gap: Could not resolve all files for configuration ':app:fdroidDebugUnitTestRuntimeClasspath'. > Could not download constraintlayout-core-1.1.2.jar: No cached version available for offline mode Warming mergeFdroidDebugUnitTestManifest only moved the failure to the next consumer of the same unresolved configuration, and assembleFdroid- Release --offline would have failed next for the same reason. Resolve the runtime classpaths explicitly instead, following the existing prefetchAndroidLintDependencies pattern.

  • Konrad Kollnig(04 Aug 26)

    ci: warm unit-test runtime classpath before offline test run (#705) The "Prefetch Gradle dependencies" step in the test job only ran assembleFdroidDebugUnitTest online, which doesn't resolve the unit test *runtime* classpath (e.g. espresso-idling-resource, pulled in via Robolectric/androidx.test). mergeFdroidDebugUnitTestManifest needs that classpath, so testFdroidDebugUnitTest --offline later failed with "No cached version available for offline mode". Co-authored-by: Claude <[email protected]>

  • dependabot[bot](04 Aug 26)

    deps: bump androidx.constraintlayout:constraintlayout (#702) Bumps the gradle-minor-patch group with 1 update: androidx.constraintlayout:constraintlayout. Updates `androidx.constraintlayout:constraintlayout` from 2.2.1 to 2.2.2 --- updated-dependencies: - dependency-name: androidx.constraintlayout:constraintlayout dependency-version: 2.2.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gradle-minor-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

  • dependabot[bot](03 Aug 26)

    ci: bump gradle/actions from 6 to 6.2.0 in the actions group (#703) Bumps the actions group with 1 update: [gradle/actions](https://github.com/gradle/actions). Updates `gradle/actions` from 6 to 6.2.0 - [Release notes](https://github.com/gradle/actions/releases) - [Commits](https://github.com/gradle/actions/compare/v6...v6.2.0) --- updated-dependencies: - dependency-name: gradle/actions dependency-version: 6.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

  • Konrad Kollnig(01 Aug 26)

    Route the full tunnel in tethering compatibility mode (#699) (#700) Before the static VPN route set landed, the tun carried a plain 0.0.0.0/0 route, so tethered traffic — including the downstream subnets and the DHCP broadcast — went through TrackerControl. On devices whose own tethering path is broken, that is what made tethering work at all. The split-tunnel route set removed the default route and, with it, the subnet/tethering/lan preferences, leaving affected users with no way back other than downgrading. Reuse the existing (opt-in, default off) tethering compatibility mode: alongside the TCP MSS clamp it now installs a single 0.0.0.0/0 route, restoring the previous behaviour for the users who need it while the split-tunnel route set stays the default for everyone else. WireGuard keeps precedence — a full tunnel would hand LAN and reserved ranges to the peer, which drops them — so an active profile's AllowedIPs still decide the routes. On Android 13+ the carrier ePDG excludeRoute() calls still apply on top of the default route, so Wi-Fi calling is unaffected. Claude-Session: https://claude.ai/code/session_01FhD7ETjNc5qb5hSw9taYtS Co-authored-by: Claude <[email protected]>

  • Konrad Kollnig(01 Aug 26)

    Show tracker activity that is recorded but currently undisplayable (#695) * Show tracker activity that is recorded but currently undisplayable (#694) Two ways the Timeline can stay empty while tracker access is being recorded and blocked normally. Unresolvable UIDs were dropped at display time. ServiceSinkhole's shouldTrackApp() deliberately defaults a UID whose package cannot be resolved to tracked ("Unknown UID, default to tracking"), so its tracker access is recorded and blocked — but both readers of the access table then discarded those rows: TimelineFragment skipped entries without a package name and InsightsDataProvider did the same. On Android 16 getPackagesForUid() throws SecurityException for UIDs of other users (work profile, Secure Folder, private space, cloned apps), which Util maps to null, so that activity was recorded and permanently invisible. List it under a UID label instead; entries without a package are not clickable, since the details screen is per package. The empty state could not say that recording is off. With "Search new trackers" (log_app) disabled, trackers are still blocked but no access row is ever written, so the Timeline stays empty forever while claiming "Watching for trackers…". Report that state separately and offer a way into settings. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TkoZNtELmKmfNMxLump749 * Handle unavailable tracker recording settings --------- Co-authored-by: Claude <[email protected]>

  • github-actions[bot](01 Aug 26)

    Update bundled tracker/host lists (#697) Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

  • Konrad Kollnig(01 Aug 26)

    Keep tracker mode tests stable across list updates

  • Konrad Kollnig(01 Aug 26)

    Restore system app filter (#698)

  • Konrad Kollnig(01 Aug 26)

    Minify updated Disconnect list asset

  • Konrad Kollnig(28 Jul 26)

    Fail over to a different Mullvad/IVPN relay when the current one stays dead (#688) * Fail over to a different Mullvad/IVPN relay when the current one stays dead WgEgress already retries a broken tunnel with backoff, but every retry re-resolved and restarted against the same fixed [Peer] endpoint baked into the profile. If the relay server itself is down (not just a local network blip), that loop never recovers. After a few consecutive full-restart cycles keep failing to produce any traffic, WgEgress now asks a new provider-aware hook (WgRelayFailover) to move the active Mullvad/IVPN profile to a different relay, reusing the existing key material, before continuing the restart loop. Self-hosted or manually imported profiles are left untouched, since there is only one server to fail over to. Fixes #679 * Address review findings on relay failover (PR #688) - Bound the relay-list fetch with a real timeout (FAILOVER_TIMEOUT_MS): previously forceRestartPending blocked every other recovery path while the unbounded network call ran, so a hang could stall recovery longer than not having failover at all. - Make failoverInFlight an AtomicBoolean to close a check-then-set race between the monitor and rebind threads. - Only trigger failover from monitor-observed breaks, not from rebind failures after a network change (those aren't evidence the relay is dead). - Reset the restart backoff after a successful switch so a fresh relay doesn't inherit the old one's penalty. - Persist the IVPN session WgRelayFailover generates/reuses; it was minting sessions that never got saved, orphaning them against the account's device limit and desyncing stored session state from the live config. - Scope the config write to the profile ID resolved before the network call (WgProfileManager#updateProfileConfigIfActive), so a profile switch mid-fetch can't overwrite a different profile's stored config. - Exclude the relay that just failed from the next pick when another candidate exists, and reject a pick that silently widens the exit country, for both Mullvad and IVPN. - Distinguish ApiRejectedException/CaptchaRequiredException from other failures in the failover log. - Add WgRelayFailoverTest and extend WgProfileManagerTest for the new guard clauses. --------- Co-authored-by: Claude <[email protected]>

  • Konrad Kollnig(28 Jul 26)

    Fix StringFormatInvalid lint errors in translated strings (#691) Five app_eula translations (da, eu, fi, nl, no) had a literal "100%" followed by a letter lint's formatter parser treats as a conversion character (e.g. "% b", "% s"), making the string an invalid format spec; escape the percent sign as "%%" in each. The Slovenian msg_wg_mullvad_countries_failed string was empty, dropping the required %s placeholder from its source counterpart entirely; added a translation with the placeholder restored. Claude-Session: https://claude.ai/code/session_01M5uW2XvYGhBTBE19Nsod6z Co-authored-by: Claude <[email protected]>

  • Konrad Kollnig(28 Jul 26)

    Attribute shared-IP DNS evidence by recency, not alphabetically (#655) (#690) getQAName() previously deduped and ordered candidate qnames for a shared IP alphabetically, so blockKnownTracker() and the traffic-log attribution in ServiceSinkhole.log() always locked onto whichever qname sorted first lexically, regardless of when it was actually resolved. Order by most recent observation instead, so the freshest DNS evidence — the one most likely to correspond to the connection actually being made — wins. Co-authored-by: Claude <[email protected]>

  • Konrad Kollnig(28 Jul 26)

    Clarify that SNI research mode leaks respect remote routing (#687) The SNI research-mode warning previously implied it always exposes the user's real IP to tracker servers. In fact SNI is parsed locally, from traffic already passing through the same block/allow decision used for every other packet, before that packet is dispatched to any egress path. When remote routing (WireGuard) is enabled, the resulting connection to the tracker goes out through the tunnel, so only the VPN provider's IP is exposed. Clarify this in the README, the in-app settings summary, and AGENTS.md's privacy rationale, per the discussion in #685. Co-authored-by: Claude <[email protected]>

  • Konrad Kollnig(28 Jul 26)

    Fix release asset ordering so the in-app updater fetches the APK (#686) * Fix release asset ordering so the in-app updater fetches the APK GitHub's API returns release assets sorted by name, not upload order. The app's checkUpdate() always downloads assets[0], so once build_and_sign.sh started attaching SHA256SUMS.txt and BUILD-INFO.txt alongside the APK, those alphabetically-earlier names could be picked up instead of the APK (#681). Rename the metadata files using the APK filename as a literal prefix so it always sorts first. * Show release version name in update notification, not asset filename checkUpdate() already opened the release's html_url (the GitHub release page) rather than a direct asset link, so tapping the notification was never downloading a raw file. But its title used the first release asset's filename, which is confusing and, per #681, could be a metadata file rather than the APK depending on asset ordering. Use the release's display name (e.g. "Version 2026072702") instead, falling back to a formatted tag if the release has no name. --------- Co-authored-by: Claude <[email protected]>

TrackerControl Website

Website

TrackerControl for Android | Monitor and control trackers and ads.

TrackerControl allows you to monitor and control the widespread, ongoing, hidden data collection in mobile apps about user behaviour (tracking).

Redirects

Does not redirect

Security Checks

All 65 security checks passed

Server Details

  • IP Address185.199.110.153
  • Hostnamecdn-185-199-110-153.github.com
  • LocationFrancisco,Indiana,United States of America,NA
  • ISPGitHub Inc.
  • ASNAS54113

Associated Countries

  • USUS

Safety Score

Website marked as safe

100%

Blacklist Check

trackercontrol.org was found on 0 blacklists

  • AntiSocial Blacklist
  • Artists Against 419
  • Badbitcoin
  • Bambenek Consulting
  • CERT Polska
  • CoinBlockerLists
  • CRDF
  • CryptoScamDB
  • EtherAddressLookup
  • EtherScamDB
  • Fake Website Buster
  • MetaMask EthPhishing
  • NABP Not Recommended Sites
  • OpenPhish
  • PetScams
  • PhishFeed
  • PhishFort
  • Phishing.Database
  • PhishStats
  • PhishTank
  • Phishunt
  • RPiList Not Serious
  • Scam.Directory
  • SecureReload Phishing List
  • Spam404
  • StopGunScams
  • Suspicious Hosting IP
  • ThreatFox
  • ThreatLog
  • TweetFeed
  • URLhaus
  • ViriBack C2 Tracker

Website Preview

Website preview

TrackerControl Android App

APK Info

Trackers

  • ACRA

Permissions

  • Access Network State
  • Access Wifi State
  • Foreground Service
  • Internet
  • Query All Packages
  • Read Phone State
  • Receive Boot Completed
  • Vibrate
  • Wake Lock
  • Write External Storage
  • Admin

TrackerControl Reviews

More Mobile Apps

About the Data: TrackerControl

Change History

  • Amended (androidApp)

Edit TrackerControl Data

You can edit TrackerControl's entry in this section of awesome-privacy.yml by submitting a PR to our GitHub repo.
Note that some of the information shown above has been aggregated from external sources, a list of these can be found data documentation.

Origin Data

Modify Data

API

You can access TrackerControl's data programmatically via our API. Simply make a GET request to:

https://api.awesome-privacy.xyz/v1/services/trackercontrol

The REST API is free, no-auth and CORS-enabled. To learn more, view the API Docs or read the API Usage Guide.

Share TrackerControl

Help your friends compare Mobile Apps, and pick privacy-respecting software and services.
Share TrackerControl and Awesome Privacy with your network!