From c44e31d9166e19561f849e5fdecc842069be85be Mon Sep 17 00:00:00 2001 From: Miha Tomsic Date: Tue, 17 Sep 2024 15:58:51 +0200 Subject: [PATCH 1/6] added DHCP Release packet --- dhcppython/client.py | 7 ++++++- dhcppython/packet.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/dhcppython/client.py b/dhcppython/client.py index 9a90749..3008c21 100644 --- a/dhcppython/client.py +++ b/dhcppython/client.py @@ -10,7 +10,7 @@ from .exceptions import DHCPClientError -COL_LEN = 80 +COL_LEN = 80-3 Lease = collections.namedtuple( "Lease", ["discover", "offer", "request", "ack", "time", "server"] @@ -150,6 +150,11 @@ def receive_ack(self, tx_id: int, verbosity: int) -> Optional[packet.DHCPPacket] print("Did not receive ack packet") return ack + def send_release( + self, server: str, release_packet: packet.DHCPPacket, verbosity: int + ): + self.send(server, self.send_to_port, release_packet.asbytes, verbosity) + def get_lease( self, mac_addr: Optional[str] = None, diff --git a/dhcppython/packet.py b/dhcppython/packet.py index 6579de0..d7bfddc 100644 --- a/dhcppython/packet.py +++ b/dhcppython/packet.py @@ -430,3 +430,46 @@ def Ack( fname, option_list, ) + + + @classmethod + def Release( + cls, + mac_addr: str, + seconds: int, + tx_id: int, + yiaddr: Union[int, str], + use_broadcast: bool = True, + relay: Optional[str] = None, + sname: bytes = b"", + fname: bytes = b"", + option_list: Optional[options.OptionList] = None, + ): + """ + Convenient constructor for a DHCP release packet. + """ + if len(mac_addr.split(":")) != 6 or len(mac_addr) != 17: + raise DHCPValueError( + "MAC address must consist of 6 octets delimited by ':'" + ) + option_list = option_list if option_list else options.OptionList() + option_list.insert(0, options.options.short_value_to_object(53, "DHCPRELEASE")) + relay_ip = ipaddress.IPv4Address(relay or 0) + return cls( + "BOOTREQUEST", + cls.htype_map[1], # 10 mb ethernet + 6, # 6 byte hardware addr + 0, # clients should set this to 0 + tx_id, + seconds, + 0b1000_0000_0000_0000 if use_broadcast else 0, + ipaddress.IPv4Address(0), + # yiaddr - "your address", address being proposed by server + ipaddress.IPv4Address(yiaddr), + ipaddress.IPv4Address(0), + relay_ip, + mac_addr, + sname, + fname, + option_list, + ) From 1637ff19fef2d136c745733afd004b9cc1cd765e Mon Sep 17 00:00:00 2001 From: Miha Tomsic Date: Fri, 20 Sep 2024 10:27:49 +0200 Subject: [PATCH 2/6] DHCP Release packet handling for client --- dhcppython/client.py | 20 ++++++++++++++++++++ dhcppython/packet.py | 18 ++++++++++++------ setup.py | 2 +- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/dhcppython/client.py b/dhcppython/client.py index 3008c21..c96892f 100644 --- a/dhcppython/client.py +++ b/dhcppython/client.py @@ -155,6 +155,26 @@ def send_release( ): self.send(server, self.send_to_port, release_packet.asbytes, verbosity) + def get_release( + self, + mac_addr: Optional[str] = None, + server: str = "255.255.255.255", + relay: Optional[str] = None, + client: Optional[str] = None, + broadcast: bool = True, + options_list: Optional[options.OptionList] = None, + verbose: int = 0, + ): + release = packet.DHCPPacket.Release( + mac_addr, + use_broadcast=broadcast, + option_list=options_list, + relay=relay, + client=client, + ) + self.send_release(server=server, release_packet=release, verbosity=verbose) + + def get_lease( self, mac_addr: Optional[str] = None, diff --git a/dhcppython/packet.py b/dhcppython/packet.py index d7bfddc..abbe40a 100644 --- a/dhcppython/packet.py +++ b/dhcppython/packet.py @@ -436,11 +436,11 @@ def Ack( def Release( cls, mac_addr: str, - seconds: int, - tx_id: int, - yiaddr: Union[int, str], + seconds: int = 0, + tx_id: int = 0, use_broadcast: bool = True, relay: Optional[str] = None, + client: Optional[str] = None, sname: bytes = b"", fname: bytes = b"", option_list: Optional[options.OptionList] = None, @@ -455,18 +455,24 @@ def Release( option_list = option_list if option_list else options.OptionList() option_list.insert(0, options.options.short_value_to_object(53, "DHCPRELEASE")) relay_ip = ipaddress.IPv4Address(relay or 0) + client_ip = ipaddress.IPv4Address(client or 0) return cls( "BOOTREQUEST", cls.htype_map[1], # 10 mb ethernet 6, # 6 byte hardware addr 0, # clients should set this to 0 - tx_id, + tx_id or random.getrandbits(32), seconds, 0b1000_0000_0000_0000 if use_broadcast else 0, - ipaddress.IPv4Address(0), + # ciaddr - client address + #ipaddress.IPv4Address(0), + client_ip, # yiaddr - "your address", address being proposed by server - ipaddress.IPv4Address(yiaddr), + #ipaddress.IPv4Address(0), + client_ip, + # siaddr - servr address ipaddress.IPv4Address(0), + # giaddr - gateway address = relay address relay_ip, mac_addr, sname, diff --git a/setup.py b/setup.py index 6f6efb0..4388348 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ EMAIL = '' AUTHOR = 'Victor Frazao' REQUIRES_PYTHON = '>=3.8.0' -VERSION = '0.1.4' +VERSION = '0.1.4a' # What packages are required for this module to be executed? REQUIRED = [] From 173490dab861372d1796b0ef6b73da56ce511073 Mon Sep 17 00:00:00 2001 From: Miha Tomsic Date: Thu, 26 Sep 2024 07:58:52 +0200 Subject: [PATCH 3/6] Option 82 handling --- dhcppython/options.py | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/dhcppython/options.py b/dhcppython/options.py index 1cfecdb..20f162f 100644 --- a/dhcppython/options.py +++ b/dhcppython/options.py @@ -1784,12 +1784,48 @@ class RelayAgentInformation(StrOption): """ Option 82 - Relay Agent Information + Relay Agent Information is comprised of suboptions with basically same + structure as a string option. + """ code = 82 key = "relay_agent_info" + code82 = { + 1: "circuit_id", + 2: "remote_id", + } + key82 = { + "circuit_id": 1, + "remote_id": 2, + } + + @property + def value(self) -> Dict[str, Dict[str, str]]: + + if self._value is None: + self._value = {} + data = self.data + while data: + code, length = struct.unpack(">BB",data[:2]) + data0 = data[2:2+length] + self._value[self.code82[code]] = data0.decode() + data = data[2+length:] + return self._value + + @classmethod + def from_value(cls, value): + data = b"" + for raisub in ["circuit_id","remote_id"]: + if raisub in value[cls.key]: + subval = value[cls.key][raisub] + data0 = subval.encode() + data += struct.pack(">Bb", cls.key82[raisub], len(data0)) + \ + struct.pack(">" + "B" * len(data0), *data0) + return cls(cls.code, len(data), data) + + class UnknownOption(BinOption): """ From 40561a9f82fc837ed78d9ac85f7035416fc5a0a5 Mon Sep 17 00:00:00 2001 From: Miha Tomsic Date: Thu, 26 Sep 2024 08:00:34 +0200 Subject: [PATCH 4/6] DHCP Inform packet handling --- dhcppython/client.py | 39 ++++++++++++++++++++++++++++++++++- dhcppython/packet.py | 49 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/dhcppython/client.py b/dhcppython/client.py index c96892f..39f19b7 100644 --- a/dhcppython/client.py +++ b/dhcppython/client.py @@ -155,7 +155,12 @@ def send_release( ): self.send(server, self.send_to_port, release_packet.asbytes, verbosity) - def get_release( + def send_inform( + self, server: str, inform_packet: packet.DHCPPacket, verbosity: int + ): + self.send(server, self.send_to_port, inform_packet.asbytes, verbosity) + + def put_release( self, mac_addr: Optional[str] = None, server: str = "255.255.255.255", @@ -174,6 +179,38 @@ def get_release( ) self.send_release(server=server, release_packet=release, verbosity=verbose) + def put_inform( + self, + mac_addr: Optional[str] = None, + broadcast: bool = True, + relay: Optional[str] = None, + client: Optional[str] = None, + server: str = "255.255.255.255", + ip_protocol: int = 4, + options_list: Optional[options.OptionList] = None, + verbose: int = 0, + ): + mac_addr = mac_addr or utils.random_mac() + logging.debug("Synthetizing inform packet") + + # I + start = int(default_timer()) + inform = packet.DHCPPacket.Inform( + mac_addr, + int(default_timer()-start), + 0, + use_broadcast=broadcast, + option_list=options_list, + client_ip=client, + relay=relay, + ) + tx_id = inform.xid + if verbose > 1: + print("INFORM Packet") + print(format_dhcp_packet(inform)) + logging.debug(f"Constructed inform packet: {inform}") + logging.debug(f"Sending inform packet to {server} with {tx_id=}") + self.send_inform(server, inform, verbose) def get_lease( self, diff --git a/dhcppython/packet.py b/dhcppython/packet.py index abbe40a..7c103a4 100644 --- a/dhcppython/packet.py +++ b/dhcppython/packet.py @@ -479,3 +479,52 @@ def Release( fname, option_list, ) + + @classmethod + def Inform( + cls, + mac_addr: str, + seconds: int = 0, + tx_id: int = 0, + use_broadcast: bool = True, + relay: Optional[str] = None, + client_ip: Optional[str] = None, + sname: bytes = b"", + fname: bytes = b"", + option_list: Optional[options.OptionList] = None, + ): + """ + Convenient constructor for a DHCP inform packet. + """ + if len(mac_addr.split(":")) != 6 or len(mac_addr) != 17: + raise DHCPValueError( + "MAC address must consist of 6 octets delimited by ':'" + ) + option_list = option_list if option_list else options.OptionList() + option_list.insert(0, options.options.short_value_to_object(53, "DHCPINFORM")) + relay_ip = ipaddress.IPv4Address(relay or 0) + client_ip = ipaddress.IPv4Address(client_ip or 0) + return cls( + "BOOTREQUEST", + cls.htype_map[1], # 10 mb ethernet + 6, # 6 byte hardware addr + 0, # clients should set this to 0 + tx_id or random.getrandbits(32), + seconds, + 0b1000_0000_0000_0000 if use_broadcast else 0, + # ciaddr - client address + #ipaddress.IPv4Address(0), + client_ip, + # yiaddr - "your address", address being proposed by server + #ipaddress.IPv4Address(0), + client_ip, + # siaddr - servr address + ipaddress.IPv4Address(0), + # giaddr - gateway address = relay address + relay_ip, + mac_addr, + sname, + fname, + option_list, + ) + \ No newline at end of file From 54e0c00dfe9fafc809585667b802bda94883719d Mon Sep 17 00:00:00 2001 From: Miha Tomsic Date: Fri, 27 Sep 2024 10:15:02 +0200 Subject: [PATCH 5/6] random_mac_prefix() function added --- dhcppython/utils.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/dhcppython/utils.py b/dhcppython/utils.py index e91e446..f94173c 100644 --- a/dhcppython/utils.py +++ b/dhcppython/utils.py @@ -48,6 +48,28 @@ def random_mac(num_bytes: int = 6, delimiter: str = ":") -> str: ["".join(random.choices(VALID_HEX, k=2)) for i in range(num_bytes)] ) +def random_mac_prefix(num_bytes: int = 6, delimiter: str = ":", prefix: str = "") -> str: + """ + Generates an 6 byte long MAC address with predefined prefix + + >>> random_mac_prefix(prefix="00:0A:A0") + '00:0A:A0:85:A4:EF' + """ + + if not prefix: + return random_mac(num_bytes=num_bytes, delimiter=delimiter) + else: + prefix = prefix.upper() + prefix = prefix.replace(":","").replace("-","").replace(".","") + if not set(prefix).issubset(set(VALID_HEX)): + raise TypeError(f"Wrong characters in MAC prefix: {prefix}") + if len(prefix)%2 != 0: + raise ValueError(f"Even number of HEX characters expected: {prefix}") + prefix_list = [ prefix[i]+prefix[i+1] for i in range(0, len(prefix), 2)] + + return delimiter.join(prefix_list+ + ["".join(random.choices(VALID_HEX, k=2)) for i in range(num_bytes-len(prefix)//2)] + ) def is_mac_addr(mac_addr: str) -> bool: """ From c9b03ef8015154980c468217e0d73cceba0170df Mon Sep 17 00:00:00 2001 From: Miha Tomsic Date: Mon, 4 Nov 2024 13:08:39 +0100 Subject: [PATCH 6/6] added decline package, version to 0.1.4b --- README.md | 2 +- dhcppython/client.py | 47 +++++++++++++++++++++++++++++++++++------- dhcppython/packet.py | 49 +++++++++++++++++++++++++++++++++++++++++++- setup.py | 2 +- 4 files changed, 90 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index a341f75..3be7457 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # DHCP Python -Version 0.1.4 +Version 0.1.4b A Python implementation of a DHCP client and the tools to manipulate DHCP packets. Includes: diff --git a/dhcppython/client.py b/dhcppython/client.py index 39f19b7..3fec57d 100644 --- a/dhcppython/client.py +++ b/dhcppython/client.py @@ -160,6 +160,11 @@ def send_inform( ): self.send(server, self.send_to_port, inform_packet.asbytes, verbosity) + def send_decline( + self, server: str, decline_packet: packet.DHCPPacket, verbosity: int + ): + self.send(server, self.send_to_port, decline_packet.asbytes, verbosity) + def put_release( self, mac_addr: Optional[str] = None, @@ -170,6 +175,7 @@ def put_release( options_list: Optional[options.OptionList] = None, verbose: int = 0, ): + logging.debug("Synthetizing release packet") release = packet.DHCPPacket.Release( mac_addr, use_broadcast=broadcast, @@ -177,6 +183,12 @@ def put_release( relay=relay, client=client, ) + tx_id = release.xid + if verbose > 1: + print("RELEASE Packet") + print(format_dhcp_packet(release)) + logging.debug(f"Constructed release packet: {release}") + logging.debug(f"Sending release packet to {server} with {tx_id=}") self.send_release(server=server, release_packet=release, verbosity=verbose) def put_inform( @@ -190,15 +202,9 @@ def put_inform( options_list: Optional[options.OptionList] = None, verbose: int = 0, ): - mac_addr = mac_addr or utils.random_mac() logging.debug("Synthetizing inform packet") - - # I - start = int(default_timer()) inform = packet.DHCPPacket.Inform( mac_addr, - int(default_timer()-start), - 0, use_broadcast=broadcast, option_list=options_list, client_ip=client, @@ -210,7 +216,34 @@ def put_inform( print(format_dhcp_packet(inform)) logging.debug(f"Constructed inform packet: {inform}") logging.debug(f"Sending inform packet to {server} with {tx_id=}") - self.send_inform(server, inform, verbose) + self.send_inform(server=server, inform_packet=inform, verbosity=verbose) + + def put_decline( + self, + mac_addr: Optional[str] = None, + server: str = "255.255.255.255", + relay: Optional[str] = None, + client: Optional[str] = None, + broadcast: bool = True, + options_list: Optional[options.OptionList] = None, + verbose: int = 0, + ): + logging.debug("Synthetizing decline packet") + decline = packet.DHCPPacket.Decline( + mac_addr, + use_broadcast=broadcast, + option_list=options_list, + relay=relay, + client=client, + ) + tx_id = decline.xid + if verbose > 1: + print("INFORM Packet") + print(format_dhcp_packet(decline)) + logging.debug(f"Constructed decline packet: {decline}") + logging.debug(f"Sending decline packet to {server} with {tx_id=}") + + self.send_decline(server=server, decline_packet=decline, verbosity=verbose) def get_lease( self, diff --git a/dhcppython/packet.py b/dhcppython/packet.py index 7c103a4..f8a64ad 100644 --- a/dhcppython/packet.py +++ b/dhcppython/packet.py @@ -431,7 +431,6 @@ def Ack( option_list, ) - @classmethod def Release( cls, @@ -527,4 +526,52 @@ def Inform( fname, option_list, ) + @classmethod + def Decline( + cls, + mac_addr: str, + seconds: int = 0, + tx_id: int = 0, + use_broadcast: bool = True, + relay: Optional[str] = None, + client: Optional[str] = None, + sname: bytes = b"", + fname: bytes = b"", + option_list: Optional[options.OptionList] = None, + ): + """ + Convenient constructor for a DHCP decline packet. + """ + if len(mac_addr.split(":")) != 6 or len(mac_addr) != 17: + raise DHCPValueError( + "MAC address must consist of 6 octets delimited by ':'" + ) + option_list = option_list if option_list else options.OptionList() + option_list.insert(0, options.options.short_value_to_object(53, "DHCPDECLINE")) + relay_ip = ipaddress.IPv4Address(relay or 0) + client_ip = ipaddress.IPv4Address(client or 0) + return cls( + "BOOTREQUEST", + cls.htype_map[1], # 10 mb ethernet + 6, # 6 byte hardware addr + 0, # clients should set this to 0 + tx_id or random.getrandbits(32), + seconds, + 0b1000_0000_0000_0000 if use_broadcast else 0, + # ciaddr - client address + #ipaddress.IPv4Address(0), + client_ip, + # yiaddr - "your address", address being proposed by server + #ipaddress.IPv4Address(0), + client_ip, + # siaddr - servr address + ipaddress.IPv4Address(0), + # giaddr - gateway address = relay address + relay_ip, + mac_addr, + sname, + fname, + option_list, + ) + \ No newline at end of file diff --git a/setup.py b/setup.py index 4388348..795aad8 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ EMAIL = '' AUTHOR = 'Victor Frazao' REQUIRES_PYTHON = '>=3.8.0' -VERSION = '0.1.4a' +VERSION = '0.1.4b' # What packages are required for this module to be executed? REQUIRED = []