Notice a pinned Private DNS when it happens, and warn in the app too - #713
Conversation
The warning only re-evaluated when the tunnel was built, so the common sequence — VPN already running, user pins a resolver in Android settings — broke name resolution and stayed silent until some unrelated network change rebuilt it. onLinkPropertiesChanged already fires for this and LinkProperties already carries the resolver name; the reload policy simply discarded it by comparing DNS servers alone, which a pinned resolver leaves untouched. Comparing the name too costs no new registration, and the reason is deliberately absent from shouldRestartWireGuard: the tunnel is fine, only the warning is stale. The notification is also the sole surface, and Util.notify() is silent without POST_NOTIFICATIONS — so a user who declined it saw nothing at all in the one state where nothing loads. A row on the main screen, alongside the local-network one and re-evaluated in the same onResume, reaches them, and covers merely reopening the app (which reloads nothing). It is gated on the VPN actually running, since port 853 is only blocked while it is, and it revives msg_private_dns, which lost its last caller when the onboarding slide went. The condition both surfaces share now lives in Util.isPrivateDnsBlocked and is covered by PrivateDnsBlockedTest, including that an unreadable specifier must not silence the warning.
The debounce collapses a burst of connectivity callbacks to its last reason, which was safe only while every reason restarted WireGuard. The new private-DNS reason does not, so a burst that ends on it now drops the rebind an earlier reason in the same 1500ms window required. Accumulate the need for a rebind across the burst instead of reading it off the surviving reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ReviewBoth behaviours are the right shape, and the split — new reason for the warning, no rebind — is the correct call. One real defect, fixed on the branch in 5f15815; the rest are notes. Fixed: the burst debounce drops the WireGuard rebind (
|
Device-checked on the Pixel 8 (Android 17)Both behaviours verified end to end on The row renders and works. With The live detection works, and this is the gap the PR set out to close. Switching the mode while the VPN was already up produced exactly the case the old comparison could not see — identical resolver list, only the name appearing: Reload fired ~1.5 s after the setting changed (the debounce), and clearing the mode fired the same reason in the other direction. No 20 s wait, no Wi-Fi cycle. Fixed: the row rendered without the quotes that name the setting
Which names no setting — "Private DNS" reads as prose, and the user is left looking for something called that. Every translation of the same string already escapes them, so English was the only one affected. Escaped, rebuilt, re-checked on the device: the quotes are now in the rendered text. Device state restored ( This also makes my earlier note about the wording sharper rather than weaker: with the quotes back the row names the setting, but it still never says anything is broken. Seeing it rendered, that reads oddest right where it matters — the row is bright red and urgent, and says only "turn this off", while the user's actual symptom is that nothing loads at all. |
Follow-up to #711, which landed the Private DNS warning. Two gaps that review turned up after it merged; both are about the warning reaching the user, not about what it says.
The warning only re-evaluated when the tunnel was built
getBuilder()is the only thing that posts or clears it, so the sequence I would expect to be the common one — VPN already running, user goes into Android settings and pins a resolver — broke name resolution and then said nothing until some unrelated network change happened to rebuild the tunnel. Device-checked on a Pixel 8 (Android 17): after changing the mode, no reload for at least 20s, and the notification appeared only once a Wi-Fi cycle forced one.No observer is needed for this.
onLinkPropertiesChangedalready fires, andLinkPropertiesalready carries the resolver name — unredacted for ordinary apps, which the app's own log confirms:private_dns_modeLinkPropertieshostnameUsePrivateDns: true PrivateDnsServerName: dns.googleoff/opportunisticNetworkReloadPolicy.onLinkPropertiesChangedsimply discarded it, comparinggetDnsServers()alone — which a pinned resolver leaves untouched. It now compares the name too. The new reason is deliberately not inshouldRestartWireGuard: the tunnel is unaffected, only the warning is stale, so this must not cost a rebind and re-handshake. There is a test pinning that.The notification was the only surface, and it can be silently unavailable
Util.notify()no-ops withoutPOST_NOTIFICATIONS. Onboarding asks, but a user can decline — and a user who declines is exactly the one who skipped through onboarding, which is who the removed slide was for. In that combination there was no warning anywhere, in the one state where nothing loads at all.So: a row on the main screen, next to the local-network one from #709 and re-evaluated in the same
onResume, tapping through to the network settings. That also covers merely reopening the app, which reloads nothing. It is gated on the VPN actually running, since port 853 is only blocked while it is — with the VPN off a pinned resolver works fine and a warning would be wrong.It reuses
msg_private_dns, which lost its last caller when the onboarding slide went, so this adds no new string and the existing translations apply.Also
The condition both surfaces share is now
Util.isPrivateDnsBlocked()rather than duplicated at each call site, covered byPrivateDnsBlockedTest: hostname vs automatic vs off, research mode (block_dotoff), and that a specifier which reads back empty must not silence the warning — failing silent in the broken state is the one outcome worth designing against.Testing
testGithubDebugUnitTestandlintGithubDebugpass. Both behaviours are unit-tested; neither has been exercised on a device in this form — the row in particular has not been seen rendered.🤖 Generated with Claude Code