]> git.saurik.com Git - apple/mdnsresponder.git/blob - mDNSMacOSX/DNSHeuristics.m
mDNSResponder-1310.40.42.tar.gz
[apple/mdnsresponder.git] / mDNSMacOSX / DNSHeuristics.m
1 /*
2 * Copyright (c) 2020 Apple Inc. All rights reserved.
3 *
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
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
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.
15 */
16
17 #import "DNSHeuristicsInternal.h"
18 #import "DNSHeuristics.h"
19 #import "mdns_symptoms.h"
20 #import "mdns_helpers.h"
21 #import <os/log.h>
22
23 #if (TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST)
24
25 MDNS_LOG_CATEGORY_DEFINE(heuristics, "heuristics");
26
27 #endif
28
29 #define DNS_MIN(A, B) ((A) < (B) ? (A) : (B))
30
31 /*
32 * Persisted heuristic data model:
33 *
34 * "DNSFailures": {
35 * "LastFailureTimestamp": <>,
36 * "LongCount": <>,
37 * "BurstCount": <>,
38 * },
39 * "FilteredNetwork": <>
40 */
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";
46
47 /*
48 * DNS resolution failures are tracked using two counters:
49 *
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.
55 */
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
62
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>
72
73 static WiFiManagerClientRef
74 getNetworkManager(void)
75 {
76 static WiFiManagerClientRef manager = NULL;
77 if (manager == NULL) {
78 manager = WiFiManagerClientCreate(kCFAllocatorDefault, kWiFiClientTypeNormal);
79 }
80 return manager;
81 }
82
83 static WiFiNetworkRef
84 copyCurrentWiFiNetwork(WiFiManagerClientRef manager)
85 {
86 if (manager == NULL) {
87 return NULL;
88 }
89
90 NSArray *interfaces = (__bridge_transfer NSArray *)WiFiManagerClientCopyInterfaces(manager);
91
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) {
97 return network;
98 }
99 }
100 }
101
102 return WiFiManagerClientCopyCurrentSessionBasedNetwork(manager);
103 }
104
105 #endif /* TARGET_OS_IPHONE */
106
107 static dispatch_queue_t
108 copyHeuristicsQueue(void)
109 {
110 static dispatch_queue_t queue = nil;
111 static dispatch_once_t onceToken;
112 dispatch_once(&onceToken, ^{
113 queue = dispatch_queue_create("DNSHeuristicsQueue", NULL);
114 });
115 return queue;
116 }
117
118 @implementation DNSHeuristics
119
120 #if (TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST)
121
122 + (NSDictionary *)copyNetworkSettings:(WiFiNetworkRef)network NS_RETURNS_RETAINED
123 {
124 if (network == NULL) {
125 return nil;
126 }
127
128 NSDictionary *networkFailures = (__bridge NSDictionary *)WiFiNetworkGetProperty(network, (__bridge CFStringRef)DNSFailureStateKey);
129 return [networkFailures copy];
130 }
131
132 + (BOOL)setNetworkSettings:(WiFiManagerClientRef)manager
133 network:(WiFiNetworkRef)network
134 value:(NSDictionary *)value
135 {
136
137 if (manager == NULL || network == NULL) {
138 return NO;
139 }
140
141 return (BOOL)WiFiManagerClientSetNetworkProperty(manager, network, (__bridge CFStringRef)DNSFailureStateKey, (__bridge CFDictionaryRef)value);
142 }
143
144 + (BOOL)getNetworkFilteredFlag:(WiFiNetworkRef)network
145 {
146 if (network == NULL) {
147 return NO;
148 }
149 CFBooleanRef value = WiFiNetworkGetProperty(network, (__bridge CFStringRef)DNSHeuristicsFilterFlagKey);
150 return value == kCFBooleanTrue ? YES : NO;
151 }
152
153 + (BOOL)setNetworkAsFiltered:(WiFiManagerClientRef)manager
154 network:(WiFiNetworkRef)network
155 {
156 if (manager == NULL || network == NULL) {
157 return NO;
158 }
159 return (BOOL)WiFiManagerClientSetNetworkProperty(manager, network, (__bridge CFStringRef)DNSHeuristicsFilterFlagKey, kCFBooleanTrue);
160 }
161
162 + (BOOL)clearNetworkAsFiltered:(WiFiManagerClientRef)manager
163 network:(WiFiNetworkRef)network
164 {
165 if (manager == NULL || network == NULL) {
166 return NO;
167 }
168 return (BOOL)WiFiManagerClientSetNetworkProperty(manager, network, (__bridge CFStringRef)DNSHeuristicsFilterFlagKey, kCFBooleanFalse);
169 }
170
171 + (BOOL)setNetworkAsFiltered:(WiFiManagerClientRef)manager
172 network:(WiFiNetworkRef)network
173 filtered:(BOOL)filtered
174 {
175 if (filtered) {
176 return [DNSHeuristics setNetworkAsFiltered:manager network:network];
177 } else {
178 return [DNSHeuristics clearNetworkAsFiltered:manager network:network];
179 }
180 }
181
182 #endif // #if TARGET_OS_IPHONE
183
184 + (BOOL)countersExceedThreshold:(NSUInteger)dailyCounter
185 burstCounter:(NSUInteger)burstCounter
186 {
187 return (dailyCounter > DNSHeuristicDefaultLongCounterThreshold || burstCounter == 0);
188 }
189
190 + (NSUInteger)currentTimeMs
191 {
192 return (NSUInteger)([[NSDate date] timeIntervalSince1970] * 1000);
193 }
194
195 + (NSDictionary *)copyEmptyHeuristicState NS_RETURNS_RETAINED
196 {
197 return @{
198 DNSHeuristicsLastFailureTimestamp: [NSNumber numberWithUnsignedInteger:0],
199 DNSHeuristicsLongCounterKey: [NSNumber numberWithUnsignedInteger:0],
200 DNSHeuristicsBurstCounterKey: [NSNumber numberWithUnsignedInteger:DNSHeuristicsDefaultBurstTokenBucketCapacity],
201 };
202 }
203
204 + (BOOL)updateHeuristicState:(BOOL)resolutionSuccess
205 isTimeout:(BOOL)isTimeout
206 {
207 BOOL result = YES;
208
209 #if (TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST)
210
211 WiFiManagerClientRef manager = getNetworkManager();
212 WiFiNetworkRef network = copyCurrentWiFiNetwork(manager);
213 NSDictionary *heuristicState = [DNSHeuristics copyNetworkSettings:network];
214 if (!heuristicState) {
215 heuristicState = @{}; // Empty dictionary to start
216 }
217 if (![heuristicState objectForKey:DNSHeuristicsLastFailureTimestamp]) {
218 heuristicState = [DNSHeuristics copyEmptyHeuristicState];
219 }
220
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];
226
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.
229 if (filteredFlag) {
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);
238 } else {
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];
242 }
243 }
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);
248 } else {
249 // The long counter always increases upon each failure.
250 NSUInteger newLongCounter = (longCounter + 1);
251
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;
257
258 BOOL newFilteredFlag = filteredFlag || [DNSHeuristics countersExceedThreshold:newLongCounter burstCounter:newBucketValue];
259
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);
266 } else {
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);
269 }
270
271 NSDictionary *newState = @{
272 DNSHeuristicsLastFailureTimestamp: [NSNumber numberWithUnsignedInteger:now],
273 DNSHeuristicsLongCounterKey: [NSNumber numberWithUnsignedInteger:newLongCounter],
274 DNSHeuristicsBurstCounterKey: [NSNumber numberWithUnsignedInteger:newBucketValue],
275 };
276
277 result &= [DNSHeuristics setNetworkSettings:manager network:network value:newState];
278 result &= [DNSHeuristics setNetworkAsFiltered:manager network:network filtered:newFilteredFlag];
279 }
280
281 if (network) {
282 CFRelease(network);
283 }
284
285 #endif
286
287 return result;
288 }
289
290 + (BOOL)reportResolutionFailure:(NSURL *)url
291 isTimeout:(BOOL)isTimeout
292 {
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);
297 #endif // DNS_XCTEST
298
299 return [DNSHeuristics updateHeuristicState:NO isTimeout:isTimeout];
300 }
301
302 @end
303
304 void
305 dns_heuristics_report_resolution_failure(NSURL *url, bool is_timeout)
306 {
307 dispatch_async(copyHeuristicsQueue(), ^{
308 [DNSHeuristics reportResolutionFailure:url isTimeout:!!is_timeout];
309 });
310 }
311
312 void
313 dns_heuristics_report_resolution_success(void)
314 {
315 dispatch_async(copyHeuristicsQueue(), ^{
316 [DNSHeuristics updateHeuristicState:YES isTimeout:NO];
317 });
318 }