Add root DNS hijacker support#27
Conversation
|
Follow-up update for DNS Daemon Watchdog RecoveryThis hardens the root-side DNS daemon supervisor so crash recovery is easier to trust and easier to inspect. What was weak before:
What changed:
This moves the DNS filtering subsystem closer to the intended model where the Android app is only the management interface and the root service can continue supervising DNS filtering independently. |
|
Follow-up update for DNS Daemon Resource StatsThis adds native daemon resource accounting to the control socket responses used by Diagnostics. What was missing:
What changed:
This fills in the memory/CPU statistics portion of the service health requirements and gives diagnostics a better view of daemon resource usage over time. |
|
Follow-up update for DNS Query History And SearchThis adds persisted DNS query history access on top of the existing live daemon ring buffer. What was missing:
What changed:
This moves the logging side closer to the requested live plus historical query workflow while keeping DNS processing inside the native service and keeping the Android app as the management interface. |
|
Follow-up update for DNS Query Export From ViewerThis fills in the export piece for the DNS query viewer itself. What was missing:
What changed:
This completes the live/history/search/export loop for DNS query logs in the management UI while keeping DNS processing and persisted query data owned by the native daemon. |
|
Follow-up update for DNS Blocklist Paste ImportThis adds clipboard/manual paste as another DNS blocklist source. What was missing:
What changed:
This fills in the clipboard paste import source while preserving the existing atomic blocklist activation behavior. |
|
Improved DNS daemon listener reliability for Android IPv4/IPv6 capture. The daemon previously used a single IPv6 listener with What changed:
This makes readiness closer to the actual Android packet path: IPv4 redirects no longer depend on IPv6 dual-stack socket behavior, and missing IPv6 listener support is visible when IPv6 capture is expected. |
|
Follow-up pushed for the DNS service branch: Fixed nftables fallback restore generation. The previous fallback built each nft statement as a single quoted shell argument, and the chain definitions include braces and semicolons that need to be parsed by nft instead of the Android shell. On devices where iptables NAT is unavailable and the app falls back to nftables, that could prevent DNS redirect rules from being restored even though the service lifecycle work continued. The restore path now builds the nft statements as a small script and feeds them through |
|
Follow-up pushed for Android power handling around the DNS service: Added a DNS service power-settings row in the Rules DNS section. The dashboard/diagnostics already report that the root daemon watchdog runs outside the normal app process, but app-side work can still be affected by Android battery optimization. That includes scheduled blocklist updates and syncing root-side service logs back into the app diagnostics. The new row shows the current app power state when Android exposes it:
Clicking the row opens Android battery optimization settings, with a fallback to this app's settings page when the device does not expose the direct battery optimization screen. Each action path writes an application log entry, including unavailable settings, so this is visible later in diagnostics. |
|
Follow-up pushed for Android Private DNS compatibility: Added a DNS-section action to review Android Private DNS settings. The app already detected and logged that Private DNS/DoT can bypass root DNS capture because the redirect rules capture UDP/TCP port 53, not encrypted DNS transports. The missing piece was an actionable path from the DNS service controls. What changed:
This does not claim that encrypted DNS can be captured by the root redirect. It makes that Android limitation visible and gives the user a direct route to disable or review Private DNS when they need full port-53 DNS capture through the daemon. |
|
Follow-up pushed for DNS redirect backend reliability: Tightened the install and repair path so DNS capture is not treated as usable until one redirect backend is actually complete. What was wrong:
How it works now:
This makes the app less likely to report DNS protection as installed when traffic still cannot reach the daemon, especially on Android builds where iptables exists but NAT/REDIRECT behavior is incomplete or backed by nftables. |
|
Follow-up pushed for DNS boot restore behavior: Fixed boot persistence so it honors the app's IPv6 setting. The generated root boot script previously restored IPv4 and IPv6 DNS redirects every time it ran. That could re-install IPv6 DNS capture at boot even when AFWall+ IPv6 handling was disabled in the app, making boot-time behavior diverge from normal apply/repair behavior. What changed:
Cleanup remains defensive and can still remove stale IPv6 DNS redirect state; this only prevents disabled IPv6 capture from being reinstalled at boot. |
|
Follow-up pushed for DNS setting changes taking effect immediately: Fixed the active-service behavior when users change settings that affect root redirect rules. Previously, changes such as the local daemon port, captured/bypassed UIDs, or captured/bypassed interfaces only marked firewall rules stale. That meant the saved UI value could differ from the running daemon and installed NAT redirects until the user manually repaired or reapplied rules. What changed:
Daemon-only settings still use the lighter reload path. Redirect-affecting settings now use the stronger root repair path because they change NAT behavior and cannot be made effective by a daemon reload alone. |
|
Update pushed for the root DNS service branch. This adds a more reliable communication path from the root-owned DNS service back into AFWall's logs:
This push also fixes another redirect-policy edge case found during the audit:
|
|
Update pushed for DNS capture routing. This changes the daemon recursion bypass from a broad root UID bypass to a daemon socket mark path:
Why this matters: the prior UID 0 bypass protected the daemon from recursive capture, but it could also allow other root-owned/system DNS traffic to skip the DNS service. The mark-based bypass narrows that exception to the daemon's upstream sockets, which is closer to the intended Android behavior of capturing normal UDP/TCP port 53 DNS while still keeping the daemon functional. |
|
Follow-up pushed for the DNS mark routing path. The previous mark-based bypass narrowed the daemon recursion exception, but there was still one important usability gap: root rule install could test whether iptables accepted the This update adds a daemon-to-installer status contract:
This keeps the tighter mark-only routing when the device actually supports it, while preserving the previous compatibility fallback on devices where daemon-side socket marking is not available. |
|
Updated the root DNS service branch with a service-boundary fix for daemon upstream traffic and diagnostics. What changed:
Why this was needed: The service runs outside the Android app process as a root-owned daemon. Redirect recursion prevention alone was not enough: once the daemon sent DNS to an upstream resolver, those packets returned from the NAT bypass into AFWall's normal OUTPUT filtering. In whitelist or strict configurations, root-owned daemon packets could still be blocked. The fix makes daemon upstream traffic identifiable by the daemon socket mark and allows that marked traffic through the filter layer without widening capture for regular applications. |
|
Added another service-usability fix for root command failure visibility. What changed:
Why this matters: The DNS service depends on root operations outside the normal app process. If a cached root grant is revoked, an iptables/nft command fails, a generated script is not executable, or redirect verification fails, the app needs enough detail to explain what happened. Previously stderr could be lost, which made some failures look like a generic apply error. This keeps those failures visible in the base app log and in the existing DNS lifecycle error reporting without adding new UI surface. |
DNS capture, daemon reliability, diagnostics, logging, blocklists, profiles, and dashboard controls. I kept this as a draft because it is large enough that it will be easier to review and test in sections instead of treating it like a small patch.
This PR brings the root DNS hijacker work into the
betabranch for review. It adds a native DNS filtering daemon, root DNS traffic redirection, app-facing DNS controls, live query visibility, blocklist import/update handling, diagnostics, repair actions, profile-aware policy, per-app/per-network capture rules, cache tuning, SafeSearch enforcement, and several daemon-side performance and reliability improvements.Recommended Review Plan
Because this is a lot of change at once, the easiest way to manage it may be to keep this as a draft and review by subsystem. If you want an even cleaner integration path, a temporary branch such as
stupid-large-changesordns-integration-passwould let the work be merged and tested there first before it is promoted into the normal beta flow.A practical review order would be:
What Was Broken Or Missing
AFWall handled firewall rules, but DNS traffic still followed the normal resolver path. That meant the app could not reliably enforce domain-level allow/block policy, inspect DNS decisions, or prove that DNS traffic was actually passing through an AFWall-controlled root path.
The app also lacked a dedicated root daemon for DNS processing, daemon health reporting, one-click DNS repair, pause/resume/restart controls, live DNS query viewing, query-to-rule actions, scheduled DNS blocklist updates, split DNS routing, upstream benchmarking, and profile-specific DNS policy storage.
Large DNS rule sets needed stronger daemon behavior as well. The daemon now prepares rule storage, exact indexes, suffix tries, cache memory, and resolver routing before swapping the active generation. Failed reloads leave the last working DNS config running instead of partially replacing it.
Found Bugs And Reliability Fixes
Root DNS capture needed to be managed from the same firewall apply, purge, repair, and boot restore paths. The new manager flow starts or refreshes the daemon before redirects are installed, tears redirects down when DNS capture is disabled, and keeps IPv4/IPv6 redirect handling together so stale DNS interception is not left behind.
The daemon supervisor startup path was corrected so the daemon receives the required config argument and waits for the control socket before reporting readiness. This avoids a class of failures where the app thinks DNS protection is active but the native daemon is not ready to answer local control or DNS traffic.
Repair, reload, stop, restart, pause, and resume actions now route through the DNS manager instead of scattered command fragments. Pause disables DNS capture, removes redirect chains, stops the daemon, and updates scheduled blocklist behavior. Resume restores the setting, prepares daemon files, starts the supervisor, reinstalls redirects, and restores boot persistence. If a pause or resume root command fails, the previous enabled setting is restored so the UI does not drift away from device state.
The nftables fallback was added for devices where iptables redirect setup is unavailable or incomplete. DNS repair, boot restore, and purge handling account for both the daemon and redirect layer.
Accepted DNS TCP clients and daemon control clients now have explicit socket timeouts. A slow or partial client can no longer hold the daemon event loop indefinitely, and timeout counters are exposed in status and health output.
Cache reload behavior was tightened so cache entries survive rule-only reloads but are cleared when resolver-affecting settings change. That avoids unnecessary cache loss while preventing stale answers from being carried across upstream, split DNS, cache-size, or policy changes that alter resolution behavior.
Notable Performance Increases
Exact allow and block rules now use daemon-side indexes built during config load. The exact-rule index is allocated per loaded generation and sized from the loaded rule count, so large rule sets are no longer capped by one fixed static table.
Suffix allow and block rules are compiled into reverse-label tries. Runtime suffix matching walks the domain by label instead of scanning every suffix rule, keeping wildcard checks bounded by domain depth rather than list size.
Exact allow, suffix allow, exact block, and suffix block rule storage now grows dynamically while config is loaded. Large compiled blocklists are limited by available memory and reload validation instead of fixed array limits. Allocation failures reject the new generation and keep the previous daemon config active.
Cache memory is dynamically sized from configuration, supports disabled-cache mode, and reports hit/miss/store/expiry/eviction counters. Cached responses age their TTLs before reuse, zero-TTL answers are not cached, and the cache evicts least-recently-used entries instead of walking a fixed slot order.
Upstream forwarding reuses connected UDP sockets per config generation, validates response IDs, tracks stale upstream replies, retries truncated UDP replies over TCP, precompiles numeric upstream addresses during config load, and exposes upstream benchmarking to compare resolver behavior from the daemon side.
The DNS UDP read path drains bursts more effectively so queued DNS packets are handled without waiting for another event-loop wakeup. This should reduce latency spikes during short bursts of DNS traffic.
App Improvements
The Log Hub now includes a DNS Protection dashboard with daemon status, redirect status, health details, query access, blocklist update, pause/resume, restart, and repair controls. The pause/resume button follows the current DNS capture setting and refreshes the dashboard after each action.
A DNS query viewer was added to show recent daemon decisions, support history search/export, and let a user turn observed domains directly into exact, suffix, or temporary allow/block rules.
Rules preferences now include controls for enabling DNS hijacking, choosing upstream DNS servers, selecting upstream protocol behavior, defining split DNS routes, configuring strict/fail behavior, tuning cache size, controlling SafeSearch, and managing exact/suffix/temporary allow and block rules.
Per-app capture and bypass controls were added with UID-based root rules. Per-network capture and bypass controls were added with interface-pattern matching such as
wlan+,rmnet+, andtun+. This lets DNS interception be scoped in the root redirect layer instead of requiring the app to inspect traffic after the fact.DNS policy can follow the active firewall profile. Profile overrides can carry upstreams, split routes, app/network capture policy, cache size, fail behavior, strict mode, timeout, rules, temporary rules, SafeSearch behavior, blocklist URLs, and scheduled update settings.
Blocklists And Imports
DnsBlocklistManageradds staged import and compilation for common DNS list formats: hosts files, domain-only lists, Pi-hole style lists, basic AdBlock-style entries, pasted text, JSON backup bundles, and configured remote URLs.Imports are staged before activation. If parsing or writing fails, the previous compiled list stays in place. Compiled exact and suffix rule files are referenced by the daemon config so request-time DNS evaluation does not repeatedly parse source list formats.
Scheduled blocklist updates use Android's alarm path. The receiver runs the same staged update flow as the manual update action, marks rules stale on success, reloads the daemon when DNS capture is enabled, and records failures without replacing the last working compiled list.
Diagnostics And Logging
Diagnostics and data dump output now include daemon status, daemon health, upstream probe results, cache/resource stats, suffix trie stats, dynamic exact index sizes, temporary rule counts, profile DNS state, split route count, blocklist update metadata, UID/interface capture policy counts, control socket responses, recent daemon logs, redirect rule checks, boot script state, and root boot hook checks.
Daemon query logging now records richer metadata, including decision/action details, upstream information, protocol, cache status, latency, and UID/interface context where available. Persistent query logging was moved off the DNS request loop so file writes do not happen synchronously in the hot path.
Logging controls were added so live and persistent query logs can be managed from the app instead of being always-on behavior.
Files Of Interest
dnsdaemon/jni/afwall_dnsd.cadds the native daemon, DNS parsing/forwarding, cache, config reload, dynamic rule storage, exact-rule indexes, suffix tries, temporary rules, split routes, query logs, stats, health, and benchmark handling.app/src/main/assets/dnsd/contains the packaged daemon binaries.app/src/main/java/dev/ukanth/ufirewall/dns/DnsHijackManager.javamanages config generation, daemon lifecycle, root redirects, nft fallback, boot restore, repair, pause/resume, health/status parsing, profile DNS policy, query parsing, and diagnostics.app/src/main/java/dev/ukanth/ufirewall/dns/DnsBlocklistManager.javahandles blocklist imports, staged writes, compiled files, URL updates, and profile-specific blocklist storage.app/src/main/java/dev/ukanth/ufirewall/activity/DnsQueriesActivity.javaadds the live query viewer and query-to-rule actions.app/src/main/java/dev/ukanth/ufirewall/activity/LogHubActivity.javaexposes the DNS dashboard actions from Log Hub.app/src/main/java/dev/ukanth/ufirewall/activity/DiagnosticsActivity.javaandDataDumpActivity.javaexpose DNS daemon diagnostics and repair details.app/src/main/java/dev/ukanth/ufirewall/preferences/RulesPreferenceFragment.java,app/src/main/java/dev/ukanth/ufirewall/util/G.java, andapp/src/main/res/xml/rules_preferences.xmladd the app-facing DNS settings.app/src/main/java/dev/ukanth/ufirewall/broadcast/DnsBlocklistUpdateReceiver.javaandOnBootReceiver.javahandle scheduled blocklist refresh and reboot restoration.Notes
This branch is intentionally draft-only for now. It is broad enough that review will be more productive by subsystem or commit sequence rather than as one flat diff.