feat(#71): per-subscription DNS resolver + show sing-box version in About#67
Conversation
…bout Per-subscription resolver: - Add optional customDnsResolver to SubscriptionBean (kryo v2->3, gated read; kept out of shared links). Wire DataStore key + group_preferences UI with strict validation (https/tls/quic scheme+host, or host[:port], or empty). - ConfigBuilder: for a subscription with a resolver set, emit a dedicated DNS server (detour=direct, address_resolver=dns-local) and a dns.rule matching ONLY that subscription's final-hop server domains, above the global force rule. Empty resolver = no change. Ambiguous shared hostnames (same domain, different resolvers) stay on the global direct path. No default/Google DNS. About version: - libcore/build.sh injects constant.Version via read_tag (git-describe fallback); get_source.sh fetches tags so read_tag resolves on CI. Fixes the About screen showing 'unknown'.
📝 WalkthroughWalkthroughAdds per-subscription custom DNS resolver storage, UI editing, validation, and DNS routing. Also updates sing-box source fetching and libcore build linking to carry the sing-box version when available. ChangesSubscription custom DNS
sing-box build versioning
Sequence Diagram(s)sequenceDiagram
participant GroupSettingsActivity
participant DataStore
participant SubscriptionBean
participant ConfigBuilder
GroupSettingsActivity->>DataStore: store subscriptionCustomDns
GroupSettingsActivity->>SubscriptionBean: copy customDnsResolver
ConfigBuilder->>SubscriptionBean: read customDnsResolver
ConfigBuilder->>ConfigBuilder: build dns-sub-<gid> servers and rules
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Comment |
…ion ldflag get_source.sh clones sing-box to the parent of the repo root; build.sh runs from libcore/, so the clone is ../../sing-box, not ../sing-box. Probe a few candidates so the constant.Version ldflag resolves on CI (the first build logged 'could not determine sing-box version').
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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.
Inline comments:
In `@app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt`:
- Around line 395-415: The resolver mapping in ConfigBuilder’s final-hop
handling is using proxyEntity.groupId, which points to the last hop instead of
the originating subscription group. Update this logic to derive the group from
the chain owner entity.groupId and apply the resolver to that subscription’s own
server hosts, so per-subscription DNS metadata is recorded for imported
subscription hostnames even when a frontProxy is present. Keep the existing
resolver lookup and host registration flow, but ensure the cache keys and host
associations are based on the originating subscription group, not the final hop.
- Around line 910-919: The hostname check in ConfigBuilder’s DNS rule generation
is too narrow because hostResolvers only tracks resolver-enabled subscriptions,
so shared hosts can still get exclusive custom routing. Update the logic around
the unambiguousCustom check and the later dns-sub rule emission to also consider
total host usage across all profiles, not just resolver-enabled ones. Only add
the custom per-subscription DNS rule when the hostname is exclusive to
resolver-enabled subscriptions; otherwise keep it in the global direct-DNS path
to avoid affecting normal/manual profiles.
- Around line 61-70: The ConfigBean host extraction in serverHostOf currently
returns too early from the JSON branch, so bean.serverAddress is never used when
the parsed config lacks a top-level server. Update serverHostOf to keep the
ConfigBean-specific JSON lookup but fall back to bean.serverAddress when
map["server"] is missing or blank, preserving the documented custom-resolver
host mapping behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7ac5d934-32c3-4ecf-82b4-c41908acdd1d
📒 Files selected for processing (9)
app/src/main/java/io/nekohasekai/sagernet/Constants.ktapp/src/main/java/io/nekohasekai/sagernet/database/DataStore.ktapp/src/main/java/io/nekohasekai/sagernet/database/SubscriptionBean.javaapp/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.ktapp/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.ktapp/src/main/res/values/strings.xmlapp/src/main/res/xml/group_preferences.xmlbuildScript/lib/core/get_source.shlibcore/build.sh
…lags gomobile bind does not reliably forward -ldflags -X to the gobind-generated package, so the previous ldflag approach left constant.Version='unknown' (verified: no version string in libgojni.so). Generate libcore/version_gen.go that sets constant.Version in init() when still default; compiled directly into libcore. version_gen.go is gitignored.
…cribe CI log showed 'sing-box version: unknown (from ../../sing-box)': read_tag prints the literal 'unknown' when it cannot resolve a tag, which bypassed the empty-string fallback. Now treat empty OR 'unknown' as failure and use git describe --tags --always, plus fetch tags before resolving.
…ncy) CI git describe only resolved a bare hash (no tags in the runner's sing-box clone). Add explicit VERSION_SING_BOX pinned alongside COMMIT_SING_BOX as the deterministic source for the About version; read_tag/git describe remain as fallbacks. Update both together when bumping the core.
…ustom hosts) Address review findings: - serverHostOf: fall back to bean.serverAddress when ConfigBean JSON has no usable 'server' field. - Derive resolver ownership from the chain owner (entity.groupId), not the last hop's group, so subscriptions with a front/landing proxy still scope their resolver to their own server hosts. - Track all final-hop hosts of non-custom chains (every hop); only emit a per-subscription resolver rule for hosts exclusive to a single custom-resolver subscription (not shared with any normal profile), preventing DNS hijack. - Use the direct domain strategy for the per-sub resolver server (reached via direct detour), not the server strategy.
Implements #71 and fixes the About screen showing
sing-box: unknown.Per-subscription DNS resolver (#71)
Some providers host servers behind domains that only resolve via the provider's own DNS, so the global DNS cannot resolve them and those servers never connect. This lets a subscription specify its own resolver, used only for that subscription's server domains; everything else keeps the global DNS.
SubscriptionBean: new optionalcustomDnsResolver(kryo serialize bump v2 → v3, read gated on version ≥ 3; existing stored subscriptions load unchanged). Kept out of shared subscription links.https://,tls://,quic://scheme+host, or barehost[:port]; empty = unset.ConfigBuilder: when a subscription has a resolver, emit a dedicated DNS server (detour=direct,address_resolver=dns-localso it never loops through the not-yet-connected outbound) plus adns.rulesentry matching only that subscription's final-hop server hostnames (full:), placed above the global force rule.type-based DNS shapes.About version fix
constant.Versiondefaulted tounknownbecauselibcore/build.shdid not pass the version ldflag. Now it injects-X .../constant.Version=<read_tag>(git-describe fallback), andget_source.shfetches tags soread_tagresolves on CI. The About row will show the real version instead ofunknown. No workflow changes required.Verification
Greptile Summary
This PR introduces two independent improvements: a per-subscription custom DNS resolver so that providers whose server domains only resolve through their own DNS can declare that resolver in the subscription settings, and a fix for the About screen always showing
sing-box: unknownby generating a tinyinit()Go file during the build instead of relying on unreliablegomobileldflags forwarding.SubscriptionBeangains acustomDnsResolverfield (kryo v2→v3, backward-compatible), a new UI preference with strict scheme/host validation, andConfigBuilderemits adns-sub-<gid>server + rule per subscription group. An exclusivity-check (isExclusiveCustomHost) prevents the custom resolver from being applied to domains shared across subscriptions with different resolvers or non-custom profiles, which correctly fall back to global direct DNS.libcore/build.shnow generatesversion_gen.gowith a sanitized version string sourced first from a newVERSION_SING_BOXpin inget_source_env.sh, then fromread_tag, then fromgit describe. The generated file is git-ignored;get_source.shfetches tags to make the fallbacks work on CI.Confidence Score: 5/5
Safe to merge. The DNS routing logic is well-structured, exclusivity guards prevent cross-subscription resolver hijacking, and the kryo version bump is backward-compatible.
All changed code paths are correctly guarded: per-subscription DNS rules use TAG_DIRECT with address_resolver=dns-local (no bootstrap loop), hosts shared across subscriptions fall back to global direct DNS, and the version-gen approach sidesteps the longstanding gomobile ldflags forwarding problem cleanly. The only finding is a narrow validation false-positive in the UI input check that sing-box catches at runtime.
No files require special attention. The most complex file, ConfigBuilder.kt, has thorough inline comments and the logic maps directly to the described invariants.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[DNS query for hostname H] --> B{H matches a per-subscription rule?} B -- yes --> C[Resolve via custom resolver\ndetour=direct, address_resolver=dns-local] B -- no --> D{H in force-bypass list?} D -- yes --> E[Resolve via dns-direct] D -- no --> F{Avoid-loopback: query from proxy outbound?} F -- yes --> E F -- no --> G{User DNS rules match?} G -- yes --> H[Route per user rule] G -- no --> I[dns-remote / global proxy DNS] subgraph Build-time host classification J[Host in bypassDNSBeans] --> K{isExclusiveCustomHost?} K -- yes --> L[Added to perGroupServerHosts, kept out of force-bypass] K -- no --> M[Added to domainListDNSDirectForce] end%%{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"}}}%% flowchart TD A[DNS query for hostname H] --> B{H matches a per-subscription rule?} B -- yes --> C[Resolve via custom resolver\ndetour=direct, address_resolver=dns-local] B -- no --> D{H in force-bypass list?} D -- yes --> E[Resolve via dns-direct] D -- no --> F{Avoid-loopback: query from proxy outbound?} F -- yes --> E F -- no --> G{User DNS rules match?} G -- yes --> H[Route per user rule] G -- no --> I[dns-remote / global proxy DNS] subgraph Build-time host classification J[Host in bypassDNSBeans] --> K{isExclusiveCustomHost?} K -- yes --> L[Added to perGroupServerHosts, kept out of force-bypass] K -- no --> M[Added to domainListDNSDirectForce] endReviews (3): Last reviewed commit: "fix(#71): scope per-sub resolver correct..." | Re-trigger Greptile