bigywater.tech
网络协议 · 21

DHCP 四步:DORA、租约与续约

你的设备一连上 Wi-Fi 就有了地址、网关和 DNS——是谁给的、怎么给的?租约到期之前设备做了什么?这篇解码本机的真实租约,向网关的 DHCP 服务器发一次真实的 DISCOVER 收下 OFFER,并用 30 分钟的被动抓包抓下本机自己的续租。

114.2 ms
广播 DHCPDISCOVER 到收到网关的 OFFER 之间的时间
801.0 秒
被动抓包里本机两次续租 REQUEST 的间隔(ACK 里给的 T1 是 817 秒)
279 字节
DHCPDISCOVER 的大小:236 字节固定头 + 4 字节 magic cookie + 39 字节选项

DHCP 报文

实测 · 本机执行输出
  op = BOOTREPLY
  htype = 1
  flags = 0x0
  hlen = 6
  hops = 0
  xid = 0x4be3ab17
  secs = 0
  ciaddr = <本机局域网IP>
  yiaddr = <本机局域网IP>
  siaddr = <本机局域网IP>
  giaddr = 0.0.0.0
  chaddr = 7e:aa:6f:xx:xx:xx
  sname = 
  file = 
  options:
  Options count is 11
  dhcp_message_type (uint8): ACK 0x5
  server_identifier (ip): <本机局域网IP>
  lease_time (uint32): 0x708
  renewal_t1_time_value (uint32): 0x331
  rebinding_t2_time_value (uint32): 0x5d4
  subnet_mask (ip): 255.255.252.0
  broadcast_address (ip): <本机局域网IP>
  router (ip_mult): {<本机局域网IP>}
  domain_name (string): lan
  domain_name_server (ip_mult): {114.114.114.114, 223.5.5.5}
  end (none): 
  租约时间 1800 秒(30 分钟);T1(开始续租)= 817 秒;T2(开始向任何服务器广播请求)= 1492 秒
  T1 / 租约 = 0.454,T2 / 租约 = 0.829(规范建议的默认值是 0.5 和 0.875)
  这个应答的 ciaddr 不是 0.0.0.0:是 —— ciaddr 非 0 说明它回应的是一次续租(客户端已有地址时发的 REQUEST),而不是首次获取

DHCP 沿用了 BOOTP 的 236 字节固定头(op、xid、ciaddr、yiaddr、siaddr、giaddr、chaddr、sname、file),后面是 4 字节 magic cookie 和 TLV 格式的选项。租约、掩码、网关、DNS 等都是选项。

报文格式

实测 · 本机执行输出
  DISCOVER 共 279 字节:固定头 236 + magic cookie 4 + 选项 39;op=1(BOOTREQUEST),xid=0x09562ce3,flags=0x8000(广播标志:请服务器用广播回应),chaddr=02:00:5e:xx:xx:xx(本地管理位 MAC)
  选项:53=报文类型;61=客户端标识;12=主机名;55=请求的参数列表;57=最大报文长度
  3 秒内在 en0 上抓到 2 个 DHCP 包,其中 xid 与我的 DISCOVER 相同的 2 个(其余是别的设备的 DHCP 流量,只统计个数)
    +    0.1 ms  DISCOVER  UDP 52440 → 67,IP 源 本机 en0 地址 → 目的 255.255.255.255(受限广播),IP 总长 307,DHCP 载荷 279 字节
    +  114.2 ms  OFFER  UDP 67 → 68,IP 源 默认网关 → 目的 255.255.255.255(受限广播),IP 总长 329,DHCP 载荷 301 字节
      OFFER 内容:yiaddr = 同网段的其它地址(给我的地址),siaddr = 默认网关,chaddr 是我伪造的:True,flags=0x8000
      选项(顺序 [53, 54, 51, 58, 59, 1, 28, 3, 15, 6]):租约 1800 秒,T1=900,T2=1575,子网掩码 255.255.252.0,路由器 1 个,DNS 2 个,服务器标识 默认网关
  没有发 REQUEST,所以没有完成 DORA 的后两步,也没有占用这个地址;下一节用 scapy 交叉解码

