Support local network privacy in your app
Privacy & Security 进阶 20m

在 App 中支持本地网络隐私

Support local network privacy in your app

2020年6月23日

在 Apple 官方观看视频

一句话判断

iOS 14 新增了本地网络权限弹窗,如果你的 App 会扫描局域网设备(如打印机、智能家居、AirPlay),必须在 Info.plist 中声明使用原因,否则网络请求会静默失败。

这场 Session 讲了什么

iOS 14 引入了一项重要的隐私保护:App 在访问本地网络之前必须获得用户授权。当你的 App 第一次尝试使用 Bonjour、mDNS、或直接访问本地 IP 地址时,系统会弹出一个权限请求对话框。用户可以选择允许或拒绝。如果用户拒绝了,所有本地网络流量会被静默丢弃——你的 App 不会收到任何错误提示。

Session 详细讲解了哪些网络行为会触发权限弹窗。主要包括:Bonjour 服务发现(NSBonjourServices)、本地 IP 地址通信(如 192.168.x.x 或 10.x.x.x)、多播和广播通信。同时,Session 介绍了如何在 Info.plist 中配置 NSLocalNetworkUsageDescription 来提供权限请求的理由文本,以及如何使用 NWPathMonitor 检测本地网络是否可用。

值得深挖的点

静默失败的陷阱

这是最需要警惕的行为。当用户拒绝本地网络权限后,你的网络请求不会返回错误码,而是直接超时或返回空数据。这意味着你可能花大量时间调试”为什么设备发现不工作”,最终发现是权限问题。建议在 App 中使用 NWPathMonitor 检测本地网络的可用性,在权限被拒绝时给用户明确的引导。

Bonjour 服务的精确声明

你需要在 Info.plist 的 NSBonjourServices 中列出你的 App 使用的所有 Bonjour 服务类型。比如 _http._tcp_airplay._tcp 等。只声明你实际需要的服务类型,不要列出所有可能的服务。Apple 会审查这个列表,过多的服务声明可能被拒绝。

代码片段

配置 Info.plist

场景:为智能家居 App 声明本地网络权限。

<!-- 在 Info.plist 中添加 -->
<!-- 本地网络使用说明 -->
<key>NSLocalNetworkUsageDescription</key>
<string>需要访问本地网络来发现和控制您的智能家居设备</string>

<!-- 声明使用的 Bonjour 服务类型 -->
<key>NSBonjourServices</key>
<array>
    <string>_http._tcp</string>
    <string>_smartplug._tcp</string>
    <string>_huebridge._tcp</string>
</array>

检测本地网络可用性

场景:在 App 启动时检查本地网络权限状态。

import Network

class LocalNetworkMonitor {
    private let monitor = NWPathMonitor(requiredInterfaceType: .other)
    private let queue = DispatchQueue(label: "network-monitor")
    
    var isLocalNetworkAvailable: Bool = false
    
    func startMonitoring() {
        monitor.pathUpdateHandler = { [weak self] path in
            // 检查是否有本地网络访问能力
            let hasLocalNetwork = path.status == .satisfied
            
            // 如果使用了本地地址,检查是否可达
            DispatchQueue.main.async {
                self?.isLocalNetworkAvailable = hasLocalNetwork
                
                if !hasLocalNetwork {
                    print("本地网络不可用,可能是权限被拒绝")
                    self?.showPermissionGuidance()
                }
            }
        }
        monitor.start(queue: queue)
    }
    
    func stopMonitoring() {
        monitor.cancel()
    }
    
    private func showPermissionGuidance() {
        // 提示用户去设置中开启本地网络权限
        let alert = UIAlertController(
            title: "无法发现设备",
            message: "请在「设置」中允许访问本地网络",
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "去设置", style: .default) { _ in
            if let url = URL(string: UIApplication.openSettingsURLString) {
                UIApplication.shared.open(url)
            }
        })
        alert.addAction(UIAlertAction(title: "取消", style: .cancel))
        
