0x00 背景
公司的硬件产品需要和软件进行 TCP 传输数据, iOS 14 以后出现的本地网络权限需要用户同意, 但是 iOS 14 系统并没有提供像相机定位那样的查询 API, 所以要自己想办法实现
0x01 方案
查询后发现, 可以利用其他的 API 辅助实现检测本地网络权限, 也就是 dnssd 和 Bonjour
0x02 dnnsd
使用 C 函数 DNSServiceBrowse
可以同步回调本地网络是否可用
DNSServiceErrorType DNSServiceBrowse(DNSServiceRef *sdRef, DNSServiceFlags flags, uint32_t interfaceIndex, const char *regtype, const char *domain, DNSServiceBrowseReply callBack, void *context);
具体参数含义可以参考这篇文章IOS14本地网络网络权限检测
需要注意的是:
- 在收到守护程序的响应之前, 该函数调用将阻塞,
DNSServiceProcessResult
就是那个守护进程阻塞的函数, 所以需要在子线程中执行 -
DNSServiceBrowseReply
这个 callback 也是在阻塞中回调回来的, 可以理解为在阻塞结束的时候就能拿到权限结果 - 该方式也是有缺点的, 就是经过多版本校验, 发现 iOS 15.5 的 A14 设备上, 如果有本地网络权限,
DNSServiceProcessResult
则会一直阻塞拿不到结果, 因此放弃了该方式
代码如下:
func requestLocalNetworkAuthorization(completion: AuthResult?) {
authResult = completion
isAuthed = true
guard #available(iOS 14, *) else {
authResult?(isAuthed)
return
}
GCD.globalAsync {
var serviceRef: DNSServiceRef?
DNSServiceBrowse(
&serviceRef,
.zero,
.zero,
"_paperang._tcp",
"local.",
{ _, _, _, error, _, _, _, _ in
GCD.mainAsync {
switch error {
case -65570:
// kDNSServiceErr_PolicyDenied = -65570
// A C function pointer cannot be formed from a closure that captures context
// 必须用类名去掉用不然会报错如上
LocalNetworkManager.isAuthed = false
default:
break
}
}
},
nil
)
guard let serviceRef = serviceRef else {
authResult?(isAuthed)
return
}
DNSServiceProcessResult(serviceRef)
DNSServiceRefDeallocate(serviceRef)
authResult?(isAuthed)
}
}
0x03 Bonjour
对于 Bonjour 了解并不是很多, 主要是多看几篇文章, 后面会列举出来
本来打算利用NetService
和NetServiceBrowser
其中一个实现, 结果发现 API 即将被苹果废弃, 而我的需求也是在 iOS14 以后的, 所以尝试找一下新的 API, 也就是 Network.framework
基于 iOS13.0 的 API
尝试了一下, NWBrowser
客户端比较靠谱, 但是打算用 NWConnection
和 NWListener
代替服务端的 NetService
, 但是结果并不是我想要的, 因为新的 API NWConnection
需要连接上服务才能拿到 ready 结果, NWListener
一直报参数错误, 所以另辟蹊径(继续 Google)
最终按照Stack Overflow
大佬提供的 NetService
和NWBrowser
组合实现, 之所以用组合的方式就是因为 NetService
的 publish 成功只有一次响应, NWBrowser
的错误也只有一次响应, NWBrowser
也可以监听有权限但是无论有没有权限都会有 ready 状态, 导致没办法直接判断是否有权限
最终代码如下:
import Foundation
import Network
import MMPublic
class LocalNetworkManager: NSObject {
typealias AuthResult = (Bool) -> Void
private
var authResult: AuthResult?
private
var netService: NetService?
func requestAuthorization(completion: AuthResult?) {
authResult = completion
guard #available(iOS 14, *) else {
authResult?(true)
return
}
iOS14_requestAuthorization()
}
}
private var gBrowserKey: Void?
private var gNWConnectionKey: Void?
@available(iOS 14.0, *)
private
extension LocalNetworkManager {
private
var browser: NWBrowser? {
get {
objc_getAssociatedObject(self, &gBrowserKey) as? NWBrowser
}
set {
objc_setAssociatedObject(self, &gBrowserKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
func iOS14_requestAuthorization() {
let type = "_paperang._tcp"
// Create parameters, and allow browsing over peer-to-peer link.
let parameters = NWParameters()
parameters.includePeerToPeer = true
// Browse for a custom service type.
let browser = NWBrowser(for: .bonjour(type: type, domain: nil), using: parameters)
self.browser = browser
browser.stateUpdateHandler = { newState in
switch newState {
case .failed(let error):
print(error.localizedDescription)
case .ready, .cancelled:
break
case let .waiting(error):
MMGlobal.errorLog("Local network permission has been denied: \(error)")
self.reset()
self.authResult?(false)
default:
break
}
}
self.netService = NetService(domain: "local.", type: type, name: "LocalNetworkPrivacy", port: 1100)
self.netService?.delegate = self
self.browser?.start(queue: .main)
self.netService?.publish()
}
func reset() {
self.browser?.cancel()
self.browser = nil
self.netService?.stop()
self.netService = nil
}
}
@available(iOS 14.0, *)
extension LocalNetworkManager : NetServiceDelegate {
func netServiceDidPublish(_ sender: NetService) {
MMGlobal.log("Local network permission has been granted")
self.reset()
self.authResult?(true)
}
}
参考链接:
- IOS14本地网络网络权限检测
- iOS--NetService和NetServiceBrowser(Bonjour网络编程)
- WWDC 2018:Network.framework 入门,现代化 Socket 编程的新选择
- (无网通信)MDNS实现设备间的发现与通信
- iOS 14 How to trigger Local Network dialog and check user answer?