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时必须传bitCount、hashCount、murmurSeed——这三个参数决定 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-providerentitlement,按上面的三段代码骨架搭出 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
- Deliver age-appropriate experiences in your app — 配合 URL Filter 给未成年人构建分级体验
- Finish tasks in the background — 理解 Extension 后台调度,用于 Bloom filter 定期拉取
- Get ahead with quantum-secure cryptography — Network Extension 同样依赖现代密码学保护链路
- Optimize home electricity usage with EnergyKit — 另一个由系统代为执行、App 不接触敏感数据的隐私模式范例
评论
GitHub Issues · utterances