标签: PPP

  • 蜂窝 PPP 路由与 DNS:metric 1000 的所有权边界

    结论:蜂窝链在 modem.lock 仲裁下启动 pppd CID 1,用 metric 1000 默认路由、策略表 table 1000 与 resolvectl DNS 接管默认网络,但 connected 字段只等于「ppp0 接口有 IP」,既不保证 peer 存在,也不保证路由/DNS 成功。这些全局名字(table 1000、pppd)不是独占资源,多链路并存时会互相破坏。本文为固定源码快照 c8fb906 的静态分析,蜂窝链只服务网络与遥测,不是车辆控制执行链。

    AT 控制口:锁与空响应的歧义

    Modem._at() 非阻塞持有 /dev/shm/modem.lock,打开 AT 控制口,清输入,逐行等 OK;锁忙、超时、ERROR 或串口 I/O 异常返回 []。六项原方法假串口探针确认:正常路径将短写剩余字节继续写出,忽略空行,收集 OK 前的响应;锁争用时关闭锁 fd 并返回 [],不会打开控制串口。因此空响应不区分「共享端口被占用」「模组离线」与「指令被拒」三种原因。

    还有两个不折叠为空响应的边界:锁文件本身的 os.open() 位于捕获块之前,若 /dev/shm 不可用异常会直接传播;finally 先 flock(LOCK_UN) 再 os.close(fd),探针注入解锁 OSError 后正常响应被该异常覆盖,且关闭语句未执行。这是条件性清理缺口,不表示目标内核的正常解锁会失败。

    PPP 数据口与路由/DNS 安装

    PPPSession.start() 以 sudo pppd 启动 CID 1;kill() 停止时执行 sudo killall -9 pppd,没有按 self._proc.pid 限定。新硬件若另有 PPP 会话,也会进入停止目标集合。maybe_install_routes() 仅 peer 非空且不同于 _peer 才校验 IPv4、清旧路由、依次安装默认路由、表 1000 默认路由与源地址规则;失败则清理并 kill PPP。

    六项原 PPPSession/假 subprocess.run 探针确认:相同 peer 即使本地 IP 从 10.0.0.2 变为 10.0.0.3,也不执行任何路由命令;无效 IPv4 在清路由前就调用全局 killall -9 pppd。新 peer 成功路径先清 ppp0 默认路由、flush 表 1000、删除相关规则,再依次安装三条配置;第二条 add 失败时再次执行全局清理并杀 PPP,_peer 不更新。DNS 侧,maybe_install_dns() 有 DNS 时设置 ppp0 DNS 与 resolved 默认路由,命令失败 kill PPP;resolvectl default-route 失败时只立即杀 PPP,该方法内没有同步调用 cleanup_routes()。

    关键交错是:同 peer 不重装路由,导致 DNS 安装也被跳过——_poll_iface() 仅当 maybe_install_routes() 返回真才调用 maybe_install_dns()。若第一次 DNS 安装失败会退出 PPP 并重拨重装;但若 resolved 配置后来单独丢失、PPP 仍在且 peer 不变,当前轮询路径没有恢复机制。此结论来自调用条件,不等于实机已复现。

    函数 源码可确认行为 不能据此证明
    Modem._at() 非阻塞持有 modem.lock,锁忙/超时/ERROR/IO 异常返回 []。 空响应不区分端口占用、模组离线与指令被拒。
    _do_searching() 同时要求 CREG、CGREG 为 home/roaming;漫游禁用只对 CREG == roaming 单独阻断。 CREG=home 而 CGREG=roaming 仍满足连接门。
    PPPSession.kill() 执行 sudo killall -9 pppd,未按 self._proc.pid 限定。 其他 PPP 会话也会进入停止目标集合。
    maybe_install_routes() 仅 peer 变化时安装默认路由、表 1000 默认路由与源地址规则;失败清理并 kill PPP。 同 peer 下外部删除路由或源 IP 改变不自愈。
    cleanup_routes() 删除 ppp0 默认路由、flush 表 1000、删所有 table 1000 规则并 resolvectl revert ppp0。 table 1000 与 pppd 是全局名字,代码未证明其独占。
    _poll_iface() 解析到 inet 地址就返回 connected=True。 不保证 peer、路由/DNS 成功,更不是互联网探活。

    状态发布与字段年龄

    _do_connecting() 启动 pppd 后立刻进入枚举名 CONNECTED,但此时共享 JSON 的 connected 仍可为 False;只有 _poll_iface() 看到 ppp0 地址才设真。因此状态机阶段和接口 IP 观测是两个证据层级。_do_disconnecting() 先发布初始快照,再全局杀 PPP、清路由、复位数据口;run() 异常只将本地 state 改为 DISCONNECTING 而不发布,下一轮断开 handler 返回 INITIALIZING,因此远端可能完全看不到该阶段。

    状态写入先创建临时 JSON,chmod 0o644 后 os.replace 到共享路径;seconds_since_boot 是最近任一字段发布时间,不是每个温度/信号/IP 的采样时间。四项原 _publish_state()/_poll() 临时文件探针确认:增量发布 connected 时旧 signal_quality 和 iccid 仍保留,但全局时间更新为本次发布时刻;七个采样函数都返回空字典时 _poll() 不发布。HardwareComma.get_modem_state() 读取该 JSON,hw_state_thread() 约十秒刷新一次再写入 deviceState,这条链没有逐字段新鲜度。

    证据层级 来源 能证明 不能证明
    状态机阶段 枚举名 CONNECTING/CONNECTED 代码走到哪一步。 接口是否已有 IP。
    接口观测 _poll_iface() 读 ppp0 接口是否解析出 IPv4 地址。 peer、路由、DNS 是否就绪。
    共享状态文件 _publish_state() 原子 JSON 本轮采样的字段增量。 每个字段是否同样新鲜。
    设备状态汇总 hw_state_thread() 约十秒一次的快照。 逐字段时间戳。

    新硬件与插件重构要求

    源码给出的方向是:CellularTransport 插件应只管理自己启动的进程和专属路由表/规则,提供资源所有者 ID;不能通过 killall pppd 或无所有者的 table 1000 清理与其他链路争用。状态应拆分 modem_seen、sim_ready、registered、ppp_running、ip_assigned、route_installed、dns_installed、internet_reachable,每项附采样时间、错误码和恢复动作。

    对 AT 锁忙、超时、设备消失、指令拒绝和空成功响应应给出不同结果;APN/漫游配置提交必须读回确认。台架故障注入需覆盖:第二个 PPP 会话、其他消费者占用表 1000、路由/DNS 被外部删除、同 peer 换 IP、AT 被 LPA 占锁、SIM 热插拔、解析异常、无 DNS 与 resolved 重启。路由/DNS、接口采样、共享状态和 AT 局部控制流共有二十三项原方法隔离探针,其余状态转移仍主要为静态追踪。

    常见问题

    connected=True 就代表蜂窝网络可用吗?

    不代表。_poll_iface() 只要解析到 inet 地址就返回 connected=True,即使 peer 字段缺失或路由安装方法返回 False 也成立;它既不保证 peer 存在,也不保证路由/DNS 成功,更不是互联网探活。

    为什么换 IP 后路由不重建?

    因为 maybe_install_routes() 只在 peer 非空且不同于 _peer 时才安装;探针确认相同 peer 下本地 IP 从 10.0.0.2 变为 10.0.0.3 也不执行任何路由命令,DNS 安装也随之被跳过。

    AT 返回空响应到底是哪种故障?

    无法从空响应区分。锁忙、超时、ERROR 与串口 I/O 异常都返回 [];插件化后应对这些情况给出不同结果与可恢复动作。

    为什么十秒刷新一次还不够新鲜?

    因为 seconds_since_boot 是最近任一字段的发布时间,不是每个字段各自的采样时间;单项采样可能刷新全局时间,旧的 signal_quality 或 network_type 可能与当前 ppp0 IP 并列出现。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/MODEM_PPP_ROUTE_DNS_OWNERSHIP_CONTRACT_DEEP_DIVE.md(PPP 进程、路由/DNS 所有权与状态可信度)。
    • 项目 02 —— 同目录探针记录:六项 AT 锁/应答、六项 PPP 命令、七项接口/DNS、四项共享状态隔离探针结论。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • 蜂窝模组六态状态机:CREG 与 CGREG 的漫游门为何不对称

    结论:蜂窝数据链的核心问题不是「有没有连上」,而是一组被代码门控范围和发布相位放大的信任缺口:漫游许可只门控 CREG=roaming、并不单独拒绝 CGREG=roaming;ICCID 只校验字符集、不校验长度;connected=True 只表示看到了 ppp0 的接口 IP,不等于外网端到端连通或 DNS 已成功。以下基于固定源码快照 c8fb906 的静态调用链与三组假依赖探针,未接 Quectel 模组、SIM 或运营商网络。

    进程与接口边界

    蜂窝链是一个常驻 Python 进程加三个对外接口:控制口、数据口与状态文件。它以「模组可用」为目标,而不是以 onroad 为前提;宿主还会在启动时临时接管系统网络服务,这一接管本身也带状态恢复风险。

    层 入口 行为
    生命周期 process_config.py、Modem.run()、stop() modem 是常驻进程,不以 onroad 为前提。启动时若服务文件存在则尝试临时屏蔽/停止 ModemManager;正常执行到 stop() 末才尝试解除屏蔽/启动,不记录其原状态,也不检查命令返回码
    控制口 Modem._at() /dev/modem_at0 以 9600 波特率打开;抢不到 /dev/shm/modem.lock、收到错误或超时均返回空响应。该锁也由 qcomgpsd 和 LPA 使用
    数据口 PPPSession.start() pppd 使用 /dev/modem_at1 拨号 CID 1;connected 不是由进程启动直接确认,而是在 ppp0 发现 IP 后更新
    配置 _do_initializing()、_params_changed() 从持久参数读取 GsmApn、GsmRoaming;SIM 身份变更、APN/漫游变更引发重新初始化
    状态输出 _publish_state()、HardwareComma.get_modem_state() 状态先写 /dev/shm 临时文件再原子替换为 /dev/shm/modem;硬件抽象读取 JSON 后由 hardwared 发布 deviceState

    六态状态机与关键函数

    状态机在 INITIALIZING → SEARCHING → CONNECTING → CONNECTED → DISCONNECTING 之间流转。_do_initializing() 清理旧 PPP/路由、配置 AT、读取 IMEI/ICCID/IMSI/版本,对 EG25 应用专用设置并写入 APN;身份不完整会留在初始化态。部分 AT 配置调用没有逐条检查返回值,因此不能仅凭进入 SEARCHING 判定所有模组配置成功——探针也确认,AT+CGDCONT 返回空列表仍会发布身份并进入搜索态。

    _poll_iface() 从 ppp0 提取 IP/peer,并调用 maybe_install_routes() 配置默认路由与策略路由表 1000,成功后调用 maybe_install_dns()。所以 connected=True 表示看到了接口 IP,不是外网端到端连通或 DNS 已成功的证明。

    注册门:CREG 与 CGREG 的不对称

    搜索态先读 CREG,没有响应则不查询 CGREG;拨号要求两者均报告 home 或 roaming。但漫游禁止分支只在 CREG 为 roaming 时阻断:当 CREG 为 home、CGREG 为 roaming 时,代码仍进入 CONNECTING。这是代码门控范围的问题——它不等于运营商实际会建立数据链,也不证明资费策略正确。

    此外,_check_iccid() 周期性发现换卡,ICCID 查询空响应不会触发换卡,收到非空且不同的值才设置标记。PPP 退出后的处理是:前两次失败各执行一次数据口 DTR 复位并再次启动 pppd,第三次转入断开、不再复位/重拨;若此时已有 SIM 变更标记则直接转断开,不增加失败计数。

    身份与遥测的宽边界

    身份解析与遥测采样的接纳规则普遍宽松,且错误常常以「整组不更新」而不是「逐字段报错」的形式出现。

    字段 接纳规则 边界
    IMEI 要求全数字且长度为 14 至 17 长度有门,格式受限
    ICCID 只检查字符是否属于十六进制集合 没有长度门,单字符 A 也可作为非空身份穿过初始化门
    _atv() 前缀 用子串判断前缀并按第一个冒号切分 示例 noise:+QCCID:12345 得到 +QCCID:12345,非精确解析所请求响应
    mcc_mnc 仅取全数字 IMSI 的前六位 不单独判定 MCC/MNC 的实际长度
    采样项 边界行为
    CSQ CSQ=99 视为缺测,却未拒绝 -1 或大于 31 的值;-1 产生负质量,40 的质量被上限钳成 100
    COPS 制式码 网络制式码无效时仍可更新运营商名
    频段通道号 无法解析时整组频段/通道都不更新
    温度 列表中的 255 被跳过,但任一非数字项会令整组返回空增量
    流量计数 tx_bytes 或 rx_bytes 其中一个读取/解析失败,整组返回空增量

    结合增量发布,这些「整组返回空增量」会让旧值继续保留,因此这些状态字段不是逐字段实时状态。

    启动接管与退出恢复

    run() 只以 ModemManager 单元文件是否存在决定执行 mask --runtime、stop,不检查两个命令的退出码;stop() 无条件尝试 unmask --runtime、start,不回忆服务接管前是否已停止或屏蔽。恢复顺序为杀 PPP、清路由、删除状态文件、恢复服务,若中途清理抛异常,后续恢复不会执行。main() 在 run() 与 stop() 之间没有 finally,所以循环外启动异常会跳过 stop()。

    另一个值得记录的相位问题是:run() 的异常捕获只修改内存中的枚举态,不在该分支发布 DISCONNECTING;随后正常的断开处理器先发布带 INITIALIZING 字段的初始快照,返回时又发布 INITIALIZING。因此若没有其他正常转移,消费者可能完全看不到这次异常的 DISCONNECTING 阶段。

    AT 初始化同样是「逐条调用、宽验收」:最终只依据 AT+CGMI 的首行是否有回显决定通过,前面 AT_INIT 的空/错误应答不会阻止前进;EG25 专用六条命令也不检查应答,非 EG25 直接跳过。此外,特定 Webbing ICCID 前缀会直接允许漫游而不读取 GsmRoaming——这是代码识别规则,不是实卡资费保证。

    断开处理的顺序也值得注意:它先发布 connected=False、state=INITIALIZING 的初始快照,随后才杀 PPP、清路由和复位数据口,因此状态发布与资源撤销不是同一事务——消费方可能先看到「已断开」,而旧 PPP 与路由仍在。

    常见问题

    connected=True 就代表设备能上网吗?

    不代表。它只表示在 ppp0 上看到了接口 IP,不是外网端到端连通,也不是 DNS 已成功的证明;路由与 DNS 是后续独立步骤。

    漫游被禁止时为什么仍可能进入拨号?

    因为漫游允许判断只门控 CREG=roaming,没有单独拒绝 CGREG=roaming。当 CREG 为 home、CGREG 为 roaming 时,代码仍进入 CONNECTING。

    ICCID 的解析有什么风险?

    它只检查字符是否属于十六进制集合,没有长度门,单字符 A 也能作为非空身份穿过初始化门;这属于代码接纳规则,不表示模组会给出这种异常应答。

    为什么模组状态字段会显示旧值?

    温度、流量等采样项在任一子项解析失败时会让整组返回空增量,结合增量发布机制,旧值可能继续保留,因此这些字段不是逐字段实时状态。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/MODEM_DEEP_DIVE.md(蜂窝模组六态状态机、身份/遥测解析与生命周期接管,含源码行号索引)
    • 项目 02 —— openpilot-research/probes/test_modem_state_machine_contract.py(十一项注册门与发布相位隔离探针)
    • 项目 02 —— openpilot-research/probes/test_modem_identity_telemetry_contract.py(八项身份与遥测字段解析探针)

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11