我們在上一節中完成了用戶端請求和伺服器應答的第一個步驟。用戶端發出DHCP_DISCOVER消息,區域網路内的所有DHCP伺服器發出DHCP_OFFER消息,在該消息中包含一個特殊字段叫Your_IP_Address,這是伺服器配置設定給用戶端的IP位址,如下圖:
用戶端可能會同時受到多個DHCP伺服器發送的回應,然後它從中選擇一個伺服器發送過來的IP位址,并構造一個DHCP_REQUEST發送給對方,在資料包的Options字段中,使用一個Option表示它向伺服器請求該IP位址:
然後伺服器會向用戶端發送DHCP_ACK消息表示确認用戶端的租借請求:
完成了上面步驟後,伺服器會記錄用戶端硬體位址與租借位址的對應關系,用戶端在租用IP後,想要續租時,依舊會與給定伺服器交流,在續租時,它會向綁定伺服器發送DHCP_REQUEST消息,在消息中附帶續租時常,同時如果允許續租的話,伺服器會向用戶端發送DHCP_ACK消息,在消息裡附帶了伺服器允許用戶端繼續租用給定IP的時間。
如果用戶端需要離開網絡,不再使用給定IP,它會向伺服器發送DHCP_RELEASE消息表示放棄目前使用的IP,如此伺服器就能回收IP資源,将寶貴的IP分發給其他需要的用戶端,DHCP_RELEASE消息中包含了用戶端目前使用的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抓包情況如下:
我們在代碼中構造一個虛假mac位址,DHCP伺服器為該mac位址配置設定了一個可用IP是192.168.2.159,同時代碼發送一個DHCP_REQUEST消息,同時伺服器正常給我們傳回了DHCP_ACK,表示接受了我們對該IP的租用請求。
更詳細的講解和代碼調試示範過程,請點選連結
更多技術資訊,包括作業系統,編譯器,面試算法,機器學習,人工智能,請關照我的公衆号: