您的位置:首页 > 编程语言

DHCP,主机动态配置协议的代码实现第一步:实现设备请求和服务器应答

2019-03-22 09:18 197 查看
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/tyler_download/article/details/88732684

本节开始,我们代码实现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协议第一步:设备询问服务器,服务器回应设备查询!在此基础上我们可以进一步完成后续协议内容。

更详细的讲解和代码调试演示过程,请点击链接

更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