Skip to content

feat: add DNS domain rewrite setting#122

Merged
hawkff merged 3 commits into
mainfrom
feat/dns-hosts-rewrite
Jul 5, 2026
Merged

feat: add DNS domain rewrite setting#122
hawkff merged 3 commits into
mainfrom
feat/dns-hosts-rewrite

Conversation

@hawkff

@hawkff hawkff commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add a global DNS domain rewrite setting for static domain-to-address overrides.
  • Emit a scoped sing-box hosts DNS server/rule for configured domains.
  • Add a multiline settings editor with a concise line-count summary.
  • Validate entries strictly: malformed lines are dropped instead of breaking the config load, internationalized domains are converted to punycode, and comment lines are excluded from the summary count.

Validation

  • git diff --check
  • Resource XML well-formedness check
  • Unit tests for the hosts parser (accepted/rejected forms) run in CI
  • coderabbit review --agent --base main (0 findings)
  • Namespace CI

Greptile Summary

This PR introduces a global DNS domain-rewrite setting: users enter domain ip [ip …] entries in a new multiline preference, which are parsed into a sing-box hosts-type DNS server and a scoped rule that intercepts A/AAAA queries for those exact domains before FakeDNS or remote DNS can handle them.

  • Parsing (ConfigBuilder.kt): parseDnsHosts validates each entry with strict IPv4 (rejects leading-zero octets) and IPv6 (rejects Go-netip-invalid forms) checks, full RFC label validation, NBSP/tab separator handling, IDN→punycode normalisation, and comment stripping — covered by a new DnsHostsParseTest suite.
  • Rule ordering: the hosts DNS rule is inserted at index 0 before the loopback/force-bypass/per-subscription rules that follow, so those higher-priority rules push it down the list to the correct position above user-DNS-routing and FakeDNS rules.
  • UI (SettingsPreferenceFragment.kt): a multiline EditTextPreference with a plural-aware line-count summary (getQuantityString), tab-to-space preprocessing, and TYPE_TEXT_FLAG_NO_SUGGESTIONS; all six locale files receive the dns_hosts_lines plural resource and not_set string.

Confidence Score: 5/5

Safe to merge — input validation is thorough, rule ordering is correct, and the change is well-tested.

The parsing layer rejects every class of malformed input (leading-zero IPv4, Go-netip-invalid IPv6, empty labels, IP-as-domain, oversized labels) before anything reaches the sing-box config. DNS rule insertion relies on later index-0 insertions pushing the hosts rule to the right position, which is correctly described in code comments and matches the actual build flow. The UI layer handles tabs before ISO-control sanitization in the right order, and locale files all have the new plural and not_set resources. No gaps found that would cause a config load failure or unexpected DNS behaviour.

No files require special attention.

Important Files Changed

Filename Overview
app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt Adds parseDnsHosts() with strict IPv4/IPv6 validation and IDN support, plus a hosts-type DNS server/rule injected at the correct priority position in the DNS rule chain (below loopback/force-bypass/per-subscription, above FakeDNS)
app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt Adds multiline EditTextPreference for DNS hosts with plural-aware line-count summary, tab-to-space preprocessing hook, and no-suggestions keyboard; dnsReloadListener is cleanly extended with a preprocess lambda defaulting to identity
app/src/test/java/io/nekohasekai/sagernet/fmt/DnsHostsParseTest.kt New unit-test suite covering basic parsing, comments/blanks, deduplication, tab/NBSP separators, valid/invalid IPv6 forms, leading-zero IPv4 rejection, non-IP token dropping, invalid domain rejection, and IDN normalisation
app/src/main/res/xml/global_preferences.xml Adds the dnsHosts EditTextPreference inside the DNS category with a dialog hint message and transform icon
app/src/main/res/values/strings.xml Adds dns_hosts_dialog_message string, dns_hosts_lines plural resource (one/other), and not_set string for the English base locale
app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt Adds dnsHosts delegated property backed by configurationStore with an empty-string default, consistent with other preference properties
app/src/main/java/io/nekohasekai/sagernet/Constants.kt Adds DNS_HOSTS key constant for the new preference, placed logically next to HTTP proxy bypass keys

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant App as App (SettingsPreferenceFragment)
    participant DS as DataStore (dnsHosts)
    participant CB as ConfigBuilder (buildConfig)
    participant SB as sing-box DNS engine

    App->>App: User saves hosts entry (tab→space, sanitize)
    App->>DS: "dnsHosts = sanitizedValue"
    CB->>DS: parseDnsHosts(DataStore.dnsHosts)
    Note over CB: Validate each domain (RFC labels,<br/>IDN→punycode, no IPs)<br/>Validate each address (strict IPv4/IPv6)
    CB->>SB: "Add DNS server {type:hosts, predefined:{domain→[IPs]}}"
    CB->>SB: "Insert DNS rule at index 0<br/>{domain:[full:x], qtype:[A,AAAA], server:dns-hosts}"
    Note over SB: Final rule priority order:<br/>1. per-subscription rules<br/>2. force-bypass rules<br/>3. loopback rule<br/>4. dnsHosts rule (new)<br/>5. user DNS routing rules<br/>6. FakeDNS rule
    SB-->>App: "A/AAAA for matched domains → static IP(s)<br/>Other qtype/unmatched → normal resolution"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant App as App (SettingsPreferenceFragment)
    participant DS as DataStore (dnsHosts)
    participant CB as ConfigBuilder (buildConfig)
    participant SB as sing-box DNS engine

    App->>App: User saves hosts entry (tab→space, sanitize)
    App->>DS: "dnsHosts = sanitizedValue"
    CB->>DS: parseDnsHosts(DataStore.dnsHosts)
    Note over CB: Validate each domain (RFC labels,<br/>IDN→punycode, no IPs)<br/>Validate each address (strict IPv4/IPv6)
    CB->>SB: "Add DNS server {type:hosts, predefined:{domain→[IPs]}}"
    CB->>SB: "Insert DNS rule at index 0<br/>{domain:[full:x], qtype:[A,AAAA], server:dns-hosts}"
    Note over SB: Final rule priority order:<br/>1. per-subscription rules<br/>2. force-bypass rules<br/>3. loopback rule<br/>4. dnsHosts rule (new)<br/>5. user DNS routing rules<br/>6. FakeDNS rule
    SB-->>App: "A/AAAA for matched domains → static IP(s)<br/>Other qtype/unmatched → normal resolution"
Loading

Reviews (3): Last reviewed commit: "fix(dns): harden hosts parsing and refin..." | Re-trigger Greptile

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds DNS hosts rewrite storage, parsing, sing-box config emission, settings UI wiring, localized guidance text, and parser tests.

Changes

DNS Hosts Rewrite

Layer / File(s) Summary
Key and data store property
Constants.kt, database/DataStore.kt
Adds Key.DNS_HOSTS and a persisted dnsHosts string backed by configuration storage.
DNS hosts parsing and config generation
fmt/ConfigBuilder.kt
Adds strict hosts-list parsing and validation, reads DataStore.dnsHosts, and emits a hosts DNS server plus a top-priority A/AAAA rule for configured domains.
Settings UI wiring
ui/SettingsPreferenceFragment.kt, res/xml/global_preferences.xml
Wires the dnsHosts preference into settings with multiline editing, line-count summary text, paste-tab normalization, and the XML preference entry.
Localized strings and parser tests
res/values*/strings.xml, test/.../DnsHostsParseTest.kt
Adds localized dialog/help text, line-count plurals, and not-set strings, plus tests covering parsing, validation, and normalization.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SettingsPreferenceFragment
  participant DataStore
  participant ConfigBuilder
  participant SingBoxConfig

  User->>SettingsPreferenceFragment: Enter DNS hosts text
  SettingsPreferenceFragment->>SettingsPreferenceFragment: Normalize tabs and sanitize input
  SettingsPreferenceFragment->>DataStore: Save dnsHosts
  ConfigBuilder->>DataStore: Read dnsHosts
  ConfigBuilder->>ConfigBuilder: parseDnsHosts(text)
  ConfigBuilder->>SingBoxConfig: Add hosts server + DNS rule
Loading

Possibly related PRs

  • hawkff/NekoBoxForAndroid#32: Both PRs add DNS preference sanitization and related config-processing changes in SettingsPreferenceFragment.kt and ConfigBuilder.kt.

Poem

A rabbit hopped through DNS skies,
With tidy hosts and careful ties.
One line, one hop, one IP glow,
Then clean rewrites begin to flow.
🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: adding a DNS domain rewrite setting.
Description check ✅ Passed The description directly matches the changeset and explains the DNS rewrite feature, UI, and validation.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

