WWDC Quick Look 💓 By SwiftGGTeam
Filter and tunnel network traffic with NetworkExtension

Filter and tunnel network traffic with NetworkExtension

观看原视频

Highlight

iOS 26 在 NetworkExtension 中加入 URL Filter API,可基于完整 URL 做内容过滤,并通过 Bloom filter、PIR、Privacy Pass 与 Apple 托管的 OHTTP Relay 在不暴露 URL 与设备身份的前提下完成查询。

核心内容

做家长控制 App 或学校上网管理 App 的开发者一直被一个问题困住:HTTPS 把流量加密了,传统的 NEFilterDataProvider 只能看到 host 和 port。想拦掉某个网站的某条具体路径,比如某个分享链接、某个游戏页面,做不到。要么整站封掉、误伤大量正常内容,要么放过去。如果想绕开这个限制走 URL 维度过滤,就得自建 VPN、自建中间人 CA,把所有 URL 拉到自己服务器上看一眼——这又意味着 App 完全掌握了用户的浏览历史,隐私风险极大。

iOS 26 给出的解法是 URL Filter。它把”基于完整 URL 的过滤决策”做成系统能力:App 不在过滤路径中,看不到任何流量数据,只负责提供两样东西——一个本地 Bloom filter、一个远端 PIR 服务器。系统先用 Bloom filter 在设备端做快速判定,没命中就直接放行;命中(可能是黑名单也可能是误判)才发起 PIR 查询。PIR 用同态加密,让服务器在密文上执行查表,自始至终拿不到 URL 明文。所有查询再经过 Apple 托管的 Oblivious HTTP Relay 转发,把设备 IP 剥掉。开发者拿到了 URL 维度的过滤能力,用户的 URL 与身份都不会泄露给任何一方,包括 Apple、开发者、PIR 服务器自己。这条路适合家长控制、校园 App、企业禁访名单等所有”要看完整 URL,但不能保留浏览历史”的场景。

详细内容

URL Filter 依赖四项技术叠加:Bloom filter 做设备端预过滤,PIR(Private Information Retrieval)让服务器在密文上查库,Privacy Pass 做匿名鉴权,OHTTP Relay 隐藏来源 IP。整个流程系统全权代办,App 与 App Extension 都不在数据通路里(19:01)。

WebKit 和 URLSession 发起的请求会被系统自动检查,但自己写网络栈的浏览器或 App 不会走这条路。这种情况下要主动调用 participation API 询问系统裁决(22:15):

// Use participation API to check URLs before sending requests

import NetworkExtension

func checkURL(url: URL) async throws -> Bool {
  var passRequest : Bool = true

  let verdict = await NEURLFilter.verdict(for: url)

  if verdict == .deny {
    passRequest = false
  }
  return passRequest
}

关键点:

  • import NetworkExtension:所有 URL Filter 相关 API 都在这个框架下。
  • NEURLFilter.verdict(for:) 是 async 调用,发起后系统会做 Bloom filter 预查 + 必要时的 PIR 查询,由系统返回最终裁决。
  • 返回值是枚举,.deny 代表命中黑名单;.allow 或非 deny 时放行请求。
  • App 自己拿不到 URL 是否在数据集中——它只能拿到一个布尔级别的”过/不过”。

接下来是 App 如何配置自己的 URL Filter(25:01):

// Configure and manage URL Filter

import NetworkExtension

let manager = NEURLFilterManager.shared

try await manager.loadFromPreferences()

try manager.setConfiguration(
    pirServerURL: URL(string:"https://pir.example.com")!,
    pirPrivacyPassIssuerURL: URL(string:"https://privacypass.example.com")!,
    pirAuthenticationToken: "1234",
    controlProviderBundleIdentifier: "com.example.myURLFilter.extension")

manager.prefilterFetchInterval = 86400 // fetch every 1 day
manager.shouldFailClosed = false
manager.localizedDescription = "Alice's URL Filter"
manager.isEnabled = true

try await manager.saveToPreferences()

关键点:

  • NEURLFilterManager.shared:单例。所有 URL Filter 配置都通过它读写。
  • loadFromPreferences():从磁盘加载已有配置;改动前先 load 再 set 是 NetworkExtension 框架的固定模式。
  • setConfiguration 三个 URL/Token 参数:分别指向你自己的 PIR 服务器、Privacy Pass Issuer,以及鉴权 token。Apple 不托管这些组件,开发者自己部署。
  • controlProviderBundleIdentifier:你的 App Extension 的 bundle id,系统据此唤醒 extension 来抓取 Bloom filter。
  • prefilterFetchInterval = 86400:单位秒,控制系统多久回调一次 extension 的 fetchPrefilter。这里设成一天一次。
  • shouldFailClosed = false:PIR 失败时放行(false)还是拒绝(true)。家长控制类场景可能要设 true。
  • isEnabled = true + saveToPreferences():写盘后过滤才真正生效。

