DHCP v4 简介

前言

最近在公司敲 DHCP server 的相关代码。或者说,是 ai 敲代码,我只是 review 。现在的 ai 非常厉害。DHCP server 这种纯用户态场景开发,对 ai 没有难度。

为了知道,要写这样的代码(目标),以及能看懂 ai 写的代码,我得找 AI 带我读下 DHCP RFC 相关的文档(让 AI 把东西嚼烂了,为我嘴里。啊,恶心~)

网上的其他参考链接:

RFC 2131 – Dynamic Host Configuration Protocol

DHCP 是什么

DHCP(Dynamic Host Configuration Protocol,动态主机配置协议)是一种运行在 UDP/IP 网络上的 配置下发协议。它的职责只有一句话:让一台主机接入网络时,自动获得一切运行所需的 IP 层配置参数——包括 IP 地址、子网掩码、默认网关、DNS 服务器等——而无需人工逐台设置。

DHCP 采用 客户端-服务器 模型。网络中的 DHCP 服务器维护着一个地址池和配置参数库;客户端启动时广播请求,服务器从中分配合适的网络地址和参数并返回给客户端。一个 DHCP 客户端从开机到完成配置,只需要在网络上完成一次简短的握手交互。

按照地址分配方式,DHCP 提供了三种机制:

  • 自动分配(Automatic allocation):服务器为客户端分配一个永久的 IP 地址,一旦分配就不再变更。
  • 动态分配(Dynamic allocation):服务器将 IP 地址以租约(lease)的形式有限期地分配给客户端。租约到期后地址可回收复用——这是 DHCP 最核心的能力,也是它区别于此前 BOOTP 等协议的关键所在。
  • 手动分配(Manual allocation):由管理员预先指定客户端的 IP 地址,DHCP 仅负责将该地址传达给客户端。

从技术渊源上看,DHCP 是 BOOTP 协议的扩展,复用了 BOOTP 的消息格式、UDP 传输和中继代理机制。但它在两方面做出了根本性改进:(1) 引入有限租约,使网络地址可以复用,解决了 IP 地址紧缺的问题;(2) 提供了一套完整的参数获取机制,使客户端在单次交互中即可拿到所需的全部配置信息。

简而言之,DHCP 解决的核心问题是:让一台主机”即插即用”地接入 IP 网络

DHCP 的报文结构与通信流程

首先是定义报文结构,即网络上数据包要承载的内容。这保证通信的双方都认识彼此,都知道对方在说什么。然后是通信流程,双方按照一定的步骤协商。几乎所有的网络协议都是这样的。上层的应用层通信也是如此。网络协议主要是用来解决网络通不通的问题,比如 DHCP, ARP, TCP等。

报文结构

DHCP 报文直接复用了 BOOTP 的报文格式。整个报文是一个定长头部 + 可变长选项区的结构:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     op (1)    |   htype (1)  |   hlen (1)   |   hops (1)    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            xid (4)                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           secs (2)            |           flags (2)           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           ciaddr (4)                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           yiaddr (4)                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           siaddr (4)                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           giaddr (4)                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                        chaddr (16)                            |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                        sname (64)                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                        file (128)                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                        options (variable)                     |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