Comment thread app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt Outdated
Comment thread app/src/main/res/values/strings.xml

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt (1)

269-307: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate sanitize/reload logic vs dnsReloadListener.

This inline listener re-implements the same "sanitize → compare → set text/needReload → return true/false" pattern already encapsulated in dnsReloadListener (lines 49-59), differing only by the tab→space pre-processing. Consider parameterizing dnsReloadListener with an optional pre-processing step so both call sites share one implementation.

♻️ Proposed refactor
-    private fun dnsReloadListener(preference: EditTextPreference, newValue: Any?): Boolean {
+    private fun dnsReloadListener(
+        preference: EditTextPreference,
+        newValue: Any?,
+        preprocess: (String) -> String = { it },
+    ): Boolean {
         val rawValue = newValue as? String ?: return reloadListener.onPreferenceChange(preference, newValue)
-        val sanitizedValue = sanitizeDnsPreferenceValue(rawValue)
+        val sanitizedValue = sanitizeDnsPreferenceValue(preprocess(rawValue))
         if (sanitizedValue != rawValue) {
             preference.text = sanitizedValue
             needReload()
             return false
         }
         needReload()
         return true
     }
...
-        dnsHosts.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
-            val rawValue = newValue as? String
-            if (rawValue == null) {
-                reloadListener.onPreferenceChange(dnsHosts, newValue)
-            } else {
-                // Tabs are valid separators in pasted hosts entries; convert them to
-                // spaces first so the control-character sanitization does not merge
-                // the domain and address tokens together.
-                val sanitized = sanitizeDnsPreferenceValue(rawValue.replace('\t', ' '))
-                if (sanitized != rawValue) {
-                    dnsHosts.text = sanitized
-                    needReload()
-                    false
-                } else {
-                    needReload()
-                    true
-                }
-            }
-        }
+        dnsHosts.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
+            dnsReloadListener(dnsHosts, newValue) { it.replace('\t', ' ') }
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt`
around lines 269 - 307, The DNS preference change handler is duplicating the
same sanitize/reload flow already implemented in dnsReloadListener. Refactor
dnsReloadListener to accept an optional pre-processing step for the raw string
(so the tab-to-space normalization can be reused), then have
dnsHosts.onPreferenceChangeListener delegate to that shared logic instead of
reimplementing the sanitize → compare → update text → needReload pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt`:
- Around line 269-307: The DNS preference change handler is duplicating the same
sanitize/reload flow already implemented in dnsReloadListener. Refactor
dnsReloadListener to accept an optional pre-processing step for the raw string
(so the tab-to-space normalization can be reused), then have
dnsHosts.onPreferenceChangeListener delegate to that shared logic instead of
reimplementing the sanitize → compare → update text → needReload pattern.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bf1b2888-b313-4d03-bc72-ff26f60d08dc

📥 Commits

Reviewing files that changed from the base of the PR and between d6a336f and 96911d8.

📒 Files selected for processing (12)
  • app/src/main/java/io/nekohasekai/sagernet/Constants.kt
  • app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
  • app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt
  • app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt
  • app/src/main/res/values-fa/strings.xml
  • app/src/main/res/values-ja/strings.xml
  • app/src/main/res/values-ko/strings.xml
  • app/src/main/res/values-ru/strings.xml
  • app/src/main/res/values-zh-rCN/strings.xml
  • app/src/main/res/values-zh-rTW/strings.xml
  • app/src/main/res/values/strings.xml
  • app/src/main/res/xml/global_preferences.xml

@hawkff hawkff force-pushed the feat/dns-hosts-rewrite branch from 399b3bd to f9293db Compare July 4, 2026 22:05
- Reject IPv6 forms with empty hextet groups that pass the app regex but
  fail strict address parsing downstream (leading/trailing single colon,
  repeated or overlapping double-colon runs).
- Convert internationalized domains to punycode and allow underscore
  labels instead of silently dropping such entries.
- Treat NBSP as a token separator for pasted content.
- Exclude comment lines from the settings line-count summary.
- Add unit tests covering accepted and rejected hosts entries.
@hawkff hawkff merged commit a60d583 into main Jul 5, 2026
8 checks passed
@hawkff hawkff deleted the feat/dns-hosts-rewrite branch July 5, 2026 00:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant