DHCP 服务器编写

DHCP 协议(Dynamic Host Configuration Protocol)是一个网络层协议,主要用于给接入设备分配 IP,是目前所有路由设备中重要的协议之一。

本文主要记录博主以前编写 DHCP 时查询的所有资料和记录编写中遇到的坑,还有编写之后对于 DHCP 整个流程的理解,希望对后来者有些帮助。

注意:本文不会写相关代码,只讲述编写思路和算法,代码不是重点。

DHCP 的工作流程

1.客户端以广播的方式发出 DHCP Discover 报文。
2.所有服务器接受这一消息,返回 DHCP Offer 报文,报文中携带 DHCP 服务器标示和预分配给客户端的 IP 地址。
3.客户端根据需要处理这些报文,一般只能处理一个报文,然后发出一个 广播 DHCP Request 报文,报文中携带选中的 DHCP 服务器的服务器标示和需要申请的 IP 地址。
4.DHCP 服务器收到报文之后,判断 Option 中携带的标示是不是自己,如果不是,服务器不作处理,只清除相应的 IP 分配信息(IP 分配在 Offer 阶段发生);如果是,服务器回应一个 DHCP ACK 消息,并且在字段中补全 IP、DNS、租期等信息。
5.客户端收到 ACK 之后,检查 IP 是否可用,如果可用则设置网络配置,启动续租流程,如果不可用向服务器发出 DHCP Decline 报文(广播),通知所有 DHCP 服务器禁用该地址,然后重新开始新的地址申请。
6.客户端成功获取 IP 之后可以随时发送 DHCP Release 报文告诉服务器释放自己的 IP,Server 收到报文后会回收相应的 IP 地址,并等待重新分配。

DHCP 服务器并不参与后续的数据传输工作,只是为接入网络的设备提供可用的 IP 这样一个服务,换句话说,DHCP 服务器只记录网络中设备的网络配置,在设备配置好相应的网络配置之后,DHCP 服务器与其交互基本没有(除了续租和释放)。现在的家用局域网环境中一般路由器既作为网络传输设备又作为 IP 分配设备,一定程度上会造成误导,其实在一个局域网中,通信设备和 IP 分配设备完全可以分开。

客户端续租流程一般发生在租期的 50% 和 87.5%,某些设备也会自定义续租周期,并不一定完全按照规范来,第一次续租叫做 Renew,是以单播形式发送 DHCP Request 报文给服务器,如果成功收到服务器的 ACK 报文则更新租期时间,继续使用该 IP,如果没用,等到 87.5% 第二次续租的时候会广播发送给全网段的 DHCP 服务器,这次续租叫做 Rebind,如果收到 ACK 消息,更新服务器标示和租期继续使用该 IP,否则等到租期结束发送 DHCP Release 释放 IP 并且重新发起 IP 申请流程。

客户端在 Rebind 过程中不携带 ServerID,接受所有服务器发过来的 NAK 和 ACK 信息,一般情况下 Rebind 不会成功,因为现阶段网络中大部分使用的是 DHCP 之间相互独立的设计,部分网络分配发生比较频繁的局域网中可能会同时存在多个设备进行压力分流接受 Rebind,一般来说 DHCP 服务器收到 Rebind 会因为找不到对应的 IP 记录直接回复 NAK 报文给客户端让客户端重新申请 IP,压力分流网络中会接受 Rebind 返回 ACK,并记录设备信息。

在 DHCP 协商阶段任意时刻,如果 DHCP 客户端请求存在问题,DHCP 服务器就会发送 NAK 消息拒绝此次请求,客户端收到 NAK 之后会判断是否来自目标服务器,如果是重新发起申请流程,如果不是忽略该消息继续申请 IP。

当客户端轮询寻找 DHCP 服务器时(反复发送 DHCP Discover 没有回应的情况),每次重试之后,时间间隔会在原先基础上 x2,当达到一个轮询周期时间之后重制间隔。比如第一次 DHCP Discover 没有回应,到第二次的时候间隔时间为 2s,如果第二次还没有回应到第三次的时候间隔为 4s,第三次到第四次间隔为 8s 依次类推。

不过 DHCP 阶段所有有要求的间隔(轮询间隔、续租间隔)都不是十分准确的时间点,一般会有 ±1s 以内的随机时间叠加,这么做是为了防止服务器瞬时压力过大而给服务器带来不必要的麻烦而设计。正因为此,DHCP 在一个轮询周期内 DHCP Discover 报文数量不固定。

DHCP 协商使用 UDP 协议,DHCP 服务预留的两个端口为 67、68,其中 68 为客户端发送的目标端口,67 为服务器的回应的目标端口。

广播

广播是一种特殊的 UDP 报文,所有子网设备都可以收到 UDP 广播报文,不需要网络分发设备支持。

发送广播与普通的 UDP 报文一样,都有一个明确的目标,不过目标的 IP 是一个特殊的值。

一般来说,某个子网的广播地址可以用下面方法计算:

1
2
3
       IP: 10.85.172.36
NetMask: 255.255.254.0
Broadcast: 10.85.172.36 & (255.255.254.0) | !(255.255.254.0) = 10.85.173.255

PS: NetMask 为子网掩码,! 为按位取反(取补码),计算在字节层面进行

换句话说,广播地址 = 自己的 IP & 子网掩码 | 子网掩码补码
也可以理解为网段中的最大 IP。

广播有一个通用地址是 255.255.255.255 ,DHCP 服务器中一般使用该地址作为广播目标。

如果使用 255.255.255.255 作为广播目标,该广播不会被路由器转发到其他设备,只在当前物理层面的局域网中传播。

顺带一提,链路层的广播地址一般是:FF:FF:FF:FF:FF:FF,这个知识在编写 PPPoE 服务器中需要了解(下一次博客更新 PPPoE 服务器编写)

DHCP 报文结构

DHCP 报文衍生自 bootp 报文,是一个 UDP 协议的网络层报文,该报文在网络层结构如下:
DHCP 报文
图片中数字为该项目的大小,单位是字节 Bytes,最后一项表示可变。

各个项目的意义:

OP:客户端到服务器为1,服务器返回客户端为 2。
Htype:硬件类型,Ethernet 为 1,实际在当前网络模式下该值始终为 1,无论无线还是有线,可以自己抓报文查看结果。
Hlen:硬件地址长度,目前的 Mac 地址都是6字节,所以值一般都是 6。
Hops:路由数量,每经过一个路由该值加一,普通网络一般用不到该值。
Transation ID:事务 ID,客户端请求与服务器的答复相互对应。
Secodes:客户端请求开始之后流逝的时间长度,并非 IP 租期时间。
Flags:标志位,目前只用到最高的一位 bit,表示 DHCP 服务器使用广播形式传包,一般不设置。
Ciaddr:用户 IP,一般为空,续租和释放时会带上自己的 IP 地址。
Yiaddr:客户 IP,服务器分配给客户端的 IP 地址。
Siaddr:bootswap 中的 IP,一般是一个回环地址,回环给自己。
Giaddr:网管地址,非下发网关地址,是记录被路由过的 DHCP 报文上级路由的 IP,一般为空。
Chaddr:客户端的硬件地址,始终是客户端自己的 Mac 地址,Mac 从前往后依次填写,也就是说现阶段只用到最前面六位。
Sname:可选,服务器名称,以 0x00 结尾。
File:启动文件名,在分配完成之后要读取或执行的额外文件。
Magic Cookie:DHCP 固定为 {0x63, 0x82, 0x53, 0x63},16 进制。

一般情况下 File,Sname 都是空的,很少用到。
有关 Magic Cookie,因为 DHCP 魔改自 bootp,所以为了让设备快速确认这是一个 DHCP 报文设置了这个特殊值,方便识别。

最后的 Option 报文的组织形式为:

OptionNum OptionLength OptionContains
4 Bytes 4 Bytes length Bytes

最后的 length 等于 OptionLength 定义的值。

Option 有如下属性信息可定义:

必须 Option编号 长度 含义
1 4 子网掩码
3 4 网关地址
6 4/8 DNS 地址
7 4 日志服务器
26 2 接口 MTU
33 8 静态路由(现阶段中多使用 Option 121)
35 4 ARP 缓存时间
42 4 NTP 服务器地址(时间同步服务器)
51 4 IP 租期
53 1 Message
1 - Discover
2 - Offer
3 - Request
4 - Decline
5 - ACK
6 - NAK
7 - Release
8 - INFO
54 4 服务器标示,一般是服务器的 IP 地址。

所有 Option 信息添加完毕之后,以0xFF表示配置结束。
报文末尾需要使用 0x00 补足不够的部分,bootp 报文在网络层上长度为 300 字节(不包含链路层报文头 42 字节),如果不足 300 字节要用 0x00 补齐,超过则不需要补齐。

其他的 Option
Option60: 有两种含义,一种是基本表征值,客户端用于报告自身硬件厂商和配置信息,一种是经过华为定制的携带有用户名密码信息的报文,这里由于商业机密原因不做过多说明,关键字 IPoE。

Option61: 客户端标示信息,一般是网络类型 + MAC 地址,正常情况与 Htype 和 Chaddr 保持一致,有些服务器会强制要求客户单携带 Option61。

Option121: 静态路由,该字段在一些特殊的网络环境下还是比较常见的。构造形式中,除去开头的 Num 和 Length,后面的形式为

Destination Router
n Bytes 4 Bytes

