How Samsung phones force Mainland China DNS service upon Hong Kong WiFi users (Part 2) – HeaDuck研究所


Following Part 1 on forcing Mainland China DNS service upon Hong Kong WiFi users, this part analyzes how the periodic DNS queries on qq.com, as seen by users capturing the DNS queries, are generated from the recent Samsung phones firmware (as at Sept 2020) when WiFi is used.

tl;dr: These queries are sent simultaneously to all DNS servers registered for the WiFi connection, including 114.114.114.114 when it is added under circumstances as examined in Part 1. They are generated every 20 to 60 seconds (depending on settings and link conditions) when the following pre-conditions are satisfied, i.e. WiFi is connected (except with some known devices) with mCurrentMode not equals 0 (elaborated at the bottom of this article), and when screen is on.  The queries are sent directly using the WiFi connection link, without going through the VPN / private DNS configured, presumably as they are intended for testing the WiFi network.  The IP addresses returned by the queries are not further used, except for checking if they are in a private IP range (in which case the checking is deemed failed).

The following is a technical (read: very boring) account of the findings. Jump to the end for hints to disable the DNS query for your home WiFi.

For better illustration of the source, a new Github repository showing the decompiled version of the same wifi-service.jar as in Part I has been created. It uses a different mode of the decompiler (JADX) to attempt to decompile more code to Java, even when there are issues in correctness. The repository is at SM-N9750-TGY-1.

The main players

The main controlling class for the logic is again under com.android.server.wifi.WifiConnectivityMonitor, in the file WifiConnectivityMonitor.java.

As briefly mentioned in the last part, this class implements a state machine which represents the status of the WiFi connection being monitored. The states accept messages and respond to them by taking actions and / or transition into another state.

Separately, a subclass, NetworkStatsAnalyzer, is responsible for the monitoring of the connectivity and signal strength. The analyzer is an Android Handler which consumes messages in an event loop. These messages can be sent by the parent state machine, from within the analyzer itself, or from other subclasses, to control the activities of the analyzer.

The most relevant messages in our context are defined as follows (unless otherwise specified, the line numbers are those of the WifiConnectivityMonitor.java file in the decompiled code):

Messages sent to the Analyzer to control the lifecycle of the checking loop (lines 115-117, 254):

 private static final int ACTIVITY_CHECK_POLL = 135221; private static final int ACTIVITY_CHECK_START = 135219; private static final int ACTIVITY_CHECK_STOP = 135220; ... private static final int NETWORK_STAT_CHECK_DNS = 135223; 

Messages representing external events sent to the WifiConnectivityMonitor state machine (lines 205-206).

 private static final int EVENT_SCREEN_OFF = 135177; private static final int EVENT_SCREEN_ON = 135176; 

Let the query loop start

When the phone screen is turned on, Android will send the android.intent.action.SCREEN_ON intent to the network receiver of this class (line 1149), which will in turn send EVENT_SCREEN_ON to the WifiConnectivityMonitor state machine. If the state is under EvaluatedState [Note 1], the processMessage() method, around line 2138, handles the message (for brevity, some reference to WifiConnectivityMonitor in the code is omitted):

 case EVENT_SCREEN_ON: //135176 ... if (mCurrentMode != 0) { sendMessage(obtainMessage(135188 /* CMD_RSSI_FETCH */, mRssiFetchToken, 0) ; if (isValidState() && getCurrentState() != mLevel2State) { if (mNetworkStatsAnalyzer != null) { mNetworkStatsAnalyzer.sendEmptyMessage(ACTIVITY_CHECK_START); // 135219 } startEleCheck(); } if (mCurrentMode == 1 || mLinkDetectMode == 1) { sendMessage(obtainMessage(CMD_TRAFFIC_POLL/*135193*/, mTrafficPollToken, 0); } } ... 

This means that, when mCurrentMode is non-zero, WiFi is connected, and the connection is not with some known devices, it will send the ACTIVITY_CHECK_START message to the NetworkStatsAnalyzer. (It will also perform other actions, like calling startEleCheck() to discover devices, but these are not relevant to our context.)

The message ACTIVITY_CHECK_START is also sent to the NetworkStatsAnalyzer from a few other places. Here is an extract of the enter() method of the Valid state, triggered when WifiConnectivityMonitor enters Valid state (i.e. upon WiFi connection) (line 3077-).

