归档

我想在 iPhone 上拦截所有国际电话,但最终放弃了用 CallKit 实现

2026年7月11日AI 翻译

最近接到的电话自称来自入管部门或中国公安,有时还会播放中文自动语音。至少在我的周围,这不像是面向日本人的普通无差别诈骗电话。

我在中国旅行时曾在酒店填写联系方式,也在预订时使用过电话号码。号码也许经由某个环节流到了中文诈骗团伙手中。

我已经启用了警察厅推荐应用的诈骗及骚扰电话拦截功能,但这个号码仍然打了进来。该功能通过匹配已登记的危险号码工作,无法阻止所有新号码或未登记号码。2

我几乎没有接听海外电话的需要。我希望日本国内电话照常响铃,而日本国家代码 +81 以外的电话全部被拒绝。但我没有找到能按国家代码统一拦截国际电话的 iPhone 应用。

我原以为可以自己做一个:把来电号码当作字符串,用 hasPrefix 或正则表达式拒绝 81 以外的号码。我以为会有类似 grep 的条件匹配,并没有想到要枚举所有电话号码。

调查 Call Directory

Call Directory app extension 通过 CXCallDirectoryProvider.beginRequest(with:) 把拦截列表交给 iOS。

电话号码类型 CXCallDirectoryPhoneNumber 实际上是 Int64,不含 + 和空格,从国家代码开始。API 只能逐个添加完整号码。

context.addBlockingEntry(
    withNextSequentialPhoneNumber: phoneNumber
)

没有前缀、正则表达式、国家代码或号码范围 API。完整号码必须去重并按升序添加,最后调用 completeRequest()。顺序、重复和数量问题分别产生 entriesOutOfOrderduplicateEntriesmaximumEntriesExceeded4

extension 不会在每次来电时启动。Apple 说明识别数据需要一次性提供,来电时不能查询 Web 服务。3 因此没有地方执行 if !number.hasPrefix("81")

Call Directory 的执行方式

  1. 用户在 iOS 来电拦截设置中启用 extension。
  2. 应用调用 reloadExtension(withIdentifier:)
  3. iOS 启动 extension,并把 context 传给 beginRequest(with:)
  4. extension 按顺序添加号码并调用 completeRequest()
  5. 之后由 iOS 完成来电匹配。

context.isIncremental 为 true 时可以差量更新和删除号码,但差量模式也不支持范围或条件式。

查到这里我才意识到,拦截全部国际电话需要枚举目标号码并交给 Call Directory。国际号码遵循 ITU E.164,包含国家代码最长15位。1

这不是已分配号码的精确数量,但把15位空间作为简单上限,大约有 1015 种组合。排除 +81 几乎不改变数量级。只保存 Int64 就需要约 8 PB,加上索引可能达到数十 PB。因此我放弃覆盖全部号码,改为只计算实际频繁来电的 +1 833

缩小到 +1 833

+1 是包含美国和加拿大等地区的北美编号计划国家代码,其后有10位数字。833 是免费电话区号,还剩7位,因此完整覆盖需要 107 = 10,000,000 条

一千万个64位号码占 80,000,000 字节,约 76.3 MiB,尚未包括数组、数据库和系统索引。仅从存储容量看约80MB是现实的,但Apple未公开的条目上限和加载时间仍需测试。

Live Caller ID Lookup

Live Caller ID Lookup 可以在来电时查询服务器,但 extension 不会收到来电事件。LiveCallerIDLookupProtocol 只提供 serviceURLtokenIssuerURLuserTierToken5

@main
struct LookupExtension: LiveCallerIDLookupProtocol {
    var context: LiveCallerIDLookupExtensionContext {
        .init(serviceURL: serviceURL,
              tokenIssuerURL: tokenIssuerURL,
              userTierToken: token)
    }
}

查询由 iOS 执行。Oblivious HTTP 隐藏用户IP,Privacy Pass进行匿名认证,KPIR把完整电话号码作为加密关键词搜索。6

服务端需要三个采用 Protocol Buffers 的 HTTP POST 端点。7

端点用途数据
/configPIR配置和密钥状态ConfigRequest / ConfigResponse
/key上传评估密钥EvaluationKeys
/queries计算加密查询Requests / Responses

拦截payload只有1字节:0放行,1拦截。发信者信息使用 CallIdentity Protocol Buffer。8

服务器不会收到明文号码,所以无法简单执行 hasPrefix("81")。它仍需要以完整号码为键的PIR数据集、Apple端点验证和KPIR服务器。

两种方式的区别

Call DirectoryLive Caller ID Lookup
数据预先载入设备服务器PIR数据集
查询iOS本地匹配iOS加密查询
单位完整电话号码以完整号码为键的条目
前缀条件不支持不能自由执行服务器代码

计算号码范围

输入号码前缀和总位数,计算完整号码条目数。

结论

+1 833 本身有实现余地:一千万条,约80MB。但它只能拦截这个前缀。对方换号码或国家代码后,又要重新调查、生成、分发并加载数据。

发信者号码也不是强身份认证。VoIP和电话转接可能显示伪造号码。910 对频繁更换或伪造号码的对方,号码拦截效果有限。

技术上可以枚举 +1 833,但每次号码变化都维护一次太麻烦,所以我放弃了。

参考资料