背景
- 有些域名可能会被劫持,或者被封,导致线上用户无法使用某些功能
- iOS系统没有提供本地dns解析的方案,难以像安卓的okhttp那样,直接提供一个dns解析规则给sdk,则接口就就使用该条解析规则进行网络请求
本文针对我前一段时间进行iOS相关的ip直连资料查找和研究做个记录,如写得不对请帮我指出,感激。
方案
1. 替换host信任ip
这个方案就是把host用ip替换掉,然后进行网络请求, 比如请求:www.baidu.com 替换掉host,url为:https://157.148.69.74, 这时候会触发证书信任错误,这时候不管三七二十一就直接信任返回的证书即可,证书信任的代码如下:
swift
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
DispatchQueue.global().async {
// 收到证书挑战,先判断需要认证的方法是不是需要认证服务器证书
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
// 从传上来的挑战对象中取出serverTrust对象
let serverTrust = challenge.protectionSpace.serverTrust {
// 从serverTrust对象中取出证书
let credential = URLCredential(trust: serverTrust)
// 返回该证书,即表示信任这个证书
completionHandler(.useCredential, credential)
} else {
completionHandler(.performDefaultHandling, nil)
}
}
}
这是最干脆的无脑信任证书,有些文章还会在信任前加一些判断,判断challenge.protectionSpace.host是否和header里的host是否一致,其实对这里信任没啥帮助,只是增加一些安全,比如,用百度的ip去请求,服务器返回别的证书,比如map.baidu.com,则这里无法匹配,这个情况在云服务器很常见,就是sni的情况,云服务器就是多对多的情况,ip是多个,证书也是多个。握手的时候必须要支持sni协议,在header中添加host,而iOS的URLSession就完全不管header中的字段,只拿url的host进行握手,就很气人。
说个题外话,这里最好用线程返回,用主线程会报线程警告。
这个方案在iOS17以下使用都没有问题,但是iOS17又加强安全了,使用上面的信任之后,依然报ssl错误,错误码为-1200,找了一下ios的更新日志,发现是ios17提高ats的安全级别了, iOS17更新日志 nsallowslocalnetworking
解决方案就是在info.plist中添加Allow Arbitrary Loads并置为YES,表示支持任意请求,如果不能开这个开关,而服务器又是SNI的,则目前这个方案还没有什么好的办法。
2. 支持sni的网络库-CFNetwork
CFNetwork是一个iOS的底层库,这里有一个kCFStreamPropertySSLSettings进行设置握手的host,这样在握手的时候就有host了
c
// 创建CFHTTPMessage对象的输入流
CFReadStreamRef readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, cfrequest);
inputStream = (__bridge_transfer NSInputStream *) readStream;
// 设置SNI host信息,关键步骤
NSString *host = [curRequest.allHTTPHeaderFields objectForKey:@"host"];
if (!host) {
host = curRequest.URL.host;
}
[inputStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey];
NSDictionary *sslProperties = [[NSDictionary alloc] initWithObjectsAndKeys:
host, (__bridge id) kCFStreamSSLPeerName,
nil];
[inputStream setProperty:sslProperties forKey:(__bridge_transfer NSString *) kCFStreamPropertySSLSettings];
虽然苹果把CFNetwok的api都标记为废弃,但其实URLSession和Network都是对接CFNetwork库,只是api比较底层,官方不建议使用
另一个问题就是CFNetwork是很底层的库,需要我们自己封装处理并发代理证书等诸多问题
不过腾讯的httpdns库里包含了一个CFNetwork的实现,里面的代码看不到,不知道实现得怎么样,我也没试过,阿http dns里就差劲了,就提供了一两个文章,把方案描述一下,让开发者自己抉择。
3. 支持sni的网络库-curl
curl是一个c级别的网络底层库,mac和linux系统都自带这个库,但iOS没有,使用这个库需要先去打包成iOS的网络库,然后集成到项目中,api也是c语言的,跟CFNetwork一样,也需要自己处理诸多问题,我觉得这个方案也不好,不多展开讨论
4. 支持sni的网络库-封装的curl库
之前搜索资料的时候找到一个oc库,是参考swift foundation里编写的一个网络库,名字不太记得了,当时用了一下,有些情况会卡死,于是用了一下就没使用了,后面自己把swift foundation中的网络库抽出来了,就没去理会这个库了,官方的应该更有保障吧
官方swift foundation库地址是swift-corelibs-foundation,这个是苹果为swift跨平台而抽离出来一个基础库,20年还有一些进度,后面就没进度了,还有大半api未完成,URLSession也有部分功能未完成,比如URLAuthenticationChallenge就没完成,吐槽中,具体完成度[# Implementation Status](swift-corelibs-foundation/Docs/Status.md at main · apple/swift-corelibs-foundation (github.com))
我从swif foundation中抽离出来网络库名为SSURLSession,zhtut/SSURLSession,把URLSession相关的代码都抽出来了,一开始手动抽,苹果有更新的时候又得维护,后面搞了一个python脚本更新,方便多了。包括编译curl,也是一起做了。
目前只支持swift,使用方法就是多调用一个SSURLSession,比如SSURLSession.URLSession.shared,其他都一样。
ip直连的用法,有两个key可以使用,效果应该是一样的,都是在URLRequest的header中添加key,第一个是resolve
swift
let resolve = "\(host):443:\(ip)"
request.setValue(resolve, forHTTPHeaderField: "resolve")
第二个是connectTo
swift
let connectTo = "\(host):443:\(ip):443"
request.setValue(connectTo, forHTTPHeaderField: "connectTo")
设置了之后就可以用SSURLSession.URLSession去发送请求了
不用替换url的host
这个方案的缺点也很明显,官方有些功能未完成,导致有些功能无法使用,比如URLAuthenticationChallenge未完成,抓包的时候,也收不到证书挑战的通知,导致ssl pining无法使用,可能存在一些其他问题,并且要集成一个额外的网络库,增大了包体积。
而且而且商用不够,希望大厂能派出大佬或团队,来编写一个iOS的支持直连的强大网络库,打压一下苹果独断专行的气焰。
结论
目前还是无法很好的解决ip直连的问题
第一次写技术文章,很多语法不熟悉,排版不好见谅。