教程 30:从 ipcp_up() 到 ppp_link_status_cb()——PPP 地址、DNS、Default Route、Link Down 与 Reconnect
摘要:从 IPCP/IPv6CP OPENED 追踪 netif 地址、peer DNS、default netif、link up/down、PPPERR 回调、关闭与应用侧重连责任,建立 PPP 网络配置生命周期。
[TOC]
Stage 29 的终点是 IPCP/IPv6CP 进入 OPENED,随后 np_up() 把 PPP session 推到 PPP_PHASE_RUNNING。Stage 30 继续回答更接近产品代码的问题:协商得到的 IP 地址写到哪里、peer DNS(Domain Name System,域名系统;这里指对端通过 IPCP 提供的 DNS server 地址)何时生效、default route(无更具体路由时的默认出口)是否自动切到 PPP、link down(链路不可用)时地址/DNS 如何清理,以及 reconnect(重新建立一个 PPP session)由谁负责。S1
阅读源码前:建议先把“协商结果”和“应用路由策略”分开
Stage 30 不再重新教学 LCP/FSM,而是接住 Stage 29 已经 OPENED 的 IPCP/IPv6CP,观察协商结果怎样真正写入 lwIP netif,以及链路结束后哪些状态必须撤销。S1
- RFC 1332 — The PPP Internet Protocol Control Protocol (IPCP)
- 用途:确认 PPP IPv4 地址参数属于 IPCP 协商结果,而不是串口/Driver 自己配置出来的地址。
- RFC 1877 — PPP Internet Protocol Control Protocol Extensions for Name Server Addresses
- 用途:理解 peer DNS 地址为什么可以作为 IPCP option 返回,以及 lwIP 的
usepeerdns/sdns()在哪里接住它。
- 用途:理解 peer DNS 地址为什么可以作为 IPCP option 返回,以及 lwIP 的
- RFC 5072 — IP Version 6 over PPP
- 用途:区分 IPv6CP Interface-Identifier(接口标识符)、PPP link-local(链路本地)地址与一般 LAN 上 SLAAC(Stateless Address Autoconfiguration,无状态地址自动配置)/DHCPv6(IPv6 Dynamic Host Configuration Protocol)的职责。
- lwIP PPP source
- 用途:对照
ipcp_up()、ipv6cp_up()、sifaddr()、sifup()、ppp_close()与 link status callback(PPP 状态通知回调)的生命周期实现。
- 用途:对照
Stage 30 的核心问题:协议已经谈妥以后,谁把结果变成“可用网络接口”
Stage 29 的 np_up() 说明某个 Network Protocol 已经协商成功,但应用真正能使用 PPP netif,还需要把 negotiated parameters 写进网络栈对象,并建立清晰的 up/down 生命周期。S1
几个后文会直接影响控制流的名称先定义清楚:
- local address:当前 lwIP PPP endpoint 自己的 IPv4 地址;IPCP 中对应
ouraddr一类 negotiated value。 - peer address:点对点链路另一端的 IPv4 地址。lwIP 的
sifaddr()把它写进netif->gw,这里的gw表示“这条 point-to-point link 的下一跳 peer”,不能机械套成 Ethernet LAN 中的网关设备。S1 - peer DNS:通过 IPCP 扩展 option 获得的 DNS server 地址;只有应用启用
usepeerdns且 peer 真正提供地址时,lwIP 才调用sdns()更新 DNS server slot。S1S6 - default netif / default route policy:lwIP 在没有更具体路由匹配时选择的默认接口。当前 PPP Core 不会因为 IPCP OPENED 就自动调用
ppp_set_default();这是应用侧路由策略,而不是 IPCP 的协议结果。S1 - link status callback:应用收到 PPP 成功或失败原因的回调;它和
netif_set_link_up/down()的 link-state 通知相关,但不是同一个抽象。S1S2 - reconnect:一次 PPP session 结束后重新启动新的连接尝试。当前 Core 会报告终止原因,但不会替应用无限自动重拨;退避、重试上限和默认路由恢复属于应用策略。S1
Stage 30 生命周期总图:IPCP/IPv6CP OPENED 只是中点,不是终点
1 | flowchart TD |
这张图刻意把“协议结果”和“应用策略”分开:sifaddr()/sdns() 属于 negotiated state 的落地;ppp_set_default() 和重连调度属于应用选择。
协议结果与 lwIP 对象的映射
| 协议/生命周期结果 | lwIP 落点 | 主要函数 | down/结束时谁撤销 |
|---|---|---|---|
| IPCP local/peer IPv4 | netif IPv4 addr/netmask/gw |
ipcp_up() → sifaddr() |
ipcp_down() → cifaddr() |
| IPCP peer DNS | global DNS server slots | sdns() |
cdns() 只清理匹配的地址 |
| IPv4 protocol available | pcb->if4_up + netif link state |
sifup() |
sifdown() |
| IPv6 protocol available | pcb->if6_up + IPv6 link-local |
ipv6cp_up() / sif6up() |
ipv6cp_down() / sif6down() |
| Application success/error observation | PPP status callback | ppp_link_status_cb() |
session end 再次 callback |
| Default interface policy | netif_default |
ppp_set_default() / pppapi_set_default() |
应用按生命周期调整 |
| Session termination | PPP phase / link adapter state | ppp_close() → ppp_link_terminated() |
callback 后由应用决定 reconnect/free |
下面从 ipcp_up() 开始逐层追踪这些映射。
1. ipcp_up() 先确认 negotiated IPv4 参数,再配置 netif
IPCP FSM 到 OPENED 后进入 ipcp_up()。在调用 sifaddr() 之前,当前实现先检查 local/remote address 是否有效,并根据 negotiation 结果决定最终 go->ouraddr 与 ho->hisaddr。S1S4
正常非 demand path 的关键调用是:S1
1 | mask = get_mask(go->ouraddr); |
所以“IPCP negotiation 完成”和“lwIP netif 已可发送 IPv4”之间还存在实际 interface configuration。
2. sifaddr() 把 peer address 写进 netif->gw
进入 sifaddr():S1
1 | int sifaddr(ppp_pcb *pcb, u32_t our_adr, u32_t his_adr, u32_t netmask) { |
这里建立的是 point-to-point interface 的 lwIP IPv4 view:
1 | netif IPv4 address = our_adr |
在 PPP 上,his_adr 是 peer IPv4 address,不是 Ethernet 场景中“同一 LAN 上 ARP 可解析的路由器 MAC”。PPP data path 没有 ARP/Ethernet neighbor resolution 这一层。S3S4
3. 当前 get_mask() 实际返回广播掩码常量
当前 target 中 get_mask() 的传统 classful/system-interface 推导代码被 #if 0 禁用,函数最后返回:S1
1 | LWIP_UNUSED_ARG(addr); |
也就是 0xffffffff。这反映 PPP point-to-point netif 不依赖 Ethernet subnet reachability 选择 peer;不要把这里的 netmask 行为机械等同于 Stage 18 的 Ethernet subnet route match。
4. sifup() 同时改变 PPP protocol gate 与 netif link state
sifup():S1
1 | int sifup(ppp_pcb *pcb) { |
这里有三个不同层次的状态变化:
pcb->if4_up = 1:PPP Core 允许PPP_IPdata plane output;netif_set_link_up():lwIPnetiflink state 变成 up;link_status_cb(... PPPERR_NONE ...):通知应用 PPP network protocol 已经成功可用。
netif_set_up() 在 ppp_new() 中已经被调用过;所以 admin up 与 PPP negotiated link up 仍然是不同状态,和 Stage 22 的 Ethernet admin/link 区分一致。S1
5. ppp_link_status_cb() 才是 example 获取 negotiated 地址的应用入口
upstream example 在 PPPERR_NONE 分支读取 ppp_netif(pcb):S2
1 | case PPPERR_NONE: |
这说明应用不应该在 ppp_connect() 返回 ERR_OK 后立刻假设地址已经可用。ppp_connect() 只表示 negotiation 已成功启动;真正 up 的异步证据是 status callback。
6. RFC 1877 的 DNS option 在 lwIP 中由 usepeerdns 控制
RFC 1877 已经定义了 IPCP 的 Primary/Secondary DNS Server Address options;这里不再解释 option negotiation,只看 lwIP 怎样采用它们。S6 当前 ipcp.h 中 CI_MS_DNS1=129、CI_MS_DNS2=131,与 RFC 1877 的 Primary/Secondary DNS option type 对应;是否主动请求这些 option 由 usepeerdns 控制。S1S6
公共 API 宏:S1
1 |
ipcp_resetci() 根据这个 setting 设置 req_dns1/req_dns2,随后 IPCP Configure negotiation 使用 CI_MS_DNS1/CI_MS_DNS2。ipcp_up() 在 negotiation 完成后只有满足:S1S6
1 | if (pcb->settings.usepeerdns && (go->dnsaddr[0] || go->dnsaddr[1])) { |
才把 peer DNS 写入 lwIP DNS subsystem。
7. sdns() 最终落到 dns_setserver()
sdns() 很直接:S1
1 | int sdns(ppp_pcb *pcb, u32_t ns1, u32_t ns2) { |
因此 PPP peer DNS 与 Stage 14 DNS cache/query 使用的是同一套 global DNS server slots。PPP 并没有单独创建一个“只属于这个 netif 的 DNS resolver”。
这也意味着 multi-netif 产品必须自己考虑 DNS source policy:如果 Ethernet DHCP 与 PPP IPCP 都写 global DNS server slots,最后生效者可能改变全局 resolver configuration。
8. IPCP down 会只清理自己曾设置的 DNS
ipcp_down() 调用:S1
1 | sifdown(pcb); |
cdns() 不是无条件把所有 DNS 清零,而是先比较当前 slot 是否仍等于该 PPP session 的 DNS;匹配才清掉:S1
1 | nsa = dns_getserver(0); |
这个细节避免了 PPP down 时误删已经被其他机制替换过的不同 DNS address。
9. ppp_set_default() 不是 IPCP 自动动作
公共 API 定义:S1
1 |
也就是说,PPP “成为默认路由”本质上只是把它的 netif 设为 netif_default。Stage 18 已经解释过 IPv4 route miss 最后如何回退到 netif_default。
当前 ipcp_up() 中传统 pppd 风格的 sifdefaultroute() 逻辑在 lwIP port 里被 #if 0 禁用。S1
因此:
1 | IPCP OPENED |
应用若希望 PPP 承担 default route,需要显式调用 ppp_set_default();在非 Core thread 中则使用 thread-safe wrapper pppapi_set_default()。S1
10. pppapi_* 是非 Core thread 调 PPP API 的桥
Stage 11 已经讲过 lwIP Core context。当前 pppapi.c 使用 tcpip_api_call() 把操作切到 tcpip_thread。S1
例如 pppapi_connect():S1
1 | err_t |
进入 pppapi_do_ppp_connect() worker,真正调用:
1 | return ppp_connect(msg->msg.ppp, msg->msg.msg.connect.holdoff); |
所以产品应用线程如果不在 Core context,应该使用 pppapi_* 版本,而不是绕过 locking/context contract 直接调内部 API。
11. IPv6CP 配置的是 link-local,不是 IPv6 default route
Stage 29 已经看到 ipv6cp_up()。它通过 sif6addr() 把 Interface-Identifier 拼成 link-local:S1S5
1 | IN6_LLADDR_FROM_EUI64(ip6, our_eui64); |
然后 sif6up():
1 | pcb->if6_up = 1; |
IPv6CP 本身不等价于 Router Advertisement,也不会自动提供任意 global IPv6 prefix/default router。Stage 15 总览中的 SLAAC/DHCPv6 是另一类链路环境与控制协议。S5
12. IPv4 与 IPv6 都 up 时,link callback 可能由各自 sif*up() 触发
当前 implementation 的 sifup() 和 sif6up() 都独立调用 link_status_cb(PPPERR_NONE)。S1
因此 dual-stack 产品 callback 不应把一次 PPPERR_NONE 简化成“所有协议都已经完全配置”。更稳妥的判断来自:
1 | ppp_netif(pcb) |
这是当前实现细节,不是 RFC 规定必须回调两次。
13. PPPERR_* 是 session 结束/失败原因的应用接口
ppp.h 定义当前错误码:S1
1 | PPPERR_NONE success |
example 的 ppp_link_status_cb() 正是按这些 error code 分支。S2
应用做 reconnect policy 时,应区分 AUTHFAIL、PEERDEAD、CONNECT、USER 等原因,而不是任何 down event 都立即无限重拨。
14. ppp_close():正常关闭优先走 LCP terminate
ppp_close(pcb, nocarrier) 首先写入:S1
1 | pcb->err_code = PPPERR_USER; |
继续阅读 ppp_close():如果 session 已经在正常 stable phase,推荐 nocarrier=0,函数调用:
1 | lcp_close(pcb, "User request"); |
即通过 LCP FSM 执行正常 Terminate-Request/Terminate-Ack teardown。S1S3
只有特定 nocarrier + PPP_PHASE_RUNNING 情况才直接 lcp_lowerdown() 并强制终止。
15. 为什么源码注释建议即使 carrier lost 也优先 nocarrier=0
ppp_close() 注释明确说明:nocarrier=1 可以在链路已消失时更快强制结束,但总是使用 nocarrier=0 更安全,只是 link down 时等待更久一些,因为它更符合 FSM 正常 shutdown path。S1
这是当前实现的 FSM 工程建议,不是所有平台都必须永远等待正常 terminate handshake。
16. IPCP down 如何撤销 IPv4 data plane
ipcp_down() 首先:S1
1 | if (pcb->ipcp_is_up) { |
然后 sifdown():
1 | pcb->if4_up = 0; |
最后 cifaddr() 清掉地址:
1 | netif_set_addr(pcb->netif, |
所以 PPP link down 不是只改一个 error flag;network protocol state、netif link state、address 与 DNS 都会分层撤销。
17. IPv6CP down 只在 IPv4 也 down 时拉低 netif link
sif6down():S1
1 | pcb->if6_up = 0; |
这保证 dual-stack 场景下:
1 | IPv6CP down |
同理,IPv4 down 时如果 if6_up 仍为 1,也不会把整个 netif link 拉低。
18. ppp_link_terminated() 把 teardown 交回 link adapter
LCP/auth/network teardown 最终进入:S1
1 | void ppp_link_terminated(ppp_pcb *pcb) { |
PPPoS adapter 的 pppos_disconnect() 将 parser/link 标记关闭,再调用:S1
1 | ppp_link_end(ppp); |
ppp_link_end() 最终进入 DEAD phase,并发 status callback:S1
1 | new_phase(pcb, PPP_PHASE_DEAD); |
这才是一次 PPP session 生命周期真正结束。
19. Core 不会在 PPPERR_CONNECT 后自动无限重连
ppp_connect() 只有在 PPP_PHASE_DEAD 才允许新连接:S1
1 | if (pcb->phase != PPP_PHASE_DEAD) { |
继续阅读 ppp_connect():holdoff 参数只是“这一次调用在真正开始 negotiation 前等待多少秒”:
1 | new_phase(pcb, PPP_PHASE_HOLDOFF); |
它不是一个“断线后自动重拨策略”。当前 Core 在 link status callback 报错后不会自己决定 5 秒后再 call ppp_connect()。
因此 reconnect policy 属于应用:
1 | flowchart TD |
20. 为什么不能在 callback 里无条件立即死循环重拨
如果错误是 PPPERR_AUTHFAIL,立即重复同一 username/password 往往不会改变结果;如果是 PPPERR_DEVICE,serial device 甚至可能尚未恢复。应用应该根据 error type、modem state、重试次数和 backoff policy 决定下一步。
这是应用层工程策略。lwIP Core 只提供 phase/error/callback 和重新调用 ppp_connect() 的机制,不替应用定义运营商/modem recovery policy。
21. ppp_free() 与 ppp_close() 生命周期不同
ppp_free() 只允许在 DEAD phase:S1
1 | if (pcb->phase != PPP_PHASE_DEAD) { |
因此正确概念是:
1 | ppp_close() |
如果产品只想断线后稍后重拨,不应该每次都 free/recreate PCB;可以保留 control block,等回到 DEAD 再 connect。
22. Default route 生命周期需要应用自己管理
因为 ppp_set_default() 直接改变全局 netif_default,多接口产品还要决定 PPP down 后默认路由回到谁。
例如:
1 | Ethernet netif + PPP netif |
lwIP PPP Core 不会保存“原 default netif”并自动恢复。Stage 18 的 multi-netif route policy 在这里重新成为应用设计的一部分。S1
23. Stage 30 的完整生命周期
1 | flowchart TD |
24. 当前实现边界
- IPCP negotiated peer address 被放进
netif->gw,但 PPP 不经过 Ethernet ARP; usepeerdns必须显式开启,peer DNS 才会写入 global lwIP DNS slots;- PPP 不会因 IPCP up 自动成为
netif_default,需要显式ppp_set_default(); pppapi_*是非 Core thread 的 thread-safe bridge;- IPv6CP 当前配置 PPP link-local address,不等价于 RA/SLAAC/DHCPv6;
ppp_close()结束 session,ppp_free()销毁 PCB/netif;- Core 不实现“断线自动无限重拨”,应用负责 error-aware reconnect/backoff;
- default netif 的恢复策略同样属于 multi-netif 应用,而不是 PPP Core 自动回滚。
资料来源
[S1] lwIP PPP network configuration 与 PPP API 源码
- 类型:目标版本上游源码
- 版本:commit
d08f4773edd0182b7910fc8f046eed82ffcd67c9 - 定位:
src/netif/ppp/ppp.c:sifaddr()、cifaddr()、sdns()、cdns()、sifup()、sifdown()、sif6addr()、sif6up()、sif6down()、ppp_connect()、ppp_close()、ppp_free()、ppp_link_end();ipcp.c:ipcp_up()/ipcp_down();ipv6cp.c;pppapi.c;src/include/netif/ppp/ppp.h - URL/文档:lwIP PPP source
- 使用位置:“地址写入”“peer DNS”“link status”“default netif”“thread-safe PPP API”“close/free/reconnect”
- 支撑内容:证明当前 pinned PPP network configuration 与 session lifecycle 的真实实现
[S2] lwIP PPPoS example
- 类型:目标版本上游 example
- 版本:commit
d08f4773edd0182b7910fc8f046eed82ffcd67c9 - 定位:
contrib/examples/ppp/pppos_example.c:ppp_link_status_cb()、pppos_example_init() - URL/文档:lwIP PPPoS example
- 使用位置:“PPPERR callback”“negotiated address/DNS 读取”
- 支撑内容:给出应用观察 PPP 成功/失败与配置结果的真实示例入口
[S3] RFC 1661:The Point-to-Point Protocol (PPP)
- 类型:IETF 标准规范
- 版本:RFC 1661,1994
- URL/文档:RFC 1661
- 使用位置:“point-to-point link”“LCP terminate”“network protocol lifecycle”
- 支撑内容:提供 PPP session establishment/termination 的协议背景,用于区分规范与 lwIP API 策略
[S4] RFC 1332:The PPP Internet Protocol Control Protocol (IPCP)
- 类型:IETF 标准规范
- 版本:RFC 1332,1992
- URL/文档:RFC 1332
- 使用位置:“IPv4 address negotiation”“IPCP address option”
- 支撑内容:提供 IPCP network-layer IPv4 configuration 的规范语义
[S5] RFC 5072:IP Version 6 over PPP
- 类型:IETF 标准规范
- 版本:RFC 5072,2007
- URL/文档:RFC 5072
- 使用位置:“IPv6CP Interface-Identifier”“PPP IPv6 link-local”
- 支撑内容:提供 IPv6 over PPP 与 IPv6CP 的标准边界,避免与 SLAAC/DHCPv6 混淆
[S6] RFC 1877:PPP IPCP Extensions for Name Server Addresses
- 类型:IETF Informational RFC
- 版本:RFC 1877,1995
- URL/文档:RFC 1877
- 使用位置:“建议提前阅读”“peer DNS /
usepeerdns”“CI_MS_DNS1/CI_MS_DNS2” - 支撑内容:定义 IPCP Primary/Secondary DNS Server Address options(Type 129/131),用于把 lwIP 的 DNS option 常量与标准扩展对应起来









