Skip to content

Private DNS Discussion #1707

Draft
bitmold wants to merge 1 commit into
masterfrom
private-dns
Draft

Private DNS Discussion #1707
bitmold wants to merge 1 commit into
masterfrom
private-dns

Conversation

@bitmold
Copy link
Copy Markdown
Collaborator

@bitmold bitmold commented Jun 2, 2026

Private DNS on android has three states we can kind of vaguely reason about:

  • Off
  • "automatic" - the system will use DoT at its own discretion. seemingly prioritizes the user getting their websites loaded over adhering to universal DoT
  • "hostname" - the system says it will force DoT, even if this leads to a broken/poor UX. in this state, we can then obtain the user-specified TLS hostname

Querying This Stuff

We can basically do this on demand whenever we want. IE on the connect screen like @tladesignz had suggested, but could also ask for it anywhere, even in Notifications hypothetically.

I'm 99.9% sure this is mostly what we want:

  • we can learn if it's set
  • possibly display some info about what the user set
  • explain to the user about say leaking a tor onion service URL
  • would be easy to ask for this information when you open a screen, or even when you are building a notification to show the user.
    sealed class PrivateDns {
        object Off : PrivateDns()
        object Opportunistic : PrivateDns()
        data class Strict(val hostname: String) : PrivateDns()
        companion object {
            const val KEY_MODE = "private_dns_mode"
            const val KEY_HOSTNAME = "private_dns_specifier"
            const val HOSTNAME_UNKNOWN = ""
            const val MODE_OFF = "off"
            const val MODE_HOSTNAME = "hostname"
            const val MODE_AUTOMATIC = "automatic"
            fun isPrivateDnsSupported(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
        }
    }

    fun getPrivateDnsConfiguration(context: Context): PrivateDns {
        if (!PrivateDns.isPrivateDnsSupported()) return PrivateDns.Off
        val dnsMode =
            Settings.Secure.getString(context.contentResolver, PrivateDns.KEY_MODE)
                ?: PrivateDns.MODE_OFF
        when (dnsMode) {
            PrivateDns.MODE_OFF -> return PrivateDns.Off
            PrivateDns.MODE_AUTOMATIC -> return PrivateDns.Opportunistic
            PrivateDns.MODE_HOSTNAME -> {
                val hostname = Settings.Secure.getString(
                    context.contentResolver,
                    PrivateDns.KEY_HOSTNAME
                )
                    ?: PrivateDns.HOSTNAME_UNKNOWN
                return PrivateDns.Strict(hostname)
            }
        }
        return PrivateDns.Off

ConnectivityManager and ContentObserver

But you can also subscribe for network events in the app, which SnowflakeProxyService does. The connectivity lets us see when the user moves between networks. Whenever one of these network event fires off, we get the set of DNS resolvers the system has curated for the user at that moment. You don't get any info on if DNS is being resolved or plain text of if it's DoT style though. You also don't get notified which resolver is actually handling your query...

You can also subscribe with ContentObsrevers to get a callback fired whenever those values from above change. Seems like you can masochistically subscribe to both the variable change events, and the network change events, and perhaps make some guesses about what's going on.

This seems complex, also battery draining. and it doesn't give us much value than the code I already wrote above did.

@bitmold bitmold requested review from n8fr8 and tladesignz June 2, 2026 15:18
@bitmold
Copy link
Copy Markdown
Collaborator Author

bitmold commented Jun 2, 2026

i unfortunately need to get on the original GitLab thread and won't be able to do that until later today 🙃

I'm very curious why OnionMasq/TorVPN are working differently...

I'm with cyBerta and concerned about how this rubs against Tor's anonymity promise.

And at the bare minimum I agree with @tladesignz in the immediate term there's UX work to do to convey exactly what is happening to the user. I bet there are a lot of users who have security fatigue, see this thing as "private" and would be upset to learn there's a leak.

I think some users presumably understand exactly what's going on and desire this. But I bet there are many other users who are in the dark, it's not especially clear...

But it gets even more complicated!!

Caution: Android 9 only! These Private DNS settings have no effect when you use a VPN like Nexus/Pixel Wi-Fi Assistant or Google Fi Enhanced Network VPNs, or third-party VPN or DNS changer apps. Those features and apps override Private DNS and do not send DNS-over-TLS queries to Google Public DNS. Most DNS changers send cleartext queries (a few like Intra use other secure DNS protocols) and VPN apps may not secure queries beyond the VPN server. This is "fixed" in Android 10.

  • Based on my cursory understanding I think Google actually has this extremely unclear system that would confuse just about everyone,wherein
    • If you have a VPNService like Orbot, with Private DNS in Strict Mode, then the DNS resolution is leaked outside of the VPN. You also have know way to verify this reliably.
    • But if you have a VPNService in Lockdown Mode and set Private DNS to strict mode, than the system seemingly quietly ignores the Private DNS Strict Mode. Incidentally the method to see if a VPNService is locked down is added in Android 29, the same version of Android where Private DNS started leaking...

FWIW Android apps can open the system setting screen where they can enable lockdown mode by starting this Intent

startActivity(Intent("android.net.vpn.SETTINGS")
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))

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