├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── dhcppython ├── __init__.py ├── client.py ├── exceptions.py ├── options.py ├── packet.py ├── runtime_assets │ ├── __init__.py │ ├── options.csv │ └── oui.txt └── utils.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── test_OptionList.py ├── test_options.py └── test_packet.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | 3 | # Boilerplate: 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # DHCP Python Changelog 2 | 3 | ## 0.1.4 (Mar 30 2021) 4 | 5 | * Merges (PR #6)[https://github.com/vfrazao-ns1/dhcppython/pull/6] fixing a bug where MAC addresses were stripped from of 0 bytes from both ends 6 | 7 | 8 | ## 0.1.3 (Aug 19 2020) 9 | 10 | * Merges (PR #4)[https://github.com/vfrazao-ns1/dhcppython/pull/4] which extends option 82 support 11 | 12 | ## 0.1.2 (Jan 29 2020) 13 | 14 | * Minor fixes and changes 15 | 16 | ## 0.1.1 (Jan 25 2020) 17 | 18 | * Minor bug fixes 19 | * Code formatting with black 20 | 21 | ## 0.1.0 (Jan 25 2020) 22 | 23 | * Initial release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Victor Frazao 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include dhcppython/runtime_assets/options.csv 3 | include dhcppython/runtime_assets/oui.txt 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DHCP Python 2 | 3 | Version 0.1.4 4 | 5 | A Python implementation of a DHCP client and the tools to manipulate DHCP packets. Includes: 6 | 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 10 | 11 | ## Installation 12 | 13 | `pip install dhcppython` 14 | 15 | ## Requirements 16 | 17 | * Python 3.8.0 or higher 18 | 19 | **NOTE: This has been tested on Ubuntu 18.04 and Windows WSL. May or may not work on other platforms.** 20 | 21 | ## The Packet Parser 22 | 23 | Two files contribute to the packet parsing: `dhcppython.packet` and `dhcppython.options`. For most operations only `dhcppython.packet` will be required. 24 | 25 | ### dhcppython.packet 26 | 27 | The main class in `dhcppython.packet` is the `DHCPPacket`. The `DHCPPacket` class contains multiple constructors for parsing and constructing DHCP packets. 28 | 29 | #### Converting a packet in wireformat to a Python object 30 | 31 | Given a DHCP packet in `bytes` format (such as what you would get from reading a DHCP packet straight from a socket) a DHCPPacket object can be instantiated by calling the `from_bytes` and supplying the bytes. 32 | 33 | ```python 34 | >>> pkt = dhcppython.packet.DHCPPacket.from_bytes(b'\x01\x01\x06\x00\xea\xbe\xc3\x97\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8cE\x00E\x12\t\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5\x01\x01=\x07\x01\x8cE\x00E\x12\t9\x02\x05\xdc<\x0eandroid-dhcp-9\x0c\tGalaxy-S97\n\x01\x03\x06\x0f\x1a\x1c3:;+\xff') 35 | >>> pkt 36 | DHCPPacket(op='BOOTREQUEST', htype='ETHERNET', hlen=6, hops=0, xid=3938370455, secs=1, flags=0, 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='8C:45:00:45:12:09', sname=b'', file=b'', options=OptionList([MessageType(code=53, length=1, data=b'\x01'), ClientIdentifier(code=61, length=7, data=b'\x01\x8cE\x00E\x12\t'), MaxDHCPMessageSize(code=57, length=2, data=b'\x05\xdc'), VendorClassIdentifier(code=60, length=14, data=b'android-dhcp-9'), Hostname(code=12, length=9, data=b'Galaxy-S9'), ParameterRequestList(code=55, length=10, data=b'\x01\x03\x06\x0f\x1a\x1c3:;+'), End(code=255, length=0, data=b'')])) 37 | ``` 38 | 39 | #### Converting a DHCPPacket object to wireformat 40 | 41 | Given a DHCPPacket object you can easily output the corresponding DHCP packet in wireformat by accessing the `asbytes` attribute of the object. 42 | 43 | ```python 44 | >>> pkt.asbytes 45 | b'\x01\x01\x06\x00\xea\xbe\xc3\x97\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8cE\x00E\x12\t\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5\x01\x01=\x07\x01\x8cE\x00E\x12\t9\x02\x05\xdc<\x0eandroid-dhcp-9\x0c\tGalaxy-S97\n\x01\x03\x06\x0f\x1a\x1c3:;+\xff' 46 | ``` 47 | 48 | This bytes output is suitable for sending over a socket to a DHCP server. 49 | 50 | #### Other Constructors of the DHCPPacket Class 51 | 52 | * The default, low level, constructor (not recommended): 53 | 54 | ```python 55 | >>> pkt = dhcppython.packet.DHCPPacket(op="BOOTREQUEST", htype="ETHERNET", hlen=6, hops=0, xid=123456, secs=0, flags=0, ciaddr=ipaddress.IPv4Address(0), yiaddr=ipaddress.IPv4Address(0), siaddr=ipaddress.IPv4Address(0), giaddr=ipaddress.IPv4Address(0), chaddr="DE:AD:BE:EF:C0:DE", sname=b'', file=b'', options=dhcppython.options.OptionList([dhcppython.options.options.short_value_to_object(53, "DHCPDISCOVER")])) 56 | >>> pkt 57 | DHCPPacket(op='BOOTREQUEST', htype='ETHERNET', hlen=6, hops=0, xid=123456, secs=0, flags=0, 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')])) 58 | ``` 59 | 60 | * Higher level constructors specific to the four main DHCP message types: DISCOVER, OFFER, REQUEST, ACK: 61 | 62 | ```python 63 | >>> dhcppython.packet.DHCPPacket.Discover('de:ad:be:ef:c0:de') 64 | DHCPPacket(op='BOOTREQUEST', htype='ETHERNET', hlen=6, hops=0, xid=4249353806, 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')])) 65 | >>> dhcppython.packet.DHCPPacket.Offer('de:ad:be:ef:c0:de', seconds=0, tx_id=4249353806, yiaddr=ipaddress.IPv4Address('192.168.56.4')) 66 | DHCPPacket(op='BOOTREPLY', htype='ETHERNET', hlen=6, hops=0, xid=4249353806, secs=0, flags=32768, ciaddr=IPv4Address('0.0.0.0'), yiaddr=IPv4Address('192.168.56.4'), 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'\x02')])) 67 | >>> dhcppython.packet.DHCPPacket.Request('de:ad:be:ef:c0:de', seconds=0, tx_id=4249353806) 68 | DHCPPacket(op='BOOTREQUEST', htype='ETHERNET', hlen=6, hops=0, xid=4249353806, 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')])) 69 | >>> dhcppython.packet.DHCPPacket.Ack('de:ad:be:ef:c0:de', seconds=0, tx_id=4249353806, yiaddr=ipaddress.IPv4Address('192.168.56.4')) 70 | DHCPPacket(op='BOOTREPLY', htype='ETHERNET', hlen=6, hops=0, xid=4249353806, secs=0, flags=32768, ciaddr=IPv4Address('0.0.0.0'), yiaddr=IPv4Address('192.168.56.4'), 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'\x05')])) 71 | ``` 72 | 73 | ### dhcppython.options 74 | 75 | This module provides classes for: 76 | 77 | 1. All DHCP options described in RFC 2132 78 | 2. An unknown option class for options not encoded 79 | 3. An abstract Option class that is easily extendable if additional options are required 80 | 4. A data structure for mananging DHCP options - the `OptionList` 81 | 5. An higher lever Option factory - the `OptionDirectory` 82 | 83 | A high level API is provided by the `dhcppython.options.options` object and the Option class: 84 | 85 | * Create an options object from bytes by calling the `bytes_to_object` method 86 | 87 | ```python 88 | >>> opt = dhcppython.options.options.bytes_to_object(b"\x3d\x07\x01\x8c\x45\x00\x45\x12\x09") 89 | >>> opt 90 | ClientIdentifier(code=61, length=7, data=b'\x01\x8cE\x00E\x12\t') 91 | ``` 92 | 93 | * Get a human readable dict of an options object value 94 | 95 | ```python 96 | >>> opt.value 97 | {'client_identifier': {'hwtype': 1, 'hwaddr': '8C:45:00:45:12:09'}} 98 | ``` 99 | 100 | * Create an options object from its human readable dict of its value: 101 | 102 | ```python 103 | >>> dhcppython.options.options.value_to_object({'client_identifier': {'hwtype': 1, 'hwaddr': '8C:45:00:45:12:09'}}) 104 | ClientIdentifier(code=61, length=7, data=b'\x01\x8cE\x00E\x12\t') 105 | ``` 106 | 107 | OR 108 | 109 | ```python 110 | >>> dhcppython.options.options.short_value_to_object(61, {'hwtype': 1, 'hwaddr': '8C:45:00:45:12:09'}) 111 | ClientIdentifier(code=61, length=7, data=b'\x01\x8cE\x00E\x12\t') 112 | ``` 113 | 114 | 4. Convert a human readable dict of an option value to the bytes representation 115 | 116 | ```python 117 | >>> dhcppython.options.options.value_to_bytes({'client_identifier': {'hwtype': 1, 'hwaddr': '8C:45:00:45:12:09'}}) 118 | b'=\x07\x01\x8cE\x00E\x12\t' 119 | ``` 120 | 121 | 5. Get the bytes representation of an option given its Option object 122 | 123 | ```python 124 | >>> opt = dhcppython.options.ClientIdentifier(code=61, length=7, data=b'\x01\x8cE\x00E\x12\t') 125 | >>> opt.asbytes 126 | b'=\x07\x01\x8cE\x00E\x12\t' 127 | ``` 128 | 129 | The `OptionList` class provides a very convenient set of methods for managing a list of DHCP options. 130 | 131 | * Create an `OptionList` instance from a list of `Option` objects 132 | 133 | ```python 134 | >>> opt_list = dhcppython.options.OptionList( 135 | ... [ 136 | ... dhcppython.options.options.short_value_to_object(61, {'hwtype': 1, 'hwaddr': "8c:45:00:23:45:67"}), 137 | ... dhcppython.options.options.short_value_to_object(57, 1500), 138 | ... dhcppython.options.options.short_value_to_object(60, "android-dhcp-9"), 139 | ... dhcppython.options.options.short_value_to_object(12, "Galaxy-S9"), 140 | ... dhcppython.options.options.short_value_to_object(55, [1, 3, 6, 15, 26, 28, 51, 58, 59, 43]) 141 | ... ] 142 | ... ) 143 | >>> opt_list 144 | OptionList([ClientIdentifier(code=61, length=7, data=b'\x01\x8cE\x00#Eg'), MaxDHCPMessageSize(code=57, length=2, data=b'\x05\xdc'), VendorClassIdentifier(code=60, length=14, data=b'android-dhcp-9'), Hostname(code=12, length=9, data=b'Galaxy-S9'), ParameterRequestList(code=55, length=10, data=b'\x01\x03\x06\x0f\x1a\x1c3:;+')]) 145 | ``` 146 | 147 | * Retrieve any options in the `OptionList` by its option code 148 | 149 | ```python 150 | >>> opt_list.by_code(12) 151 | Hostname(code=12, length=9, data=b'Galaxy-S9') 152 | >>> opt_list.by_code(13) 153 | >>> 154 | ``` 155 | 156 | * Append (add) any options using the `append` method 157 | 158 | ```python 159 | >>> opt_list.append(dhcppython.options.options.short_value_to_object(53, "DHCPDISCOVER")) 160 | >>> opt_list 161 | OptionList([ClientIdentifier(code=61, length=7, data=b'\x01\x8cE\x00#Eg'), MaxDHCPMessageSize(code=57, length=2, data=b'\x05\xdc'), VendorClassIdentifier(code=60, length=14, data=b'android-dhcp-9'), Hostname(code=12, length=9, data=b'Galaxy-S9'), ParameterRequestList(code=55, length=10, data=b'\x01\x03\x06\x0f\x1a\x1c3:;+'), MessageType(code=53, length=1, data=b'\x01')]) 162 | ``` 163 | 164 | * Protects against duplicate options (duplicate overwrites in place) 165 | 166 | ```python 167 | >>> opt_list 168 | OptionList([ClientIdentifier(code=61, length=7, data=b'\x01\x8cE\x00#Eg'), MaxDHCPMessageSize(code=57, length=2, data=b'\x13\x88'), VendorClassIdentifier(code=60, length=14, data=b'android-dhcp-9'), Hostname(code=12, length=9, data=b'Galaxy-S9'), ParameterRequestList(code=55, length=10, data=b'\x01\x03\x06\x0f\x1a\x1c3:;+'), MessageType(code=53, length=1, data=b'\x01')]) 169 | ``` 170 | 171 | * Allows for iteration like a list 172 | 173 | ```python 174 | >>> for opt in opt_list: 175 | ... print(opt) 176 | ... 177 | ClientIdentifier(code=61, length=7, data=b'\x01\x8cE\x00#Eg') 178 | MaxDHCPMessageSize(code=57, length=2, data=b'\x13\x88') 179 | VendorClassIdentifier(code=60, length=14, data=b'android-dhcp-9') 180 | Hostname(code=12, length=9, data=b'Galaxy-S9') 181 | ParameterRequestList(code=55, length=10, data=b'\x01\x03\x06\x0f\x1a\x1c3:;+') 182 | MessageType(code=53, length=1, data=b'\x01') 183 | ``` 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 | ``` 205 | -------------------------------------------------------------------------------- /dhcppython/__init__.py: -------------------------------------------------------------------------------- 1 | from . import packet, options, client, exceptions -------------------------------------------------------------------------------- /dhcppython/client.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import socket 3 | import select 4 | from typing import Tuple, List, Optional 5 | from timeit import default_timer 6 | from time import sleep 7 | import json 8 | import collections 9 | from . import packet, options, utils 10 | from .exceptions import DHCPClientError 11 | 12 | 13 | COL_LEN = 80 14 | 15 | Lease = collections.namedtuple( 16 | "Lease", ["discover", "offer", "request", "ack", "time", "server"] 17 | ) 18 | 19 | 20 | def format_dhcp_packet(pkt: packet.DHCPPacket) -> str: 21 | line_divider = ";-" + "".ljust(COL_LEN, "-") + ";\n" 22 | options_list = options.OptionList(pkt.options) 23 | msg_type_option = options_list.by_code(53) 24 | padding = " " 25 | if msg_type_option: 26 | msg_type = list(msg_type_option.value.values())[0] 27 | else: 28 | msg_type = "UNKNOWN MSG TYPE" 29 | broadcast = "BROADCAST" if pkt.flags else "UNICAST" 30 | client_info_padding = 18 31 | client_info = f"{pkt.htype} - {pkt.chaddr} ({utils.mac2vendor(pkt.chaddr)})" 32 | if ( 33 | visual_diff := ( 34 | utils.visual_length(client_info) - (COL_LEN - client_info_padding) 35 | ) 36 | ) > 0: 37 | client_info = client_info[:-visual_diff] 38 | 39 | output = ( 40 | f"{pkt.op} / {msg_type} / {broadcast}\n" 41 | + f"{len(pkt.asbytes)} bytes / TX ID {hex(pkt.xid).upper()} / {pkt.secs} seconds elapsed\n" 42 | + "Client info:".ljust(client_info_padding) 43 | + client_info 44 | + "\n" 45 | + "Client address:".ljust(client_info_padding) 46 | + f"{pkt.ciaddr}\n" 47 | + "Your address:".ljust(client_info_padding) 48 | + f"{pkt.yiaddr}\n" 49 | + "Next server:".ljust(client_info_padding) 50 | + f"{pkt.siaddr}\n" 51 | + "Relay:".ljust(client_info_padding) 52 | + f"{pkt.giaddr}" 53 | ) 54 | 55 | output = ( 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" 63 | ) 64 | output = line_divider + output + line_divider 65 | output += "; " + "OPTIONS:".ljust(COL_LEN, padding) + ";\n" 66 | output += ( 67 | "\n".join( 68 | [ 69 | f"; {line.ljust(COL_LEN, padding)};" 70 | for line in options_list.json.split("\n") 71 | ] 72 | ) 73 | + "\n" 74 | ) 75 | output += line_divider 76 | 77 | return output 78 | 79 | 80 | class DHCPClient(object): 81 | def __init__( 82 | self, 83 | interface: str = None, 84 | send_from_port: int = 68, 85 | send_to_port: int = 67, 86 | max_retries: int = 10, 87 | socket_poll_interval: int = 10, 88 | retry_interval: int = 100, 89 | ): 90 | self.listening_ports = [67] 91 | self.send_from_port = send_from_port 92 | self.send_to_port = send_to_port 93 | self.max_pkt_size = 4096 94 | self.interface = interface 95 | self.listening_sockets = self.get_listening_sockets() 96 | self.writing_sockets = self.get_writing_sockets() 97 | self.listening_sockets += self.writing_sockets 98 | # self.listening_sockets += self.writing_sockets 99 | logging.debug(f"listening sockets: {self.listening_sockets}") 100 | logging.debug(f"write sockets: {self.writing_sockets}") 101 | self.except_sockets: List[socket.socket] = [] 102 | self.max_tries = max_retries 103 | self.socket_poll_interval = socket_poll_interval 104 | self.retry_interval = retry_interval 105 | self.select_timout = 0 106 | self.offer_servers: List[str] = [] 107 | self.ack_server: str = "" 108 | 109 | def send_discover( 110 | self, server: str, discover_packet: packet.DHCPPacket, verbosity: int 111 | ): 112 | self.send(server, self.send_to_port, discover_packet.asbytes, verbosity) 113 | 114 | def receive_offer(self, tx_id: int, verbosity: int) -> Optional[packet.DHCPPacket]: 115 | logging.debug("Listening for offer packet...") 116 | if verbosity > 1: 117 | print("Listening for OFFER packet") 118 | offer, addr = self.listen(tx_id, "DHCPOFFER", verbosity) 119 | if offer: 120 | logging.debug(f"Received offer packet from {addr} {offer}") 121 | if verbosity > 1: 122 | print(f"<< OFFER received from {addr[0]}:{addr[1]}") 123 | print(format_dhcp_packet(offer)) 124 | self.offer_servers.append(addr) 125 | else: 126 | logging.debug("Did not receive offer, retrying") 127 | if verbosity > 1: 128 | print("Did not receive offer packet") 129 | return offer 130 | 131 | def send_request( 132 | self, server: str, request_packet: packet.DHCPPacket, verbosity: int 133 | ): 134 | self.send(server, self.send_to_port, request_packet.asbytes, verbosity) 135 | 136 | def receive_ack(self, tx_id: int, verbosity: int) -> Optional[packet.DHCPPacket]: 137 | logging.debug("Listening for ack packet...") 138 | if verbosity > 1: 139 | print("Listening for ACK packet") 140 | ack, addr = self.listen(tx_id, "DHCPACK", verbosity) 141 | if ack: 142 | logging.debug(f"Received ack packet from {addr} {ack}") 143 | if verbosity: 144 | print(f"<< ACK received from {addr[0]}:{addr[1]}") 145 | print(format_dhcp_packet(ack)) 146 | self.ack_server = addr 147 | else: 148 | logging.debug("Did not receive ack, retrying") 149 | if verbosity > 1: 150 | print("Did not receive ack packet") 151 | return ack 152 | 153 | def get_lease( 154 | self, 155 | mac_addr: Optional[str] = None, 156 | broadcast: bool = True, 157 | relay: Optional[str] = None, 158 | server: str = "255.255.255.255", 159 | ip_protocol: int = 4, 160 | options_list: Optional[options.OptionList] = None, 161 | verbose: int = 0, 162 | ) -> Lease: 163 | mac_addr = mac_addr or utils.random_mac() 164 | logging.debug("Synthetizing discover packet") 165 | 166 | # D 167 | discover = packet.DHCPPacket.Discover( 168 | mac_addr, use_broadcast=broadcast, option_list=options_list, relay=relay 169 | ) 170 | tx_id = discover.xid 171 | logging.debug(f"Constructed discover packet: {discover}") 172 | if verbose > 1: 173 | print(format_dhcp_packet(discover)) 174 | start = default_timer() 175 | logging.debug(f"Sending discover packet to {server} with {tx_id=}") 176 | self.send_discover(server, discover, verbose) 177 | # O 178 | tries = 0 179 | while not (offer := self.receive_offer(tx_id, verbose)): 180 | logging.debug(f"Sleeping {self.retry_interval} ms then retrying discover") 181 | sleep(self.retry_interval / 1000) 182 | logging.debug( 183 | f"Attempt {tries} - Sending discover packet to {server} with {tx_id=}" 184 | ) 185 | if verbose > 1: 186 | print("Resending DISCOVER packet") 187 | self.send_discover(server, discover, verbose) 188 | if tries > self.max_tries: 189 | raise DHCPClientError( 190 | "Unable to obtain offer run client with -d for debug info" 191 | ) 192 | tries += 1 193 | # R 194 | request = packet.DHCPPacket.Request( 195 | mac_addr, 196 | int(default_timer() - start), 197 | tx_id, 198 | use_broadcast=broadcast, 199 | option_list=options_list, 200 | client_ip=offer.yiaddr, 201 | relay=relay, 202 | ) 203 | if verbose > 1: 204 | print("REQUEST Packet") 205 | print(format_dhcp_packet(request)) 206 | logging.debug(f"Constructed request packet: {request}") 207 | logging.debug(f"Sending request packet to {server} with {tx_id=}") 208 | self.send_request(server, request, verbose) 209 | # A 210 | tries = 0 211 | while not (ack := self.receive_ack(tx_id, verbose)): 212 | logging.debug(f"Sleeping {self.retry_interval} ms then retrying request") 213 | sleep(self.retry_interval / 1000) 214 | logging.debug( 215 | f"Attempt {tries} - Sending request packet to {server} with {tx_id=}" 216 | ) 217 | if verbose > 1: 218 | print("Resending REQUEST packet") 219 | self.send_request(server, request, verbose) 220 | if tries > self.max_tries: 221 | raise DHCPClientError( 222 | "Unable to obtain ack run client with -d for debug info" 223 | ) 224 | tries += 1 225 | 226 | lease_time = default_timer() - start 227 | lease = Lease(discover, offer, request, ack, lease_time, self.ack_server) 228 | 229 | if verbose: 230 | print(f"Client terminated after {lease_time * 1000:.0f} ms") 231 | else: 232 | print( 233 | f"Lease succesful: {ack.yiaddr} -- {ack.chaddr} -- {lease_time * 1000:.0f} ms elapsed" 234 | ) 235 | return lease 236 | 237 | def get_valid_pkt(self, data: bytes) -> Optional[packet.DHCPPacket]: 238 | pkt = None 239 | try: 240 | pkt = packet.DHCPPacket.from_bytes(data) 241 | except Exception as e: 242 | logging.debug( 243 | f"Unable to parse received data as DHCP packet: {e} --- {data!r}" 244 | ) 245 | return pkt 246 | 247 | def listen( 248 | self, tx_id: int, msg_type: str, verbosity: int 249 | ) -> Tuple[Optional[packet.DHCPPacket], Optional[str]]: 250 | logging.debug( 251 | f"Listening on {self.interface or 'all interfaces'}, UDP ports {self.listening_ports}" 252 | ) 253 | tries = 0 254 | dhcp_packet, addr = None, None 255 | while tries < self.max_tries: 256 | logging.debug( 257 | f"Select: {select.select(self.listening_sockets, self.writing_sockets, self.except_sockets, 0)}" 258 | ) 259 | if len( 260 | socks := select.select( 261 | self.listening_sockets, 262 | self.writing_sockets, 263 | self.except_sockets, 264 | self.select_timout, 265 | )[0] 266 | ): 267 | for sock in socks: 268 | data, addr = sock.recvfrom(self.max_pkt_size) 269 | logging.debug(f"Received data from {addr}: {data}") 270 | if ( 271 | (dhcp_packet := self.get_valid_pkt(data)) is not None 272 | and dhcp_packet.xid == tx_id 273 | and dhcp_packet.msg_type == msg_type 274 | ): 275 | logging.debug( 276 | f"Received valid DHCP packet of {dhcp_packet.msg_type} type" 277 | ) 278 | return dhcp_packet, addr 279 | else: 280 | if dhcp_packet is None: 281 | logging.debug("Invalid DHCP packet") 282 | elif dhcp_packet.xid != tx_id: 283 | logging.debug( 284 | f"TX ID does not match expected ID {dhcp_packet.xid} != {tx_id}" 285 | ) 286 | elif (msg_type_actual := dhcp_packet.msg_type) != msg_type: 287 | logging.debug( 288 | f"DHCP message type does not match expected: {msg_type_actual} != {msg_type}" 289 | ) 290 | else: 291 | logging.debug("Something is wrong with this packet") 292 | logging.debug(dhcp_packet) 293 | dhcp_packet = None 294 | tries += 1 295 | else: 296 | logging.debug( 297 | f"Attempt {tries} - No sockets available to read from... " 298 | f"sleeping for {self.socket_poll_interval} ms" 299 | ) 300 | if verbosity > 2: 301 | print("Did not receive packet, sleeping...") 302 | tries += 1 303 | sleep(self.socket_poll_interval / 1000) 304 | return dhcp_packet, addr 305 | 306 | def get_socket(self, host: str, port: int) -> socket.socket: 307 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 308 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) 309 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 310 | sock.setblocking(False) 311 | if self.interface: 312 | try: 313 | # Option 25 is SO_BINDTODEVICE, allows us to specify a device 314 | # to bind to with this socket 315 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, self.interface.encode()) 316 | logging.info(f"Binding to {self.interface}") 317 | except: 318 | # Less reliable method of binding to interface, required where 319 | # socket option 25 does not exist (Windows) 320 | sock.bind((utils.get_ip_by_iface(self.interface), port)) 321 | else: 322 | sock.bind((host, port)) 323 | else: 324 | sock.bind((host, port)) 325 | 326 | logging.info(f"Bound {socket}") 327 | return sock 328 | 329 | def get_writing_sockets(self) -> List[socket.socket]: 330 | host = "" 331 | port = self.send_from_port 332 | logging.debug(f"Creating socket to send data, binding to {(host, port)}") 333 | client_sock = self.get_socket(host, port) 334 | return [client_sock] 335 | 336 | def get_listening_sockets(self) -> List[socket.socket]: 337 | socks = [] 338 | host = "" 339 | for port in self.listening_ports: 340 | logging.debug(f"Creating socket to receiving data, binding to {(host, port)}") 341 | server_sock = self.get_socket(host, port) 342 | socks.append(server_sock) 343 | return socks 344 | 345 | def send(self, remote_addr: str, remote_port: int, data: bytes, verbosity: int): 346 | tries = 0 347 | while tries < self.max_tries: 348 | logging.debug(f"Select: {select.select(self.listening_sockets, self.writing_sockets, self.except_sockets, self.select_timout,)}") 349 | if len( 350 | socks := select.select( 351 | self.listening_sockets, 352 | self.writing_sockets, 353 | self.except_sockets, 354 | self.select_timout, 355 | )[1] 356 | ): 357 | sock = socks[0] 358 | logging.debug(f"Connecting to {remote_addr}:{remote_port}") 359 | logging.debug(f"Sending data {data!r}") 360 | if verbosity > 1: 361 | print(f">> Sending packet {remote_addr}:{remote_port}") 362 | sock.sendto(data, (remote_addr, remote_port)) 363 | logging.debug(f"Packet Sent") 364 | break 365 | else: 366 | logging.warning( 367 | f"Attempt {tries} - No sockets available to write to... " 368 | f"sleeping for {self.socket_poll_interval} ms" 369 | ) 370 | tries += 1 371 | sleep(self.socket_poll_interval / 1000) 372 | -------------------------------------------------------------------------------- /dhcppython/exceptions.py: -------------------------------------------------------------------------------- 1 | class DHCPException(Exception): 2 | """ 3 | Base exception for our DHCP functions 4 | """ 5 | 6 | class MalformedPacketError(DHCPException): 7 | """ 8 | The DHCP packet has some sort of issue 9 | """ 10 | 11 | 12 | class DHCPValueError(DHCPException): 13 | """ 14 | Something wrong with the DHCP semantics 15 | """ 16 | 17 | 18 | class DHCPClientError(DHCPException): 19 | """ 20 | Something went wrong in the DHCP client 21 | """ 22 | -------------------------------------------------------------------------------- /dhcppython/options.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides set of classes for decoding and encoding DHCP options. 3 | 4 | A high level API is provided by the `options` object and the Option class: 5 | 6 | 1. Create an options object from bytes by calling the `bytes_to_object` method 7 | e.g., 8 | 9 | ``` 10 | >>> options.bytes_to_object(b'\x3d\x07\x01\x8c\x45\x00\x1d\x48\x16') 11 | ClientIdentifier(code=61, length=7, data=b'\x01\x8cE\x00\x1dH\x16') 12 | ``` 13 | 14 | 2. Get a human readable dict of an options object value 15 | 16 | ``` 17 | >>> ClientIdentifier(code=61, length=7, data=b'\x01\x8cE\x00\x1dH\x16').value 18 | {'client_identifier': {'hwtype': 1, 'hwaddr': '8C:45:00:1D:48:16'}} 19 | >>> options.bytes_to_object(b'\x3d\x07\x01\x8c\x45\x00\x1d\x48\x16').value 20 | {'client_identifier': {'hwtype': 1, 'hwaddr': '8C:45:00:1D:48:16'}} 21 | ``` 22 | 23 | 3. Create an options object from its human readable dict of its value: 24 | 25 | ``` 26 | >>> options.value_to_object({'client_identifier': {'hwtype': 1, 'hwaddr': '8C:45:00:1D:48:16'}}) 27 | ClientIdentifier(code=61, length=7, data=b'\x01\x8cE\x00\x1dH\x16') 28 | ``` 29 | 30 | 4. Convert a human readable dict of an option value to the bytes representation 31 | 32 | ``` 33 | >>> options.value_to_bytes(({'client_identifier': {'hwtype': 1, 'hwaddr': '8C:45:00:1D:48:16'}})) 34 | b'=\x07\x01\x8cE\x00\x1dH\x16' 35 | >>> [int(i) for i in options.value_to_bytes(({'client_identifier': {'hwtype': 1, 'hwaddr': '8C:45:00:1D:48:16'}}))] 36 | [61, 7, 1, 140, 69, 0, 29, 72, 22] 37 | ``` 38 | 39 | 5. Get the bytes representation of an option given its Option object 40 | 41 | ``` 42 | >>> ClientIdentifier(code=61, length=7, data=b'\x01\x8cE\x00\x1dH\x16').asbytes 43 | b'=\x07\x01\x8cE\x00\x1dH\x16' 44 | ``` 45 | 46 | 6. Create an options object from its code and "short value" 47 | 48 | ``` 49 | >>> options.short_value_to_object(61, {'hwtype': 1, 'hwaddr': '8C:45:00:1D:48:16'}) 50 | ClientIdentifier(code=61, length=7, data=b'\x01\x8cE\x00\x1dH\x16') 51 | ``` 52 | """ 53 | from __future__ import annotations 54 | import csv 55 | from abc import ABC, abstractmethod 56 | import collections.abc 57 | from typing import Dict, Union, List, Tuple, Optional, TypedDict 58 | import ipaddress 59 | import struct 60 | import json 61 | import importlib.resources 62 | from .exceptions import DHCPValueError 63 | from . import runtime_assets 64 | 65 | OPTIONS: Dict[int, Dict[str, Union[str, int]]] = { 66 | int(line[0]): { 67 | "name": line[1], 68 | "len": int(line[2]) if line[2].isdigit() else line[2], 69 | "description": line[3], 70 | "rfc": line[4].split("RFC")[-1][:-1], 71 | } 72 | for line in csv.reader(importlib.resources.open_text(runtime_assets, "options.csv")) 73 | if line[0].isdigit() 74 | } 75 | 76 | 77 | class CodeDataMapping(TypedDict): 78 | obj: Option 79 | index: int 80 | 81 | 82 | class OptionList(collections.abc.MutableSequence): 83 | def __init__(self, options_array: Optional[List[Option]] = None): 84 | self.data: List[Option] = list(options_array) if options_array else [] 85 | self.code_to_data: Dict[int, CodeDataMapping] = { 86 | opt.code: {"obj": opt, "index": i} for i, opt in enumerate(self.data) 87 | } 88 | 89 | def __repr__(self): 90 | return f"OptionList({self.data})" 91 | 92 | def by_code(self, code: int) -> Optional[Option]: 93 | return self.code_to_data.get(code, {}).get("obj") 94 | 95 | def append(self, item: Option): 96 | if item.code not in self.code_to_data: 97 | self.data.append(item) 98 | self.code_to_data[item.code] = {"obj": item, "index": len(self.data) - 1} 99 | else: 100 | self.data[self.code_to_data[item.code]["index"]] = item 101 | self.code_to_data[item.code]["obj"] = item 102 | 103 | def insert(self, index: int, obj: Option): 104 | if obj.code in self.code_to_data: 105 | # delete previous object and insert this one at the specified pos 106 | del self[self.code_to_data[obj.code]["index"]] 107 | 108 | self.data.insert(index, obj) 109 | 110 | # Re-index entire list... 111 | for opt in self.code_to_data.values(): 112 | if opt["index"] >= index: 113 | opt["index"] += 1 114 | 115 | self.code_to_data[obj.code] = { 116 | "obj": obj, 117 | "index": index, 118 | } 119 | 120 | def __len__(self): 121 | return len(self.data) 122 | 123 | def __getitem__(self, key: int) -> Option: 124 | return self.data[key] 125 | 126 | def __setitem__(self, key: int, value: Option): 127 | # Remove entry of option in current index 128 | for opt in self.code_to_data.values(): 129 | if opt["index"] == key: 130 | del self.code_to_data[opt["obj"].code] 131 | break 132 | # update self.data list with object 133 | self.data[key] = value 134 | # reindex the object that is in the list 135 | self.code_to_data[value.code] = { 136 | "obj": value, 137 | "index": key, 138 | } 139 | for index, opt in enumerate(self.data): 140 | if opt.code == value.code and index != key: 141 | del self[index] 142 | if index < key: 143 | self.code_to_data[value.code] = { 144 | "obj": value, 145 | "index": key, 146 | } 147 | break 148 | 149 | def __delitem__(self, key: int): 150 | code = self.data[key].code 151 | # problematic cause it reindexes the whole list 152 | for opt in self.code_to_data.values(): 153 | if opt["index"] > key: 154 | opt["index"] -= 1 155 | del self.code_to_data[code] 156 | del self.data[key] 157 | 158 | def __contains__(self, other): 159 | if hasattr(other, "asbytes"): 160 | return other in self.data 161 | return other in self.code_to_data 162 | 163 | def __eq__(self, other): 164 | for self_item, other_item in zip(self, other): 165 | if not (self_item == other_item): 166 | return False 167 | return True 168 | 169 | def as_dict(self): 170 | opt_dict = {} 171 | for opt in self.data: 172 | opt_dict.update(opt.value) 173 | return opt_dict 174 | 175 | @property 176 | def json(self): 177 | return json.dumps(self.as_dict(), indent=4) 178 | 179 | 180 | class OptionDirectory(object): 181 | def __init__(self): 182 | self.directory = {} 183 | self.key_code_map = {} 184 | temp = dict(globals()) 185 | for obj in temp: 186 | try: 187 | cls = globals()[obj] 188 | code = cls.__dict__["code"] 189 | key = cls.__dict__["key"] 190 | except: 191 | pass 192 | else: 193 | self.directory[code] = cls 194 | self.key_code_map[key] = code 195 | 196 | def value_to_code(self, value: dict) -> int: 197 | code = self.key_code_map.get(list(value)[0]) 198 | return code 199 | 200 | def code_to_class(self, code: int) -> Option: 201 | return self.directory.get(code, UnknownOption) 202 | 203 | def value_to_bytes(self, value: dict): 204 | code = self.value_to_code(value) 205 | return self.code_to_class(code).from_value(value).asbytes 206 | 207 | def value_to_object(self, value: dict): 208 | code = self.value_to_code(value) 209 | return self.code_to_class(code).from_value(value) 210 | 211 | def short_value_to_object( 212 | self, code: int, short_value: Union[str, int, bool, dict, List[int], List[str]] 213 | ): 214 | cls = self.code_to_class(code) 215 | return cls.from_value({cls.key: short_value}) 216 | 217 | def bytes_to_object(self, data: bytes): 218 | if data[0] in [0, 255]: 219 | code, length, data = (data[0], 0, b"") 220 | else: 221 | code, length, data = struct.unpack(f">BB{len(data) - 2}s", data) 222 | return self.code_to_class(code)(code, length, data) 223 | 224 | 225 | class Option(ABC): 226 | # __slots__ = ("code", "key", "length", "data", "_value", "name", "description") # Probably dont need this right now 227 | code: int = -1 228 | key = "" 229 | 230 | def __init__(self, code: int, length: int, data: bytes) -> None: 231 | global OPTIONS 232 | # Option code, single byte, values from 0 to 255 are valid 233 | if code != self.code: 234 | raise DHCPValueError(f"Option code does not match {code} != {self.code}") 235 | self.length = ( 236 | length # Option size (# of bytes), options 0 and 255 are fixed size (0) 237 | ) 238 | self.data = data # Option data in bytes 239 | self._value: Optional[dict] = None 240 | self.name = OPTIONS.get(self.code, {}).get("name", "Unknown") 241 | self.description = OPTIONS.get(self.code, {}).get("description", "Unknown") 242 | 243 | def __repr__(self): 244 | return f"{self.__class__.__name__}(code={self.code}, length={self.length}, data={self.data})" 245 | 246 | def __eq__(self, other): 247 | return self.asbytes == other.asbytes 248 | 249 | @property 250 | @abstractmethod 251 | def value(self): 252 | """ 253 | DHCP option data in Python dict containing human readable keys and 254 | values 255 | """ 256 | 257 | @classmethod 258 | @abstractmethod 259 | def from_value(cls, value): 260 | """ 261 | Construct the option class given a dict of option kvps 262 | """ 263 | 264 | @property 265 | def asbytes(self) -> bytes: 266 | """ 267 | Wireformat for option including code and length 268 | """ 269 | return struct.pack(">BB", self.code, self.length) + self.data 270 | 271 | def data2IParray(self) -> List[str]: 272 | """ 273 | It is common to see lists of IP addrs as option values. This returns a 274 | list of IPs from the options data. 275 | """ 276 | num_addrs = len(self.data) // 4 277 | return [ 278 | str(ipaddress.IPv4Address(ip)) 279 | for ip in struct.unpack(">" + "L" * num_addrs, self.data) 280 | ] 281 | 282 | def data2string(self) -> str: 283 | """ 284 | Converts a data field to a string. 285 | """ 286 | return struct.unpack(f">{len(self.data)}s", self.data)[0].decode().strip() 287 | 288 | def data2bool(self) -> bool: 289 | """ 290 | Converts data to bool value. 291 | """ 292 | return struct.unpack(">?", self.data)[0] 293 | 294 | def data2bin(self) -> str: 295 | """ 296 | Converts data to a string representation of the binary data 297 | """ 298 | return " ".join([f"0x{d:02X}" for d in self.data]) 299 | 300 | def data2IPpairs(self) -> List[Tuple[str, str]]: 301 | """ 302 | Converts data to tuples of IP pairs. 303 | """ 304 | num_pairs = len(self.data) // 8 305 | pairs: List[Tuple[str, str]] = [] 306 | for i in range(num_pairs): 307 | ip1, ip2 = [ 308 | str(ipaddress.IPv4Address(ip)) 309 | for ip in struct.unpack(">LL", self.data[i * 8 : (i + 1) * 8]) 310 | ] 311 | pairs.append((ip1, ip2)) 312 | return pairs 313 | 314 | def data2uint8(self) -> int: 315 | """ 316 | Converts data to unsigned 8 bit integer. 317 | """ 318 | return struct.unpack(">B", self.data)[0] 319 | 320 | def data2uint16(self) -> int: 321 | """ 322 | Converts data to unsigned 16 bit integer. 323 | """ 324 | return struct.unpack(">H", self.data)[0] 325 | 326 | def data2uint32(self) -> int: 327 | """ 328 | Converts data to unsigned 32 bit integer. 329 | """ 330 | return struct.unpack(">L", self.data)[0] 331 | 332 | def data2int32(self) -> int: 333 | """ 334 | Converts data to signed 32 bit integer. 335 | """ 336 | return struct.unpack(">l", self.data)[0] 337 | 338 | def data2uint8array(self) -> List[int]: 339 | """ 340 | Converts data to list of unsigned 8 bit integers. 341 | """ 342 | return list(struct.unpack(">" + "B" * len(self.data), self.data)) 343 | 344 | def data2uint16array(self) -> List[int]: 345 | """ 346 | Converts data to list of unsigned 16 bit integers. 347 | """ 348 | return list(struct.unpack(">" + "H" * (len(self.data) // 2), self.data)) 349 | 350 | @staticmethod 351 | def IParray2data(value: List[str]) -> bytes: 352 | """ 353 | Converts list of IP addresses to bytes 354 | """ 355 | return b"".join([ipaddress.IPv4Address(ip).packed for ip in value]) 356 | 357 | @staticmethod 358 | def int32array2data(value: List[int]) -> bytes: 359 | """ 360 | Converts list of int32s to bytes 361 | """ 362 | return struct.pack(">" + "l" * len(value), *value) 363 | 364 | @staticmethod 365 | def uint8array2data(value: List[int]) -> bytes: 366 | """ 367 | Converts list of uint8s to bytes 368 | """ 369 | return struct.pack(">" + "B" * len(value), *value) 370 | 371 | @staticmethod 372 | def uint16array2data(value: List[int]) -> bytes: 373 | """ 374 | Converts list of uint16s to bytes 375 | """ 376 | return struct.pack(">" + "H" * len(value), *value) 377 | 378 | @staticmethod 379 | def uint32array2data(value: List[int]) -> bytes: 380 | """ 381 | Converts list of uint32s to bytes 382 | """ 383 | return struct.pack(">" + "L" * len(value), *value) 384 | 385 | @staticmethod 386 | def bool2data(value: bool) -> bytes: 387 | """ 388 | Converts bool to bytes 389 | """ 390 | return struct.pack(">?", value) 391 | 392 | @staticmethod 393 | def bin2data(value: str) -> bytes: 394 | """ 395 | Converts string representing binary data to bytes 396 | """ 397 | return struct.pack( 398 | ">" + "B" * len(value.split()), *[int(val[2:], 16) for val in value.split()] 399 | ) 400 | 401 | 402 | class BinOption(Option): 403 | """ 404 | Generic implementation of binary option 405 | """ 406 | 407 | @property 408 | def value(self) -> Dict[str, str]: 409 | if self._value is None: 410 | self._value = {self.key: self.data2bin()} 411 | return self._value 412 | 413 | @classmethod 414 | def from_value(cls, value: Dict[str, str]): 415 | is_unknown_option = True if cls.code == -1 else False 416 | if is_unknown_option: 417 | code = int(list(value)[0].split("_")[1]) 418 | key = list(value)[0] 419 | else: 420 | code = cls.code 421 | key = cls.key 422 | data = cls.bin2data(value[key]) 423 | return cls(code, len(data), data) 424 | 425 | 426 | class BoolOption(Option): 427 | """ 428 | Generic implementation of boolean option 429 | """ 430 | 431 | @property 432 | def value(self) -> Dict[str, bool]: 433 | if self._value is None: 434 | self._value = {self.key: self.data2bool()} 435 | return self._value 436 | 437 | @classmethod 438 | def from_value(cls, value: Dict[str, bool]): 439 | data = cls.bool2data(value[cls.key]) 440 | return cls(cls.code, len(data), data) 441 | 442 | 443 | class StrOption(Option): 444 | """ 445 | Generic implementation of string option 446 | """ 447 | 448 | @property 449 | def value(self) -> Dict[str, str]: 450 | if self._value is None: 451 | self._value = {self.key: self.data2string()} 452 | return self._value 453 | 454 | @classmethod 455 | def from_value(cls, value: Dict[str, str]): 456 | data = value[cls.key].encode() 457 | return cls(cls.code, len(data), data) 458 | 459 | 460 | class IPOption(Option): 461 | """ 462 | Generic implementation of an IP option 463 | """ 464 | 465 | @property 466 | def value(self) -> Dict[str, str]: 467 | if self._value is None: 468 | self._value = {self.key: self.data2IParray()[0]} 469 | return self._value 470 | 471 | @classmethod 472 | def from_value(cls, value: Dict[str, str]): 473 | data = cls.IParray2data([value[cls.key]]) 474 | return cls(cls.code, len(data), data) 475 | 476 | 477 | class IPArrayOption(Option): 478 | """ 479 | Generic implementation of an IP array 480 | """ 481 | 482 | @property 483 | def value(self) -> Dict[str, List[str]]: 484 | if self._value is None: 485 | self._value = {self.key: self.data2IParray()} 486 | return self._value 487 | 488 | @classmethod 489 | def from_value(cls, value: Dict[str, List[str]]): 490 | data = cls.IParray2data(value[cls.key]) 491 | return cls(cls.code, len(data), data) 492 | 493 | 494 | class uint8Option(Option): 495 | """ 496 | Generic implementation of an uint8 option 497 | """ 498 | 499 | @property 500 | def value(self) -> Dict[str, int]: 501 | if self._value is None: 502 | self._value = {self.key: self.data2uint8()} 503 | return self._value 504 | 505 | @classmethod 506 | def from_value(cls, value: Dict[str, int]): 507 | data = cls.uint8array2data([value[cls.key]]) 508 | return cls(cls.code, len(data), data) 509 | 510 | 511 | class uint16Option(Option): 512 | """ 513 | Generic implementation of an uint16 option 514 | """ 515 | 516 | @property 517 | def value(self) -> Dict[str, int]: 518 | if self._value is None: 519 | self._value = {self.key: self.data2uint16()} 520 | return self._value 521 | 522 | @classmethod 523 | def from_value(cls, value: Dict[str, int]): 524 | data = cls.uint16array2data([value[cls.key]]) 525 | return cls(cls.code, len(data), data) 526 | 527 | 528 | class uint32Option(Option): 529 | """ 530 | Generic implementation of an uint32 option 531 | """ 532 | 533 | @property 534 | def value(self) -> Dict[str, int]: 535 | if self._value is None: 536 | self._value = {self.key: self.data2uint32()} 537 | return self._value 538 | 539 | @classmethod 540 | def from_value(cls, value: Dict[str, int]): 541 | data = cls.uint32array2data([value[cls.key]]) 542 | return cls(cls.code, len(data), data) 543 | 544 | 545 | class uint8ArrayOption(Option): 546 | """ 547 | Generic implementation of an uint8 array option 548 | """ 549 | 550 | @property 551 | def value(self) -> Dict[str, List[int]]: 552 | if self._value is None: 553 | self._value = {self.key: self.data2uint8array()} 554 | return self._value 555 | 556 | @classmethod 557 | def from_value(cls, value: Dict[str, List[int]]): 558 | data = cls.uint8array2data(value[cls.key]) 559 | return cls(cls.code, len(data), data) 560 | 561 | 562 | class uint16ArrayOption(Option): 563 | """ 564 | Generic implementation of an uint16 array option 565 | """ 566 | 567 | @property 568 | def value(self) -> Dict[str, List[int]]: 569 | if self._value is None: 570 | self._value = {self.key: self.data2uint16array()} 571 | return self._value 572 | 573 | @classmethod 574 | def from_value(cls, value: Dict[str, List[int]]): 575 | data = cls.uint16array2data(value[cls.key]) 576 | return cls(cls.code, len(data), data) 577 | 578 | 579 | class int32Option(Option): 580 | """ 581 | Generic implementation of an int32 option 582 | """ 583 | 584 | @property 585 | def value(self) -> Dict[str, int]: 586 | if self._value is None: 587 | self._value = {self.key: self.data2int32()} 588 | return self._value 589 | 590 | @classmethod 591 | def from_value(cls, value: Dict[str, int]): 592 | data = cls.int32array2data([value[cls.key]]) 593 | return cls(cls.code, len(data), data) 594 | 595 | 596 | class Pad(Option): 597 | """ 598 | Option 0 599 | 600 | The pad option can be used to cause subsequent fields to align on word 601 | boundaries. 602 | """ 603 | 604 | code = 0 605 | key = "pad_option" 606 | 607 | @property 608 | def value(self) -> Dict[str, str]: 609 | return {self.key: ""} 610 | 611 | @classmethod 612 | def from_value(cls, value: dict): 613 | return cls(0, 0, b"") 614 | 615 | @property 616 | def asbytes(self) -> bytes: 617 | return b"\x00" 618 | 619 | 620 | class End(Option): 621 | """ 622 | Option 255 623 | 624 | End 625 | """ 626 | 627 | code = 255 628 | key = "end_option" 629 | 630 | @property 631 | def value(self) -> Dict[str, str]: 632 | return {self.key: ""} 633 | 634 | @classmethod 635 | def from_value(cls, value: dict): 636 | return cls(255, 0, b"") 637 | 638 | @property 639 | def asbytes(self): 640 | return b"\xff" 641 | 642 | 643 | class SubnetMask(IPOption): 644 | """ 645 | Option 1 646 | Subnet Mask 647 | If both the subnet mask and the router option are specified in a DHCP 648 | reply, the subnet mask option MUST be first. 649 | 650 | e.g., 255.255.255.0 651 | 652 | Option value defined as {"subnet_mask": '255.255.255.0'} 653 | """ 654 | 655 | code = 1 656 | key = "subnet_mask" 657 | 658 | 659 | class TimeOffset(int32Option): 660 | """ 661 | Option 2 662 | Time Offset 663 | Specifies the offset of the client's subnet in seconds from Coordinated 664 | Universal Time (UTC). 665 | 666 | e.g., 3600 seconds (+1 hours) 667 | Option value defined as {"time_offset_s": 3600, "time_offset_h": 1]} 668 | """ 669 | 670 | code = 2 671 | key = "time_offset_s" 672 | 673 | 674 | class Router(IPArrayOption): 675 | """ 676 | Option 3 677 | Specifies a list of IP addresses for routers on the client's subnet. 678 | Routers SHOULD be listed in order of preference. 679 | 680 | Minimum length for the router option is 4 octets, and the length MUST 681 | always be a multiple of 4. 682 | 683 | e.g., 192.168.0.1 684 | Option value defined as {"routers": ['1.1.1.1', '2.2.2.2']} 685 | """ 686 | 687 | code = 3 688 | key = "routers" 689 | 690 | 691 | class TimeServer(IPArrayOption): 692 | """ 693 | Option 4 694 | Specifies a list of RFC 868 [6] time servers available to the client. 695 | Servers SHOULD be listed in order of preference. 696 | 697 | The minimum length for 698 | this option is 4 octets, and the length MUST always be a multiple of 699 | 4. 700 | 701 | Option value defined as {"time_servers": ['1.1.1.1', ...]} 702 | """ 703 | 704 | code = 4 705 | key = "time_servers" 706 | 707 | 708 | class NameServer(IPArrayOption): 709 | """ 710 | Option 5 711 | 712 | Specifies a list of IEN 116 name servers available to the client. 713 | 714 | Listed in order, multiple of 4 715 | 716 | Option value defined as {"name_servers": ['1.1.1.1', ...]} 717 | """ 718 | 719 | code = 5 720 | key = "name_servers" 721 | 722 | 723 | class DNSServer(IPArrayOption): 724 | """ 725 | Option 6 726 | 727 | Specifies a list of Domain Name System (STD 13, RFC 1035) name servers 728 | available. 729 | 730 | Listed in order, multiple of 4 731 | 732 | Option value defined as {"dns_servers": ['1.1.1.1', ...]} 733 | """ 734 | 735 | code = 6 736 | key = "dns_servers" 737 | 738 | 739 | class LogServer(IPArrayOption): 740 | """ 741 | Option 7 742 | 743 | Specifies a list of MIT-LCS UDP log servers available to the client. 744 | 745 | Listed in order, multiple of 4 746 | 747 | Option value defined as {"log_servers": ['1.1.1.1', ...]} 748 | """ 749 | 750 | code = 7 751 | key = "log_servers" 752 | 753 | 754 | class CookieServer(IPArrayOption): 755 | """ 756 | Option 8 757 | 758 | Specifies a list of RFC 865 [9] cookie servers available to the client. 759 | 760 | Listed in order, multiple of 4 761 | Option value defined as {"cookie_servers": ['1.1.1.1', ...]} 762 | """ 763 | 764 | code = 8 765 | key = "cookie_servers" 766 | 767 | 768 | class LPRServer(IPArrayOption): 769 | """ 770 | Option 9 771 | 772 | Specifies a list of RFC 1179 [10] line printer servers available to the client. 773 | 774 | Listed in order, multiple of 4 775 | Option value defined as {"lpr_servers": ['1.1.1.1', ...]} 776 | """ 777 | 778 | code = 9 779 | key = "lpr_servers" 780 | 781 | 782 | class ImpressServer(IPArrayOption): 783 | """ 784 | Option 10 785 | 786 | Specifies a list of Imagen Impress servers available to the client. 787 | 788 | Listed in order, multiple of 4 789 | Option value defined as {"impress_servers": ['1.1.1.1', ...]} 790 | """ 791 | 792 | code = 10 793 | key = "impress_servers" 794 | 795 | 796 | class ResourceLocationServer(IPArrayOption): 797 | """ 798 | Option 11 799 | 800 | Specifies a list of RFC 887 [11] Resource Location servers available to the client. 801 | 802 | Listed in order, multiple of 4 803 | Option value defined as {"resource_location_servers": ['1.1.1.1', ...]} 804 | """ 805 | 806 | code = 11 807 | key = "resource_location_servers" 808 | 809 | 810 | class Hostname(StrOption): 811 | """ 812 | Option 12 813 | 814 | Specifies the name of the client. The name may or may not be qualified 815 | with the local domain name (see section 3.17 for the preferred way to 816 | retrieve the domain name). See RFC 1035 for character set restrictions. 817 | 818 | Min len 1 819 | Option value defined as {"hostname": "laptop01"} 820 | """ 821 | 822 | code = 12 823 | key = "hostname" 824 | 825 | 826 | class BootfileSize(uint16Option): 827 | """ 828 | Option 13 829 | 830 | Specifies the length in 512-octet blocks of the default boot image for 831 | the client. 832 | 833 | Len 2 834 | Option value defined as {"bootfile_size": 256} 835 | """ 836 | 837 | code = 13 838 | key = "bootfile_size" 839 | 840 | 841 | class MeritDumpFile(StrOption): 842 | """ 843 | Option 14 844 | 845 | Specifies the path-name of a file to which the client's core image 846 | should be dumped in the event the client crashes. 847 | 848 | Min len 1 849 | Option value defined as {"merit_dump_file": "something"} 850 | """ 851 | 852 | code = 14 853 | key = "merit_dump_file" 854 | 855 | 856 | class DomainName(StrOption): 857 | """ 858 | Option 15 859 | 860 | Specifies the domain name that client should use when resolving 861 | hostnames via the Domain Name System. 862 | 863 | Min len 1 864 | Option value defined as {"domain_name": "google.com"} 865 | """ 866 | 867 | code = 15 868 | key = "domain_name" 869 | 870 | 871 | class SwapServer(IPOption): 872 | """ 873 | Option 16 874 | 875 | Sspecifies the IP address of the client's swap server. 876 | 877 | Len 4 878 | Option value defined as {"swap_server": "1.1.1.1"} 879 | """ 880 | 881 | code = 16 882 | key = "swap_server" 883 | 884 | 885 | class RootPath(StrOption): 886 | """ 887 | Option 17 888 | 889 | Specifies the path-name that contains the client's root disk. 890 | 891 | Min len 1 892 | Option value defined as {"root_path": "something"} 893 | """ 894 | 895 | code = 17 896 | key = "root_path" 897 | 898 | 899 | class ExtensionPath(StrOption): 900 | """ 901 | Option 18 902 | 903 | String to specify a file, retrievable via TFTP, which contains 904 | information which can be interpreted in the same way as the 64-octet 905 | vendor-extension field within the BOOTP response. 906 | 907 | Option value defined as {"extensions_path": "something"} 908 | """ 909 | 910 | code = 18 911 | key = "extensions_path" 912 | 913 | 914 | class IPForwarding(BoolOption): 915 | """ 916 | Option 19 917 | 918 | Specifies whether the client should configure its IP layer for packet 919 | forwarding. 920 | 921 | Option value defined as {"ip_forwarding": True} 922 | """ 923 | 924 | code = 19 925 | key = "ip_forwarding" 926 | 927 | 928 | class NonLocalSourceRouting(BoolOption): 929 | """ 930 | Option 20 931 | 932 | Specifies whether the client should configure its IP layer to allow 933 | forwarding of datagrams with non-local source routes. 934 | 935 | Option value defined as {"non_local_source_routing": True} 936 | """ 937 | 938 | code = 20 939 | key = "non_local_source_routing" 940 | 941 | 942 | class PolicyFilter(Option): 943 | """ 944 | Option 21 945 | 946 | Specifies policy filters for non-local source routing. The filters 947 | consist of a list of IP addresses and masks which specify 948 | destination/mask pairs with which to filter incoming source routes. 949 | 950 | Option value defined as: 951 | { 952 | "policy_filters": [{"address": "1.1.1.1", "mask": "255.255.255.0"}, ...] 953 | } 954 | """ 955 | 956 | code = 21 957 | key = "policy_filters" 958 | 959 | @property 960 | def value(self) -> Dict[str, List[Dict[str, str]]]: 961 | if self._value is None: 962 | self._value = { 963 | self.key: [ 964 | {"address": pair[0], "mask": pair[1]} 965 | for pair in self.data2IPpairs() 966 | ] 967 | } 968 | return self._value 969 | 970 | @classmethod 971 | def from_value(cls, value: Dict[str, List[Dict[str, str]]]): 972 | ip_array: List[str] = [] 973 | for pair in value[cls.key]: 974 | ip_array.extend(pair.values()) 975 | data = cls.IParray2data(ip_array) 976 | return cls(cls.code, len(data), data) 977 | 978 | 979 | class MaxDGRAMReassemblySize(uint16Option): 980 | """ 981 | Option 22 982 | 983 | Specifies the maximum size datagram that the client should be prepared 984 | to reassemble. 985 | 986 | Option value defined as {"max_datagram_reassembly_size": 512} 987 | """ 988 | 989 | code = 22 990 | key = "max_datagram_reassembly_size" 991 | 992 | 993 | class IPTTL(uint8Option): 994 | """ 995 | Option 23 996 | 997 | Specifies the default time-to-live that the client should use on 998 | outgoing datagrams. 999 | 1000 | Object value is defined as: {"default_ip_ttl": 123} 1001 | """ 1002 | 1003 | code = 23 1004 | key = "default_ip_ttl" 1005 | 1006 | 1007 | class PathMTUAgingTimeout(uint32Option): 1008 | """ 1009 | Option 24 1010 | 1011 | Specifies the timeout (in seconds) to use when aging Path MTU values 1012 | discovered by the mechanism defined. 1013 | 1014 | Len 4 1015 | Object value is defined as: {"path_MTU_aging_timeout":1234} 1016 | """ 1017 | 1018 | code = 24 1019 | key = "path_MTU_aging_timeout" 1020 | 1021 | 1022 | class PathMTUAgingTable(uint16ArrayOption): 1023 | """ 1024 | Option 25 1025 | 1026 | Specifies a table of MTU sizes to use when performing Path MTU Discovery 1027 | as defined in RFC 1191. 1028 | 1029 | Object value defined as: {"path_mtu_aging_table": [123, 234, ...]} 1030 | """ 1031 | 1032 | code = 25 1033 | key = "path_mtu_aging_table" 1034 | 1035 | 1036 | class InterfaceMTU(uint16Option): 1037 | """ 1038 | Option 26 1039 | 1040 | Specifies the MTU to use on this interface. 1041 | Object value defined as: {"interface_mtu": 1234} 1042 | """ 1043 | 1044 | code = 26 1045 | key = "interface_mtu" 1046 | 1047 | 1048 | class AllSubnetsLocal(BoolOption): 1049 | """ 1050 | Option 27 1051 | 1052 | Specifies whether or not the client may assume that all subnets of the 1053 | IP network to which the client is connected use the same MTU as the 1054 | subnet of that network to which the client is directly connected. 1055 | 1056 | Option value defined as: {"all_subnets_local": True} 1057 | """ 1058 | 1059 | code = 27 1060 | key = "all_subnets_local" 1061 | 1062 | 1063 | class BroadcastAddress(IPOption): 1064 | """ 1065 | Option 28 1066 | 1067 | Specifies the broadcast address in use on the client's subnet. 1068 | 1069 | Objected defined as: {"broadcast_address": "1.1.1.1"} 1070 | """ 1071 | 1072 | code = 28 1073 | key = "broadcast_address" 1074 | 1075 | 1076 | class PerformMaskDiscovery(BoolOption): 1077 | """ 1078 | Option 29 1079 | 1080 | Specifies whether or not the client should perform subnet mask 1081 | discovery using ICMP. 1082 | 1083 | Object value defined as: {"perform_mask_discovery"} 1084 | """ 1085 | 1086 | code = 29 1087 | key = "perform_mask_discovery" 1088 | 1089 | 1090 | class MaskSupplier(BoolOption): 1091 | """ 1092 | Option 30 1093 | 1094 | Specifies whether or not the client should respond to subnet mask 1095 | requests using ICMP. 1096 | 1097 | Object defined as: {"mask_supplier": True} 1098 | """ 1099 | 1100 | code = 30 1101 | key = "mask_supplier" 1102 | 1103 | 1104 | class PerformRouterDiscovery(BoolOption): 1105 | """ 1106 | Option 31 1107 | 1108 | Specifies whether or not the client should solicit routers using the 1109 | Router Discovery mechanism defined in RFC 1256 [13]. 1110 | 1111 | Object defined as: {"perform_router_discovery": True} 1112 | """ 1113 | 1114 | code = 31 1115 | key = "perform_router_discovery" 1116 | 1117 | 1118 | class RouterSolicitationAddress(IPOption): 1119 | """ 1120 | Option 32 1121 | 1122 | Specifies the address to which the client should transmit router 1123 | solicitation requests. 1124 | 1125 | Option value defined as: {"router_solicitation_address": "1.1.1.1"} 1126 | """ 1127 | 1128 | code = 32 1129 | key = "router_solicitation_address" 1130 | 1131 | 1132 | class StaticRoute(Option): 1133 | """ 1134 | Option 33 1135 | 1136 | Specifies a list of static routes that the client should install in its 1137 | routing cache. If multiple routes to the same destination are specified, 1138 | they are listed in descending order of priority. 1139 | """ 1140 | 1141 | code = 33 1142 | key = "static_routes" 1143 | 1144 | @property 1145 | def value(self) -> Dict[str, List[Dict[str, str]]]: 1146 | if self._value is None: 1147 | self._value = { 1148 | self.key: [ 1149 | {"destination": pair[0], "router": pair[1]} 1150 | for pair in self.data2IPpairs() 1151 | ] 1152 | } 1153 | return self._value 1154 | 1155 | @classmethod 1156 | def from_value(cls, value: Dict[str, List[Dict[str, str]]]): 1157 | ip_array: List[str] = [] 1158 | for pair in value[cls.key]: 1159 | ip_array.extend(pair.values()) 1160 | data = cls.IParray2data(ip_array) 1161 | return cls(cls.code, len(data), data) 1162 | 1163 | 1164 | class TrailerEncapsulation(BoolOption): 1165 | """ 1166 | Option 34 1167 | 1168 | Specifies whether or not the client should negotiate the use of trailers 1169 | (RFC 893 [14]) when using the ARP protocol. 1170 | 1171 | Option value defined as: {"trailer_encapsulation": True} 1172 | """ 1173 | 1174 | code = 34 1175 | key = "trailer_encapsulation" 1176 | 1177 | 1178 | class ARPCacheTimeout(uint32Option): 1179 | """ 1180 | Option 35 1181 | 1182 | Specifies the timeout in seconds for ARP cache entries. 1183 | 1184 | Option value defined as: {"arp_cache_timeout": 123} 1185 | """ 1186 | 1187 | code = 35 1188 | key = "arp_cache_timeout" 1189 | 1190 | 1191 | class EthernetEncapsulation(BoolOption): 1192 | """ 1193 | Option 36 1194 | 1195 | Specifies whether or not the client should use Ethernet Version 2 1196 | (RFC 894 [15]) or IEEE 802.3 (RFC 1042 [16]) encapsulation if the 1197 | interface is an Ethernet. 1198 | 1199 | Option value defined as: {"ethernet_encapsulation": True} 1200 | """ 1201 | 1202 | code = 36 1203 | key = "ethernet_encapsulation" 1204 | 1205 | 1206 | class TCPDefaultTTL(uint8Option): 1207 | """ 1208 | Option 37 1209 | 1210 | Specifies the default TTL that the client should use when sending TCP 1211 | segments. 1212 | 1213 | Option value defined as: {"tcp_default_ttl": 123} 1214 | """ 1215 | 1216 | code = 37 1217 | key = "tcp_default_ttl" 1218 | 1219 | 1220 | class TCPKeepaliveInterval(uint32Option): 1221 | """ 1222 | Option 38 1223 | 1224 | Specifies the interval (in seconds) that the client TCP should wait 1225 | before sending a keepalive message on a TCP connection. 1226 | 1227 | Option value defined as: {"tcp_keepalive_interval": 123} 1228 | """ 1229 | 1230 | code = 38 1231 | key = "tcp_keepalive_interval" 1232 | 1233 | 1234 | class TCPKeepaliveGarbage(BoolOption): 1235 | """ 1236 | Option 39 1237 | 1238 | Specifies the whether or not the client should send TCP keepalive 1239 | messages with a octet of garbage for compatibility with older 1240 | implementations. 1241 | 1242 | Option value defined as: {"tcp_keepalive_garbage": True} 1243 | """ 1244 | 1245 | code = 39 1246 | key = "tcp_keepalive_garbage" 1247 | 1248 | 1249 | class NISDomain(StrOption): 1250 | """ 1251 | Option 40 1252 | 1253 | Specifies the name of the client's NIS [17] domain. 1254 | 1255 | Option value defined as: {"network_information_service_domain": "google.com"} 1256 | """ 1257 | 1258 | code = 40 1259 | key = "network_information_service_domain" 1260 | 1261 | 1262 | class NISServer(IPArrayOption): 1263 | """ 1264 | Option 41 1265 | 1266 | Specifies a list of IP addresses indicating NIS servers available to 1267 | the client. 1268 | 1269 | Option value defined as: {"network_information_servers": ["1.1.1.1", "2.2.2.2"]} 1270 | """ 1271 | 1272 | code = 41 1273 | key = "network_information_servers" 1274 | 1275 | 1276 | class NTPServers(IPArrayOption): 1277 | """ 1278 | Option 42 1279 | 1280 | Specifies a list of IP addresses indicating NTP [18] servers available 1281 | to the client. 1282 | 1283 | Option value defined as: {"ntp_servers": ["1.1.1.1", "2.2.2.2"]} 1284 | """ 1285 | 1286 | code = 42 1287 | key = "ntp_servers" 1288 | 1289 | 1290 | class VendorSpecificInformation(BinOption): 1291 | """ 1292 | Option 43 1293 | 1294 | Super complicated, basically arbitrary data. This option can redefine 1295 | any option other than 0 and 255. 1296 | 1297 | Option value defined as: {"vender_specific_information": "0x0b 0x1c ..."} 1298 | """ 1299 | 1300 | code = 43 1301 | key = "vendor_specific_information" 1302 | 1303 | 1304 | class NetbiosNameServer(IPArrayOption): 1305 | """ 1306 | Option 44 1307 | 1308 | Specifies a list of RFC 1001/1002 [19] [20] NBNS name servers listed in 1309 | order of preference. 1310 | 1311 | Option value defined as: {"netbios_name_servers": ["1.1.1.1", "2.2.2.2"]} 1312 | """ 1313 | 1314 | code = 44 1315 | key = "netbios_name_servers" 1316 | 1317 | 1318 | class NetbiosDatagramDistributionServer(IPArrayOption): 1319 | """ 1320 | Option 45 1321 | 1322 | Specifies a list of RFC 1001/1002 NBDD servers listed in order of 1323 | preference. 1324 | 1325 | Option value defined as: {"netbios_datagram_distribution_server": ["1.1.1.1", "2.2.2.2"]} 1326 | """ 1327 | 1328 | code = 45 1329 | key = "netbios_datagram_distribution_server" 1330 | 1331 | 1332 | class NetbiosNodeType(Option): 1333 | """ 1334 | Option 46 1335 | 1336 | Node type option allows NetBIOS over TCP/IP clients which are 1337 | configurable to be configured as described in RFC 1001/1002. 1338 | 1339 | Option value defined as: {"netbios_node_type": "B-node"} 1340 | """ 1341 | 1342 | code = 46 1343 | key = "netbios_node_type" 1344 | 1345 | @property 1346 | def value(self) -> Dict[str, str]: 1347 | if self._value is None: 1348 | self._value = { 1349 | self.key: {0x1: "B-node", 0x2: "P-node", 0x4: "M-node", 0x8: "H-node"}[ 1350 | int.from_bytes(self.data, "big") 1351 | ] 1352 | } 1353 | return self._value 1354 | 1355 | @classmethod 1356 | def from_value(cls, value: Dict[str, str]): 1357 | data = { 1358 | "B-node": b"\x01", 1359 | "P-node": b"\x02", 1360 | "M-node": b"\x04", 1361 | "H-node": b"\x08", 1362 | }[value[cls.key]] 1363 | return cls(cls.code, len(data), data) 1364 | 1365 | 1366 | class NetbiosScope(StrOption): 1367 | """ 1368 | Option 47 1369 | 1370 | Specifies the NetBIOS over TCP/IP scope parameter for the client as 1371 | specified in RFC 1001/1002. 1372 | 1373 | Option value defined as: {"netbios_scope": "something"} 1374 | """ 1375 | 1376 | code = 47 1377 | key = "netbios_scope" 1378 | 1379 | 1380 | class NetbiosXWindowSystemFontServer(IPArrayOption): 1381 | """ 1382 | Option 48 1383 | 1384 | Specifies a list of X Window System [21] Font servers available to the 1385 | client. 1386 | 1387 | Option value defined as: {"netbios_x_window_system_font_servers": ["1.1.1.1", "2.2.2.2"]} 1388 | """ 1389 | 1390 | code = 48 1391 | key = "netbios_x_window_system_font_servers" 1392 | 1393 | 1394 | class XWindowSystemDisplayManager(IPArrayOption): 1395 | """ 1396 | Option 49 1397 | 1398 | Specifies a list of IP addresses of systems that are running the X 1399 | Window System Display Manager and are available to the client. 1400 | 1401 | Option value is defined as: {"x_window_system_display_manager": ["1.1.1.1", "2.2.2.2"]} 1402 | """ 1403 | 1404 | code = 49 1405 | key = "x_window_system_display_manager" 1406 | 1407 | 1408 | class RequestedIPAddress(IPOption): 1409 | """ 1410 | Option 50 1411 | 1412 | This option is used in a client request (DHCPDISCOVER) to allow the 1413 | client to request that a particular IP address be assigned. 1414 | 1415 | Option value is defined as: {"requested_ip_address": "1.1.1.1"} 1416 | """ 1417 | 1418 | code = 50 1419 | key = "requested_ip_address" 1420 | 1421 | 1422 | class IPAddressLeaseTime(uint32Option): 1423 | """ 1424 | Option 51 1425 | 1426 | This option is used in a client request (DHCPDISCOVER or DHCPREQUEST) 1427 | to allow the client to request a lease time for the IP address. In a 1428 | server reply (DHCPOFFER), a DHCP server uses this option to specify the 1429 | lease time it is willing to offer. 1430 | """ 1431 | 1432 | code = 51 1433 | key = "lease_time" 1434 | 1435 | 1436 | class Overload(Option): 1437 | """ 1438 | Option 52 1439 | 1440 | This option is used to indicate that the DHCP 'sname' or 'file' fields 1441 | are being overloaded by using them to carry DHCP options. A DHCP server 1442 | inserts this option if the returned parameters will exceed the usual 1443 | space allotted for options. 1444 | """ 1445 | 1446 | code = 52 1447 | key = "option_overload" 1448 | 1449 | @property 1450 | def value(self) -> Dict[str, str]: 1451 | if self._value is None: 1452 | self._value = { 1453 | self.key: { 1454 | 1: "'file' field is used to hold options", 1455 | 2: "'sname' field is used to hold options", 1456 | 3: "both fields are used to hold options", 1457 | }[int.from_bytes(self.data, "big")] 1458 | } 1459 | return self._value 1460 | 1461 | @classmethod 1462 | def from_value(cls, value: Dict[str, str]): 1463 | data = { 1464 | "'file' field is used to hold options": b"\x01", 1465 | "'sname' field is used to hold options": b"\x02", 1466 | "both fields are used to hold options": b"\x03", 1467 | }[value[cls.key]] 1468 | return cls(cls.code, len(data), data) 1469 | 1470 | 1471 | class MessageType(Option): 1472 | """ 1473 | Option 53 1474 | 1475 | This option is used to convey the type of the DHCP message. 1476 | """ 1477 | 1478 | code = 53 1479 | key = "dhcp_message_type" 1480 | 1481 | @property 1482 | def value(self) -> Dict[str, str]: 1483 | if self._value is None: 1484 | self._value = { 1485 | self.key: { 1486 | 1: "DHCPDISCOVER", 1487 | 2: "DHCPOFFER", 1488 | 3: "DHCPREQUEST", 1489 | 4: "DHCPDECLINE", 1490 | 5: "DHCPACK", 1491 | 6: "DHCPNAK", 1492 | 7: "DHCPRELEASE", 1493 | 8: "DHCPINFORM", 1494 | }[int.from_bytes(self.data, "big")] 1495 | } 1496 | return self._value 1497 | 1498 | @classmethod 1499 | def from_value(cls, value: Dict[str, str]): 1500 | data = { 1501 | "DHCPDISCOVER": b"\x01", 1502 | "DHCPOFFER": b"\x02", 1503 | "DHCPREQUEST": b"\x03", 1504 | "DHCPDECLINE": b"\x04", 1505 | "DHCPACK": b"\x05", 1506 | "DHCPNAK": b"\x06", 1507 | "DHCPRELEASE": b"\x07", 1508 | "DHCPINFORM": b"\x08", 1509 | }[value[cls.key]] 1510 | return cls(cls.code, len(data), data) 1511 | 1512 | 1513 | class ServerIdentifier(IPOption): 1514 | """ 1515 | Option 54 1516 | 1517 | This option is used in DHCPOFFER and DHCPREQUEST messages, and may 1518 | optionally be included in the DHCPACK and DHCPNAK messages. 1519 | """ 1520 | 1521 | code = 54 1522 | key = "dhcp_server" 1523 | 1524 | 1525 | class ParameterRequestList(uint8ArrayOption): 1526 | """ 1527 | Option 55 1528 | 1529 | This option is used by a DHCP client to request values for specified 1530 | configuration parameters. The list of requested parameters is 1531 | specified as n octets, where each octet is a valid DHCP option code 1532 | as defined in this document. 1533 | """ 1534 | 1535 | code = 55 1536 | key = "parameter_request_list" 1537 | 1538 | 1539 | class Message(StrOption): 1540 | """ 1541 | Option 56 1542 | 1543 | This option is used by a DHCP server to provide an error message to a 1544 | DHCP client in a DHCPNAK message in the event of a failure. A client 1545 | may use this option in a DHCPDECLINE message to indicate the why the 1546 | client declined the offered parameters. 1547 | """ 1548 | 1549 | code = 56 1550 | key = "message" 1551 | 1552 | 1553 | class MaxDHCPMessageSize(uint16Option): 1554 | """ 1555 | Option 57 1556 | 1557 | This option specifies the maximum length DHCP message that it is 1558 | willing to accept. 1559 | """ 1560 | 1561 | code = 57 1562 | key = "max_dhcp_message_size" 1563 | 1564 | 1565 | class RenewalTime(uint32Option): 1566 | """ 1567 | Option 58 1568 | 1569 | This option specifies the time interval from address assignment until 1570 | the client transitions to the RENEWING state. 1571 | """ 1572 | 1573 | code = 58 1574 | key = "renewal_time" 1575 | 1576 | 1577 | class RebindingTime(uint32Option): 1578 | """ 1579 | Option 59 1580 | 1581 | This option specifies the time interval from address assignment until 1582 | the client transitions to the REBINDING state. 1583 | """ 1584 | 1585 | code = 59 1586 | key = "rebinding_time" 1587 | 1588 | 1589 | class VendorClassIdentifier(StrOption): 1590 | """ 1591 | Option 60 1592 | 1593 | This option is used by DHCP clients to optionally identify the vendor 1594 | type and configuration of a DHCP client. 1595 | """ 1596 | 1597 | code = 60 1598 | key = "vendor_class_identifier" 1599 | 1600 | 1601 | class ClientIdentifier(Option): 1602 | """ 1603 | Option 61 1604 | 1605 | This option is used by DHCP clients to specify their unique 1606 | identifier. DHCP servers use this value to index their database of 1607 | address bindings. This value is expected to be unique for all 1608 | clients in an administrative domain. 1609 | """ 1610 | 1611 | code = 61 1612 | key = "client_identifier" 1613 | 1614 | @property 1615 | def value(self) -> Dict[str, Dict[str, str]]: 1616 | if self._value is None: 1617 | hwtype, hwaddr = struct.unpack(">B6s", self.data) 1618 | self._value = { 1619 | self.key: { 1620 | "hwtype": hwtype, 1621 | "hwaddr": ":".join([f"{b:02X}" for b in hwaddr]), 1622 | } 1623 | } 1624 | return self._value 1625 | 1626 | @classmethod 1627 | def from_value(cls, value): 1628 | hwtype = value[cls.key]["hwtype"] 1629 | hwaddr = value[cls.key]["hwaddr"] 1630 | data = struct.pack(">B", hwtype) + struct.pack( 1631 | ">" + "B" * len(hwaddr.split(":")), *[int(i, 16) for i in hwaddr.split(":")] 1632 | ) 1633 | return cls(cls.code, len(data), data) 1634 | 1635 | 1636 | class NISPlusDomain(StrOption): 1637 | """ 1638 | Option 64 1639 | 1640 | Specifies the name of the client's NIS+ [17] domain. 1641 | """ 1642 | 1643 | code = 64 1644 | key = "nis_plus_domain" 1645 | 1646 | 1647 | class NISPlusServers(IPArrayOption): 1648 | """ 1649 | Option 65 1650 | 1651 | Specifies a list of IP addresses indicating NIS+ servers available to 1652 | the client. 1653 | """ 1654 | 1655 | code = 65 1656 | key = "nis_plus_servers" 1657 | 1658 | 1659 | class TFTPServerName(StrOption): 1660 | """ 1661 | Option 66 1662 | 1663 | This option is used to identify a TFTP server when the 'sname' field in 1664 | the DHCP header has been used for DHCP options. 1665 | """ 1666 | 1667 | code = 66 1668 | key = "tftp_server_name" 1669 | 1670 | 1671 | class BootfileName(StrOption): 1672 | """ 1673 | Option 67 1674 | 1675 | This option is used to identify a bootfile when the 'file' field in the 1676 | DHCP header has been used for DHCP options. 1677 | """ 1678 | 1679 | code = 67 1680 | key = "bootfile_name" 1681 | 1682 | 1683 | class MobileIPHomeAgent(IPArrayOption): 1684 | """ 1685 | Option 68 1686 | 1687 | Specifies a list of IP addresses indicating mobile IP home agents 1688 | available to the client. 1689 | """ 1690 | 1691 | code = 68 1692 | key = "mobile_ip_home_agent" 1693 | 1694 | 1695 | class SMTPServer(IPArrayOption): 1696 | """ 1697 | Option 69 1698 | 1699 | Specifies a list of SMTP servers available to the client. 1700 | """ 1701 | 1702 | code = 69 1703 | key = "smtp_servers" 1704 | 1705 | 1706 | class POP3Server(IPArrayOption): 1707 | """ 1708 | Option 70 1709 | 1710 | Specifies a list of POP3 available to the client. 1711 | """ 1712 | 1713 | code = 70 1714 | key = "pop3_servers" 1715 | 1716 | 1717 | class NNTPServer(IPArrayOption): 1718 | """ 1719 | Option 71 1720 | 1721 | Specifies a list of NNTP available to the client. 1722 | """ 1723 | 1724 | code = 71 1725 | key = "nntp_servers" 1726 | 1727 | 1728 | class WWWServer(IPArrayOption): 1729 | """ 1730 | Option 72 1731 | 1732 | Specifies a list of WWW available to the client. 1733 | """ 1734 | 1735 | code = 72 1736 | key = "world_wide_web_servers" 1737 | 1738 | 1739 | class FingerServer(IPArrayOption): 1740 | """ 1741 | Option 73 1742 | 1743 | Specifies a list of Finger available to the client. 1744 | """ 1745 | 1746 | code = 73 1747 | key = "finger_servers" 1748 | 1749 | 1750 | class IRCServer(IPArrayOption): 1751 | """ 1752 | Option 74 1753 | 1754 | Specifies a list of IRC available to the client. 1755 | """ 1756 | 1757 | code = 74 1758 | key = "irc_servers" 1759 | 1760 | 1761 | class StreetTalkServer(IPArrayOption): 1762 | """ 1763 | Option 75 1764 | 1765 | Specifies a list of StreetTalk servers available to the client. 1766 | """ 1767 | 1768 | code = 75 1769 | key = "streettalk_servers" 1770 | 1771 | 1772 | class StreetTalkDirectoryAssistanceServer(IPArrayOption): 1773 | """ 1774 | Option 76 1775 | 1776 | Specifies a list of STDA servers available to the client. 1777 | """ 1778 | 1779 | code = 76 1780 | key = "stda_servers" 1781 | 1782 | 1783 | class RelayAgentInformation(StrOption): 1784 | """ 1785 | Option 82 1786 | 1787 | Relay Agent Information 1788 | """ 1789 | 1790 | code = 82 1791 | key = "relay_agent_info" 1792 | 1793 | 1794 | class UnknownOption(BinOption): 1795 | """ 1796 | Represents any options not defined here. 1797 | """ 1798 | 1799 | def __init__(self, code, length, data): 1800 | self.code = code 1801 | self.key = ( 1802 | "".join(OPTIONS.get(code, {}).get("name", "Unknown").split()) + f"_{code}" 1803 | ) 1804 | super().__init__(code, length, data) 1805 | 1806 | 1807 | # this should come after the last option is defined 1808 | options = OptionDirectory() 1809 | -------------------------------------------------------------------------------- /dhcppython/packet.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import ipaddress 3 | import random 4 | from dataclasses import dataclass 5 | from typing import ClassVar, List, Dict, Union, Optional 6 | from . import options, utils 7 | from .exceptions import MalformedPacketError, DHCPValueError 8 | 9 | 10 | OPTIONS_INTERFACE = options.options 11 | 12 | 13 | @dataclass 14 | class DHCPPacket(object): 15 | """ 16 | This class models a DHCP packet. From RFC 2131: 17 | 0 1 2 3 18 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 19 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 20 | | op (1) | htype (1) | hlen (1) | hops (1) | 21 | +---------------+---------------+---------------+---------------+ 22 | | xid (4) | 23 | +-------------------------------+-------------------------------+ 24 | | secs (2) | flags (2) | 25 | +-------------------------------+-------------------------------+ 26 | | ciaddr (4) | 27 | +---------------------------------------------------------------+ 28 | | yiaddr (4) | 29 | +---------------------------------------------------------------+ 30 | | siaddr (4) | 31 | +---------------------------------------------------------------+ 32 | | giaddr (4) | 33 | +---------------------------------------------------------------+ 34 | | | 35 | | chaddr (16) | 36 | | | 37 | | | 38 | +---------------------------------------------------------------+ 39 | | | 40 | | sname (64) | 41 | +---------------------------------------------------------------+ 42 | | | 43 | | file (128) | 44 | +---------------------------------------------------------------+ 45 | | | 46 | | options (variable) | 47 | +---------------------------------------------------------------+ 48 | """ 49 | 50 | op: str # 1 octet - Message Type: 1 is a BOOTREQUEST, 2 is a BOOTREPLY 51 | htype: str # 1 octet - Hardware Type: 1 for 10mb ethernet 52 | hlen: int # 1 octet - Hardware Address Length: 6 for 10mb ethernet 53 | hops: int # 1 octet - Hops: clients should set this to 0, may be used by relay 54 | xid: int # 4 octets - Transaction ID: random number, maintained for entire tx 55 | secs: int # 2 octets - Seconds: number of seconds since addr process began 56 | flags: int # 2 octets - Flags: bits 1-15 reserved, bit 0 indicates whether to use broadcast 57 | ciaddr: ipaddress.IPv4Address # 4 octets - Client Address: filled in if client can respond to ARP 58 | yiaddr: ipaddress.IPv4Address # 4 octets - 'your' (client) IP address 59 | siaddr: ipaddress.IPv4Address # 4 octets - Next Server: IP of next server to use for bootstrap (OFFER/ACK) 60 | giaddr: ipaddress.IPv4Address # 4 octets - Relay Agent: relay IP 61 | chaddr: str # 16 octets - Client Hardware Addr: MAC addr of client (usually len 6 + 10 padding) 62 | sname: bytes # 64 octets - Server Name: optional, host name, null terminated 63 | file: bytes # 128 octets - File Name: Null terminated str, boot file name 64 | options: options.OptionList # N octets - Options Field: variable length, options section started by the DHCP 65 | magic_cookie: ClassVar[bytes] = b"\x63\x82\x53\x63" 66 | cookie_offset_start: ClassVar[int] = 236 67 | cookie_offset_end: ClassVar[int] = 240 68 | packet_fmt: ClassVar[str] = "!BBBBLHHLLLL16s64s128s" 69 | op_map: ClassVar[Dict[int, str]] = {1: "BOOTREQUEST", 2: "BOOTREPLY"} 70 | inverse_op_map: ClassVar[Dict[str, int]] = {v: k for k, v in op_map.items()} 71 | htype_map: ClassVar[Dict[int, str]] = { 72 | 1: "ETHERNET", 73 | 2: "EXPERIMENTAL", 74 | 3: "AMATEUR", 75 | 4: "PROTEON", 76 | 5: "CHAOS", 77 | 6: "IEEE", 78 | 7: "ARCNET", 79 | 8: "HYPERCHANNEL", 80 | 9: "LANSTAR", 81 | } 82 | 83 | inverse_htype_map: ClassVar[Dict[str, int]] = {v: k for k, v in htype_map.items()} 84 | 85 | @property 86 | def asbytes(self): 87 | str2bin = lambda s: bytes([int(i, 16) for i in s.split(":")]) 88 | packet_head = [ 89 | self.inverse_op_map[self.op.upper()], 90 | self.inverse_htype_map[self.htype.upper()], 91 | self.hlen, 92 | self.hops, 93 | self.xid, 94 | self.secs, 95 | self.flags, 96 | int(self.ciaddr), 97 | int(self.yiaddr), 98 | int(self.siaddr), 99 | int(self.giaddr), 100 | str2bin(self.chaddr).ljust(16, b"\x00"), 101 | self.sname.ljust(64, b"\x00"), 102 | self.file.ljust(128, b"\x00"), 103 | ] 104 | encoded_packet = struct.pack(self.packet_fmt, *packet_head) 105 | encoded_packet += self.magic_cookie 106 | for option in self.options: 107 | encoded_packet += option.asbytes 108 | if encoded_packet[-1] != 255: 109 | encoded_packet += b"\xff" 110 | return encoded_packet 111 | 112 | @property 113 | def msg_type(self) -> Optional[str]: 114 | if msg_type_option := self.options.by_code(53): 115 | return list(msg_type_option.value.values())[0] 116 | else: 117 | return None 118 | 119 | @classmethod 120 | def from_bytes(cls, packet: bytes): 121 | """ 122 | Given a DHCP packet in bytes / wire format return a DHCPPacket object. 123 | """ 124 | if packet[cls.cookie_offset_start : cls.cookie_offset_end] != cls.magic_cookie: 125 | raise MalformedPacketError("Magic cookie missing") 126 | try: 127 | decoded_packet = [ 128 | field.rstrip(b"\x00") if isinstance(field, bytes) else field 129 | for field in struct.unpack( 130 | cls.packet_fmt, packet[: cls.cookie_offset_start] 131 | ) 132 | ] 133 | except: 134 | raise MalformedPacketError("Unable to parse DHCP packet") 135 | 136 | options_list = options.OptionList() 137 | read_pos = cls.cookie_offset_end 138 | code = 0 139 | while read_pos < len(packet) and code != 255: 140 | code = packet[read_pos] 141 | if code in [0, 255]: 142 | data_read_size = 1 143 | else: 144 | length = packet[read_pos + 1] 145 | data_read_size = 1 + 1 + length 146 | 147 | option_bytes = packet[read_pos : read_pos + data_read_size] 148 | options_object = OPTIONS_INTERFACE.bytes_to_object(option_bytes) 149 | options_list.append(options_object) 150 | read_pos += data_read_size 151 | 152 | decoded_packet.append(options_list) 153 | # Decode the op code 154 | decoded_packet[0] = cls.op_map[decoded_packet[0]] 155 | # Decode hardware type 156 | decoded_packet[1] = cls.htype_map[decoded_packet[1]] 157 | # Convert the ciaddr, yiaddr, siaddr, and giaddr into python IP objects 158 | decoded_packet[7:11] = [ 159 | ipaddress.IPv4Address(field) for field in decoded_packet[7:11] 160 | ] 161 | # Convert MAC addr into bin string 162 | decoded_packet[11] = decoded_packet[11].ljust(6, b"\x00") 163 | bin2str = lambda b: ":".join([f"{i:02X}" for i in b]) 164 | decoded_packet[11] = bin2str(decoded_packet[11]) 165 | return cls(*decoded_packet) 166 | 167 | def format_options(self, opt_str, line_divider, line_len): 168 | """ 169 | Given a string with all the options in a packet this will format 170 | the string into an ASCII table format. 171 | """ 172 | line_pos = 0 173 | output = "" 174 | new_line = "|\n" + line_divider + "|" 175 | skip_next_space = False # Need this for alignment 176 | last_char = "" 177 | for char in opt_str: 178 | if char == " " and skip_next_space: 179 | skip_next_space = False 180 | continue 181 | char = " " if last_char == "|" and char == "|" else char 182 | output += char 183 | line_pos = (line_pos + 1) % line_len 184 | if line_pos == 0: 185 | output += new_line 186 | skip_next_space = True 187 | last_char = output[-1] 188 | return output 189 | 190 | def view_packet(self): 191 | """ 192 | A fun way of visualising the DHCP packet in ASCII table format. 193 | """ 194 | bytes_per_line = 4 195 | byte_len = 15 196 | spacing = lambda num_bytes: (num_bytes * byte_len) + num_bytes - 1 197 | column = ( 198 | lambda str_to_space, num_bytes: f"{str_to_space[:spacing(num_bytes)].center(spacing(num_bytes))}|" 199 | ) 200 | line = "+" + ("-" * (byte_len * bytes_per_line + bytes_per_line))[:-1] + "+\n" 201 | base_packet = ( 202 | "0 1 2 3 \n" 203 | "0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 \n" 204 | "+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n" 205 | "|" 206 | + column(f"{self.op} (1)", 1) 207 | + column(f"{self.htype} (1)", 1) 208 | + column(f"len {self.hlen} (1)", 1) 209 | + column(f"{self.hops} hops (1)", 1) 210 | + "\n" 211 | "+---------------+---------------+---------------+---------------+\n" 212 | "|" + column(f"xid=0x{self.xid:08X} (4)", 4) + "\n" 213 | "+-------------------------------+-------------------------------+\n" 214 | "|" 215 | + f"{self.secs} secs (2)".center(spacing(2)) 216 | + "|" 217 | + f"{'BROADCAST' if self.flags else 'UNICAST'} (2)".center(spacing(2)) 218 | + "|\n" 219 | "+-------------------------------+-------------------------------+\n" 220 | "|" + column(f"client addr: {self.ciaddr!s} (4)", 4) + "\n" 221 | "+---------------------------------------------------------------+\n" 222 | "|" + column(f"your addr: {self.yiaddr!s} (4)", 4) + "\n" 223 | "+---------------------------------------------------------------+\n" 224 | "|" + column(f"next server: {self.siaddr!s} (4)", 4) + "\n" 225 | "+---------------------------------------------------------------+\n" 226 | "|" + column(f"relay: {self.giaddr!s} (4)", 4) + "\n" 227 | "+---------------------------------------------------------------+\n" 228 | "| |\n" 229 | "|" + column(f"client mac: {self.chaddr} (16)", 4) + "\n" 230 | "| |\n" 231 | "| |\n" 232 | "+---------------------------------------------------------------+\n" 233 | "| |\n" 234 | "|" + column(f"server name: {self.sname} (64)", 4) + "\n" 235 | "+---------------------------------------------------------------+\n" 236 | "| |\n" 237 | "|" + column(f"boot file: {self.file} (128)", 4) + "\n" 238 | "+---------------------------------------------------------------+\n" 239 | "|" 240 | + column( 241 | f"magic cookie: {hex(int.from_bytes(self.magic_cookie, 'big'))}", 242 | len(self.magic_cookie), 243 | ) 244 | + "\n" 245 | "+---------------------------------------------------------------+\n" 246 | ) 247 | 248 | base_packet += "|" 249 | opt_str = "" 250 | for opt in self.options: 251 | opt_str += column(f"code={opt.code} (1)", 1) 252 | if opt.code not in [0, 255]: 253 | opt_str += column(f"len={opt.length} (1)", 1) 254 | if opt.code == 53: 255 | # Shortening DHCP msg type for display -- special case 256 | opt_str += column( 257 | f"{opt.value[opt.key]} ({opt.length})", opt.length 258 | ) 259 | else: 260 | opt_str += column( 261 | f"{opt.key} {opt.value[opt.key]} ({opt.length})", opt.length 262 | ) 263 | 264 | base_packet += self.format_options(opt_str, line, spacing(bytes_per_line)) 265 | 266 | base_packet += "\n" + line[: len(base_packet.split("\n")[-1])] 267 | base_packet = base_packet[:-1] + "+" 268 | return base_packet 269 | 270 | @classmethod 271 | def Discover( 272 | cls, 273 | mac_addr: str, 274 | seconds: int = 0, 275 | tx_id: Optional[int] = None, 276 | use_broadcast: bool = True, 277 | relay: Optional[str] = None, 278 | option_list: Optional[options.OptionList] = None, 279 | ): 280 | """ 281 | Convenient constructor for a DHCP discover packet. 282 | """ 283 | if not utils.is_mac_addr(mac_addr): 284 | raise DHCPValueError( 285 | "MAC address must consist of 6 octets delimited by ':'" 286 | ) 287 | option_list = option_list if option_list else options.OptionList() 288 | option_list.insert(0, options.options.short_value_to_object(53, "DHCPDISCOVER")) 289 | relay_ip = ipaddress.IPv4Address(relay or 0) 290 | return cls( 291 | "BOOTREQUEST", 292 | cls.htype_map[1], # 10 mb ethernet 293 | 6, # 6 byte hardware addr 294 | 0, # clients should set this to 0 295 | tx_id or random.getrandbits(32), 296 | seconds, 297 | 0b1000_0000_0000_0000 if use_broadcast else 0, 298 | ipaddress.IPv4Address(0), # Must be 0 299 | ipaddress.IPv4Address(0), 300 | ipaddress.IPv4Address(0), 301 | relay_ip, 302 | mac_addr, 303 | b"", 304 | b"", 305 | option_list, 306 | ) 307 | 308 | @classmethod 309 | def Offer( 310 | cls, 311 | mac_addr: str, 312 | seconds: int, 313 | tx_id: int, 314 | yiaddr: Union[int, str], 315 | use_broadcast: bool = True, 316 | relay: Optional[str] = None, 317 | sname: bytes = b"", 318 | fname: bytes = b"", 319 | option_list: Optional[options.OptionList] = None, 320 | ): 321 | """ 322 | Convenient constructor for a DHCP offer packet. 323 | """ 324 | if len(mac_addr.split(":")) != 6 or len(mac_addr) != 17: 325 | raise DHCPValueError( 326 | "MAC address must consist of 6 octets delimited by ':'" 327 | ) 328 | option_list = option_list if option_list else options.OptionList() 329 | option_list.insert(0, options.options.short_value_to_object(53, "DHCPOFFER")) 330 | relay_ip = ipaddress.IPv4Address(relay or 0) 331 | return cls( 332 | "BOOTREPLY", 333 | cls.htype_map[1], # 10 mb ethernet 334 | 6, # 6 byte hardware addr 335 | 0, # clients should set this to 0 336 | tx_id, 337 | seconds, 338 | 0b1000_0000_0000_0000 if use_broadcast else 0, 339 | ipaddress.IPv4Address(0), 340 | # yiaddr - "your address", address being proposed by server 341 | ipaddress.IPv4Address(yiaddr), 342 | ipaddress.IPv4Address(0), 343 | relay_ip, 344 | mac_addr, 345 | sname, 346 | fname, 347 | option_list, 348 | ) 349 | 350 | @classmethod 351 | def Request( 352 | cls, 353 | mac_addr: str, 354 | seconds: int, 355 | tx_id: int, 356 | use_broadcast: bool = True, 357 | relay: Optional[str] = None, 358 | sname: bytes = b"", 359 | fname: bytes = b"", 360 | client_ip=ipaddress.IPv4Address(0), 361 | option_list: Optional[options.OptionList] = None, 362 | ): 363 | """ 364 | Convenient constructor for a DHCP request packet. 365 | """ 366 | if len(mac_addr.split(":")) != 6 or len(mac_addr) != 17: 367 | raise DHCPValueError( 368 | "MAC address must consist of 6 octets delimited by ':'" 369 | ) 370 | option_list = option_list if option_list else options.OptionList() 371 | option_list.insert(0, options.options.short_value_to_object(53, "DHCPREQUEST")) 372 | relay_ip = ipaddress.IPv4Address(relay or 0) 373 | return cls( 374 | "BOOTREQUEST", 375 | cls.htype_map[1], # 10 mb ethernet 376 | 6, # 6 byte hardware addr 377 | 0, # clients should set this to 0 378 | tx_id, 379 | seconds, 380 | 0b1000_0000_0000_0000 if use_broadcast else 0, 381 | client_ip, 382 | ipaddress.IPv4Address(0), 383 | ipaddress.IPv4Address(0), 384 | relay_ip, 385 | mac_addr, 386 | sname, 387 | fname, 388 | option_list, 389 | ) 390 | 391 | @classmethod 392 | def Ack( 393 | cls, 394 | mac_addr: str, 395 | seconds: int, 396 | tx_id: int, 397 | yiaddr: Union[int, str], 398 | use_broadcast: bool = True, 399 | relay: Optional[str] = None, 400 | sname: bytes = b"", 401 | fname: bytes = b"", 402 | option_list: Optional[options.OptionList] = None, 403 | ): 404 | """ 405 | Convenient constructor for a DHCP ack packet. 406 | """ 407 | # Can be refactored to just use the Request constructor if it turns out that Ack has no special needs. 408 | if len(mac_addr.split(":")) != 6 or len(mac_addr) != 17: 409 | raise DHCPValueError( 410 | "MAC address must consist of 6 octets delimited by ':'" 411 | ) 412 | option_list = option_list if option_list else options.OptionList() 413 | option_list.insert(0, options.options.short_value_to_object(53, "DHCPACK")) 414 | relay_ip = ipaddress.IPv4Address(relay or 0) 415 | return cls( 416 | "BOOTREPLY", 417 | cls.htype_map[1], # 10 mb ethernet 418 | 6, # 6 byte hardware addr 419 | 0, # clients should set this to 0 420 | tx_id, 421 | seconds, 422 | 0b1000_0000_0000_0000 if use_broadcast else 0, 423 | ipaddress.IPv4Address(0), 424 | # yiaddr - "your address", address being proposed by server 425 | ipaddress.IPv4Address(yiaddr), 426 | ipaddress.IPv4Address(0), 427 | relay_ip, 428 | mac_addr, 429 | sname, 430 | fname, 431 | option_list, 432 | ) 433 | -------------------------------------------------------------------------------- /dhcppython/runtime_assets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvfrazao/dhcppython/c442c3f6eca8244667df8a19d370f7569d81f08f/dhcppython/runtime_assets/__init__.py -------------------------------------------------------------------------------- /dhcppython/runtime_assets/options.csv: -------------------------------------------------------------------------------- 1 | Tag,Name,"Data 2 | Length",Meaning,Reference 3 | 0,Pad,0,None,[RFC2132] 4 | 1,Subnet Mask,4,Subnet Mask Value,[RFC2132] 5 | 2,Time Offset,4,"Time Offset in Seconds from UTC 6 | (note: deprecated by 100 and 101)",[RFC2132] 7 | 3,Router,N,N/4 Router addresses,[RFC2132] 8 | 4,Time Server,N,N/4 Timeserver addresses,[RFC2132] 9 | 5,Name Server,N,N/4 IEN-116 Server addresses,[RFC2132] 10 | 6,Domain Server,N,N/4 DNS Server addresses,[RFC2132] 11 | 7,Log Server,N,N/4 Logging Server addresses,[RFC2132] 12 | 8,Quotes Server,N,N/4 Quotes Server addresses,[RFC2132] 13 | 9,LPR Server,N,N/4 Printer Server addresses,[RFC2132] 14 | 10,Impress Server,N,N/4 Impress Server addresses,[RFC2132] 15 | 11,RLP Server,N,N/4 RLP Server addresses,[RFC2132] 16 | 12,Hostname,N,Hostname string,[RFC2132] 17 | 13,Boot File Size,2,Size of boot file in 512 byte chunks,[RFC2132] 18 | 14,Merit Dump File,N,Client to dump and name the file to dump it to,[RFC2132] 19 | 15,Domain Name,N,The DNS domain name of the client,[RFC2132] 20 | 16,Swap Server,N,Swap Server address,[RFC2132] 21 | 17,Root Path,N,Path name for root disk,[RFC2132] 22 | 18,Extension File,N,Path name for more BOOTP info,[RFC2132] 23 | 19,Forward On/Off,1,Enable/Disable IP Forwarding,[RFC2132] 24 | 20,SrcRte On/Off,1,Enable/Disable Source Routing,[RFC2132] 25 | 21,Policy Filter,N,Routing Policy Filters,[RFC2132] 26 | 22,Max DG Assembly,2,Max Datagram Reassembly Size,[RFC2132] 27 | 23,Default IP TTL,1,Default IP Time to Live,[RFC2132] 28 | 24,MTU Timeout,4,Path MTU Aging Timeout,[RFC2132] 29 | 25,MTU Plateau,N,Path MTU Plateau Table,[RFC2132] 30 | 26,MTU Interface,2,Interface MTU Size,[RFC2132] 31 | 27,MTU Subnet,1,All Subnets are Local,[RFC2132] 32 | 28,Broadcast Address,4,Broadcast Address,[RFC2132] 33 | 29,Mask Discovery,1,Perform Mask Discovery,[RFC2132] 34 | 30,Mask Supplier,1,Provide Mask to Others,[RFC2132] 35 | 31,Router Discovery,1,Perform Router Discovery,[RFC2132] 36 | 32,Router Request,4,Router Solicitation Address,[RFC2132] 37 | 33,Static Route,N,Static Routing Table,[RFC2132] 38 | 34,Trailers,1,Trailer Encapsulation,[RFC2132] 39 | 35,ARP Timeout,4,ARP Cache Timeout,[RFC2132] 40 | 36,Ethernet,1,Ethernet Encapsulation,[RFC2132] 41 | 37,Default TCP TTL,1,Default TCP Time to Live,[RFC2132] 42 | 38,Keepalive Time,4,TCP Keepalive Interval,[RFC2132] 43 | 39,Keepalive Data,1,TCP Keepalive Garbage,[RFC2132] 44 | 40,NIS Domain,N,NIS Domain Name,[RFC2132] 45 | 41,NIS Servers,N,NIS Server Addresses,[RFC2132] 46 | 42,NTP Servers,N,NTP Server Addresses,[RFC2132] 47 | 43,Vendor Specific,N,Vendor Specific Information,[RFC2132] 48 | 44,NETBIOS Name Srv,N,NETBIOS Name Servers,[RFC2132] 49 | 45,NETBIOS Dist Srv,N,NETBIOS Datagram Distribution,[RFC2132] 50 | 46,NETBIOS Node Type,1,NETBIOS Node Type,[RFC2132] 51 | 47,NETBIOS Scope,N,NETBIOS Scope,[RFC2132] 52 | 48,X Window Font,N,X Window Font Server,[RFC2132] 53 | 49,X Window Manager,N,X Window Display Manager,[RFC2132] 54 | 50,Address Request,4,Requested IP Address,[RFC2132] 55 | 51,Address Time,4,IP Address Lease Time,[RFC2132] 56 | 52,Overload,1,"Overload ""sname"" or ""file""",[RFC2132] 57 | 53,DHCP Msg Type,1,DHCP Message Type,[RFC2132] 58 | 54,DHCP Server Id,4,DHCP Server Identification,[RFC2132] 59 | 55,Parameter List,N,Parameter Request List,[RFC2132] 60 | 56,DHCP Message,N,DHCP Error Message,[RFC2132] 61 | 57,DHCP Max Msg Size,2,DHCP Maximum Message Size,[RFC2132] 62 | 58,Renewal Time,4,DHCP Renewal (T1) Time,[RFC2132] 63 | 59,Rebinding Time,4,DHCP Rebinding (T2) Time,[RFC2132] 64 | 60,Class Id,N,Class Identifier,[RFC2132] 65 | 61,Client Id,N,Client Identifier,[RFC2132] 66 | 62,NetWare/IP Domain,N,NetWare/IP Domain Name,[RFC2242] 67 | 63,NetWare/IP Option,N,NetWare/IP sub Options,[RFC2242] 68 | 64,NIS-Domain-Name,N,NIS+ v3 Client Domain Name,[RFC2132] 69 | 65,NIS-Server-Addr,N,NIS+ v3 Server Addresses,[RFC2132] 70 | 66,Server-Name,N,TFTP Server Name,[RFC2132] 71 | 67,Bootfile-Name,N,Boot File Name,[RFC2132] 72 | 68,Home-Agent-Addrs,N,Home Agent Addresses,[RFC2132] 73 | 69,SMTP-Server,N,Simple Mail Server Addresses,[RFC2132] 74 | 70,POP3-Server,N,Post Office Server Addresses,[RFC2132] 75 | 71,NNTP-Server,N,Network News Server Addresses,[RFC2132] 76 | 72,WWW-Server,N,WWW Server Addresses,[RFC2132] 77 | 73,Finger-Server,N,Finger Server Addresses,[RFC2132] 78 | 74,IRC-Server,N,Chat Server Addresses,[RFC2132] 79 | 75,StreetTalk-Server,N,StreetTalk Server Addresses,[RFC2132] 80 | 76,STDA-Server,N,ST Directory Assist. Addresses,[RFC2132] 81 | 77,User-Class,N,User Class Information,[RFC3004] 82 | 78,Directory Agent,N,directory agent information,[RFC2610] 83 | 79,Service Scope,N,service location agent scope,[RFC2610] 84 | 80,Rapid Commit,0,Rapid Commit,[RFC4039] 85 | 81,Client FQDN,N,Fully Qualified Domain Name,[RFC4702] 86 | 82,Relay Agent Information,N,Relay Agent Information,[RFC3046] 87 | 83,iSNS,N,Internet Storage Name Service,[RFC4174] 88 | 84,REMOVED/Unassigned,,,[RFC3679] 89 | 85,NDS Servers,N,Novell Directory Services,[RFC2241] 90 | 86,NDS Tree Name,N,Novell Directory Services,[RFC2241] 91 | 87,NDS Context,N,Novell Directory Services,[RFC2241] 92 | 88,BCMCS Controller Domain Name list,,,[RFC4280] 93 | 89,BCMCS Controller IPv4 address option,,,[RFC4280] 94 | 90,Authentication,N,Authentication,[RFC3118] 95 | 91,client-last-transaction-time option,,,[RFC4388] 96 | 92,associated-ip option,,,[RFC4388] 97 | 93,Client System,N,Client System Architecture,[RFC4578] 98 | 94,Client NDI,N,Client Network Device Interface,[RFC4578] 99 | 95,LDAP,N,Lightweight Directory Access Protocol,[RFC3679] 100 | 96,REMOVED/Unassigned,,,[RFC3679] 101 | 97,UUID/GUID,N,UUID/GUID-based Client Identifier,[RFC4578] 102 | 98,User-Auth,N,Open Group's User Authentication,[RFC2485] 103 | 99,GEOCONF_CIVIC,,,[RFC4776] 104 | 100,PCode,N,IEEE 1003.1 TZ String,[RFC4833] 105 | 101,TCode,N,Reference to the TZ Database,[RFC4833] 106 | 102-107,REMOVED/Unassigned,,,[RFC3679] 107 | 108,REMOVED/Unassigned,,,[RFC3679] 108 | 109,OPTION_DHCP4O6_S46_SADDR,16,DHCPv4 over DHCPv6 Softwire Source Address Option,[RFC8539] 109 | 110,REMOVED/Unassigned,,,[RFC3679] 110 | 111,Unassigned,,,[RFC3679] 111 | 112,Netinfo Address,N,NetInfo Parent Server Address,[RFC3679] 112 | 113,Netinfo Tag,N,NetInfo Parent Server Tag,[RFC3679] 113 | 114,URL,N,URL,[RFC3679] 114 | 115,REMOVED/Unassigned,,,[RFC3679] 115 | 116,Auto-Config,N,DHCP Auto-Configuration,[RFC2563] 116 | 117,Name Service Search,N,Name Service Search,[RFC2937] 117 | 118,Subnet Selection Option,4,Subnet Selection Option,[RFC3011] 118 | 119,Domain Search,N,DNS domain search list,[RFC3397] 119 | 120,SIP Servers DHCP Option,N,SIP Servers DHCP Option,[RFC3361] 120 | 121,Classless Static Route Option,N,Classless Static Route Option,[RFC3442] 121 | 122,CCC,N,CableLabs Client Configuration,[RFC3495] 122 | 123,GeoConf Option,16,GeoConf Option,[RFC6225] 123 | 124,V-I Vendor Class,,Vendor-Identifying Vendor Class,[RFC3925] 124 | 125,V-I Vendor-Specific Information,,Vendor-Identifying Vendor-Specific Information,[RFC3925] 125 | 126,Removed/Unassigned,,,[RFC3679] 126 | 127,Removed/Unassigned,,,[RFC3679] 127 | 128,PXE - undefined (vendor specific),,,[RFC4578] 128 | 128,"Etherboot signature. 6 bytes: 129 | E4:45:74:68:00:00",,, 130 | 128,"DOCSIS ""full security"" server IP 131 | address",,, 132 | 128,"TFTP Server IP address (for IP 133 | Phone software load)",,, 134 | 129,PXE - undefined (vendor specific),,,[RFC4578] 135 | 129,"Kernel options. Variable length 136 | string",,, 137 | 129,Call Server IP address,,, 138 | 130,PXE - undefined (vendor specific),,,[RFC4578] 139 | 130,"Ethernet interface. Variable 140 | length string.",,, 141 | 130,"Discrimination string (to 142 | identify vendor)",,, 143 | 131,PXE - undefined (vendor specific),,,[RFC4578] 144 | 131,Remote statistics server IP address,,, 145 | 132,PXE - undefined (vendor specific),,,[RFC4578] 146 | 132,IEEE 802.1Q VLAN ID,,, 147 | 133,PXE - undefined (vendor specific),,,[RFC4578] 148 | 133,IEEE 802.1D/p Layer 2 Priority,,, 149 | 134,PXE - undefined (vendor specific),,,[RFC4578] 150 | 134,"Diffserv Code Point (DSCP) for 151 | VoIP signalling and media streams",,, 152 | 135,PXE - undefined (vendor specific),,,[RFC4578] 153 | 135,"HTTP Proxy for phone-specific 154 | applications",,, 155 | 136,OPTION_PANA_AGENT,,,[RFC5192] 156 | 137,OPTION_V4_LOST,,,[RFC5223] 157 | 138,OPTION_CAPWAP_AC_V4,N,CAPWAP Access Controller addresses,[RFC5417] 158 | 139,OPTION-IPv4_Address-MoS,N,a series of suboptions,[RFC5678] 159 | 140,OPTION-IPv4_FQDN-MoS,N,a series of suboptions,[RFC5678] 160 | 141,SIP UA Configuration Service Domains,N,List of domain names to search for SIP User Agent Configuration,[RFC6011] 161 | 142,OPTION-IPv4_Address-ANDSF,N,ANDSF IPv4 Address Option for DHCPv4,[RFC6153] 162 | 143,OPTION_V4_SZTP_REDIRECT,N,This option provides a list of URIs for SZTP bootstrap servers,[RFC8572] 163 | 144,GeoLoc,16,Geospatial Location with Uncertainty,[RFC6225] 164 | 145,FORCERENEW_NONCE_CAPABLE,1,Forcerenew Nonce Capable,[RFC6704] 165 | 146,RDNSS Selection,N,Information for selecting RDNSS,[RFC6731] 166 | 147-149,Unassigned,,,[RFC3942] 167 | 150,TFTP server address,,,[RFC5859] 168 | 150,Etherboot,,, 169 | 150,GRUB configuration path name,,, 170 | 151,status-code,N+1,Status code and optional N byte text message describing status.,[RFC6926] 171 | 152,base-time,4,"Absolute time (seconds since Jan 1, 1970) message was sent.",[RFC6926] 172 | 153,start-time-of-state,4,Number of seconds in the past when client entered current state.,[RFC6926] 173 | 154,query-start-time,4,"Absolute time (seconds since Jan 1, 1970) for beginning of query.",[RFC6926] 174 | 155,query-end-time,4,"Absolute time (seconds since Jan 1, 1970) for end of query.",[RFC6926] 175 | 156,dhcp-state,1,State of IP address.,[RFC6926] 176 | 157,data-source,1,Indicates information came from local or remote server.,[RFC6926] 177 | 158,OPTION_V4_PCP_SERVER,Variable; the minimum length is 5.,"Includes one or multiple lists of PCP server IP addresses; 178 | each list is treated as a separate PCP server.",[RFC7291] 179 | 159,OPTION_V4_PORTPARAMS,4,"This option is used to configure a set of ports bound to a 180 | shared IPv4 address.",[RFC7618] 181 | 160,DHCP Captive-Portal,N,DHCP Captive-Portal,[RFC7710] 182 | 161,OPTION_MUD_URL_V4,N (variable),Manufacturer Usage Descriptions,[RFC8520] 183 | 162-174,Unassigned,,,[RFC3942] 184 | 175,"Etherboot (Tentatively Assigned - 185 | 2005-06-23)",,, 186 | 176,"IP Telephone (Tentatively Assigned - 187 | 2005-06-23)",,, 188 | 177,"Etherboot (Tentatively Assigned - 189 | 2005-06-23)",,, 190 | 177,"PacketCable and CableHome (replaced by 191 | 122)",,, 192 | 178-207,Unassigned,,,[RFC3942] 193 | 208,PXELINUX Magic,4,magic string = F1:00:74:7E,[RFC5071][Deprecated] 194 | 209,Configuration File,N,Configuration file,[RFC5071] 195 | 210,Path Prefix,N,Path Prefix Option,[RFC5071] 196 | 211,Reboot Time,4,Reboot Time,[RFC5071] 197 | 212,OPTION_6RD,18 + N,OPTION_6RD with N/4 6rd BR addresses,[RFC5969] 198 | 213,OPTION_V4_ACCESS_DOMAIN,N,Access Network Domain Name,[RFC5986] 199 | 214-219,Unassigned,,, 200 | 220,Subnet Allocation Option,N,Subnet Allocation Option,[RFC6656] 201 | 221,Virtual Subnet Selection (VSS) Option,,,[RFC6607] 202 | 222-223,Unassigned,,,[RFC3942] 203 | 224-254,Reserved (Private Use),,, 204 | 255,End,0,None,[RFC2132] 205 | -------------------------------------------------------------------------------- /dhcppython/utils.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | import datetime 4 | import socket 5 | import unicodedata 6 | from typing import Dict 7 | import importlib.resources 8 | from . import runtime_assets 9 | 10 | 11 | VALID_HEX = list(set(string.hexdigits.upper())) 12 | 13 | 14 | def cur_datetime(us_precision: bool = False) -> str: 15 | fmt = "%Y-%m-%dT%H:%M:%S" + (".%f" if us_precision else "") + "Z" 16 | return datetime.datetime.now(datetime.timezone.utc).strftime(fmt) 17 | 18 | 19 | def cur_timestamp() -> int: 20 | return int(datetime.datetime.utcnow().timestamp() * 10 ** 9) 21 | 22 | 23 | def visual_length(text: str) -> int: 24 | """ 25 | Given a string it returns the visual length of the string as opposed to the 26 | len function which returns the number of printable characters. 27 | """ 28 | # See https://www.unicode.org/reports/tr11/ for how this dict in constructed 29 | visual_len = { 30 | "F": 1, 31 | "H": 1, 32 | "Na": 1, 33 | "N": 1, 34 | "W": 2, 35 | "A": 2, 36 | } 37 | return sum([visual_len[unicodedata.east_asian_width(char)] for char in text]) + 1 38 | 39 | 40 | def random_mac(num_bytes: int = 6, delimiter: str = ":") -> str: 41 | """ 42 | Generates an 6 byte long MAC address. 43 | 44 | >>> random_mac() 45 | 'CC:AC:3C:85:A4:EF' 46 | """ 47 | return delimiter.join( 48 | ["".join(random.choices(VALID_HEX, k=2)) for i in range(num_bytes)] 49 | ) 50 | 51 | 52 | def is_mac_addr(mac_addr: str) -> bool: 53 | """ 54 | Returns True if the string is a valid MAC address. 55 | 56 | Accepts ":" or "-" as valid MAC address delimiters. 57 | """ 58 | mac_addr = mac_addr.upper() 59 | delimiter = ":" if ":" in mac_addr else "-" 60 | if len(mac_addr.split(delimiter)) != 6 or len(mac_addr) != 17: 61 | return False 62 | if any([b not in VALID_HEX for b in "".join(mac_addr.split(delimiter))]): 63 | return False 64 | return True 65 | 66 | 67 | mac_vendor_map: Dict[str, str] = { 68 | line.split("\t\t")[0].split(" ")[0]: line.split("\t\t")[1] 69 | for line in [ 70 | line.strip() 71 | for line in importlib.resources.read_text(runtime_assets, "oui.txt").split("\n") 72 | if "(base 16)" in line 73 | ] 74 | } 75 | 76 | 77 | def mac2vendor(mac_addr: str) -> str: 78 | if is_mac_addr(mac_addr): 79 | return mac_vendor_map.get( 80 | mac_addr.replace(":", "").replace("-", "")[:6].upper(), 81 | "Unknown Manufacturer", 82 | ) 83 | else: 84 | raise ValueError(f"{mac_addr} is not a valid MAC address") 85 | 86 | 87 | def get_ip_by_iface(iface: str) -> str: 88 | rand_port = 61224 89 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 90 | s.setsockopt(socket.SOL_SOCKET, 25, iface.encode()) 91 | s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 92 | s.connect(("255.255.255.255", rand_port)) 93 | return s.getsockname()[0] 94 | 95 | 96 | def get_ip_by_server(server: str) -> str: 97 | rand_port = 61222 98 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 99 | s.connect((server, rand_port)) 100 | return s.getsockname()[0] 101 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvfrazao/dhcppython/c442c3f6eca8244667df8a19d370f7569d81f08f/requirements.txt -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import sys 4 | from shutil import rmtree 5 | 6 | from setuptools import find_packages, setup, Command 7 | 8 | # From https://github.com/kennethreitz/setup.py/blob/master/setup.py 9 | # Kenneth Reitz 10 | 11 | # Package meta-data. 12 | NAME = 'dhcppython' 13 | DESCRIPTION = 'Package for working with DHCP packets - including a DHCP client', 14 | URL = 'https://github.com/vfrazao-ns1/dhcppython' 15 | EMAIL = '' 16 | AUTHOR = 'Victor Frazao' 17 | REQUIRES_PYTHON = '>=3.8.0' 18 | VERSION = '0.1.4' 19 | 20 | # What packages are required for this module to be executed? 21 | REQUIRED = [] 22 | 23 | # What packages are optional? 24 | EXTRAS = {} 25 | 26 | here = os.path.abspath(os.path.dirname(__file__)) 27 | 28 | try: 29 | with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f: 30 | long_description = '\n' + f.read() 31 | except FileNotFoundError: 32 | long_description = DESCRIPTION 33 | 34 | # Load the package's __version__.py module as a dictionary. 35 | about = {} 36 | if not VERSION: 37 | with open(os.path.join(here, NAME, '__version__.py')) as f: 38 | exec(f.read(), about) 39 | else: 40 | about['__version__'] = VERSION 41 | 42 | 43 | class UploadCommand(Command): 44 | """Support setup.py upload.""" 45 | 46 | description = 'Build and publish the package.' 47 | user_options = [] 48 | 49 | @staticmethod 50 | def status(s): 51 | """Prints things in bold.""" 52 | print('\033[1m{0}\033[0m'.format(s)) 53 | 54 | def initialize_options(self): 55 | pass 56 | 57 | def finalize_options(self): 58 | pass 59 | 60 | def run(self): 61 | try: 62 | self.status('Removing previous builds…') 63 | rmtree(os.path.join(here, 'dist')) 64 | except OSError: 65 | pass 66 | 67 | self.status('Building Source and Wheel (universal) distribution…') 68 | os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) 69 | 70 | self.status('Uploading the package to PyPI via Twine…') 71 | os.system('twine upload dist/*') 72 | 73 | self.status('Pushing git tags…') 74 | os.system('git tag v{0}'.format(about['__version__'])) 75 | os.system('git push --tags') 76 | 77 | sys.exit() 78 | 79 | 80 | # Where the magic happens: 81 | setup( 82 | name=NAME, 83 | version=about['__version__'], 84 | description=DESCRIPTION, 85 | long_description=long_description, 86 | long_description_content_type='text/markdown', 87 | author=AUTHOR, 88 | author_email=EMAIL, 89 | python_requires=REQUIRES_PYTHON, 90 | url=URL, 91 | packages=find_packages(exclude=('tests',)), 92 | install_requires=REQUIRED, 93 | extras_require=EXTRAS, 94 | include_package_data=True, 95 | license='Apache 2.0', 96 | classifiers=[ 97 | # Trove classifiers 98 | # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers 99 | 'License :: OSI Approved :: Apache Software License', 100 | 'Programming Language :: Python', 101 | 'Programming Language :: Python :: 3', 102 | 'Programming Language :: Python :: 3.8', 103 | 'Programming Language :: Python :: Implementation :: CPython', 104 | ], 105 | # $ setup.py publish support. 106 | cmdclass={ 107 | 'upload': UploadCommand, 108 | }, 109 | ) 110 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvfrazao/dhcppython/c442c3f6eca8244667df8a19d370f7569d81f08f/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_OptionList.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from dhcppython import options 3 | 4 | 5 | class OptionListTestCases(unittest.TestCase): 6 | def gen_optionslist(self): 7 | return options.OptionList( 8 | [ 9 | options.options.short_value_to_object(61, {'hwtype': 1, 'hwaddr': "8c:45:00:1d:48:16"}), 10 | options.options.short_value_to_object(57, 1500), 11 | options.options.short_value_to_object(60, "android-dhcp-9"), 12 | options.options.short_value_to_object(12, "Galaxy-S9"), 13 | options.options.short_value_to_object(55, [1, 3, 6, 15, 26, 28, 51, 58, 59, 43]) 14 | ] 15 | ) 16 | 17 | def test_OptionsList_append1(self): 18 | opt_list = self.gen_optionslist() 19 | opt_list.append(options.options.short_value_to_object(1, "255.255.255.0")) 20 | self.assertEqual( 21 | opt_list, 22 | options.OptionList( 23 | [ 24 | options.options.short_value_to_object(61, {'hwtype': 1, 'hwaddr': "8c:45:00:1d:48:16"}), 25 | options.options.short_value_to_object(57, 1500), 26 | options.options.short_value_to_object(60, "android-dhcp-9"), 27 | options.options.short_value_to_object(12, "Galaxy-S9"), 28 | options.options.short_value_to_object(55, [1, 3, 6, 15, 26, 28, 51, 58, 59, 43]), 29 | options.options.short_value_to_object(1, "255.255.255.0") 30 | ] 31 | ) 32 | ) 33 | 34 | def test_OptionsList_append2(self): 35 | opt_list = self.gen_optionslist() 36 | opt_list.append(options.options.short_value_to_object(57, 2000)) 37 | self.assertEqual( 38 | opt_list, 39 | options.OptionList( 40 | [ 41 | options.options.short_value_to_object(61, {'hwtype': 1, 'hwaddr': "8c:45:00:1d:48:16"}), 42 | options.options.short_value_to_object(57, 2000), 43 | options.options.short_value_to_object(60, "android-dhcp-9"), 44 | options.options.short_value_to_object(12, "Galaxy-S9"), 45 | options.options.short_value_to_object(55, [1, 3, 6, 15, 26, 28, 51, 58, 59, 43]), 46 | ] 47 | ) 48 | ) 49 | 50 | def test_OptionList_update_by_index1(self): 51 | opt_list = self.gen_optionslist() 52 | opt_list[1] = options.options.short_value_to_object(57, 2000) 53 | self.assertEqual( 54 | opt_list, 55 | options.OptionList( 56 | [ 57 | options.options.short_value_to_object(61, {'hwtype': 1, 'hwaddr': "8c:45:00:1d:48:16"}), 58 | options.options.short_value_to_object(57, 2000), 59 | options.options.short_value_to_object(60, "android-dhcp-9"), 60 | options.options.short_value_to_object(12, "Galaxy-S9"), 61 | options.options.short_value_to_object(55, [1, 3, 6, 15, 26, 28, 51, 58, 59, 43]), 62 | ] 63 | ) 64 | ) 65 | 66 | def test_OptionList_update_by_index2(self): 67 | opt_list = self.gen_optionslist() 68 | opt_list[0] = options.options.short_value_to_object(57, 2000) 69 | self.assertEqual( 70 | opt_list, 71 | options.OptionList( 72 | [ 73 | options.options.short_value_to_object(57, 2000), 74 | options.options.short_value_to_object(60, "android-dhcp-9"), 75 | options.options.short_value_to_object(12, "Galaxy-S9"), 76 | options.options.short_value_to_object(55, [1, 3, 6, 15, 26, 28, 51, 58, 59, 43]), 77 | ] 78 | ) 79 | ) 80 | 81 | def test_OptionList_update_by_index3(self): 82 | opt_list = self.gen_optionslist() 83 | opt_list[3] = options.options.short_value_to_object(57, 2000) 84 | self.assertEqual( 85 | opt_list, 86 | options.OptionList( 87 | [ 88 | options.options.short_value_to_object(61, {'hwtype': 1, 'hwaddr': "8c:45:00:1d:48:16"}), 89 | options.options.short_value_to_object(60, "android-dhcp-9"), 90 | options.options.short_value_to_object(57, 2000), 91 | options.options.short_value_to_object(55, [1, 3, 6, 15, 26, 28, 51, 58, 59, 43]), 92 | ] 93 | ) 94 | ) 95 | 96 | def test_OptionList_insert1(self): 97 | opt_list = self.gen_optionslist() 98 | opt_list.insert(1, options.options.short_value_to_object(57, 2000)) 99 | self.assertEqual( 100 | opt_list, 101 | options.OptionList( 102 | [ 103 | options.options.short_value_to_object(61, {'hwtype': 1, 'hwaddr': "8c:45:00:1d:48:16"}), 104 | options.options.short_value_to_object(57, 2000), 105 | options.options.short_value_to_object(60, "android-dhcp-9"), 106 | options.options.short_value_to_object(12, "Galaxy-S9"), 107 | options.options.short_value_to_object(55, [1, 3, 6, 15, 26, 28, 51, 58, 59, 43]), 108 | ] 109 | ) 110 | ) 111 | 112 | def test_OptionList_insert2(self): 113 | opt_list = self.gen_optionslist() 114 | opt_list.insert(0, options.options.short_value_to_object(57, 2000)) 115 | self.assertEqual( 116 | opt_list, 117 | options.OptionList( 118 | [ 119 | options.options.short_value_to_object(57, 2000), 120 | options.options.short_value_to_object(61, {'hwtype': 1, 'hwaddr': "8c:45:00:1d:48:16"}), 121 | options.options.short_value_to_object(60, "android-dhcp-9"), 122 | options.options.short_value_to_object(12, "Galaxy-S9"), 123 | options.options.short_value_to_object(55, [1, 3, 6, 15, 26, 28, 51, 58, 59, 43]), 124 | ] 125 | ) 126 | ) 127 | 128 | def test_OptionList_insert3(self): 129 | opt_list = self.gen_optionslist() 130 | opt_list.insert(3, options.options.short_value_to_object(57, 2000)) 131 | self.assertEqual( 132 | opt_list, 133 | options.OptionList( 134 | [ 135 | options.options.short_value_to_object(61, {'hwtype': 1, 'hwaddr': "8c:45:00:1d:48:16"}), 136 | options.options.short_value_to_object(60, "android-dhcp-9"), 137 | options.options.short_value_to_object(12, "Galaxy-S9"), 138 | options.options.short_value_to_object(57, 2000), 139 | options.options.short_value_to_object(55, [1, 3, 6, 15, 26, 28, 51, 58, 59, 43]), 140 | ] 141 | ) 142 | ) 143 | 144 | def test_OptionList_insert4(self): 145 | opt_list = self.gen_optionslist() 146 | opt_list.insert(0, options.options.short_value_to_object(1, "255.255.255.0")) 147 | self.assertEqual( 148 | opt_list, 149 | options.OptionList( 150 | [ 151 | options.options.short_value_to_object(1, "255.255.255.0"), 152 | options.options.short_value_to_object(61, {'hwtype': 1, 'hwaddr': "8c:45:00:1d:48:16"}), 153 | options.options.short_value_to_object(57, 1500), 154 | options.options.short_value_to_object(60, "android-dhcp-9"), 155 | options.options.short_value_to_object(12, "Galaxy-S9"), 156 | options.options.short_value_to_object(55, [1, 3, 6, 15, 26, 28, 51, 58, 59, 43]), 157 | ] 158 | ) 159 | ) 160 | 161 | def test_OptionList_insert5(self): 162 | opt_list = self.gen_optionslist() 163 | opt_list.insert(-1, options.options.short_value_to_object(1, "255.255.255.0")) 164 | self.assertEqual( 165 | opt_list, 166 | options.OptionList( 167 | [ 168 | options.options.short_value_to_object(61, {'hwtype': 1, 'hwaddr': "8c:45:00:1d:48:16"}), 169 | options.options.short_value_to_object(57, 1500), 170 | options.options.short_value_to_object(60, "android-dhcp-9"), 171 | options.options.short_value_to_object(12, "Galaxy-S9"), 172 | options.options.short_value_to_object(1, "255.255.255.0"), 173 | options.options.short_value_to_object(55, [1, 3, 6, 15, 26, 28, 51, 58, 59, 43]), 174 | ] 175 | ) 176 | ) 177 | 178 | def test_OptionList_insert6(self): 179 | opt_list = self.gen_optionslist() 180 | opt_list.insert(5, options.options.short_value_to_object(1, "255.255.255.0")) 181 | self.assertEqual( 182 | opt_list, 183 | options.OptionList( 184 | [ 185 | options.options.short_value_to_object(61, {'hwtype': 1, 'hwaddr': "8c:45:00:1d:48:16"}), 186 | options.options.short_value_to_object(57, 1500), 187 | options.options.short_value_to_object(60, "android-dhcp-9"), 188 | options.options.short_value_to_object(12, "Galaxy-S9"), 189 | options.options.short_value_to_object(55, [1, 3, 6, 15, 26, 28, 51, 58, 59, 43]), 190 | options.options.short_value_to_object(1, "255.255.255.0"), 191 | ] 192 | ) 193 | ) 194 | 195 | def test_OptionList_del1(self): 196 | opt_list = self.gen_optionslist() 197 | del opt_list[0] 198 | self.assertEqual( 199 | opt_list, 200 | options.OptionList( 201 | [ 202 | options.options.short_value_to_object(57, 1500), 203 | options.options.short_value_to_object(60, "android-dhcp-9"), 204 | options.options.short_value_to_object(12, "Galaxy-S9"), 205 | options.options.short_value_to_object(55, [1, 3, 6, 15, 26, 28, 51, 58, 59, 43]), 206 | ] 207 | ) 208 | ) 209 | 210 | def test_OptionList_del2(self): 211 | opt_list = self.gen_optionslist() 212 | del opt_list[-1] 213 | self.assertEqual( 214 | opt_list, 215 | options.OptionList( 216 | [ 217 | options.options.short_value_to_object(61, {'hwtype': 1, 'hwaddr': "8c:45:00:1d:48:16"}), 218 | options.options.short_value_to_object(57, 1500), 219 | options.options.short_value_to_object(60, "android-dhcp-9"), 220 | options.options.short_value_to_object(12, "Galaxy-S9"), 221 | ] 222 | ) 223 | ) 224 | 225 | def test_OptionList_del3(self): 226 | opt_list = self.gen_optionslist() 227 | del opt_list[2] 228 | self.assertEqual( 229 | opt_list, 230 | options.OptionList( 231 | [ 232 | options.options.short_value_to_object(61, {'hwtype': 1, 'hwaddr': "8c:45:00:1d:48:16"}), 233 | options.options.short_value_to_object(57, 1500), 234 | options.options.short_value_to_object(12, "Galaxy-S9"), 235 | options.options.short_value_to_object(55, [1, 3, 6, 15, 26, 28, 51, 58, 59, 43]), 236 | ] 237 | ) 238 | ) 239 | 240 | def test_OptionList_len1(self): 241 | self.assertEqual( 242 | len(self.gen_optionslist()), 243 | 5 244 | ) 245 | 246 | def test_OptionList_len2(self): 247 | opt_list = self.gen_optionslist() 248 | opt_list.insert(5, options.options.short_value_to_object(1, "255.255.255.0")) 249 | opt_list.append(options.options.short_value_to_object(2, 3600)) 250 | del opt_list[5] 251 | opt_list.append(options.options.short_value_to_object(1, "255.255.255.0")) 252 | del opt_list[5] 253 | 254 | self.assertEqual( 255 | len(opt_list), 256 | 6 257 | ) 258 | 259 | def test_OptionList_contains1(self): 260 | self.assertEqual( 261 | 57 in self.gen_optionslist(), 262 | True 263 | ) 264 | 265 | def test_OptionList_contains2(self): 266 | self.assertEqual( 267 | 1 in self.gen_optionslist(), 268 | False 269 | ) 270 | 271 | def test_OptionList_contains3(self): 272 | self.assertEqual( 273 | options.options.short_value_to_object(57, 1500) in self.gen_optionslist(), 274 | True 275 | ) 276 | 277 | def test_OptionList_contains4(self): 278 | self.assertEqual( 279 | options.options.short_value_to_object(2, 3600) in self.gen_optionslist(), 280 | False 281 | ) 282 | 283 | def test_OptionList_as_dict(self): 284 | self.assertEqual( 285 | self.gen_optionslist().as_dict(), 286 | {'client_identifier': {'hwtype': 1, 'hwaddr': '8C:45:00:1D:48:16'}, 'max_dhcp_message_size': 1500, 'vendor_class_identifier': 'android-dhcp-9', 'hostname': 'Galaxy-S9', 'parameter_request_list': [1, 3, 6, 15, 26, 28, 51, 58, 59, 43]} 287 | ) 288 | 289 | def test_OptionList_json(self): 290 | json_expected = ( 291 | '{\n "client_identifier": {\n "hwtype": 1,\n ' 292 | '"hwaddr": "8C:45:00:1D:48:16"\n },\n "max_dhcp_message_' 293 | 'size": 1500,\n "vendor_class_identifier": "android-dhcp-9"' 294 | ',\n "hostname": "Galaxy-S9",\n "parameter_request_list"' 295 | ': [\n 1,\n 3,\n 6,\n 15,\n ' 296 | ' 26,\n 28,\n 51,\n 58,\n 59,\n ' 297 | ' 43\n ]\n}' 298 | ) 299 | self.assertEqual( 300 | self.gen_optionslist().json, 301 | json_expected 302 | ) 303 | 304 | 305 | if __name__ == "__main__": 306 | unittest.main() 307 | -------------------------------------------------------------------------------- /tests/test_options.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from dhcppython import options 3 | 4 | 5 | class OptionsTestCases(unittest.TestCase): 6 | def setUp(self): 7 | self.options_client = options.options 8 | self.ip_array_list = ["192.168.56.0", "1.1.1.1", "255.255.255.0"] 9 | self.ip_array_bytes = b"\xc0\xa8\x38\x00" + b"\x01\x01\x01\x01" + b"\xff\xff\xff\x00" 10 | # Option 12 11 | self.string_str = "Galaxy-S9" 12 | self.string_bytes = b"\x47\x61\x6c\x61\x78\x79\x2d\x53\x39" 13 | self.opt12_bytes = b"\x0c\x09\x47\x61\x6c\x61\x78\x79\x2d\x53\x39" 14 | # Option 13 15 | self.uint16_int = 256 16 | self.uint16_bytes = b'\x01\x00' 17 | self.opt13_bytes = b'\x0d\x02\x01\x00' 18 | # Option 19 19 | self.bool_bool = True 20 | self.bool_bytes = b"\x01" 21 | self.opt19_bytes = b"\x13\x01\x01" 22 | # Option 21 23 | self.policy_filter_dict = [ 24 | {"address": "1.1.1.1", "mask": "255.255.255.0"}, 25 | {"address": "192.168.56.2", "mask": "255.255.255.0"} 26 | ] 27 | self.policy_filter_bytes = ( 28 | b"\x01\x01\x01\x01" + b"\xff\xff\xff\x00" + 29 | b"\xc0\xa8\x38\x02" + b"\xff\xff\xff\x00" 30 | ) 31 | self.opt21_bytes = b"\x15\x10" + self.policy_filter_bytes 32 | # Option 23 33 | self.uint8_int = 123 34 | self.uint8_bytes = b'\x7b' 35 | self.opt23_bytes = b'\x17\x01\x7b' 36 | # Option 24 37 | self.uint32_int = 1234567 38 | self.uint32_bytes = b'\x00\x12\xd6\x87' 39 | self.opt24_bytes = b'\x18\x04' + self.uint32_bytes 40 | # Option 25 41 | self.uint16array_list = [12349, 23459, 34569, 45679] 42 | self.uint16array_bytes = ( 43 | b'\x30\x3d' + b'\x5b\xa3' + b'\x87\x09' + b'\xb2\x6f' 44 | ) 45 | self.opt25_bytes = b"\x19\x08" + self.uint16array_bytes 46 | # Option 33 47 | self.staticroute_list = [ 48 | {"destination": "1.1.1.1", "router": "255.255.255.0"}, 49 | {"destination": "192.168.56.2", "router": "255.255.255.0"} 50 | ] 51 | self.staticroute_bytes = ( 52 | b"\x01\x01\x01\x01" + b"\xff\xff\xff\x00" + 53 | b"\xc0\xa8\x38\x02" + b"\xff\xff\xff\x00" 54 | ) 55 | self.opt33_bytes = b"\x21\x10" + self.staticroute_bytes 56 | # Option 43 57 | self.bin_str = "0x0B 0x1C 0x01 0x02" 58 | self.bin_bytes = b"\x0b\x1c\x01\x02" 59 | self.opt43_bytes = b"\x2b\x04" + self.bin_bytes 60 | # Option 46 61 | self.netbios_node_str = "B-node" 62 | self.netbios_node_bytes = b"\x01" 63 | self.opt46_bytes = b"\x2e\x01" + self.netbios_node_bytes 64 | # Option 52 65 | self.overload_str = "'file' field is used to hold options" 66 | self.overload_bytes = b"\x01" 67 | self.opt52_bytes = b"\x34\x01" + self.overload_bytes 68 | # Option 53 69 | self.message_type_str = "DHCPREQUEST" 70 | self.message_type_bytes = b"\x03" 71 | self.opt53_bytes = b"\x35\x01" + self.message_type_bytes 72 | # Option 55 73 | self.parameter_request_list = [43, 53, 56, 74] 74 | self.parameter_request_bytes = b"\x2b\x35\x38\x4a" 75 | self.opt55_bytes = b"\x37\x04" + self.parameter_request_bytes 76 | # Option 61 77 | self.client_identifier_dict = {'hwtype': 1, 'hwaddr': '8C:45:00:1D:48:16'} 78 | self.client_identifier_bytes = b'\x01\x8c\x45\x00\x1d\x48\x16' 79 | self.opt61_bytes = b'\x3d\x07' + self.client_identifier_bytes 80 | # Unknown Opt 81 | self.unknown_value = {'Unknown_250': "0x0A 0x12 0xDE 0xCA"} 82 | self.unknown_data = b'\x0a\x12\xde\xca' 83 | self.unknownopt_bytes = b'\xfa\x04' + self.unknown_data 84 | 85 | # Option 0 - Pad 86 | def test_pad_bytes_to_obj(self): 87 | self.assertEqual(self.options_client.bytes_to_object(b"\x00"), options.Pad(0, 0, b"")) 88 | 89 | def test_pad_value_to_obj(self): 90 | self.assertEqual(self.options_client.value_to_object({"pad_option": ""}), options.Pad(0, 0, b"")) 91 | 92 | def test_pad_value_to_bytes(self): 93 | self.assertEqual(self.options_client.value_to_bytes({"pad_option": ""}), b"\x00") 94 | 95 | def test_pad_obj_to_value(self): 96 | self.assertEqual( 97 | options.Pad(0, 0, b"").value, 98 | {"pad_option": ""} 99 | ) 100 | 101 | # Option 255 - End 102 | def test_opt255_bytes_to_obj(self): 103 | self.assertEqual(self.options_client.bytes_to_object(b"\xff"), options.End(255, 0, b"")) 104 | 105 | def test_opt255_value_to_obj(self): 106 | self.assertEqual(self.options_client.value_to_object({"end_option": ""}), options.End(255, 0, b"")) 107 | 108 | def test_opt255_value_to_bytes(self): 109 | self.assertEqual(self.options_client.value_to_bytes({"end_option": ""}), b"\xff") 110 | 111 | def test_opt255_obj_to_value(self): 112 | self.assertEqual( 113 | options.End(255, 0, b"").value, 114 | {"end_option": ""} 115 | ) 116 | 117 | # Option 1 - SubnetMask <- IPOption 118 | def test_opt1_bytes_to_obj(self): 119 | self.assertEqual( 120 | self.options_client.bytes_to_object(b'\x01\x04\xff\xff\xff\x00'), 121 | options.SubnetMask(1, 4, b'\xff\xff\xff\x00') 122 | ) 123 | 124 | def test_opt1_value_to_obj(self): 125 | self.assertEqual( 126 | self.options_client.value_to_object({"subnet_mask": "255.255.255.0"}), 127 | options.SubnetMask(1, 4, b'\xff\xff\xff\x00') 128 | ) 129 | 130 | def test_opt1_value_to_bytes(self): 131 | self.assertEqual( 132 | self.options_client.value_to_bytes({"subnet_mask": "255.255.255.0"}), 133 | b'\x01\x04\xff\xff\xff\x00' 134 | ) 135 | 136 | def test_opt1_obj_to_value(self): 137 | self.assertEqual( 138 | options.SubnetMask(1, 4, b'\xff\xff\xff\x00').value, 139 | {"subnet_mask": "255.255.255.0"} 140 | ) 141 | 142 | # Option 2 - TimeOffset <- int32Option 143 | def test_opt2_bytes_to_obj(self): 144 | self.assertEqual( 145 | self.options_client.bytes_to_object(b'\x02\x04\x00\x00\x0e\x10'), 146 | options.TimeOffset(2, 4, b'\x00\x00\x0e\x10') 147 | ) 148 | 149 | def test_opt2_value_to_obj(self): 150 | self.assertEqual( 151 | self.options_client.value_to_object({"time_offset_s": 3600}), 152 | options.TimeOffset(2, 4, b'\x00\x00\x0e\x10') 153 | ) 154 | 155 | def test_opt2_value_to_bytes(self): 156 | self.assertEqual( 157 | self.options_client.value_to_bytes({"time_offset_s": 3600}), 158 | b'\x02\x04\x00\x00\x0e\x10' 159 | ) 160 | 161 | def test_opt2_obj_to_value(self): 162 | self.assertEqual( 163 | options.TimeOffset(2, 4, b'\x00\x00\x0e\x10').value, 164 | {"time_offset_s": 3600} 165 | ) 166 | 167 | # Use -3600 168 | def test_opt2_bytes_to_obj2(self): 169 | self.assertEqual( 170 | self.options_client.bytes_to_object(b'\x02\x04\xff\xff\xf1\xf0'), 171 | options.TimeOffset(2, 4, b'\xff\xff\xf1\xf0') 172 | ) 173 | 174 | def test_opt2_value_to_obj2(self): 175 | self.assertEqual( 176 | self.options_client.value_to_object({"time_offset_s": -3600}), 177 | options.TimeOffset(2, 4, b'\xff\xff\xf1\xf0') 178 | ) 179 | 180 | def test_opt2_value_to_bytes2(self): 181 | self.assertEqual( 182 | self.options_client.value_to_bytes({"time_offset_s": -3600}), 183 | b'\x02\x04\xff\xff\xf1\xf0' 184 | ) 185 | 186 | # Option 3 - Router <- IPArrayOption 187 | def test_opt3_bytes_to_obj(self): 188 | self.assertEqual( 189 | self.options_client.bytes_to_object(b'\x03\x0c' + self.ip_array_bytes), 190 | options.Router(3, 12, self.ip_array_bytes) 191 | ) 192 | 193 | def test_opt3_value_to_obj(self): 194 | self.assertEqual( 195 | self.options_client.value_to_object({"routers": self.ip_array_list}), 196 | options.Router(3, 12, self.ip_array_bytes) 197 | ) 198 | 199 | def test_opt3_value_to_bytes(self): 200 | self.assertEqual( 201 | self.options_client.value_to_bytes({"routers": self.ip_array_list}), 202 | b'\x03\x0c' + self.ip_array_bytes 203 | ) 204 | 205 | def test_opt3_obj_to_value(self): 206 | self.assertEqual( 207 | options.Router(3, 12, self.ip_array_bytes).value, 208 | {"routers": self.ip_array_list} 209 | ) 210 | 211 | # Option 12 - Hostname <- StrOption 212 | def test_opt12_bytes_to_obj(self): 213 | self.assertEqual( 214 | self.options_client.bytes_to_object(self.opt12_bytes), 215 | options.Hostname(12, 9, self.string_bytes) 216 | ) 217 | 218 | def test_opt12_value_to_obj(self): 219 | self.assertEqual( 220 | self.options_client.value_to_object({"hostname": self.string_str}), 221 | options.Hostname(12, 9, self.string_bytes) 222 | ) 223 | 224 | def test_opt12_value_to_bytes(self): 225 | self.assertEqual( 226 | self.options_client.value_to_bytes({"hostname": self.string_str}), 227 | self.opt12_bytes 228 | ) 229 | 230 | def test_opt12_obj_to_value(self): 231 | self.assertEqual( 232 | options.Hostname(12, 9, self.string_bytes).value, 233 | {"hostname": self.string_str} 234 | ) 235 | 236 | # Option 13 - BootfileSize <- uint16Option 237 | def test_opt13_bytes_to_obj(self): 238 | self.assertEqual( 239 | self.options_client.bytes_to_object(self.opt13_bytes), 240 | options.BootfileSize(13, 2, self.uint16_bytes) 241 | ) 242 | 243 | def test_opt13_value_to_obj(self): 244 | self.assertEqual( 245 | self.options_client.value_to_object({"bootfile_size": self.uint16_int}), 246 | options.BootfileSize(13, 2, self.uint16_bytes) 247 | ) 248 | 249 | def test_opt13_value_to_bytes(self): 250 | self.assertEqual( 251 | self.options_client.value_to_bytes({"bootfile_size": self.uint16_int}), 252 | self.opt13_bytes 253 | ) 254 | 255 | def test_opt13_obj_to_value(self): 256 | self.assertEqual( 257 | options.BootfileSize(13, 2, self.uint16_bytes).value, 258 | {"bootfile_size": self.uint16_int} 259 | ) 260 | 261 | # Option 19 - IPForwarding <- BoolOption 262 | def test_opt19_bytes_to_obj(self): 263 | self.assertEqual( 264 | self.options_client.bytes_to_object(self.opt19_bytes), 265 | options.IPForwarding(19, 1, self.bool_bytes) 266 | ) 267 | 268 | def test_opt19_value_to_obj(self): 269 | self.assertEqual( 270 | self.options_client.value_to_object({"ip_forwarding": self.bool_bool}), 271 | options.IPForwarding(19, 1, self.bool_bytes) 272 | ) 273 | 274 | def test_opt19_value_to_bytes(self): 275 | self.assertEqual( 276 | self.options_client.value_to_bytes({"ip_forwarding": self.bool_bool}), 277 | self.opt19_bytes 278 | ) 279 | 280 | def test_opt19_obj_to_value(self): 281 | self.assertEqual( 282 | options.IPForwarding(19, 1, self.bool_bytes).value, 283 | {"ip_forwarding": self.bool_bool} 284 | ) 285 | 286 | # Option 21 - PolicyFilter <- Complex Option 287 | def test_opt21_bytes_to_obj(self): 288 | self.assertEqual( 289 | self.options_client.bytes_to_object(self.opt21_bytes), 290 | options.PolicyFilter(21, 16, self.policy_filter_bytes) 291 | ) 292 | 293 | def test_opt21_value_to_obj(self): 294 | self.assertEqual( 295 | self.options_client.value_to_object({"policy_filters": self.policy_filter_dict}), 296 | options.PolicyFilter(21, 16, self.policy_filter_bytes) 297 | ) 298 | 299 | def test_opt21_value_to_bytes(self): 300 | self.assertEqual( 301 | self.options_client.value_to_bytes({"policy_filters": self.policy_filter_dict}), 302 | self.opt21_bytes 303 | ) 304 | 305 | def test_opt21_obj_to_value(self): 306 | self.assertEqual( 307 | options.PolicyFilter(21, 16, self.policy_filter_bytes).value, 308 | {"policy_filters": self.policy_filter_dict} 309 | ) 310 | 311 | # Option 23 - IPTTL <- uint8Option 312 | def test_opt23_bytes_to_obj(self): 313 | self.assertEqual( 314 | self.options_client.bytes_to_object(self.opt23_bytes), 315 | options.IPTTL(23, 1, self.uint8_bytes) 316 | ) 317 | 318 | def test_opt23_value_to_obj(self): 319 | self.assertEqual( 320 | self.options_client.value_to_object({"default_ip_ttl": self.uint8_int}), 321 | options.IPTTL(23, 1, self.uint8_bytes) 322 | ) 323 | 324 | def test_opt23_value_to_bytes(self): 325 | self.assertEqual( 326 | self.options_client.value_to_bytes({"default_ip_ttl": self.uint8_int}), 327 | self.opt23_bytes 328 | ) 329 | 330 | def test_opt23_obj_to_value(self): 331 | self.assertEqual( 332 | options.IPTTL(23, 1, self.uint8_bytes).value, 333 | {"default_ip_ttl": self.uint8_int} 334 | ) 335 | 336 | # Option 24 - PathMTUAgingTimeout <- uint32Option 337 | def test_opt24_bytes_to_obj(self): 338 | self.assertEqual( 339 | self.options_client.bytes_to_object(self.opt24_bytes), 340 | options.PathMTUAgingTimeout(24, 4, self.uint32_bytes) 341 | ) 342 | 343 | def test_opt24_value_to_obj(self): 344 | self.assertEqual( 345 | self.options_client.value_to_object({"path_MTU_aging_timeout": self.uint32_int}), 346 | options.PathMTUAgingTimeout(24, 4, self.uint32_bytes) 347 | ) 348 | 349 | def test_opt24_value_to_bytes(self): 350 | self.assertEqual( 351 | self.options_client.value_to_bytes({"path_MTU_aging_timeout": self.uint32_int}), 352 | self.opt24_bytes 353 | ) 354 | 355 | def test_opt24_obj_to_value(self): 356 | self.assertEqual( 357 | options.PathMTUAgingTimeout(24, 4, self.uint32_bytes).value, 358 | {"path_MTU_aging_timeout": self.uint32_int} 359 | ) 360 | 361 | # Option 25 - PathMTUAgingTable <- uint16ArrayOption 362 | def test_opt25_bytes_to_obj(self): 363 | self.assertEqual( 364 | self.options_client.bytes_to_object(self.opt25_bytes), 365 | options.PathMTUAgingTable(25, 8, self.uint16array_bytes) 366 | ) 367 | 368 | def test_opt25_value_to_obj(self): 369 | self.assertEqual( 370 | self.options_client.value_to_object({"path_mtu_aging_table": self.uint16array_list}), 371 | options.PathMTUAgingTable(25, 8, self.uint16array_bytes) 372 | ) 373 | 374 | def test_opt25_value_to_bytes(self): 375 | self.assertEqual( 376 | self.options_client.value_to_bytes({"path_mtu_aging_table": self.uint16array_list}), 377 | self.opt25_bytes 378 | ) 379 | 380 | def test_opt25_obj_to_value(self): 381 | self.assertEqual( 382 | options.PathMTUAgingTable(25, 8, self.uint16array_bytes).value, 383 | {"path_mtu_aging_table": self.uint16array_list} 384 | ) 385 | 386 | # Option 33 - StaticRoute <- Complex 387 | def test_opt33_bytes_to_obj(self): 388 | self.assertEqual( 389 | self.options_client.bytes_to_object(self.opt33_bytes), 390 | options.StaticRoute(33, 16, self.staticroute_bytes) 391 | ) 392 | 393 | def test_opt33_value_to_obj(self): 394 | self.assertEqual( 395 | self.options_client.value_to_object({"static_routes": self.staticroute_list}), 396 | options.StaticRoute(33, 16, self.staticroute_bytes) 397 | ) 398 | 399 | def test_opt33_value_to_bytes(self): 400 | self.assertEqual( 401 | self.options_client.value_to_bytes({"static_routes": self.staticroute_list}), 402 | self.opt33_bytes 403 | ) 404 | 405 | def test_opt33_obj_to_value(self): 406 | self.assertEqual( 407 | options.StaticRoute(33, 16, self.staticroute_bytes).value, 408 | {"static_routes": self.staticroute_list} 409 | ) 410 | 411 | # Option 43 - VendorSpecificInformation <- BinOption 412 | def test_opt43_bytes_to_obj(self): 413 | self.assertEqual( 414 | self.options_client.bytes_to_object(self.opt43_bytes), 415 | options.VendorSpecificInformation(43, 4, self.bin_bytes) 416 | ) 417 | 418 | def test_opt43_value_to_obj(self): 419 | self.assertEqual( 420 | self.options_client.value_to_object({"vendor_specific_information": self.bin_str}), 421 | options.VendorSpecificInformation(43, 4, self.bin_bytes) 422 | ) 423 | 424 | def test_opt43_value_to_bytes(self): 425 | self.assertEqual( 426 | self.options_client.value_to_bytes({"vendor_specific_information": self.bin_str}), 427 | self.opt43_bytes 428 | ) 429 | 430 | def test_opt43_obj_to_value(self): 431 | self.assertEqual( 432 | options.VendorSpecificInformation(43, 4, self.bin_bytes).value, 433 | {"vendor_specific_information": self.bin_str} 434 | ) 435 | 436 | # Option 46 - NetbiosNodeType <- Complex 437 | def test_opt46_bytes_to_obj(self): 438 | self.assertEqual( 439 | self.options_client.bytes_to_object(self.opt46_bytes), 440 | options.NetbiosNodeType(46, 1, self.netbios_node_bytes) 441 | ) 442 | 443 | def test_opt46_value_to_obj(self): 444 | self.assertEqual( 445 | self.options_client.value_to_object({"netbios_node_type": self.netbios_node_str}), 446 | options.NetbiosNodeType(46, 1, self.netbios_node_bytes) 447 | ) 448 | 449 | def test_opt46_value_to_bytes(self): 450 | self.assertEqual( 451 | self.options_client.value_to_bytes({"netbios_node_type": self.netbios_node_str}), 452 | self.opt46_bytes 453 | ) 454 | 455 | def test_opt46_obj_to_value(self): 456 | self.assertEqual( 457 | options.NetbiosNodeType(46, 1, self.netbios_node_bytes).value, 458 | {"netbios_node_type": self.netbios_node_str} 459 | ) 460 | 461 | # Option 52 - Overload <- Complex 462 | def test_opt52_bytes_to_obj(self): 463 | self.assertEqual( 464 | self.options_client.bytes_to_object(self.opt52_bytes), 465 | options.Overload(52, 1, self.overload_bytes) 466 | ) 467 | 468 | def test_opt52_value_to_obj(self): 469 | self.assertEqual( 470 | self.options_client.value_to_object({"option_overload": self.overload_str}), 471 | options.Overload(52, 1, self.overload_bytes) 472 | ) 473 | 474 | def test_opt52_value_to_bytes(self): 475 | self.assertEqual( 476 | self.options_client.value_to_bytes({"option_overload": self.overload_str}), 477 | self.opt52_bytes 478 | ) 479 | 480 | def test_opt52_obj_to_value(self): 481 | self.assertEqual( 482 | options.Overload(52, 1, self.overload_bytes).value, 483 | {"option_overload": self.overload_str} 484 | ) 485 | 486 | # Option 53 - MessageType <- Complex 487 | def test_opt53_bytes_to_obj(self): 488 | self.assertEqual( 489 | self.options_client.bytes_to_object(self.opt53_bytes), 490 | options.MessageType(53, 1, self.message_type_bytes) 491 | ) 492 | 493 | def test_opt53_value_to_obj(self): 494 | self.assertEqual( 495 | self.options_client.value_to_object({"dhcp_message_type": self.message_type_str}), 496 | options.MessageType(53, 1, self.message_type_bytes) 497 | ) 498 | 499 | def test_opt53_value_to_bytes(self): 500 | self.assertEqual( 501 | self.options_client.value_to_bytes({"dhcp_message_type": self.message_type_str}), 502 | self.opt53_bytes 503 | ) 504 | 505 | def test_opt53_obj_to_value(self): 506 | self.assertEqual( 507 | options.MessageType(53, 1, self.message_type_bytes).value, 508 | {"dhcp_message_type": self.message_type_str} 509 | ) 510 | 511 | # Option 55 - ParameterRequestList <- uint8ArrayOption 512 | def test_opt55_bytes_to_obj(self): 513 | self.assertEqual( 514 | self.options_client.bytes_to_object(self.opt55_bytes), 515 | options.ParameterRequestList(55, 4, self.parameter_request_bytes) 516 | ) 517 | 518 | def test_opt55_value_to_obj(self): 519 | self.assertEqual( 520 | self.options_client.value_to_object({"parameter_request_list": self.parameter_request_list}), 521 | options.ParameterRequestList(55, 4, self.parameter_request_bytes) 522 | ) 523 | 524 | def test_opt55_value_to_bytes(self): 525 | self.assertEqual( 526 | self.options_client.value_to_bytes({"parameter_request_list": self.parameter_request_list}), 527 | self.opt55_bytes 528 | ) 529 | 530 | def test_opt55_obj_to_value(self): 531 | self.assertEqual( 532 | options.ParameterRequestList(55, 4, self.parameter_request_bytes).value, 533 | {"parameter_request_list": self.parameter_request_list} 534 | ) 535 | 536 | # Option 61 - ClientIdentifier <- Complex 537 | def test_opt61_bytes_to_obj(self): 538 | self.assertEqual( 539 | self.options_client.bytes_to_object(self.opt61_bytes), 540 | options.ClientIdentifier(61, 7, self.client_identifier_bytes) 541 | ) 542 | 543 | def test_opt61_value_to_obj(self): 544 | self.assertEqual( 545 | self.options_client.value_to_object({"client_identifier": self.client_identifier_dict}), 546 | options.ClientIdentifier(61, 7, self.client_identifier_bytes) 547 | ) 548 | 549 | def test_opt61_value_to_bytes(self): 550 | self.assertEqual( 551 | self.options_client.value_to_bytes({"client_identifier": self.client_identifier_dict}), 552 | self.opt61_bytes 553 | ) 554 | 555 | def test_opt61_obj_to_value(self): 556 | self.assertEqual( 557 | options.ClientIdentifier(61, 7, self.client_identifier_bytes).value, 558 | {"client_identifier": self.client_identifier_dict} 559 | ) 560 | 561 | # Unkown options <- UnknownOption 562 | def test_unknownopt_bytes_to_obj(self): 563 | self.assertEqual( 564 | self.options_client.bytes_to_object(self.unknownopt_bytes), 565 | options.UnknownOption(250, 4, self.unknown_data) 566 | ) 567 | 568 | def test_unknownopt_value_to_obj(self): 569 | self.assertEqual( 570 | self.options_client.value_to_object(self.unknown_value), 571 | options.UnknownOption(250, 4, self.unknown_data) 572 | ) 573 | 574 | def test_unknownopt_value_to_bytes(self): 575 | self.assertEqual( 576 | self.options_client.value_to_bytes(self.unknown_value), 577 | self.unknownopt_bytes 578 | ) 579 | 580 | def test_unknownopt_obj_to_value(self): 581 | self.assertEqual( 582 | options.UnknownOption(250, 4, self.unknown_data).value, 583 | self.unknown_value 584 | ) 585 | 586 | if __name__ == "__main__": 587 | unittest.main() 588 | -------------------------------------------------------------------------------- /tests/test_packet.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import struct 3 | import ipaddress 4 | from typing import List 5 | from dhcppython import packet 6 | from dhcppython import options 7 | 8 | 9 | class PacketTestCases(unittest.TestCase): 10 | def test_parse_discover1(self): 11 | self.assertEqual( 12 | packet.DHCPPacket.from_bytes(discover_linux).asbytes, 13 | discover_linux 14 | ) 15 | def test_parse_discover2(self): 16 | self.assertEqual( 17 | packet.DHCPPacket.Discover( 18 | "8c:45:00:1d:48:16", 19 | seconds=1, 20 | tx_id=0xeabec397, 21 | use_broadcast=False, 22 | option_list=[ 23 | options.options.short_value_to_object(61, {'hwtype': 1, 'hwaddr': '8C:45:00:1D:48:16'}), 24 | options.options.short_value_to_object(57, 1500), 25 | options.options.short_value_to_object(60, "android-dhcp-9"), 26 | options.options.short_value_to_object(12, "Galaxy-S9"), 27 | options.options.short_value_to_object(55, [1, 3, 6, 15, 26, 28, 51, 58, 59, 43]) 28 | ] 29 | ).asbytes, 30 | discover_android 31 | ) 32 | def test_parse_offer1(self): 33 | self.assertEqual( 34 | packet.DHCPPacket.from_bytes(offer_linux).asbytes, 35 | offer_linux 36 | ) 37 | def test_parse_request1(self): 38 | self.assertEqual( 39 | packet.DHCPPacket.from_bytes(request_linux).asbytes, 40 | request_linux 41 | ) 42 | def test_parse_request2(self): 43 | self.assertEqual( 44 | packet.DHCPPacket.from_bytes(request_android_bytes).asbytes, 45 | request_android_bytes 46 | ) 47 | def test_parse_ack1(self): 48 | self.assertEqual( 49 | packet.DHCPPacket.from_bytes(ack_linux).asbytes, 50 | ack_linux 51 | ) 52 | 53 | 54 | request_android: List[int] = [ 55 | 0x01, 0x01, 0x06, 0x00, 0xea, 0xbe, 56 | 0xc3, 0x97, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 57 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8c, 0x45, 58 | 0x00, 0x1d, 0x48, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 59 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 60 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 61 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 62 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 63 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 64 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 65 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 66 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 67 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 68 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 69 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 70 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 71 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 72 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 73 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 74 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 75 | 0x00, 0x00, 0x63, 0x82, 0x53, 0x63, 0x35, 0x01, 0x03, 0x3d, 0x07, 0x01, 76 | 0x8c, 0x45, 0x00, 0x1d, 0x48, 0x16, 0x32, 0x04, 0xc0, 0xa8, 0x01, 0xa6, 77 | 0x36, 0x04, 0xc0, 0xa8, 0x01, 0xfe, 0x39, 0x02, 0x05, 0xdc, 0x3c, 0x0e, 78 | 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2d, 0x64, 0x68, 0x63, 0x70, 79 | 0x2d, 0x39, 0x0c, 0x09, 0x47, 0x61, 0x6c, 0x61, 0x78, 0x79, 0x2d, 0x53, 80 | 0x39, 0x37, 0x0a, 0x01, 0x03, 0x06, 0x0f, 0x1a, 0x1c, 0x33, 0x3a, 0x3b, 81 | 0x2b, 0xff 82 | ] 83 | request_android_bytes: bytes = struct.pack( 84 | ">" + len(request_android) * "B", *request_android 85 | ) 86 | 87 | discover_android = ( 88 | b"\x01\x01\x06\x00\xea\xbe" 89 | b"\xc3\x97\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 90 | b"\x00\x00\x00\x00\x00\x00\x8c\x45\x00\x1d\x48\x16\x00\x00\x00\x00" 91 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 92 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 93 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 94 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 95 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 96 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 97 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 98 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 99 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 100 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 101 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 102 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 103 | b"\x00\x00\x00\x00\x00\x00\x63\x82\x53\x63\x35\x01\x01\x3d\x07\x01" 104 | b"\x8c\x45\x00\x1d\x48\x16\x39\x02\x05\xdc\x3c\x0e\x61\x6e\x64\x72" 105 | b"\x6f\x69\x64\x2d\x64\x68\x63\x70\x2d\x39\x0c\x09\x47\x61\x6c\x61" 106 | b"\x78\x79\x2d\x53\x39\x37\x0a\x01\x03\x06\x0f\x1a\x1c\x33\x3a\x3b" 107 | b"\x2b\xff" 108 | 109 | ) 110 | 111 | discover_linux = ( 112 | b"\x01\x01\x06\x00\x2e\xf9" 113 | b"\x31\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 114 | b"\x00\x00\x00\x00\x00\x00\x08\x00\x27\x92\x1f\xae\x00\x00\x00\x00" 115 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 116 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 117 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 118 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 119 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 120 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 121 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 122 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 123 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 124 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 125 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 126 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 127 | b"\x00\x00\x00\x00\x00\x00\x63\x82\x53\x63\x35\x01\x01\x32\x04\xc0" 128 | b"\xa8\x38\x03\x0c\x05\x6d\x61\x72\x69\x6f\x37\x0d\x01\x1c\x02\x03" 129 | b"\x0f\x06\x77\x0c\x2c\x2f\x1a\x79\x2a\xff\x00\x00\x00\x00\x00\x00" 130 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 131 | b"\x00\x00\x00\x00\x00\x00" 132 | ).strip(b"\x00") 133 | offer_linux = ( 134 | b"\x02\x01\x06\x00\x2e\xf9" 135 | b"\x31\x7f\x00\x00\x00\x00\x00\x00\x00\x00\xc0\xa8\x38\x03\x00\x00" 136 | b"\x00\x00\x00\x00\x00\x00\x08\x00\x27\x92\x1f\xae\x00\x00\x00\x00" 137 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 138 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 139 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 140 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 141 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 142 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 143 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 144 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 145 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 146 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 147 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 148 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 149 | b"\x00\x00\x00\x00\x00\x00\x63\x82\x53\x63\x01\x04\xff\xff\xff\x00" 150 | b"\x03\x04\x0a\x97\x01\x01\x06\x04\x0a\x68\x01\x08\x0c\x09\x6d\x61" 151 | b"\x72\x69\x6f\x2e\x63\x6f\x6d\x0f\x0e\x73\x77\x65\x65\x74\x77\x61" 152 | b"\x74\x65\x72\x2e\x63\x6f\x6d\x33\x04\x00\x01\x51\x80\x35\x01\x02" 153 | b"\x36\x04\xc0\xa8\x38\x02\x3a\x04\x00\x00\x54\x60\x3b\x04\x00\x00" 154 | b"\xa8\xc0\xff" 155 | ).strip(b"\x00") 156 | request_linux = ( 157 | b"\x01\x01\x06\x00\x2e\xf9" 158 | b"\x31\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 159 | b"\x00\x00\x00\x00\x00\x00\x08\x00\x27\x92\x1f\xae\x00\x00\x00\x00" 160 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 161 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 162 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 163 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 164 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 165 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 166 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 167 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 168 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 169 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 170 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 171 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 172 | b"\x00\x00\x00\x00\x00\x00\x63\x82\x53\x63\x35\x01\x03\x36\x04\xc0" 173 | b"\xa8\x38\x02\x32\x04\xc0\xa8\x38\x03\x0c\x05\x6d\x61\x72\x69\x6f" 174 | b"\x37\x0d\x01\x1c\x02\x03\x0f\x06\x77\x0c\x2c\x2f\x1a\x79\x2a\xff" 175 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 176 | b"\x00\x00\x00\x00\x00\x00" 177 | ).strip(b"\x00") 178 | ack_linux = ( 179 | b"\x02\x01\x06\x00\x2e\xf9" 180 | b"\x31\x7f\x00\x00\x00\x00\x00\x00\x00\x00\xc0\xa8\x38\x03\x00\x00" 181 | b"\x00\x00\x00\x00\x00\x00\x08\x00\x27\x92\x1f\xae\x00\x00\x00\x00" 182 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 183 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 184 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 185 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 186 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 187 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 188 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 189 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 190 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 191 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 192 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 193 | b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 194 | b"\x00\x00\x00\x00\x00\x00\x63\x82\x53\x63\x01\x04\xff\xff\xff\x00" 195 | b"\x03\x04\x0a\x97\x01\x01\x06\x04\x0a\x68\x01\x08\x0c\x09\x6d\x61" 196 | b"\x72\x69\x6f\x2e\x63\x6f\x6d\x0f\x0e\x73\x77\x65\x65\x74\x77\x61" 197 | b"\x74\x65\x72\x2e\x63\x6f\x6d\x33\x04\x00\x01\x51\x80\x35\x01\x05" 198 | b"\x36\x04\xc0\xa8\x38\x02\x3a\x04\x00\x00\x54\x60\x3b\x04\x00\x00" 199 | b"\xa8\xc0\xff" 200 | ).strip(b"\x00") 201 | 202 | 203 | if __name__ == "__main__": 204 | unittest.main() 205 | --------------------------------------------------------------------------------