Python网络编程5-实现DHCP Client
DHCP(Dynamic Host Configuration Protocol,动态主机配置协议),前身是BOOTP协议,是一个局域网的网络协议,使用UDP协议工作,统一使用两个IANA分配的端口:67(服务器端),68(客户端)。主要作用是集中的管理、分配IP地址,使client动态的获得IP地址、Gateway地址、DNS服务器地址等信息。
一、DHCP报文格式
DHCP报文格式1.1报文字段解释
- op:报文的操作类型。分为请求报文和响应报文。1:请求报文,即client送给server的封包 2:应答报文。
- 请求报文:DHCP Discover、DHCP Request、DHCP Release、DHCP Inform和DHCP Decline。
- 应答报文:DHCP Offer、DHCP ACK和DHCP NAK。
- htype:DHCP客户端的MAC地址类型。MAC地址类型其实是指明网络类型。
- htype值为1时表示为最常见的以太网MAC地址类型。
- hlen:DHCP客户端的MAC地址长度。
- hlen值为6时表示为以太网。以太网MAC地址长度为6个字节
- hops:DHCP报文经过的DHCP中继的数目,默认为0。
- DHCP请求报文每经过一个DHCP中继,该字段就会增加1。没有经过DHCP中继时值为0。(若数据包需经过router传送,每站加1,若在同一网内,为0。)
- xid:相当于请求标识。用来标识一次IP地址请求过程。
- 客户端通过DHCP Discover报文发起一次IP地址请求时选择的随机数,在一次请求中所有报文的Xid都是一样的。
- secs:DHCP客户端从获取到IP地址或者续约过程开始到现在所消耗的时间,以秒为单位。
- 在没有获得IP地址前该字段始终为0。(DHCP客户端开始DHCP请求后所经过的时间。目前尚未使用,固定为0。)
- flags:标志位,只使用第0比特位,用来标识DHCP服务器应答报文是采用单播还是广播发送
- 0 表示采用单播发送方式,
- 1 表示采用广播发送方式。
- 其余位尚未使用。(即从0-15bits,最左1bit为1时表示server将以广播方式传送封包给client。)
- ciaddr:DHCP客户端的IP地址。
- 仅在DHCP服务器发送的ACK报文中显示,因为在得到DHCP服务器确认前,DHCP客户端是还没有分配到IP地址的。
- yiaddr:DHCP服务器分配给客户端的IP地址。
- 仅在DHCP服务器发送的Offer和ACK报文中显示,其他报文中显示为0。
- siaddr:下一个为DHCP客户端分配IP地址等信息的DHCP服务器IP地址。
- 仅在DHCP Offer、DHCP ACK报文中显示,其他报文中显示为0。(用于bootstrap过程中的IP地址)
- 一般来说是服务器的ip地址,通常认为option>srever_id字段为真正的服务器ip,siaddr有可能是多次路由跳转中的某一个路由的ip。
- giaddr:DHCP客户端发出请求报文后经过的第一个DHCP中继的IP地址。
- 如果没有经过DHCP中继,则显示为0。(转发代理(网关)IP地址)
- chaddr:DHCP客户端的MAC地址。
- 在每个报文中都会显示对应DHCP客户端的MAC地址。
- sname:为DHCP客户端分配IP地址的DHCP服务器名称(DNS域名格式)。
- 在Offer和ACK报文中显示发送报文的DHCP服务器名称,其他报文显示为0。
- file:DHCP服务器为DHCP客户端指定的启动配置文件名称及路径信息。
- 仅在DHCP Offer报文中显示,其他报文中显示为空。
- options:可选项字段,长度可变,格式为"代码+长度+数据"。
option字段
DHCP报文中的Options字段可以用来存放普通协议中没有定义的控制信息和参数。如果用户在DHCP服务器端配置了Options字段,DHCP客户端在申请IP地址的时候,会通过服务器端回应的DHCP报文获得Options字段中的配置信息。
Options字段由Type、Length和Value三部分组成。
1.2 工作原理
获取IP地址过程
-
发现过程
DHCP Client以广播的方式发送一个DHCP Discover消息,多个DHCP Server接收到PC发送的DHCP Discover消息,都会对所收到的DHCP Discover消息做出回应。 -
提供阶段
DHCP Server从地址池中选一个IP地址通过,DHCP Offer消息(单播)将这个IP地址发送给DHCP Client。 -
请求阶段
DHCP Client会以广播方式发送一个DHCP Request消息
1.其意图就是向路由器R上的DHCP Server提出请求,希望获取到该DHCP Server发送给自己的DHCP Offer消息中所提供的那个IP地址。
2.这个DHCP Request消息中携带有R上的DHCP Server的标识(称为Server Identifier),表示PC上的DHCP Client只愿意接受R上的DHCP Server所给出的Offer
3.其他的DHCP Server收到并分析了该DHCP Request消息后,会明白PC拒绝了自己的Offer。于是,这些DHCP Server就会收回自己当初给予PC的Offer -
确认阶段
DHCP Server会向DHCP Client发送一个DHCP Ack消息。DHCP Server也可能会向PC上的DHCP Client发送一个DHCP Nak消息。如果PC接收到了DHCP Nak消息,就说明这次获取IP地址的尝试失败了。在这种情况下, PC只能重新回到发现阶段来开始新一轮的IP地址申请过程。
二、实验环境
实验使用的linux 主机由两个网络接口,其中ens33使用DHCP获取IP地址,ens37使用静态IP地址;因此需要使用ens33来发送数据包。
实验环境
三、Python实现DHCP Client
3.1 Python脚本
Change_MAC.py用于MAC地址与Bytes类型相互转换。
#!/usr/bin/python3.4
# -*- coding=utf-8 -*-
import struct
def Change_Chaddr_To_MAC(chaddr):
'''转换16字节chaddr为MAC地址,前6字节为MAC'''
MAC_ADDR_INT_List = struct.unpack('>16B', chaddr)[:6]
MAC_ADDR_List = []
for MAC_ADDR_INT in MAC_ADDR_INT_List:
if MAC_ADDR_INT < 16:
MAC_ADDR_List.append('0' + str(hex(MAC_ADDR_INT))[2:])
else:
MAC_ADDR_List.append(str(hex(MAC_ADDR_INT))[2:])
MAC_ADDR = MAC_ADDR_List[0] + ':' + MAC_ADDR_List[1] + ':' + MAC_ADDR_List[2] + ':' + MAC_ADDR_List[3] + ':' + MAC_ADDR_List[4] + ':' + MAC_ADDR_List[5]
return MAC_ADDR
def Str_to_Int(string)
if ord(string[0]) > 90:
int1 = ord(string[0]) - 87
else:
int1 = ord(string[0]) - 48
if ord(string[1]) > 90:
int2 = ord(string[1]) - 87
else:
int2 = ord(string[1]) - 48
int_final = int1 * 16 + int2
return int_final
def Change_MAC_To_Bytes(MAC):
section1 = Str_to_Int(MAC.split(':')[0])
section2 = Str_to_Int(MAC.split(':')[1])
section3 = Str_to_Int(MAC.split(':')[2])
section4 = Str_to_Int(MAC.split(':')[3])
section5 = Str_to_Int(MAC.split(':')[4])
section6 = Str_to_Int(MAC.split(':')[5])
Bytes_MAC = struct.pack('!6B', section1, section2, section3, section4, section5, section6)
return Bytes_MAC
DHCP_Discover.py用于发送DHCP Discover报文;其中GET_MAC.py见ARP章节。
#!/usr/bin/python3.4
# -*- coding=utf-8 -*-
from kamene.all import *
from GET_MAC import get_mac_address
from Change_MAC import Change_MAC_To_Bytes
import time
def DHCP_Discover_Sendonly(ifname, MAC, wait_time = 1):
if wait_time != 0:
time.sleep(wait_time)
Bytes_MAC = Change_MAC_To_Bytes(MAC)#把MAC地址转换为二进制格式
#chaddr一共16个字节,MAC地址只有6个字节,所以需要b'\x00'*10填充到16个字节
#param_req_list为请求的参数,没有这个部分服务器只会回送IP地址,什么参数都不给
discover = Ether(dst='ff:ff:ff:ff:ff:ff', src=MAC, type=0x0800) \
/ IP(src='0.0.0.0', dst='255.255.255.255') \
/ UDP(dport=67,sport=68) \
/ BOOTP(op=1, chaddr=Bytes_MAC + b'\x00'*10) \
/ DHCP(options=[('message-type','discover'), ('param_req_list', b'\x01\x06\x0f,\x03!\x96+'), ('end')])
sendp(discover, iface = ifname, verbose=False)
else:
Bytes_MAC = Change_MAC_To_Bytes(MAC)
discover = Ether(dst='ff:ff:ff:ff:ff:ff', src=MAC, type=0x0800) \
/ IP(src='0.0.0.0', dst='255.255.255.255') \
/ UDP(dport=67,sport=68) \
/ BOOTP(op=1, chaddr=Bytes_MAC + b'\x00'*10) \
/ DHCP(options=[('message-type','discover'), ('param_req_list', b'\x01\x06\x0f,\x03!\x96+'), ('end')])
sendp(discover, iface = ifname, verbose=False)
DHCP_Request.py用于发送DHCP Request报文。
#!/usr/bin/python3.4
# -*- coding=utf-8 -*-
from kamene.all import *
import time
def DHCP_Request_Sendonly(ifname, options, wait_time = 1):
request = Ether(dst='ff:ff:ff:ff:ff:ff',src=options['MAC'],type=0x0800)\
/IP(src='0.0.0.0', dst='255.255.255.255')\
/UDP(dport=67,sport=68)\
/BOOTP(op=1,chaddr=options['client_id'] + b'\x00'*10,siaddr=options['Server_IP'],)\
/DHCP(options=[('message-type','request'),
('server_id', options['Server_IP']),
('requested_addr', options['requested_addr']),
('client_id', b'\x01' + options['client_id']),
('param_req_list', b'\x01\x06\x0f,\x03!\x96+'), ('end')])#’end‘作为结束符,方便后续程序读取
if wait_time != 0:
time.sleep(wait_time)
sendp(request, iface = ifname, verbose=False)
else:
sendp(request, iface = ifname, verbose=False)
DHCP_FULL.py用于完成DHCP Client与DHCP Server的报文交互
#!/usr/bin/python3.4
# -*- coding=utf-8 -*-
from kamene.all import *
import multiprocessing
from Change_MAC import Change_MAC_To_Bytes
from GET_MAC import get_mac_address
from Change_MAC import Change_Chaddr_To_MAC
from DHCP_Discover import DHCP_Discover_Sendonly
from DHCP_Request import DHCP_Request_Sendonly
def DHCP_Monitor_Control(pkt):
try:
if pkt.getlayer(DHCP).fields['options'][0][1]== 1:#发现并且打印DHCP Discover
print('发现DHCP Discover包,MAC地址为:',end='')
MAC_Bytes = pkt.getlayer(BOOTP).fields['chaddr']
MAC_ADDR = Change_Chaddr_To_MAC(MAC_Bytes)
print('Request包中发现如下Options:')
for option in pkt.getlayer(DHCP).fields['options']:
if option == 'end':
break
print('%-15s ==> %s' %(str(option[0]),str(option[1])))
elif pkt.getlayer(DHCP).fields['options'][0][1]== 2:#发现并且打印DHCP OFFER
options = {}
MAC_Bytes = pkt.getlayer(BOOTP).fields['chaddr']
MAC_ADDR = Change_Chaddr_To_MAC(MAC_Bytes)
#把从OFFER得到的信息读取并且写入options字典
options['MAC'] = MAC_ADDR
options['client_id'] = Change_MAC_To_Bytes(MAC_ADDR)
print('发现DHCP OFFER包,请求者得到的IP为:' + pkt.getlayer(BOOTP).fields['yiaddr'])
print('OFFER包中发现如下Options:')
for option in pkt.getlayer(DHCP).fields['options']:
if option == 'end':
break
print('%-15s ==> %s' %(str(option[0]),str(option[1])))
options['requested_addr'] = pkt.getlayer(BOOTP).fields['yiaddr']
for i in pkt.getlayer(DHCP).fields['options']:
if i[0] == 'server_id' :
options['Server_IP'] = i[1]
Send_Request = multiprocessing.Process(target=DHCP_Request_Sendonly, args=(Global_IF,options))
Send_Request.start()
elif pkt.getlayer(DHCP).fields['options'][0][1]== 3:#发现并且打印DHCP Request
print('发现DHCP Request包,请求的IP为:' + pkt.getlayer(BOOTP).fields['yiaddr'])
print('Request包中发现如下Options:')
for option in pkt.getlayer(DHCP).fields['options']:
if option == 'end':
break
print('%-15s ==> %s' %(str(option[0]),str(option[1])))
elif pkt.getlayer(DHCP).fields['options'][0][1]== 5:#发现并且打印DHCP ACK
print('发现DHCP ACK包,确认的IP为:' + pkt.getlayer(BOOTP).fields['yiaddr'])
print('ACK包中发现如下Options:')
for option in pkt.getlayer(DHCP).fields['options']:
if option == 'end':
break
print('%-15s ==> %s' %(str(option[0]),str(option[1])))
except Exception as e:
print(e)
pass
def DHCP_FULL(ifname, MAC, timeout = 10):
global Global_IF
Global_IF = ifname
Send_Discover = multiprocessing.Process(target=DHCP_Discover_Sendonly, args=(Global_IF,MAC))#执行多线程,target是目标程序,args是给目标闯入的参数
Send_Discover.start()
sniff(prn=DHCP_Monitor_Control, filter="port 68 and port 67", store=0, iface=Global_IF, timeout = timeout)#用于捕获DHCP交互的报文
if __name__ == '__main__':
ifname = 'ens33'
DHCP_FULL('ens33', get_mac_address(ifname))
3.2 执行效果
执行效果Wireshark对远程linux主机抓包,结果如下
客户端以广播发送DHCP Discover包,其中报文操作类型为1(请求报文),DHCP客户端的MAC地址设置为00:0c:29:03:a1:08,option53设置报文类型为Discover,option55(请求选项列表)中包含请求的参数。
DHCP Discover
服务器以单播向客户端回复信息,其中报文操作类型为2(应答报文),分配给客户端的IP为192.168.160.146,option 53设置报文类型为offer,Option 54设置服务器标识为192.168.160.254,其他option为客户端请求列表的应答。
DHCP Offer
客户端以广播发送Request报文,其中服务器标识为192.168.160.146(表明是给这台服务器的回复),确认请求的IP为192.168.160.146.
DHCP Request
服务器单播向客户端发送ACK报文,再次确认给其分配的IP为192.168.160.146,服务器标识为192.168.160.254.
DHCP ACK
值得注意的是,交互的四个报文中Transaction ID均为0x00000000,表明是同一次DHCP交互报文。