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) withmCurrentMode
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; andmCurrentMode
!= 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.