        // 显示弹窗
        if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
           let rootVC = windowScene.windows.first?.rootViewController {
            rootVC.present(alert, animated: true)
        }
    }
}

使用 Bonjour 发现设备

场景:发现局域网中的智能设备。

import Foundation

class DeviceDiscoveryManager: NSObject, NetServiceBrowserDelegate, NetServiceDelegate {
    
    private var serviceBrowser: NetServiceBrowser?
    private var discoveredServices: [NetService] = []
    var onDeviceFound: ((String, String) -> Void)?  // (名称, 地址)
    
    func startDiscovery() {
        serviceBrowser = NetServiceBrowser()
        serviceBrowser?.delegate = self
        // 搜索声明的 Bonjour 服务
        serviceBrowser?.searchForServices(
            ofType: "_smartplug._tcp",
            inDomain: "local."
        )
    }
    
    func stopDiscovery() {
        serviceBrowser?.stop()
        serviceBrowser = nil
    }
    
    // NetServiceBrowserDelegate
    func netServiceBrowser(_ browser: NetServiceBrowser,
                           didFind service: NetService,
                           moreComing: Bool) {
        print("发现设备: \(service.name)")
        service.delegate = self
        service.resolve(withTimeout: 5.0)
        discoveredServices.append(service)
    }
    
    func netServiceBrowser(_ browser: NetServiceBrowser,
                           didRemove service: NetService,
                           moreComing: Bool) {
        print("设备离线: \(service.name)")
        discoveredServices.removeAll { $0 === service }
    }
    
    // NetServiceDelegate
    func netServiceDidResolveAddress(_ sender: NetService) {
        // 获取设备地址
        if let hostName = sender.hostName {
            print("设备地址: \(hostName):\(sender.port)")
            onDeviceFound?(sender.name, "\(hostName):\(sender.port)")
        }
    }
}

使用 Network.framework 进行本地通信

场景:与局域网设备建立 TCP 连接。

import Network

class DeviceConnection {
    private var connection: NWConnection?
    
    func connect(to host: String, port: UInt16) {
        let endpointHost = NWEndpoint.Host(host)
        guard let endpointPort = NWEndpoint.Port(rawValue: port) else { return }
        
        // 创建 TCP 连接
        connection = NWConnection(
            host: endpointHost,
            port: endpointPort,
            using: .tcp
        )
        
        // 设置状态回调
        connection?.stateUpdateHandler = { state in
            switch state {
            case .ready:
                print("连接成功")
            case .failed(let error):
                print("连接失败: \(error)")
                // 如果是权限问题,不会返回特定错误
                // 需要通过超时来判断
            case .waiting(let error):
                print("等待连接: \(error)")
            default:
                break
            }
        }
        
        connection?.start(queue: .main)
    }
    
    func send(data: Data) {
        connection?.send(content: data, completion: .contentProcessed { error in
            if let error = error {
                print("发送失败: \(error)")
            }
        })
    }
}

最佳实践

已有项目:立即在 Info.plist 中添加 NSLocalNetworkUsageDescriptionNSBonjourServices。否则 iOS 14 上你的设备发现功能会直接失效。在 App 中加入本地网络可用性检测,当用户拒绝权限时给出明确的引导。在每次 App 更新时检查 Bonjour 服务列表是否需要更新。

新项目:从第一天就配置好本地网络权限声明。NSLocalNetworkUsageDescription 的文案应该具体说明你的 App 需要访问本地网络的原因——“需要访问网络”这样的模糊描述会被 App Review 打回。只为你的 App 实际使用的服务声明 Bonjour 类型。

还有什么值得关注

  • 本地网络权限只影响局域网通信(RFC 1918 地址段),不影响互联网通信。
  • 用户可以随时在「设置 > 隐私 > 本地网络」中更改权限。
  • AirPlay 和 AirDrop 不受这个权限影响,它们有独立的系统级权限管理。
WWDC 2020