如何获取Android设备的当前DNS服务器?

33

我正在尝试获取应用程序中当前使用的DNS服务器的地址,无论是通过Wifi还是移动网络连接。 DhcpInfo对象应该提供此信息,但我如何获得有效的DhcpInfo对象?

8个回答

27

调用getRuntime().exec可能会导致应用程序挂起

android.net.NetworkUtils.runDhcp()会导致不必要的网络请求。

所以我更喜欢这样做:

Class<?> SystemProperties = Class.forName("android.os.SystemProperties");
Method method = SystemProperties.getMethod("get", new Class[] { String.class });
ArrayList<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 && !"".equals(value) && !servers.contains(value))
        servers.add(value);
}

如果您正在AOSP(系统应用程序或库)中工作,可以直接导入android.os.SystemProperties并使用public static String get(String key, String def) - Arnaud Courtecuisse
4
似乎在安卓 Oreo 中不再允许这样做。这方面有任何更新吗? - atsakiridis
1
是的,这在奥利奥中不再被允许。 - Hades
1
我可以确认这种方法在Android 8.0中已经不再起作用。官方的Android文档在文章《Android 8.0行为变更》中非常明确地说明了这一点。系统属性net.dns1、net.dns2、net.dns3和net.dns4已不再可用,这一改变提高了平台的隐私保护能力。https://developer.android.com/about/versions/oreo/android-8.0-changes.html#o-pri - Grigore Madalin
它适用于 mDNS 吗? - Varun

21

不幸的是,大多数提供的解决方案在Android 8.0上已经无法使用了。

官方的Android文档在文章“Android 8.0行为变化”中非常清楚地说明了这一点。系统属性net.dns1、net.dns2、net.dns3和net.dns4不再可用,这一改变可以提高平台的隐私保护。

https://developer.android.com/about/versions/oreo/android-8.0-changes.html#o-pri

Dnsjava库也受到影响,dnsjava中使用的检测方法没有意识到Oreo的更改。

Varun Anand的解决方案在Oreo上工作,但存在处理与默认路由连接的弱点。因此,结果可能会被无效的DNS服务器污染,并且调用者可能会花费很多时间迭代列表并尝试连接到无法访问的DNS服务器。这在我的解决方案中得到了修复。Varun Anand解决方案的另一个问题是,它仅适用于API 21及以上版本。但是我必须说,这对我来说是个宝藏,让我能够编写自己的解决方案。所以谢谢!

为了方便起见,我提供了一个完整的DNS服务器检测类,您可以在任何Android版本上使用。完整的注释包括了回答为什么和如何的内容。

/**
 * DNS servers detector
 *
 * IMPORTANT: don't cache the result.
 *
 * Or if you want to cache the result make sure you invalidate the cache
 * on any network change.
 *
 * It is always better to use a new instance of the detector when you need
 * current DNS servers otherwise you may get into troubles because of invalid/changed
 * DNS servers.
 *
 * This class combines various methods and solutions from:
 * Dnsjava http://www.xbill.org/dnsjava/
 * Minidns https://github.com/MiniDNS/minidns
 *
 * Unfortunately both libraries are not aware of Orero changes so new method was added to fix this.
 *
 * Created by Madalin Grigore-Enescu on 2/24/18.
 */

public class DnsServersDetector {

    private static final String TAG = "DnsServersDetector";

    /**
     * Holds some default DNS servers used in case all DNS servers detection methods fail.
     * Can be set to null if you want caller to fail in this situation.
     */
    private static final String[] FACTORY_DNS_SERVERS = {
            "8.8.8.8",
            "8.8.4.4"
    };

    /**
     * Properties delimiter used in exec method of DNS servers detection
     */
    private static final String METHOD_EXEC_PROP_DELIM = "]: [";

    /**
     * Holds context this was created under
     */
    private Context context;

    //region - public //////////////////////////////////////////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////////////////

    /**
     * Constructor
     */
    public DnsServersDetector(Context context) {

        this.context = context;

    }

    /**
     * Returns android DNS servers used for current connected network
     * @return Dns servers array
     */
    public String [] getServers() {

        // Will hold the consecutive result
        String[] result;

        // METHOD 1: old deprecated system properties
        result = getServersMethodSystemProperties();
        if (result != null && result.length > 0) {

            return result;

        }

        // METHOD 2 - use connectivity manager
        result = getServersMethodConnectivityManager();
        if (result != null && result.length > 0) {

            return result;

        }

        // LAST METHOD: detect android DNS servers by executing getprop string command in a separate process
        // This method fortunately works in Oreo too but many people may want to avoid exec
        // so it's used only as a failsafe scenario
        result = getServersMethodExec();
        if (result != null && result.length > 0) {

            return result;

        }

        // Fall back on factory DNS servers
        return FACTORY_DNS_SERVERS;

    }

