本文檔介紹如何在iOS用戶端中使用DoH
背景
目前在iOS上接入使用HTTPDNS,普遍做法是,通過引入HTTPDNS SDK,再針對HTTPS認證校正、SNI擴充等問題做對應處理,從而可以在App內按需使用HTTPDNS的解析能力。參考文檔:iOS端Native情境使用HTTPDNS
Apple在iOS 14+引入了安全DNS,新增了一種可行方案,可以通過DoH的方式,接入使用HTTPDNS,下面兩個WWDC視頻可供參考。
Improve DNS security for apps and servers:介紹了安全DNS相關的背景知識。
Enable encrypted DNS:介紹如何使用安全DNS,包括系統級和App級的兩種使用方式。
使用安全DNS的方式,是iOS原生支援,直接在App或者裝置全域網路處理上生效,無需在代碼層面修改網路請求細節,方案整體更優雅。但與此同時,它也有很大的局限性,假設通過安全DNS的方式接入HTTPDNS,則:
只能在App維度或者裝置維度生效,對應範圍內的網域名稱解析全部會走HTTPDNS,包括App中三方SDK中的網路請求等,無法做細粒度控制。
如果選擇在裝置維度內生效,需要終端使用者授予特殊許可權。一般來說只有網路工具類應用才能申請此許可權。
前提條件
開啟DoH並擷取DoH接入地址,請參考配置DoH服務。
如果 DoH 沒有處於開啟狀態,解析請求會失敗,HTTPDNS 服務端會返回 400 錯誤碼。
如果「網域名稱解析範圍」設定為「網域名稱列表中的網域名稱」時,對於沒有在接入網域名稱中添加的網域名稱,HTTPDNS 服務端會返回 200 的狀態代碼但沒有解析結果。
如果「網域名稱解析範圍」設定為「所有網域名稱」時,對於在黑名單的網域名稱,HTTPDNS 服務端會返回 200 的狀態代碼但沒有解析結果。
使用應用級的DoH配置
iOS 14+ network.framework提供了privacyContext為應用獨立配置DoH,可以控制應用在生命週期內的DNS解析過程。
範例程式碼
建立用於管理NSURLSession的DataTaskManager
@interface DataTaskManager : NSObject <NSURLSessionTaskDelegate>#import "DataTaskManager.h"
@import Network;
@import Foundation;
- (instancetype)init {
self = [super init];
if (self) {
_networkQueue = dispatch_queue_create("com.taskmanager.queue", DISPATCH_QUEUE_SERIAL);
[self setupDoHConfiguration];
}
return self;
}
- (void)setupDoHConfiguration {
dispatch_async(self.networkQueue, ^{
NSLog(@"Setting up DoH configuration...");
// Create URL endpoint for HTTPDNS DoH
const char *dohServerURL = "https://1xxxx3.aliyunhttpdns.com/dns-query";
nw_endpoint_t urlEndpoint = nw_endpoint_create_url(dohServerURL);
NSLog(@"Using DoH server: %s", dohServerURL);
nw_resolver_config_t resolverConfig = nw_resolver_config_create_https(urlEndpoint);
nw_privacy_context_require_encrypted_name_resolution(NW_DEFAULT_PRIVACY_CONTEXT, true, resolverConfig);
NSLog(@"DoH configuration applied to privacy context");
});
}推薦在ViewController.viewDidLoad完成初始化
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
......
self.dataTaskManager = [[DataTaskManager alloc] init];
......
}URLSession和基於URLSession的三方網路程式庫在進行網路請求時,會使用預設的PrivacyContext執行個體,當完成DoH配置後,當前App所有的URLSession請求的DNS解析都會使用DoH。
解析效能資料埋點
可以使用NSURLSessionTaskMetrics對DNS過程進行埋點,查看DoH解析是否生效以及DNS解析耗時。
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics {
NSLog(@"\n=== Collecting metrics for request to: %@ ===\n", task.originalRequest.URL);
task.taskDescription = [NSString stringWithFormat:@"%.2f,%@",
metrics.taskInterval.duration,
task.originalRequest.URL.absoluteString];
if ([metrics.transactionMetrics count] > 0) {
[metrics.transactionMetrics enumerateObjectsUsingBlock:^(NSURLSessionTaskTransactionMetrics *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
NSString *fetchTypeStr = @"Unknown";
switch (obj.resourceFetchType) {
case NSURLSessionTaskMetricsResourceFetchTypeNetworkLoad:
fetchTypeStr = @"Network Load";
break;
case NSURLSessionTaskMetricsResourceFetchTypeServerPush:
fetchTypeStr = @"Server Push";
break;
case NSURLSessionTaskMetricsResourceFetchTypeLocalCache:
fetchTypeStr = @"Local Cache";
break;
}
NSLog(@"Fetch Type: %@", fetchTypeStr);
if (obj.resourceFetchType == NSURLSessionTaskMetricsResourceFetchTypeNetworkLoad) {
NSURLSessionTaskMetricsDomainResolutionProtocol dnsProtocol = obj.domainResolutionProtocol;
NSString *dnsProtocolStr = @"Unknown (0)";
BOOL isDoH = NO;
switch (dnsProtocol) {
case NSURLSessionTaskMetricsDomainResolutionProtocolUDP:
dnsProtocolStr = @"UDP (1)";
break;
case NSURLSessionTaskMetricsDomainResolutionProtocolTCP:
dnsProtocolStr = @"TCP (2)";
break;
case NSURLSessionTaskMetricsDomainResolutionProtocolTLS:
dnsProtocolStr = @"TLS (3)";
break;
case NSURLSessionTaskMetricsDomainResolutionProtocolHTTPS:
dnsProtocolStr = @"HTTPS/DoH (4)";
isDoH = YES;
break;
}
NSLog(@"DNS Protocol: %@", dnsProtocolStr);
#if TARGET_OS_SIMULATOR
NSLog(@"Running in simulator - DNS protocol detection not supported");
#else
if (!isDoH) {
NSLog(@"DoH not detected");
}
#endif
// 擷取DNS解析效能資料
if (obj.domainLookupStartDate && obj.domainLookupEndDate) {
int dnsLookupTime = ceil([obj.domainLookupEndDate timeIntervalSinceDate:obj.domainLookupStartDate] * 1000);
NSLog(@"DNS Lookup Details:");
NSLog(@" Start: %@", obj.domainLookupStartDate);
NSLog(@" End: %@", obj.domainLookupEndDate);
NSLog(@" Duration: %d ms", dnsLookupTime);
} else {
NSLog(@"No DNS lookup performed (might be cached)");
}
// 擷取網路請求效能資料
if (obj.connectStartDate && obj.connectEndDate) {
NSTimeInterval connectionTime = [obj.connectEndDate timeIntervalSinceDate:obj.connectStartDate];
NSLog(@"Connection Time: %.3f seconds", connectionTime);
}
}
}];
} else {
NSLog(@"No transaction metrics available");
}
}降級機制
在完成降級後,會使應用中後續所有基於URLSession的請求生效。
DoH配置可以在iOS 14+真機和模擬器生效,但是在模擬器運行時,通過
NSURLSessionTaskMetrics讀取的NSURLSessionTaskMetricsDomainResolutionProtocol固定為 0 (Unknown),在模擬器測試相關功能時需要關注這個差異。
配置逾時設定,在DNS解析失敗時可以及時拿到異常資訊:
// 設定連線逾時
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.waitsForConnectivity = NO;
config.timeoutIntervalForRequest = 5;
config.timeoutIntervalForResource = 10;在上文中的埋點鏈路中,可以制定合適的降級策略,以下是一個識別到DNS異常時,全域降級到LocalDNS的例子:
......
if (obj.resourceFetchType == NSURLSessionTaskMetricsResourceFetchTypeNetworkLoad) {
NSURLSessionTaskMetricsDomainResolutionProtocol dnsProtocol = obj.domainResolutionProtocol;
NSString *dnsProtocolStr = @"Unknown (0)";
BOOL isDoH = NO;
switch (dnsProtocol) {
case NSURLSessionTaskMetricsDomainResolutionProtocolUDP:
dnsProtocolStr = @"UDP (1)";
break;
case NSURLSessionTaskMetricsDomainResolutionProtocolTCP:
dnsProtocolStr = @"TCP (2)";
break;
case NSURLSessionTaskMetricsDomainResolutionProtocolTLS:
dnsProtocolStr = @"TLS (3)";
break;
case NSURLSessionTaskMetricsDomainResolutionProtocolHTTPS:
dnsProtocolStr = @"HTTPS/DoH (4)";
isDoH = YES;
break;
}
NSLog(@"DNS Protocol: %@", dnsProtocolStr);
#if TARGET_OS_SIMULATOR
NSLog(@"Running in simulator - DNS protocol detection not supported");
#else
if (!isDoH) {
NSLog(@"DoH not detected, falling back to local DNS");
dispatch_async(dispatch_get_main_queue(), ^{
// 關閉 DoH 並使用 LocalDNS
nw_privacy_context_require_encrypted_name_resolution(NW_DEFAULT_PRIVACY_CONTEXT, false, nil);
});
}
#endif
}
......如果「網域名稱解析範圍」設定為「網域名稱列表中的網域名稱」。
對於沒有在接入網域名稱中添加的網域名稱,樣本中的 dnsProtocolStr 會列印出 "Unknown (0)"。
對於在接入網域名稱中添加的網域名稱,樣本中的 dnsProtocolStr 會列印出 "HTTPS/DoH (4)"。
如果「網域名稱解析範圍」設定為「所有網域名稱」。
對於在黑名單的網域名稱,樣本中的 dnsProtocolStr 會列印出 "Unknown (0)"。
對於不在黑名單中的網域名稱,樣本中的 dnsProtocolStr 會列印出 "HTTPS/DoH (4)"。
使用系統級的DoH配置
可以通過配置系統的描述檔案為iOS裝置配置DoH,注意系統級DoH會影響裝置上的所有應用。
參考以下步驟配置DoH地址:
將樣本的內容,替換 DoH 地址後,儲存為
.mobileconfig檔案,例如,my_company_doh.mobileconfig
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array>
<dict>
<key>DNSSettings</key>
<dict>
<key>DNSProtocol</key>
<string>HTTPS</string>
<key>ServerURL</key>
<!---- 把此處的地址替換為 DoH 接入地址 --->
<string>https://1xxxx3.aliyunhttpdns.com/dns-query</string>
</dict>
<key>PayloadDescription</key>
<string>Configures iOS to use EMAS HTTPDNS DoH</string>
<key>PayloadDisplayName</key>
<string>EMAS HTTPDNS DoH</string>
<key>PayloadIdentifier</key>
<string>com.apple.dnsSettings.managed.9B498EC0C-EF6C-44F0-BFB7-0000658B99AC</string>
<key>PayloadType</key>
<string>com.apple.dnsSettings.managed</string>
<key>PayloadUUID</key>
<string>465AB183-5E34-4794-9BEB-B5327CF61F27</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>ProhibitDisablement</key>
<false/>
</dict>
</array>
<key>PayloadDescription</key>
<string>Adds EMAS HTTPDNS DoH configuration to iOS</string>
<key>PayloadDisplayName</key>
<string>EMAS HTTPDNS DoH Configuration</string>
<key>PayloadIdentifier</key>
<string>com.emas.apple-dns</string>
<key>PayloadRemovalDisallowed</key>
<false/>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>130E6D6F-69A2-4515-9D77-99342CB9AE76</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>
將
my_company_doh.mobileconfig發布到檔案儲存體伺服器或者通過郵件發送到iOS裝置。在iOS裝置上通過瀏覽器或者郵箱用戶端下載
my_company_doh.mobileconfig。在 設定 > 通用 > VPN與裝置管理 中安裝此描述檔案。