在上一篇文章中讲到UDP的基本内容,UDP的三层封包协议和UDP的软件开发。在上一篇文章中获取客户端IP地址的方法是很简单粗暴的,说实在的是一个错误的做法。虽然也是截取DHCP数据包,但是方法不对,所以今天我们来描述一下如何通过正确的方式获取IP地址。
DHCP(动态主机配置协议),它是一种局域网的网络协议,使用的还是UDP数据包,DHCP采用C/S模式,分服务端采用67端口号和客户端采用78端口号。DHCP客户端向DHCP服务端发送报文的过程称为请求报文,DHCP服务端向DHCP客户端发送报文称为应答报文。DHCP可以为客户机自动分配IP地址,子网掩码,缺省网关,DNS服务器的IP地址等,并能够提升地址的使用率。
DHCP报文属于UDP报文,DHCP协议包含在UDP协议栈的用户数据部分。如下图红框部分:
DHCP报文的种类有八种:分别为DHCP Discover、DHCP Offer、DHCP Request、DHCP ACK、DHCP NAK、DHCP Release、DHCP Decline、DHCP Inform。各种类型报文的基本功能如下(表格内容参考网上资料):
DHCP报文类型 | 说明 |
---|---|
DHCP Discover | DHCP客户端在请求IP地址时并不知道DHCP服务器的位置,因此DHCP客户端会在本地网络内以广播方式发送Discover请求报文,以发现网络中的DHCP服务器。所有收到Discover报文的DHCP服务器都会发送应答报文,DHCP客户端据此可以知道网络中存在的DHCP服务器的位置。 |
DHCP Offer | DHCP服务器收到Discover报文后,就会在所配置的地址池中查找一个合适的IP地址,加上相应的租约期限和其他配置信息(如网关、DNS服务器等),构造一个Offer报文,发送给DHCP客户端,告知用户本服务器可以为其提供IP地址。但这个报文只是告诉DHCP客户端可以提供IP地址,最终还需要客户端通过ARP来检测该IP地址是否重复. |
DHCP Request | DHCP客户端可能会收到很多Offer请求报文,所以必须在这些应答中选择一个。通常是选择第一个Offer应答报文的服务器作为自己的目标服务器,并向该服务器发送一个广播的Request请求报文,通告选择的服务器,希望获得所分配的IP地址。另外,DHCP客户端在成功获取IP地址后,在地址使用租期达到50%时,会向DHCP服务器发送单播Request请求报文请求续延租约,如果没有收到ACK报文,在租期达到87.5%时,会再次发送广播的Request请求报文以请求续延租约。 |
DHCP ACK | DHCP服务器收到Request请求报文后,根据Request报文中携带的用户MAC来查找有没有相应的租约记录,如果有则发送ACK应答报文,通知用户可以使用分配的IP地址。 |
DHCP NAK | 如果DHCP服务器收到Request请求报文后,没有发现有相应的租约记录或者由于某些原因无法正常分配IP地址,则向DHCP客户端发送NAK应答报文,通知用户无法分配合适的IP地址。 |
DHCP Release | 当DHCP客户端不再需要使用分配IP地址时,就会主动向DHCP服务器发送RELEASE请求报文,告知服务器用户不再需要分配IP地址,请求DHCP服务器释放对应的IP地址。 |
DHCP Decline | DHCP客户端收到DHCP服务器ACK应答报文后,通过地址冲突检测发现服务器分配的地址冲突或者由于其他原因导致不能使用,则会向DHCP服务器发送Decline请求报文,通知服务器所分配的IP地址不可用,以期获得新的IP地址。 |
DHCP Inform | DHCP客户端如果需要从DHCP服务器端获取更为详细的配置信息,则向DHCP服务器发送Inform请求报文;DHCP服务器在收到该报文后,将根据租约进行查找到相应的配置信息后,向DHCP客户端发送ACK应答报文。目前基本上不用了。 |
DHCP的8种报文格式是一样的,它是通过报文中的字段的取值不同,来划分类型和信息。如下图为DHCP报文格式:
字段 | 长度 | 说明 |
---|---|---|
OP | 1Byte | 报文操作类型,分为请求报文和响应报文。1:请求报文,2:应答报文。其中,请求报文:DHCP Discover、DHCP Request、DHCP Release、DHCP Inform和DHCP Decline。应答报文:DHCP Offer、DHCP ACK和DHCP NAK |
Htype | 1Byte | DHCP客户端的MAC地址类型。MAC地址类型其实是指明网络类型。 |
Hlen | 1Byte | DHCP客户端的MAC地址长度。以太网MAC地址长度为6个字节,即以太网时Hlen值为6。 |
Hops | 1Byte | DHCP报文经过的DHCP中继的数目,默认为0。DHCP请求报文每经过一个DHCP中继,该字段就会增加1。没有经过DHCP中继时值为0。 |
Xid | 4Byte | 客户端通过DHCP Discover报文发起一次IP地址请求时选择的随机数,相当于请求标识。用来标识一次IP地址请求过程。在一次请求中所有报文的Xid都是一样的。 |
Secs | 2Byte | DHCP客户端从获取到IP地址或者续约过程开始到现在所消耗的时间,以秒为单位。在没有获得IP地址前该字段始终为0。 |
Flags | 2Byte | 标志位,只使用第0比特位,是广播应答标识位,用来标识DHCP服务器应答报文是采用单播还是广播发送,0表示采用单播发送方式,1表示采用广播发送方式。其余位尚未使用。 |
Ciaddr | 4Byte | DHCP客户端的IP地址。仅在DHCP服务器发送的ACK报文中显示,在其他报文中均显示0,因为在得到DHCP服务器确认前,DHCP客户端是还没有分配到IP地址的。只有客户端是Bound、Renew、Rebinding状态,并且能响应ARP请求时,才能被填充。 |
Yiaddr | 4Byte | DHCP服务器分配给客户端的IP地址。仅在DHCP服务器发送的Offer和ACK报文中显示,其他报文中显示为0。 |
Siaddr | 4Byte | 下一个为DHCP客户端分配IP地址等信息的DHCP服务器IP地址。仅在DHCP Offer、DHCP ACK报文中显示,其他报文中显示为0。 |
Giaddr | 4Byte | DHCP客户端发出请求报文后经过的第一个DHCP中继的IP地址。如果没有经过DHCP中继,则显示为0。 |
Chaddr | 16Byte | DHCP客户端的MAC地址。在每个报文中都会显示对应DHCP客户端的MAC地址。 |
Sname | 64Byte | 为DHCP客户端分配IP地址的DHCP服务器名称(DNS域名格式)。在Offer和ACK报文中显示发送报文的DHCP服务器名称,其他报文显示为0。 |
File | 128Byte | DHCP服务器为DHCP客户端指定的启动配置文件名称及路径信息。仅在DHCP Offer报文中显示,其他报文中显示为空。 |
Options | variable-length | 可选项字段,长度可变,格式为"代码+长度+数据" |
代码 | 长度 | 说明 |
---|---|---|
1 | 4Byte | 子网掩码 |
3 | 可变长,必须是4Byte的倍数 | 默认网关 |
6 | 可变长,必须是4Byte的倍数 | DNS服务器 |
15 | 可变长 | 域名称(主DNS服务器名称) |
44 | 可变长,必须是4Byte的倍数 | WINS服务器 |
51 | 4Byte | 有效租约期(以秒为单位) |
53 | 1Byte | 报文类型:1: DHCP Discover、2: DHCP Offer、3: DHCP Request、4: DHCP Decline、5: DHCP ACK、6: DHCP NAK、7: DHCP Release、8: DHCP Inform |
58 | 4Byte | 续约时间 |
在每次的连接中,客户端都会主动发送DHCP请求,从而获取IP地址等信息。如下图
在上述的描述中,我们DHCP包的各个字段进行了描述。但是我们真正用到的就几个字段。
DHCP报文的Chaddr字段:客户端MAC地址,区分每一个客户端的最好方式就是MAC地址,因为它是唯一标志客户端设备。所以我们可以通过DHCP报文中的CHaddr字段来捕获属于本客户端的DHCP报文。
DHCP报文的可选字段(代码:53):我们通过此字段来捕获DHCP ACK报文。
DHCP报文的Yiaddr字段:当上面两个字段条件满足,我们就可以通过此字段获取客户端的IP地址。
下面我们来说说代码实现:
注意:在下面的结构体中多了一个cookie字段,其实它是包含在可选字段的前四个字节。为了方便写代码,所以直接在独立出来。
struct mini_udp_dhcp_msg {
rt_uint8_t op;
rt_uint8_t htype;
rt_uint8_t hlen;
rt_uint8_t hops;
rt_uint8_t xid[4];
rt_uint16_t secs;
rt_uint16_t flags;
rt_uint8_t ciaddr[4];
rt_uint8_t yiaddr[4];
rt_uint8_t siaddr[4];
rt_uint8_t giaddr[4];
rt_uint8_t chaddr[16];
rt_uint8_t sname[64];
rt_uint8_t file[128];
rt_uint8_t cookie[4];
rt_uint8_t options[312];
};
在原来的代码中进行修改,增加DHCP报文解析。
int mini_udp_input(const void *packet, uint32_t packet_len)
{
struct mini_mac_header *mac_hdr = NULL;
struct mini_ip_header *ip_hdr = NULL;
struct mini_udp_header *udp_hdr = NULL;
mac_hdr = (struct mini_mac_header *)(packet);
if(mac_hdr->type != htons(ETHTYPE_IP)) //判断类型
{
return -1;
}
ip_hdr = (struct mini_ip_header *)((uint8_t *)mac_hdr + MAC_HDR_SIZE);
if(IPH_V_GET(ip_hdr) != 4) //判断版本是否为IPV4
{
return -1;
}
if(IPPROTO_UDP != IPH_PROTO_GET(ip_hdr)) //判断是否为数据报
{
return -1;
}
udp_hdr = (struct mini_udp_header *)((uint8_t *)ip_hdr + IP_HDR_SIZE);
switch(ntohs(udp_hdr->src_port))
{
case DHCP_SERVER_PORT: //读取DHCP包,获取本地IP
{
// 获取DHCP报文
struct mini_udp_dhcp_msg *dhcp_msg = (struct mini_udp_dhcp_msg *)((rt_uint8_t *)udp_hdr + UDP_HDR_SIZE);
// 判断是否为本客户端的MAC地址
if(mac_cmp(dhcp_msg->chaddr))
{
rt_uint32_t option_length = packet_len - member_offset(struct mini_udp_dhcp_msg, options);
rt_uint8_t *option_start = dhcp_msg->options;
rt_uint8_t *option_end = option_start + option_length;
// 在可选字段中查找(代码53)
while (option_start < option_end) {
if((uint8_t)*option_start == DHCP_OPTION_CODE_MSG_TYPE)
{
// 判断是否为DHCP ACK报文
if(*(option_start + 2) == DHCP_MESSAGE_TYPE_ACK)
{
rt_memcpy(&udp_info.local_ip, &dhcp_msg->yiaddr, sizeof(struct ip_addr)); //save local ip
LOG_I("local ip: %d:%d:%d:%d", udp_info.local_ip.addr[0], udp_info.local_ip.addr[1],
udp_info.local_ip.addr[2], udp_info.local_ip.addr[3]);
break;
}
}
option_start += option_start[1] + 2;
}
}
break;
}
case NTP_SERVER_PORT: //接收指定端口号的广播包,并dump出来。
{
hex_dump(packet, packet_len);
mini_udp_output(mac_hdr, ip_hdr, "Rice is best", sizeof("Rice is best")); //接收成功,返回数据"Rice is best"
break;
}
default:
{
return -1;
}
}
return 0;
}
通过查看Wireshark抓包工具和客户端捕获的IP地址是一致的。
关注微信公众号『Rice嵌入式开发技术分享』,后台回复“微信”添加作者微信,备注”入群“,便可邀请进入技术交流群。