其中 Destination 为掩码长+掩码固定字段的形式,这也就造成了长度不固定。举个例子:
{ 0x00, 0x0A, 0x0A, 0x0A, 0x0A }
这样表示一条静态路由,所有的要访问的 IP 报文无条件发送到目标 10.10.10.10,相当于 0/0 -> 10.10.10.10
再比如:
{ 0x18, 0x10, 0x10, 0x10, 0x0A, 0x0A, 0x0A, 0x0A }
这表示所有要访问 16.16.16.0/24 网段的信息全部发送到 10.10.10.10,相当于 16.16.16.0/24 -> 10.10.10.10
Option121 可以含有多条静态路由,发送时需要一条一条组织,解析错误会造成设备联网异常。

Option125: 该字段为服务器向客户端提供的认证信息,有些情况下,客户端只希望收到指定的 DHCP 服务器消息,所以会要求 DHCP 服务器携带 Option125,收到报文之后将信息与预先存储的信息进行比对,比对通过才接受服务器的答复。

其他一些注意事项和技巧

  • NAK 报文一般只在下列情况下发送

    1.续租请求非法(租约过期、无法找到租约、请求的 IP 分配给了其他设备、请求的地址不在地址池中、请求的地址与 Offer 不一致)
    2.Chaddr 为空

  • DHCP 服务器分配并记录 IP 的过程不是在 Request 时发生,而是在 Offer 时就已经发生,Request 阶段只是确认客户端是否申请了该地址,申请了就开始对客户端的租期,否则清掉 Offer 阶段存储的信息。
    换句话说,Mac->IP 的映射在 Offer 阶段就已经记录,但是 Offer 阶段不会定义租约。
  • DHCP 服务器不会主动清理过期的 IP,也不会主动探测发现不可用的 IP,DHCP 服务器只是被动的分配 IP 给客户端,答复客户端消息,所以 DHCP 服务器其实不占用很多系统资源,通常一台普通 PC 就可以应付。
    所以 DHCP 服务器分配出去的 IP 可能是冲突的 IP,判断 IP 是否可用的过程是交给客户端判断的。
    但是 DHCP 服务器应当接受所有网络设备发出的 Decline 信息,如果自己没有记录 Decline 中携带的 IP 信息,那么就应该把该 IP 设置为不可用,以防止下次分配该 IP 给其他设备。
  • 编写 DHCP 服务器 IP 分配流程时,策略有很多种选择,这里我说一下我的设计方案,策略没有绝对的要求

    设计一个游标和 IP 分配池,游标初始指向第一个位置。
    每申请一次 IP,先查看游标位置的 IP 是否可用,如果可用,把该 IP 分配给设备,然后将游标移动到下一个位置,否则向后寻找可用的 IP。
    寻找的过程中,首先优先寻找未分配的 IP,如果没有,寻找租期到期的 IP,如果还没有,寻找以前标记为 Decline 的 IP。
    如果经过几次寻找都没有找到合适的 IP,这时候有两种策略,一种是不答复客户端的 Request 请求,另一种是发送 NAK 给客户端。
    每当有 IP 清除(Release 或者 Request 非自己),这时候可以把游标移动到清理的位置上,提高 IP 分配效率。

写在最后

DHCP 说到底只是一个协议,协议有些固定的内容,比如报文中消息的组织形式,但是也没有那么死板,比如续租流程,比如答复流程中 Option 携带的信息,这些都是可以定制和发挥的地方。
根据 bootp 报文的性质,其实是完全有能力设计一套自己的 IP 分配报文类型的,只是能不能流通就只能看时势了……
客户端的请求和服务器的答复都有可能发生异常,解析过程中要考虑到这些异常的情况。
当然胡搞也是可以的,比如无条件接受所有 Request,无条件拒绝所有 Request,分配 IP 的时候故意分配毫无意义的 IP 或者根本没法用的 IP,故意写错网关的地址,故意分配一个虚假的 DNS 信息,Renew 中更新客户端 IP 等等,这些都是可以做到的,只是这样违背了 DHCP 服务器初衷而已(不过真的很有趣 www)。
几个关键的地方:DHCP 服务器最关键的是 IP 分配策略,其他的基本没有太大变化,按照报文要求事先组织好消息形式即可。

可以拓展的地方:

  • DHCP 服务器自我管理租期到期的 IP 和自行检查不可用的 IP,不过这样会给 DHCP 服务器造成一定负担。
  • 主动与已经开始租期的 IP 通信判读设备是否还在线,不在线主动清理,同样很耗资源。
  • 多个 DHCP 服务器协同工作,就是上文提到的 Rebind 问题,这个往往在大型局域网 IP 管理策略中会用到,可以尝试一下。

第一篇博客就先这样吧,希望能够帮到你。

后面会陆续把以前总结在本上的知识写上来,敬请期待咯。

相关资料

RFC 2131(DHCP)
RFC 2132(Options in DHCP)
RFC 3442(Option 121)
RFC 3925(DHCPv4 Option 43/60/125)