天天看點

DHCP,代碼實作主機動态配置協定IP租用請求和應答

我們在上一節中完成了用戶端請求和伺服器應答的第一個步驟。用戶端發出DHCP_DISCOVER消息,區域網路内的所有DHCP伺服器發出DHCP_OFFER消息,在該消息中包含一個特殊字段叫Your_IP_Address,這是伺服器配置設定給用戶端的IP位址,如下圖:

DHCP,代碼實作主機動态配置協定IP租用請求和應答

用戶端可能會同時受到多個DHCP伺服器發送的回應,然後它從中選擇一個伺服器發送過來的IP位址,并構造一個DHCP_REQUEST發送給對方,在資料包的Options字段中,使用一個Option表示它向伺服器請求該IP位址:

DHCP,代碼實作主機動态配置協定IP租用請求和應答

然後伺服器會向用戶端發送DHCP_ACK消息表示确認用戶端的租借請求:

DHCP,代碼實作主機動态配置協定IP租用請求和應答

完成了上面步驟後,伺服器會記錄用戶端硬體位址與租借位址的對應關系,用戶端在租用IP後,想要續租時,依舊會與給定伺服器交流,在續租時,它會向綁定伺服器發送DHCP_REQUEST消息,在消息中附帶續租時常,同時如果允許續租的話,伺服器會向用戶端發送DHCP_ACK消息,在消息裡附帶了伺服器允許用戶端繼續租用給定IP的時間。

如果用戶端需要離開網絡,不再使用給定IP,它會向伺服器發送DHCP_RELEASE消息表示放棄目前使用的IP,如此伺服器就能回收IP資源,将寶貴的IP分發給其他需要的用戶端,DHCP_RELEASE消息中包含了用戶端目前使用的IP位址,其基本内容如下:

DHCP,代碼實作主機動态配置協定IP租用請求和應答

本節我們就使用代碼實作該流程。 由于代碼涉及到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抓包情況如下:

DHCP,代碼實作主機動态配置協定IP租用請求和應答

我們在代碼中構造一個虛假mac位址,DHCP伺服器為該mac位址配置設定了一個可用IP是192.168.2.159,同時代碼發送一個DHCP_REQUEST消息,同時伺服器正常給我們傳回了DHCP_ACK,表示接受了我們對該IP的租用請求。

更詳細的講解和代碼調試示範過程,請點選連結

更多技術資訊,包括作業系統,編譯器,面試算法,機器學習,人工智能,請關照我的公衆号:

DHCP,代碼實作主機動态配置協定IP租用請求和應答

繼續閱讀