美文网首页
Python网络编程5-实现DHCP Client

Python网络编程5-实现DHCP Client

作者: 净坛使者_猪悟能 | 来源:发表于2021-08-15 10:04 被阅读0次

      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字段中的配置信息。

    DHCP option
    Options字段由Type、Length和Value三部分组成。

    1.2 工作原理

    获取IP地址过程

    DHCP 获取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交互报文。

    相关文章

      网友评论

          本文标题:Python网络编程5-实现DHCP Client

          本文链接:https://www.haomeiwen.com/subject/dxkmbltx.html