retrieve current DNS servers.
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. -------- Also changed how LinkChange notification works, switching from the older BroadcastReceiver of a ConnectivityManager Intent to the newer ConnectivityManager.registerNetworkCallback. We need this because the onAvailable event is too early, we get notified that LTE is up before its DNS servers have been set. We need to wait for the onLinkPropertiesChanged event instead, which is only available with registerNetworkCallback. Fixes https://github.com/tailscale/tailscale/issues/2116 Updates https://github.com/tailscale/tailscale/issues/988 Signed-off-by: Denton Gentry <dgentry@tailscale.com>pull/23/head
parent
0c00fa3374
commit
5c9cec0064
@ -0,0 +1,360 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package com.tailscale.ipn;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.DhcpInfo;
|
||||
import android.net.LinkProperties;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.wifi.WifiManager;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
// Tailscale DNS Config retrieval
|
||||
//
|
||||
// Tailscale's DNS support can either override the local DNS servers with a set of servers
|
||||
// configured in the admin panel, or supplement the local DNS servers with additional
|
||||
// servers for specific domains like example.com.beta.tailscale.net. In the non-override mode,
|
||||
// we need to retrieve the current set of DNS servers from the platform. These will typically
|
||||
// be the DNS servers received from DHCP.
|
||||
//
|
||||
// Importantly, after the Tailscale VPN comes up it will set a DNS server of 100.100.100.100
|
||||
// but we still want to retrieve the underlying DNS servers received from DHCP. If we roam
|
||||
// from Wi-Fi to LTE, we want the DNS servers received from LTE.
|
||||
//
|
||||
// --------------------- Android 7 and later -----------------------------------------
|
||||
//
|
||||
// ## getDnsConfigFromLinkProperties
|
||||
// Android provides a getAllNetworks interface in the ConnectivityManager. We walk through
|
||||
// each interface to pick the most appropriate one.
|
||||
// - If there is an Ethernet interface active we use that.
|
||||
// - If Wi-Fi is active we use that.
|
||||
// - If LTE is active we use that.
|
||||
// - We never use a VPN's DNS servers. That VPN is likely us. Even if not us, Android
|
||||
// only allows one VPN at a time so a different VPN's DNS servers won't be available
|
||||
// once Tailscale comes up.
|
||||
//
|
||||
// getAllNetworks() is used as the sole mechanism for retrieving the DNS config with
|
||||
// Android 7 and later.
|
||||
//
|
||||
// --------------------- Releases older than Android 7 -------------------------------
|
||||
//
|
||||
// We support Tailscale back to Android 5. Android versions 5 and 6 supply a getAllNetworks()
|
||||
// implementation but it always returns an empty list.
|
||||
//
|
||||
// ## getDnsConfigFromLinkProperties with getActiveNetwork
|
||||
// ConnectivityManager also supports a getActiveNetwork() routine, which Android 5 and 6 do
|
||||
// return a value for. If Tailscale isn't up yet and we can get the Wi-Fi/LTE/etc DNS
|
||||
// config using getActiveNetwork(), we use that.
|
||||
//
|
||||
// Once Tailscale is up, getActiveNetwork() returns tailscale0 with DNS server 100.100.100.100
|
||||
// and that isn't useful. So we try two other mechanisms:
|
||||
//
|
||||
// ## getDnsServersFromSystemProperties
|
||||
// Android versions prior to 8 let us retrieve the actual system DNS servers from properties.
|
||||
// Later Android versions removed the properties and only return an empty string.
|
||||
//
|
||||
// We check the net.dns1 - net.dns4 DNS servers. If Tailscale is up the DNS server will be
|
||||
// 100.100.100.100, which isn't useful, but if we get something different we'll use that.
|
||||
//
|
||||
// getDnsServersFromSystemProperties can only retrieve the IPv4 or IPv6 addresses of the
|
||||
// configured DNS servers. We also want to know the DNS Search Domains configured, but
|
||||
// we have no way to retrieve this using these interfaces. We return an empty list of
|
||||
// search domains. Sorry.
|
||||
//
|
||||
// ## getDnsServersFromNetworkInfo
|
||||
// ConnectivityManager supports an older API called getActiveNetworkInfo to return the
|
||||
// active network interface. It doesn't handle VPNs, so the interface will always be Wi-Fi
|
||||
// or Cellular even if Tailscale is up.
|
||||
//
|
||||
// For Wi-Fi interfaces we retrieve the DHCP response from the WifiManager. For Cellular
|
||||
// interfaces we check for properties populated by most of the radio drivers.
|
||||
//
|
||||
// getDnsServersFromNetworkInfo does not have a way to retrieve the DNS Search Domains,
|
||||
// so we return an empty list. Additionally, these interfaces are so old that they only
|
||||
// support IPv4. We can't retrieve IPv6 DNS server addresses this way.
|
||||
|
||||
public class DnsConfig {
|
||||
private Context ctx;
|
||||
|
||||
public DnsConfig(Context ctx) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
// getDnsConfigAsString returns the current DNS configuration as a multiline string:
|
||||
// line[0] DNS server addresses separated by spaces
|
||||
// line[1] search domains separated by spaces
|
||||
//
|
||||
// For example:
|
||||
// 8.8.8.8 8.8.4.4
|
||||
// example.com
|
||||
//
|
||||
// an empty string means the current DNS configuration could not be retrieved.
|
||||
String getDnsConfigAsString() {
|
||||
String s = getDnsConfigFromLinkProperties();
|
||||
if (!s.trim().isEmpty()) {
|
||||
return s;
|
||||
}
|
||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||
// If ConnectivityManager.getAllNetworks() works, it is the
|
||||
// authoritative mechanism and we rely on it. The other methods
|
||||
// which follow involve more compromises.
|
||||
return "";
|
||||
}
|
||||
|
||||
s = getDnsServersFromSystemProperties();
|
||||
if (!s.trim().isEmpty()) {
|
||||
return s;
|
||||
}
|
||||
return getDnsServersFromNetworkInfo();
|
||||
}
|
||||
|
||||
// getDnsConfigFromLinkProperties finds the DNS servers for each Network interface
|
||||
// returned by ConnectivityManager getAllNetworks().LinkProperties, and return the
|
||||
// one that (heuristically) would be the primary DNS servers.
|
||||
//
|
||||
// on a Nexus 4 with Android 5.1 on wifi: 2602:248:7b4a:ff60::1 10.1.10.1
|
||||
// on a Nexus 7 with Android 6.0 on wifi: 2602:248:7b4a:ff60::1 10.1.10.1
|
||||
// on a Pixel 3a with Android 12.0 on wifi: 2602:248:7b4a:ff60::1 10.1.10.1\nlocaldomain
|
||||
// on a Pixel 3a with Android 12.0 on LTE: fd00:976a::9 fd00:976a::10
|
||||
//
|
||||
// One odd behavior noted on Pixel3a with Android 12:
|
||||
// With Wi-Fi already connected, starting Tailscale returned DNS servers 2602:248:7b4a:ff60::1 10.1.10.1
|
||||
// Turning off Wi-Fi and connecting LTE returned DNS servers fd00:976a::9 fd00:976a::10.
|
||||
// Turning Wi-Fi back on return DNS servers: 10.1.10.1. The IPv6 DNS server is gone.
|
||||
// This appears to be the ConnectivityManager behavior, not something we are doing.
|
||||
//
|
||||
// This implementation can work through Android 12 (SDK 30). In SDK 31 the
|
||||
// getAllNetworks() method is deprecated and we'll need to implement a
|
||||
// android.net.ConnectivityManager.NetworkCallback instead to monitor
|
||||
// link changes and track which DNS server to use.
|
||||
String getDnsConfigFromLinkProperties() {
|
||||
ConnectivityManager cMgr = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (cMgr == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
Network[] networks = cMgr.getAllNetworks();
|
||||
if (networks == null) {
|
||||
// Android 6 and before often returns an empty list, but we
|
||||
// can try again with just the active network.
|
||||
//
|
||||
// Once Tailscale is connected, the active network will be Tailscale
|
||||
// which will have 100.100.100.100 for its DNS server. We reject
|
||||
// TYPE_VPN in getPreferabilityForNetwork, so it won't be returned.
|
||||
Network active = cMgr.getActiveNetwork();
|
||||
if (active == null) {
|
||||
return "";
|
||||
}
|
||||
networks = new Network[]{active};
|
||||
}
|
||||
|
||||
// getPreferabilityForNetwork returns an index into dnsConfigs from 0-3.
|
||||
String[] dnsConfigs = new String[]{"", "", "", ""};
|
||||
for (Network network : networks) {
|
||||
int idx = getPreferabilityForNetwork(cMgr, network);
|
||||
if ((idx < 0) || (idx > 3)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
LinkProperties linkProp = cMgr.getLinkProperties(network);
|
||||
NetworkCapabilities nc = cMgr.getNetworkCapabilities(network);
|
||||
List<InetAddress> dnsList = linkProp.getDnsServers();
|
||||
StringBuilder sb = new StringBuilder("");
|
||||
for (InetAddress ip : dnsList) {
|
||||
sb.append(ip.getHostAddress() + " ");
|
||||
}
|
||||
|
||||
String d = linkProp.getDomains();
|
||||
if (d != null) {
|
||||
sb.append("\n");
|
||||
sb.append(d);
|
||||
}
|
||||
|
||||
dnsConfigs[idx] = sb.toString();
|
||||
}
|
||||
|
||||
// return the lowest index DNS config which exists. If an Ethernet config
|
||||
// was found, return it. Otherwise if Wi-fi was found, return it. Etc.
|
||||
for (String s : dnsConfigs) {
|
||||
if (!s.trim().isEmpty()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
// getDnsServersFromSystemProperties returns DNS servers found in system properties.
|
||||
// On Android versions prior to Android 8, we can directly query the DNS
|
||||
// servers the system is using. More recent Android releases return empty strings.
|
||||
//
|
||||
// Once Tailscale is connected these properties will return 100.100.100.100, which we
|
||||
// suppress.
|
||||
//
|
||||
// on a Nexus 4 with Android 5.1 on wifi: 2602:248:7b4a:ff60::1 10.1.10.1
|
||||
// on a Nexus 7 with Android 6.0 on wifi: 2602:248:7b4a:ff60::1 10.1.10.1
|
||||
// on a Pixel 3a with Android 12.0 on wifi:
|
||||
// on a Pixel 3a with Android 12.0 on LTE:
|
||||
//
|
||||
// The list of DNS search domains does not appear to be available in system properties.
|
||||
String getDnsServersFromSystemProperties() {
|
||||
try {
|
||||
Class SystemProperties = Class.forName("android.os.SystemProperties");
|
||||
Method method = SystemProperties.getMethod("get", String.class);
|
||||
List<String> servers = new ArrayList<String>();
|
||||
for (String name : new String[]{"net.dns1", "net.dns2", "net.dns3", "net.dns4"}) {
|
||||
String value = (String) method.invoke(null, name);
|
||||
if (value != null && !value.isEmpty() &&
|
||||
!value.equals("100.100.100.100") &&
|
||||
!servers.contains(value)) {
|
||||
servers.add(value);
|
||||
}
|
||||
}
|
||||
return String.join(" ", servers);
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
String intToInetString(int hostAddress) {
|
||||
return String.format("%d.%d.%d.%d",
|
||||
(byte)(0xff & hostAddress),
|
||||
(byte)(0xff & (hostAddress >> 8)),
|
||||
(byte)(0xff & (hostAddress >> 16)),
|
||||
(byte)(0xff & (hostAddress >> 24)));
|
||||
}
|
||||
|
||||
// getDnsServersFromNetworkInfo retrieves DNS servers using ConnectivityManager
|
||||
// getActiveNetworkInfo() plus interface-specific mechanisms to retrieve the DNS servers.
|
||||
// Only IPv4 DNS servers are supported by this mechanism, neither the WifiManager nor the
|
||||
// interface-specific dns properties appear to populate IPv6 DNS server addresses.
|
||||
//
|
||||
// on a Nexus 4 with Android 5.1 on wifi: 10.1.10.1
|
||||
// on a Nexus 7 with Android 6.0 on wifi: 10.1.10.1
|
||||
// on a Pixel-3a with Android 12.0 on wifi: 10.1.10.1
|
||||
// on a Pixel-3a with Android 12.0 on LTE:
|
||||
//
|
||||
// The list of DNS search domains is not available in this way.
|
||||
String getDnsServersFromNetworkInfo() {
|
||||
ConnectivityManager cMgr = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (cMgr == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
NetworkInfo info = cMgr.getActiveNetworkInfo();
|
||||
if (info == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
Class SystemProperties;
|
||||
Method method;
|
||||
|
||||
try {
|
||||
SystemProperties = Class.forName("android.os.SystemProperties");
|
||||
method = SystemProperties.getMethod("get", String.class);
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
|
||||
List<String> servers = new ArrayList<String>();
|
||||
|
||||
switch(info.getType()) {
|
||||
case ConnectivityManager.TYPE_WIFI:
|
||||
case ConnectivityManager.TYPE_WIMAX:
|
||||
for (String name : new String[]{
|
||||
"net.wifi0.dns1", "net.wifi0.dns2", "net.wifi0.dns3", "net.wifi0.dns4",
|
||||
"net.wlan0.dns1", "net.wlan0.dns2", "net.wlan0.dns3", "net.wlan0.dns4",
|
||||
"net.eth0.dns1", "net.eth0.dns2", "net.eth0.dns3", "net.eth0.dns4",
|
||||
"dhcp.wlan0.dns1", "dhcp.wlan0.dns2", "dhcp.wlan0.dns3", "dhcp.wlan0.dns4",
|
||||
"dhcp.tiwlan0.dns1", "dhcp.tiwlan0.dns2", "dhcp.tiwlan0.dns3", "dhcp.tiwlan0.dns4"}) {
|
||||
try {
|
||||
String value = (String) method.invoke(null, name);
|
||||
if (value != null && !value.isEmpty() && !servers.contains(value)) {
|
||||
servers.add(value);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
WifiManager wMgr = (WifiManager) ctx.getSystemService(Context.WIFI_SERVICE);
|
||||
if (wMgr != null) {
|
||||
DhcpInfo dhcp = wMgr.getDhcpInfo();
|
||||
if (dhcp.dns1 != 0) {
|
||||
String value = intToInetString(dhcp.dns1);
|
||||
if (value != null && !value.isEmpty() && !servers.contains(value)) {
|
||||
servers.add(value);
|
||||
}
|
||||
}
|
||||
if (dhcp.dns2 != 0) {
|
||||
String value = intToInetString(dhcp.dns2);
|
||||
if (value != null && !value.isEmpty() && !servers.contains(value)) {
|
||||
servers.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return String.join(" ", servers);
|
||||
case ConnectivityManager.TYPE_MOBILE:
|
||||
case ConnectivityManager.TYPE_MOBILE_HIPRI:
|
||||
for (String name : new String[]{
|
||||
"net.rmnet0.dns1", "net.rmnet0.dns2", "net.rmnet0.dns3", "net.rmnet0.dns4",
|
||||
"net.rmnet1.dns1", "net.rmnet1.dns2", "net.rmnet1.dns3", "net.rmnet1.dns4",
|
||||
"net.rmnet2.dns1", "net.rmnet2.dns2", "net.rmnet2.dns3", "net.rmnet2.dns4",
|
||||
"net.rmnet3.dns1", "net.rmnet3.dns2", "net.rmnet3.dns3", "net.rmnet3.dns4",
|
||||
"net.rmnet4.dns1", "net.rmnet4.dns2", "net.rmnet4.dns3", "net.rmnet4.dns4",
|
||||
"net.rmnet5.dns1", "net.rmnet5.dns2", "net.rmnet5.dns3", "net.rmnet5.dns4",
|
||||
"net.rmnet6.dns1", "net.rmnet6.dns2", "net.rmnet6.dns3", "net.rmnet6.dns4",
|
||||
"net.rmnet7.dns1", "net.rmnet7.dns2", "net.rmnet7.dns3", "net.rmnet7.dns4",
|
||||
"net.pdp0.dns1", "net.pdp0.dns2", "net.pdp0.dns3", "net.pdp0.dns4",
|
||||
"net.pdpbr0.dns1", "net.pdpbr0.dns2", "net.pdpbr0.dns3", "net.pdpbr0.dns4"}) {
|
||||
try {
|
||||
String value = (String) method.invoke(null, name);
|
||||
if (value != null && !value.isEmpty() && !servers.contains(value)) {
|
||||
servers.add(value);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
// getPreferabilityForNetwork is a utility routine which implements a priority for
|
||||
// different types of network transport, used in a heuristic to pick DNS servers to use.
|
||||
int getPreferabilityForNetwork(ConnectivityManager cMgr, Network network) {
|
||||
NetworkCapabilities nc = cMgr.getNetworkCapabilities(network);
|
||||
|
||||
if (nc == null) {
|
||||
return -1;
|
||||
}
|
||||
if (nc.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
|
||||
// tun0 has both VPN and WIFI set, have to check VPN first and return.
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (nc.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
|
||||
return 0;
|
||||
} else if (nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
|
||||
return 1;
|
||||
} else if (nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
|
||||
return 2;
|
||||
} else {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue