DHCP,代码实现主机动态配置协议IP租用请求和应答

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/tyler_download/article/details/88967533

我们在上一节中完成了客户端请求和服务器应答的第一个步骤。客户端发出DHCP_DISCOVER消息,局域网内的所有DHCP服务器发出DHCP_OFFER消息,在该消息中包含一个特殊字段叫Your_IP_Address,这是服务器分配给客户端的IP地址,如下图:

屏幕快照 2019-04-01 下午5.58.14.png

客户端可能会同时受到多个DHCP服务器发送的回应,然后它从中选择一个服务器发送过来的IP地址,并构造一个DHCP_REQUEST发送给对方,在数据包的Options字段中,使用一个Option表示它向服务器请求该IP地址:

屏幕快照 2019-04-01 下午6.01.23.png

然后服务器会向客户端发送DHCP_ACK消息表示确认客户端的租借请求:

屏幕快照 2019-04-01 下午6.10.32.png

完成了上面步骤后,服务器会记录客户端硬件地址与租借地址的对应关系,客户端在租用IP后,想要续租时,依旧会与给定服务器交流,在续租时,它会向绑定服务器发送DHCP_REQUEST消息,在消息中附带续租时常,同时如果允许续租的话,服务器会向客户端发送DHCP_ACK消息,在消息里附带了服务器允许客户端继续租用给定IP的时间。

如果客户端需要离开网络,不再使用给定IP,它会向服务器发送DHCP_RELEASE消息表示放弃当前使用的IP,如此服务器就能回收IP资源,将宝贵的IP分发给其他需要的客户端,DHCP_RELEASE消息中包含了客户端当前使用的IP地址,其基本内容如下:

屏幕快照 2019-04-01 下午6.17.31.png

本节我们就使用代码实现该流程。 由于代码涉及到IP分配,为了不影响运行机器的网络功能,我们在运行协议时使用虚构的mac地址:

 //构造一个不存在的mac地址
	    public byte[] deviceFakeMacAddress() {
	    	byte[] fakeMac = new byte[macAddress.length];
	    	for (int i = 0; i < macAddress.length; i++) {
	    		fakeMac[i] = (byte) (macAddress[i] + 1);
	    	}
	    	
	    	return fakeMac;
	    }

在DHCPAppliacation中,我们把上节使用到mac地址的地方都改成上面函数,同时我们修改上节对DHCP_OFFER消息的解读,记录下服务器提供的IP:

private boolean readFirstPart(byte[] data) {
....
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 {
			//记录下服务器提供的可用ip
			server_supply_ip = InetAddress.getByAddress(your_addr);
			System.out.println(server_supply_ip.getHostAddress());
		} catch (UnknownHostException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
....
}

当我们在DHCP_OVER消息中拿到your_client_ip之后,我们就可以向服务器发送一个DHCP_REQUEST请求来租用这个IP:

private byte[] constructDHCPRequestOptions() {
		byte[] option_msg_type = new byte[OPTION_MSG_TYPE_LENGTH];
    	ByteBuffer buffer = ByteBuffer.wrap(option_msg_type);
    	buffer.put(DHCP_MSG_TYPE);
    	buffer.put(OPTION_MSG_REQUEST_LENGTH);
    	buffer.put(OPTION_MSG_REQUEST_TYPE);
    	//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);
    	
    	//add ip request
    	byte[] requested_ip_addr = new byte[OPTION_REQUESTED_IP_TYPE_LENGTH + server_supply_ip.getAddress().length];
    	buffer = ByteBuffer.wrap(requested_ip_addr);
    	buffer.put(OPTION_REQUESTED_IP_TYPE);
    	buffer.put(OPTION_REQUESTED_IP_LENGTH);
    	buffer.put(server_supply_ip.getAddress());
    	
    	//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().deviceFakeMacAddress());
    	
    	//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 +
    	                                 + requested_ip_addr.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);
    	
    	return buffer.array();
	}
	
	public void dhcpRequest() {
		if (this.server_supply_ip == null) {
			return;
		}
		
		byte[] options = constructDHCPRequestOptions();
		byte[] dhcpDiscBuffer = new byte[dhcp_first_part.length + MAGIC_COOKIE.length + options.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);
	}

上面两个函数中,constructDHCPRequestOptions用于构建DHCP_REQUEST数据包的options部分,它与DHCP_DISCOVER唯一的区别在于增加了一个option字段,也就是DHCP_REQUESTED_IP,把要租用的ip地址传递给服务器。接下来的dhcpRequest()与原来实现的dhcpDiscovery差不多,也是生成udp和ip包头后,把数据包发送给服务器。

前面说过,DHCP客户端要维持一个状态机,记录它在协议执行中不同的状态变化,因此我们在代码中也增加了表示当前状态的记录:

    private static byte DHCP_MSG_ACK = 5;
    
    private final static int DHCP_STATE_DISCOVER = 0;
    private final static int DHCP_STATE_REQUESTING = 1;
    
    private static int dhcp_current_state = DHCP_STATE_DISCOVER; 

我们将根据接收到服务器发来的数据包内容改变当前状态:

private void readOptions(byte[] data) {
....

			switch(type) {
			case DHCP_MSG_TYPE:
				//越过长度字段
				buff.get();
                                byte msg_type = buff.get();
				if (msg_type == DHCP_MSG_OFFER) {
					System.out.println("receive DHCP OFFER message from server");
					//接收到DHCP_OFFER后,将状态转变为requesting
					dhcp_current_state = DHCP_STATE_REQUESTING; 
				}
				
			    //receive ack msg
			    if (msg_type == DHCP_MSG_ACK) {
			    	System.out.println("receive DHCP ACK message from server");
			    }
				break;
....
    }
....
trigger_action_by_state();
}

private void trigger_action_by_state() {
		switch(dhcp_current_state) {
		case DHCP_STATE_REQUESTING:
			dhcpRequest();
			break;
			default:
				break; 
		}
	}

上面代码用于读取服务器发来数据包的options字段,根据字段中的内容我们转换状态,一开始程序处于DHCP_STATE_DISCOVER状态,一旦收到服务器发来的回应包中包含DHCP_MSG_OFFER字段时,我们将状态转换为DHCP_STATE_REQUESTING状态。

然后根据状态执行相应动作,在函数trigger_action_by_state中,一旦检测到状态为DHCP_STATE_REQUESTING时,他就构造一个DHCP_REQUEST消息吧发送给服务器,然后等待服务器回发DHCP_ACK数据包,这样我们就走完了ip的租借流程。

当运行上面代码后,使用wireshark抓包情况如下:

屏幕快照 2019-04-02 上午11.48.27.png

我们在代码中构造一个虚假mac地址,DHCP服务器为该mac地址分配了一个可用IP是192.168.2.159,同时代码发送一个DHCP_REQUEST消息,同时服务器正常给我们返回了DHCP_ACK,表示接受了我们对该IP的租用请求。

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

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

猜你喜欢

转载自blog.csdn.net/tyler_download/article/details/88967533