Skip to content

Commit d3bd03d

Browse files
authored
Merge pull request #1 from vfrazao-ns1/0.1.1/bug-fixes
0.1.1/bug fixes
2 parents f77b1ae + b606dc9 commit d3bd03d

File tree

8 files changed

+139
-167
lines changed

8 files changed

+139
-167
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# DHCP Python Changelog
2+
3+
## 0.1.1 (Jan 25 2020)
4+
5+
* Minor bug fixes
6+
* Code formatting with black
7+
8+
## 0.1.0 (Jan 25 2020)
9+
10+
* Initial release

README.md

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# DHCP Python
22

3-
Version 0.1.0
3+
Version 0.1.1
44

55
A Python implementation of a DHCP client and the tools to manipulate DHCP packets. Includes:
66

7-
1. A parser of DHCP packets, returning Python objects - supports all DHCP options in RFC 2132
8-
2. A CLI DHCP client that is useful for debugging
7+
1. A parser of DHCP packets, returning Python objects
8+
2. Supports for all DHCP options in RFC 2132
9+
3. A rudimentary DHCP client
910

1011
## Installation
1112

@@ -15,9 +16,9 @@ A Python implementation of a DHCP client and the tools to manipulate DHCP packet
1516

1617
* Python 3.8.0 or higher
1718

18-
NOTE: This has been tested on Ubuntu 18.04 and Windows WSL. May or may not work on other platforms.
19+
**NOTE: This has been tested on Ubuntu 18.04 and Windows WSL. May or may not work on other platforms.**
1920

20-
## Packet Parser
21+
## The Packet Parser
2122

2223
Two files contribute to the packet parsing: `dhcppython.packet` and `dhcppython.options`. For most operations only `dhcppython.packet` will be required.
2324

@@ -180,3 +181,24 @@ Hostname(code=12, length=9, data=b'Galaxy-S9')
180181
ParameterRequestList(code=55, length=10, data=b'\x01\x03\x06\x0f\x1a\x1c3:;+')
181182
MessageType(code=53, length=1, data=b'\x01')
182183
```
184+
185+
## The DHCP Client
186+
187+
A very primitive DHCP client is included in this package in the `dhcppython.client` module. The client is able to negotiate a lease with a DHCP server and can be configured to use:
188+
189+
* A given interface
190+
* Option to send broadcast packets or unicast packets to a specific server
191+
* Set a relay in the giaddr field
192+
* "Spoof" MAC addresses
193+
* Specify options to send with request
194+
195+
The high level interface to negotiate a lease is the `get_lease` method of the `dhcppython.client.DHCPClient` object. This method goes through the DORA DHCP handshake and returns a `Lease` namedtuple which includes all the packets in the :
196+
197+
```python
198+
>>> import dhcppython
199+
>>> client = dhcppython.client.DHCPClient(interface="enp0s8")
200+
>>> lease = client.get_lease(mac_addr="de:ad:be:ef:c0:de", broadcast=True, relay=None, server="255.255.255.255", options_list=None)
201+
Lease succesful: 192.168.56.3 -- DE:AD:BE:EF:C0:DE -- 3 ms elapsed
202+
>>> lease
203+
Lease(discover=DHCPPacket(op='BOOTREQUEST', htype='ETHERNET', hlen=6, hops=0, xid=2829179566, secs=0, flags=32768, ciaddr=IPv4Address('0.0.0.0'), yiaddr=IPv4Address('0.0.0.0'), siaddr=IPv4Address('0.0.0.0'), giaddr=IPv4Address('0.0.0.0'), chaddr='de:ad:be:ef:c0:de', sname=b'', file=b'', options=OptionList([MessageType(code=53, length=1, data=b'\x01')])), offer=DHCPPacket(op='BOOTREPLY', htype='ETHERNET', hlen=6, hops=0, xid=2829179566, secs=0, flags=32768, ciaddr=IPv4Address('0.0.0.0'), yiaddr=IPv4Address('192.168.56.3'), siaddr=IPv4Address('0.0.0.0'), giaddr=IPv4Address('0.0.0.0'), chaddr='DE:AD:BE:EF:C0:DE', sname=b'', file=b'', options=OptionList([SubnetMask(code=1, length=4, data=b'\xff\xff\xff\x00'), Router(code=3, length=4, data=b'\n\x97\x01\x01'), DNSServer(code=6, length=4, data=b'\nh\x01\x08'), Hostname(code=12, length=22, data=b'dhcp.-192-168-56-3.com'), DomainName(code=15, length=14, data=b'example.com'), IPAddressLeaseTime(code=51, length=4, data=b'\x00\x01Q\x80'), MessageType(code=53, length=1, data=b'\x02'), ServerIdentifier(code=54, length=4, data=b'\xc0\xa88\x02'), RenewalTime(code=58, length=4, data=b'\x00\x00T`'), RebindingTime(code=59, length=4, data=b'\x00\x00\xa8\xc0'), End(code=255, length=0, data=b'')])), request=DHCPPacket(op='BOOTREQUEST', htype='ETHERNET', hlen=6, hops=0, xid=2829179566, secs=0, flags=32768, ciaddr=IPv4Address('0.0.0.0'), yiaddr=IPv4Address('0.0.0.0'), siaddr=IPv4Address('0.0.0.0'), giaddr=IPv4Address('0.0.0.0'), chaddr='de:ad:be:ef:c0:de', sname=b'', file=b'', options=OptionList([MessageType(code=53, length=1, data=b'\x03')])), ack=DHCPPacket(op='BOOTREPLY', htype='ETHERNET', hlen=6, hops=0, xid=2829179566, secs=0, flags=32768, ciaddr=IPv4Address('0.0.0.0'), yiaddr=IPv4Address('192.168.56.3'), siaddr=IPv4Address('0.0.0.0'), giaddr=IPv4Address('0.0.0.0'), chaddr='DE:AD:BE:EF:C0:DE', sname=b'', file=b'', options=OptionList([SubnetMask(code=1, length=4, data=b'\xff\xff\xff\x00'), Router(code=3, length=4, data=b'\n\x97\x01\x01'), DNSServer(code=6, length=4, data=b'\nh\x01\x08'), Hostname(code=12, length=22, data=b'dhcp.-192-168-56-3.com'), DomainName(code=15, length=14, data=b'example.com'), IPAddressLeaseTime(code=51, length=4, data=b'\x00\x01Q\x80'), MessageType(code=53, length=1, data=b'\x05'), ServerIdentifier(code=54, length=4, data=b'\xc0\xa88\x02'), RenewalTime(code=58, length=4, data=b'\x00\x00T`'), RebindingTime(code=59, length=4, data=b'\x00\x00\xa8\xc0'), End(code=255, length=0, data=b'')])), time=0.0032514659978915006, server=('192.168.56.2', 67))
204+
```

dhcppython/client.py

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
from . import packet, options, utils
1010
from .exceptions import DHCPClientError
1111

12-
CLIENT_VER = "0.0.1"
13-
CLIENT_NAME = "DORA"
12+
1413
COL_LEN = 80
1514

1615
Lease = collections.namedtuple(
@@ -30,15 +29,19 @@ def format_dhcp_packet(pkt: packet.DHCPPacket) -> str:
3029
broadcast = "BROADCAST" if pkt.flags else "UNICAST"
3130
client_info_padding = 18
3231
client_info = f"{pkt.htype} - {pkt.chaddr} ({utils.mac2vendor(pkt.chaddr)})"
33-
if (visual_diff := (utils.visual_length(client_info) - (COL_LEN - client_info_padding))) > 0:
32+
if (
33+
visual_diff := (
34+
utils.visual_length(client_info) - (COL_LEN - client_info_padding)
35+
)
36+
) > 0:
3437
client_info = client_info[:-visual_diff]
3538

3639
output = (
3740
f"{pkt.op} / {msg_type} / {broadcast}\n"
3841
+ f"{len(pkt.asbytes)} bytes / TX ID {hex(pkt.xid).upper()} / {pkt.secs} seconds elapsed\n"
3942
+ "Client info:".ljust(client_info_padding)
40-
# + f"{pkt.htype} - {pkt.chaddr} ({vendor := utils.mac2vendor(pkt.chaddr)})"[:COL_LEN - 18] + "\n"
41-
+ client_info + "\n"
43+
+ client_info
44+
+ "\n"
4245
+ "Client address:".ljust(client_info_padding)
4346
+ f"{pkt.ciaddr}\n"
4447
+ "Your address:".ljust(client_info_padding)
@@ -50,13 +53,22 @@ def format_dhcp_packet(pkt: packet.DHCPPacket) -> str:
5053
)
5154

5255
output = (
53-
"\n".join([f"; {line.ljust(COL_LEN if utils.visual_length(line) < COL_LEN else 0, padding)};" for line in output.split("\n")]) + "\n"
56+
"\n".join(
57+
[
58+
f"; {line.ljust(COL_LEN if utils.visual_length(line) < COL_LEN else 0, padding)};"
59+
for line in output.split("\n")
60+
]
61+
)
62+
+ "\n"
5463
)
5564
output = line_divider + output + line_divider
5665
output += "; " + "OPTIONS:".ljust(COL_LEN, padding) + ";\n"
5766
output += (
5867
"\n".join(
59-
[f"; {line.ljust(COL_LEN, padding)};" for line in options_list.json.split("\n")]
68+
[
69+
f"; {line.ljust(COL_LEN, padding)};"
70+
for line in options_list.json.split("\n")
71+
]
6072
)
6173
+ "\n"
6274
)
@@ -70,7 +82,7 @@ def __init__(
7082
self,
7183
interface: str = None,
7284
send_from_port: int = 68,
73-
send_to_port:int = 67,
85+
send_to_port: int = 67,
7486
max_retries: int = 10,
7587
socket_poll_interval: int = 10,
7688
retry_interval: int = 100,
@@ -94,7 +106,9 @@ def __init__(
94106
self.offer_servers: List[str] = []
95107
self.ack_server: str = ""
96108

97-
def send_discover(self, server: str, discover_packet: packet.DHCPPacket, verbosity: int):
109+
def send_discover(
110+
self, server: str, discover_packet: packet.DHCPPacket, verbosity: int
111+
):
98112
self.send(server, self.send_to_port, discover_packet.asbytes, verbosity)
99113

100114
def receive_offer(self, tx_id: int, verbosity: int) -> Optional[packet.DHCPPacket]:
@@ -114,7 +128,9 @@ def receive_offer(self, tx_id: int, verbosity: int) -> Optional[packet.DHCPPacke
114128
print("Did not receive offer packet")
115129
return offer
116130

117-
def send_request(self, server: str, request_packet: packet.DHCPPacket, verbosity: int):
131+
def send_request(
132+
self, server: str, request_packet: packet.DHCPPacket, verbosity: int
133+
):
118134
self.send(server, self.send_to_port, request_packet.asbytes, verbosity)
119135

120136
def receive_ack(self, tx_id: int, verbosity: int) -> Optional[packet.DHCPPacket]:
@@ -137,7 +153,6 @@ def receive_ack(self, tx_id: int, verbosity: int) -> Optional[packet.DHCPPacket]
137153
def get_lease(
138154
self,
139155
mac_addr: Optional[str] = None,
140-
iface: Optional[str] = None,
141156
broadcast: bool = True,
142157
relay: Optional[str] = None,
143158
server: str = "255.255.255.255",
@@ -213,7 +228,9 @@ def get_lease(
213228
if verbose:
214229
print(f"Client terminated after {lease_time * 1000:.0f} ms")
215230
else:
216-
print(f"Lease succesful: {ack.yiaddr} -- {ack.chaddr} -- {lease_time * 1000:.0f} ms elapsed")
231+
print(
232+
f"Lease succesful: {ack.yiaddr} -- {ack.chaddr} -- {lease_time * 1000:.0f} ms elapsed"
233+
)
217234
return lease
218235

219236
def get_valid_pkt(self, data: bytes) -> Optional[packet.DHCPPacket]:
@@ -226,26 +243,49 @@ def get_valid_pkt(self, data: bytes) -> Optional[packet.DHCPPacket]:
226243
)
227244
return pkt
228245

229-
def listen(self, tx_id: int, msg_type: str, verbosity: int) -> Tuple[Optional[packet.DHCPPacket], Optional[str]]:
230-
logging.debug(f"Listening on {self.interface or 'all interfaces'}, UDP ports {self.listening_ports}")
246+
def listen(
247+
self, tx_id: int, msg_type: str, verbosity: int
248+
) -> Tuple[Optional[packet.DHCPPacket], Optional[str]]:
249+
logging.debug(
250+
f"Listening on {self.interface or 'all interfaces'}, UDP ports {self.listening_ports}"
251+
)
231252
tries = 0
232253
dhcp_packet, addr = None, None
233254
while tries < self.max_tries:
234-
logging.debug(f"Select: {select.select(self.listening_sockets, self.writing_sockets, self.except_sockets, 0)}")
235-
if len(socks := select.select(self.listening_sockets, self.writing_sockets, self.except_sockets, self.select_timout)[0]):
255+
logging.debug(
256+
f"Select: {select.select(self.listening_sockets, self.writing_sockets, self.except_sockets, 0)}"
257+
)
258+
if len(
259+
socks := select.select(
260+
self.listening_sockets,
261+
self.writing_sockets,
262+
self.except_sockets,
263+
self.select_timout,
264+
)[0]
265+
):
236266
for sock in socks:
237267
data, addr = sock.recvfrom(self.max_pkt_size)
238268
logging.debug(f"Received data from {addr}: {data}")
239-
if (dhcp_packet := self.get_valid_pkt(data)) is not None and dhcp_packet.xid == tx_id and dhcp_packet.msg_type == msg_type:
240-
logging.debug(f"Received valid DHCP packet of {dhcp_packet.msg_type} type")
269+
if (
270+
(dhcp_packet := self.get_valid_pkt(data)) is not None
271+
and dhcp_packet.xid == tx_id
272+
and dhcp_packet.msg_type == msg_type
273+
):
274+
logging.debug(
275+
f"Received valid DHCP packet of {dhcp_packet.msg_type} type"
276+
)
241277
return dhcp_packet, addr
242278
else:
243279
if dhcp_packet is None:
244280
logging.debug("Invalid DHCP packet")
245281
elif dhcp_packet.xid != tx_id:
246-
logging.debug(f"TX ID does not match expected ID {dhcp_packet.xid} != {tx_id}")
282+
logging.debug(
283+
f"TX ID does not match expected ID {dhcp_packet.xid} != {tx_id}"
284+
)
247285
elif (msg_type_actual := dhcp_packet.msg_type) != msg_type:
248-
logging.debug(f"DHCP message type does not match expected: {msg_type_actual} != {msg_type}")
286+
logging.debug(
287+
f"DHCP message type does not match expected: {msg_type_actual} != {msg_type}"
288+
)
249289
else:
250290
logging.debug("Something is wrong with this packet")
251291
logging.debug(dhcp_packet)
@@ -302,7 +342,10 @@ def send(self, remote_addr: str, remote_port: int, data: bytes, verbosity: int):
302342
while tries < self.max_tries:
303343
if len(
304344
socks := select.select(
305-
self.listening_sockets, self.writing_sockets, self.except_sockets, self.select_timout
345+
self.listening_sockets,
346+
self.writing_sockets,
347+
self.except_sockets,
348+
self.select_timout,
306349
)[1]
307350
):
308351
sock = socks[0]

dhcppython/dora.json

Lines changed: 0 additions & 21 deletions
This file was deleted.

dhcppython/dora.py

Lines changed: 0 additions & 99 deletions
This file was deleted.

0 commit comments

Comments
 (0)