    //endregion

    //region - private /////////////////////////////////////////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////////////////

    /**
     * Detect android DNS servers by using connectivity manager
     *
     * This method is working in android LOLLIPOP or later
     *
     * @return Dns servers array
     */
    private String [] getServersMethodConnectivityManager() {

        // This code only works on LOLLIPOP and higher
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {

            try {

                ArrayList<String> priorityServersArrayList  = new ArrayList<>();
                ArrayList<String> serversArrayList          = new ArrayList<>();

                ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(CONNECTIVITY_SERVICE);
                if (connectivityManager != null) {

                    // Iterate all networks
                    // Notice that android LOLLIPOP or higher allow iterating multiple connected networks of SAME type
                    for (Network network : connectivityManager.getAllNetworks()) {

                        NetworkInfo networkInfo = connectivityManager.getNetworkInfo(network);
                        if (networkInfo.isConnected()) {

                            LinkProperties linkProperties    = connectivityManager.getLinkProperties(network);
                            List<InetAddress> dnsServersList = linkProperties.getDnsServers();

                            // Prioritize the DNS servers for link which have a default route
                            if (linkPropertiesHasDefaultRoute(linkProperties)) {

                                for (InetAddress element: dnsServersList) {

                                    String dnsHost = element.getHostAddress();
                                    priorityServersArrayList.add(dnsHost);

                                }

                            } else {

                                for (InetAddress element: dnsServersList) {

                                    String dnsHost = element.getHostAddress();
                                    serversArrayList.add(dnsHost);

                                }

                            }

                        }

                    }

                }

                // Append secondary arrays only if priority is empty
                if (priorityServersArrayList.isEmpty()) {

                    priorityServersArrayList.addAll(serversArrayList);

                }

                // Stop here if we have at least one DNS server
                if (priorityServersArrayList.size() > 0) {

                    return priorityServersArrayList.toArray(new String[0]);

                }

            } catch (Exception ex) {

                Log.d(TAG, "Exception detecting DNS servers using ConnectivityManager method", ex);

            }

        }

        // Failure
        return null;

    }

    /**
     * Detect android DNS servers by using old deprecated system properties
     *
     * This method is NOT working anymore in Android 8.0
     * Official Android documentation state this in the article Android 8.0 Behavior Changes.
     * The system properties net.dns1, net.dns2, net.dns3, and net.dns4 are no longer available,
     * a change that improves privacy on the platform.
     *
     * https://developer.android.com/about/versions/oreo/android-8.0-changes.html#o-pri
     * @return Dns servers array
     */
    private String [] getServersMethodSystemProperties() {


        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {

            // This originally looked for all lines containing .dns; but
            // http://code.google.com/p/android/issues/detail?id=2207#c73
            // indicates that net.dns* should always be the active nameservers, so
            // we use those.
            final String re1 = "^\\d+(\\.\\d+){3}$";
            final String re2 = "^[0-9a-f]+(:[0-9a-f]*)+:[0-9a-f]+$";
            ArrayList<String> serversArrayList = new ArrayList<>();
            try {

                Class SystemProperties = Class.forName("android.os.SystemProperties");
                Method method = SystemProperties.getMethod("get", new Class[]{String.class});
                final String[] netdns = new String[]{"net.dns1", "net.dns2", "net.dns3", "net.dns4"};
                for (int i = 0; i < netdns.length; i++) {

                    Object[] args = new Object[]{netdns[i]};
                    String v = (String) method.invoke(null, args);
                    if (v != null && (v.matches(re1) || v.matches(re2)) && !serversArrayList.contains(v)) {
                        serversArrayList.add(v);
                    }

                }

                // Stop here if we have at least one DNS server
                if (serversArrayList.size() > 0) {

                    return serversArrayList.toArray(new String[0]);

                }

            } catch (Exception ex) {

                Log.d(TAG, "Exception detecting DNS servers using SystemProperties method", ex);

            }

        }

        // Failed
        return null;

    }

    /**
     * Detect android DNS servers by executing getprop string command in a separate process
     *
     * Notice there is an android bug when Runtime.exec() hangs without providing a Process object.
     * This problem is fixed in Jelly Bean (Android 4.1) but not in ICS (4.0.4) and probably it will never be fixed in ICS.
     * https://dev59.com/hmoy5IYBdhLWcg3wPLn0#11362081
     *
     * @return Dns servers array
     */
    private String [] getServersMethodExec() {

        // We are on the safe side and avoid any bug
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {

            try {

                Process process = Runtime.getRuntime().exec("getprop");
                InputStream inputStream = process.getInputStream();
                LineNumberReader lineNumberReader = new LineNumberReader(new InputStreamReader(inputStream));
                Set<String> serversSet = methodExecParseProps(lineNumberReader);
                if (serversSet != null && serversSet.size() > 0) {

                    return serversSet.toArray(new String[0]);

                }

            } catch (Exception ex) {

                Log.d(TAG, "Exception in getServersMethodExec", ex);

            }

        }

        // Failed
        return null;

    }

    /**
     * Parse properties produced by executing getprop command
     * @param lineNumberReader
     * @return Set of parsed properties
     * @throws Exception
     */
    private Set<String> methodExecParseProps(BufferedReader lineNumberReader) throws Exception {

        String line;
        Set<String> serversSet = new HashSet<String>(10);

        while ((line = lineNumberReader.readLine()) != null) {
            int split = line.indexOf(METHOD_EXEC_PROP_DELIM);
            if (split == -1) {
                continue;
            }
            String property = line.substring(1, split);

            int valueStart  = split + METHOD_EXEC_PROP_DELIM.length();
            int valueEnd    = line.length() - 1;
            if (valueEnd < valueStart) {

                // This can happen if a newline sneaks in as the first character of the property value. For example
                // "[propName]: [\n…]".
                Log.d(TAG, "Malformed property detected: \"" + line + '"');
                continue;

            }

            String value = line.substring(valueStart, valueEnd);

            if (value.isEmpty()) {

                continue;

            }

            if (property.endsWith(".dns") || property.endsWith(".dns1") ||
                    property.endsWith(".dns2") || property.endsWith(".dns3") ||
                    property.endsWith(".dns4")) {

                // normalize the address
                InetAddress ip = InetAddress.getByName(value);
                if (ip == null) continue;
                value = ip.getHostAddress();

                if (value == null) continue;
                if (value.length() == 0) continue;

                serversSet.add(value);

            }

        }

        return serversSet;

    }

