首发: Rice 嵌入式开发技术分享
作者:RiceDIY
背景
- 在上一篇文章中讲到UDP的基本内容,UDP的三层封包协议和UDP的软件开发。在上一篇文章中获取客户端IP地址的方法是很简单粗暴的,说实在的是一个错误的做法。虽然也是截取DHCP数据包,但是方法不对,所以今天我们来描述一下如何通过正确的方式获取IP地址。
- DHCP(动态主机配置协议),它是一种局域网的网络协议,使用的还是UDP数据包,DHCP采用C/S模式,分服务端采用67端口号和客户端采用78端口号。DHCP客户端向DHCP服务端发送报文的过程称为请求报文,DHCP服务端向DHCP客户端发送报文称为应答报文。DHCP可以为客户机自动分配IP地址,子网掩码,缺省网关,DNS服务器的IP地址等,并能够提升地址的使用率。
UDP理论讲解
DHCP报文种类
DHCP报文属于UDP报文,DHCP协议包含在UDP协议栈的用户数据部分。如下图红框部分:
DHCP报文的种类有八种:分别为DHCP Discover、DHCP Offer、DHCP Request、DHCP ACK、DHCP NAK、DHCP Release、DHCP Decline、DHCP Inform。各种类型报文的基本功能如下(表格内容参考网上资料):
DHCP报文格式
DHCP的8种报文格式是一样的,它是通过报文中的字段的取值不同,来划分类型和信息。如下图为DHCP报文格式:
DHCP报文中部分可选字段的说明
如何获取IP地址
在每次的连接中,客户端都会主动发送DHCP请求,从而获取IP地址等信息。如下图
在上述的描述中,我们DHCP包的各个字段进行了描述。但是我们真正用到的就几个字段。
- DHCP报文的Chaddr字段:客户端MAC地址,区分每一个客户端的最好方式就是MAC地址,因为它是唯一标志客户端设备。所以我们可以通过DHCP报文中的CHaddr字段来捕获属于本客户端的DHCP报文。
- DHCP报文的可选字段(代码:53):我们通过此字段来捕获DHCP ACK报文。
- DHCP报文的Yiaddr字段:当上面两个字段条件满足,我们就可以通过此字段获取客户端的IP地址。
下面我们来说说代码实现:
DHCP报文结构体定义
注意:在下面的结构体中多了一个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报文捕获代码修改
在原来的代码中进行修改,增加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地址是一致的。!
UDP协议栈系列篇阅读
更多嵌入式技术干货请关注Rice 嵌入式开发技术分享