Skip to content
Merged
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
1 change: 1 addition & 0 deletions app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ add_library( netguard
src/main/jni/netguard/netguard.c
src/main/jni/netguard/session.c
src/main/jni/netguard/ip.c
src/main/jni/netguard/route.c
src/main/jni/netguard/tls.c
src/main/jni/netguard/tcp.c
src/main/jni/netguard/udp.c
Expand Down
14 changes: 14 additions & 0 deletions app/src/main/java/eu/faircode/netguard/ActivitySettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,12 @@ else if ("socks5_addr".equals(name)) {
} else if ("wg_keepalive_when_screen_off".equals(name)) {
ServiceSinkhole.reload("changed " + name, this, false);

} else if (Rule.PREF_WG_ROUTE_MODE.equals(name)) {
// The reload restarts the tunnel thread, which is what picks up the
// matching fwd53 change — that flag is only read at jni_run.
Rule.clearCache(this);
ServiceSinkhole.reload("changed " + name, this, false);

} else if ("wg_config".equals(name)) {
String wg_config = prefs.getString(name, null);
boolean valid = true;
Expand Down Expand Up @@ -1257,6 +1263,10 @@ private void xmlExport(OutputStream _out) throws IOException {
xmlExport(getSharedPreferences("tracker_protect", Context.MODE_PRIVATE), serializer);
serializer.endTag(null, "tracker_protect");

serializer.startTag(null, Rule.PREF_WG_ROUTE);
xmlExport(getSharedPreferences(Rule.PREF_WG_ROUTE, Context.MODE_PRIVATE), serializer);
serializer.endTag(null, Rule.PREF_WG_ROUTE);

serializer.startTag(null, "notify");
xmlExport(getSharedPreferences("notify", Context.MODE_PRIVATE), serializer);
serializer.endTag(null, "notify");
Expand Down Expand Up @@ -1435,6 +1445,7 @@ private void xmlImport(InputStream in) throws IOException, SAXException, ParserC
xmlImport(handler.application, prefs);
xmlImport(handler.apply, getSharedPreferences("apply", Context.MODE_PRIVATE));
xmlImport(handler.tracker_protect, getSharedPreferences("tracker_protect", Context.MODE_PRIVATE));
xmlImport(handler.wg_route, getSharedPreferences(Rule.PREF_WG_ROUTE, Context.MODE_PRIVATE));
xmlImport(handler.notify, getSharedPreferences("notify", Context.MODE_PRIVATE));
xmlImport(handler.blocklist, getSharedPreferences(PREF_BLOCKLIST, Context.MODE_PRIVATE));

Expand Down Expand Up @@ -1489,6 +1500,7 @@ private class XmlImportHandler extends DefaultHandler {
public Map<String, Object> roaming = new HashMap<>();
public Map<String, Object> apply = new HashMap<>();
public Map<String, Object> tracker_protect = new HashMap<>();
public Map<String, Object> wg_route = new HashMap<>();
public Map<String, Object> notify = new HashMap<>();
public Map<String, Object> blocklist = new HashMap<>();
private Map<String, Object> current = null;
Expand Down Expand Up @@ -1528,6 +1540,8 @@ else if (qName.equals("apply"))

else if (qName.equals("tracker_protect"))
current = tracker_protect;
else if (qName.equals(Rule.PREF_WG_ROUTE))
current = wg_route;

else if (qName.equals("notify"))
current = notify;
Expand Down
29 changes: 29 additions & 0 deletions app/src/main/java/eu/faircode/netguard/Rule.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import net.kollnig.missioncontrol.R;
import net.kollnig.missioncontrol.data.Pair;
import net.kollnig.missioncontrol.data.BlockingMode;
import net.kollnig.missioncontrol.data.RemoteRoutingLogic;
import net.kollnig.missioncontrol.data.TrackerBlocklist;
import net.kollnig.missioncontrol.data.TrackerList;

Expand All @@ -59,6 +60,10 @@

public class Rule {
private static final String TAG = "TrackerControl.Rule";
/** Per-package remote-routing overrides, mirroring "apply"/"tracker_protect". */
public static final String PREF_WG_ROUTE = "wg_route";
/** Global remote-routing mode, in the default shared preferences. */
public static final String PREF_WG_ROUTE_MODE = "wg_route_mode";

public int uid;
public String packageName;
Expand All @@ -84,6 +89,11 @@ public class Rule {

public boolean apply = true; // If false, completely exclude from VPN (no DNS, no routing)
public boolean tracker_protect = true; // If false, don't block trackers (but still route through VPN)
// Whether this app's traffic is forwarded through the remote VPN. Filtering
// and remote routing are independent choices (#723): an app can be fully
// monitored and blocked while still connecting from the device's own
// network. Resolved from the global mode plus an optional per-app override.
public boolean wg_route = true;
public boolean notify = true;

public boolean relateduids = false;
Expand Down Expand Up @@ -301,7 +311,10 @@ public static List<Rule> getRules(final boolean all, boolean self, Context conte
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences apply = context.getSharedPreferences("apply", Context.MODE_PRIVATE);
SharedPreferences tracker_protect = context.getSharedPreferences("tracker_protect", Context.MODE_PRIVATE);
SharedPreferences wg_route = context.getSharedPreferences(PREF_WG_ROUTE, Context.MODE_PRIVATE);
SharedPreferences notify = context.getSharedPreferences("notify", Context.MODE_PRIVATE);
String wgRouteMode = RemoteRoutingLogic.normalizeMode(
prefs.getString(PREF_WG_ROUTE_MODE, RemoteRoutingLogic.getDefaultMode()));

// Get settings
boolean manage_system = prefs.getBoolean("manage_system", false);
Expand Down Expand Up @@ -417,6 +430,12 @@ public static List<Rule> getRules(final boolean all, boolean self, Context conte
rule.apply = apply.getBoolean(info.packageName, true);
rule.tracker_protect = BlockingMode.isTrackerProtectionEnabled(
context, tracker_protect, info.packageName);
rule.wg_route = RemoteRoutingLogic.routesThroughTunnel(
wgRouteMode,
wg_route.contains(info.packageName)
? wg_route.getBoolean(info.packageName, true)
: null,
rule.apply);
rule.notify = notify.getBoolean(info.packageName, true);

// Related packages
Expand Down Expand Up @@ -557,7 +576,11 @@ public static List<Rule> getSearchStubs(Context context) {

SharedPreferences apply = context.getSharedPreferences("apply", Context.MODE_PRIVATE);
SharedPreferences tracker_protect = context.getSharedPreferences("tracker_protect", Context.MODE_PRIVATE);
SharedPreferences wg_route = context.getSharedPreferences(PREF_WG_ROUTE, Context.MODE_PRIVATE);
SharedPreferences notify = context.getSharedPreferences("notify", Context.MODE_PRIVATE);
String wgRouteMode = RemoteRoutingLogic.normalizeMode(
PreferenceManager.getDefaultSharedPreferences(context)
.getString(PREF_WG_ROUTE_MODE, RemoteRoutingLogic.getDefaultMode()));

DatabaseHelper dh = DatabaseHelper.getInstance(context);
for (PackageInfo info : getPackages(context))
Expand All @@ -570,6 +593,12 @@ public static List<Rule> getSearchStubs(Context context) {
rule.apply = apply.getBoolean(info.packageName, true);
rule.tracker_protect = BlockingMode.isTrackerProtectionEnabled(
context, tracker_protect, info.packageName);
rule.wg_route = RemoteRoutingLogic.routesThroughTunnel(
wgRouteMode,
wg_route.contains(info.packageName)
? wg_route.getBoolean(info.packageName, true)
: null,
rule.apply);
rule.notify = notify.getBoolean(info.packageName, true);
rule.updateChanged();
listStub.add(rule);
Expand Down
136 changes: 135 additions & 1 deletion app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
import net.kollnig.missioncontrol.data.BlockingMode;
import net.kollnig.missioncontrol.data.BlockingModeLogic;
import net.kollnig.missioncontrol.data.InternetBlocklist;
import net.kollnig.missioncontrol.data.RemoteRoutingLogic;
import net.kollnig.missioncontrol.data.Tracker;
import net.kollnig.missioncontrol.data.TrackerBlocklist;
import net.kollnig.missioncontrol.data.TrackerList;
Expand All @@ -111,10 +112,12 @@
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
Expand Down Expand Up @@ -186,6 +189,16 @@ public class ServiceSinkhole extends VpnService {
private static final Map<Network, Long> mapValidated = new ConcurrentHashMap<>();
private Map<Integer, Boolean> mapUidAllowed = new HashMap<>();
private Map<Integer, Integer> mapUidKnown = new HashMap<>();
// UIDs whose routing differs from the global default. Pushed down to the
// packet path as a sorted array; unlisted UIDs follow the global default.
private Set<Integer> mapUidRouteOverride = new HashSet<>();
private boolean defaultTunnel = true;
// Resolver used by apps routed around the remote tunnel. Null when the
// system offers none, in which case their DNS stays in the tunnel.
private String directDnsTarget = null;
// Whether any app is routed around the tunnel, which is what makes the
// extra per-query DNS cost worth paying.
private boolean anyDirectRouting = false;
private final Map<IPKey, Map<InetAddress, IPRule>> mapUidIPFilters = new HashMap<>();
private Map<Integer, Forward> mapForward = new HashMap<>();
public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
Expand Down Expand Up @@ -297,6 +310,16 @@ public enum Command {
* dropped instead of forwarded directly, so restarts never leak. */
private native void jni_wireguard_required(boolean required);

/**
* Pushes the set of UIDs whose routing differs from the global default down
* to the packet path, plus that default (applied to every UID not in the
* set — unknown UIDs and system traffic included) and whether direct apps'
* DNS is redirected to the system resolver rather than always taking the
* tunnel. Java writes this while the tunnel thread reads it, so the native
* side guards it with its own lock.
*/
private native void jni_wireguard_route(int[] overrideUids, boolean defaultTunnel, boolean dnsDirect);

private native void jni_done(long context);

public static void setPcap(boolean enabled, Context context) {
Expand Down Expand Up @@ -1851,6 +1874,17 @@ private boolean startNative(final ParcelFileDescriptor vpn, List<Rule> listAllow

int prio = Integer.parseInt(prefs.getString("loglevel", Integer.toString(Log.WARN)));
final int rcode = Integer.parseInt(prefs.getString("rcode", "3"));
// Port 53 is the only UDP traffic that skips the UID lookup entirely
// today, so turning fwd53 on costs a lookup, a JNI upcall and a real
// UDP session per query. Only pay that when some app is routed around
// the tunnel and a non-null underlying resolver target is available —
// which are both properties of the resolved rules, not of the mode: a
// per-app override sends an app direct in either mode, and gating on
// the mode alone left such an app tunnelling its DNS while its traffic
// went direct, exactly the split the redirect exists to avoid.
final boolean fwd53 = mapForward.containsKey(53)
|| RemoteRoutingLogic.redirectDirectDns(anyDirectRouting,
directDnsTarget != null);
if (prefs.getBoolean("socks5_enabled", false))
jni_socks5(
prefs.getString("socks5_addr", ""),
Expand All @@ -1861,6 +1895,7 @@ private boolean startNative(final ParcelFileDescriptor vpn, List<Rule> listAllow
jni_socks5("", 0, "", "");

jni_sni(prefs.getBoolean("sni_enabled", false));
pushRoutingToNative();
updateUnderlyingNetworks();

// WireGuard egress. startOrUpdate is idempotent: same config +
Expand Down Expand Up @@ -1898,7 +1933,7 @@ private boolean startNative(final ParcelFileDescriptor vpn, List<Rule> listAllow
@Override
public void run() {
Log.i(TAG, "Running tunnel context=" + jni_context);
jni_run(jni_context, vpn.getFd(), mapForward.containsKey(53), rcode);
jni_run(jni_context, vpn.getFd(), fwd53, rcode);
Log.i(TAG, "Tunnel exited");
tunnelThread = null;
}
Expand Down Expand Up @@ -1982,9 +2017,100 @@ private void prepareUidAllowed(List<Rule> listAllowed, List<Rule> listRule) {
for (Rule rule : listRule)
mapUidKnown.put(rule.uid, rule.uid);

defaultTunnel = RemoteRoutingLogic.defaultTunnel(
RemoteRoutingLogic.normalizeMode(
PreferenceManager.getDefaultSharedPreferences(this)
.getString(Rule.PREF_WG_ROUTE_MODE,
RemoteRoutingLogic.getDefaultMode())));
mapUidRouteOverride = mapUidRouteOverride(listRule, defaultTunnel);
anyDirectRouting = anyDirectRouting(listRule);
// Util.getDefaultDNS does binder calls, so resolve it once per reload
// rather than on the DNS path.
directDnsTarget = resolveDirectDnsTarget();

lock.writeLock().unlock();
}

/**
* The UIDs whose routing differs from the global default.
* <p>
* A shared UID is tunnelled if any of its packages is: the packet path only
* ever sees the UID, so the finer per-package answer cannot be honoured and
* the more private of the two is the right way to round.
* <p>
* Bypassed apps are skipped rather than counted as direct. They are handed
* to {@code addDisallowedApplication}, so no packet of theirs ever reaches
* the tun — listing them would put an entry in the override set for every
* bypassed app and defeat the empty-set fast path for no benefit.
*/
private static Set<Integer> mapUidRouteOverride(List<Rule> listRule, boolean defaultTunnel) {
Map<Integer, Boolean> tunnelByUid = new HashMap<>();
for (Rule rule : listRule) {
if (!rule.apply)
continue;
Boolean current = tunnelByUid.get(rule.uid);
tunnelByUid.put(rule.uid, (current != null && current) || rule.wg_route);
}

Set<Integer> overrides = new HashSet<>();
for (Map.Entry<Integer, Boolean> entry : tunnelByUid.entrySet())
if (RemoteRoutingLogic.isRouteOverride(entry.getValue(), defaultTunnel))
overrides.add(entry.getKey());
return overrides;
}

/**
* The system resolver for apps routed around the remote tunnel.
* <p>
* With the tunnel up, the tun advertises the tunnel's own resolvers, which
* a direct app may not be able to reach at all. Redirecting its queries to
* the underlying network's resolver is what makes direct routing work when
* the tunnel drops — at the cost, stated in the UI, of that app's DNS being
* visible to its local network rather than to the VPN provider.
*/
private String resolveDirectDnsTarget() {
List<String> sysDns = Util.getDefaultDNS(ServiceSinkhole.this);
for (String dns : sysDns)
if (!TextUtils.isEmpty(dns))
return dns;

Log.w(TAG, "No system DNS for direct apps; keeping their DNS in the tunnel");
return null;
}

private static boolean anyDirectRouting(List<Rule> listRule) {
for (Rule rule : listRule)
if (rule.apply && !rule.wg_route)
return true;
return false;
}

/**
* Whether this UID is routed around the remote tunnel. Mirrors the native
* is_tunnel_uid: a UID absent from the override set follows the global
* default rather than counting as "not tunnelled", which a bare
* mapUidRouteOverride lookup would wrongly report.
*/
private boolean routesDirect(int uid) {
return RemoteRoutingLogic.routesDirect(
mapUidRouteOverride.contains(uid), defaultTunnel);
}

/** Hands the current routing decision to the packet path. */
private void pushRoutingToNative() {
lock.readLock().lock();
int[] uids = new int[mapUidRouteOverride.size()];
int i = 0;
for (Integer uid : mapUidRouteOverride)
uids[i++] = uid;
boolean defaults = defaultTunnel;
boolean dnsDirect = RemoteRoutingLogic.redirectDirectDns(anyDirectRouting,
directDnsTarget != null);
lock.readLock().unlock();

jni_wireguard_route(uids, defaults, dnsDirect);
}

public static void prepareHostsBlocked(Context c) {
BufferedReader br = null;
InputStreamReader is = null;
Expand Down Expand Up @@ -2393,6 +2519,13 @@ private Allowed isAddressAllowed(Packet packet) {
allowed = new Allowed(fwd.raddr, fwd.rport);
packet.data = "> " + fwd.raddr + "/" + fwd.rport;
}
} else if (packet.dport == 53 && directDnsTarget != null
&& routesDirect(packet.uid)) {
// An app routed around the remote tunnel resolves against the
// underlying network instead of the tunnel's resolver. The
// redirect is transparent: replies are rebuilt from the
// session's original addresses, so the app never sees it.
allowed = new Allowed(directDnsTarget, 53);
} else
allowed = new Allowed();

Expand Down Expand Up @@ -2919,6 +3052,7 @@ private void handlePackageChanged(Context context, Intent intent) {
context.getSharedPreferences("apply", Context.MODE_PRIVATE).edit().remove(packageName).apply();
BlockingMode.clearAutoExcludedApp(context, packageName);
context.getSharedPreferences("tracker_protect", Context.MODE_PRIVATE).edit().remove(packageName).apply();
context.getSharedPreferences(Rule.PREF_WG_ROUTE, Context.MODE_PRIVATE).edit().remove(packageName).apply();
context.getSharedPreferences("notify", Context.MODE_PRIVATE).edit().remove(packageName).apply();

int uid = intent.getIntExtra(Intent.EXTRA_UID, 0);
Expand Down
Loading