在 iPhone 上挪动那个点, 最后连 VPN 也一起做了

14 min

前一篇写过 Android 上的定位工具. 这次单独写 iPhone.

项目叫 Aurora Location. 在地图上选一个点, 让手机的位置停在那里; 或者选起点和终点, 沿路线模拟步行. 表面上看, 与 Android 版本做的是相近的事, 底下却要重新走一条路.

我先把它做成自己愿意打开的 App, 后来又为了让定位和日常代理一起工作, 做了独立的 Aurora VPN. 最后手机上留下两个图标, 一个管定位, 一个管通道和上网.

如今, Wi-Fi 和蜂窝下都可以修改定位, 代理也能继续用. 我还专门试了先在 Wi-Fi 下建立定位会话, 再关闭 Wi-Fi, 切到蜂窝. 已经建立的会话能够保留下来.

这一步看起来只是关掉一个开关. 为了走到这里, 中间绕了不少路.

先把地图上的操作做好

Aurora Location 用 SwiftUI 写界面, 地图交给 MapKit. 定点和模拟步行分成两个标签, 各自有自己的操作位置.

选点可以直接在地图上点, 也可以搜索地点或手工填经纬度. 常用位置存进收藏, 最近选过的地方也留着. 手里已经有坐标时, 就不用再对着地图反复放大缩小.

步行比定点多一些状态. 起点、终点和路线要先确定, 运行时再沿路线推进. 暂停是停留在当前点, 结束则要发送恢复真实定位的指令. 两个按钮不能只是换一个名字.

规划路线也遇到过服务不覆盖目标区域的情况. 后来保留原生路线规划, 再补上可用的步行路线服务作回退. 取不到路线就明确报出来, 不画一条直线假装规划成功.

这些细节让它像一个能反复使用的工具. 可地图和按钮做好以后, 真正费时间的部分才开始.

iPhone 上, 先得把自己配对好

Aurora Location 使用 iPhone 的开发者定位服务. 当前这套本机配对流程在 iOS 27 上验证, 需要开发者模式、签名安装和可用的开发者服务环境.

App 里做了配对向导, 配对记录保存在本机. 之后每次定位, 还要建立开发者连接、找到对应服务, 才能发送设置或恢复指令. 配对文件存在, 不代表后面的连接永远都有效.

定点成功后会继续维护当前位置. 如果后续指令失败, 页面应该显示保持中断或状态未知, 不能因为上一次成功过, 就一直挂着成功状态. App 重新启动时也不会自动重放上次的定位指令.

我后来越来越在意这种区别. 页面上选了哪里, 最近发出了什么指令, 系统实际表现如何, 得分别看. 尤其是恢复真实定位, 不能只把界面按钮改回开始, 就当作事情做完了.

连上了, 然后呢

最早在 Wi-Fi 下, 我复用了 Shadowrocket 的本机 WireGuard 通道. 定位相关的包经过本机中继回到系统, 普通上网照常走代理. 配对和定位引擎都有可工作的结果, 所以最初很容易把蜂窝问题想成少了一个设置.

后来遇到了很磨人的现象: 有些检查能通过, 页面却仍然无法完成定位.

TCP 显示连接成功, 不等于后面的开发者握手成功. UDP 包回来了, 不等于系统服务接受了它. VPN 图标亮着, 也只能说明 VPN 连接状态.

这些区别写在文档里很平常, 排查时却很容易忽略. 尤其是刚看到一条成功日志, 总想顺着它往下推, 觉得剩下的应该只是小问题.

IKEv2 也试过

有一段时间, 我把希望放在另一条 VPN 通道上.

原生 IKEv2、服务器端口绕行、路由旁路, 都做过实验. 某些条件下, 数据确实能过去, 但开发者端口仍然拒绝连接. 再把原来的代理一起打开, 又出现了另一类数据面问题.

这时才必须把问题拆开: 一条网络通道可用, 和系统愿意让某个服务使用它, 是两件需要分别验证的事.

继续改旁路或加长超时, 很容易让配置越来越多, 却没有更接近真正的失败位置. 这些实验最后留下了有用的对照, 没有成为正式使用的方案.

把一个 EOF 拆开看

后来做的一个有效改动, 是把日志拆细.

原来只看到外层报错, 不清楚失败发生在初始连接、配对验证, 还是后续隧道. 给底层加上阶段记录后, Wi-Fi 成功的流程和蜂窝失败的流程才真正能够放在一起比较.

系统日志里也出现了与接口类型限制有关的线索. 自己写一个普通监听器做对照, 加上相近限制后, 能看到类似差异.

