📜 ⬆️ ⬇️

DHCP + Mysql server in Python



The purpose of this project was:


Result: works;) Tested on FreeBSD OS and Ubuntu. Theoretically, the code can be asked to work under any OS, since There are no specific bindings in the code.
Caution! Then a lot.

Link to the repository for fans of "touch alive . "
')
The process of installing, configuring, and using the result of “studying the materiel” is much lower, and then a little bit of theory using the DHCP protocol. For myself. And for the story;)

Little theory


What is DHCP?


This is a network protocol that allows a device to find out its IP address (well, other parameters like a gateway, DNS, etc.) from a DHCP server. Packet exchange is via UDP protocol. The general principle of operation of the device when requesting network parameters is as follows:

  1. The device (client) sends a broadcast UDP request (DHCPDISCOVER) throughout the network with the request "well, someone, give me an IP address." And usually (but not always) the request occurs from port 68 (source), and the destination is port 67 (destination). Some devices also send packets from port 67. Inside the DHCPDISCOVER packet, the MAC address of the client device is included.
  2. All DHCP servers that are on the network (and there may be several) form the DHCPOFFER offer with network settings for the device that sent the DHCPDISCOVER, and also broadcast it over the network. Identification to whom this packet is intended goes to the MAC address of the client provided earlier in the DHCPDISCOVER request.
  3. The client accepts packets with network settings, selects the most attractive (criteria may be different, for example, including packet delivery time, number of intermediate routes), and makes the DHCPREQUEST “official request” with network settings for the DHCP server you like. In this case, the packet goes to a specific DHCP server.
  4. The server that received the DHCPREQUEST sends a packet in the DHCPACK format, in which it once again lists the network settings intended for this client



In addition, there are DHCPINFORM packages that come from the client, and the purpose of which is to inform the DHCP server that the “client is alive” and uses the issued network settings. In the implementation of this server, these packages are ignored.

Package format



In general, an Ethernet packet frame looks like this:



In our case, we consider only the data directly from the contents of the UDP packet, without the protocol headers of the OSI layers, namely the DHCP structure:

DHCPDISCOVER


So, the process of obtaining an IP address for a device begins with the fact that a DHCP client sends a broadcast request from port 68 to 255.255.255.255:67. In this package, the client includes its MAC address, as well as what exactly it wants to receive from the DHCP server. The package structure is described in the table below.

DHCPDISCOVER package structure table
Package PositionName of the valueExampleRepresentationByteExplanation
oneBoot requestoneHexoneMessage type 1 - request from client to server, 2 - response from server to client
2Hardware typeoneHexoneType of hardware address in this protocol 1 - MAC
3Hardware adrees length6HexoneMAC address length
fourHopsoneHexoneNumber of intermediate routes
fiveTransaction ID23: cf: de: 1dHexfourThe unique identifier of the transaction. Generates the client at the beginning of the request operation
7Second elapsed0HexfourTime in seconds since the beginning of the process of obtaining the address
9Bootp flags0Hex2Some flags that can be set, as an indication of the parameters of the protocol
elevenClient IP address0.0.0.0LinefourClient IP address (if any)
15Your client IP address0.0.0.0LinefourIP address offered by the server (if any)
nineteenNext server IP address0.0.0.0LinefourServer IP (if known)
23Relay agent IP address172.16.114.41LinefourIP address of relay agent (for example, switch)
27Client MAC address14: d6: 4d: a7: c9: 55Hex6MAC address of the sender of the packet (client)
31Client hardware address paddingHextenReserved place. Usually filled with zeros
41Server host nameLine64The name of the DHCP server. Normally not transmitted
105Boot file nameLine128The file name on the server used by diskless stations when booting
235Magic cookie63: 82: 53: 63HexfourThe "magic" number by which including it can be determined that this packet belongs to the DHCP protocol
DHCP options. Can go in any order
236Option number53DeconeOption 53 defining the type of DHCP packet

1 - DHCPDISCOVER
3 - DHCPREQUEST
2 - DHCPOFFER
5 - DHCPACK
8 - DHCPINFORM
Option lengthoneDecone
Option valueoneDecone
Option number50DeconeWhat IP address does the client want to receive
Option lengthfourDecone
Option value172.16.134.61Linefour
Option number55oneThe network parameters requested by the client. The composition may be different