    /**
     * Returns true if the specified link properties have any default route
     * @param linkProperties
     * @return true if the specified link properties have default route or false otherwise
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private boolean linkPropertiesHasDefaultRoute(LinkProperties linkProperties) {

        for (RouteInfo route : linkProperties.getRoutes()) {
            if (route.isDefaultRoute()) {
                return true;
            }
        }
        return false;

    }

    //endregion

}

1
这个救了我。对于将来使用dnsjava的任何人,在ResolverConfig.findAndroid中调用此例程,将结果添加到lserver列表中。 - urSus
这将始终返回所有活动连接的所有DNS服务器。如果您在WiFi网络上,而蜂窝连接仍然处于活动状态,则会返回两个连接的DNS服务器,但只有WiFi连接的服务器有效。 - Chrissi
如何使用这个类? - zeleven
connectivityManager.getNetworkInfo已经过时。我尝试使用NetworkCallbacks来解决获取dns的问题。 - Manikandan
@Manikandan,如果您能做到,请提供解决方案,这样我就可以编辑我的答案并包含您的研究。我也会研究您在此处写的内容,并尝试更新我的代码(如果有必要)。 - Grigore Madalin
getprop不再返回.dns属性。在Android 10上进行了检查。 - Hsilgos

13

android.net.ConnectivityManager会使用getAllNetworkInfo()方法向您提供一系列的NetworkInfo。然后,您可以使用android.net.NetworkUtils.runDhcp()方法获取给定网络接口的DhcpInfo - DhcpInfo结构体包含该接口的dns1dns2的IP地址(这些是表示IP地址的整数值)。

如果你想知道如何将整数转换成IP地址,你可以这样做:

/**
* Convert int IP adress to String 
* cf. http://teneo.wordpress.com/2008/12/23/java-ip-address-to-integer-and-back/
*/
private String intToIp(int i) {
    return ( i & 0xFF) + "." +
        (( i >> 8 ) & 0xFF) + "." +
        (( i >> 16 ) & 0xFF) + "." +
        (( i >> 24 ) & 0xFF);
}

编辑

你也可以通过类似下面的方式获取一个DchcpInfo对象:

WiFiManager wifi = (WifiManager) getSystemService(WIFI_SERVICE); 
DhcpInfo info = wifi.getDhcpInfo();

1
但是android.net.NetworkUtils.runDhcp()不是本地函数吗?我该如何访问它? - John
8
从Wifi获取DhcpInfo很简单,但是如何在移动网络(如UMTS)上实现呢? 我是否需要加载一些本地库才能访问android.net.NetworkUtils.runDhcp()? - John
@John - 我已经发布了适用于移动网络的本地解决方案。看一下。 - Grimmace

11

以下代码适用于API 21及以上版本。它可以返回WiFi和移动数据接口的正确DNS服务器地址。我已使用shell实用程序"getprop"验证了所返回数值的正确性。

ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
    for (Network network : connectivityManager.getAllNetworks()) {
        NetworkInfo networkInfo = connectivityManager.getNetworkInfo(network);
        if (networkInfo.isConnected()) {
            LinkProperties linkProperties = connectivityManager.getLinkProperties(network);
            Log.d("DnsInfo", "iface = " + linkProperties.getInterfaceName());
            Log.d("DnsInfo", "dns = " + linkProperties.getDnsServers());
            return linkProperties.getDnsServers();
        }
    }

这是一个为我提供自己的解决方案的宝藏,可以处理所有版本。请检查我的答案。再次感谢! - Grigore Madalin

2

一种本地的替代方案是:

char dns1[PROP_VALUE_MAX];
__system_property_get("net.dns1", dns1);

或者更好的做法是提供一份全面的清单:
for (i = 1; i <= MAX_DNS_PROPERTIES; i++) {
    char prop_name[PROP_NAME_MAX];
    snprintf(prop_name, sizeof(prop_name), "net.dns%d", i);
    __system_property_get(prop_name, dns);
}

这种方法有几个优点:
  1. runDHCP非常慢。它可能需要5-10秒钟的时间。如果使用不当,这可能会导致主要的挂起。
  2. runDCHP对3G/4G似乎无效。
  3. 由于runDCHP是一个隐藏的API,因此它可能会发生变化。事实上,在ICS中已经发生了变化。在ICS中,它采用了新的DhcpInfoInternal,因此您必须创建两个不同的来支持所有手机。

6
Android L中已经移除了__system_property_get()函数。 - Ales Teska

2

我建议在Android上进行复杂的DNS使用时使用dnsjava。让我们看看dnsjava如何确定连接的当前活动DNS服务器。从dnsjava ResolverConfig.java:428中可以看到:

/**
 * Parses the output of getprop, which is the only way to get DNS
 * info on Android. getprop might disappear in future releases, so
 * this code comes with a use-by date.
 */
private void
findAndroid() {
    // This originally looked for all lines containing .dns; but
    // http://code.google.com/p/android/issues/detail?id=2207#c73
    // indicates that net.dns* should always be the active nameservers, so
    // we use those.
    String re1 = "^\\d+(\\.\\d+){3}$";
    String re2 = "^[0-9a-f]+(:[0-9a-f]*)+:[0-9a-f]+$";
    try { 
        ArrayList lserver = new ArrayList(); 
        ArrayList lsearch = new ArrayList(); 
        String line; 
        Process p = Runtime.getRuntime().exec("getprop"); 
        InputStream in = p.getInputStream();
        InputStreamReader isr = new InputStreamReader(in);
        BufferedReader br = new BufferedReader(isr);
        while ((line = br.readLine()) != null ) { 
            StringTokenizer t = new StringTokenizer(line, ":");
            String name = t.nextToken();
            if (name.indexOf( "net.dns" ) > -1) {
                String v = t.nextToken();
                v = v.replaceAll("[ \\[\\]]", "");
                if ((v.matches(re1) || v.matches(re2)) &&
                    !lserver.contains(v))
                    lserver.add(v);
            }
        }
        configureFromLists(lserver, lsearch);
    } catch ( Exception e ) { 
        // ignore resolutely
    }
}

警告:此代码可能导致Runtime.getRuntime().exec()中的挂起。 - Nappy

1
首先将layoutlib.jar添加到您的构建路径中,该文件位于$SDK_PATH/platforms/android-xxx/data/。
String dnsStr1 = android.os.SystemProperties.get("net.dns1");
String dnsStr2 = android.os.SystemProperties.get("net.dns2");

您还可以使用 $getprop 命令在 adb shell 中查看所有属性。


-2

您可以使用Java反射。例如:

 ConnectivityManager mgr =
 (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);          
 Method getLinkPropeties;
 try{
 getLinkPropeties = mgr.getClass().getMethod("getLinkProperties", int.class);
 }catch (InvocationTargetException e) {
     e.printStackTrace();
 }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接