diff --git a/app/src/main/java/dev/ukanth/ufirewall/Api.java b/app/src/main/java/dev/ukanth/ufirewall/Api.java index 2e423851c..34a6f9359 100644 --- a/app/src/main/java/dev/ukanth/ufirewall/Api.java +++ b/app/src/main/java/dev/ukanth/ufirewall/Api.java @@ -218,8 +218,9 @@ public final class Api { private static final String[] dynChains = {"-3g-postcustom", "-3g-fork", "-wifi-postcustom", "-wifi-fork"}; private static final String[] natChains = {"", "-tor-check", "-tor-filter"}; private static final String[] staticChains = {"", "-input", "-3g", "-wifi", "-reject", "-vpn", "-3g-tether", "-3g-home", "-3g-roam", "-wifi-tether", "-wifi-wan", "-wifi-lan", "-usb-tether", "-tor", "-tor-reject", "-tether", "-3g-home-reject", "-3g-roam-reject", "-wifi-wan-reject", "-wifi-lan-reject", "-vpn-reject", "-tether-reject"}; - private static final String[] LOCAL_RESERVED_IPV4_RANGES = {"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16"}; - private static final String[] LOCAL_RESERVED_IPV6_RANGES = {"fc00::/7", "fe80::/10"}; + // LAN-selected apps also need discovery destinations such as mDNS, SSDP, and broadcast. + private static final String[] LOCAL_RESERVED_IPV4_RANGES = {"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", "224.0.0.0/4", "255.255.255.255/32"}; + private static final String[] LOCAL_RESERVED_IPV6_RANGES = {"fc00::/7", "fe80::/10", "ff00::/8"}; private static volatile boolean globalStatus = false; private static final Object GLOBAL_STATUS_LOCK = new Object(); @@ -744,7 +745,7 @@ private static void addChangedUidRules(List cmds, PackageInfoData app, b } if (G.enableTor()) { cmds.add("#NOCHK# -D " + chainName + "-tor-reject -m owner --uid-owner " + uid + " -j " + chainName + "-reject"); - if (app.selected_tor && (G.enableInbound() || ipv6)) { + if (app.selected_tor && ipv6) { cmds.add("-I " + chainName + "-tor-reject 1 -m owner --uid-owner " + uid + " -j " + chainName + "-reject"); } if (!ipv6) { @@ -760,19 +761,7 @@ private static void addRejectRules(List cmds, String chainName) { // set up reject chain to log or not log // this can be changed dynamically through the Firewall Logs activity - if (G.enableLogService()) { - if (G.logTarget().trim().equals("LOG")) { - //cmds.add("-A " + chainName + " -m limit --limit 1000/min -j LOG --log-prefix \"{AFL-ALLOW}\" --log-level 4 --log-uid"); - String logRule = "-A " + chainName + "-reject" + " -m limit --limit 1000/min -j LOG --log-prefix \"{AFL}\" --log-level 4 --log-uid --log-tcp-options --log-ip-options"; - Log.d(TAG, "Adding LOG rule to reject chain: " + logRule); - cmds.add(logRule); - } else if (G.logTarget().trim().equals("NFLOG")) { - //cmds.add("-A " + chainName + " -j NFLOG --nflog-prefix \"{AFL-ALLOW}\" --nflog-group 40"); - String nflogRule = "-A " + chainName + "-reject" + " -j NFLOG --nflog-prefix \"{AFL}\" --nflog-group 40"; - Log.d(TAG, "Adding NFLOG rule to reject chain: " + nflogRule); - cmds.add(nflogRule); - } - } + addLogRuleForRejectChain(cmds, chainName + "-reject"); String rejectRule = "-A " + chainName + "-reject" + " -j REJECT"; Log.d(TAG, "Adding final REJECT rule: " + rejectRule); cmds.add(rejectRule); @@ -783,35 +772,58 @@ private static void addRejectRules(List cmds, String chainName) { for (String suffix : rejectChainSuffixes) { String individualRejectChain = chainName + suffix; Log.d(TAG, "Populating individual reject chain: " + individualRejectChain); - if (G.enableLogService() && G.logTarget().trim().equals("NFLOG")) { - String nflogRule = "-A " + individualRejectChain + " -j NFLOG --nflog-prefix \"{AFL}\" --nflog-group 40"; - Log.d(TAG, "Adding NFLOG to individual reject chain: " + nflogRule); - cmds.add(nflogRule); - } + addLogRuleForRejectChain(cmds, individualRejectChain); String individualRejectRule = "-A " + individualRejectChain + " -j REJECT"; Log.d(TAG, "Adding REJECT to individual reject chain: " + individualRejectRule); cmds.add(individualRejectRule); } } + private static void addLogRuleForRejectChain(List cmds, String rejectChain) { + if (!G.enableLogService()) { + return; + } + String logTarget = G.logTarget().trim(); + if (logTarget.equals("LOG")) { + // Whitelist mode uses per-interface reject chains, so LOG must be + // added anywhere packets can be rejected, not only the shared chain. + String logRule = "-A " + rejectChain + " -m limit --limit 1000/min -j LOG --log-prefix \"{AFL}\" --log-level 4 --log-uid --log-tcp-options --log-ip-options"; + Log.d(TAG, "Adding LOG rule to reject chain: " + logRule); + cmds.add(logRule); + } else if (logTarget.equals("NFLOG")) { + String nflogRule = "-A " + rejectChain + " -j NFLOG --nflog-prefix \"{AFL}\" --nflog-group 40"; + Log.d(TAG, "Adding NFLOG rule to reject chain: " + nflogRule); + cmds.add(nflogRule); + } + } + private static void addTorRules(List cmds, List uids, Boolean whitelist, Boolean ipv6, String chainName) { + Integer socks_port = 9050; + Integer http_port = 8118; + Integer dns_port = 5400; + Integer tcp_port = 9040; + + Log.i(TAG, "Adding Tor redirect rules before interface filters"); + // Tor selection is an outbound owner match; jumping from INPUT breaks on several iptables backends. + for (Integer uid : uids) { if (uid != null && uid >= 0) { - if (G.enableInbound() || ipv6) { + if (ipv6) { cmds.add("-A " + chainName + "-tor-reject -m owner --uid-owner " + uid + " -j " + chainName + "-reject"); } if (!ipv6) { cmds.add("-t nat -A " + chainName + "-tor-check -m owner --uid-owner " + uid + " -j " + chainName + "-tor-filter"); + // Tor rules run before interface chains so redirected traffic is not rejected as plain Wi-Fi/mobile. + cmds.add("-A " + chainName + "-tor -m owner --uid-owner " + uid + " -d 127.0.0.1 -p tcp --dport " + socks_port + " -j ACCEPT"); + cmds.add("-A " + chainName + "-tor -m owner --uid-owner " + uid + " -d 127.0.0.1 -p tcp --dport " + http_port + " -j ACCEPT"); + cmds.add("-A " + chainName + "-tor -m owner --uid-owner " + uid + " -d 127.0.0.1 -p tcp --dport " + tcp_port + " -j ACCEPT"); + cmds.add("-A " + chainName + "-tor -m owner --uid-owner " + uid + " -d 127.0.0.1 -p udp --dport " + dns_port + " -j ACCEPT"); } } } if (ipv6) { cmds.add("-A " + chainName + " -j " + chainName + "-tor-reject"); } else { - Integer socks_port = 9050; - Integer http_port = 8118; - Integer dns_port = 5400; - Integer tcp_port = 9040; cmds.add("-t nat -A " + chainName + "-tor-filter -d 127.0.0.1 -p tcp --dport " + socks_port + " -j RETURN"); cmds.add("-t nat -A " + chainName + "-tor-filter -d 127.0.0.1 -p tcp --dport " + http_port + " -j RETURN"); cmds.add("-t nat -A " + chainName + "-tor-filter -p udp --dport 53 -j REDIRECT --to-ports " + dns_port); @@ -821,9 +833,6 @@ private static void addTorRules(List cmds, List uids, Boolean w cmds.add("-A " + chainName + "-tor -m mark --mark 0x500 -j " + chainName + "-reject"); cmds.add("-A " + chainName + " -j " + chainName + "-tor"); } - if (G.enableInbound()) { - cmds.add("-A " + chainName + "-input -j " + chainName + "-tor-reject"); - } } private static String sanitizeRule(String rule) { @@ -1176,9 +1185,20 @@ private static boolean applyIptablesRulesImpl(final Context ctx, RuleDataSet rul // custom rules in afwall-{3g,wifi,reject} supersede everything else addCustomRules(Api.PREF_CUSTOMSCRIPT, cmds, ipv6); + // Loopback is self-device traffic, not LAN or WAN. Keep it out of + // the LAN split chains so local app services continue to work when + // LAN control is enabled in either firewall mode. + cmds.add("-A " + chainName + " -o lo -j RETURN"); + if (G.enableInbound()) { + cmds.add("-A " + chainName + "-input -i lo -j RETURN"); + } + cmds.add("-A " + chainName + "-3g -j " + chainName + "-3g-postcustom"); cmds.add("-A " + chainName + "-wifi -j " + chainName + "-wifi-postcustom"); addRejectRules(cmds, chainName); + if (ipv6) { + addIpv6ControlTrafficRules(cmds, chainName); + } if (G.enableInbound()) { // we don't have any rules in the INPUT chain prohibiting inbound traffic, but @@ -1187,6 +1207,10 @@ private static boolean applyIptablesRulesImpl(final Context ctx, RuleDataSet rul cmds.add("-A " + chainName + "-input -m state --state ESTABLISHED -j RETURN"); } + if (G.enableTor()) { + addTorRules(cmds, ruleDataSet.torList, whitelist, ipv6, chainName); + } + addInterfaceRouting(ctx, cmds, ipv6, chainName, ruleDataSet); // send wifi, 3G, VPN packets to the appropriate dynamic chain based on interface @@ -1235,6 +1259,7 @@ private static boolean applyIptablesRulesImpl(final Context ctx, RuleDataSet rul if (containsUidOrAny(ruleDataSet.wifiList, SPECIAL_UID_TETHER)) { // DHCP replies to client addRuleForUsers(cmds, users_dhcp, "-A " + chainName + "-wifi-tether", "-p udp --sport=67 --dport=68" + action); + addTetherDhcpReplyRule(cmds, chainName + "-wifi-tether", action); // DNS replies to client addRuleForUsers(cmds, users_dns, "-A " + chainName + "-wifi-tether", "-p udp --sport=53" + action); addRuleForUsers(cmds, users_dns, "-A " + chainName + "-wifi-tether", "-p tcp --sport=53" + action); @@ -1245,6 +1270,7 @@ private static boolean applyIptablesRulesImpl(final Context ctx, RuleDataSet rul if (containsUidOrAny(ruleDataSet.wifiList, SPECIAL_UID_TETHER) || containsUidOrAny(ruleDataSet.tetherList, SPECIAL_UID_TETHER)) { // DHCP replies to USB tethered client addRuleForUsers(cmds, users_dhcp, "-A " + chainName + "-usb-tether", "-p udp --sport=67 --dport=68" + action); + addTetherDhcpReplyRule(cmds, chainName + "-usb-tether", action); // DNS replies to USB tethered client addRuleForUsers(cmds, users_dns, "-A " + chainName + "-usb-tether", "-p udp --sport=53" + action); addRuleForUsers(cmds, users_dns, "-A " + chainName + "-usb-tether", "-p tcp --sport=53" + action); @@ -1252,6 +1278,7 @@ private static boolean applyIptablesRulesImpl(final Context ctx, RuleDataSet rul if (containsUidOrAny(ruleDataSet.tetherList, SPECIAL_UID_TETHER)) { // DHCP replies to client addRuleForUsers(cmds, users_dhcp, "-A " + chainName + "-tether", "-p udp --sport=67 --dport=68" + action); + addTetherDhcpReplyRule(cmds, chainName + "-tether", action); // DNS replies to client addRuleForUsers(cmds, users_dns, "-A " + chainName + "-tether", "-p udp --sport=53" + action); addRuleForUsers(cmds, users_dns, "-A " + chainName + "-tether", "-p tcp --sport=53" + action); @@ -1308,9 +1335,6 @@ private static boolean applyIptablesRulesImpl(final Context ctx, RuleDataSet rul addRulesForUidlist(cmds, ruleDataSet.lanList, chainName + "-wifi-lan", whitelist); addRulesForUidlist(cmds, ruleDataSet.vpnList, chainName + "-vpn", whitelist); addRulesForUidlist(cmds, ruleDataSet.tetherList, chainName + "-tether", whitelist); - if (G.enableTor()) { - addTorRules(cmds, ruleDataSet.torList, whitelist, ipv6, chainName); - } cmds.add("-P OUTPUT ACCEPT"); } catch (Exception e) { @@ -1321,6 +1345,12 @@ private static boolean applyIptablesRulesImpl(final Context ctx, RuleDataSet rul return true; } + private static void addTetherDhcpReplyRule(List cmds, String chain, String action) { + // dnsmasq can run under device-specific app UIDs, so the special tethering entry + // must allow DHCP replies by port instead of relying only on a fixed UID list. + cmds.add("-A " + chain + " -p udp --sport=67 --dport=68" + action); + } + /** * Checks if a collection contains specified uid or {@code SPECIAL_UID_ANY} * @@ -1432,10 +1462,38 @@ private static void completeRootCommandFailure(Context ctx, RootCommand callback } } + private static RootCommand wrapApplyCompletionCallback(RootCommand callback) { + final RootCommand completionCallback = callback == null ? new RootCommand() : callback; + final RootCommand.Callback originalCallback = completionCallback.cb; + completionCallback.setCallback(new RootCommand.Callback() { + @Override + public void cbFunc(RootCommand state) { + try { + if (originalCallback != null) { + originalCallback.cbFunc(state); + } + } finally { + synchronized (GLOBAL_STATUS_LOCK) { + globalStatus = false; + setRulesUpToDate(state.exitCode == 0); + } + } + } + }); + return completionCallback; + } + + private static RootCommand newIntermediateApplyCommand(RootCommand finalCallback) { + return new RootCommand() + .setFailureToast(finalCallback.failureToast) + .setReopenShell(finalCallback.reopenShell); + } + public static void applySavedIptablesRules(Context ctx, boolean showErrors, RootCommand callback) { synchronized (GLOBAL_STATUS_LOCK) { if(!globalStatus) { globalStatus = true; + final RootCommand completionCallback = wrapApplyCompletionCallback(callback); try { Log.i(TAG, "Starting full firewall rules apply"); @@ -1446,37 +1504,57 @@ public static void applySavedIptablesRules(Context ctx, boolean showErrors, Root // Create thread-safe chain name for this execution final String chainName = getThreadSafeChainName(); - // Apply IPv4 rules first (sequentially) + // Apply IPv4 rules first. When IPv6 is enabled, wait for IPv4 + // completion before starting IPv6 so the apply dialog and final + // callback represent the entire ruleset, not only IPv4. try { Log.i(TAG, "Applying IPv4 rules"); applyIptablesRulesImpl(ctx, dataSet, showErrors, ipv4cmds, false, chainName); - applySavedIp4tablesRules(ctx, ipv4cmds, callback); - Log.i(TAG, "Submitted IPv4 rule commands"); + if (G.enableIPv6()) { + final List finalIpv6cmds = ipv6cmds; + RootCommand ipv4Callback = newIntermediateApplyCommand(completionCallback) + .setCallback(new RootCommand.Callback() { + @Override + public void cbFunc(RootCommand state) { + if (state.exitCode != 0) { + completionCallback.cb.cbFunc(state); + return; + } + try { + Log.i(TAG, "Applying IPv6 rules"); + applyIptablesRulesImpl(ctx, dataSet, showErrors, finalIpv6cmds, true, chainName); + if (applySavedIp6tablesRules(ctx, finalIpv6cmds, completionCallback)) { + Log.i(TAG, "Submitted IPv6 rule commands"); + } else { + completeRootCommandFailure(ctx, completionCallback, "applySavedIp6tablesRules", null); + } + } catch (Exception e) { + Log.e(TAG, "Error applying IPv6 rules", e); + completeRootCommandFailure(ctx, completionCallback, "applySavedIp6tablesRules", e); + } + } + }); + if (applySavedIp4tablesRules(ctx, ipv4cmds, ipv4Callback)) { + Log.i(TAG, "Submitted IPv4 rule commands"); + } else { + completeRootCommandFailure(ctx, completionCallback, "applySavedIp4tablesRules", null); + } + } else { + if (applySavedIp4tablesRules(ctx, ipv4cmds, completionCallback)) { + Log.i(TAG, "Submitted IPv4 rule commands"); + } else { + completeRootCommandFailure(ctx, completionCallback, "applySavedIp4tablesRules", null); + } + } } catch (Exception e) { Log.e(TAG, "Error applying IPv4 rules", e); throw new RuntimeException(e); } - - // Apply IPv6 rules second (sequentially after IPv4) - if (G.enableIPv6()) { - try { - Log.i(TAG, "Applying IPv6 rules"); - applyIptablesRulesImpl(ctx, dataSet, showErrors, ipv6cmds, true, chainName); - applySavedIp6tablesRules(ctx, ipv6cmds, new RootCommand()); - Log.i(TAG, "Submitted IPv6 rule commands"); - } catch (Exception e) { - Log.e(TAG, "Error applying IPv6 rules", e); - throw new RuntimeException(e); - } - } - Log.i(TAG, "Submitted all firewall rule commands"); + Log.i(TAG, "Submitted firewall rule command sequence"); } catch (Exception e) { - completeRootCommandFailure(ctx, callback, "applySavedIptablesRules", e); - } finally { - globalStatus = false; - setRulesUpToDate(true); + completeRootCommandFailure(ctx, completionCallback, "applySavedIptablesRules", e); } } else { Log.w(TAG, "Full apply ignored because another apply is already running"); @@ -1842,6 +1920,17 @@ public static void purgeIptables(Context ctx, boolean showErrors, RootCommand ca /** * Add DNS-specific iptables rules for identified DNS servers instead of broad LAN access */ + private static void addIpv6ControlTrafficRules(List cmds, String chainName) { + // IPv6 connectivity depends on router and neighbor discovery before app UID rules match. + String[] icmpv6Types = {"133", "134", "135", "136"}; + for (String type : icmpv6Types) { + cmds.add("-A " + chainName + " -p ipv6-icmp --icmpv6-type " + type + " -j RETURN"); + if (G.enableInbound()) { + cmds.add("-A " + chainName + "-input -p ipv6-icmp --icmpv6-type " + type + " -j RETURN"); + } + } + } + private static void addDnsServerRules(List cmds, InterfaceDetails cfg, String chain, boolean ipv6) { String protocol = ipv6 ? "ip6tables" : "iptables"; java.util.List dnsServers = ipv6 ? cfg.dnsServersV6 : cfg.dnsServersV4; @@ -2118,6 +2207,7 @@ public static List getApps(Context ctx, GetAppList appList) { appList.doStageProgress(1); } List installed = pkgmanager.getInstalledApplications(pkgManagerFlags); + HashMap internetPermissionCache = new HashMap<>(); // On Android 11+ (API 30+), PackageManager may not return all apps without // QUERY_ALL_PACKAGES. Supplement using root shell "pm list packages -U" to discover @@ -2157,7 +2247,7 @@ public static List getApps(Context ctx, GetAppList appList) { } catch (NameNotFoundException e) { // PackageManager can't see this app (no QUERY_ALL_PACKAGES). // Check INTERNET permission via shell before adding. - if (uid >= 0 && hasInternetPermissionViaShell(pkg)) { + if (uid >= 0 && hasInternetPermissionViaShell(pkg, internetPermissionCache)) { ApplicationInfo ai = new ApplicationInfo(); ai.packageName = pkg; ai.uid = uid; @@ -2187,11 +2277,13 @@ public static List getApps(Context ctx, GetAppList appList) { SparseArray multiUserAppsMap = new SparseArray<>(); HashMap packagesForUser = new HashMap<>(); + HashMap profileMarkers = new HashMap<>(); if(G.supportDual()) { if (appList != null) { appList.doStageProgress(3); } packagesForUser = getPackagesForUser(listOfUids); + profileMarkers = getUserProfileMarkers(listOfUids); } if (appList != null) { @@ -2273,11 +2365,14 @@ public static List getApps(Context ctx, GetAppList appList) { applySelectedStates(app, selected_wifi, selected_3g, selected_roam, selected_vpn, selected_tether, selected_lan, selected_tor); if (G.supportDual()) { - checkPartOfMultiUser(apinfo, name, listOfUids, packagesForUser, multiUserAppsMap); + checkPartOfMultiUser(apinfo, name, listOfUids, packagesForUser, profileMarkers, multiUserAppsMap); } } if (G.supportDual()) { + addProfileOnlyPackages(pkgmanager, packagesForUser, profileMarkers, syncMap, + selected_wifi, selected_3g, selected_roam, selected_vpn, + selected_tether, selected_lan, selected_tor, internetPermissionCache); //run through multi user map for (int i = 0; i < multiUserAppsMap.size(); i++) { app = multiUserAppsMap.valueAt(i); @@ -2361,18 +2456,21 @@ public static List getSpecialData() { return specialData; } - private static void checkPartOfMultiUser(ApplicationInfo apinfo, String name, List uid1, HashMap pkgs, SparseArray syncMap) { + private static void checkPartOfMultiUser(ApplicationInfo apinfo, String name, List uid1, + HashMap pkgs, + HashMap profileMarkers, + SparseArray syncMap) { try { for (Integer integer : uid1) { - int appUid = Integer.parseInt(integer + "" + apinfo.uid + ""); + int appUid = UidResolver.createMultiUserUid(integer, UidResolver.getAppId(apinfo.uid)); try{ //String[] pkgs = pkgmanager.getPackagesForUid(appUid); if (packagesExistForUserUid(pkgs, appUid)) { PackageInfoData app = new PackageInfoData(); app.uid = appUid; - app.installTime = new File(apinfo.sourceDir).lastModified(); + app.installTime = getInstallTime(null, apinfo, apinfo.packageName); app.names = new ArrayList(); - app.names.add(name + "(M)"); + app.names.add(name + getProfileMarker(profileMarkers, integer)); app.appinfo = apinfo; if (app.appinfo != null && (app.appinfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { //user app @@ -2394,10 +2492,152 @@ private static void checkPartOfMultiUser(ApplicationInfo apinfo, String name, Li } private static boolean packagesExistForUserUid(HashMap pkgs, int appUid) { - if(pkgs.containsKey(appUid)){ - return true; + return pkgs != null && pkgs.containsKey(appUid); + } + + private static void addProfileOnlyPackages(PackageManager pkgmanager, + HashMap packagesForUser, + HashMap profileMarkers, + SparseArray syncMap, + Set selectedWifi, + Set selected3g, + Set selectedRoam, + Set selectedVpn, + Set selectedTether, + Set selectedLan, + Set selectedTor, + HashMap internetPermissionCache) { + if (packagesForUser == null || packagesForUser.isEmpty()) { + return; + } + int addedPackages = 0; + for (Map.Entry entry : packagesForUser.entrySet()) { + int uid = entry.getKey(); + String packageName = entry.getValue(); + if (syncMap.get(uid) != null || packageName == null || packageName.trim().length() == 0) { + continue; + } + if (!showAllApps() && !hasInternetPermission(pkgmanager, packageName, internetPermissionCache)) { + continue; + } + + ApplicationInfo apinfo = getApplicationInfoForPackage(pkgmanager, packageName, uid); + PackageInfoData app = new PackageInfoData(); + app.uid = uid; + app.installTime = getInstallTime(pkgmanager, apinfo, packageName); + app.names = new ArrayList(); + app.names.add(getApplicationLabel(pkgmanager, apinfo, packageName) + + getProfileMarker(profileMarkers, UidResolver.getUserId(uid))); + app.appinfo = apinfo; + app.appType = (apinfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0 ? 1 : 0; + app.pkgName = packageName; + applySelectedStates(app, selectedWifi, selected3g, selectedRoam, selectedVpn, + selectedTether, selectedLan, selectedTor); + syncMap.put(uid, app); + addedPackages++; + } + if (addedPackages > 0) { + Log.i(TAG, "Added " + addedPackages + " profile-only package(s) to app list"); } - return false; + } + + private static ApplicationInfo getApplicationInfoForPackage(PackageManager pkgmanager, + String packageName, + int uid) { + try { + ApplicationInfo apinfo = pkgmanager.getApplicationInfo(packageName, + PackageManager.GET_META_DATA | PackageManager.GET_UNINSTALLED_PACKAGES); + apinfo.uid = uid; + return apinfo; + } catch (Exception ignored) { + ApplicationInfo apinfo = new ApplicationInfo(); + apinfo.packageName = packageName; + apinfo.uid = uid; + // Profile-only packages can be invisible to PackageManager. Keep a + // minimal installed entry so rules can still target the pm-reported UID. + apinfo.flags = ApplicationInfo.FLAG_INSTALLED; + return apinfo; + } + } + + private static boolean hasInternetPermission(PackageManager pkgmanager, + String packageName, + HashMap internetPermissionCache) { + try { + if (PackageManager.PERMISSION_GRANTED == pkgmanager.checkPermission(Manifest.permission.INTERNET, packageName)) { + if (internetPermissionCache != null) { + internetPermissionCache.put(packageName, true); + } + return true; + } + } catch (Exception e) { + Log.w(TAG, "PackageManager permission check failed for " + packageName + ": " + e.getMessage()); + } + return hasInternetPermissionViaShell(packageName, internetPermissionCache); + } + + private static String getApplicationLabel(PackageManager pkgmanager, + ApplicationInfo apinfo, + String packageName) { + try { + return pkgmanager.getApplicationLabel(apinfo).toString(); + } catch (Exception ignored) { + return packageName; + } + } + + private static long getInstallTime(PackageManager pkgmanager, ApplicationInfo apinfo, String packageName) { + if (apinfo != null && apinfo.sourceDir != null) { + return new File(apinfo.sourceDir).lastModified(); + } + if (pkgmanager != null) { + try { + return pkgmanager.getPackageInfo(packageName, 0).firstInstallTime; + } catch (Exception ignored) { + } + } + return 0; + } + + private static HashMap getUserProfileMarkers(List userProfile) { + HashMap profileMarkers = new HashMap<>(); + for (Integer userId : userProfile) { + profileMarkers.put(userId, "(M)"); + } + try { + Shell.Result result = Shell.cmd("pm list users").exec(); + Pattern userInfoPattern = Pattern.compile("UserInfo\\{(\\d+):([^:}]*)"); + for (String line : result.getOut()) { + Matcher matcher = userInfoPattern.matcher(line); + if (matcher.find()) { + int userId = Integer.parseInt(matcher.group(1)); + if (profileMarkers.containsKey(userId)) { + profileMarkers.put(userId, markerForProfileName(matcher.group(2))); + } + } + } + } catch (Exception e) { + Log.w(TAG, "Failed to label user profiles: " + e.getMessage()); + } + return profileMarkers; + } + + private static String markerForProfileName(String profileName) { + String name = profileName == null ? "" : profileName.toLowerCase(Locale.US); + if (name.contains("work")) { + return "(W)"; + } + if (name.contains("private")) { + return "(P)"; + } + return "(M)"; + } + + private static String getProfileMarker(HashMap profileMarkers, int userId) { + if (profileMarkers != null && profileMarkers.containsKey(userId)) { + return profileMarkers.get(userId); + } + return "(M)"; } private static void applySelectedStates(PackageInfoData app, @@ -2425,15 +2665,17 @@ public static HashMap getPackagesForUser(List userProf Shell.Result result = Shell.cmd("pm list packages -U --user " + integer).exec(); List out = result.getOut(); Matcher matcher; + int userPackageCount = 0; for (String item : out) { matcher = dual_pattern.matcher(item); if (matcher.find() && matcher.groupCount() > 0) { String packageName = matcher.group(1); String packageId = matcher.group(2); - Log.i(TAG, packageId + " " + packageName); listApps.put(Integer.parseInt(packageId), packageName); + userPackageCount++; } } + Log.i(TAG, "Discovered " + userPackageCount + " package(s) for user " + integer); } catch (java.util.concurrent.RejectedExecutionException e) { Log.w(TAG, "Package listing rejected for user " + integer + ": " + e.getMessage()); break; // Stop processing other users if execution rejected @@ -2442,7 +2684,7 @@ public static HashMap getPackagesForUser(List userProf // Continue with next user on other errors } } - return listApps.size() > 0 ? listApps : null; + return listApps; } private static boolean isRecentlyInstalled(String packageName) { @@ -3996,17 +4238,41 @@ public static boolean isNetfilterSupported() { * Used for packages invisible to PackageManager due to package visibility restrictions. */ private static boolean hasInternetPermissionViaShell(String packageName) { + return hasInternetPermissionViaShell(packageName, null); + } + + private static boolean hasInternetPermissionViaShell(String packageName, + HashMap internetPermissionCache) { + if (internetPermissionCache != null && internetPermissionCache.containsKey(packageName)) { + return internetPermissionCache.get(packageName); + } try { - Shell.Result result = Shell.cmd("dumpsys package " + packageName + " | grep android.permission.INTERNET").exec(); + Shell.Result result = Shell.cmd("dumpsys package " + packageName).exec(); + if (!result.isSuccess()) { + Log.w(TAG, "dumpsys package failed while checking INTERNET permission for " + packageName); + if (internetPermissionCache != null) { + internetPermissionCache.put(packageName, false); + } + return false; + } List out = result.getOut(); + boolean hasPermission = false; for (String line : out) { if (line.contains("android.permission.INTERNET")) { - return true; + hasPermission = true; + break; } } + if (internetPermissionCache != null) { + internetPermissionCache.put(packageName, hasPermission); + } + return hasPermission; } catch (Exception e) { Log.w(TAG, "Failed to check INTERNET permission for " + packageName + ": " + e.getMessage()); } + if (internetPermissionCache != null) { + internetPermissionCache.put(packageName, false); + } return false; } @@ -4771,6 +5037,25 @@ public String toStringWithUID() { return tostr; } + public String toStringForList(boolean includeUid, boolean includePackageName) { + StringBuilder s = new StringBuilder(); + if (includeUid) { + s.append("[ "); + s.append(uid); + s.append(" ] "); + } + for (int i = 0; i < names.size(); i++) { + if (i != 0) s.append(", "); + s.append(names.get(i)); + } + if (includePackageName && pkgName != null && !pkgName.startsWith("dev.afwall.special.")) { + s.append("\n"); + s.append(pkgName); + } + s.append("\n"); + return s.toString(); + } + } public static void copySharedPreferences(SharedPreferences fromPreferences, SharedPreferences.Editor toEditor) { diff --git a/app/src/main/java/dev/ukanth/ufirewall/InterfaceTracker.java b/app/src/main/java/dev/ukanth/ufirewall/InterfaceTracker.java index 4164af6a5..8220d49fd 100644 --- a/app/src/main/java/dev/ukanth/ufirewall/InterfaceTracker.java +++ b/app/src/main/java/dev/ukanth/ufirewall/InterfaceTracker.java @@ -60,9 +60,9 @@ public final class InterfaceTracker { "wwan+", "cdma_rmnet+", "clat4+", "cc2mni+", "bond1+", "rmnet_smux+", "ccinet+", "v4-rmnet+", "seth_w+", "v4-rmnet_data+", "rmnet_ipa+", "rmnet_data+", "r_rmnet_data+"}; - public static final String[] ITFS_VPN = {"tun+", "ppp+", "tap+"}; + public static final String[] ITFS_VPN = {"tun+", "ppp+", "tap+", "wg+"}; - public static final String[] ITFS_TETHER = {"bt-pan", "usb+", "rndis+", "rmnet_usb+"}; + public static final String[] ITFS_TETHER = {"bt-pan", "bnep+", "usb+", "rndis+", "rmnet_usb+"}; public static final String BOOT_COMPLETED = "BOOT_COMPLETED"; public static final String CONNECTIVITY_CHANGE = "CONNECTIVITY_CHANGE"; diff --git a/app/src/main/java/dev/ukanth/ufirewall/MainActivity.java b/app/src/main/java/dev/ukanth/ufirewall/MainActivity.java index f880b282b..419b3f74d 100644 --- a/app/src/main/java/dev/ukanth/ufirewall/MainActivity.java +++ b/app/src/main/java/dev/ukanth/ufirewall/MainActivity.java @@ -160,6 +160,7 @@ public class MainActivity extends AppCompatActivity implements AdapterView.OnIte private TextWatcher filterTextWatcher; private MaterialDialog runProgress; private String currentSearchQuery = ""; + private boolean launchSecurityPassed = false; private BroadcastReceiver uiProgressReceiver4, uiProgressReceiver6, toastReceiver, themeRefreshReceiver, uiRefreshReceiver; private IntentFilter uiFilter4, uiFilter6; @@ -805,6 +806,12 @@ private void reloadPreferences() { setupMultiProfile(); } + if (isLaunchSecurityLocked()) { + concealProtectedContent(); + return; + } + revealProtectedContent(); + // Use async loading to avoid blocking the main thread // If the app list is already cached, filterApps will use the cache (fast path) // If not cached, showOrLoadApplications will load asynchronously with a progress dialog @@ -1118,6 +1125,10 @@ private void showApplications(final String searchStr) { break; } } + // Package names are stored separately from labels, so include them in search. + if (!matches && app.pkgName != null && app.pkgName.toLowerCase().contains(normalizedSearch)) { + matches = true; + } if (!matches && G.showUid()) { matches = String.valueOf(app.uid).contains(normalizedSearch); } @@ -1679,14 +1690,22 @@ private void takePersistablePermission(Intent data, int permissionFlag) { if (uri == null) { return; } - int flags = data.getFlags() & permissionFlag; - if (flags == 0) { - return; + int grantedFlags = data.getFlags(); + if ((permissionFlag & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0 + && (grantedFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) { + try { + getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + } catch (SecurityException e) { + Log.w(TAG, "Unable to persist picker read URI permission", e); + } } - try { - getContentResolver().takePersistableUriPermission(uri, flags); - } catch (SecurityException e) { - Log.w(TAG, "Unable to persist picker URI permission", e); + if ((permissionFlag & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0 + && (grantedFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) { + try { + getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } catch (SecurityException e) { + Log.w(TAG, "Unable to persist picker write URI permission", e); + } } } @@ -1786,7 +1805,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { case REQ_ENTER_PATTERN: { switch (resultCode) { case RESULT_OK: - showOrLoadApplications(); + onLaunchSecurityPassed(); break; default: MainActivity.this.finish(); @@ -3097,10 +3116,61 @@ private void runLaunchSecurityCheck() { Intent intent = getIntent(); if (intent != null && intent.getBooleanExtra(EXTRA_SKIP_SECURITY_CHECK_ON_RECREATE, false)) { intent.removeExtra(EXTRA_SKIP_SECURITY_CHECK_ON_RECREATE); + launchSecurityPassed = true; Log.i(Api.TAG, "Skipping security check for internal UI/theme refresh"); return; } - new SecurityUtil(MainActivity.this).passCheck(); + SecurityUtil securityUtil = new SecurityUtil(MainActivity.this, this::onLaunchSecurityPassed); + if (!securityUtil.isPasswordProtected()) { + onLaunchSecurityPassed(); + return; + } + concealProtectedContent(); + if (!securityUtil.passCheck()) { + onLaunchSecurityPassed(); + } + } + + private boolean isLaunchSecurityLocked() { + Intent intent = getIntent(); + if (intent != null && intent.getBooleanExtra(EXTRA_SKIP_SECURITY_CHECK_ON_RECREATE, false)) { + return false; + } + return !launchSecurityPassed && new SecurityUtil(MainActivity.this).isPasswordProtected(); + } + + private void onLaunchSecurityPassed() { + launchSecurityPassed = true; + reloadPreferences(); + } + + private void concealProtectedContent() { + // Launch security must hide cached app rows until auth succeeds. + if (listview != null) { + listview.setAdapter(null); + listview.setVisibility(View.INVISIBLE); + } + if (mSwipeLayout != null) { + mSwipeLayout.setEnabled(false); + } + setOptionalViewVisibility(R.id.filerOption, View.INVISIBLE); + setOptionalViewVisibility(R.id.profileOption, View.INVISIBLE); + } + + private void revealProtectedContent() { + if (listview != null) { + listview.setVisibility(View.VISIBLE); + } + if (mSwipeLayout != null) { + mSwipeLayout.setEnabled(true); + } + } + + private void setOptionalViewVisibility(int id, int visibility) { + View view = findViewById(id); + if (view != null) { + view.setVisibility(visibility); + } } @RequiresApi(28) diff --git a/app/src/main/java/dev/ukanth/ufirewall/log/LogInfo.java b/app/src/main/java/dev/ukanth/ufirewall/log/LogInfo.java index 36b0f7889..190db30df 100644 --- a/app/src/main/java/dev/ukanth/ufirewall/log/LogInfo.java +++ b/app/src/main/java/dev/ukanth/ufirewall/log/LogInfo.java @@ -292,13 +292,18 @@ public static LogInfo parseLogs(String result, final Context ctx, String pattern //system level packages try { if (!appNameMap.containsKey(uid)) { - appName = ctx.getPackageManager().getNameForUid(uid); + appName = null; for (PackageInfoData app : apps) { if (app.uid == uid) { appName = app.names.get(0); break; } } + // Android package visibility can hide packages from PackageManager. + // Fall back to shell-backed UID resolution before showing "Deleted App". + if (appName == null || appName.length() == 0) { + appName = UidResolver.resolveUid(ctx, uid); + } } else { appName = appNameMap.get(uid); } diff --git a/app/src/main/java/dev/ukanth/ufirewall/plugin/PluginBundleManager.java b/app/src/main/java/dev/ukanth/ufirewall/plugin/PluginBundleManager.java index 79c4be1ac..5de2b68ab 100644 --- a/app/src/main/java/dev/ukanth/ufirewall/plugin/PluginBundleManager.java +++ b/app/src/main/java/dev/ukanth/ufirewall/plugin/PluginBundleManager.java @@ -63,9 +63,18 @@ public static boolean isBundleValid(final Bundle bundle) return false; } /* - * Make sure the extra isn't null or empty + * Make sure the extra is a numeric action/profile id. Locale/Tasker callers can send + * arbitrary exported extras, so reject malformed values before parseInt() call sites. */ - return !TextUtils.isEmpty(bundle.getString(BUNDLE_EXTRA_STRING_MESSAGE)); + String message = bundle.getString(BUNDLE_EXTRA_STRING_MESSAGE); + if (TextUtils.isEmpty(message)) { + return false; + } + String index = message; + if (message.contains("::")) { + index = message.split("::", 2)[0]; + } + return !TextUtils.isEmpty(index) && TextUtils.isDigitsOnly(index); } /** @@ -89,4 +98,4 @@ private PluginBundleManager() { throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$ } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/preferences/PreferencesActivity.java b/app/src/main/java/dev/ukanth/ufirewall/preferences/PreferencesActivity.java index d6566e0e8..175eddb6b 100644 --- a/app/src/main/java/dev/ukanth/ufirewall/preferences/PreferencesActivity.java +++ b/app/src/main/java/dev/ukanth/ufirewall/preferences/PreferencesActivity.java @@ -341,7 +341,7 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin Context ctx = getApplicationContext(); boolean isRefreshRequired = false; - if (key.equals("showUid") || key.equals("disableIcons") || key.equals("enableVPN") + if (key.equals("showUid") || key.equals("showPackageName") || key.equals("disableIcons") || key.equals("enableVPN") || key.equals("enableTether") || key.equals("enableLAN") || key.equals("enableRoam") || key.equals("enableCustomRules") diff --git a/app/src/main/java/dev/ukanth/ufirewall/preferences/SecPreferenceFragment.java b/app/src/main/java/dev/ukanth/ufirewall/preferences/SecPreferenceFragment.java index 1caf20fac..93d6ae046 100644 --- a/app/src/main/java/dev/ukanth/ufirewall/preferences/SecPreferenceFragment.java +++ b/app/src/main/java/dev/ukanth/ufirewall/preferences/SecPreferenceFragment.java @@ -442,6 +442,9 @@ public void onInput(MaterialDialog dialog, CharSequence input) { itemList.setValueIndex(3); dialog.dismiss(); }); + dialog.setOnFingerprintUnavailable(() -> { + itemList.setValueIndex(0); + }); dialog.setOnFingerprintSuccess(() -> { G.isFingerprintEnabled(false); Api.toast(context, getString(R.string.fingerprint_disabled_successfully)); diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/AppListArrayAdapter.java b/app/src/main/java/dev/ukanth/ufirewall/util/AppListArrayAdapter.java index 4314248e3..31aca6e18 100644 --- a/app/src/main/java/dev/ukanth/ufirewall/util/AppListArrayAdapter.java +++ b/app/src/main/java/dev/ukanth/ufirewall/util/AppListArrayAdapter.java @@ -172,11 +172,7 @@ public View getView(final int position, View convertView, ViewGroup parent) { holder.app = listApps.get(position); - if (G.showUid()) { - holder.text.setText(holder.app.toStringWithUID()); - } else { - holder.text.setText(holder.app.toString()); - } + holder.text.setText(holder.app.toStringForList(G.showUid(), G.showPackageName())); final int id = holder.app.uid; final View finalConvertView = convertView; diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/BootRuleManager.java b/app/src/main/java/dev/ukanth/ufirewall/util/BootRuleManager.java index 418858b9f..0a1858807 100644 --- a/app/src/main/java/dev/ukanth/ufirewall/util/BootRuleManager.java +++ b/app/src/main/java/dev/ukanth/ufirewall/util/BootRuleManager.java @@ -48,6 +48,12 @@ public class BootRuleManager { public static void initializeBootRuleApplication(Context context) { synchronized (ruleApplicationLock) { Log.i(TAG, "Initializing boot rule application"); + + if (!shouldApplyBootRules(context)) { + Log.i(TAG, "Firewall disabled or inactive at boot; skipping boot rule application"); + markBootComplete(); + return; + } // Mark boot as in progress isBootInProgress.set(true); @@ -114,6 +120,11 @@ private static void scheduleDelayedBootRules(Context context) { Runnable delayedRules = () -> { synchronized (ruleApplicationLock) { if (isBootInProgress.get()) { + if (!shouldApplyBootRules(context)) { + Log.i(TAG, "Firewall disabled before delayed boot apply; skipping delayed rules"); + markBootComplete(); + return; + } Log.i(TAG, "Applying delayed boot rules"); try { // Force interface configuration refresh for delayed rules @@ -147,6 +158,12 @@ private static void cancelDelayedBootRules() { } delayedBootRulesScheduled.set(false); } + + private static boolean shouldApplyBootRules(Context context) { + // BootRuleManager calls applyBootRules directly, so keep the same enabled checks + // that protect normal connectivity-change rule application. + return Api.isEnabled(context) && G.activeRules(); + } /** * Mark boot process as complete @@ -214,4 +231,4 @@ public static String getBootState() { initialBootRulesApplied.get(), delayedBootRulesScheduled.get()); } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/FingerprintUtil.java b/app/src/main/java/dev/ukanth/ufirewall/util/FingerprintUtil.java index 7b3117b5b..06786de9c 100644 --- a/app/src/main/java/dev/ukanth/ufirewall/util/FingerprintUtil.java +++ b/app/src/main/java/dev/ukanth/ufirewall/util/FingerprintUtil.java @@ -89,6 +89,7 @@ public static class FingerprintDialog extends Dialog { // callbacks OnFingerprintFailure failureCallback; OnFingerprintSuccess successCallback; + OnFingerprintUnavailable unavailableCallback; @RequiresApi(api = Build.VERSION_CODES.M) public FingerprintDialog(Context context) { @@ -186,6 +187,10 @@ public void setOnFingerprintSuccess(OnFingerprintSuccess doSomething){ successCallback = doSomething; } + public void setOnFingerprintUnavailable(OnFingerprintUnavailable mayHappen){ + unavailableCallback = mayHappen; + } + /** * Created by whit3hawks on 11/16/16. * Modified by vzool on 1/14/17. @@ -200,19 +205,23 @@ void startReadFingerTip(){ * because we already checked if device support fingerprint before enable it. * We just leave it as-is for the days, who knows! :) */ - errorText.setText(R.string.device_with_no_fingerprint_sensor); + handleUnavailableFingerprint(R.string.device_with_no_fingerprint_sensor, + "Fingerprint hardware is not available"); }else { // Checks whether fingerprint permission is set on manifest if (ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED) { - errorText.setText(R.string.fingerprint_permission_manifest_missing); + handleUnavailableFingerprint(R.string.fingerprint_permission_manifest_missing, + "Fingerprint permission is missing"); }else{ // Check whether at least one fingerprint is registered if (!fingerprintManager.hasEnrolledFingerprints()) { - errorText.setText(R.string.register_at_least_one_fingerprint); + handleUnavailableFingerprint(R.string.register_at_least_one_fingerprint, + "No enrolled fingerprints are available"); }else{ // Checks whether lock screen security is enabled or not if (!keyguardManager.isKeyguardSecure()) { - errorText.setText(R.string.lock_screen_not_enabled); + handleUnavailableFingerprint(R.string.lock_screen_not_enabled, + "Lock screen security is not enabled"); }else{ generateKey(); @@ -227,6 +236,9 @@ void startReadFingerTip(){ } helper.startAuth(fingerprintManager, cryptoObject); + } else { + handleUnavailableFingerprint(R.string.fingerprint_security_changed, + "Fingerprint keystore key was invalidated"); } } } @@ -252,6 +264,24 @@ private void triggerSuccess(){ } } + private void handleUnavailableFingerprint(int messageResId, String reason) { + // Fingerprint enrollment or lock-screen changes invalidate the saved keystore key. + Log.e(TAG, reason); + G.isFingerprintEnabled(false); + if (errorText != null) { + errorText.setText(messageResId); + } + Api.toast(getContext(), getContext().getString(R.string.fingerprint_security_changed)); + if(isShowing()) { + dismiss(); + } + if(unavailableCallback != null){ + unavailableCallback.then(); + } else if(failureCallback != null){ + failureCallback.then(); + } + } + @TargetApi(Build.VERSION_CODES.M) private void generateKey() { try { @@ -304,6 +334,7 @@ private boolean cipherInit() { cipher.init(Cipher.ENCRYPT_MODE, key); return true; } catch (KeyPermanentlyInvalidatedException e) { + Log.e(TAG, "Fingerprint keystore key permanently invalidated", e); return false; } catch (KeyStoreException | CertificateException | UnrecoverableKeyException | IOException | NoSuchAlgorithmException | InvalidKeyException e) { throw new RuntimeException("Failed to init Cipher", e); @@ -394,4 +425,9 @@ public interface OnFingerprintFailure{ public interface OnFingerprintSuccess{ void then(); } + + // interface for callback when fingerprint setup is no longer usable + public interface OnFingerprintUnavailable{ + void then(); + } } diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/G.java b/app/src/main/java/dev/ukanth/ufirewall/util/G.java index d92f666a0..5eff0faba 100644 --- a/app/src/main/java/dev/ukanth/ufirewall/util/G.java +++ b/app/src/main/java/dev/ukanth/ufirewall/util/G.java @@ -34,6 +34,7 @@ import android.net.ConnectivityManager; import android.net.LinkProperties; import android.net.Network; +import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.os.Build; import android.os.Bundle; @@ -51,6 +52,7 @@ import java.io.File; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Matcher; @@ -157,6 +159,7 @@ public static Context getContext() { private static final String MULTI_USER = "multiUser"; private static final String MULTI_USER_ID = "multiUserId"; private static final String IS_MIGRATED = "isMigrated"; + private static final String SHOW_PACKAGE_NAME = "showPackageName"; private static final String SHOW_FILTER = "showFilter"; private static final String PATTERN_MAX_TRY = "patternMax"; private static final String PATTERN_STEALTH = "stealthMode"; @@ -628,6 +631,15 @@ public static boolean showUid(boolean val) { return val; } + public static boolean showPackageName() { + return gPrefs.getBoolean(SHOW_PACKAGE_NAME, false); + } + + public static boolean showPackageName(boolean val) { + gPrefs.edit().putBoolean(SHOW_PACKAGE_NAME, val).commit(); + return val; + } + public static boolean showFilter() { return gPrefs.getBoolean(SHOW_FILTER, false); } @@ -1559,6 +1571,8 @@ public static boolean getPrivateDnsStatus() { } private static ConnectivityManager.NetworkCallback callback = null; + private static final Object VPN_NETWORK_LOCK = new Object(); + private static final Set vpnNetworks = new HashSet<>(); public static void registerPrivateLink() { if(!enabledPrivateLink) { @@ -1569,13 +1583,37 @@ public static void registerPrivateLink() { public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) { super.onLinkPropertiesChanged(network, linkProperties); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - if(linkProperties.isPrivateDnsActive() != privateDns) { - Log.i(Api.TAG, "Private DNS status changed: " + privateDns); - privateDns = linkProperties.isPrivateDnsActive(); - InterfaceTracker.applyRules(getContext(), "Private DNS changed.. reapplying rules"); + boolean privateDnsActive = linkProperties.isPrivateDnsActive(); + if(privateDnsActive != privateDns) { + Log.i(Api.TAG, "Private DNS status changed: " + privateDnsActive); + privateDns = privateDnsActive; + scheduleNetworkCallbackApply("Private DNS changed", true); } } } + + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) { + super.onCapabilitiesChanged(network, networkCapabilities); + boolean isVpn = networkCapabilities != null + && networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN); + boolean wasVpn = updateTrackedVpnNetwork(network, isVpn); + if (isVpn || wasVpn) { + scheduleNetworkCallbackApply(isVpn + ? "VPN network capabilities changed" + : "VPN network capabilities removed", false); + } + } + + @Override + public void onLost(Network network) { + super.onLost(network); + if (removeTrackedVpnNetwork(network)) { + scheduleNetworkCallbackApply("VPN network lost", false); + } else { + Log.d(Api.TAG, "Non-VPN network lost; no VPN rule refresh needed"); + } + } }; } cm.registerNetworkCallback(new NetworkRequest.Builder().build(), callback); @@ -1585,6 +1623,45 @@ public void onLinkPropertiesChanged(Network network, LinkProperties linkProperti } } + private static boolean updateTrackedVpnNetwork(Network network, boolean isVpn) { + if (network == null) { + return false; + } + synchronized (VPN_NETWORK_LOCK) { + boolean wasVpn = vpnNetworks.contains(network); + if (isVpn) { + vpnNetworks.add(network); + } else { + vpnNetworks.remove(network); + } + return wasVpn; + } + } + + private static boolean removeTrackedVpnNetwork(Network network) { + if (network == null) { + return false; + } + synchronized (VPN_NETWORK_LOCK) { + return vpnNetworks.remove(network); + } + } + + private static void scheduleNetworkCallbackApply(String reason, boolean force) { + Context context = getContext(); + if (context == null || !Api.isEnabled(context) || !activeRules()) { + Log.d(TAG, reason + ": firewall inactive, not scheduling rule apply"); + return; + } + if (!force && !enableVPN()) { + Log.d(TAG, reason + ": VPN control is disabled, not scheduling rule apply"); + return; + } + Log.i(Api.TAG, reason + ", scheduling network rule refresh"); + // VPN connect/disconnect is not delivered through the legacy connectivity broadcast on all devices. + NetworkChangeDebouncer.scheduleNetworkChange(context, InterfaceTracker.CONNECTIVITY_CHANGE); + } + @Override public void onTerminate() { try { diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/PackageComparator.java b/app/src/main/java/dev/ukanth/ufirewall/util/PackageComparator.java index 26110f989..f664fa726 100644 --- a/app/src/main/java/dev/ukanth/ufirewall/util/PackageComparator.java +++ b/app/src/main/java/dev/ukanth/ufirewall/util/PackageComparator.java @@ -22,15 +22,37 @@ public int compare(Api.PackageInfoData o1, Api.PackageInfoData o2) { if (o1_selected == o2_selected) { switch (G.sortBy()) { case "s0": - return String.CASE_INSENSITIVE_ORDER.compare(o1.names.get(0), o2.names.get(0)); + return compareIdentity(o1, o2); case "s1": - return (o1.installTime > o2.installTime) ? -1: (o1.installTime < o2.installTime) ? 1 : 0; + int installCompare = (o1.installTime > o2.installTime) ? -1: (o1.installTime < o2.installTime) ? 1 : 0; + if (installCompare != 0) { + return installCompare; + } + // Dual/profile apps can share sort values; keep their order deterministic. + return compareIdentity(o1, o2); case "s2": - return Integer.compare(o2.uid, o1.uid); + int uidCompare = Integer.compare(o2.uid, o1.uid); + if (uidCompare != 0) { + return uidCompare; + } + // Dual/profile apps can share UID values; keep their order deterministic. + return compareIdentity(o1, o2); } } if (o1_selected) return -1; return 1; } + + private int compareIdentity(Api.PackageInfoData o1, Api.PackageInfoData o2) { + String firstName = (o1.names != null && !o1.names.isEmpty()) ? o1.names.get(0) : ""; + String secondName = (o2.names != null && !o2.names.isEmpty()) ? o2.names.get(0) : ""; + int nameCompare = String.CASE_INSENSITIVE_ORDER.compare(firstName, secondName); + if (nameCompare != 0) { + return nameCompare; + } + String firstPackage = o1.pkgName != null ? o1.pkgName : ""; + String secondPackage = o2.pkgName != null ? o2.pkgName : ""; + return String.CASE_INSENSITIVE_ORDER.compare(firstPackage, secondPackage); + } } diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/SecurityUtil.java b/app/src/main/java/dev/ukanth/ufirewall/util/SecurityUtil.java index 71cc501ce..22ee283ef 100644 --- a/app/src/main/java/dev/ukanth/ufirewall/util/SecurityUtil.java +++ b/app/src/main/java/dev/ukanth/ufirewall/util/SecurityUtil.java @@ -34,10 +34,16 @@ public class SecurityUtil { public static final int LOCK_VERIFICATION = 1212; private final Activity activity; + private final Runnable successCallback; public SecurityUtil(Activity activity) { + this(activity, null); + } + + public SecurityUtil(Activity activity, Runnable successCallback) { this.activity = activity; this.context = activity.getApplicationContext(); + this.successCallback = successCallback; } private void deviceCheck() { @@ -54,9 +60,11 @@ private void deviceCheck() { } } else { Toast.makeText(activity, context.getText(R.string.android_version), Toast.LENGTH_SHORT).show(); + notifyAuthSuccess(); } } else { Api.donateDialog(activity, true); + notifyAuthSuccess(); } } } @@ -72,6 +80,7 @@ public boolean passCheck() { case "p1": final String oldpwd = G.profile_pwd(); if (oldpwd.length() == 0) { + notifyAuthSuccess(); return true; } else { // Check the password @@ -81,6 +90,7 @@ public boolean passCheck() { case "p2": final String pwd = G.sPrefs.getString("LockPassword", ""); if (pwd.length() == 0) { + notifyAuthSuccess(); return true; } else { requestPassword(); @@ -118,6 +128,7 @@ private void requestFingerprint() { dialog.setOnFingerprintFailureListener(() -> { gracefulShutdown(); }); + dialog.setOnFingerprintSuccess(this::notifyAuthSuccess); dialog.show(); } @@ -127,6 +138,7 @@ private void requestFingerprintQ() { dialog.setOnFingerprintFailureListener(() -> { gracefulShutdown(); }); + dialog.setOnFingerprintSuccess(this::notifyAuthSuccess); dialog.show(); } @@ -154,6 +166,7 @@ private void requestPassword() { if (isAllowed) { dialog.dismiss(); + notifyAuthSuccess(); } else { Api.toast(activity, context.getString(R.string.wrong_password)); } @@ -199,4 +212,10 @@ private void gracefulShutdown() { activity.finish(); } } + + private void notifyAuthSuccess() { + if (successCallback != null) { + activity.runOnUiThread(successCallback); + } + } } diff --git a/app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidgetActivity.java b/app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidgetActivity.java index 995f5341d..0f1015c52 100644 --- a/app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidgetActivity.java +++ b/app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidgetActivity.java @@ -19,8 +19,11 @@ import java.util.ArrayList; import java.util.List; +import com.afollestad.materialdialogs.MaterialDialog; + import dev.ukanth.ufirewall.Api; import dev.ukanth.ufirewall.R; +import dev.ukanth.ufirewall.log.Log; import dev.ukanth.ufirewall.profiles.ProfileData; import dev.ukanth.ufirewall.profiles.ProfileHelper; import dev.ukanth.ufirewall.service.RootCommand; @@ -445,6 +448,33 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { private void startAction(final int i) { actionType = i; + if (i == 2 && G.enableConfirm()) { + confirmDisableFromWidget(); + return; + } + continueActionAfterConfirmation(); + } + + private void confirmDisableFromWidget() { + new MaterialDialog.Builder(this) + .title(R.string.confirmMsg) + .cancelable(false) + .positiveText(R.string.Yes) + .negativeText(R.string.No) + .onPositive((dialog, which) -> { + Log.i(Api.TAG, "Widget firewall disable confirmed"); + dialog.dismiss(); + continueActionAfterConfirmation(); + }) + .onNegative((dialog, which) -> { + Log.i(Api.TAG, "Widget firewall disable canceled"); + dialog.dismiss(); + finish(); + }) + .show(); + } + + private void continueActionAfterConfirmation() { SecurityUtil util = new SecurityUtil(ToggleWidgetActivity.this); boolean isProtected = util.isPasswordProtected(); if (!isProtected) { @@ -524,18 +554,20 @@ public void cbFunc(RootCommand state) { break; } if (actionType > 2) { - final Message msg = new Message(); Api.applySavedIptablesRules(context, true, new RootCommand() .setSuccessToast(R.string.rules_applied) .setFailureToast(R.string.error_apply) .setCallback(new RootCommand.Callback() { @Override public void cbFunc(RootCommand state) { + final Message msg = new Message(); if (state.exitCode == 0) { msg.arg1 = R.string.rules_applied; + Log.i(Api.TAG, "Widget profile rules applied"); } else { // error details are already in logcat msg.arg1 = R.string.error_apply; + Log.e(Api.TAG, "Widget profile rule apply failed"); } toaster.sendMessage(msg); } @@ -565,4 +597,4 @@ public void cbFunc(RootCommand state) { })); return ret; }*/ -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidgetOldActivity.java b/app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidgetOldActivity.java index 91d7bc8b0..549849a46 100644 --- a/app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidgetOldActivity.java +++ b/app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidgetOldActivity.java @@ -19,8 +19,11 @@ import java.util.List; +import com.afollestad.materialdialogs.MaterialDialog; + import dev.ukanth.ufirewall.Api; import dev.ukanth.ufirewall.R; +import dev.ukanth.ufirewall.log.Log; import dev.ukanth.ufirewall.profiles.ProfileData; import dev.ukanth.ufirewall.profiles.ProfileHelper; import dev.ukanth.ufirewall.service.RootCommand; @@ -30,12 +33,12 @@ public class ToggleWidgetOldActivity extends Activity implements OnClickListener { - private static Button enableButton; - private static Button disableButton; - private static Button defaultButton; - private static Button profButton1; - private static Button profButton2; - private static Button profButton3; + private Button enableButton; + private Button disableButton; + private Button defaultButton; + private Button profButton1; + private Button profButton2; + private Button profButton3; private String profileName; private int buttonId; @@ -202,6 +205,33 @@ public void onClick(View button) { profileName = ((Button) button).getText().toString(); buttonId = button.getId(); + if (buttonId == R.id.toggle_disable_firewall && G.enableConfirm()) { + confirmDisableFromWidget(); + return; + } + continueClickAfterConfirmation(); + } + + private void confirmDisableFromWidget() { + new MaterialDialog.Builder(this) + .title(R.string.confirmMsg) + .cancelable(false) + .positiveText(R.string.Yes) + .negativeText(R.string.No) + .onPositive((dialog, which) -> { + Log.i(Api.TAG, "Legacy widget firewall disable confirmed"); + dialog.dismiss(); + continueClickAfterConfirmation(); + }) + .onNegative((dialog, which) -> { + Log.i(Api.TAG, "Legacy widget firewall disable canceled"); + dialog.dismiss(); + finish(); + }) + .show(); + } + + private void continueClickAfterConfirmation() { SecurityUtil util = new SecurityUtil(ToggleWidgetOldActivity.this); boolean passCheck = util.isPasswordProtected(); if (!passCheck) { @@ -483,4 +513,4 @@ public void run() { }); } -} \ No newline at end of file +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2a439dc55..53a8575fe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -324,6 +324,9 @@ Show UID for apps Show app UID\'s in the main view Hide app UID\'s in the main view. Enabling can be useful to track logs. + Show package names for apps + Show app package names in the main view. + Hide app package names in the main view. Package names are still searchable. Languages Change Language @@ -612,6 +615,7 @@ Lock screen security not enabled in Settings Fingerprint Enabled Fingerprint Disabled + Fingerprint setup changed. Re-enable fingerprint protection in settings. Zoom In diff --git a/app/src/main/res/xml/ui_preferences.xml b/app/src/main/res/xml/ui_preferences.xml index eb045f6b9..58237cc70 100644 --- a/app/src/main/res/xml/ui_preferences.xml +++ b/app/src/main/res/xml/ui_preferences.xml @@ -80,6 +80,11 @@ android:summaryOff="@string/showuid_summary_off" android:summaryOn="@string/showuid_summary_on" android:title="@string/showUid" /> +