Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android ConnectivityManager->getLinkProperties->getDnsServers can provide current DNS config #2116

Closed
DentonGentry opened this issue Jun 13, 2021 · 9 comments
Labels
dns enhancement New feature or request OS-android

Comments

@DentonGentry
Copy link
Contributor

Noting this for reference later: currently the Android app applies DNS configurations without being able to know the current DNS config. It looks like there is a way to roughly reconstruct the Android DNS config: there is a DNSDig app on Android which does so. From disassembling the APK, it uses ConnectivityManager->getLinkProperties->getDnsServers for each link. It likely chooses the set of DNS servers from the currently active link.

This mechanism isn't perfect, for example in the Settings > Network & internet > Private DNS one can set a DNS-over-TLS server to use. DNSDig doesn't reflect it when a Private DNS server is active, so doesn't appear to have a way to read out the Private DNS setting.

@namidairo
Copy link

DNSDig doesn't reflect it when a Private DNS server is active, so doesn't appear to have a way to read out the Private DNS setting.

I know this one, Settings.Global.PRIVATE_DNS_MODE to check the setting (off/opportunistic/hostname being off, automatic and user-specified respectively) and Settings.Global.PRIVATE_DNS_SPECIFIER for the user-specified hostname of the private dns.

I think you might have to manually grant permissions for writing secure settings if you want to change it however, which you have to do via adb/pm since the Play Store probably won't allow it in the manifest.

@DentonGentry
Copy link
Contributor Author

In Android prior to 8 there was an API to ask Android what DNS config it was currently using, by querying system properties net.dns1, net.dns2, etc. That API has been disabled, the net.dns* properties are now empty strings.

The ConnectivityManager APIs return correct values in current Android versions, but we'd be reverse engineering what Android does to assemble its DNS config rather than being able to ask Android directly.

@DentonGentry
Copy link
Contributor Author

DentonGentry commented Oct 10, 2021

Proof of concept in https://github.com/tailscale/tailscale-android/commits/dns and https://github.com/tailscale/tailscale/commits/android-dns

There is something missing: when my phone moved from Wi-Fi to LTE, it kept trying to use the Wi-Fi DNS servers which were no longer reachable. Currently there is no code which reacts to link changes in Android in order to update the DNS servers, something would need to.

@DentonGentry
Copy link
Contributor Author

Findings so far:

  • Asking the Android ConnectivityManager for getActiveNetwork() isn't very useful: when the VPN comes up, Android says tun0 is the currently active interface with DNS server 100.100.100.100.
  • We can iterate over all ConnectivityManager.getAllNetworks() and retrieve their DNS servers using getLinkProperties(network).getDnsServers()
  • Even when the Wi-Fi link is up and is the primary network connection, the LTE interface v4-rmnet_data1 has an IP address assigned and DNS servers fd00:976a::9 and fd00:976a::10. So we can't just report all DNS servers from all links.

It is surprisingly difficult to figure out if Android is using Wi-Fi or LTE for connectivity. One way is by running ip route show and looking for the first default route.

Wi-fi up:

% adb shell 'ip route show table 0 | grep "default via"'
default via 10.1.10.1 dev wlan0 table 1030 proto static
default via 192.0.0.4 dev v4-rmnet_data1 table 2312 proto static
default via fe80::a236:9fff:fe59:19b2 dev wlan0 table 1030 proto ra metric 1024 expires 1797sec mtu 1280 hoplimit 64 pref medium
default via fe80::18fb:c18c:8b9e:100e dev rmnet_data1 table 1011 proto ra metric 1024 expires 60551sec hoplimit 255 pref medium
default via fe80::54ed:de0c:56a0:c4d0 dev rmnet_data2 table 1012 proto ra metric 1024 expires 60553sec hoplimit 255 pref medium

Wi-fi down:

% adb shell 'ip route show table 0 | grep "default via"'
default via 192.0.0.4 dev v4-rmnet_data1 table 2312 proto static
default via fe80::a236:9fff:fe59:19b2 dev wlan0 table 1030 proto ra metric 1024 expires 1799sec mtu 1280 hoplimit 64 pref medium
default via fe80::18fb:c18c:8b9e:100e dev rmnet_data1 table 1011 proto ra metric 1024 expires 60553sec hoplimit 255 pref medium
default via fe80::54ed:de0c:56a0:c4d0 dev rmnet_data2 table 1012 proto ra metric 1024 expires 60555sec hoplimit 255 pref medium