固定头加选项:选项是类型、长度、值的重复。自己的解码器与 scapy 对同一个 OFFER 解出同样的 10 个选项。

DORA

实测 · 本机执行输出
  自己解码出的选项编号:[53, 54, 51, 58, 59, 1, 28, 3, 15, 6]
  scapy 解码出的选项名:['message-type', 'server_id', 'lease_time', 'renewal_time', 'rebinding_time', 'subnet_mask', 'broadcast_address', 'router', 'domain', 'name_server']
  选项个数:自己 10 / scapy 10;scapy 看到的 xid 0x09562ce3 与自己的 0x09562ce3一致;op=2,chaddr 前 6 字节一致:True
  DHCP 包总长 301 字节 = 固定头 236 + cookie 4 + 选项 61;BOOTP 的 sname(64 字节)和 file(128 字节)两个字段在 DHCP 里基本空着:非 0 字节数 sname 0、file 0

客户端广播 DISCOVER,服务器回 OFFER(这里是 114.2 ms),客户端再发 REQUEST,服务器回 ACK。实验只做了前两步,没有发 REQUEST,所以没有占用地址。

租约与续租

实测 · 本机执行输出
  按 ipconfig 给出的值:租约 1800 秒:在 T1=817 秒时(约 13.6 分钟)向原服务器单播 REQUEST 续租;若没回应,到 T2=1492 秒(24.9 分钟)改为广播 REQUEST;到 1800 秒(30 分钟)租约过期就要放弃这个地址、重新从 DISCOVER 开始
  被动抓包(从 09:55:46 起约 30 分钟):属于本机 MAC 的 DHCP 报文 4 个,其它设备的 157 个(只统计个数)
    起点后   309.5 s  REQUEST  xid=0x4be3ab15  IP 本机 en0 地址 → 默认网关  以太网单播  ciaddr=本机地址
    起点后   309.5 s  ACK      xid=0x4be3ab15  IP 默认网关 → 本机 en0 地址  以太网单播  ciaddr=本机地址
    起点后  1110.5 s  REQUEST  xid=0x4be3ab16  IP 本机 en0 地址 → 默认网关  以太网单播  ciaddr=本机地址
    起点后  1110.5 s  ACK      xid=0x4be3ab16  IP 默认网关 → 本机 en0 地址  以太网单播  ciaddr=本机地址
  两次 REQUEST 之间的间隔:[801.0] 秒(T1 = 817 秒)

本机租约 1800 秒。到 T1(ACK 里给 817 秒,实测续租间隔 801 秒)时单播 REQUEST 给原服务器,ACK 回来租约重置;没回应就在 T2 广播,到期则重新 DISCOVER。

配置生效

实测 · 本机执行输出
  子网掩码:DHCP 255.255.252.0 / ifconfig 255.255.252.0 → 一致(前缀长度 /22)
  广播地址:DHCP 与 ifconfig 一致
  路由器:DHCP 给了 1 个,默认路由的网关就是其中第一个
  DNS:DHCP 给了 2 个(公共 DNS、公共 DNS);scutil --dns 里出现的 nameserver 共 2 个,其中 DHCP 给的 2 个都在里面
  地址、掩码、网关、DNS、域名这些都不用手配,是 DHCP 一次交给客户端的

掩码、网关、DNS 都和 ifconfig、route、scutil 里的一致,说明系统直接采用 DHCP 给的配置。

分步演示

DHCP 四步
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。第二节向局域网广播了一次 DHCPDISCOVER(用伪造的本地管理 MAC,没有发 REQUEST,服务器可能短暂保留了一个 offered 地址);第四节来自 30 分钟的被动抓包,其中别的设备的 DHCP 报文只统计个数;默认网关、服务器和本机地址做了遮盖
    • 没有验证:REQUEST / ACK 的第一次获取(完整的 DORA);T2 广播和租约过期后的重新开始;DHCP 中继(giaddr);服务器是否限制客户端的 MAC;OFFER 里 T1 = 900 而续租 ACK 里 T1 = 817 的差异原因;DHCP 与 ARP 冲突检测的配合
    京ICP备2026067502号-1京公网安备11010502063487号