Background
When investigating #152, the root causes for DNS stuck at 127.0.0.1 after disable traced to two backend bugs (state mutation order in set_dns_mode_disable, and error-swallowing in cleanup_dns_on_exit). These will be fixed by the current fix/dns-cancel-149 PR.
However, the investigation surfaced a third, structural weakness: the frontend has no independent way to verify what the system DNS is actually pointing at.
Current behavior
get_dns_mode (src-tauri/src/commands/dns.rs:745) returns the in-memory AtomicBool dns_enabled:
pub async fn get_dns_mode(state: tauri::State<'_, AppState>) -> Result<bool, MhostError> {
Ok(state.dns_enabled.load(Ordering::Relaxed))
}
It does not probe the OS. The frontend's truth-fetch on error therefore trusts Rust's in-memory view, which can disagree with reality if:
- The Rust state machine has a bug we haven't found yet
- A future change introduces a new path where
dns_enabled and networksetup -getdnsservers diverge
- An external event (manual
networksetup edit, another tool) modifies system DNS while mHost thinks it's the one in charge
Proposed change
Add a new IPC command that actually probes the OS:
// new IPC, bound in src-tauri/src/lib.rs
#[tauri::command]
pub async fn probe_system_dns(
state: tauri::State<'_, AppState>,
) -> Result<SystemDnsSnapshot, MhostError> {
// runs networksetup -getdnsservers <iface>, returns the actual server list
// + a derived bool `points_at_loopback: bool` via is_local_resolver
}
#[derive(Serialize, Deserialize)]
pub struct SystemDnsSnapshot {
pub interface: String,
pub servers: Vec<String>,
pub points_at_loopback: bool,
}
Frontend integration
toggleDnsModeAtom (src/stores/profiles/actions.ts:409) currently truth-fetches only on error:
const actual = await getDnsMode();
set(dnsEnabledAtom, actual);
Add a parallel probeSystemDns() call and compare:
dnsEnabledAtom |
probe_system_dns.points_at_loopback |
UI state |
true |
true |
"Running" (consistent) |
true |
false |
"Discrepancy" banner — show "system DNS not pointing at mHost proxy, click to force sync" |
false |
true |
"Discrepancy" banner — show "system DNS still at 127.0.0.1, click to force restore" |
false |
false |
"Stopped" (consistent) |
The "force sync" / "force restore" buttons would call the existing set_dns_mode(false) / set_dns_mode(true) IPC — same code path as the normal toggle, but with the user now informed why they're clicking.
Acceptance criteria
- New IPC
probe_system_dns runs networksetup -getdnsservers on the active interface and returns the snapshot.
probe_system_dns is gated to macOS only (DNS mode is macOS-only; Linux/Windows can return a clear "not supported" error).
- Frontend calls
probeSystemDns in parallel with getDnsMode in the truth-fetch path; surfaces the discrepancy banner only when they disagree.
- The discrepancy banner offers a one-click recovery action that calls the existing IPC.
- Unit tests for the new IPC's pure-logic part (parsing, loopback detection) —
networksetup_get_dns wrapper is already covered by test_networksetup_get_dns_filter_pipeline.
- macOS E2E scenario added to
doc/tech/dns-mode-e2e-recipe.md: manually set system DNS to a stale value via networksetup -setdnsservers Wi-Fi 8.8.8.8 while Rust thinks dns_enabled=false, verify the UI surfaces the discrepancy banner.
Out of scope
- Not changing
get_dns_mode to probe the OS (would change semantics for all callers and break the existing "trust in-memory" contract).
- Not auto-recovering when discrepancy is detected — the UI banner is informational, the user decides whether to act.
Related
Background
When investigating #152, the root causes for
DNS stuck at 127.0.0.1 after disabletraced to two backend bugs (state mutation order inset_dns_mode_disable, and error-swallowing incleanup_dns_on_exit). These will be fixed by the currentfix/dns-cancel-149PR.However, the investigation surfaced a third, structural weakness: the frontend has no independent way to verify what the system DNS is actually pointing at.
Current behavior
get_dns_mode(src-tauri/src/commands/dns.rs:745) returns the in-memoryAtomicBool dns_enabled:It does not probe the OS. The frontend's truth-fetch on error therefore trusts Rust's in-memory view, which can disagree with reality if:
dns_enabledandnetworksetup -getdnsserversdivergenetworksetupedit, another tool) modifies system DNS while mHost thinks it's the one in chargeProposed change
Add a new IPC command that actually probes the OS:
Frontend integration
toggleDnsModeAtom(src/stores/profiles/actions.ts:409) currently truth-fetches only on error:Add a parallel
probeSystemDns()call and compare:dnsEnabledAtomprobe_system_dns.points_at_loopbacktruetruetruefalsefalsetruefalsefalseThe "force sync" / "force restore" buttons would call the existing
set_dns_mode(false)/set_dns_mode(true)IPC — same code path as the normal toggle, but with the user now informed why they're clicking.Acceptance criteria
probe_system_dnsrunsnetworksetup -getdnsserverson the active interface and returns the snapshot.probe_system_dnsis gated to macOS only (DNS mode is macOS-only; Linux/Windows can return a clear "not supported" error).probeSystemDnsin parallel withgetDnsModein the truth-fetch path; surfaces the discrepancy banner only when they disagree.networksetup_get_dnswrapper is already covered bytest_networksetup_get_dns_filter_pipeline.doc/tech/dns-mode-e2e-recipe.md: manually set system DNS to a stale value vianetworksetup -setdnsservers Wi-Fi 8.8.8.8while Rust thinksdns_enabled=false, verify the UI surfaces the discrepancy banner.Out of scope
get_dns_modeto probe the OS (would change semantics for all callers and break the existing "trust in-memory" contract).Related
is_local_resolveralready lives inmhost_dns::platform; reuse it viapub(crate)if not already visible to commands/dns.rs.