2 * Copyright (c) 2020 Apple Inc. All rights reserved.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 #import "DNSHeuristicsInternal.h"
18 #import "DNSHeuristics.h"
19 #import "mdns_symptoms.h"
20 #import "mdns_helpers.h"
23 #if (TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST)
25 MDNS_LOG_CATEGORY_DEFINE(heuristics, "heuristics");
29 #define DNS_MIN(A, B) ((A) < (B) ? (A) : (B))
32 * Persisted heuristic data model:
35 * "LastFailureTimestamp": <>,
39 * "FilteredNetwork": <>
41 const NSString *DNSFailureStateKey = @"DNSFailures";
42 const NSString *DNSHeuristicsLastFailureTimestamp = @"LastFailureTimestamp";
43 const NSString *DNSHeuristicsLongCounterKey = @"LongCount";
44 const NSString *DNSHeuristicsBurstCounterKey = @"BurstCount";
45 const NSString *DNSHeuristicsFilterFlagKey = @"FilteredNetwork";
48 * DNS resolution failures are tracked using two counters:
50 * 1. A "long" counter, tracking the total number of failures. If the total number of failures exceeds DNSHeuristicDefaultLongCounterThreshold
51 * the network is marked as an active filterer. After a cooldown period of DNSHeuristicDefaultLongCounterTimeWindow seconds
52 * since the last failure the network stops being marked as such and the counter is reset to zero.
53 * 2. A "burst" counter, implemented as a token bucket. The token bucket is replenished every two minutes. Each failure
54 * removes a token from the bucket. If there are ever no tokens available, the network is marked as an active filterer.
56 NSUInteger DNSHeuristicDefaultLongCounterThreshold = 10; // long counter
57 NSUInteger DNSHeuristicDefaultLongCounterTimeWindow = 24*60*60*1000; // one day
58 NSUInteger DNSHeuristicsDefaultBurstTokenBucketCapacity = 10; // token bucket
59 NSUInteger DNSHeuristicsDefaultBurstTokenBucketRefillTime = 2*60*1000; // refill every two minutes
60 NSUInteger DNSHeuristicsDefaultBurstTokenBucketRefillCount = 1; // put one token back in every epoch
61 NSUInteger DNSHeuristicsDefaultMultipleTimeoutWindow = 30*1000; // only penalize for one timeout every thirty seconds
63 #if (TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST)
64 #import <Apple80211/Apple80211API.h>
65 #import <Kernel/IOKit/apple80211/apple80211_var.h>
66 #import <Kernel/IOKit/apple80211/apple80211_ioctl.h>
67 #import <MobileWiFi/WiFiTypes.h>
68 #import <MobileWiFi/WiFiManagerClient.h>
69 #import <MobileWiFi/WiFiDeviceClient.h>
70 #import <MobileWiFi/WiFiKeys.h>
71 #import <MobileWiFi/MobileWiFi.h>
73 static WiFiManagerClientRef
74 getNetworkManager(void)
76 static WiFiManagerClientRef manager = NULL;
77 if (manager == NULL) {
78 manager = WiFiManagerClientCreate(kCFAllocatorDefault, kWiFiClientTypeNormal);
84 copyCurrentWiFiNetwork(WiFiManagerClientRef manager)
86 if (manager == NULL) {
90 NSArray *interfaces = (__bridge_transfer NSArray *)WiFiManagerClientCopyInterfaces(manager);
92 for (id interface in interfaces) {
93 if (WiFiDeviceClientGetInterfaceRoleIndex((__bridge WiFiDeviceClientRef)interface) == WIFI_MANAGER_MAIN_INTERFACE_ROLE) {
94 WiFiDeviceClientRef device = (__bridge WiFiDeviceClientRef)interface;
95 WiFiNetworkRef network = WiFiDeviceClientCopyCurrentNetwork(device);
96 if (network != NULL) {
102 return WiFiManagerClientCopyCurrentSessionBasedNetwork(manager);
105 #endif /* TARGET_OS_IPHONE */
107 static dispatch_queue_t
108 copyHeuristicsQueue(void)
110 static dispatch_queue_t queue = nil;
111 static dispatch_once_t onceToken;
112 dispatch_once(&onceToken, ^{
113 queue = dispatch_queue_create("DNSHeuristicsQueue", NULL);
118 @implementation DNSHeuristics
120 #if (TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST)
122 + (NSDictionary *)copyNetworkSettings:(WiFiNetworkRef)network NS_RETURNS_RETAINED
124 if (network == NULL) {
128 NSDictionary *networkFailures = (__bridge NSDictionary *)WiFiNetworkGetProperty(network, (__bridge CFStringRef)DNSFailureStateKey);
129 return [networkFailures copy];
132 + (BOOL)setNetworkSettings:(WiFiManagerClientRef)manager
133 network:(WiFiNetworkRef)network
134 value:(NSDictionary *)value
137 if (manager == NULL || network == NULL) {
141 return (BOOL)WiFiManagerClientSetNetworkProperty(manager, network, (__bridge CFStringRef)DNSFailureStateKey, (__bridge CFDictionaryRef)value);
144 + (BOOL)getNetworkFilteredFlag:(WiFiNetworkRef)network
146 if (network == NULL) {
149 CFBooleanRef value = WiFiNetworkGetProperty(network, (__bridge CFStringRef)DNSHeuristicsFilterFlagKey);
150 return value == kCFBooleanTrue ? YES : NO;
153 + (BOOL)setNetworkAsFiltered:(WiFiManagerClientRef)manager
154 network:(WiFiNetworkRef)network
156 if (manager == NULL || network == NULL) {
159 return (BOOL)WiFiManagerClientSetNetworkProperty(manager, network, (__bridge CFStringRef)DNSHeuristicsFilterFlagKey, kCFBooleanTrue);
162 + (BOOL)clearNetworkAsFiltered:(WiFiManagerClientRef)manager
163 network:(WiFiNetworkRef)network
165 if (manager == NULL || network == NULL) {
168 return (BOOL)WiFiManagerClientSetNetworkProperty(manager, network, (__bridge CFStringRef)DNSHeuristicsFilterFlagKey, kCFBooleanFalse);
171 + (BOOL)setNetworkAsFiltered:(WiFiManagerClientRef)manager
172 network:(WiFiNetworkRef)network
173 filtered:(BOOL)filtered
176 return [DNSHeuristics setNetworkAsFiltered:manager network:network];
178 return [DNSHeuristics clearNetworkAsFiltered:manager network:network];
182 #endif // #if TARGET_OS_IPHONE
184 + (BOOL)countersExceedThreshold:(NSUInteger)dailyCounter
185 burstCounter:(NSUInteger)burstCounter
187 return (dailyCounter > DNSHeuristicDefaultLongCounterThreshold || burstCounter == 0);
190 + (NSUInteger)currentTimeMs
192 return (NSUInteger)([[NSDate date] timeIntervalSince1970] * 1000);
195 + (NSDictionary *)copyEmptyHeuristicState NS_RETURNS_RETAINED
198 DNSHeuristicsLastFailureTimestamp: [NSNumber numberWithUnsignedInteger:0],
199 DNSHeuristicsLongCounterKey: [NSNumber numberWithUnsignedInteger:0],
200 DNSHeuristicsBurstCounterKey: [NSNumber numberWithUnsignedInteger:DNSHeuristicsDefaultBurstTokenBucketCapacity],
204 + (BOOL)updateHeuristicState:(BOOL)resolutionSuccess
205 isTimeout:(BOOL)isTimeout
209 #if (TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST)
211 WiFiManagerClientRef manager = getNetworkManager();
212 WiFiNetworkRef network = copyCurrentWiFiNetwork(manager);
213 NSDictionary *heuristicState = [DNSHeuristics copyNetworkSettings:network];
214 if (!heuristicState) {
215 heuristicState = @{}; // Empty dictionary to start
217 if (![heuristicState objectForKey:DNSHeuristicsLastFailureTimestamp]) {
218 heuristicState = [DNSHeuristics copyEmptyHeuristicState];
221 NSUInteger now = [DNSHeuristics currentTimeMs];
222 NSUInteger lastFailureTimestamp = [(NSNumber *)heuristicState[DNSHeuristicsLastFailureTimestamp] unsignedIntegerValue];
223 NSUInteger longCounter = [(NSNumber *)heuristicState[DNSHeuristicsLongCounterKey] unsignedIntegerValue];
224 NSUInteger burstCounter = [(NSNumber *)heuristicState[DNSHeuristicsBurstCounterKey] unsignedIntegerValue];
225 BOOL filteredFlag = [DNSHeuristics getNetworkFilteredFlag:network];
227 if (resolutionSuccess) {
228 // Check to see if the network can be forgiven, i.e., if we've gone over a day since the last failure.
230 if (lastFailureTimestamp + DNSHeuristicDefaultLongCounterTimeWindow < now) {
231 const uint64_t delta = (now - lastFailureTimestamp);
232 os_log(_mdns_heuristics_log(), "Logging DoH success after %llums, clearing filtered state", delta);
233 result &= [DNSHeuristics setNetworkSettings:manager network:network value:[DNSHeuristics copyEmptyHeuristicState]];
234 result &= [DNSHeuristics setNetworkAsFiltered:manager network:network filtered:NO];
235 } else if (lastFailureTimestamp < now) {
236 const uint64_t delta = (now - lastFailureTimestamp);
237 os_log_info(_mdns_heuristics_log(), "Logging DoH success after %llums, keeping filtered state", delta);
239 os_log(_mdns_heuristics_log(), "Logging DoH success, invalid last failure, clearing filtered state");
240 result &= [DNSHeuristics setNetworkSettings:manager network:network value:[DNSHeuristics copyEmptyHeuristicState]];
241 result &= [DNSHeuristics setNetworkAsFiltered:manager network:network filtered:NO];
244 } else if (isTimeout && lastFailureTimestamp < now &&
245 lastFailureTimestamp + DNSHeuristicsDefaultMultipleTimeoutWindow > now) {
246 const uint64_t delta = (now - lastFailureTimestamp);
247 os_log_info(_mdns_heuristics_log(), "Logging DoH timeout failure after only %llums, not incrementing failure counter", delta);
249 // The long counter always increases upon each failure.
250 NSUInteger newLongCounter = (longCounter + 1);
252 // Replenish the burst token bucket, and then compute the new bucket value.
253 NSUInteger refillCount = (now - lastFailureTimestamp) / DNSHeuristicsDefaultBurstTokenBucketRefillTime;
254 NSUInteger refillAmount = refillCount * DNSHeuristicsDefaultBurstTokenBucketRefillCount;
255 NSUInteger refilledBucketValue = DNS_MIN(DNSHeuristicsDefaultBurstTokenBucketCapacity, burstCounter + refillAmount);
256 NSUInteger newBucketValue = (refilledBucketValue > 0) ? (refilledBucketValue - 1) : 0;
258 BOOL newFilteredFlag = filteredFlag || [DNSHeuristics countersExceedThreshold:newLongCounter burstCounter:newBucketValue];
260 if (!filteredFlag && newFilteredFlag) {
261 os_log(_mdns_heuristics_log(), "Logging DoH %sfailure %llu (bucket %llu), moving into filtered state",
262 isTimeout ? "timeout " : "", (uint64_t)newLongCounter, (uint64_t)newBucketValue);
263 } else if (filteredFlag) {
264 os_log_info(_mdns_heuristics_log(), "Logging DoH %sfailure %llu (bucket %llu), keeping filtered state",
265 isTimeout ? "timeout " : "", (uint64_t)newLongCounter, (uint64_t)newBucketValue);
267 os_log_info(_mdns_heuristics_log(), "Logging DoH %sfailure %llu (bucket %llu), keeping unfiltered state",
268 isTimeout ? "timeout " : "", (uint64_t)newLongCounter, (uint64_t)newBucketValue);
271 NSDictionary *newState = @{
272 DNSHeuristicsLastFailureTimestamp: [NSNumber numberWithUnsignedInteger:now],
273 DNSHeuristicsLongCounterKey: [NSNumber numberWithUnsignedInteger:newLongCounter],
274 DNSHeuristicsBurstCounterKey: [NSNumber numberWithUnsignedInteger:newBucketValue],
277 result &= [DNSHeuristics setNetworkSettings:manager network:network value:newState];
278 result &= [DNSHeuristics setNetworkAsFiltered:manager network:network filtered:newFilteredFlag];
290 + (BOOL)reportResolutionFailure:(NSURL *)url
291 isTimeout:(BOOL)isTimeout
293 #ifndef DNS_XCTEST // Skip this symptoms report in the XCTests
294 NSString *urlHostname = [url host];
295 const char *hostname = urlHostname ? [urlHostname UTF8String] : "";
296 mdns_symptoms_report_encrypted_dns_connection_failure(hostname);
299 return [DNSHeuristics updateHeuristicState:NO isTimeout:isTimeout];
305 dns_heuristics_report_resolution_failure(NSURL *url, bool is_timeout)
307 dispatch_async(copyHeuristicsQueue(), ^{
308 [DNSHeuristics reportResolutionFailure:url isTimeout:!!is_timeout];
313 dns_heuristics_report_resolution_success(void)
315 dispatch_async(copyHeuristicsQueue(), ^{
316 [DNSHeuristics updateHeuristicState:YES isTimeout:NO];