教程 25:从 autoip_start() 到 ACD_IP_OK——IPv4 Link-Local、169.254/16、ARP Probe/Announce、Conflict 与 DHCP Cooperation
摘要:从 AutoIP 入口追踪 169.254 地址选择、AutoIP/ACD 双层状态机、ARP Probe/Announce、冲突防御、DHCP cooperation 与链路变化。
[TOC]
AutoIP 是 lwIP 对 IPv4 Link-Local 自动配置机制的实现。IPv4 Link-Local 的目标是在没有可经路由器转发的 IPv4 配置时,仍让同一二层链路上的设备获得本地 IPv4 连通性;动态候选地址来自 169.254.1.0 到 169.254.254.255,绑定后使用 /16,不依赖默认 gateway,因此它解决的是同链路连通性,不是“没有 DHCP 时自动获得 Internet access”。S3
地址不能选中后立刻使用,因为同一链路上的另一台设备可能已经使用同一个地址。ACD(Address Conflict Detection,地址冲突检测)用 ARP(Address Resolution Protocol,地址解析协议)完成这一检查:candidate 是尚未正式绑定的候选地址;Probe 使用 ARP 询问该 candidate 是否已经被占用;Announcement 在确认可用后声明本机正在使用该地址;地址投入使用后若再次检测到冲突,可以进行一次 Defense,短时间内再次冲突则需要 Retreat(退让) 并重新选择地址;持续冲突还需要 rate limiting(速率限制),避免反复探测形成广播风暴。S4
AutoIP 与 DHCPv4(Dynamic Host Configuration Protocol for IPv4,IPv4 动态主机配置协议)不是互斥机制。配置了 LWIP_DHCP_AUTOIP_COOP 时,DHCP 在多次 Discover 未成功后可以启动 AutoIP;如果 DHCP 随后获得可路由 lease(租约),主 IPv4 地址可以切回 DHCP,同时 ACD/AutoIP 仍参与旧 Link-Local 地址的冲突处理与接收边界。S1 Stage 13 已经讲 DHCPv4,Stage 22 已经讲 link state;Stage 25 只追踪这两条线怎样通过 autoip.c、acd.c、ARP input 与 netif callback 汇合。
阅读源码前:建议提前阅读
这些资料用于规范核对与进一步阅读,正文即使脱离外链也会完整解释当前主线:
- RFC 3927:Dynamic Configuration of IPv4 Link-Local Addresses —— 重点核对候选地址范围、link-local scope、与 DHCP 的协作原则。S3
- RFC 5227:IPv4 Address Conflict Detection —— 重点核对 Probe、Announcement、Conflict、Defense 与 rate limiting。S4
- Microsoft Learn:How to use automatic TCP/IP addressing without a DHCP server —— 用工程视角观察 DHCP 不可用时
169.254/16的典型表现和同链路边界。S5
进入源码前先看完整协议流程
ACD 完成 Probe/Announcement 且确认 candidate 可以使用时,会通过 callback 上报 ACD_IP_OK;AutoIP 收到这个事件后才执行 autoip_bind()。因此 ACD_IP_OK 是“候选地址已经通过冲突检测”的实现事件,不是一种线上报文。
1 | flowchart TD |
AutoIP 自身只关心 OFF/CHECKING/BOUND,真正的 ARP 冲突检测由嵌套的 ACD 状态机推进。表中的 PROBING 表示正在发送 Probe,ANNOUNCING 表示正在发送 Announcement,ONGOING 表示地址已经投入使用后继续监视冲突;后文会沿 acd_tmr() 和 ARP receive path 展开这些迁移。
协议动作与本文源码落点如下:
| 协议阶段 | 协议动作 | lwIP 实现入口 | 关键对象/状态 | 下一步 |
|---|---|---|---|---|
| 启动 | 选择 Link-Local candidate | autoip_start() / autoip_create_addr() |
struct autoip |
acd_start() |
| 使用前冲突检测 | ARP Probe | acd_tmr() → etharp_acd_probe() |
struct acd + PROBING |
Announcement 或 conflict |
| 宣告地址 | ARP Announcement | acd_tmr() → etharp_acd_announce() |
ANNOUNCING |
ACD_IP_OK |
| 正式绑定 | 将 candidate 写入 interface | autoip_conflict_callback() → autoip_bind() |
AUTOIP_STATE_BOUND |
same-link traffic |
| 运行中冲突 | Defense / Retreat | etharp_input() → acd_arp_reply() |
ONGOING / rate-limit state |
Defense 或 restart |
| DHCP cooperation | DHCP success 替换主地址 | dhcp_bind() / acd_netif_ip_addr_changed() |
routable IPv4 + passive ACD | 保留必要 Link-Local 接收能力 |
后文仍从公开入口 autoip_start() 开始沿真实执行顺序下钻,协议图只负责导航。
1. 当前 example 默认没有直接启用 AutoIP
src/include/lwip/opt.h 默认 LWIP_AUTOIP=0。当前 contrib/examples/example_app/test_configs/opt_default.h 也显式关闭 AutoIP;而 example_app/lwipopts.h 另一套通用配置会把 LWIP_AUTOIP 绑定到 LWIP_DHCP,并允许 LWIP_DHCP_AUTOIP_COOP。S2
因此需要区分:
1 | Core feature exists |
本文主要解释当前 pinned source 的真实机制,没有在本轮重新启用 AutoIP 做 TAP 抓包实验。
2. lwIP 如何编码 RFC 3927 的 Link-Local candidate 范围
IPv4 Link-Local 使用 169.254/16,但 RFC 3927 把动态 candidate 范围限制在 169.254.1.0 到 169.254.254.255;也就是说 169.254.0.x 与 169.254.255.x 不参与本文这一动态选择过程。S3
lwIP 在 prot/autoip.h 中直接编码同一范围:S1
1 |
这一代码范围与 RFC 3927 的候选范围对应。绑定后的地址用于同一链路通信,不建立可跨路由的默认 gateway;后面的 autoip_bind() 会把这一点直接落实为 /16 netmask 和 0.0.0.0 gateway。S1S3
3. AutoIP 自己只有三态
当前 autoip_state_enum_t 很简单:S1
1 | typedef enum { |
struct autoip 保存:
1 | struct autoip |
如果只读这里,很容易误以为 probing/announcing 没有状态。实际上这些状态已经下沉到 struct acd。
4. autoip_start() 是真实公开入口
当前函数首先要求 netif 已经 administrative up,然后按需分配 struct autoip 并挂到 netif client data。S1
1 | err_t |
因此 AutoIP state 是 per-netif,而不是一个全局地址选择器。
5. 地址 seed 默认来自 MAC 地址末尾字节
继续阅读函数式宏 LWIP_AUTOIP_CREATE_SEED_ADDR(netif) 的默认定义:S1
1 |
autoip_create_addr() 再叠加 tried_llipaddr 并折回合法范围。这样当前实现倾向于同一硬件接口在无冲突时重复选择稳定的 Link-Local address,而不是每次完全随机挑一个。S1
RFC 3927 也建议不同主机应产生不同序列,并允许利用设备唯一信息帮助形成稳定选择。S3
6. autoip_start() 不会立刻写 netif->ip_addr
继续阅读 autoip_start():S1
1 | if (autoip->state == AUTOIP_STATE_OFF) { |
关键顺序是:
1 | choose candidate |
候选地址在 conflict detection 完成前不是正式 interface IPv4 address。
7. ACD 是当前真正负责 Probe/Announce 的状态机
AutoIP 通过 acd_add() 把自己的 struct acd 挂到 netif->acd_list,并注册 autoip_conflict_callback()。S1
当前 ACD 状态包括:
1 | OFF |
这说明 ACD 已经从“AutoIP 私有 helper”演进成可被 DHCP 地址冲突检测等路径复用的通用 IPv4 address conflict module。
8. acd_start():候选地址进入 PROBE_WAIT
acd_start(netif, acd, ipaddr) 保存 candidate address,清理 Probe counters,把状态设置为 ACD_STATE_PROBE_WAIT,再生成首次随机等待时间。S1
1 | acd->ipaddr = ipaddr; |
当前 random source 优先使用 LWIP_RAND();若没有,则可兼容 LWIP_AUTOIP_RAND,再退到 MAC-derived fallback。S1
9. ACD timer 每 100 ms 推进状态
timeouts.c 在 LWIP_ACD 启用时注册 acd_tmr();ACD_TMR_INTERVAL 是 100 ms。S1
acd_tmr() 遍历每个 netif 的 acd_list。在 PROBE_WAIT/PROBING 中,当 ttw 到 0 时调用:
1 | etharp_acd_probe(netif, &acd->ipaddr); |
达到 PROBE_NUM 后进入 ANNOUNCE_WAIT。S1
RFC 5227 定义了 Probe、Announcement、Conflict Detection 与 Defense 的通用 ACD 规则;当前 lwIP 的 ACD constants 与这些规则对应。S4
10. etharp_acd_probe() 如何把 RFC 5227 Probe 落到 ARP 字段
ACD Probe 的目标是询问“是否已有节点正在使用这个 candidate”,但 Probe 本身还不能把 candidate 当作本机正式 IPv4 源地址。因此 RFC 5227 使用 0.0.0.0 作为 ARP sender IP,并把 candidate 放在 target IP;当前 lwIP 正是按这一语义构造报文。S4
当前发送结果逻辑上是:
1 | Ethernet dst = ff:ff:ff:ff:ff:ff |
与 Stage 4 的普通 ARP address resolution 相比,差异点只在这里需要识别:该报文由 ACD 路径构造,用于 candidate ownership detection。
11. Probe 计数完成后,lwIP 如何进入 Announcement
acd_tmr() 完成 Probe count 后等待 ANNOUNCE_WAIT,随后进入 ANNOUNCING 并调用:S1
1 | etharp_acd_announce(netif, &acd->ipaddr); |
1 |
当 ANNOUNCE_NUM 达到后,ACD 进入 ONGOING 并通过 callback 报告 ACD_IP_OK。
12. ACD_IP_OK 才回到 AutoIP 并真正绑定地址
autoip_conflict_callback() 是 ACD → AutoIP 的关键桥接:S1
1 | static void |
这就是异步 return path:autoip_start() 早已返回,后续由 100 ms timer 和 incoming ARP events 推进 ACD,最终 callback 回到 AutoIP。
13. autoip_bind():/16 netmask、无 gateway
ACD_IP_OK 进入 autoip_bind():S1
1 | static err_t |
也就是:
1 | address = 169.254.x.x |
这与 RFC 3927 的 link-local scope 一致:该地址不是为了通过 router 到达远端网络。S3
14. AutoIP 与 ACD 两层状态机必须一起看
AutoIP 上层状态:
1 | stateDiagram-v2 |
ACD 下层状态:
1 | stateDiagram-v2 |
上层决定“这个 candidate 接下来怎么处理”;下层决定“这个 candidate 在链路上是否冲突”。
15. ARP conflict 的真实入口在 etharp_input(),不是 ACD 自己主动轮询网络
前 14 节已经跑完 autoip_start() → acd_start() → Probe/Announce → ACD_IP_OK。地址进入 ONGOING 后,ACD 仍需要持续监听链路上是否出现同地址主机;这个入口不是新的 timer,而是所有 incoming ARP 都会经过的 etharp_input()。S1
继续阅读 etharp_input() 在完成 ARP header 基本合法性检查后的调用点:S1
1 |
|
因此 ACD conflict path 的触发模型是:
1 | flowchart LR |
这也解释了为什么“别人正常发一个 ARP”就足以暴露冲突,不需要对端专门回复本机 Probe。
16. 进入 acd_arp_reply():Probe 阶段和地址已使用阶段采用不同冲突条件
etharp_input() 直接把当前 netif 与 ARP header 交给 acd_arp_reply()。函数先取出 sender IP、target IP 与本机 MAC,然后遍历该 netif 上的 ACD clients。S1
在 PROBE_WAIT / PROBING / ANNOUNCE_WAIT 阶段,candidate 还没有被确认可用。继续阅读 acd_arp_reply() 的第一组状态分支:S1S4
1 | case ACD_STATE_PROBE_WAIT: |
这里有两种冲突证据:
sender IP == candidate:说明已经有人在使用这个地址;sender IP == 0且target IP == candidate且 sender MAC 不是自己:说明另一台主机也在 Probe 同一个 candidate。
地址已经进入 ANNOUNCING / ONGOING / PASSIVE_ONGOING 后,判断条件发生变化。继续阅读 acd_arp_reply() 的下一组状态:S1S4
1 | case ACD_STATE_ANNOUNCING: |
这时地址已经在使用,只有看到别的 MAC 以该 IP 作为 sender IP才进入 ongoing conflict handler。Probe 阶段与 Ongoing 阶段因此不是同一个布尔判断。
17. 进入 acd_handle_arp_conflict():第一次 Defense、短时间再次冲突 Retreat、Passive 直接退出
acd_arp_reply() 在已使用地址的状态下调用 acd_handle_arp_conflict()。这个函数直接决定“保地址还是退让”。S1S4
1 | static void |
运行语义因此是:S4
1 | ACTIVE ONGOING 第一次冲突 |
lastconflict 又会由 acd_tmr() 每个 ACD tick 递减,因此 Defense window 本身也是状态机的一部分,而不是一条孤立 if。
18. 进入 acd_restart():Decline 先通知上层,随后决定立即重启还是进入 Rate Limit
无论 Probe 阶段直接发现冲突,还是 Ongoing 阶段在 Defense window 内再次冲突,最终都会进入 acd_restart()。继续阅读该函数:S1S4
1 | static void |
这里存在两个 callback,不应压成“冲突后重启”:
ACD_DECLINE先告诉上层“当前 candidate 已经不能继续使用”;- 若未达到
MAX_CONFLICTS,再通过ACD_RESTART_CLIENT请求上层立即重新选地址; - 达到阈值则进入
ACD_STATE_RATE_LIMIT,暂时不发第二个 callback。
Rate Limit 的完成点仍在 acd_tmr()。继续阅读 ACD_STATE_RATE_LIMIT 分支:S1
1 | case ACD_STATE_RATE_LIMIT: |
所以 Rate Limit 不是死状态;等待结束后仍通过同一个 callback bridge 返回 AutoIP client。
19. 回到 AutoIP 的 callback:ACD_RESTART_CLIENT 才真正选择下一个 169.254 candidate
autoip_start() 在前半篇已经通过 acd_add(netif, &autoip->acd, autoip_conflict_callback) 注册 ACD callback。因此第 18 节的 acd->acd_conflict_callback() 实际回到 autoip_conflict_callback()。S1
1 | static void |
当收到 ACD_RESTART_CLIENT 时继续进入 autoip_restart():S1
1 | static void |
然后执行又回到前面已经读过的 autoip_start():如果 llipaddr 已经被 ACD_DECLINE 清空,autoip_create_addr() 使用新的 tried_llipaddr 生成下一个 candidate,再调用 acd_start() 重新 Probe。需要注意当前 pinned source 的一个实现细节:AutoIP 的 ACD_DECLINE 分支会先执行一次 tried_llipaddr++,随后 ACD_RESTART_CLIENT 进入 autoip_restart() 时又执行一次 tried_llipaddr++;这是当前 lwIP callback 组合产生的实现行为,不应泛化成 RFC 3927/5227 规定的固定“地址步长”。S1
因此完整冲突重试链现在能够连续跑通:
1 | flowchart TD |
20. DHCP cooperation 的第一半:dhcp_discover() 尝试达到阈值后才启动 AutoIP
当 LWIP_DHCP_AUTOIP_COOP=1 时,AutoIP 不是和 DHCP 二选一。DHCP client 仍然先尝试获得 routable lease;只有 tries 达到配置阈值后,dhcp_discover() 才调用 autoip_start(netif)。S1
继续阅读 dhcp_discover() 的入口部分:S1
1 | static err_t |
当前默认 LWIP_DHCP_AUTOIP_COOP_TRIES 为 9。S1 这条路径解决的是:DHCP Server 暂时不可达时,设备仍可通过 169.254/16 获得 same-link connectivity,同时 DHCP client 继续自己的状态机。
21. DHCP cooperation 的第二半:DHCP 后来成功时,dhcp_bind() 会把 netif 主地址切成 routable lease
只讲“DHCP 失败后启动 AutoIP”是不完整的。DHCP 后续收到可用 lease 时会进入 dhcp_bind(),这个函数把状态切到 DHCP_STATE_BOUND,再通过 netif_set_addr() 覆盖 netif 的 IPv4 address/mask/gateway。S1
继续阅读 dhcp_bind() 的收尾:S1
1 | /* netif is now bound to DHCP leased address - set this before assigning the address |
netif_set_addr() 最终进入 netif_do_set_ipaddr()。当地址真正变化时,该函数先保存 old/new address,并把变化通知 ACD:S1
1 | static int |
这里第一次把 DHCP 与“旧 Link-Local ACD”真正接起来。
22. 进入 acd_netif_ip_addr_changed():Link-Local 被 routable IPv4 替换后转入 PASSIVE_ONGOING
netif_do_set_ipaddr() 直接调用 acd_netif_ip_addr_changed()。继续进入这个函数:S1
1 | void |
接着进入 acd_put_in_passive_mode()。如果旧 ACD 已经在 ANNOUNCING/ONGOING,状态变成 ACD_STATE_PASSIVE_ONGOING;如果还处于 probing/rate-limit,当前实现则停止它并报告 ACD_DECLINE。S1
这个状态的意义是:netif 的主 IPv4 已经换成 DHCP 地址,但旧 Link-Local 地址仍可能暂时服务已经建立的连接,因此后台继续被动监听冲突,却不再把这个地址当成需要主动保卫的主地址。
23. 为什么旧 Link-Local 连接在主地址切换后还能继续:ip4_input_accept() 显式调用 autoip_accept_packet()
仅有 passive ACD 还不能解释“旧地址的 packet 为什么 IP input 仍会接收”。答案在 IPv4 input path。S1S3
继续阅读 ip4_input_accept() 的 AutoIP 分支:S1
1 |
|
它调用的 autoip_accept_packet() 很直接:S1
1 | u8_t |
因此当前实现中的“地址切换后仍接受旧 LLA packet”不是泛泛的兼容性描述,而是 ip4_input_accept() 的显式输入条件。它和第 22 节的 passive ACD 合在一起,构成 DHCP cooperation 的完整后半段。
24. Link Up/Down 的触发入口也在 netif.c,不是 AutoIP 自己检测 PHY
Stage 22 已经建立 netif_set_link_up/down() 与 PHY/Driver 的关系。本篇只继续追 AutoIP/ACD 的桥接调用点。
netif_set_link_up() 在 link flag 从 down 变成 up 后直接通知 DHCP 和 AutoIP:S1
1 |
|
netif_set_link_down() 则通知 AutoIP 与所有 ACD clients:S1
1 |
|
因此调用链是:
1 | PHY/Driver 检测 link |
AutoIP 并不自己轮询 PHY。
25. autoip_network_changed_link_up() 为什么优先重新 Probe 上次使用的地址
进入 autoip_network_changed_link_up():S1S3
1 | void |
非 DHCP cooperation 模式下,link 恢复后不立刻生成一个新地址,而是把状态退回 CHECKING,对保存在 autoip->llipaddr 的旧 candidate 再做 ACD。这与 RFC 3927 倾向尽量复用同一 Link-Local 地址的目标一致。S3
Link Down 的 cooperation 分支则相反:S1
1 | void |
因为下一次 link up 后应先让 DHCP 重新尝试,必要时再由 dhcp_discover() 按阈值启动 AutoIP。
26. AutoIP、DHCP、mDNS 各解决不同层的问题
源码主链闭环后再统一区分这三个经常一起出现的“零配置”组件:
1 | DHCP |
autoip_bind() 明确把 netmask 设为 /16、gateway 设为 0.0.0.0;RFC 3927 也限定 Link-Local scope。S1S3 因此 AutoIP 不是“没有 DHCP 时自动获得 Internet access”,也不是 DHCP server 的替代品。
同样,有 AutoIP 不会自动发布 device.local;那是 Stage 24 mDNS/DNS-SD 的职责。IPv6 link-local/SLAAC 同样也可以承载 mDNS,所以二者只是常见组合,不是强绑定。
27. Stage 25 的完整源码心智模型
1 | flowchart TD |
Stage 25 最终建立的是两个嵌套 lifecycle:AutoIP 决定“选择哪个 Link-Local candidate、何时把它绑定到 netif”,ACD 决定“这个 candidate 在链路上是否安全、发生冲突时 defend、retreat 还是 rate-limit”。DHCP cooperation 和 Link change 又通过明确的 callback/netif bridge 驱动这两个状态机,而不是旁路修改地址。
下一篇进入 SNMP/MIB2:从网络配置生命周期转向设备运行状态的管理视角,观察 lwIP 怎样把 netif、IP、ICMP、TCP、UDP 统计映射成 OID tree,并通过 UDP 161 响应管理端查询。
资料来源
[S1] lwIP AutoIP、ACD、DHCP cooperation 与 netif 源码
- 类型:目标版本上游源码
- 版本:commit
d08f4773edd0182b7910fc8f046eed82ffcd67c9 - 定位:
src/core/ipv4/autoip.c:autoip_start()、autoip_create_addr()、autoip_conflict_callback()、autoip_bind()、link-change handlers;src/core/ipv4/acd.c:acd_start()、acd_tmr()、acd_arp_reply()、acd_handle_arp_conflict();src/core/ipv4/dhcp.c:cooperation bridge;src/core/netif.c、src/core/ipv4/ip4.c - URL/文档:lwIP upstream commit
- 使用位置:“地址选择”“AutoIP/ACD 双状态机”“Probe/Announce”“Conflict/Defense”“DHCP cooperation”“link change”“old LLA persistence”
- 支撑内容:证明当前实现真实调用链、callback bridge、状态迁移、timer 与 routing/input integration
[S2] lwIP AutoIP 配置
- 类型:目标版本上游配置
- 版本:commit
d08f4773edd0182b7910fc8f046eed82ffcd67c9 - 定位:
src/include/lwip/opt.h;contrib/examples/example_app/lwipopts.h;contrib/examples/example_app/test_configs/opt_default.h - URL/文档:lwIP example_app
- 使用位置:“当前 profile 默认关闭”“DHCP/AutoIP cooperation compile options”
- 支撑内容:区分 Core 能力、通用 example 配置与当前 test profile
[S3] RFC 3927:Dynamic Configuration of IPv4 Link-Local Addresses
- 类型:IETF 标准规范
- 版本:RFC 3927,2005
- URL/文档:RFC 3927
- 使用位置:“169.254/16”“动态选择范围”“link-local scope”“稳定地址倾向”“routing boundary”
- 支撑内容:给出 IPv4 Link-Local 地址选择、使用范围与同链路通信规则
[S4] RFC 5227:IPv4 Address Conflict Detection
- 类型:IETF 标准规范
- 版本:RFC 5227,2008
- URL/文档:RFC 5227
- 使用位置:“ARP Probe/Announcement”“PROBE_NUM/ANNOUNCE_NUM”“Conflict Defense”“RATE_LIMIT_INTERVAL”“DEFEND_INTERVAL”
- 支撑内容:提供 lwIP ACD 状态机和 timing constants 对应的规范语义
[S5] Microsoft Learn:Automatic TCP/IP addressing without DHCP
- 类型:厂商官方工程说明
- 版本:访问日期 2026-10-03
- URL/文档:How to use automatic TCP/IP addressing without a DHCP server
- 使用位置:“阅读源码前:建议提前阅读”
- 支撑内容:从工程现象解释 DHCP 不可用时的 APIPA/
169.254/16、同一 LAN 通信范围与不能跨子网的边界;规范细节仍以 RFC 3927/5227 为准