However not all Android devices have an ip command, and /proc/net/route is empty:

% adb shell "cat /proc/net/route"
Iface	Destination	Gateway 	Flags	RefCnt	Use	Metric	Mask		MTU	Window	IRTT
%

@bradfitz
Copy link
Member

It is surprisingly difficult to figure out if Android is using Wi-Fi or LTE for connectivity.

The correct way has changed over time, but there are definitely official ways without resorting to inspecting route tables.

https://stackoverflow.com/questions/3841317/how-do-i-see-if-wi-fi-is-connected-on-android has a bunch of ways (some the old deprecated ways)

@DentonGentry
Copy link
Contributor Author

DentonGentry commented Oct 19, 2021

There are several good possibilities in that stackoverflow question, I'll make some notes on findings here:


NetworkCapabilities.NET_CAPABILITY_INTERNET

                        NetworkCapabilities nc = cMgr.getNetworkCapabilities(network);
                        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
                                internet.append(ifname + " ");
                        }

with Wifi connected, both rmnet_data1 and wlan0 have NET_CAPABILITY_INTERNET set.


Also how to get notified when the Private DNS settings change:
https://stackoverflow.com/questions/52005338/get-notified-when-private-dns-changes-on-android-pie

@DentonGentry
Copy link
Contributor Author

DentonGentry commented Oct 23, 2021

Using this code:

                        NetworkCapabilities nc = cMgr.getNetworkCapabilities(network);
                        sb.append(String.format("%s in=%b eth=%b wifi=%b cell=%b | ", ifname,
                                nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET),
                                nc.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET),
                                nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI),
                                nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
                                ));

Pixel3a running Android 12

With Wi-Fi on we get:

rmnet_data2 in=false eth=false wifi=false cell=true | | |
rmnet_data1 in=true eth=false wifi=false cell=true |fd00:976a::9 fd00:976a::10 | |
wlan0 in=true eth=false wifi=true cell=false |2602:248:7b4a:ff60::1 10.1.10.1 | localdomain|

With Wi-Fi off we get:

rmnet_data2 in=false eth=false wifi=false cell=true | | |
rmnet_data1 in=true eth=false wifi=false cell=true |fd00:976a::9 fd00:976a::10 | |

The best option I can see is heuristic:

  1. If an Ethernet link is active, use the DNS servers from it.
  2. If Wi-Fi is active, use the DNS servers from it.
  3. If mobile data is active, use the DNS servers from it.

Nexus 7 (2013) running Android 6.0.1:

No interfaces returned by getAllNetworks() ?

The API is present: the app doesn't crash, and getAllNetworks() was added in API level 21. It just doesn't seem to return any interfaces, even with the Wi-Fi on. This device is Wi-Fi only, no cellular radio.

xamarin/Essentials#911 reports roughly the same issue, some devices with Android 6 just don't return anything in getAllNetworks(). That issue implements a fallback to try GetAllNetworkInfo(), but I don't see a way to get the DNS servers starting from just a NetworkInfo. We need a way to get the LinkProperties.

However with Android versions prior to 8 we can query the {net.dns1, net.dns2, net.dns3, net.dns4} SystemProperties directly. Access to those properties was removed in Android 8. So I think we can do an implementation of:

  1. try to query the DNS servers via SystemProperties
  2. If empty, try to reconstruct the DNS server list using the heuristic described above with getAllNetworks().

Code implementing both of these ideas is in https://github.com/tailscale/tailscale-android/commits/dns
It adds Java code to query the system properties (only works on old Android releases) then getAllNetworks() (works on current Android releases).

It depends on OSS support in https://github.com/tailscale/tailscale/commits/android-dns


Update 11/19:
For Android 6 we can implement a fallback where we call ConnectivityManager getActiveNetworkInfo, then call android.net.NetworkUtils.runDhcp which only needs the interfaceName and will fill in a DhcpResults.

DentonGentry added a commit that referenced this issue Oct 25, 2021
Allow users of CallbackRouter to supply a GetBaseConfig
implementation, to be used on Android.

iOS/macOS also use the CallbackRouter but have platform
support for SplitDNS, so don't need getBaseConfig.

Updates #2116
Updates #988

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
DentonGentry added a commit that referenced this issue Oct 25, 2021
Allow users of CallbackRouter to supply a GetBaseConfig
implementation. This is expected to be used on Android,
which currently lacks both a) platform support for
Split-DNS and b) a way to retrieve the current DNS
servers.