01 - net mask
03 - Gateway
06 - DNS
oc - Host Name
0f - network domain name
1c - address of the broadcast request (Broadcast)
42 - TFTP server name
79 - Classless Static Route
Option lengtheightone
Option value01: 03: 06: 0c: 0f: 1c: 42: 79eight
Option number82DecOption 82, in which the MAC address of the repeater device and some additional values ​​are transmitted.

Most often, the switch port on which the final DHCP client is running. This option is “nested” with additional parameters. The first byte is the “suboption” number, the second is its length, then its value.

In this case, in option 82, the suboptions are nested:
Agent Circuit ID = 00: 04: 00: 01: 00: 04, where the last two bytes are the DHCP client port from which the request came

Agent Remote ID = 00: 06: c8: be: 19: 93: 11: 48 - the MAC address of the device of the DHCP relay
Option length18Dec
Option value01:06
00: 04: 00: 01: 00: 04
02:08
00: 06: c8: be: 19: 93: 11: 48
Hex
End of package255Decone255 symbolizes the end of the pack.


DHCPOFFER


As soon as the server receives the DHCPDISCOVER packet and if it sees that it can offer the client something from the requested one, then it forms the answer for it - DHCPOFFER. The answer is sent to the port “from where it came”, by Broadcast, since At this moment, the client does not yet have an IP address, therefore he can only accept the packet if he has been sent by broadcast. The client recognizes that this is a package for him by his MAC address inside the package, as well as the transaction number that he generates at the time of the creation of the first package.

DHCPOFFER packet structure table
Package PositionName of value (common)ExampleRepresentationByteExplanation
oneBoot requestoneHexoneMessage type 1 - request from client to server, 2 - response from server to client
2Hardware typeoneHexoneType of hardware address in this protocol 1 - MAC
3Hardware adrees length6HexoneMAC address length
fourHopsoneHexoneNumber of intermediate routes
fiveTransaction ID23: cf: de: 1dHexfourThe unique identifier of the transaction. Generates the client at the beginning of the request operation
7Second elapsed0HexfourTime in seconds since the beginning of the process of obtaining the address
9Bootp flags0Hex2Some flags that can be set, as an indication of the parameters of the protocol. In this case, 0 indicates the type of Unicast request.
elevenClient IP address0.0.0.0LinefourClient IP address (if any)
15Your client IP address172.16.134.61LinefourIP address offered by the server (if any)
nineteenNext server IP address0.0.0.0LinefourServer IP (if known)
23Relay agent IP address172.16.114.41LinefourIP address of relay agent (for example, switch)
27Client MAC address14: d6: 4d: a7: c9: 55Hex6MAC address of the sender of the packet (client)
31Client hardware address paddingHextenReserved place. Usually filled with zeros
41Server host nameLine64The name of the DHCP server. Normally not transmitted
105Boot file nameLine128The file name on the server used by diskless stations when booting
235Magic cookie63: 82: 53: 63HexfourThe "magic" number by which including it can be determined that this packet belongs to the DHCP protocol
DHCP options. Can go in any order
236Option number53DeconeOption 53 defining DHCP 2 packet type - DHCPOFFER
Option lengthoneDecone
Option value2Decone
Option numberoneDeconeOption offering DHCP client network mask
Option lengthfourDecone
Option value255.255.224.0Linefour
Option number3DeconeOption offering DHCP client default gateway
Option lengthfourDecone
Option value172.16.12.1Linefour
Option number6DeconeOption offering DHCP client DNS
Option lengthfourDecone
Option value8.8.8.8Linefour
Option number51DeconeThe lifetime of the issued network parameters in seconds after which the DHCP client must request them again
Option lengthfourDecone
Option value86400Decfour
Option number82DeconeOption 82, repeats what came in DHCPDISCOVER
Option length18Decone
Option value01: 08: 00: 06: 00
01: 01: 00: 00: 01
02: 06: 00: 03: 0f
26: 4d: ec
Dec18
End of package255Decone255 symbolizes the end of the pack.


DHCPREQUEST


