Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions app/src/main/java/eu/faircode/netguard/ActivityMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -525,8 +525,7 @@ public void onClick(View v) {
@Override
public void onClick(View v) {
Intent settings;
if (privateDnsWarningState
== ServiceSinkhole.PRIVATE_DNS_WARNING_ALLOWED_DOT)
if (ServiceSinkhole.opensTrackerControlSettings(privateDnsWarningState))
settings = new Intent(ActivityMain.this, ActivitySettings.class);
else {
settings = new Intent(Settings.ACTION_WIRELESS_SETTINGS);
Expand Down Expand Up @@ -588,9 +587,7 @@ public void onRefresh() {
// Handle intent
checkExtras(getIntent());

// Warn about disabled blocking if ADB logging is on
if (!Util.isPlayStoreInstall() && prefs.getBoolean("log_logcat", false))
Toast.makeText(this, R.string.msg_log_logcat, Toast.LENGTH_SHORT).show();
updateResearchModeNotice();
}

@Override
Expand Down Expand Up @@ -657,6 +654,7 @@ protected void onResume() {
vpnEnabled && Util.isPrivateDnsBlocked(this) ? View.VISIBLE : View.GONE);
}
refreshPrivateDnsBypassBanner();
updateResearchModeNotice();

super.onResume();
}
Expand Down Expand Up @@ -687,9 +685,22 @@ public void onConfigurationChanged(Configuration newConfig) {
updateHorizontalPadding(findViewById(R.id.tvLocalNetwork), horizontalPadding);
updateHorizontalPadding(findViewById(R.id.tvPrivateDns), horizontalPadding);
updateHorizontalPadding(findViewById(R.id.tvPrivateDnsBypass), horizontalPadding);
updateResearchModeNotice();
updatePrivateDnsBypassBanner();
}

private void updateResearchModeNotice() {
TextView notice = findViewById(R.id.tvResearchMode);
if (notice == null)
return;
updateHorizontalPadding(notice, Math.max(
getResources().getDimensionPixelSize(R.dimen.activity_horizontal_margin),
Math.round(16 * getResources().getDisplayMetrics().density)));
notice.setVisibility(PreferenceManager.getDefaultSharedPreferences(this)
.getBoolean("log_logcat", false) ? View.VISIBLE : View.GONE);
notice.setOnClickListener(view -> startActivity(new Intent(this, ActivitySettings.class)));
}

private void updatePrivateDnsBypassBanner() {
TextView tvPrivateDnsBypass = findViewById(R.id.tvPrivateDnsBypass);
if (tvPrivateDnsBypass == null)
Expand All @@ -704,7 +715,9 @@ private void updatePrivateDnsBypassBanner() {
String resolver = TextUtils.isEmpty(specifier)
? getString(R.string.msg_private_dns_unknown_resolver) : specifier;
tvPrivateDnsBypass.setText(getString(R.string.msg_private_dns_bypass_notify, resolver));
} else if (state == ServiceSinkhole.PRIVATE_DNS_WARNING_ALLOWED_DOT)
} else if (state == ServiceSinkhole.PRIVATE_DNS_WARNING_ACTIVE)
tvPrivateDnsBypass.setText(R.string.msg_private_dns_bypass_active_notify);
else if (state == ServiceSinkhole.PRIVATE_DNS_WARNING_ALLOWED_DOT)
tvPrivateDnsBypass.setText(R.string.msg_private_dns_bypass_allowed_notify);
tvPrivateDnsBypass.setVisibility(
state != ServiceSinkhole.PRIVATE_DNS_WARNING_NONE ? View.VISIBLE : View.GONE);
Expand Down Expand Up @@ -1001,6 +1014,9 @@ public void onSharedPreferenceChanged(SharedPreferences prefs, String name) {

updatePrivateDnsBypassBanner();

} else if ("log_logcat".equals(name)) {
updateResearchModeNotice();

} else if ("block_dot".equals(name) || "log".equals(name)) {
refreshPrivateDnsBypassBanner();

Expand Down
61 changes: 61 additions & 0 deletions app/src/main/java/eu/faircode/netguard/PrivateDnsMonitor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package eu.faircode.netguard;

import android.net.ConnectivityManager;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.os.Build;

/** Observes VPN DNS validation without polling or changing network routing. */
final class PrivateDnsMonitor {
private final ConnectivityManager connectivity;
private final Runnable onChanged;
private ConnectivityManager.NetworkCallback callback;

PrivateDnsMonitor(ConnectivityManager connectivity, Runnable onChanged) {
this.connectivity = connectivity;
this.onChanged = onChanged;
}

synchronized void setEnabled(boolean enabled) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P || connectivity == null)
return;
if (!enabled) {
if (callback != null) {
ConnectivityManager.NetworkCallback previous = callback;
callback = null;
connectivity.unregisterNetworkCallback(previous);
}
} else if (callback == null) {
callback = new ConnectivityManager.NetworkCallback() {
@Override
public void onLinkPropertiesChanged(Network network, LinkProperties properties) {
changed(this);
}

@Override
public void onLost(Network network) {
changed(this);
}
};
try {
connectivity.registerNetworkCallback(new NetworkRequest.Builder()
// The default request excludes VPNs, and TC's active
// network is the underlying network because it excludes itself.
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.build(), callback);
} catch (RuntimeException ex) {
callback = null;
throw ex;
}
}
}

private synchronized void changed(ConnectivityManager.NetworkCallback source) {
// An event already queued before unregistering must not revive a warning.
if (callback == source)
onChanged.run();
}
}
107 changes: 85 additions & 22 deletions app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java
Original file line number Diff line number Diff line change
Expand Up @@ -380,14 +380,18 @@ private static int getIntPref(SharedPreferences prefs, String key, int def) {
static final int PRIVATE_DNS_WARNING_NONE = 0;
static final int PRIVATE_DNS_WARNING_HOSTNAME = 1;
static final int PRIVATE_DNS_WARNING_ALLOWED_DOT = 2;
static final int PRIVATE_DNS_WARNING_ACTIVE = 3;
private static final long PRIVATE_DNS_WARNING_WINDOW_MS = 60 * 60 * 1000L;
private int privateDnsWarningState = PRIVATE_DNS_WARNING_NONE;
private final Handler privateDnsWarningExpiryHandler = new Handler(Looper.getMainLooper());
private final Runnable privateDnsWarningExpiryRunnable = this::requestPrivateDnsWarningUpdate;
private PrivateDnsMonitor privateDnsMonitor;
private final SharedPreferences.OnSharedPreferenceChangeListener privateDnsPreferenceListener =
(preferences, key) -> {
if ("block_dot".equals(key) || "log".equals(key))
requestPrivateDnsWarningUpdate();
if ("block_dot".equals(key))
updatePrivateDnsMonitoring();
};

public static final String EXTRA_COMMAND = "Command";
Expand Down Expand Up @@ -904,6 +908,7 @@ private void stop(boolean temporary) {
stopVPN(vpn);
vpn = null;
vpnEstablished = false;
updatePrivateDnsMonitoring();
// A teardown the service performs itself, so it is not one
// VpnRestartWorker should undo, whether it is a switch-off or
// the pause for a phone call.
Expand Down Expand Up @@ -1203,11 +1208,15 @@ private void log(Packet packet, int connection, boolean interactive) {
if (prefs.getBoolean("sni_enabled", false)
&& packet.data != null
&& !packet.data.isEmpty()) {
uncertain = DatabaseHelper.ACCESS_UNCERTAIN_NONE;
uncertain = sniUncertainty(uncertain);
if (!packet.data.equals(originalDname)) {
Log.d(TAG, "Using SNI " + packet.data + " instead of originalDname " + originalDname);
dname = packet.data;
isTracker = getDecloakedTracker(dname, dh).first != null;
Pair<Tracker, String> sniTracker = getDecloakedTracker(packet.data, dh);
isTracker = sniTracker.first != null;
// Record the name that matched, which is the CNAME target
// for a cloaked tracker. The timeline and Protection screen
// look the stored name up again and drop rows that miss.
dname = isTracker ? sniTracker.second : packet.data;
}
}

Expand Down Expand Up @@ -1247,20 +1256,7 @@ private Pair<Tracker, String> getDecloakedTracker(String qname, DatabaseHelper d
}

private Pair<Tracker, String> getDecloakedTracker(String qname, String aname) {
String foundDname = null;
Tracker t = TrackerList.findTracker(qname);

if (t != null) {
foundDname = qname;
} else { // DNS uncloaking
if (aname != null) {
t = TrackerList.findTracker(aname);
if (t != null)
foundDname = aname;
}
}

return new Pair<>(t, foundDname);
return decloakTracker(qname, aname);
}

private void usage(Usage usage) {
Expand Down Expand Up @@ -1756,6 +1752,7 @@ private ParcelFileDescriptor startVPN(Builder builder) throws SecurityException
updateUnderlyingNetworks();
vpnEstablished = true;
markStoppedCleanly(false);
updatePrivateDnsMonitoring();
return pfd;
} catch (SecurityException ex) {
throw ex;
Expand All @@ -1766,17 +1763,28 @@ private ParcelFileDescriptor startVPN(Builder builder) throws SecurityException
}

static int getPrivateDnsWarningState(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (prefs.getBoolean("block_dot", true))
return PRIVATE_DNS_WARNING_NONE;
return getPrivateDnsWarningState(context, Util.isPrivateDnsActiveOnVpn(context));
}

static int getPrivateDnsWarningState(Context context, boolean privateDnsActiveOnVpn) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean blockDot = prefs.getBoolean("block_dot", true);
if (blockDot)
return PRIVATE_DNS_WARNING_NONE;

// Case A takes precedence and avoids querying the log. Case B is only
// Case A takes precedence and avoids querying the log. Case B covers
// "automatic" mode, where Android upgrades to DoT on its own once the
// resolvers the VPN advertises answer on port 853. Case C is only
// meaningful when the full traffic log is enabled: insertLog runs only
// on that path. log_app defaults to true but records recognized tracker
// contacts in access, not all flows, so it cannot support this warning.
if (Util.isPrivateDnsDetectionDefeated(context))
return PRIVATE_DNS_WARNING_HOSTNAME;
if (privateDnsActiveOnVpn)
return PRIVATE_DNS_WARNING_ACTIVE;
if (!prefs.getBoolean("log", false))
return PRIVATE_DNS_WARNING_NONE;

Expand All @@ -1785,6 +1793,14 @@ static int getPrivateDnsWarningState(Context context) {
? PRIVATE_DNS_WARNING_ALLOWED_DOT : PRIVATE_DNS_WARNING_NONE;
}

/**
* Whether the warning's fix is TrackerControl's own "Block Android's
* Private DNS" setting rather than Android's Private DNS setting.
*/
static boolean opensTrackerControlSettings(int state) {
return state == PRIVATE_DNS_WARNING_ALLOWED_DOT || state == PRIVATE_DNS_WARNING_ACTIVE;
}

static boolean shouldShowPrivateDnsWarning(int previousState, int currentState) {
return currentState != PRIVATE_DNS_WARNING_NONE && previousState != currentState;
}
Expand Down Expand Up @@ -2754,6 +2770,35 @@ private void dnsResolved(ResourceRecord rr) {
}
}

static Pair<Tracker, String> decloakTracker(String qname, String aname) {
String foundDname = null;
Tracker t = TrackerList.findTracker(qname);

if (t != null) {
foundDname = qname;
} else { // DNS uncloaking
if (aname != null) {
t = TrackerList.findTracker(aname);
if (t != null)
foundDname = aname;
}
}

return new Pair<>(t, foundDname);
}

/**
* Uncertainty of a contact attributed by its SNI. The SNI names the host,
* so it settles which of an IP's DNS questions the flow belongs to. The
* block decision is still made by IP, though, so an IP shared by a tracker
* and a non-tracker keeps its mark: it explains why standard mode let the
* contact through.
*/
static int sniUncertainty(int dnsUncertainty) {
return dnsUncertainty == DatabaseHelper.ACCESS_UNCERTAIN_MIXED_TRACKER_AND_NON_TRACKER
? dnsUncertainty : DatabaseHelper.ACCESS_UNCERTAIN_NONE;
}

static final class DnsEvidence {
final Set<String> questions = new HashSet<>();
final Set<String> trackerQuestions = new HashSet<>();
Expand Down Expand Up @@ -3797,6 +3842,9 @@ public void onCreate() {
commandHandler = new CommandHandler(commandLooper);
logHandler = new LogHandler(logLooper);
statsHandler = new StatsHandler(statsLooper);
privateDnsMonitor = new PrivateDnsMonitor(
(ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE),
this::requestPrivateDnsWarningUpdate);
prefs.registerOnSharedPreferenceChangeListener(privateDnsPreferenceListener);

net.kollnig.missioncontrol.wg.WgEgress.INSTANCE.addStateListener(wgStateListener);
Expand Down Expand Up @@ -4157,6 +4205,7 @@ public void onRevoke() {
// worker leaves it alone. Coming back from one is the OS's job, through
// the always-on designation it either kept or cleared above.
vpnEstablished = false;
updatePrivateDnsMonitoring();
markStoppedCleanly(true);

// Feedback: the tunnel really did drop, so inform the user (mirrors
Expand Down Expand Up @@ -4209,6 +4258,7 @@ public void onDestroy() {
// Cancel any debounced network-change reload so it doesn't fire post-teardown
networkReloadDebounceHandler.removeCallbacksAndMessages(NETWORK_RELOAD_TOKEN);
privateDnsWarningExpiryHandler.removeCallbacks(privateDnsWarningExpiryRunnable);
updatePrivateDnsMonitoring();
PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this)
.unregisterOnSharedPreferenceChangeListener(privateDnsPreferenceListener);
cancelNativeRecovery(true);
Expand Down Expand Up @@ -4681,9 +4731,9 @@ private void clearPrivateDnsNotification() {

private void requestPrivateDnsWarningUpdate() {
Handler handler = logHandler;
if (handler != null && !destroying)
if (handler != null && !destroying && vpnEstablished)
handler.post(() -> {
if (!destroying)
if (!destroying && vpnEstablished)
updatePrivateDnsBypassWarning();
});
}
Expand All @@ -4698,6 +4748,17 @@ private synchronized void schedulePrivateDnsWarningExpiry() {
privateDnsWarningExpiryHandler.postDelayed(privateDnsWarningExpiryRunnable, delay);
}

private void updatePrivateDnsMonitoring() {
if (privateDnsMonitor == null)
return;
try {
privateDnsMonitor.setEnabled(!destroying && vpnEstablished
&& !PreferenceManager.getDefaultSharedPreferences(this).getBoolean("block_dot", true));
} catch (RuntimeException ex) {
Log.w(TAG, "Could not update Private DNS monitor", ex);
}
}

private synchronized void cancelPrivateDnsWarningExpiry() {
privateDnsWarningExpiryHandler.removeCallbacks(privateDnsWarningExpiryRunnable);
}
Expand Down Expand Up @@ -4746,7 +4807,7 @@ private synchronized void resetPrivateDnsBypassWarning() {

private void showPrivateDnsBypassNotification(int state) {
Intent settings;
if (state == PRIVATE_DNS_WARNING_ALLOWED_DOT)
if (opensTrackerControlSettings(state))
settings = new Intent(this, ActivitySettings.class);
else {
settings = new Intent(Settings.ACTION_WIRELESS_SETTINGS);
Expand All @@ -4762,7 +4823,9 @@ private void showPrivateDnsBypassNotification(int state) {
String resolver = TextUtils.isEmpty(specifier)
? getString(R.string.msg_private_dns_unknown_resolver) : specifier;
detail = getString(R.string.msg_private_dns_bypass_notify, resolver);
} else
} else if (state == PRIVATE_DNS_WARNING_ACTIVE)
detail = getString(R.string.msg_private_dns_bypass_active_notify);
else
detail = getString(R.string.msg_private_dns_bypass_allowed_notify);

NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "notify");
Expand Down
Loading
Loading