DHCP,主机动态配置协议的代码实现第一步:实现设备请求和服务器应答
本节开始,我们代码实现DHCP的协议流程。其本质上是在相应的阶段,构造相应的数据包进行发送和接收,总体而言,DHCP数据包的格式如下:
它最复杂的其实是填写options字段,该字段种类及其繁杂,我们根据不同协议的不同阶段去搞清楚options字段的内容。根据我们前面描述,DHCP协议启动时,第一步是客户端在子网内广播dhcp discover消息,然后子网内相应的dhcp服务器回发dhcp offer消息,因此我们的代码先完成这一步骤。
首先我们要解析DHCP DISCOVER数据包结构。如果使用的是mac系统,在命令行控制台中输入命令:
sudo ipconfig set en0 DHCP
如果使用的是windows,那么在控制台中输入命令:
ipconfig renew
然后按照上一节使用wireshark抓包,我们就可以抓取DHCP DISCOVER消息:
点击打开该消息后,我们可以看到消息的字段结构:
这里我们需要把握几个option结构,第一个是option 53,它用来表明该数据包的类型,它的code值是53,数据长度1个字节,一般取值为1,数据内容用于表明数据包的类型,取值1表示消息类型为DHCPDISCOVERY,2为DHCPOFFER等,具体内容如下:
第2个option的code值为55,data_length 占据的字节数可变,数据区用于表明设备想从服务器获得哪些消息,从我抓包的情况看,它包含如下信息如下:
从上图看,我的设备向服务器请求一系列数据,例如子网掩码,路由器,域名服务器等,这些请求各自使用不同的数值来表示,例如数值1表示请求子网掩码,数值3请求路由器IP等。
第3个option的code值是57,data_length字节数是2,它用来确定相互交互的DHCP数据包的最大长度,数据区的内容是长度值。
第4个option的code值是61,它用来表示设备的身份标识,data_length的值根据具体情况而定,通常情况下它是1字节,在我们抓包中它取值7,数据区第一个字节表示硬件类型,接下来6个字节存储设备的mac地址。
第5个option的code值是51,它用来表示ip的租借时长。它的data_length字段占据4个字节,数据区存储的是一个数值,用于表示租借时长的秒数。
第6个option的code值是12,它用来表示设备名称,一般来说是你的主机名称,data_length占据长度根据名字长度而定,数据区存储的是设备名称字符串。
第7个option的code值是255,它表示结束,它只包含1个字节。
接下来我们看看如何使用代码组装该数据包:
package Application; import java.nio.ByteBuffer; import java.util.Random; import datalinklayer.DataLinkLayer; import protocol.ProtocolManager; public class DHCPApplication extends Application{ private static byte HARDWARE_TYPE = 1; private static byte HARDWARE_ADDR_LENGTH = 6; private static byte DHCP_HOPS = 0; private static byte MESSAGE_TYPE_REQUEST = 1; private short secs_elapsed = 0; private short bootp_flags = 0; private byte[] client_ip_address = new byte[4]; private byte[] your_ip_address = new byte[4]; private byte[] next_server_ip_address = new byte[4]; private byte[] relay_agent_ip_address = new byte[4]; private static byte[] MAGIC_COOKIE = new byte[] {63, 82, 52, 63}; private static byte [] dhcp_first_part; private static int DHCP_FIRST_PART_LENGTH = 236; private int transaction_id = 0; public DHCPApplication() { Random rand = new Random(); transaction_id = rand.nextInt(); constructDHCPFirstPart(); } private void constructDHCPFirstPart() { dhcp_first_part = new byte[DHCP_FIRST_PART_LENGTH]; ByteBuffer buffer = ByteBuffer.wrap(dhcp_first_part); //设置数据包类型 buffer.put(MESSAGE_TYPE_REQUEST); //设置网络类型 buffer.put(HARDWARE_TYPE); //设置硬件地址长度 buffer.put(HARDWARE_ADDR_LENGTH); //设置数据包跳转次数 buffer.put(DHCP_HOPS); //设置会话id buffer.putInt(transaction_id); //设置等待时间 buffer.putShort(secs_elapsed); //设置标志位 buffer.putShort(bootp_flags); //设置设备ip buffer.put(client_ip_address); //设置租借ip buffer.put(your_ip_address); //设置下一个服务器ip buffer.put(next_server_ip_address); //设置网关ip buffer.put(relay_agent_ip_address); //设置硬件地址 buffer.put(DataLinkLayer.getInstance().deviceMacAddress()); //填充接下来的10个字节 byte[] padding = new byte[10]; buffer.put(padding); //设置64字节的服务器名称 byte[] host_name = new byte[64]; buffer.put(host_name); //设置128位的byte字段 byte[] file = new byte[128]; buffer.put(file); } }
上面的代码用于构建数据包不包含option部分的数据。由于他们的内容变动不大,因此单独抽出来构建,option字段部分变动频繁,所以我们要专门处理。接着我们按照DISCOVER数据包构建对应的多个options字段:
private void constructDHCPOptionsPart() { //option 53 DHCP Message Type byte[] option_msg_type = new byte[OPTION_MSG_TYPE_LENGTH]; ByteBuffer buffer = ByteBuffer.wrap(option_msg_type); buffer.put(OPTION_MSG_TYPE); buffer.put(OPTION_MSG_DATA_LENGTH); buffer.put(OPTION_MSG_TYPE_DISCOVERY); //option 55 Parameter Request List byte[] parameter_request_list = new byte[OPTION_PARAMETER_REQUEST_LENGTH]; buffer = ByteBuffer.wrap(parameter_request_list); buffer.put(OPTION_PARAMETER_REQUEST_LIST); buffer.put(OPTION_PARAMETER_REQUEST_DATA_LENGTH); byte[] option_buffer = new byte[] {OPTIONS_PARAMETER_SUBNET_MASK, OPTIONS_PARAMETER_STATIC_ROUTER, OPTIONS_PARAMETER_ROUTER, OPTIONS_PARAMETER_DOMAIN_NAME_SERVER, OPTIONS_PARAMETER_DOMAIN_NAME, OPTIONS_PARAMETER_DOMAIN_SEARCH,OPTIONS_PARAMETER_PROXY,OPTIONS_PARAMETER_LDPA, OPTIONS_PARAMETER_IP_NAME_SERVER,OPTIONS_PARAMETER_IP_NODE_TYPE}; buffer.put(option_buffer); //option 57 Maximum DHCP Message Size byte[] maximun_dhcp_msg_size = new byte[OPTION_MAXIMUN_DHCP_MESSAGE_SIZE_LENGTH]; buffer = ByteBuffer.wrap(maximun_dhcp_msg_size); buffer.put(OPTION_MAXIMUM_DHCP_MESSAGE_SIZE_TYPE); buffer.put(OPTION_MAXIMUN_DHCP_MESSAGE_SIZE_DATA_LENGTH); buffer.putShort(OPTION_MAXIMUN_DHCP_MESSAGE_SIZE_CONTENT); //option 61 Client identifier byte[] client_identifier = new byte[OPTION_CLIENT_IDENTIFIER_LENGTH]; buffer = ByteBuffer.wrap(client_identifier); buffer.put(OPTION_CLIENT_IDENTIFIER); buffer.put(OPTION_CLIENT_IDENTIFIER_DATA_LENGTH); buffer.put(OPTION_CLIENT_IDENTIFIER_HARDWARE_TYPE); buffer.put(DataLinkLayer.getInstance().deviceMacAddress()); //option 51 ip address lease time byte[] ip_lease_time = new byte[OPTION_IP_LEASE_TIME_LENGTH]; buffer = ByteBuffer.wrap(ip_lease_time); buffer.put(OPTION_IP_LEASE_TIME); buffer.put(OPTION_IP_LEASE_TIME_DATA_LENGTH); buffer.putInt(OPTION_IP_LEASE_TIME_CONTENT); //option 12 Host Name byte[] host_name = new byte[OPTION_HOST_NAME_LENGTH]; buffer = ByteBuffer.wrap(host_name); buffer.put(OPTION_HOST_NAME); buffer.put(OPTION_HOST_NAME_DATA_LENGTH); buffer.put(OPTION_HOST_NAME_CONTENT); //option end byte[] end = new byte[1]; end[0] = OPTION_END; byte[] padding = new byte[13]; dhcp_options_part = new byte[ + option_msg_type.length + parameter_request_list.length + maximun_dhcp_msg_size.length + client_identifier.length + ip_lease_time.length + host_name.length + end.length + padding.length]; buffer = ByteBuffer.wrap(dhcp_options_part); buffer.put(option_msg_type); buffer.put(parameter_request_list); buffer.put(maximun_dhcp_msg_size); buffer.put(client_identifier); buffer.put(ip_lease_time); buffer.put(host_name); buffer.put(end); buffer.put(padding); }
上面代码目的在于构造DHCP Discover消息的options字段,这些字段包含设备与服务器交互的各种信息,例如规定了数据包的最大长度,设备需要请求哪些网络配置信息等,完成上面代码后,一个DHCP数据请求包就构造完毕,我们再构造IP包头和UDP包头,包裹住上面构造的数据广播到网络上就可以完成DHCP协议的第一步,代码如下:
public void dhcpDiscovery() { byte[] dhcpDiscBuffer = new byte[dhcp_first_part.length + MAGIC_COOKIE.length + dhcp_options_part.length]; ByteBuffer buffer = ByteBuffer.wrap(dhcpDiscBuffer); buffer.put(dhcp_first_part); buffer.put(MAGIC_COOKIE); buffer.put(dhcp_options_part); byte[] udpHeader = createUDPHeader(dhcpDiscBuffer); byte[] ipHeader = createIP4Header(udpHeader.length); byte[] dhcpPacket = new byte[ udpHeader.length + ipHeader.length]; buffer = ByteBuffer.wrap(dhcpPacket); buffer.put(ipHeader); buffer.put(udpHeader); //将消息广播出去 ProtocolManager.getInstance().broadcastData(dhcpPacket); }
接着我们在代码主入口处调用上面代码发送我们自己构造的数据包:
try { DHCPApplication dhcpApp = new DHCPApplication(); dhcpApp.dhcpDiscovery(); } catch(Exception e) { e.printStackTrace(); }
此时通过wireshark发现,我们发出的数据包以及路由器的应答数据包都被抓取到:
当服务器返回数据包后,我们的程序要接收它,并对它进行读取。DHCPApplication程序设定的端口是68,因此当路由器返回DHCP Offer消息后会被ProtocolManager抓取到,它会根据数据包指向的端口号,将该数据包分发给DHCPApplication,然后后者再对收到的数据进行解读,在ProtocoalManager中增加代码如下:
private void handleUDPPacket(Packet packet, HashMap<String, Object> infoFromUpLayer) { IProtocol udpProtocol = new UDPProtocolLayer(); HashMap<String, Object> headerInfo = udpProtocol.handlePacket(packet); short dstPort = (short)headerInfo.get("dest_port"); //根据端口获得应该接收UDP数据包的程序 IApplication app = ApplicationManager.getInstance().getApplicationByPort(dstPort); app.handleData(headerInfo); }
然后进入UDPProtocolLayer增加解析UDP数据包的代码:
@Override public HashMap<String, Object> handlePacket(Packet packet) { ByteBuffer buffer= ByteBuffer.wrap(packet.header); HashMap<String, Object> headerInfo = new HashMap<String, Object>(); headerInfo.put("src_port", buffer.getShort(UDP_SRC_PORT_OFFSET)); headerInfo.put("dest_port", buffer.getShort(UDP_DST_PORT_OFFSET)); headerInfo.put("length", buffer.getShort(UDP_LENGTH_OFFSET)); headerInfo.put("data", packet.data); return headerInfo; }
如此一来,UDP协议层从收到的数据包中拿到接收程序对应的端口和数据,ProtocolManager能根据端口将数据返回给对应程序。当UDP层协议对接收数据分析完后,把分析结果提交给DHCPApplication,后者会解析DHCP数据包中的内容:
public void handleData(HashMap<String, Object> headerInfo) { byte[] data = (byte[])headerInfo.get("data"); boolean readSuccess = readFirstPart(data); if (readSuccess) { readOptions(data); } } private boolean readFirstPart(byte[] data) { ByteBuffer buffer = ByteBuffer.wrap(data); byte reply = buffer.get(DHCP_MSG_TYPE_OFFSET); if (reply != DHCP_MSG_REPLY) { return false; } byte[] your_addr = new byte[4]; buffer.position(DHCP_YOUR_IP_ADDRESS_OFFSET); buffer.get(your_addr, 0, your_addr.length); System.out.println("available ip offer by dhcp server is: "); try { InetAddress addr = InetAddress.getByAddress(your_addr); System.out.println(addr.getHostAddress()); } catch (UnknownHostException e) { // TODO Auto-generated catch block e.printStackTrace(); } buffer.position(DHCP_NEXT_IP_ADDRESS_OFFSET); byte[] next_server_addr = new byte[4]; buffer.get(next_server_addr, 0, next_server_addr.length); try { InetAddress addr = InetAddress.getByAddress(next_server_addr); System.out. 1d25c println(addr.getHostAddress()); } catch (UnknownHostException e) { // TODO Auto-generated catch block e.printStackTrace(); } return true; }
DHCPApplication解析接收到的回复数据时分两部分,因为DHCP数据包逻辑上可以分成两部分,一部分是options,另一部分是上面的一系列信息。因此我们解读时也相应分两步走,第一步解读options上面的各种ip信息,第二步解读optins信息:
private void readOptions(byte[] data) { ByteBuffer buff = ByteBuffer.wrap(data); buff.position(DHCP_OPTIONS_OFFSET); while (true) { byte type = buff.get(); if (type == OPTION_END) { break; } switch(type) { case DHCP_MSG_TYPE: //越过长度字段 buff.get(); if (buff.get() == DHCP_MSG_OFFER) { System.out.println("receive DHCP OFFER message from server"); } break; case DHCP_SERVER_IDENTIFER: printOptionArray("DHCP server identifier:", buff); break; case DHCP_IP_ADDRESS_LEASE_TIME: //越过长度字段 buff.get(); int lease_time_secs = buff.getInt(); System.out.println("The ip will lease to us for " + lease_time_secs + "seconds" ); break; case DHCP_RENEWAL_TIME: //越过长度字段 buff.get(); int renew_time = buff.getInt(); System.out.println("we need to renew ip after " + renew_time + "seconds"); break; case DHCP_REBINDING_TIME: //越过长度字段 buff.get(); int rebinding_time = buff.getInt(); System.out.println("we need to rebinding new ip after " + rebinding_time + "seconds"); break; case DHCP_SUBNET_MASK: printOptionArray("Subnet mask is : ", buff); break; case DHCP_BROADCAST_ADDRESS: printOptionArray("Broadcasting Address is : ", buff); break; case DHCP_ROUTER: printOptionArray("Broadcasting Address is : ", buff); break; case DHCP_DOMAIN_NAME_SERVER: printOptionArray("Domain name server is : ", buff); break; case DHCP_DOMAIN_NAME: int len = buff.get(); for(int i = 0; i < len; i++) { System.out.print((char)buff.get() + " "); } break; } } } private void printOptionArray(String content, ByteBuffer buff) { System.out.println(content); int len = buff.get(); if (len == 4) { byte[] buf = new byte[4]; for (int i = 0; i < len; i++) { buf[i] = buff.get(); } try { InetAddress addr = InetAddress.getByAddress(buf); System.out.println(addr.getHostAddress()); } catch (UnknownHostException e) { // TODO Auto-generated catch block e.printStackTrace(); } } else { for (int i = 0; i < len; i++) { System.out.print(buff.get() + "."); } } System.out.println("\n"); }
由于options字段由多个option结构提组合在一起,因此我们用循环依次遍历整个options字段,每次抽取出一个option结构进行解读,一旦解读到code值为255的option时,我们知道所有结构都解读完毕。
上面代码运行后,我们解读DHCP Offer数据包的结果如下:
通过抓包比对可以发现,我们解读的信息与wireshark抓包获得的信息完全一致,如此我们就完成了DHCP协议第一步:设备询问服务器,服务器回应设备查询!在此基础上我们可以进一步完成后续协议内容。
更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:
- DHCP,代码实现主机动态配置协议IP租用请求和应答
- RHEL5中配置DHCP服务器实现动态主机配置
- HTTP协议的介绍,Web服务器配置,虚拟主机的配置,如何用SSL实现HTTPS。
- CentOS 6.5中实现动态主机协议--DHCP
- HTTP协议的介绍,Web服务器配置,虚拟主机的配置,如何用SSL实现HTTPS。
- 教你如何快速的在linux5.0上为大型公司企业搭建DHCP(动态主机配置)服务以及中继代理服务
- USB 主机在请求HID 设备的配置描述符时,设备首先返回的描述符为:
- DHCP服务器和中继代理服务器,实现跨网段动态IP分配
- 一个服务器上面配置多个IP ,实现指定IP的域名请求
- 通过批处理实现DHCP服务器批量配置保留地址
- 通过路由器配置DMZ主机实现搭建公网SVN服务器
- DHCP_动态主机配置
- DHCP2:为什么叫做动态主机配置,而不是动态网络配置
- maven 配置tomcat插件实现热部署,上传代码到服务器。
- nginx 配置虚拟主机,实现在一个服务器可以访问多个网站的方法
- DHCP 是 Dynamic Host Configuration Protocol(动态主机分配协议)
- linux下dhcp服务器配置和windows server 2003 实现中继服务 (二)
- 【转载】茶叶蛋干货!《超容易的Linux系统管理入门书》(连载十)进行动态主机配置DHCP
- apache中使用mod_gnutls模块实现多个SSL站点配置(多个HTTPS协议的虚拟主机)
- Google Maps API 2.0解析(11-GCopyrightCollection动态向图片服务器请求代码)