iOS/macOS also use the CallbackRouter but have platform
support for SplitDNS, so don't need getBaseConfig.

Updates #2116
Updates #988

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
DentonGentry added a commit that referenced this issue Oct 25, 2021
Allow users of CallbackRouter to supply a GetBaseConfig
implementation. This is expected to be used on Android,
which currently lacks both a) platform support for
Split-DNS and b) a way to retrieve the current DNS
servers.

iOS/macOS also use the CallbackRouter but have platform
support for SplitDNS, so don't need getBaseConfig.

Updates #2116
Updates #988

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
DentonGentry added a commit that referenced this issue Oct 27, 2021
Allow users of CallbackRouter to supply a GetBaseConfig
implementation. This is expected to be used on Android,
which currently lacks both a) platform support for
Split-DNS and b) a way to retrieve the current DNS
servers.

iOS/macOS also use the CallbackRouter but have platform
support for SplitDNS, so don't need getBaseConfig.

Updates #2116
Updates #988

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
DentonGentry added a commit that referenced this issue Oct 30, 2021
Allow users of CallbackRouter to supply a GetBaseConfig
implementation. This is expected to be used on Android,
which currently lacks both a) platform support for
Split-DNS and b) a way to retrieve the current DNS
servers.

iOS/macOS also use the CallbackRouter but have platform
support for SplitDNS, so don't need getBaseConfig.

Updates #2116
Updates #988

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
DentonGentry added a commit that referenced this issue Nov 1, 2021
Allow users of CallbackRouter to supply a GetBaseConfig
implementation. This is expected to be used on Android,
which currently lacks both a) platform support for
Split-DNS and b) a way to retrieve the current DNS
servers.

iOS/macOS also use the CallbackRouter but have platform
support for SplitDNS, so don't need getBaseConfig.

Updates #2116
Updates #988

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
@DentonGentry
Copy link
Contributor Author

DentonGentry commented Nov 8, 2021

Noting this here to ensure we don't lose track of it: Android API level 29 (Android 10) added a DnsResolver.RawQuery which would allow submission of a DNS packet for the OS to handle. Since the Tailscale app is marked as not using the VPN for its own traffic, there is a chance that this will use only the platform, non-VPN DNS resolvers. This could be used to construct a fallback.
https://developer.android.com/reference/android/net/DnsResolver#rawQuery(android.net.Network,%20byte[],%20int,%20java.util.concurrent.Executor,%20android.os.CancellationSignal,%20android.net.DnsResolver.Callback%3C?%20super%20byte[]%3E)

DentonGentry added a commit that referenced this issue Nov 19, 2021
Allow users of CallbackRouter to supply a GetBaseConfig
implementation. This is expected to be used on Android,
which currently lacks both a) platform support for
Split-DNS and b) a way to retrieve the current DNS
servers.

iOS/macOS also use the CallbackRouter but have platform
support for SplitDNS, so don't need getBaseConfig.

Updates #2116
Updates #988

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
DentonGentry added a commit to tailscale/tailscale-android that referenced this issue Nov 26, 2021
Add getDnsConfigAsString() to retrieve the current DNS
configuration from the Android platform. This implements
several mechanisms to retrieve DNS information, suitable
for different Android versions:

Android 7 and later use ConnectivityManager getAllNetworks(),
then iterate over each network to retrieve DNS servers and
search domains using the LinkProperties.

Android 6 and earlier can only retrieve the currently active
interface using ConnectivityManager getActiveNetwork(), but have
two additional fallback options which leverage the system
properties available in older Android releases.

Fixes tailscale/tailscale#2116
Updates tailscale/tailscale#988

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
@DentonGentry
Copy link
Contributor Author

DentonGentry commented Nov 29, 2021

Out and about yesterday using a debug build with tailscale/tailscale-android#23, I noticed the phone DNS wasn't working until I turned Tailscale off. getDnsConfigFromLinkProperties had returned an empty list, right after the phone woke up from deep sleep.