if (mCurrentMode != 0) { ... mNetworkStatsAnalyzer.sendEmptyMessage(ACTIVITY_CHECK_START); mNetworkStatsAnalyzer.sendEmptyMessage(NETWORK_STAT_CHECK_DNS); }

ACTIVITY_CHECK_START is also sent when mCurrentMode switches from 0 to other values under Valid state (line 3112).  Together, it is ensured that all paths arriving at the status, i.e. Wifi is connected, screen is on, and mCurrentMode is non-zero, will trigger ACTIVITY_CHECK_START.

Note that upon entering the Valid state, NETWORK_STAT_CHECK_DNS is also sent to the NetworkStatsAnalyzer. Let’s look at what this message does first.

The NetworkStatsAnalyzer

The “loop” structure

The below shows a skeleton of the NetworkStatsAnalyzer handler loop handling the above messages, with bodies of ACTIVITY_CHECK_POLL and ACTIVITY_CHECK_STOP omitted for the moment. (lines 4157-, 4477-, 5107-)

private class NetworkStatsAnalyzer extends Handler { ... private boolean mDnsInterrupted = false; private boolean mDnsQueried = false; private long mLastDnsCheckTime = 0; private boolean mPollingStarted = false; private boolean mPublicDnsCheckProcess = false; private boolean mSkipRemainingDnsResults = false; ... public void handleMessage(Message string) { long now = SystemClock.elapsedRealtime(); final long elapsedRealtime = SystemClock.elapsedRealtime(); final WifiInfo wifiInfo = WifiConnectivityMonitor.this.syncGetCurrentWifiInfo(); final int what = string.what; switch (what) { ... case ACTIVITY_CHECK_START: if (this.mPollingStarted) break; if (WifiConnectivityMonitor.this.isMobileHotspot()) break; if (this.isBackhaulDetectionEnabled()) { this.sendEmptyMessage(TCP_BACKHAUL_DETECTION_START); // 135226 } this.sendEmptyMessage(ACTIVITY_CHECK_POLL); WifiConnectivityMonitor.this.initNetworkStatHistory(); this.mLastRssi = wifiInfo.getRssi(); this.mPollingStarted = true; break; case ACTIVITY_CHECK_POLL: ... break; case ACTIVITY_CHECK_STOP: ... break; case NETWORK_STAT_CHECK_DNS: if (!WifiConnectivityMonitor.this.isMobileHotspot()) { checkPublicDns(); } break; } } } 

The message NETWORK_STAT_CHECK_DNS, sent upon WiFi connection, will invoke the method checkPublicDns() if the WiFi is not a mobile hotspot. The method is as follows (line 4262-).

public void checkPublicDns() { if (WifiConnectivityMonitor.this.inChinaNetwork()) { mPublicDnsCheckProcess = false; return; } mPublicDnsCheckProcess = true; mNsaQcStep = 1; WifiConnectivityMonitor wifiConnectivityMonitor = WifiConnectivityMonitor.this; String str = wifiConnectivityMonitor.mParam.DEFAULT_URL_STRING; DnsThread mDnsThread = new DnsThread(true, str, this, 10000); mDnsThread.start(); WifiConnectivityMonitor.this.mDnsThreadID = mDnsThread.getId(); if (WifiConnectivityMonitor.DBG) { Log.d(TAG, "wait publicDnsThread results [" + WifiConnectivityMonitor.this.mDnsThreadID + "]"); } }

We see the problematic inChinaNetwork() method as discussed in Part I being used. It can be seen that the above code handles the “normal” case outside China. Upon WiFi connection, it will start an asynchronous DNS query in a separate thread called DnsThread, using the address in the constant DEFAULT_URL_STRING (hardcoded to “www.google.com” as shown in Part I). This is done once at the start of every WiFi connection. However, if inChinaNetwork() returns true, e.g. in the case of a phone in Hong Kong, it would just set the flag mPublicDnsCheckProcess to false and return, skipping the DNS query for google.

Then we look at ACTIVITY_CHECK_START. On receipt of ACTIVITY_CHECK_START, the NetworkStatsAnalyzer, among other actions, sends the ACTIVITY_CHECK_POLL message to itself, and sets the flag mPollingStarted to true. This would kick start the checking loop, as to be explained below. Before taking these actions, it would ensure that the loop is not already started by checking mPollingStarted, and that the WiFi connection is not acting as a hotspot.

The main flow controlling logic of the body of ACTIVITY_CHECK_POLL polling loop is as follows (line 4509-).

case ACTIVITY_CHECK_POLL: if (WifiConnectivityMonitor.this.SMARTCM_DBG) { Log.i(TAG, "mPollingStarted : " + mPollingStarted); } if (!mPollingStarted) { break; } if ((WifiConnectivityMonitor.this.mCurrentBssid != null) && (WifiConnectivityMonitor.this.mCurrentBssid != WifiConnectivityMonitor.this.mEmptyBssid)) { WifiConnectivityMonitor.this.mIWCChannel.sendMessage(CMD_IWC_ACTIVITY_CHECK_POLL); // 135376 int rssi2 = wifiInfo.getRssi(); if (rssi2 < -90) { if (!WifiConnectivityMonitor.this.mClientModeImpl.isConnected()) { Log.i(TAG, "already disconnected : " + rssi2); removeMessages(ACTIVITY_CHECK_POLL); sendEmptyMessage(ACTIVITY_CHECK_STOP); break; } if (rssi2 < -95) { if (rssi2 == -127) break; rssi2 = -95; } } /* * Checking body omitted for the time being */ removeMessages(ACTIVITY_CHECK_POLL); sendEmptyMessageDelayed(ACTIVITY_CHECK_POLL, 1000L); ... break; } Log.e(TAG, "currentBssid is null."); this.removeMessages(ACTIVITY_CHECK_POLL); this.sendEmptyMessage(ACTIVITY_CHECK_STOP); break; 

The code above ensures that the mPollingStarted flag remains on and WiFi remains connected (by checking if BSSID is empty, or signal is weak and status returned from ClientModeImpl indicates disconnection), before proceeding with various checking (details below).

After each run of the checking code, it resends the ACTIVITY_CHECK_POLL message to itself with a delay of 1000 ms (i.e. 1 second), essentially forming a loop running once per second. In other words, Samsung phones keep running this checking loop, which is not found on standard Android phones, once every second when both screen and WiFi is on! (Furthermore, it also runs another similar loop for “backhaul detection” if mCurrentMode >= 2, not covered here).

On the other hand, if WiFi has been disconnected, the loop will terminate – ACTIVITY_CHECK_POLL is not sent again, and instead ACTIVITY_CHECK_STOP is sent to itself.

For completeness, here is the body of code handling the ACTIVITY_CHECK_STOP message (line 4492-):

case ACTIVITY_CHECK_STOP: removeMessages(ACTIVITY_CHECK_POLL); removeMessages(TCP_BACKHAUL_DETECTION_START); // 135226 mPollingStarted = false; mPublicDnsCheckProcess = false; ... mDnsQueried = false; mDnsInterrupted = false; ... break; 

The above code removes any pending ACTIVITY_CHECK_POLL messages and resets the mPollingStarted flag and other flags.

The main checking – preconditions

Back to the main checking body omitted above, after doing some statistics concerning packet throughput and signal strength (not covered here), the code checks for the need to do a DNS query test (line 4618-, please note that the decompiled code has some issues in expanding the nested structure. The code extract below has corrected the issue).

if (!WifiConnectivityMonitor.this.mIsScanning && !WifiConnectivityMonitor.this.mIsInRoamSession && !WifiConnectivityMonitor.this.mIsInDhcpSession && WifiConnectivityMonitor.this.mIsScreenOn) { if (!this.mPublicDnsCheckProcess && WifiConnectivityMonitor.this.mCurrentMode != 0){ if (this.mDnsQueried) { // If DNS query is ongoing .... if (WifiConnectivityMonitor.SMARTCM_DBG) { Log.i(TAG, "waiting dns responses or the quality result now!"); } boolean stopQC = false; if (WifiConnectivityMonitor.this.mCurrentMode == 3) { if (WifiConnectivityMonitor.this.mInAggGoodStateNow) { stopQC = true; } } else if (diffRx >= ((long) WifiConnectivityMonitor.this.mParam.mGoodRxPacketsBase) && rxBytesPerPacket > 500) { stopQC = true; } else if (diffTxBytes >= 100000) { stopQC = true; } if (stopQC) { if (WifiConnectivityMonitor.SMARTCM_DBG) { Log.i(TAG, "Good Rx!, don't need to keep evaluating quality!"); } if (mDnsQueried) { mSkipRemainingDnsResults = true; mDnsQueried = false; mDnsInterrupted = false; } } } else { /* * Further code to check DNS, see below */ } } }

We see some further preconditions for performing the DNS query test here:

  • WiFi scanning is not in progress;
  • WiFi roaming is not in progress;
  • DHCP query is not in progress;
  • Screen is turned on;
  • mPublicDnsCheckProcess is false, i.e. not doing the initial network check above; and
  • mCurrentMode != 0

The first 3 conditions is for avoiding the DNS query under some transient conditions when the network may change. Item 5 avoids doing the initial check and the periodic check at the same time. Other conditions are for revalidation of the conditions.

In addition, if mDnsQueried is true, the previous DNS query is still ongoing, and a new query will not be considered.

Instead, if it is determined that the connection is in a good state (using several metrics which are not elaborated here), it will set the flag mSkipRemainingDnsResults to true. From a literal reading of the log messages and variable names, it seems that the intention is to perform DNS query only once if the network is in good condition. However, it is noted that nothing in the code actually reads the mSkipRemainingDnsResults flag! The DNS checking queries would continue to be issued in the loop, even when the connection is good and stable. It is not known whether this is a bug, or is deliberate despite the comments, say, due to a subsequent design change.

The core checking section

The core part of the checking loop is as follows (lines 4720-, 4896-)

if (WifiConnectivityMonitor.this.mCurrentMode != 3 || !WifiConnectivityMonitor.this.mInAggGoodStateNow) { if (diffRx > 0 || diffTx > 0) { if (now - mLastDnsCheckTime > ((long) (WifiConnectivityMonitor.this.mCurrentMode == 3 ? 30000 : 60000))) { if (WifiConnectivityMonitor.SMARTCM_DBG) { Log.d(TAG, "PERIODIC DNS CHECK TRIGGER (SIMPLE CONNECTION TEST) - Last DNS check was " + ((now4 - mLastDnsCheckTime) / 1000) + " seconds ago."); } mNsaQcTrigger = 44; needCheckInternetIsAlive = true; } } // Other conditions setting the flag needCheckInternetIsAlive; if (needCheckInternetIsAlive) { if (now - mLastDnsCheckTime >= 20000) { mCumulativePoorRx.clear(); mSkipRemainingDnsResults = false; mDnsQueried = true; mNsaQcStep = 1; int timeoutMS = 10000; if (WifiConnectivityMonitor.this.mCurrentMode == 3) { timeoutMS = 5000; } String dnsTargetUrl2 = WifiConnectivityMonitor.this.mParam.DEFAULT_URL_STRING; if (WifiConnectivityMonitor.this.inChinaNetwork()) { dnsTargetUrl = "www.qq.com"; } else { dnsTargetUrl = dnsTargetUrl2; } DnsThread mDnsThread = new DnsThread(true, dnsTargetUrl, this, (long) timeoutMS); mDnsThread.start(); mLastDnsCheckTime = now; WifiConnectivityMonitor.this.mDnsThreadID = mDnsThread.getId(); if (WifiConnectivityMonitor.DBG) { Log.d(TAG, "wait needCheck DnsThread results [" + WifiConnectivityMonitor.this.mDnsThreadID + "]"); } } } }

The first part determines the “minimum” frequency to query the DNS on qq.com, so long as there are any WiFi activities. It would set the flag needCheckInternetIsAlive to true if it is more than 60 seconds since the last DNS check normally, but if mCurrentMode == 3 which means “Aggressive WiFi to cellular handover” mode (i.e. requiring a high WiFi quality or else switch to mobile data, thus needs more aggressive checks), it would shorten the interval to 30 seconds. (But in Aggressive mode, this would only be triggered if connection is not in “good” state defined for Aggressive mode). Remember that this loop is run once per second when the screen is on, which means that the checking would be run approximately every 60 (or 30) seconds, by setting the needCheckInternetIsAlive flag to indicate that a check is needed.

There are some more criteria for switching on the needCheckInternetIsAlive flag in the subsequent code (omitted), based on network metrics gathered. The effect is to increase checking when a poor status of the WiFi network is detected. This might increase the frequency of DNS to qq.com in some cases. But the frequency is limited to at most once per 20 seconds by a later checking.

Finally, the code sets dnsTargetUrl to www.qq.com (assuming inChinaNetwork() is true in our context. Otherwise it would query for DEFAULT_URL_STRING, i.e. google), and performs the query asynchronously using the DnsThread class, as with the “normal” case under checkPublicDns(). It also records the current time for future calculation of time lapsed.

Drilling into DnsThread

Here is an extract of the DnsThread class. (Only the case mForce == true under run() is shown) (line 6855-)

public final class DnsThread extends Thread { ... private final CountDownLatch latch = new CountDownLatch(1); public DnsThread(boolean force, String url, Handler handler, long timeout) { mCallBackHandler = handler; if (timeout >= 1000) { mTimeout = timeout; } mForce = force; mUrl = url; } public void run() { WifiConnectivityMonitor.this.mAnalyticsDisconnectReason = 0; if (mForce) { HandlerThread dnsPingerThread = new HandlerThread("dnsPingerThread"); dnsPingerThread.start(); try { mDnsPingerHandler = new DnsPingerHandler(dnsPingerThread.getLooper(), mCallBackHandler, getId()); mDnsPingerHandler.sendDnsPing(this.mUrl, this.mTimeout); if (!latch.await(mTimeout, TimeUnit.MILLISECONDS)) { if (WifiConnectivityMonitor.DBG) { Log.d(TAG, "DNS_CHECK_TIMEOUT [" + getId() + "-F] - latch timeout"); } mCallBackHandler.sendMessage(WifiConnectivityMonitor.this.obtainMessage(WifiConnectivityMonitor.RESULT_DNS_CHECK, 3, -1, null)); } else { mCallBackHandler.sendMessage(WifiConnectivityMonitor.this.obtainMessage(WifiConnectivityMonitor.RESULT_DNS_CHECK, mForcedCheckResult, mForcedCheckRtt, mForcedCheckAddress)); } } catch (Exception e) { if (WifiConnectivityMonitor.DBG) { Log.d(TAG, "DNS_CHECK_TIMEOUT [" + getId() + "-F] " + e); } mCallBackHandler.sendMessage(WifiConnectivityMonitor.this.obtainMessage(WifiConnectivityMonitor.RESULT_DNS_CHECK, 3, -1, null)); } } else { ... } } }

Essentially, when the run() method of DnsThread is called, it creates an Android HandlerThread, uses its looper to create a new DnsPingerHandler, and calls sendDnsPing() of the DnsPingerHandler to ask it to perform the DNS query. It then waits for the query to complete on this thread using a CountDownLatch with the required timeout, and returns the result via the RESULT_DNS_CHECK message.

The relevant code under DnsPingerHandler(line 7087-):

private class DnsPingerHandler extends Handler { Handler mCallbackHandler; private DnsCheck mDnsPingerCheck; long mId; public DnsPingerHandler(Looper looper, Handler callbackHandler, long id) { super(looper); mDnsPingerCheck = new DnsCheck(this, "WifiConnectivityMonitor.DnsPingerHandler"); mCallbackHandler = callbackHandler; mId = id; } public void sendDnsPing(String url, long timeout) { if (!mDnsPingerCheck.requestDnsQuerying(1, (int) timeout, url)) { if (WifiConnectivityMonitor.DBG) { Log.e(DnsThread.TAG, "DNS List is empty, need to check quality"); } if (DnsThread.this.mCallBackHandler != null) { DnsThread.this.mCallBackHandler.sendMessage(obtainMessage(WifiConnectivityMonitor.RESULT_DNS_CHECK, 3, -1, null)); DnsThread.this.latch.countDown(); } } } }

DnsPingerHandler in turn calls requestDnsQuerying() of the class DnsCheck. The method requestDnsQuerying() under DnsCheck class is listed below. (line 7150-)

public class DnsCheck { private List mDnsServerList = null; private List mDnsList; private DnsPinger mDnsPinger; private HashMap<Integer, Integer> mIdDnsMap = new HashMap<>(); public DnsCheck(Handler handler, String tag) { mDnsPinger = new DnsPinger(WifiConnectivityMonitor.this.mContext, tag, handler.getLooper(), handler, 1); mDnsCheckTAG = tag; mDnsPinger.setCurrentLinkProperties(WifiConnectivityMonitor.this.mLinkProperties); } public boolean requestDnsQuerying(int num, int timeoutMS, String url) { List dnses; boolean requested = false; mDnsList = new ArrayList(); if (!(WifiConnectivityMonitor.this.mLinkProperties == null || (dnses = WifiConnectivityMonitor.this.mLinkProperties.getDnsServers()) == null || dnses.size() == 0)) { mDnsServerList = new ArrayList(dnses); } List dnses2 = mDnsServerList; if (dnses2 != null) { mDnsList.addAll(dnses2); } int numDnses = mDnsList.size(); ... mIdDnsMap.clear(); for (int i2 = 0; i2 < num; i2++) { for (int j = 0; j < numDnses; j++) { try { if (mDnsList.get(j) == null || mDnsList.get(j).isLoopbackAddress()) { Log.d(DnsThread.TAG, "Loopback address (::1) is detected at DNS" + j); } else { if (url == null) { mIdDnsMap.put(Integer.valueOf(mDnsPinger.pingDnsAsync(mDnsList.get(j), timeoutMS, (i2 * 0) + 100)), Integer.valueOf(j)); } else { mIdDnsMap.put(Integer.valueOf(mDnsPinger.pingDnsAsyncSpecificForce(mDnsList.get(j), timeoutMS, (i2 * 0) + 100, url)), Integer.valueOf(j)); } requested = true; } } catch (IndexOutOfBoundsException e2) { if (WifiConnectivityMonitor.DBG) { Log.i(DnsThread.TAG, "IndexOutOfBoundsException"); } } } } if (WifiConnectivityMonitor.SMARTCM_DBG) { Log.i(DnsThread.TAG, "[REQUEST] " + this.mDnsCheckTAG + " : " + this.mIdDnsMap); } return requested; } }

This retrieves all the DNS servers configured for the connection from LinkProperties (a standard Android class). Then it loops through ALL DNS servers, calls the method pingDnsAsyncSpecificForce(this.mDnsList.get(j), timeoutMS, (i2 * 0) + 100, url) for each of them. The method is “asynchronous” as it sends a message within the DnsPinger class and immediately returns. In other words, DNS queries for the url are sent simutaneously to all DNS servers configured.

The method pingDnsAsyncSpecificForce() of class DnsPinger is located under a separate Java file, DnsPinger.java. It performs the actual DNS query with UDP. We are not going into its details here, but it is notable that in the constructor of DnsCheck, it is provided with a specific WiFi connection by using DnsPinger.setCurrentLinkProperties(), and it will send its DNS query directly to the Wifi interface. In other words these DNS queries will disregard the VPN or private DNS settings in the mobile device.

The checking results

Since the process is asynchronous, after DnsPinger finished its job (result of DNS obtained or error occurred), it will send the result using the message DNS_PING_RESULT_SPECIFIC (593925). The result sent back is mainly the time needed for the DNS query, or an error code in case of error.  If a private network address is returned, it would be treated as an error (value = 2) [2].

The DNS_PING_RESULT_SPECIFIC is handled by handleMessage of DnsPingerHandler (line 7087-):

if (WifiConnectivityMonitor.SMARTCM_DBG) { Log.i(DnsThread.TAG, "[DNS_PING_RESULT_SPECIFIC]"); } DnsCheck dnsCheck = mDnsPingerCheck; if (dnsCheck != null) { try { int dnsResult = dnsCheck.checkDnsResult(msg.arg1, msg.arg2, 1); if (dnsResult != 10) { if (WifiConnectivityMonitor.DBG) { Log.d(DnsThread.TAG, "send DNS CHECK Result [" + this.mId + "]"); } DnsThread.this.mForcedCheckResult = dnsResult; DnsThread.this.mForcedCheckRtt = msg.arg2; DnsThread.this.mForcedCheckAddress = (InetAddress) msg.obj; DnsThread.this.latch.countDown(); } else if (WifiConnectivityMonitor.SMARTCM_DBG) { Log.d(DnsThread.TAG, "wait until the responses about remained DNS Request arrive!"); } } catch (NullPointerException ne) { Log.e(DnsThread.TAG, "DnsPingerHandler - " + ne); } }

The error conditions are checked by DnsCheck.checkDnsResult() (not elaborated here). It is notable that the result of the DNS query (i.e. the IP address of qq.com)is not actually used, besides the validity check (to ensure that it is not a private IP address) under DnsPinger above. Finally it issues countDown() to the CountDownLatch under DnsThread, releasing the CountDownLatch there as mentioned above. This would in normal cases cause DnsThread to send a RESULT_DNS_CHECK message, with the test results, back to the NetworkStatsAnalyzer.

The part of NetworkStatsAnalyzer handling the message is as follows (line 4426-):

case RESULT_DNS_CHECK: int mDnsResult = WifiConnectivityMonitor.this.checkDnsThreadResult(message.arg1, message.arg2); mDnsQueried = false; if (mDnsInterrupted) { this.mDnsInterrupted = false; if (WifiConnectivityMonitor.DBG) { Log.d(TAG, "Result: " + mDnsResult + " - This DNS query is interrupted."); } } else if (WifiConnectivityMonitor.this.mIsInDhcpSession || WifiConnectivityMonitor.this.mIsScanning || WifiConnectivityMonitor.this.mIsInRoamSession) { if (WifiConnectivityMonitor.DBG) { Log.d(TAG, "Result: " + mDnsResult + " - This DNS query is interrupted by DHCP session or Scanning."); } } else if (mDnsResult != 0) { if (WifiConnectivityMonitor.DBG) { Log.e(TAG, "single DNS Checking FAILURE"); } if (WifiConnectivityMonitor.this.mCurrentMode != 3 || !WifiConnectivityMonitor.this.mInAggGoodStateNow) { ... } else { if (WifiConnectivityMonitor.DBG) { Log.e(TAG, "But, do not check the quality in AGG good rx state"); } mSkipRemainingDnsResults = true; } } mPublicDnsCheckProcess = false; break;

Processing of the results is mainly done under checkDnsThreadResult(), which ultimately leads to BssidStatistics.updateBssidLatestDnsResultType() to record the result for the connection (line 8021). Afterwards, it housekeeps the relevant flags (mDnsQueried etc). It also sets the useless mSkipRemainingDnsResults flag. (This part also handles the “normal” checkPublicDns() query and thus also clears the mPublicDnsCheckProcess flag).

Ending the DNS queries loop

Finally, back to the top level state machine. When the screen turns off, the EvaluatedState would stop the loop by sending ACTIVITY_CHECK_STOP to the NetworkStatsAnalyzer (line 2177-).

case EVENT_SCREEN_OFF: ... screenOffEleInitialize(); removeMessages(WifiConnectivityMonitor.CMD_RSSI_FETCH); removeMessages(WifiConnectivityMonitor.CMD_TRAFFIC_POLL); mRssiFetchToken++; if (WifiConnectivityMonitor.this.mNetworkStatsAnalyzer == null) { break; } mNetworkStatsAnalyzer.sendEmptyMessage(WifiConnectivityMonitor.ACTIVITY_CHECK_STOP); break;

The Current Mode flag (or how trick the Phone to stop the extra DNS checking)

All along we see the reference to mCurrentMode, which when set to 0 would disable the DNS checking. This is set by the determineMode() method (line 3614-):

private void determineMode() { int i; String ssid = mWifiInfo.getSSID(); if (mCurrentMode != 0) { if (isIgnorableNetwork(ssid)) { setCurrentMode(0); } else if (!mPoorNetworkDetectionEnabled) { setCurrentMode(1); } else if (isQCExceptionOnly()) { if (SMARTCM_DBG) { logi("isQCExceptionOnly"); } setCurrentMode(1); } else if (isAggressiveModeEnabled()) { if (SMARTCM_DBG) { logi("mAggressiveModeEnabled"); } setCurrentMode(3); } else { setCurrentMode(2); } } ... }

A look at isIgnorableNetwork() would therefore provide hints on how to disable the DNS checking loop (line 9611-):

public boolean isIgnorableNetwork(String _ssid) { int reason = -1; String ssid = null; int networkId = -1; WifiInfo wifiInfo = this.mWifiInfo; if (wifiInfo != null && _ssid == null) { ssid = wifiInfo.getSSID(); networkId = this.mWifiInfo.getNetworkId(); } if (ssid == null && _ssid != null) { ssid = _ssid; } WifiConfiguration wifiConfiguration = null; if (networkId != -1) { wifiConfiguration = getWifiConfiguration(networkId); } if ("ATT".equals(SemCscFeature.getInstance().getString(CscFeatureTagWifi.TAG_CSCFEATURE_WIFI_CAPTIVEPORTALEXCEPTION)) && isPackageRunning(this.mContext, "com.synchronoss.dcs.att.r2g")) { reason = 1; } else if (ssid != null && ssid.contains("DIRECT-") && ssid.contains(":NEX-")) { reason = 2; } else if (isPackageRunning(this.mContext, "de.telekom.hotspotlogin")) { reason = 3; } else if (isPackageRunning(this.mContext, "com.belgacom.fon")) { reason = 4; } else if ("CHM".equals(SemCscFeature.getInstance().getString(CscFeatureTagWifi.TAG_CSCFEATURE_WIFI_CAPTIVEPORTALEXCEPTION)) && (isPackageRunning(this.mContext, "com.chinamobile.cmccwifi") || isPackageRunning(this.mContext, "com.chinamobile.cmccwifi.WelcomeActivity") || isPackageRunning(this.mContext, "com.chinamobile.cmccwifi.MainActivity") || isPackageRunning(this.mContext, "com.android.settings.wifi.CMCCChargeWarningDialog"))) { reason = 6; } else if (("\"au_Wi-Fi\"".equals(ssid) || "\"Wi2\"".equals(ssid) || "\"Wi2premium\"".equals(ssid) || "\"Wi2premium_club\"".equals(ssid) || "\"UQ_Wi-Fi\"".equals(ssid) || "\"wifi_square\"".equals(ssid)) && (isPackageExists("com.kddi.android.au_wifi_connect") || isPackageExists("com.kddi.android.au_wifi_connect2"))) { reason = 7; } else if (FactoryTest.isFactoryBinary()) { reason = 8; } else if ("\"mailsky\"".equals(ssid) && this.mIsUsingProxy) { reason = 9; } else if ("\"COPconnect\"".equals(ssid) && wifiConfiguration.allowedKeyManagement.get(2)) { reason = 10; } else if ("\"SpirentATTEVSAP\"".equals(ssid)) { reason = 11; } if (reason == -1) { return false; } Log.d(TAG, "isIgnorableNetwork - No need to check connectivity: " + ssid + ", reason: " + reason); return true; }

Presumably, these are hardcoded rules looking for signs that the network is not connected to the Internet (e.g. captive portal), and thus there is no need to check for connectivity.  Among the criteria above, the most practical method to cause a WiFi network to be excluded from DNS checking (i.e. making the method to return true) is to set the SSID to contain both strings DIRECT- and :NEX-, e.g. DIRECT-:NEX-Home [3].

(Update on 17/10/2020: It was found that, for Samsung phones sold in Hong Kong or phones connected to Hong Kong network (as well as Macau / Mainland network), the Samsung firmware may initiate DNS queries for, as well as connections with, the Mainland websites http://www.qq.com, http://www.baidu.com, http://m.taobao.com and http://m.hao123.com, and the regular connectivity check changed to http://connectivity.samsung.com.cn/generate_204 instead of the default google site, as part of captive portal detection separately. The DNS loop above may trigger such detection.  Details will be discussed in Part 3.)

[1] The state machine for WifiConnectivityMonitor is hierarchial. Evaluated state covers invalid and valid connected state, and Valid state covers several states including Level1 and Level2. It appears that Level2 state corresponds to some state where the WiFi connection is with some devices (e.g. pedometer) and thus network connectivity checking is not needed.

[2] This is determined by isDnsResponsePrivateAddress() of DnsPinger.

[3] The method isPackageRunning() tests for the Apps running as the foreground activity, not just for the existence of the package. So making a package named com.belgacom.fon and installing it on the device has no effect.