杰瑞科技汇

Python DHCP客户端如何配置与使用?

Of course! Creating a DHCP client in Python is an excellent way to understand networking protocols at a low level. We'll build a simple one from scratch using Python's built-in socket library.

Python DHCP客户端如何配置与使用?-图1
(图片来源网络,侵删)

What is a DHCP Client?

A DHCP (Dynamic Host Configuration Protocol) client is a program that automatically configures a device's network settings by requesting them from a DHCP server. The key settings it requests are:

  • IP Address
  • Subnet Mask
  • Default Gateway (Router)
  • DNS Server

The process happens through a series of messages, most commonly over UDP port 67 (server) and 68 (client).


The DHCP Process in a Nutshell

A standard DHCP client-server interaction involves four steps (often called the DORA process):

  1. Discover: The client broadcasts a "DHCP Discover" message to find any available DHCP servers on the network.
  2. Offer: A DHCP server that receives the Discover message responds with a "DHCP Offer". This offer includes an available IP address and other network configuration details.
  3. Request: The client then broadcasts a "DHCP Request" message, telling the server it wants to accept the specific offer it received.
  4. Acknowledge (ACK): The server finalizes the configuration by sending a "DHCP Acknowledge" message. The client can now use the assigned IP address.

We will simulate this entire process.

Python DHCP客户端如何配置与使用?-图2
(图片来源网络,侵删)

Prerequisites

You need to run this script on a network where you have a DHCP server running (e.g., your home or office router). You should not run this on a critical system, as it will attempt to acquire a new lease, potentially interfering with an existing one.


The Python Code

This script will construct and send the raw DHCP packets. It's a simplified version but captures the essence of the protocol.

import socket
import struct
import random
import time
import ipaddress
# --- DHCP Constants ---
# DHCP Message Types
DHCPDISCOVER = 1
DHCPOFFER    = 2
DHCPREQUEST  = 3
DHCPACK      = 5
DHCPNAK      = 6
DHCPRELEASE  = 7
# DHCP Options
DHCP_MESSAGE_TYPE = 53
DHCP_SERVER_ID    = 54  # IP address of the DHCP server
DHCP_LEASE_TIME   = 51
DHCP_SUBNET_MASK  = 1
DHCP_ROUTER       = 3
DHCP_DNS_SERVER   = 6
DHCP_END          = 255
# --- Network Configuration ---
# Use a broadcast address to find the DHCP server
BROADCAST_IP = '255.255.255.255'
DHCP_SERVER_PORT = 67
DHCP_CLIENT_PORT = 68
def create_dhcp_discover(xid):
    """
    Creates a DHCP Discover packet.
    """
    # Create a BOOTP header (the basis for DHCP)
    # See RFC 2131 for details on the BOOTP/DHCP header format
    bootp = struct.pack(
        '!BBBBHHHHH',
        1,          # op (1 = BOOTREQUEST)
        1,          # htype (1 = 10mb ethernet)
        6,          # hlen (6 bytes for MAC address)
        0,          # hops
        xid,        # transaction ID (a random number)
        0,          # seconds
        0,          # flags
        0,          # ciaddr (client IP - 0.0.0.0)
        0,          # yiaddr (your IP - 0.0.0.0)
        0,          # siaddr (server IP - 0.0.0.0)
        0,          # giaddr (relay agent IP - 0.0.0.0)
        # MAC address placeholder - will be filled by OS
        b'\x00' * 16 # chaddr (client hardware address)
    )
    # DHCP Options
    options = struct.pack(
        '!BB',
        DHCP_MESSAGE_TYPE,
        DHCPDISCOVER
    )
    options += struct.pack('!BB', DHCP_END, 0) # End of options + padding for BOOTP
    # The OS will fill in the MAC address in the chaddr field of the BOOTP header.
    # We just need to make sure the total length is correct.
    # A BOOTP packet is 300 bytes. Our bootp struct is 44 + 16 = 60.
    # The options field needs to be padded to 300 - 60 = 240 bytes.
    # However, the OS handles this. We just send the BOOTP header and options.
    # The total packet will be assembled by the socket sendto function.
    return bootp + options
def parse_dhcp_packet(data):
    """
    Parses a DHCP packet to extract the message type and server IP.
    """
    # Parse BOOTP header
    # op, htype, hlen, hops, xid, secs, flags, ciaddr, yiaddr, siaddr, giaddr, chaddr
    bootp_header = struct.unpack('!BBBBHHHHH16s208s', data[:236])
    op = bootp_header[0]
    server_ip = socket.inet_ntoa(bootp_header[9]) # siaddr
    # Parse DHCP Options
    options_data = data[240:]
    options = {}
    i = 0
    while i < len(options_data):
        opt_code = options_data[i]
        if opt_code == DHCP_END:
            break
        opt_len = options_data[i+1]
        opt_value = options_data[i+2:i+2+opt_len]
        options[opt_code] = opt_value
        i += 2 + opt_len
    message_type = options.get(DHCP_MESSAGE_TYPE, b'')[0] if DHCP_MESSAGE_TYPE in options else 0
    return {
        'op': op,
        'server_ip': server_ip,
        'message_type': message_type,
        'options': options
    }
def main():
    """
    Main function to run the DHCP client.
    """
    # Generate a random Transaction ID (XID) to identify our request
    xid = random.randint(1, 0xFFFFFFFF)
    print(f"Starting DHCP Client with Transaction ID: {xid:x}\n")
    # Create a UDP socket
    # We need to allow broadcasting
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    client_socket.bind(('', DHCP_CLIENT_PORT))
    client_socket.settimeout(10) # Wait for 10 seconds for a response
    # 1. DHCP DISCOVER
    print("1. Sending DHCP DISCOVER...")
    discover_packet = create_dhcp_discover(xid)
    client_socket.sendto(discover_packet, (BROADCAST_IP, DHCP_SERVER_PORT))
    # 2. DHCPOFFER
    try:
        print("   Waiting for DHCPOFFER...")
        offer_data, server_addr = client_socket.recvfrom(1024)
        offer = parse_dhcp_packet(offer_data)
        if offer['message_type'] == DHCPOFFER:
            print(f"   Received DHCPOFFER from server: {offer['server_ip']}")
            server_ip = offer['server_ip']
            # Extract the offered IP address from the yiaddr field
            offered_ip = socket.inet_ntoa(offer_data[16:20])
            print(f"   Offered IP Address: {offered_ip}")
        else:
            print("   Received unexpected message type. Exiting.")
            return
    except socket.timeout:
        print("   Timeout waiting for DHCPOFFER. Exiting.")
        return
    except Exception as e:
        print(f"   An error occurred: {e}")
        return
    # 3. DHCP REQUEST
    print("\n2. Sending DHCP REQUEST to accept the offer...")
    # For simplicity, we will reuse the discover packet and just change the message type.
    # A real client would build a new request packet with more details.
    request_packet = create_dhcp_discover(xid)
    # Overwrite the message type option
    # Find the start of the options section and replace the type
    request_packet = request_packet[:240] + struct.pack('!BB', DHCP_MESSAGE_TYPE, DHCPREQUEST) + request_packet[242:]
    # The request is sent to the specific server that made the offer
    client_socket.sendto(request_packet, (server_ip, DHCP_SERVER_PORT))
    # 4. DHCP ACK
    try:
        print("   Waiting for DHCP ACK...")
        ack_data, server_addr = client_socket.recvfrom(1024)
        ack = parse_dhcp_packet(ack_data)
        if ack['message_type'] == DHCPACK:
            print(f"   Received DHCP ACK from server: {ack['server_ip']}")
            # Parse the offered IP from the yiaddr field
            assigned_ip = socket.inet_ntoa(ack_data[16:20])
            print(f"\n   --- Configuration Successful ---")
            print(f"   Assigned IP Address:   {assigned_ip}")
            # Parse other options
            if DHCP_LEASE_TIME in ack['options']:
                lease_time = int.from_bytes(ack['options'][DHCP_LEASE_TIME], 'big')
                print(f"   Lease Time:           {lease_time} seconds")
            if DHCP_SUBNET_MASK in ack['options']:
                subnet_mask = socket.inet_ntoa(ack['options'][DHCP_SUBNET_MASK])
                print(f"   Subnet Mask:          {subnet_mask}")
            if DHCP_ROUTER in ack['options']:
                router_ip = socket.inet_ntoa(ack['options'][DHCP_ROUTER])
                print(f"   Default Gateway:      {router_ip}")
            if DHCP_DNS_SERVER in ack['options']:
                dns_ip = socket.inet_ntoa(ack['options'][DHCP_DNS_SERVER])
                print(f"   DNS Server:           {dns_ip}")
            print("   ------------------------------")
        elif ack['message_type'] == DHCPNAK:
            print("   Received DHCP NAK (Negative Acknowledgement). The server denied the request.")
        else:
            print("   Received unexpected message type. Exiting.")
    except socket.timeout:
        print("   Timeout waiting for DHCP ACK. Exiting.")
    except Exception as e:
        print(f"   An error occurred: {e}")
    finally:
        client_socket.close()
if __name__ == "__main__":
    main()

How to Run the Script

  1. Save the code above as a Python file (e.g., dhcp_client.py).
  2. Open your terminal or command prompt.
  3. Run the script: python dhcp_client.py

You should see output similar to this (your IPs and server address will be different):

Starting DHCP Client with Transaction ID: 5a1b2c3d
1. Sending DHCP DISCOVER...
   Waiting for DHCPOFFER...
   Received DHCPOFFER from server: 192.168.1.1
   Offered IP Address: 192.168.1.105
2. Sending DHCP REQUEST to accept the offer...
   Waiting for DHCP ACK...
   Received DHCP ACK from server: 192.168.1.1
   --- Configuration Successful ---
   Assigned IP Address:   192.168.1.105
   Lease Time:           86400 seconds
   Subnet Mask:          255.255.255.0
   Default Gateway:      192.168.1.1
   DNS Server:           8.8.8.8
   ------------------------------

Important Considerations and Limitations

  1. MAC Address: The script relies on the underlying operating system to fill in the MAC address (the chaddr field in the BOOTP header). We don't need to manually set it.
  2. Simplified Packet Creation: The create_dhcp_discover function reuses the BOOTP header for both the DISCOVER and REQUEST packets. A full-fledged client would build more distinct REQUEST packets, often including the "server identifier" option to specify which offer it's accepting.
  3. No RELEASE: This client does not send a DHCPRELEASE packet when it's done. It's a "fire and forget" client.
  4. No Renew/Rebind: It only performs the initial DORA process. It doesn't handle renewing the lease before it expires.
  5. Permissions: On some systems (especially Linux), you might need root/administrator privileges to create a socket that can send and receive on raw-like ports or to bind to a low port. You may need to run sudo python dhcp_client.py.
  6. Network Interference: As mentioned, running this will make your machine attempt to get a new IP. It might briefly disconnect you from your network if you already have a lease from the same server. Use it in a safe environment.
分享:
扫描分享到社交APP
上一篇
下一篇