2021-11-28 17:50:07.86317278 +0000 UTC: 8.1M/58.9M wgengine: Reconfig done
2021-11-28 17:50:07.863472832 +0000 UTC: 8.1M/58.9M authReconfig: ra=true dns=true 0x03: <nil>
2021-11-28 17:57:21.324973478 +0000 UTC: 8.3M/54.0M monitor: time jumped (probably wake from sleep); synthesizing major change event
2021-11-28 17:57:21.33002577 +0000 UTC: 8.3M/54.0M LinkChange: major, rebinding. New state: interfaces.State{defaultRoute= ifs={rmnet_data1:[2607:fb90:9eae:5ac1:1a8:a39e:7f0f:2523/64] rmnet_data2:[2607:fc20:9e6d:aa71:3b0d:df63:9b52:3646/64] tun0:[100.75.174.41/32 fd7a:115c:a1e0:ab12:4843:cd96:624b:ae29/128] v4-rmnet_data1:[192.0.0.4/32]} v4=true v6=true}
2021-11-28 17:57:21.332459885 +0000 UTC: 8.4M/54.0M dns: Set: {DefaultResolvers:[] Routes:{geekhold.com.beta.tailscale.net.:[] ts-dns.test.:[100.120.55.47:53] ts.net.:[]}+65arpa SearchDomains:[chihuahua-tyrannosaurus.ts.net. geekhold.com.beta.tailscale.net. ts-dns.test.] Hosts:48}
2021-11-28 17:57:21.344311813 +0000 UTC: getDnsConfigFromLinkProperties:
2021-11-28 17:57:21.345735407 +0000 UTC: getDnsServersFromSystemProperties:
2021-11-28 17:57:21.347835251 +0000 UTC: getDnsServersFromNetworkInfo:
2021-11-28 17:57:21.354097335 +0000 UTC: 8.4M/54.0M dns: Resolvercfg: {Routes:{.:[] ts-dns.test.:[100.120.55.47:53]} Hosts:48 LocalDomains:[ts.net. geekhold.com.beta.tailscale.net.]+65arpa}
2021-11-28 17:57:21.354363221 +0000 UTC: 8.4M/54.0M dns: OScfg: {Nameservers:[100.100.100.100] SearchDomains:[chihuahua-tyrannosaurus.ts.net. geekhold.com.beta.tailscale.net. ts-dns.test.] MatchDomains:[]}
2021-11-28 17:57:21.35495171 +0000 UTC: 8.4M/54.0M wgengine: set DNS config again after major link change
2021-11-28 17:57:21.359282388 +0000 UTC: 8.4M/54.0M magicsock: closing connection to derp-2 (rebind), age 4m42s

What to do about this?

  1. maybe keep the last functional set of DNS servers cached? Runs the risk of trying to continue using DNS servers which are no longer reachable because we've roamed to a different network.
  2. implement android.net.ConnectivityManager.NetworkCallback to be notified of link changes? Though in this case two of the radio interfaces had IP addresses they seem to have just lost their DNS server config.

DentonGentry added a commit to tailscale/tailscale-android that referenced this issue Nov 30, 2021
Add getDnsConfigAsString() to retrieve the current DNS
configuration from the Android platform. This implements
several mechanisms to retrieve DNS information, suitable
for different Android versions:

Android 7 and later use ConnectivityManager getAllNetworks(),
then iterate over each network to retrieve DNS servers and
search domains using the LinkProperties.

Android 6 and earlier can only retrieve the currently active
interface using ConnectivityManager getActiveNetwork(), but have
two additional fallback options which leverage the system
properties available in older Android releases.

Fixes tailscale/tailscale#2116
Updates tailscale/tailscale#988

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
DentonGentry added a commit that referenced this issue Dec 5, 2021
Allow users of CallbackRouter to supply a GetBaseConfig
implementation. This is expected to be used on Android,
which currently lacks both a) platform support for
Split-DNS and b) a way to retrieve the current DNS
servers.

iOS/macOS also use the CallbackRouter but have platform
support for SplitDNS, so don't need getBaseConfig.

Updates #2116
Updates #988

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
DentonGentry added a commit that referenced this issue Dec 6, 2021
Allow users of CallbackRouter to supply a GetBaseConfig
implementation. This is expected to be used on Android,
which currently lacks both a) platform support for
Split-DNS and b) a way to retrieve the current DNS
servers.

iOS/macOS also use the CallbackRouter but have platform
support for SplitDNS, so don't need getBaseConfig.

Updates #2116
Updates #988

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
DentonGentry added a commit that referenced this issue Dec 9, 2021
Allow users of CallbackRouter to supply a GetBaseConfig
implementation. This is expected to be used on Android,
which currently lacks both a) platform support for
Split-DNS and b) a way to retrieve the current DNS
servers.

iOS/macOS also use the CallbackRouter but have platform
support for SplitDNS, so don't need getBaseConfig.

Updates #2116
Updates #988

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dns enhancement New feature or request OS-android
Projects
None yet
Development

No branches or pull requests

3 participants