几个关键字段的含义:

  • op:消息方向。1 = 客户端 → 服务器(BOOTREQUEST),2 = 服务器 → 客户端(BOOTREPLY)。
  • htype / hlen:硬件地址类型和长度。以太网下 htype=1,hlen=6。
  • hops:跳数。客户端初始为 0,每经过一个中继代理加 1。
  • xid:事务 ID。客户端随机生成,用于将请求与响应一一匹配——类似 TCP 的序列号,是整场对话的”对暗号”。
  • secs:客户端从开始请求到现在经过的秒数。
  • flags:16 位,只有最高位有意义——广播标志位(BROADCAST flag)。当客户端尚未获得 IP 地址、无法接收单播时,将这个位置 1,要求服务器以广播方式回复。其余 15 位保留,必须为 0。
  • ciaddr:客户端当前 IP 地址。仅在客户端处于 BOUND / RENEWING / REBINDING 状态且有 IP 时填写。
  • yiaddr:”你的 IP 地址”(your IP address)。服务器在 OFFER/ACK 中将要分配给客户端的地址填在这里。
  • siaddr:下一阶段引导服务器的 IP 地址(比如 TFTP 服务器),服务器在 OFFER/ACK 中填写。
  • giaddr:中继代理(relay agent)的 IP 地址。如果请求经过了中继代理,此字段记录中继代理所在子网的接口地址,服务器据此判断客户端属于哪个子网。
  • chaddr:客户端硬件地址(MAC 地址)。16 字节长,以太网下只用前 6 字节。
  • sname / file:服务器主机名和启动文件名。这两个字段继承自 BOOTP 的引导功能,在 DHCP 中一般不使用,但可以作为 options 字段的”溢出区”(option overload)。
  • options:可变长的选项区,这是 DHCP 真正的配置信息承载区。前 4 字节必须是 magic cookie 99.130.83.99,后面跟着一个个 TLV(Type-Length-Value)格式的选项。最关键的选项是 DHCP Message Type(option 53),取值 1~8,标识这条报文属于哪种 DHCP 消息。

DHCP 一共定义了 8 种消息类型,以 DHCP Message Type 选项的值区分:

消息类型方向含义
1DHCPDISCOVER客户端 → 服务器“这里有 DHCP 服务器吗?” 客户端广播寻找可用服务器
2DHCPOFFER服务器 → 客户端“你可以用这个地址。” 服务器响应 DISCOVER,提供一个 IP
3DHCPREQUEST客户端 → 服务器“我就要这个地址了。” 客户端选定服务器并请求绑定;也用于续租
4DHCPACK服务器 → 客户端“这个地址归你了。” 服务器确认绑定,同时携带租约和配置参数
5DHCPNAK服务器 → 客户端“这个地址不能给你。” 服务器拒绝请求,客户端需从头开始
6DHCPDECLINE客户端 → 服务器“这个地址已经被占用了。” 客户端用 ARP 检测到冲突后通知服务器
7DHCPRELEASE客户端 → 服务器“我不要这个地址了。” 客户端主动归还地址,放弃剩余租约
8DHCPINFORM客户端 → 服务器“我已经有 IP 了,给点别的配置就行。” 客户端仅请求 DNS 等参数

通信流程

DHCP 的通信流程可归纳为 五个典型场景

场景一:初次获取 IP 地址——四次握手

这是最常见的情况:一台新主机接入网络,没有任何缓存状态。

      客户端 (Client)                       服务器 (Server)
          |                                      |
          |  (1) DHCPDISCOVER  (广播)            |
          |------------------------------------->|
          |                                      |
          |  (2) DHCPOFFER      (广播或单播)      |
          |<-------------------------------------|
          |                                      |
          |  (3) DHCPREQUEST     (广播)           |
          |------------------------------------->|
          |                                      |
          |  (4) DHCPACK         (广播或单播)      |
          |<-------------------------------------|
          |                                      |
          |  (可选) ARP check (免费ARP)           |
          |                                      |

  1. DHCPDISCOVER:客户端在本地子网广播(目的 IP 255.255.255.255,源 IP 0.0.0.0)。消息中可选地携带期望的 IP 地址、期望的租约时长、需要的参数列表。xid 由客户端随机生成。



  2. DHCPOFFER:每台收到 DISCOVER 的服务器,如果能够满足客户端请求,就从地址池中选择一个可用 IP,通过 yiaddr 字段提供给客户端。OFFER 中可以附带子网掩码、路由器、DNS、租约时间等参数。多台服务器可能同时回复,客户端后续自己选择。



  3. DHCPREQUEST:客户端从收到的 OFFER 中选择一个(通常选最先到达的),然后再次广播 DHCPREQUEST。注意这里是广播而非单播——目的是让那些没被选中的服务器也知道”这个地址已经被别的服务器承诺了,你们不必再留”。REQUEST 中通过 server identifier 选项指明选中了哪台服务器,requested IP address 选项指明想要的地址。



  4. DHCPACK:被选中的服务器收到 REQUEST 后,正式将地址与客户端绑定(commit binding),回复 ACK。ACK 中携带最终的租约时间和全套配置参数。客户端收到 ACK 后,通常还会发送一次免费 ARP 来探测地址是否真的空闲;如果发现冲突,发送 DHCPDECLINE 并回到初始状态重来。


为什么客户端不直接使用收到的第一个 OFFER,还要再多一轮 REQUEST/ACK?
因为同一个网络上可能有多台 DHCP 服务器。客户端广播 DISCOVER 后,每台服务器都各自返回 OFFER 并暂时预留了地址。客户端选择其中一台,广播 REQUEST——选中的服务器收到后正式绑定,未选中的服务器收到后释放预留。如果只有 DISCOVER → OFFER 两步,就无法实现这个”多服务器竞选”的互斥机制。

为什么上图里 OFFER 和 ACK 是”广播或单播”?
这取决于客户端是否设置了 flags 字段的广播标志位。客户端在 DISCOVER/REQUEST 中,如果还没有 IP、无法接收单播,就把广播位置 1,服务器就以广播回复;如果客户端实现能处理单播(比如提前把 yiaddr 临时配到网卡上),就不用设广播位,服务器直接单播到 yiaddr。简单说:客户端告诉服务器”我还不能收单播”,服务器就广播;否则单播。

场景二:客户端还记得上次的地址——快速恢复

如果客户端之前拿到过地址并且还记得(比如重启后),它可以跳过 DISCOVER 阶段,直接广播 DHCPREQUEST,其中携带 requested IP address 选项,问服务器”我上次用的这个地址还能继续用吗”。

  • 服务器同意 → DHCPACK,客户端直接进入 BOUND 状态。
  • 服务器不同意(比如地址已经给了别人,或者客户端换到了另一个子网)→ DHCPNAK,客户端回到 INIT 状态重新走完整流程。

场景三:续租——悄悄话式的单播

每个 IP 地址都有租约期限。客户端在租约的 T1 时刻(默认为租约的一半)进入 RENEWING 状态,开始向服务器单播 DHCPREQUEST 续租。因为此时客户端已经有 IP 了,所以不需要广播。

如果服务器回应 DHCPACK,租约被延长,一切照旧。如果服务器不回应(比如挂掉了),客户端持续重试直到 T2 时刻(默认为租约的 87.5%),此时进入 REBINDING 状态,转为广播 DHCPREQUEST——因为原来的服务器可能已经挂了,客户端需要向网络上任何可用的服务器求助。

如果直到租约到期都没有收到任何 ACK,客户端必须停止使用该地址,回到 INIT 状态重新开始。

场景四:主动释放

客户端不再需要 IP 地址时(比如关机),可以发送 DHCPRELEASE 主动归还。这是单播消息,直接发给服务器。注意这是个”尽力而为”的消息——客户端不等待确认,服务器也不需要回复。

场景五:已有 IP,只想要配置参数

如果客户端已经通过其他途径获得了 IP 地址(比如手动配置),但仍然需要子网掩码、路由器、DNS 等参数,可以发送 DHCPINFORM。服务器收到后回复 DHCPACK,其中不包含 IP 地址分配和租约信息,只携带配置参数。

客户端状态机

上述过程,如果从客户端内部视角来看,就是下面这张状态转换图。RFC 2131 的 Figure 5 给了一个很清晰的图示,这里用文字描述:

                          +-----------+
                          |   INIT    | <------------------------------------+
                          +-----+-----+                                      |
                                | 发送 DHCPDISCOVER                           |
                                v                                            |
                          +-----------+                                      |
                          | SELECTING |  等待 DHCPOFFER                       |
                          +-----+-----+                                      |
                                | 收集 OFFER, 选定服务器, 发送 DHCPREQUEST      |
                                v                                            |
                          +------------+                                     |
                          | REQUESTING |  等待 DHCPACK                       |
                          +-----+------+                                    |
                                | DHCPACK 到达                               |
                                v                                            |
                          +----------+                                       |
                    +---->|  BOUND   |                                       |
                    |     +----+-----+                                       |
                    |          | T1 (0.5 * 租约) 到期, 发送单播 DHCPREQUEST      |
                    |          v                                            |
                    |     +----------+ 收到 DHCPACK → 回到 BOUND              |
                    |     | RENEWING |                                       |
                    |     +-----+----+                                      |
                    |          | T2 (0.875 * 租约) 到期, 发送广播 DHCPREQUEST   |
                    |          v                                            |
                    |     +----------+ 收到 DHCPACK → 回到 BOUND              |
                    |     |REBINDING |                                      |
                    |     +-----+----+                                      |
                    |           | 租约到期  /  收到 DHCPNAK                    |
                    |           +--------------------------------------------+
                    |           发送 DHCPRELEASE → 回到 INIT
                    +-------------------------------------------------+

几个关键时间点:

  • T1(默认租约的 50%):从 BOUND 进入 RENEWING,单播续租。
  • T2(默认租约的 87.5%):从 RENEWING 进入 REBINDING,广播求救。
  • 租约到期:停止使用地址,回到 INIT。

重传机制

DHCP 的可靠性完全基于 UDP,没有 TCP 的确认重传机制,所以自己在应用层实现了一套随机化指数退避的重传策略:

  • 客户端发送消息后,如果在超时时间内未收到响应,就重发。
  • 初始超时时间为 4 秒,之后每次超时翻倍(8s → 16s → 32s),上限 64 秒
  • 每次重传前加入一个小的随机抖动(-1s ~ +1s),避免大量客户端同时启动时形成”报文风暴”。
  • 特殊处理:客户端在 INIT 状态下发送 DHCPDISCOVER 时,第一次超时可以缩短为 0~4 秒之间的随机值,加快初始接入速度。

服务器如何选择地址

服务器在收到 DHCPDISCOVER 后,按以下优先级选择分配给客户端的地址:

  1. 当前绑定:该客户端(通过 client identifier 或 chaddr 识别)当前已经绑定的地址,直接续用。
  2. 历史绑定:该客户端之前用过、目前仍空闲的地址。
  3. 客户端建议的地址:DHCPDISCOVER 中 requested IP address 选项填写的地址,如果可用。
  4. 池中取新地址:以上都不满足时,从空闲地址池中分配一个新的。

选好地址后,服务器在回复 OFFER/ACK 时,会按照固定的优先级填充配置参数:显式配置的默认值 > Host Requirements RFC 定义的默认值 > 该客户端历史 binding 中的参数 > 为该客户端单独配置的参数 > 为客户端类别配置的参数 > 为该子网配置的参数


现实世界

我之前在一家,和我年纪一样大的软件安全公司,打过工。上班的电脑,都要使用静态IP。这种管理方式不好。

应该使用 DHCP MAC 白名单的方式,这样保证了只有允许的设备可以联网(但是,桥接的虚拟网卡,也要去申请,有点麻烦)。或者,采用认证的方式,保证了只有允许的人(账户)可以联网。

RFC 2132 – DHCP Options and BOOTP Vendor Extensions

如果说 RFC 2131 定义了 DHCP 的骨架(报文格式、通信流程、状态机),那 RFC 2132 定义的就是 DHCP 的血肉——DHCP 到底能下发哪些配置参数,每项参数怎么编码

编码格式:TLV

所有选项放在 options 可变长区,采用 TLV(Type-Length-Value) 格式:1 字节 code + 1 字节 length + length 字节 value。前 4 字节必须是 magic cookie 99.130.83.99。两个特殊 code:0(Pad) 用于对齐填充,255(End) 标识选项区结束。所有多字节数值一律大端。

关键选项

RFC 2132 定义了 60 多个选项,下面是理解 DHCP 协议行为最核心的几个:

