Stefenson's Blog

一个渣渣程序员的笔记

Stefenson Wang's avatar Stefenson Wang

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是一个特殊的值。

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

       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,bootp文档解释为让服务器确定在这个报文中它看到的信息类型,有点像特殊标示。

最后的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)