这没有让系统内部的一切都变得清楚. 它至少让后面的工作有了方向: 不能再拿普通网络测试成功, 当作开发者服务一定可用的依据.

那段时间的代码看起来没多几个功能, 却比继续加按钮有用.

先有网络, 再短暂关掉

另一组对照给出了可走的路: 短暂关闭蜂窝后, 本机开发者会话能够建立, 然后再恢复网络.

顺序很重要. 如果先把网络全关掉, 系统有时连 VPN 本身都不让新建. 所以要先启动通道, 再临时关蜂窝, 完成定位, 最后把蜂窝恢复.

最开始是手动两步. 真正在手机上跑通后, 再用系统快捷指令把步骤串起来. App 和快捷指令会短暂来回切换, 但不用每次记住一串开关顺序.

顺手补上的还有失败处理. 关闭蜂窝之前要记住恢复任务, 取消了也要尝试恢复. App 重新打开时, 可以继续处理网络恢复, 不能把上次的位置指令悄悄再发一遍.

这时定位已经有了可用办法. 但我原本的要求里, 还有代理.

一个 VPN, 两件事

最终选择是在同一个 PacketTunnel 里处理两类流量.

Aurora VPN 基于 Hako-Client, 复用它的 mihomo 衍生核心、配置管理、订阅、节点选择和网络变化处理. 这些已经有人做好的部分, 没必要再写一套.

新增的部分放在收包入口: 严格符合本机开发者协议的包进入本机处理, 普通流量继续交给代理核心. 地址、协议和长度都要检查, 不能把普通互联网流量误送进去.

Aurora Location 仍然负责配对、定点和步行. 需要连接时唤起 Aurora VPN, 等它真正准备好, 再做开发者握手. 结束定位也不会顺手把代理关闭.

两个 App 各自负责的事情终于清楚了. 手机上看见两个图标, 底下承担代理与本机通道的是同一个 VPN.

真正的配置, 才把问题带出来

刚开始测试用的配置很小. 换成平时实际使用的完整 YAML, 扩展启动一会儿又退出了.

规则集比测试配置大得多. 有一个集合需要处理大约十九万条规则. 先把首次加载并发降下来, 还不够; 临时加了有界的内存采样后, 才把分配集中到下载和域名集合构建的代码上.

我还特意回头看了 mihomo 主仓库. 相关实现与所用核心一致, 原生 MRS 格式也是已有方向. 不过, 为了眼前的问题再加一套下载和转换流程, 会引入新的事情要维护.

最后的改动很小: 能共用的字符串不再重复分配, 遍历队列也不在开始时就按全部规则数量预留空间. 测试通过后, 完整规则终于能加载, 短时观察的内存占用也降了下来.

这次没有靠更多重试撑过去. 找到具体分配位置以后, 要改的东西反而少了.

网页打开了, 还不能高兴太早

验收时还有一个容易漏掉的条件: 我用的 Wi-Fi 本来就有透明代理.

于是, 网页能打开, 甚至出口看起来正确, 都不能单独说明 Aurora VPN 已经在正常处理流量. 有可能底下的路由器早就替它做完了.

后来用明确的网页请求, 对照 App 内部的代理链和节点传输字节, 再单独切到蜂窝测试. 定位也要看实际效果, 不能只看界面写着成功.

最后我确认了两件事: 两种网络下定位和代理都能用; Wi-Fi 下建立的定位会话切到蜂窝后可以保留.

后一个结果意味着日常从 Wi-Fi 离开时, 正常情况下不用主动断开重连. 首次在蜂窝下建立会话, 仍沿用短暂关闭蜂窝的步骤. 这两个场景的区别, 终于能在使用说明里说清楚.

写下来, 下次少绕一点

这轮最值得留下的, 除了能用的 App, 还有那些没有成为最终方案的实验.

为什么 IKEv2 传输可用还不够, 为什么普通连接检查会误导, 为什么小配置测不出大规则的内存问题, 为什么 Wi-Fi 网页成功也需要多看一眼代理链. 把条件和证据记下来, 下次再遇到相近问题, 至少不用从同一个猜测重新开始.

代码里的固定依赖、补丁和构建校验也一并保留. 不能今天源码改了, 明天构建时却悄悄连着昨天的旧 SDK.

长时间锁屏、节点故障恢复和不同系统版本, 还需要继续分别测试. 这次先记下已经在自己手机上确认的结果: 定位和代理终于可以一起工作, 从 Wi-Fi 切到蜂窝, 也不必为了保留定位再走一遍启动流程.


Aurora Location 开发历程记录了阶段、证据与边界. 本文为个人设备上的开发和使用记录, 不承诺所有 iOS 版本或第三方应用表现一致.

↑ 回到顶部