From 0df840da815ed1ed23f0d9b3fba6b7452b0989d7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 19 Sep 2026 20:02:00 +0000 Subject: [PATCH] Compare search domains and excluded routes in Builder.equals Builder.equals decides whether reload() replaces the live tun or takes the "Native restart" shortcut, so any field it does not capture cannot reach a running interface. Two were still missing after #960 fixed the metered flag: - search domains (addSearchDomain); - the carrier ePDG exclusions (excludeRoute, API 33+). These are re-resolved on every rebuild behind a 1.5s timeout, so a first establish whose lookup timed out was never replaced by a later rebuild that resolved them, and Wi-Fi calling stayed broken until some unrelated change forced a real replacement. Both are now recorded by overriding the corresponding Builder methods and compared like the existing lists. Also: two offline builders (both networkInfo null) now compare equal instead of forcing a needless replacement on every reload while there is no active network, and the cast to Builder is guarded by an instanceof check rather than relying on a following null check. The extra interface replacements this produces go through VpnReplacementSequencer, which was not in place when the issue was first triaged. Refs #763 Claude-Session: https://claude.ai/code/session_016eKiHiWiQwMQosDXj8qY4s --- .../eu/faircode/netguard/ServiceSinkhole.java | 57 +++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java index fc8604a14..806a8a2b0 100644 --- a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java +++ b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java @@ -4822,6 +4822,8 @@ private class Builder extends VpnService.Builder { private int mtu; private boolean metered = true; private List listAddress = new ArrayList<>(); + private List listSearchDomain = new ArrayList<>(); + private List listExcludedRoute = new ArrayList<>(); private List listRoute = new ArrayList<>(); private List listDns = new ArrayList<>(); private List listDisallowed = new ArrayList<>(); @@ -4881,6 +4883,32 @@ public Builder addDnsServer(InetAddress address) { return this; } + /** + * Recorded for the comparison below (#763): a changed search domain only + * reaches apps through {@link VpnService.Builder#establish()}. + */ + @Override + public Builder addSearchDomain(String domain) { + listSearchDomain.add(domain); + super.addSearchDomain(domain); + return this; + } + + /** + * Recorded for the comparison below (#763). The ePDG exclusions are + * re-resolved on every rebuild behind a short timeout, so a rebuild that + * resolves them after an earlier one timed out must replace the live + * interface — otherwise Wi-Fi calling stays broken until some unrelated + * change forces a real replacement. + */ + @RequiresApi(api = Build.VERSION_CODES.TIRAMISU) + @Override + public Builder excludeRoute(IpPrefix prefix) { + listExcludedRoute.add(prefix.toString()); + super.excludeRoute(prefix); + return this; + } + /** * Excludes apps, such as system apps if disabled, as well as deactivated apps * by user @@ -4898,16 +4926,21 @@ public Builder addDisallowedApplication(String packageName) throws PackageManage @Override public boolean equals(Object obj) { - Builder other = (Builder) obj; - - if (other == null) + if (!(obj instanceof Builder)) return false; + Builder other = (Builder) obj; + if (!Objects.equals(this.activeNetwork, other.activeNetwork)) return false; - if (this.networkInfo == null || other.networkInfo == null || - this.networkInfo.getType() != other.networkInfo.getType()) + // Two offline builders are equal: treating both-null as different + // forced a needless interface replacement on every reload while + // there was no active network at all. + if (this.networkInfo == null || other.networkInfo == null) { + if (this.networkInfo != other.networkInfo) + return false; + } else if (this.networkInfo.getType() != other.networkInfo.getType()) return false; if (this.mtu != other.mtu) @@ -4928,6 +4961,12 @@ public boolean equals(Object obj) { if (this.listDisallowed.size() != other.listDisallowed.size()) return false; + if (this.listSearchDomain.size() != other.listSearchDomain.size()) + return false; + + if (this.listExcludedRoute.size() != other.listExcludedRoute.size()) + return false; + for (String address : this.listAddress) if (!other.listAddress.contains(address)) return false; @@ -4944,6 +4983,14 @@ public boolean equals(Object obj) { if (!other.listDisallowed.contains(pkg)) return false; + for (String domain : this.listSearchDomain) + if (!other.listSearchDomain.contains(domain)) + return false; + + for (String route : this.listExcludedRoute) + if (!other.listExcludedRoute.contains(route)) + return false; + return true; } }