App Extension 这一侧实现 NEURLFilterControlProvider 协议,负责把 Bloom filter 数据交给系统(26:41):

// Implement NEURLFilterControlProvider protocol

import NetworkExtension

class URLFilterControlProvider: NEURLFilterControlProvider {

  func fetchPrefilter() async throws -> NEURLFilterPrefilter? {
        
    // Fetch your Bloom filters data from your app bundle or from your server
    let data = NEURLFilterPrefilter.PrefilterData.temporaryFilepath(fileURL)
    let result = NEURLFilterPrefilter(data: data,
                                      bitCount: numberOfBits,
                                      hashCount: numberOfHashes,
                                      murmurSeed: murmurSeed)
    return result
  }
}

关键点:

  • 继承 NEURLFilterControlProvider,由 Xcode 的 URL Filter app extension 模板生成骨架。
  • fetchPrefilter() 由系统按 prefilterFetchInterval 调度调用。Extension 可从 app bundle 或自家服务器拉数据。
  • NEURLFilterPrefilter.PrefilterData.temporaryFilepath(fileURL):通过临时文件路径而非内存 buffer 把数据交给系统,适合大数据集。
  • 构造 NEURLFilterPrefilter 时必须传 bitCounthashCountmurmurSeed——这三个参数决定 Bloom filter 的尺寸、哈希函数数量、murmur 哈希的 seed,必须与你在服务端构建 Bloom filter 时使用的参数完全一致,否则匹配失效。

部署上还有两个关键点:URL Filter 通过 OHTTP Relay 出网,需要事先向 Apple 申请 Relay 能力,开发版构建可豁免,但 App Store / TestFlight / Ad-hoc / 企业签名分发都必须走完审批流程(19:38)。PIR 服务器侧的 use case 名字必须以 App 的 bundle id 开头、跟上 url.filtering 字符串,记录格式为 URL 字符串作 key、整数 1 作 value(23:22)。

核心启发

  • 做家长控制 / 校园上网管理 App,迁移到 URL Filter:为什么值得做——传统 NEFilterDataProvider 看不到 HTTPS 的完整 URL,只能整站封禁,体验粗糙;URL Filter 能精确到资源级别,且面向消费级设备开放,不再仅限 supervised device。怎么开始——先用 Apple 提供的 PIRService 示例代码 跑通服务端,App 这边申请 url-filter-provider entitlement,按上面的三段代码骨架搭出 Manager 配置、Extension 抓 Bloom filter、participation API 兜底自定义网络栈。

  • 企业内网访问改用 Network Relay 替代传统 VPN:为什么值得做——MASQUE 协议针对 TCP/UDP 应用流量优化,平台原生支持,不需要写 NEPacketTunnelProvider,企业邮件和协作类云应用最适合走这条路(03:53);全隧道 IP VPN 反而是少数高合规场景才需要。怎么开始——用 NERelayManager API 配置 relay 与认证,或通过 MDM 配置文件下发;用户侧无需安装额外 extension。

  • 检查现有 VPN App 是否还在用 Packet Filter 或直接改路由表:为什么值得做——Apple 在 Session 中明确说这不被支持,会与系统及其他 App 的过滤、路由规则冲突,并破坏 AirDrop、Mac Virtual Display、Sidecar 等功能(07:34)。怎么开始——读 TN3165 与 TN3120 两份 Technote,把自定义隧道逻辑迁到 NEPacketTunnelProvider,并按场景启用 enforceRoutes(split tunnel)或 includeAllNetworks(full tunnel),再配合 excludeLocalNetworks 让 AirDrop 这类本地服务绕开隧道。

  • 不要用 NEPacketTunnelProvider 做内容过滤:为什么值得做——它工作在 IP 层,拿不到流级或应用级元数据,做不出基于 host/URL 的判断(08:26)。怎么开始——按目标重新选 API:拦流量用 NEFilterDataProvider/NEFilterPacketProvider,拦 URL 用新的 NEURLFilter,DNS 安全则走 DNS Configuration & Proxy API。

关联 Session

评论

GitHub Issues · utterances