├── .gitignore ├── dhcp ├── client.py ├── lease.py ├── packet.py ├── server.py ├── udp.py └── utils.py ├── examples ├── client.py └── server.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # PyCharm 60 | .idea 61 | 62 | -------------------------------------------------------------------------------- /dhcp/client.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2016 iXsystems, Inc. 3 | # All rights reserved 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | # 26 | ##################################################################### 27 | 28 | import ipaddress 29 | import enum 30 | import random 31 | import threading 32 | import logging 33 | import netif 34 | from bsd import bpf 35 | from .packet import Packet, Option, PacketOption, PacketType, MessageType 36 | from .udp import UDPPacket 37 | from .lease import Lease 38 | from .utils import pack_mac 39 | 40 | 41 | BPF_PROGRAM = [ 42 | bpf.Statement(bpf.InstructionClass.LD | bpf.OperandSize.H | bpf.OperandMode.ABS, 36), 43 | bpf.Jump(bpf.InstructionClass.JMP | bpf.Opcode.JEQ | bpf.Source.K, 68, 0, 5), 44 | bpf.Statement(bpf.InstructionClass.LD | bpf.OperandSize.B | bpf.OperandMode.ABS, 23), 45 | bpf.Jump(bpf.InstructionClass.JMP | bpf.Opcode.JEQ | bpf.Source.K, 0x11, 0, 3), 46 | bpf.Statement(bpf.InstructionClass.LD | bpf.OperandSize.H | bpf.OperandMode.ABS, 12), 47 | bpf.Jump(bpf.InstructionClass.JMP | bpf.Opcode.JEQ | bpf.Source.K, 0x0800, 0, 1), 48 | bpf.Statement(bpf.InstructionClass.RET | bpf.Source.K, 0x0fffffff), 49 | bpf.Statement(bpf.InstructionClass.RET | bpf.Source.K, 0) 50 | ] 51 | 52 | 53 | class State(enum.Enum): 54 | INIT = 1 55 | SELECTING = 2 56 | REQUESTING = 3 57 | INIT_REBOOT = 4 58 | REBOOTING = 5 59 | BOUND = 6 60 | RENEWING = 7 61 | REBINDING = 8 62 | EXITING = 9 63 | 64 | 65 | class UnbindReason(enum.Enum): 66 | EXPIRE = 1 67 | REVOKE = 2 68 | RELEASE = 3 69 | 70 | 71 | class Client(object): 72 | def __init__(self, interface, hostname=''): 73 | self.logger = logging.getLogger(self.__class__.__name__) 74 | self.interface = interface 75 | self.bpf = None 76 | self.port = 68 77 | self.default_lifetime = 300 78 | self.listen_thread = None 79 | self.send_thread = None 80 | self.t1_timer = None 81 | self.t2_timer = None 82 | self.expire_timer = None 83 | self.client_ident = None 84 | self.hostname_source = hostname 85 | self.lease = None 86 | self.requested_address = None 87 | self.server_mac = None 88 | self.server_address = None 89 | self.server_name = None 90 | self.server_id = None 91 | self.cv = threading.Condition() 92 | self.state = State.INIT 93 | self.xid = None 94 | self.error = None 95 | self.on_bind = lambda old_lease, lease: None 96 | self.on_unbind = lambda lease, reason: None 97 | self.on_reject = lambda reason: None 98 | self.on_state_change = lambda state: None 99 | self.source_if = netif.get_interface(self.interface) 100 | self.hwaddr = str(self.source_if.link_address.address) 101 | 102 | @property 103 | def hostname(self): 104 | if callable(self.hostname_source): 105 | return self.hostname_source() 106 | 107 | return self.hostname_source 108 | 109 | def start(self): 110 | self.logger.info('Starting') 111 | self.bpf = bpf.BPF() 112 | self.bpf.open() 113 | self.bpf.immediate = True 114 | self.bpf.interface = self.interface 115 | self.bpf.apply_filter(BPF_PROGRAM) 116 | self.listen_thread = threading.Thread(target=self.__listen, daemon=True, name='py-dhcp listen thread') 117 | self.listen_thread.start() 118 | self.discover(False) 119 | 120 | def stop(self): 121 | self.logger.info('Stopping') 122 | with self.cv: 123 | self.__setstate(State.EXITING) 124 | self.bpf.close() 125 | 126 | def __getstate__(self): 127 | return { 128 | 'state': self.state.name, 129 | 'server_address': self.server_address, 130 | 'server_name': self.server_name, 131 | 'error': self.error, 132 | 'lease_starts_at': self.lease.started_at if self.lease else None, 133 | 'lease_ends_at': self.lease.ends_at if self.lease else None, 134 | } 135 | 136 | def __setstate(self, state): 137 | self.state = state 138 | self.on_state_change(state) 139 | self.cv.notify_all() 140 | 141 | def __t1(self): 142 | """ 143 | T1 aka renew timer 144 | """ 145 | self.logger.debug('Renewing IP address lease') 146 | with self.cv: 147 | self.__setstate(State.RENEWING) 148 | 149 | self.request(block=False, renew=True) 150 | 151 | def __t2(self): 152 | """ 153 | T2 aka rebind timer 154 | """ 155 | self.logger.debug('Renew timed out; rebinding') 156 | with self.cv: 157 | self.__setstate(State.REBINDING) 158 | 159 | self.discover(block=False, rebind=True) 160 | 161 | def __expire(self): 162 | self.logger.debug('Rebind timed out; expiring the lease and going back to INIT state') 163 | with self.cv: 164 | self.on_unbind(self.lease, UnbindReason.EXPIRE) 165 | self.lease = None 166 | self.server_mac = None 167 | self.server_address = None 168 | self.__setstate(State.INIT) 169 | 170 | self.discover(block=False) 171 | 172 | def __send(self, mac, src_ip, dst_ip, payload): 173 | udp = UDPPacket( 174 | src_mac=self.hwaddr, dst_mac=mac, src_address=ipaddress.ip_address(src_ip), 175 | dst_address=ipaddress.ip_address(dst_ip), src_port=68, dst_port=67, 176 | payload=payload 177 | ) 178 | 179 | self.bpf.write(udp.pack()) 180 | 181 | def __listen(self): 182 | for buf in self.bpf.read(): 183 | udp = UDPPacket() 184 | udp.unpack(buf) 185 | 186 | if udp.dst_port != 68 and udp.src_port != 67: 187 | continue 188 | 189 | if udp.dst_mac not in (self.hwaddr, 'ff:ff:ff:ff:ff:ff'): 190 | continue 191 | 192 | packet = Packet() 193 | packet.unpack(udp.payload) 194 | 195 | opt = packet.find_option(PacketOption.MESSAGE_TYPE) 196 | if not opt: 197 | self.logger.warning('Received DHCP packet without message type, discarding') 198 | continue 199 | 200 | if opt.value == MessageType.DHCPOFFER: 201 | if packet.xid != self.xid: 202 | continue 203 | 204 | if self.state not in (State.SELECTING, State.REBINDING): 205 | self.logger.debug('DHCPOFFER received and ignored') 206 | continue 207 | 208 | server_id = packet.find_option(PacketOption.SERVER_IDENT) 209 | 210 | self.logger.debug('DHCP server is {0}'.format(packet.siaddr)) 211 | with self.cv: 212 | self.server_mac = udp.src_mac 213 | self.server_address = udp.src_address 214 | self.requested_address = packet.yiaddr 215 | self.server_id = server_id.value if server_id else self.server_address 216 | self.__setstate(State.REQUESTING if self.state == State.SELECTING else State.REBINDING) 217 | self.request(False) 218 | continue 219 | 220 | if opt.value == MessageType.DHCPACK: 221 | if packet.xid != self.xid: 222 | continue 223 | 224 | if self.state not in (State.REQUESTING, State.RENEWING, State.REBINDING): 225 | self.logger.debug('DHCPACK received and ignored') 226 | continue 227 | 228 | lease = Lease() 229 | lease.client_ip = packet.yiaddr 230 | lease.client_mac = self.hwaddr 231 | 232 | for opt in packet.options: 233 | if opt.id == PacketOption.LEASE_TIME: 234 | lease.lifetime = opt.value 235 | 236 | if opt.id == PacketOption.SUBNET_MASK: 237 | lease.client_mask = opt.value 238 | 239 | if opt.id == PacketOption.ROUTER: 240 | lease.router = opt.value 241 | 242 | if opt.id == PacketOption.DOMAIN_NAME: 243 | lease.domain_name = opt.value 244 | 245 | if opt.id == PacketOption.DOMAIN_NAME_SERVER: 246 | lease.dns_addresses = opt.value 247 | 248 | if opt.id == PacketOption.STATIC_ROUTES: 249 | lease.static_routes = opt.value 250 | 251 | if opt.id == PacketOption.HOST_NAME: 252 | lease.host_name = opt.value 253 | 254 | self.logger.debug('Lease time is {0}'.format(lease.lifetime)) 255 | 256 | # (re)start T1, T2 and expire timers 257 | if self.t1_timer: 258 | self.t1_timer.cancel() 259 | self.t1_timer = threading.Timer(lease.lifetime * 0.500, self.__t1) 260 | self.t1_timer.start() 261 | 262 | if self.t2_timer: 263 | self.t2_timer.cancel() 264 | self.t2_timer = threading.Timer(lease.lifetime * 0.875, self.__t2) 265 | self.t2_timer.start() 266 | 267 | if self.expire_timer: 268 | self.expire_timer.cancel() 269 | self.expire_timer = threading.Timer(lease.lifetime, self.__expire) 270 | self.expire_timer.start() 271 | 272 | self.on_bind(self.lease, lease) 273 | self.logger.debug('Bound to {0}'.format(lease.client_ip)) 274 | 275 | with self.cv: 276 | self.lease = lease 277 | self.server_name = packet.sname 278 | self.error = None 279 | self.__setstate(State.BOUND) 280 | 281 | if opt.value == MessageType.DHCPNAK: 282 | if self.state not in (State.REQUESTING, State.RENEWING, State.REBINDING): 283 | self.logger.debug('DHCPNAK received and ignored') 284 | continue 285 | 286 | self.logger.warning('DHCP server declined our request') 287 | with self.cv: 288 | error = packet.find_option(PacketOption.ERROR_MESSAGE) 289 | if error: 290 | self.logger.warning('DHCP error message: {0}'.format(error.value)) 291 | 292 | msg = error.value if error else 'DHCP request declined' 293 | self.on_reject(msg) 294 | self.lease = None 295 | self.server_mac = None 296 | self.server_address = None 297 | self.error = msg 298 | self.__setstate(State.INIT) 299 | 300 | def __discover(self, rebind=False): 301 | with self.cv: 302 | self.__setstate(State.REBINDING if rebind else State.SELECTING) 303 | 304 | self.xid = random.randint(0, 2**32 - 1) 305 | packet = Packet() 306 | packet.op = PacketType.BOOTREQUEST 307 | packet.xid = self.xid 308 | packet.chaddr = pack_mac(self.hwaddr) 309 | packet.options = [ 310 | Option(PacketOption.MESSAGE_TYPE, MessageType.DHCPDISCOVER), 311 | Option(PacketOption.CLIENT_IDENT, pack_mac(self.hwaddr)), 312 | Option(PacketOption.HOST_NAME, self.hostname) 313 | ] 314 | 315 | if self.requested_address: 316 | packet.options.append(Option(PacketOption.REQUESTED_IP, self.requested_address)) 317 | 318 | retries = 0 319 | 320 | while True: 321 | self.logger.debug('Sending DHCPDISCOVER') 322 | try: 323 | self.__send( 324 | 'FF:FF:FF:FF:FF:FF', 325 | '0.0.0.0', 326 | '255.255.255.255', 327 | packet.pack() 328 | ) 329 | except OSError as err: 330 | self.logger.debug('Cannot send message: {0}'.format(str(err))) 331 | 332 | with self.cv: 333 | if self.cv.wait_for( 334 | lambda: self.state in (State.REQUESTING, State.EXITING), 335 | 5 if retries < 10 else 30 336 | ): 337 | return 338 | 339 | retries += 1 340 | 341 | def __request(self, renew=False): 342 | if renew: 343 | with self.cv: 344 | self.__setstate(State.RENEWING) 345 | 346 | packet = Packet() 347 | packet.op = PacketType.BOOTREQUEST 348 | packet.xid = self.xid 349 | packet.chaddr = pack_mac(self.hwaddr) 350 | packet.options = [ 351 | Option(PacketOption.MESSAGE_TYPE, MessageType.DHCPREQUEST), 352 | Option(PacketOption.HOST_NAME, self.hostname), 353 | Option(PacketOption.CLIENT_IDENT, pack_mac(self.hwaddr)), 354 | Option(PacketOption.SERVER_IDENT, self.server_id), 355 | Option(PacketOption.PARAMETER_REQUEST_LIST, [ 356 | PacketOption.SUBNET_MASK, 357 | PacketOption.ROUTER, 358 | PacketOption.DOMAIN_NAME, 359 | PacketOption.DOMAIN_NAME_SERVER, 360 | PacketOption.LEASE_TIME 361 | ]) 362 | ] 363 | 364 | if self.requested_address: 365 | packet.options.append(Option(PacketOption.REQUESTED_IP, self.requested_address)) 366 | 367 | if renew: 368 | packet.ciaddr = int(self.lease.client_ip) 369 | 370 | retries = 0 371 | 372 | while True: 373 | self.logger.debug('Sending DHCPREQUEST') 374 | try: 375 | self.__send( 376 | 'FF:FF:FF:FF:FF:FF', 377 | str(self.lease.client_ip) if renew else '0.0.0.0', 378 | '255.255.255.255', 379 | packet.pack() 380 | ) 381 | except OSError as err: 382 | self.logger.debug('Cannot send message: {0}'.format(str(err))) 383 | 384 | with self.cv: 385 | if self.cv.wait_for( 386 | lambda: self.state not in (State.REQUESTING, State.RENEWING), 387 | 5 if retries < 10 else 30 388 | ): 389 | return 390 | 391 | retries += 1 392 | 393 | def discover(self, block=True, timeout=None, rebind=False): 394 | self.send_thread = threading.Thread( 395 | target=self.__discover, 396 | args=(rebind,), 397 | daemon=True, 398 | name='py-dhcp discover thread' 399 | ) 400 | 401 | self.send_thread.start() 402 | 403 | if block: 404 | with self.cv: 405 | self.cv.wait_for(lambda: self.state == State.REQUESTING, timeout) 406 | return self.lease 407 | 408 | def request(self, block=True, timeout=None, renew=False): 409 | if renew and not self.lease: 410 | raise RuntimeError('Cannot renew without a lease') 411 | 412 | self.send_thread = threading.Thread( 413 | target=self.__request, 414 | args=(renew,), 415 | daemon=True, 416 | name='py-dhcp request thread' 417 | ) 418 | 419 | self.send_thread.start() 420 | 421 | if block: 422 | with self.cv: 423 | self.cv.wait_for(lambda: self.state == State.BOUND, timeout) 424 | return self.lease 425 | 426 | def wait_for_bind(self, timeout=None): 427 | with self.cv: 428 | self.cv.wait_for(lambda: self.state == State.BOUND, timeout) 429 | return self.lease 430 | 431 | def release(self): 432 | if self.state != State.BOUND: 433 | return 434 | 435 | packet = Packet() 436 | packet.op = PacketType.BOOTREQUEST 437 | packet.xid = random.randint(0, 2**32 - 1) 438 | packet.chaddr = pack_mac(self.hwaddr) 439 | packet.siaddr = int(self.server_address) 440 | packet.options = [ 441 | Option(PacketOption.MESSAGE_TYPE, MessageType.DHCPRELEASE), 442 | Option(PacketOption.SERVER_IDENT, self.server_address), 443 | Option(PacketOption.HOST_NAME, self.hostname) 444 | ] 445 | 446 | with self.cv: 447 | self.on_unbind(self.lease, UnbindReason.RELEASE) 448 | self.lease = None 449 | self.server_address = None 450 | self.server_mac = None 451 | self.__setstate(State.INIT) 452 | 453 | def cancel(self): 454 | with self.cv: 455 | if self.t1_timer: 456 | self.t1_timer.cancel() 457 | 458 | if self.t2_timer: 459 | self.t2_timer.cancel() 460 | 461 | if self.expire_timer: 462 | self.expire_timer.cancel() 463 | 464 | if self.state in (State.SELECTING, State.REBINDING): 465 | pass -------------------------------------------------------------------------------- /dhcp/lease.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 iXsystems, Inc. 3 | # All rights reserved 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | # 26 | ##################################################################### 27 | 28 | import ipaddress 29 | from datetime import datetime, timedelta 30 | from .packet import Option, PacketOption 31 | 32 | 33 | class Lease(object): 34 | def __init__(self): 35 | self.started_at = datetime.utcnow() 36 | self.client_mac = None 37 | self.client_ip = None 38 | self.client_mask = None 39 | self.lifetime = 86400 40 | self.router = None 41 | self.host_name = None 42 | self.domain_name = None 43 | self.dns_addresses = [] 44 | self.dns_search = [] 45 | self.static_routes = [] 46 | self.active = False 47 | 48 | def __getstate__(self): 49 | return { 50 | 'client_mac': self.client_mac, 51 | 'client_ip': str(self.client_ip), 52 | 'client_mask': str(self.client_mask), 53 | 'lifetime': self.lifetime, 54 | 'router': str(self.router) if self.router else None, 55 | 'dns_addresses': [str(i) for i in self.dns_addresses], 56 | 'active': self.active 57 | } 58 | 59 | @property 60 | def client_interface(self): 61 | return ipaddress.ip_interface('{0}/{1}'.format(self.client_ip, self.client_mask)) 62 | 63 | @property 64 | def ends_at(self): 65 | return self.started_at + timedelta(seconds=self.lifetime) 66 | 67 | @property 68 | def options(self): 69 | yield Option(PacketOption.LEASE_TIME, self.lifetime) 70 | yield Option(PacketOption.SUBNET_MASK, self.client_mask) 71 | 72 | if self.router: 73 | yield Option(PacketOption.ROUTER, self.router) 74 | 75 | if self.dns_addresses: 76 | yield Option(PacketOption.DOMAIN_NAME_SERVER, self.dns_addresses) 77 | 78 | if self.static_routes: 79 | yield Option(PacketOption.STATIC_ROUTES, self.static_routes) 80 | -------------------------------------------------------------------------------- /dhcp/packet.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 iXsystems, Inc. 3 | # All rights reserved 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | # 26 | ##################################################################### 27 | 28 | import enum 29 | import ipaddress 30 | import socket 31 | import struct 32 | import logging 33 | from .utils import first_or_default 34 | 35 | 36 | MAGIC_COOKIE = b'\x63\x82\x53\x63' 37 | logger = logging.getLogger(__name__) 38 | 39 | 40 | class PacketType(enum.IntEnum): 41 | BOOTREQUEST = 1 42 | BOOTREPLY = 2 43 | 44 | 45 | class PacketFlags(enum.IntEnum): 46 | BROADCAST = 1 << 15 47 | 48 | 49 | class HardwareAddressType(enum.IntEnum): 50 | ETHERNET = 1 51 | IEEE802 = 6 52 | 53 | 54 | class MessageType(enum.IntEnum): 55 | DHCPDISCOVER = 1 56 | DHCPOFFER = 2 57 | DHCPREQUEST = 3 58 | DHCPDECLINE = 4 59 | DHCPACK = 5 60 | DHCPNAK = 6 61 | DHCPRELEASE = 7 62 | DHCPINFORM = 8 63 | 64 | 65 | class PacketOption(enum.IntEnum): 66 | SUBNET_MASK = 1 67 | ROUTER = 3 68 | TIME_SERVER = 4 69 | NAME_SERVER = 5 70 | DOMAIN_NAME_SERVER = 6 71 | LOG_SERVER = 7 72 | QUOTE_SERVER = 8 73 | LPR_SERVER = 9 74 | IMPRESS_SERVER = 10 75 | RESOURCE_LOCATION_SERVER = 11 76 | HOST_NAME = 12 77 | DOMAIN_NAME = 15 78 | ROOT_PATH = 17 79 | EXTENSIONS_PATH = 18 80 | BROADCAST = 28 81 | REQUESTED_IP = 50 82 | LEASE_TIME = 51 83 | MESSAGE_TYPE = 53 84 | SERVER_IDENT = 54 85 | PARAMETER_REQUEST_LIST = 55 86 | ERROR_MESSAGE = 56 87 | MAX_MESSAGE_SIZE = 57 88 | CLASS_IDENT = 60 89 | CLIENT_IDENT = 61 90 | TFTP_SERVER = 66 91 | STATIC_ROUTES = 121 92 | WPAD_URL = 252 93 | 94 | 95 | class Packet(object): 96 | def __init__(self): 97 | self.op = None 98 | self.htype = HardwareAddressType.ETHERNET 99 | self.hlen = 6 100 | self.hops = 0 101 | self.secs = 0 102 | self.flags = PacketFlags.BROADCAST 103 | self.xid = None 104 | self.ciaddr = ipaddress.ip_address('0.0.0.0') 105 | self.yiaddr = ipaddress.ip_address('0.0.0.0') 106 | self.siaddr = ipaddress.ip_address('0.0.0.0') 107 | self.giaddr = ipaddress.ip_address('0.0.0.0') 108 | self.chaddr = b'\x00\x00\x00\x00\x00\x00' 109 | self.cookie = MAGIC_COOKIE 110 | self.sname = '' 111 | self.options = [] 112 | 113 | def clone_from(self, other): 114 | self.htype = other.htype 115 | self.hlen = other.hlen 116 | self.hops = other.hops 117 | self.xid = other.xid 118 | self.secs = other.secs 119 | self.flags = other.flags 120 | self.chaddr = other.chaddr 121 | 122 | def unpack(self, payload): 123 | self.op, self.htype, self.hlen, self.hops = struct.unpack_from('BBBB', payload, 0) 124 | self.xid = struct.unpack_from('!I', payload, 4)[0] 125 | self.secs, self.flags = struct.unpack_from('HH', payload, 8) 126 | self.ciaddr = ipaddress.ip_address(struct.unpack_from('!I', payload, 12)[0]) 127 | self.yiaddr = ipaddress.ip_address(struct.unpack_from('!I', payload, 16)[0]) 128 | self.siaddr = ipaddress.ip_address(struct.unpack_from('!I', payload, 20)[0]) 129 | self.giaddr = ipaddress.ip_address(struct.unpack_from('!I', payload, 24)[0]) 130 | self.chaddr = struct.unpack_from('12s', payload, 28)[0][:6] 131 | self.sname = struct.unpack_from('64s', payload, 44)[0].decode('ascii') 132 | 133 | self.op = PacketType(self.op) 134 | self.cookie = struct.unpack_from('4s', payload, 236)[0] 135 | 136 | offset = 240 137 | while offset < len(payload): 138 | code = struct.unpack_from('B', payload, offset)[0] 139 | offset += 1 140 | 141 | if code == 0: 142 | continue 143 | 144 | if code == 255: 145 | break 146 | 147 | length = struct.unpack_from('B', payload, offset)[0] 148 | offset += 1 149 | value = struct.unpack_from('{0}s'.format(length), payload, offset)[0] 150 | offset += length 151 | 152 | try: 153 | optid = PacketOption(code) 154 | self.options.append(Option(optid, packed=value)) 155 | except ValueError: 156 | logger.debug('Unknown DHCP option {0}, skipped'.format(code)) 157 | continue 158 | 159 | def pack(self): 160 | result = bytearray(bytes(240)) 161 | struct.pack_into('BBBB', result, 0, int(self.op), self.htype, self.hlen, self.hops) 162 | struct.pack_into('!I', result, 4, self.xid) 163 | struct.pack_into('!HH', result, 8, self.secs, self.flags) 164 | struct.pack_into('!II', result, 12, int(self.ciaddr), int(self.yiaddr)) 165 | struct.pack_into('!II', result, 20, int(self.siaddr), int(self.giaddr)) 166 | struct.pack_into('12s', result, 28, self.chaddr) 167 | struct.pack_into('64s', result, 40, self.sname.encode('ascii')) 168 | struct.pack_into('4s', result, 236, MAGIC_COOKIE) 169 | 170 | for i in self.options: 171 | packed = i.pack() 172 | result += struct.pack('BB{0}s'.format(len(packed)), int(i.id), len(packed), packed) 173 | 174 | result += b'\xff' 175 | 176 | if len(result) < 300: 177 | result += b'\x00' * (300 - len(result)) 178 | 179 | return result 180 | 181 | def dump(self, f): 182 | print("Op: {0}".format(self.op.name), file=f) 183 | print("Client address: {0}".format(self.ciaddr), file=f) 184 | print("Your address: {0}".format(self.yiaddr), file=f) 185 | print("Server address: {0}".format(self.siaddr), file=f) 186 | print("Gateway address: {0}".format(self.giaddr), file=f) 187 | print("Client hardware address: {0}".format(':'.join('%02x' % b for b in self.chaddr[:6])), file=f) 188 | print("XID: {0}".format(self.xid), file=f) 189 | print("Sname: {0}".format(self.sname), file=f) 190 | print("Magic cookie: {0}".format(self.cookie), file=f) 191 | print("Options:", file=f) 192 | for i in self.options: 193 | print("\t{0} = {1}".format(i.id.name, i.value), file=f) 194 | 195 | def find_option(self, opt): 196 | return first_or_default(lambda x: x.id == opt, self.options) 197 | 198 | 199 | class Option(object): 200 | def __init__(self, id, value=None, packed=None): 201 | self.id = id 202 | self.value = None 203 | 204 | if value is not None: 205 | self.value = value 206 | return 207 | 208 | if packed: 209 | self.unpack(packed) 210 | 211 | def __pack_route(self, subnet, gateway): 212 | result = struct.pack('B', subnet.prefixlen) 213 | packed = subnet.network_address.packed 214 | for i in range(0, 4): 215 | if packed[i] != b'\x00': 216 | result += packed[i:i+1] 217 | 218 | result += gateway.packed 219 | return result 220 | 221 | def unpack(self, value): 222 | if self.id in ( 223 | PacketOption.ROUTER, PacketOption.REQUESTED_IP, PacketOption.SUBNET_MASK, 224 | PacketOption.SERVER_IDENT, PacketOption.BROADCAST 225 | ): 226 | self.value = ipaddress.ip_address(value) 227 | return 228 | 229 | if self.id in ( 230 | PacketOption.HOST_NAME, PacketOption.DOMAIN_NAME, PacketOption.TFTP_SERVER, 231 | PacketOption.WPAD_URL 232 | ): 233 | self.value = value.decode('ascii') 234 | return 235 | 236 | if self.id == PacketOption.ERROR_MESSAGE: 237 | self.value = value.decode('utf-8') 238 | return 239 | 240 | if self.id in ( 241 | PacketOption.DOMAIN_NAME_SERVER, PacketOption.LOG_SERVER, PacketOption.TIME_SERVER, 242 | PacketOption.QUOTE_SERVER, PacketOption.LPR_SERVER, PacketOption.IMPRESS_SERVER, 243 | PacketOption.RESOURCE_LOCATION_SERVER 244 | ): 245 | self.value = [] 246 | for i, in struct.iter_unpack('I', value): 247 | self.value.append(ipaddress.ip_address(socket.ntohl(i))) 248 | 249 | return 250 | 251 | if self.id == PacketOption.MESSAGE_TYPE: 252 | self.value = MessageType(value[0]) 253 | return 254 | 255 | if self.id == PacketOption.LEASE_TIME: 256 | self.value = struct.unpack('!I', value)[0] 257 | return 258 | 259 | if self.id == PacketOption.PARAMETER_REQUEST_LIST: 260 | self.value = [] 261 | for i, in struct.iter_unpack('B', value): 262 | try: 263 | self.value.append(PacketOption(i)) 264 | except ValueError: 265 | continue 266 | 267 | return 268 | 269 | self.value = value 270 | 271 | def pack(self): 272 | if self.id in ( 273 | PacketOption.ROUTER, PacketOption.REQUESTED_IP, PacketOption.SUBNET_MASK, 274 | PacketOption.SERVER_IDENT, PacketOption.BROADCAST 275 | ): 276 | return self.value.packed 277 | 278 | if self.id in ( 279 | PacketOption.DOMAIN_NAME_SERVER, PacketOption.LOG_SERVER, PacketOption.TIME_SERVER, 280 | PacketOption.QUOTE_SERVER, PacketOption.LPR_SERVER, PacketOption.IMPRESS_SERVER, 281 | PacketOption.RESOURCE_LOCATION_SERVER 282 | ): 283 | return b''.join(i.packed for i in self.value) 284 | 285 | if self.id in ( 286 | PacketOption.HOST_NAME, PacketOption.DOMAIN_NAME, PacketOption.TFTP_SERVER, 287 | PacketOption.WPAD_URL 288 | ): 289 | return self.value.encode('ascii') 290 | 291 | if self.id == PacketOption.ERROR_MESSAGE: 292 | return self.value.encode('utf-8') 293 | 294 | if self.id == PacketOption.CLIENT_IDENT: 295 | return struct.pack('!B6s', 1, self.value) 296 | 297 | if self.id == PacketOption.MESSAGE_TYPE: 298 | return bytes([int(self.value)]) 299 | 300 | if self.id == PacketOption.LEASE_TIME: 301 | return struct.pack('!I', self.value) 302 | 303 | if self.id == PacketOption.STATIC_ROUTES: 304 | return b''.join(self.__pack_route(s, g) for s, g in self.value) 305 | 306 | if self.id == PacketOption.PARAMETER_REQUEST_LIST: 307 | return b''.join(struct.pack('B', int(i)) for i in self.value) 308 | -------------------------------------------------------------------------------- /dhcp/server.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 iXsystems, Inc. 3 | # All rights reserved 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | # 26 | ##################################################################### 27 | 28 | import ipaddress 29 | import logging 30 | import netif 31 | from bsd import bpf 32 | from .udp import UDPPacket 33 | from .utils import format_mac 34 | from .packet import Packet, PacketType, PacketOption, Option, MessageType 35 | 36 | 37 | BPF_PROGRAM = [ 38 | bpf.Statement(bpf.InstructionClass.LD | bpf.OperandSize.H | bpf.OperandMode.ABS, 36), 39 | bpf.Jump(bpf.InstructionClass.JMP | bpf.Opcode.JEQ | bpf.Source.K, 67, 0, 5), 40 | bpf.Statement(bpf.InstructionClass.LD | bpf.OperandSize.B | bpf.OperandMode.ABS, 23), 41 | bpf.Jump(bpf.InstructionClass.JMP | bpf.Opcode.JEQ | bpf.Source.K, 0x11, 0, 3), 42 | bpf.Statement(bpf.InstructionClass.LD | bpf.OperandSize.H | bpf.OperandMode.ABS, 12), 43 | bpf.Jump(bpf.InstructionClass.JMP | bpf.Opcode.JEQ | bpf.Source.K, 0x0800, 0, 1), 44 | bpf.Statement(bpf.InstructionClass.RET | bpf.Source.K, 0x0fffffff), 45 | bpf.Statement(bpf.InstructionClass.RET | bpf.Source.K, 0) 46 | ] 47 | 48 | 49 | class Server(object): 50 | def __init__(self): 51 | self.bpf = None 52 | self.address = None 53 | self.server_name = None 54 | self.port = 67 55 | self.leases = [] 56 | self.requests = {} 57 | self.logger = logging.getLogger(self.__class__.__name__) 58 | self.on_packet = None 59 | self.on_request = None 60 | self.source_if = None 61 | self.hwaddr = None 62 | self.handlers = { 63 | MessageType.DHCPDISCOVER: self.handle_discover, 64 | MessageType.DHCPREQUEST: self.handle_request, 65 | MessageType.DHCPRELEASE: self.handle_release 66 | } 67 | 68 | def start(self, interface, source_address): 69 | if not self.server_name: 70 | raise RuntimeError('Please set server_name') 71 | 72 | if not self.on_request: 73 | raise RuntimeError('Please set on_request') 74 | 75 | self.address = source_address 76 | self.source_if = netif.get_interface(interface) 77 | self.hwaddr = str(self.source_if.link_address.address) 78 | self.bpf = bpf.BPF() 79 | self.bpf.open() 80 | self.bpf.immediate = True 81 | self.bpf.interface = interface 82 | self.bpf.apply_filter(BPF_PROGRAM) 83 | 84 | def serve(self): 85 | for buf in self.bpf.read(): 86 | udp = UDPPacket() 87 | udp.unpack(buf) 88 | 89 | if udp.dst_port != 67 and udp.src_port != 68: 90 | continue 91 | 92 | packet = Packet() 93 | packet.unpack(udp.payload) 94 | 95 | if self.on_packet: 96 | self.on_packet(packet) 97 | 98 | message_type = packet.find_option(PacketOption.MESSAGE_TYPE) 99 | if not message_type: 100 | self.logger.debug('Malformed packet: no MESSAGE_TYPE option') 101 | continue 102 | 103 | handler = self.handlers.get(message_type.value) 104 | if handler: 105 | handler(packet, None) 106 | 107 | def send_packet(self, packet, mac, dst_ip): 108 | udp = UDPPacket( 109 | src_mac=self.hwaddr, dst_mac=mac, src_address=self.address, 110 | dst_address=ipaddress.ip_address(dst_ip), src_port=67, dst_port=68, 111 | payload=packet.pack() 112 | ) 113 | 114 | self.bpf.write(udp.pack()) 115 | 116 | def handle_discover(self, packet, sender): 117 | offer = Packet() 118 | offer.clone_from(packet) 119 | offer.op = PacketType.BOOTREPLY 120 | offer.sname = self.server_name 121 | offer.options.append(Option(PacketOption.MESSAGE_TYPE, MessageType.DHCPOFFER)) 122 | 123 | hostname = packet.find_option(PacketOption.HOST_NAME) 124 | lease = self.on_request(format_mac(packet.chaddr), hostname.value if hostname else None) 125 | if not lease: 126 | # ignore 127 | return 128 | 129 | self.requests[packet.xid] = lease 130 | offer.yiaddr = lease.client_ip 131 | offer.siaddr = ipaddress.ip_address(self.address) 132 | offer.options += lease.options 133 | self.send_packet(offer, 'ff:ff:ff:ff:ff:ff', '255.255.255.255') 134 | 135 | def handle_request(self, packet, sender): 136 | ack = Packet() 137 | ack.clone_from(packet) 138 | ack.op = PacketType.BOOTREPLY 139 | ack.htype = packet.htype 140 | ack.sname = self.server_name 141 | ack.options.append(Option(PacketOption.MESSAGE_TYPE, MessageType.DHCPACK)) 142 | 143 | hostname = packet.find_option(PacketOption.HOST_NAME) 144 | lease = self.requests.pop(packet.xid, None) 145 | 146 | if not lease: 147 | lease = self.on_request(format_mac(packet.chaddr), hostname.value if hostname else None) 148 | 149 | if not lease: 150 | # send NAK 151 | return 152 | 153 | self.leases.append(lease) 154 | ack.yiaddr = lease.client_ip 155 | ack.siaddr = ipaddress.ip_address(self.address) 156 | ack.options += lease.options 157 | self.send_packet(ack, 'ff:ff:ff:ff:ff:ff', '255.255.255.255') 158 | 159 | def handle_release(self, packet): 160 | pass 161 | -------------------------------------------------------------------------------- /dhcp/udp.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2016 iXsystems, Inc. 3 | # All rights reserved 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | # 26 | ##################################################################### 27 | 28 | import ipaddress 29 | import enum 30 | import struct 31 | import random 32 | import socket 33 | from .utils import format_mac, pack_mac 34 | 35 | 36 | ETHERNET_HEADER_FMT = '!6s6sH' 37 | ETHERNET_HEADER_LENGTH = 14 38 | IPV4_HEADER_FMT = '!BBHHHBBH4s4s' 39 | IPV4_HEADER_LENGTH = 20 40 | UDP_HEADER_FMT = '!HHHH' 41 | UDP_HEADER_LENGTH = 8 42 | 43 | 44 | def checksum(data): 45 | """ Compute the Internet Checksum of the supplied data. The 46 | checksum is initialized to zero. Place the return value in 47 | the checksum field of a packet. When the packet is received, 48 | check the checksum by passing in the packet. If the result is 49 | zero, then the checksum has not detected an error. 50 | """ 51 | 52 | sum = 0 53 | # make 16 bit words out of every two adjacent 8 bit words in the packet 54 | # and add them up 55 | for i in range(0, len(data), 2): 56 | if i + 1 >= len(data): 57 | sum += data[i] & 0xFF 58 | else: 59 | w = ((data[i] << 8) & 0xFF00) + (data[i + 1] & 0xFF) 60 | sum += w 61 | 62 | # take only 16 bits out of the 32 bit sum and add up the carries 63 | while (sum >> 16) > 0: 64 | sum = (sum & 0xFFFF) + (sum >> 16) 65 | 66 | # one's complement the result 67 | sum = ~sum 68 | return sum & 0xFFFF 69 | 70 | 71 | class EtherType(enum.IntEnum): 72 | IPv4 = 0x0800 73 | ARP = 0x0806 74 | 75 | 76 | class UDPPacket(object): 77 | def __init__(self, **kwargs): 78 | # Ethernet header 79 | self.src_mac = None 80 | self.dst_mac = None 81 | self.ethertype = EtherType.IPv4 82 | 83 | # IP header 84 | self.version = 4 85 | self.header_length = 5 86 | self.tos = 0 87 | self.length = None 88 | self.identification = random.randint(0, 2**16 - 1) 89 | self.flags = 0 90 | self.ttl = 20 91 | self.protocol = socket.IPPROTO_UDP 92 | self.header_checksum = 0 93 | self.src_address = None 94 | self.dst_address = None 95 | 96 | # UDP header 97 | self.src_port = None 98 | self.dst_port = None 99 | self.udp_length = None 100 | self.udp_checksum = 0 101 | 102 | # Payload 103 | self.payload = None 104 | 105 | for k, v in kwargs.items(): 106 | setattr(self, k, v) 107 | 108 | def unpack(self, data): 109 | self.dst_mac, self.src_mac, self.ethertype = \ 110 | struct.unpack_from(ETHERNET_HEADER_FMT, data, 0) 111 | 112 | ihl, self.tos, self.length, self.identification, \ 113 | self.flags, self.ttl, self.protocol, self.header_checksum, \ 114 | self.src_address, self.dst_address = struct.unpack_from(IPV4_HEADER_FMT, data, 14) 115 | 116 | self.src_port, self.dst_port, self.udp_length, self.udp_checksum = \ 117 | struct.unpack_from(UDP_HEADER_FMT, data, 34) 118 | 119 | self.version = ihl >> 4 & 0xf 120 | self.header_length = ihl & 0xf 121 | self.src_mac = format_mac(self.src_mac) 122 | self.dst_mac = format_mac(self.dst_mac) 123 | self.src_address = ipaddress.ip_address(self.src_address) 124 | self.dst_address = ipaddress.ip_address(self.dst_address) 125 | self.payload = data[42:] 126 | 127 | def pack(self): 128 | ip_hdr = bytearray(20) 129 | buffer = bytearray( 130 | ETHERNET_HEADER_LENGTH + 131 | IPV4_HEADER_LENGTH + 132 | UDP_HEADER_LENGTH + 133 | len(self.payload) 134 | ) 135 | 136 | ihl = self.header_length | (self.version << 4) 137 | self.length = len(self.payload) + IPV4_HEADER_LENGTH + UDP_HEADER_LENGTH 138 | self.udp_length = len(self.payload) + UDP_HEADER_LENGTH 139 | 140 | struct.pack_into( 141 | ETHERNET_HEADER_FMT, buffer, 0, 142 | pack_mac(self.dst_mac), pack_mac(self.src_mac), self.ethertype 143 | ) 144 | 145 | struct.pack_into( 146 | IPV4_HEADER_FMT, ip_hdr, 0, ihl, 147 | self.tos, self.length, self.identification, self.flags, self.ttl, 148 | self.protocol, self.header_checksum, self.src_address.packed, self.dst_address.packed 149 | ) 150 | 151 | self.header_checksum = checksum(ip_hdr) 152 | 153 | struct.pack_into( 154 | IPV4_HEADER_FMT, buffer, 14, ihl, 155 | self.tos, self.length, self.identification, self.flags, self.ttl, 156 | self.protocol, self.header_checksum, self.src_address.packed, self.dst_address.packed 157 | ) 158 | 159 | struct.pack_into( 160 | UDP_HEADER_FMT, buffer, 34, 161 | self.src_port, self.dst_port, self.udp_length, self.udp_checksum 162 | ) 163 | 164 | buffer[42:] = self.payload 165 | return buffer 166 | 167 | def dump(self, f): 168 | print(self.__dict__, file=f) 169 | -------------------------------------------------------------------------------- /dhcp/utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2016 iXsystems, Inc. 3 | # All rights reserved 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | # 26 | ##################################################################### 27 | 28 | import binascii 29 | 30 | 31 | def format_mac(mac): 32 | return ':'.join('{0:02x}'.format(s) for s in mac) 33 | 34 | 35 | def pack_mac(macstr): 36 | return binascii.unhexlify(macstr.replace(':', '')) 37 | 38 | 39 | def first_or_default(f, iterable, default=None): 40 | i = list(filter(f, iterable)) 41 | if i: 42 | return i[0] 43 | 44 | return default 45 | -------------------------------------------------------------------------------- /examples/client.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2016 iXsystems, Inc. 3 | # All rights reserved 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | # 26 | ##################################################################### 27 | 28 | import sys 29 | import time 30 | import logging 31 | from dhcp.client import Client 32 | 33 | 34 | def main(): 35 | s = Client(sys.argv[1], 'test') 36 | s.start() 37 | print(s.wait_for_bind().__getstate__()) 38 | while True: 39 | time.sleep(100) 40 | 41 | if __name__ == '__main__': 42 | logging.basicConfig(level=logging.DEBUG) 43 | main() 44 | -------------------------------------------------------------------------------- /examples/server.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 iXsystems, Inc. 3 | # All rights reserved 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | # 26 | ##################################################################### 27 | 28 | import ipaddress 29 | import sys 30 | from dhcp.server import Server 31 | from dhcp.lease import Lease 32 | 33 | 34 | def print_packet(packet): 35 | packet.dump(sys.stdout) 36 | 37 | 38 | def request(mac, hostname): 39 | print('request from {0}, hostname {1}'.format(mac, hostname)) 40 | ret = Lease() 41 | ret.client_ip = ipaddress.ip_address('192.168.1.1') 42 | ret.lifetime = 300 43 | return ret 44 | 45 | 46 | def main(): 47 | s = Server() 48 | s.server_name = 'example server' 49 | s.on_packet = print_packet 50 | s.on_request = request 51 | s.start(sys.argv[1]) 52 | s.serve() 53 | 54 | 55 | if __name__ == '__main__': 56 | main() 57 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 iXsystems, Inc. 3 | # All rights reserved 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted providing that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 18 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 22 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 23 | # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | # POSSIBILITY OF SUCH DAMAGE. 25 | # 26 | ##################################################################### 27 | 28 | 29 | from distutils.core import setup 30 | 31 | 32 | setup( 33 | name='dhcp', 34 | version='0.1', 35 | packages=['dhcp'] 36 | ) 37 | --------------------------------------------------------------------------------