Code名称作用
1Subnet Mask子网掩码
3Router默认网关
6DNS ServerDNS 服务器列表
15Domain NameDNS 域名后缀
50Requested IP Address客户端向服务器暗示/确认想要的 IP
51IP Address Lease Time租约时长(秒),客户端可以请求,服务器返回实际值
53DHCP Message Type消息类型(1~8),每条 DHCP 报文必带
54Server Identifier服务器 IP。SELECTING 时必带(选谁),RENEWING 时不带(谁都行)
55Parameter Request List客户端列出想要哪些选项(1 字节一个编号),减少不必要传输
58 / 59Renewal (T1) / Rebinding (T2) Time控制租约生命周期的两个计时器
61Client-identifier客户端唯一标识,优先于 chaddr 用于识别客户端

两个值得了解的机制

Option Overload(code 52):当选项太多装不下时,sname(64B)和 file(128B)字段可以被征用来装选项。原来放在这两个字段的内容改用 code 66(TFTP Server Name)和 code 67(Bootfile Name)传递。

Vendor-Specific(code 60 + code 43):客户端通过 code 60 自报厂商类型,服务器通过 code 43 返回厂商自定义数据(可嵌套子 TLV,无需 magic cookie)。


DHCP Relay

为什么需要中继

DHCP 客户端启动时靠广播发现服务器。但广播包止于路由器——不会跨子网转发。这意味着如果每个子网都要部署一台 DHCP 服务器,运维成本就太高了。DHCP Relay(BOOTP 中继代理)解决了这个问题:在每个子网放一个轻量的中继代理,它把客户端的广播请求转成单播发给远端的 DHCP 服务器。

工作流程

  客户端 (无 IP)          中继代理              DHCP 服务器
  192.168.1.0/24      两个接口都有 IP        10.0.0.0/24
       |                   |                      |
       | DHCPDISCOVER      |                      |
       | (广播)            |                      |
       |------------------>|                      |
       |                   | DHCPDISCOVER         |
       |                   | (单播, giaddr=       |
       |                   |  192.168.1.1,        |
       |                   |  hops=1)             |
       |                   |--------------------->|
       |                   |                      |
       |                   |         DHCPOFFER    |
       |                   | (单播到 giaddr,       |
       |                   |  yiaddr=192.168.1.100)|
       |                   |<---------------------|
       |    DHCPOFFER      |                      |
       |<------------------|                      |
       |                   |                      |
       |    DHCPREQUEST    |                      |
       | (广播)            |                      |
       |------------------>|                      |
       |                   |      DHCPREQUEST     |
       |                   |--------------------->|
       |                   |                      |
       |                   |        DHCPACK       |
       |                   |<---------------------|
       |      DHCPACK      |                      |
       |<------------------|                      |

关键机制:

  • 中继代理收到客户端广播后,把 giaddr 设为自己在客户端子网上的接口 IP,hops 加 1,然后单播发给配置好的 DHCP 服务器。
  • 服务器根据 giaddr 判断客户端属于哪个子网,从对应地址池中分配 IP。回复时也是单播到 giaddr,由中继代理转发给客户端(广播或单播,取决于广播标志位)。

与报文字段的关系

  • giaddr:中继代理的入接口 IP。服务器靠它选地址池,回复也靠它回到正确的中继。
  • hops:跳数。默认最多 16 跳(可配置),防止环路。
  • chaddr / client-identifier:中继代理不碰这些,服务器直接用来识别客户端。

最后

在没有阅读过 DHCP server 开源实现的情况下,从头实现一个完善的 DHCP server ,即使有 AI 也不太可能。主要原因是 (1) DHCP 的 option 很多,不可能把这些 option 都搞搞明白。(2) DHCP 没有一个标准的功能和性能测试工具。没有办法,很好的验证,我们写的代码,是否完善。

但是,实现一个简单凑活用的 DHCPv4 server,没啥难度。

产品需要在被使用的过程中,螺旋迭代完善。

这种纯用户态的网络应用,不要用 C/C++ 开发。一个字节一个字节的填充构造数据包,太容易翻车了。应用层开发,直接调用库,最好。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