After the client receives a DHCPOFFER, it forms a packet requesting the network parameters not to all DHCP servers on the network, but only to one specific one, the DHCPOFFER offer of which he liked the most. The “liked” criteria may be different and depend on the implementation of the DHCP client. The recipient of the request is indicated by the MAC address of the DHCP server. Also, the DHCPREQUEST packet can be sent by the client without having previously formed a DHCPDISCOVER if the server’s IP address was previously received.

DHCPREQUEST packet structure table
Package PositionName of value (common)ExampleRepresentationByteExplanation
oneBoot requestoneHexoneMessage type 1 - request from client to server, 2 - response from server to client
2Hardware typeoneHexoneType of hardware address in this protocol 1 - MAC
3Hardware adrees length6HexoneMAC address length
fourHopsoneHexoneNumber of intermediate routes
fiveTransaction ID23: cf: de: 1dHexfourThe unique identifier of the transaction. Generates the client at the beginning of the request operation
7Second elapsed0HexfourTime in seconds since the beginning of the process of obtaining the address
9Bootp flags8,000Hex2Some flags that can be set, as an indication of the protocol parameters. In this case, “Broadcast” is set.
elevenClient IP address0.0.0.0LinefourClient IP address (if any)
15Your client IP address172.16.134.61LinefourIP address offered by the server (if any)
nineteenNext server IP address0.0.0.0LinefourServer IP (if known)
23Relay agent IP address172.16.114.41LinefourIP address of relay agent (for example, switch)
27Client MAC address14: d6: 4d: a7: c9: 55Hex6MAC address of the sender of the packet (client)
31Client hardware address paddingHextenReserved place. Usually filled with zeros
41Server host nameLine64The name of the DHCP server. Normally not transmitted
105Boot file nameLine128The file name on the server used by diskless stations when booting
235Magic cookie63: 82: 53: 63HexfourThe "magic" number by which including it can be determined that this packet belongs to the DHCP protocol
DHCP options. Can go in any order
236Option number53Dec3Option 53 defining DHCP 3 packet type - DHCPREQUEST
Option lengthoneDecone
Option value3Decone
Option number61DeconeClient ID: 01 (for Ehernet) + client MAC address
Option length7Decone
Option value01: 2c: ab: 25: ff: 72: a6Hex7
Option number60Dec"Vendor class identifier". In my case, it reports the DHCP client version. Perhaps other devices return something else. Windows for example reports MSFT 5.0
Option lengthelevenDec
Option valueudhcp 0.9.8Line
Option number55oneThe network parameters requested by the client. The composition may be different

01 - net mask
03 - Gateway
06 - DNS
oc - Host Name
0f - network domain name
1c - address of the broadcast request (Broadcast)
42 - TFTP server name
79 - Classless Static Route
Option lengtheightone
Option value01: 03: 06: 0c: 0f: 1c: 42: 79eight
Option number82DeconeOption 82, repeats what came in DHCPDISCOVER
Option length18Decone
Option value01: 08: 00: 06: 00
01: 01: 00: 00: 01
02: 06: 00: 03: 0f
26: 4d: ec
Dec18
End of package255Decone255 symbolizes the end of the pack.


DHCPACK


As a confirmation of the fact that “yes for sure, this is your IP address, and I will not give it out to anyone else” from a DHCP server, the packet is in DHCPACK format from the server to the client. He is the same as the rest of the packages sent to the broadcast. Although, in the code below, the DHCP server is implemented in Python, I just in case duplicate any broadcast request by sending a packet to a specific client IP, if it is already known. Moreover, the DHCP server does not care at all whether the DHCPACK packet reached the client. If the client does not receive a DHCPACK, then after a while it simply repeats the DHCPREQUEST

DHCPACK packet structure table
Package PositionName of value (common)ExampleRepresentationByteExplanation
oneBoot request2HexoneMessage type 1 - request from client to server, 2 - response from server to client
2Hardware typeoneHexoneType of hardware address in this protocol 1 - MAC
3Hardware adrees length6HexoneMAC address length
fourHopsoneHexoneNumber of intermediate routes
fiveTransaction ID23: cf: de: 1dHexfourThe unique identifier of the transaction. Generates the client at the beginning of the request operation
7Second elapsed0HexfourTime in seconds since the beginning of the process of obtaining the address
9Bootp flags8,000Hex2Some flags that can be set, as an indication of the protocol parameters. In this case, “Broadcast” is set.
elevenClient IP address0.0.0.0LinefourClient IP address (if any)
15Your client IP address172.16.134.61LinefourIP address offered by the server (if any)
nineteenNext server IP address0.0.0.0LinefourServer IP (if known)
23Relay agent IP address172.16.114.41LinefourIP address of relay agent (for example, switch)
27Client MAC address14: d6: 4d: a7: c9: 55Hex6MAC address of the sender of the packet (client)
31Client hardware address paddingHextenReserved place. Usually filled with zeros
41Server host nameLine64The name of the DHCP server. Normally not transmitted
105Boot file nameLine128The file name on the server used by diskless stations when booting
235Magic cookie63: 82: 53: 63HexfourThe "magic" number by which including it can be determined that this packet belongs to the DHCP protocol
DHCP options. Can go in any order
236Option number53Dec3Option 53 defining the type of DHCP packet 5 - DHCPACK
Option lengthoneDecone
Option valuefiveDecone
Option numberoneDeconeOption offering DHCP client network mask
Option lengthfourDecone
Option value255.255.224.0Linefour
Option number3DeconeOption offering DHCP client default gateway
Option lengthfourDecone
Option value172.16.12.1Linefour
Option number6DeconeOption offering DHCP client DNS
Option lengthfourDecone
Option value8.8.8.8Linefour
Option number51DeconeThe lifetime of the issued network parameters in seconds after which the DHCP client must request them again
Option lengthfourDecone
Option value86400Decfour
Option number82DeconeOption 82, repeats what came in DHCPDISCOVER
Option length18Decone
Option value01: 08: 00: 06: 00
01: 01: 00: 00: 01
02: 06: 00: 03: 0f
26: 4d: ec
Dec18
End of package255Decone255 symbolizes the end of the pack.



Installation


Installation actually consists in installation of the python modules necessary for work. It is assumed that MySQL is already installed and configured.

Freebsd


  pkg install python3
 python3 -m ensurepip
 pip3 install mysql-connector 

Ubuntu


  sudo apt-get install python3
 sudo apt-get install pip3
 sudo pip3 install mysql-connector 

We create a MySQL database, upload the pydhcp.sql dump into it, configure the configuration file.

Configuration


All server settings are in the xml file. Reference file:

  <? xml version = "1.0"?>
 <config>
     <dhcpserver>
	 <host> 0.0.0.0 </ host>
         <broadcast> 255.255.255.255 </ broadcast>
         <DHCPServer> 192.168.0.71 </ DHCPServer>
	 <LeaseTime> 8600 </ LeaseTime>
	 <ThreadLimit> 1 </ ThreadLimit>
         <defaultMask> 255.255.255.0 </ defaultMask>
         <defaultRouter> 192.168.0.1 </ defaultRouter>
         <defaultDNS> 8.8.8.8 </ defaultDNS>
     </ dhcpserver>
     <mysql>
         <host> localhost </ host>
	 <username> test </ username>
	 <password> test </ password>
	 <basename> pydhcp </ basename>
     </ mysql>
     <options>
        <option> option_82_hex: sw_port1: 20: 22 </ option>       
        <option> option_82_hex: sw_port2: 16: 18 </ option>       
        <option> option_82_hex: sw_mac: 26: 40 </ option>
     </ options>    
     <query>
         <offer_count> 3 </ offer_count>
	 <offer_1> select ip, mask, router, dns from users where upper (mac) = upper ('{option_82_AgentRemoteId_hex}') and upper (port) = upper ('{option_82_AgentCircuitId_port_hex}') </ offer_1>
         <offer_2> select ip, mask, router, dns from users where upper (mac) = upper ('{sw_mac}') and upper (port) = upper ('{sw_port2}') </ offer_2>
         <offer_3> select ip, mask, router, dns from users where upper (mac) = upper ('{ClientMacAddress}') </ offer_3>
	 <history_sql> insert into history (id, dt, mac, ip, comment) values ​​(null, now (), '{ClientMacAddress}', '{RequestedIpAddress}', 'DHCPACK / INFORM') </ history_sql>
     </ query>
 </ config> 

Now more detail on the tags:

The dhcpserver section describes the basic settings for running the server, namely:

Mysql section:

host, username, password, basename - everything speaks for itself. Sample database structure is laid out on GitHub

Query section: requests for getting OFFER / ACK are described here:


Requests can include any variables from the options section or options from the DHCP protocol.

Section options. Here it is more interesting. Here we can create variables that can be used later in the query section.

For example:

option_82_hex:sw_port1:20:22 

, this line is a command to take the entire line that came in option 82 DHCP request, in hex format, in the range from 20 to 22 bytes inclusively and put it in the new variable sw_port1 (switch port where the request came from)

 option_82_hex:sw_mac:26:40 

, we define the variable sw_mac, taking hex from the range 26:40

You can see all possible options that can be used in queries by starting the server with the -d switch. We will see something like this log:

  - the DHCPINFORM package came to port 67, from 0025224ad764, b '\ x91 \ xa5 \ xe0 \ xa3 \ xa5 \ xa9- \ x8f \ x8a', ('172.30.114.25', 68)
 {'ClientMacAddress': '0025224ad764',
  'ClientMacAddressByte': b '\ x00%' J \ xd7d ',
  'HType': 'Ethernet',
  'HostName': b '\ x91 \ xa5 \ xe0 \ xa3 \ xa5 \ xa9- \ x8f \ x8a',
  'ReqListDNS': True,
  'ReqListDomainName': True,
  'ReqListPerfowmRouterDiscover': True,
  'ReqListRouter': True,
  'ReqListStaticRoute': True,
  'ReqListSubnetMask': True,
  'ReqListVendorSpecInfo': 43,
  'RequestedIpAddress': '0.0.0.0',
  'Vendor': b'MSFT 5.0 ',
  'chaddr': '0025224ad764',
  'ciaddr': '172.30.128.13',
  'flags': b '\ x00 \ x00',
  'giaddr': '172.30.114.25',
  'gpoz': 308,
  'hlen': 6,
  'hops': 1,
  'htype': 'MAC',
  'magic_cookie': b'c \ x82Sc ',
  'op': 'DHCPINFORM',
  'option12': 12,
  'option53': 53,
  'option55': 55,
  'option60': 60,
  'option61': 61,
  'option82': 82,
  'option_82_byte': b '\ x12 \ x01 \ x06 \ x00 \ x04 \ x00 \ x01 \ x00 \ x06 \ x02 \ x08 \ x00'
                    b '\ x06 \ x00 \ x1eX \ x9e \ xb2 \ xad',
  'option_82_hex': '12010600040001000602080006001e589eb2ad',
  'option_82_len': 18,
  'option_82_str': "b '\\ x12 \\ x01 \\ x06 \\ x00 \\ x04 \\ x00 \\ x01 \\ x00 \\ x06 \\ x02 \\ x08 \\ x00 \\ x06 \\ x00 \ \ x1eX \\ x9e \\ xb2 \\ xad '",
  'result': False,
  'secs': 768,
  'siaddr': '0.0.0.0',
  'sw_mac': '001e589eb2ad',
  'sw_port1': '06',
  'xidbyte': b '<\ x89} \ x8c',
  'xidhex': '3c897d8c',
  'yiaddr': '0.0.0.0'} 

Accordingly, we can wrap any variable in {} and it will be used in the SQL query.

Let's record for the history that the client’s IP address was:





Server startup


./pydhcpdb.py -d -c config.xml

- d. DEBUG console output mode
- c <file_name> configuration file

Debriefing


And now more about the implementation of the server in Python. It is a pain. Python was studied on the fly. Many moments are made in the style: “wow, somehow did what works.” Not optimized at all, and left in this form mainly due to the small experience of development in python. I will dwell on the most interesting points of the server implementation in the “code”.

XML configuration file parser


The standard Python xml.dom module is used. It seems to be simple, but the implementation did not have enough sensible documentation and examples on the network using this module.

  tree = minidom.parse (gconfig ["config_file"])
     mconfig = tree.getElementsByTagName ("mysql")
     for elem in mconfig:        
         gconfig ["mysql_host"] = elem.getElementsByTagName ("host") [0] .firstChild.data      
         gconfig ["mysql_username"] = elem.getElementsByTagName ("username") [0] .firstChild.data      
         gconfig ["mysql_password"] = elem.getElementsByTagName ("password") [0] .firstChild.data      
         gconfig ["mysql_basename"] = elem.getElementsByTagName ("basename") [0] .firstChild.data      
     dconfig = tree.getElementsByTagName ("dhcpserver")
     for elem in dconfig:        
         gconfig ["broadcast"] = elem.getElementsByTagName ("broadcast") [0] .firstChild.data      
         gconfig ["dhcp_host"] = elem.getElementsByTagName ("host") [0] .firstChild.data      
         gconfig ["dhcp_LeaseTime"] = elem.getElementsByTagName ("LeaseTime") [0] .firstChild.data      
         gconfig ["dhcp_ThreadLimit"] = int (elem.getElementsByTagName ("ThreadLimit") [0] .firstChild.data)              
         gconfig ["dhcp_Server"] = elem.getElementsByTagName ("DHCPServer") [0] .firstChild.data              
         gconfig ["dhcp_defaultMask"] = elem.getElementsByTagName ("defaultMask") [0] .firstChild.data              
         gconfig ["dhcp_defaultRouter"] = elem.getElementsByTagName ("defaultRouter") [0] .firstChild.data              
         gconfig ["dhcp_defaultDNS"] = elem.getElementsByTagName ("defaultDNS") [0] .firstChild.data              
     qconfig = tree.getElementsByTagName ("query")
     for elem in qconfig:  
         gconfig ["offer_count"] = elem.getElementsByTagName ("offer_count") [0] .firstChild.data                          
         for num in range (int (gconfig ["offer_count"])):
             gconfig ["offer _" + str (num + 1)] = elem.getElementsByTagName ("offer _" + str (num + 1)) [0] .firstChild.data      
         gconfig ["history_sql"] = elem.getElementsByTagName ("history_sql") [0] .firstChild.data                          
     options = tree.getElementsByTagName ("options")       
     for elem in options:          
         node = elem.getElementsByTagName ("option")
         for options in node:
             optionsMod.append (options.firstChild.data) 

Multithreading


Oddly enough, multithreading in Python is implemented very clearly and simply.

  def PacketWork (data, addr): 
 ...
 # implementation of parsing the incoming packet, and the answer to it
 ...


 while true:
     data, addr = udp_socket.recvfrom (1024) # waiting for the UDP packet
     thread = threading. Thread (target = PacketWork, args = (data, addr,)). start () # how come - run in the background the previously defined PacketWork function with parameters
     while threading.active_count ()> gconfig ["dhcp_ThreadLimit"]:
        time.sleep (1) # if the number of threads already running is greater than in the settings, wait until they become fewer 


Receive / Send DHCP Package


In order to intercept UDP packets going through a network card, you need to "raise" the socket:
  udp_socket = socket.socket (socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
 udp_socket.bind ((gconfig ["dhcp_host"], 67)) 

where flags are:


Sending a package can be as Broadcast:

  udp_socket.setsockopt (socket.SOL_SOCKET, socket.SO_BROADCAST, 1) # switch the socket to send broadcast mode
                     rz = udp_socket.sendto (packetack, (gconfig ["broadcast"], 68)) 

, and to the address, "where the package came from":
  udp_socket.setsockopt (socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # switch the socket to the "many listeners" mode
                         rz = udp_socket.sendto (packetack, addr) 

where SOL_SOCKET means “protocol level” for setting options,

, SO_BROADCAST option that package helmet "Broadcast"

The SO_REUSEADDR option switches the socket to the multi-listener mode. In theory, it is unnecessary in this case, but on one of the FreeBSD servers on which it was tested, the code did not work without this option.

DHCP packet parsing


This is where I really liked Python. It turns out from the "box", he allows quite freely with the byte-code. Allowing it to be very simple to translate into decimal values, strings and hex — that is, what we actually need to understand the package structure. So for example you can get a range of bytes in HEX and just bytes:

  res ["xidhex"] = data [4: 8] .hex ()
     res ["xidbyte"] = data [4: 8] 

, pack bytes into a structure:

  res ["flags"] = pack ('BB', data [10], data [11]) 

Get IP from structure:

  res ["ciaddr"] = socket.inet_ntoa (pack ('BBBB', data [12], data [13], data [14], data [15])); 


And vice versa:

  res = res + socket.inet_pton (socket.AF_INET, gconfig ["dhcp_Server"]) 

That's all ;)

Source: https://habr.com/ru/post/445338/


All Articles