Remove old DHCP integration preferring dnsmasq
This commit is contained in:
		| @@ -1,874 +0,0 @@ | ||||
| #!/usr/bin/python3 | ||||
|  | ||||
| # DHCPServer.py - PVC router DHCP server with Zookeeper database | ||||
| # Part of the Parallel Virtual Cluster (PVC) system | ||||
| # | ||||
| #    Copyright (C) 2018  Joshua M. Boniface <joshua@boniface.me> | ||||
| # | ||||
| #    This program is free software: you can redistribute it and/or modify | ||||
| #    it under the terms of the GNU General Public License as published by | ||||
| #    the Free Software Foundation, either version 3 of the License, or | ||||
| #    (at your option) any later version. | ||||
| # | ||||
| #    This program is distributed in the hope that it will be useful, | ||||
| #    but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| #    GNU General Public License for more details. | ||||
| # | ||||
| #    You should have received a copy of the GNU General Public License | ||||
| #    along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| # | ||||
| ############################################################################### | ||||
| #  | ||||
| # Modified from python_dhcp_server | ||||
| # Source: https://github.com/niccokunzmann/python_dhcp_server | ||||
| # | ||||
| #    Copyright (c) 2015 Nicco Kunzmann and released under the MIT license | ||||
| # | ||||
| ############################################################################### | ||||
|  | ||||
| import time | ||||
| import threading | ||||
| import struct | ||||
| import queue | ||||
| import collections | ||||
| import traceback | ||||
| import random | ||||
| import base64 | ||||
| import select | ||||
| import ipaddress | ||||
| from socket import * | ||||
|  | ||||
| import daemon_lib.ansiiprint as ansiiprint | ||||
| import daemon_lib.zkhandler as zkhandler | ||||
|  | ||||
| # see https://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Protocol | ||||
| # section DHCP options | ||||
|  | ||||
| def inet_ntoaX(data): | ||||
|     return ['.'.join(map(str, data[i:i + 4])) for i in range(0, len(data), 4)] | ||||
|  | ||||
| def inet_atonX(ips): | ||||
|     return b''.join(map(inet_aton, ips)) | ||||
|  | ||||
| dhcp_message_types = { | ||||
|     1 : 'DHCPDISCOVER', | ||||
|     2 : 'DHCPOFFER', | ||||
|     3 : 'DHCPREQUEST', | ||||
|     4 : 'DHCPDECLINE', | ||||
|     5 : 'DHCPACK', | ||||
|     6 : 'DHCPNAK', | ||||
|     7 : 'DHCPRELEASE', | ||||
|     8 : 'DHCPINFORM', | ||||
| } | ||||
| reversed_dhcp_message_types = dict() | ||||
| for i, v in dhcp_message_types.items(): | ||||
|     reversed_dhcp_message_types[v] = i | ||||
|  | ||||
| shortunpack = lambda data: (data[0] << 8) + data[1] | ||||
| shortpack = lambda i: bytes([i >> 8, i & 255]) | ||||
|  | ||||
|  | ||||
| def macunpack(data): | ||||
|     s = base64.b16encode(data) | ||||
|     return ':'.join([s[i:i+2].decode('ascii') for i in range(0, 12, 2)]) | ||||
|  | ||||
| def macpack(mac): | ||||
|     return base64.b16decode(mac.replace(':', '').replace('-', '').encode('ascii')) | ||||
|  | ||||
| def unpackbool(data): | ||||
|     return data[0] | ||||
|  | ||||
| def packbool(bool): | ||||
|     return bytes([bool]) | ||||
|  | ||||
| options = [ | ||||
| # RFC1497 vendor extensions | ||||
|     ('pad', None, None), | ||||
|     ('subnet_mask', inet_ntoa, inet_aton), | ||||
|     ('time_offset', None, None), | ||||
|     ('router', inet_ntoaX, inet_atonX), | ||||
|     ('time_server', inet_ntoaX, inet_atonX), | ||||
|     ('name_server', inet_ntoaX, inet_atonX), | ||||
|     ('domain_name_server', inet_ntoaX, inet_atonX), | ||||
|     ('log_server', inet_ntoaX, inet_atonX), | ||||
|     ('cookie_server', inet_ntoaX, inet_atonX), | ||||
|     ('lpr_server', inet_ntoaX, inet_atonX), | ||||
|     ('impress_server', inet_ntoaX, inet_atonX), | ||||
|     ('resource_location_server', inet_ntoaX, inet_atonX), | ||||
|     ('host_name', lambda d: d.decode('ASCII'), lambda d: d.encode('ASCII')), | ||||
|     ('boot_file_size', None, None), | ||||
|     ('merit_dump_file', None, None), | ||||
|     ('domain_name', None, None), | ||||
|     ('swap_server', inet_ntoa, inet_aton), | ||||
|     ('root_path', None, None), | ||||
|     ('extensions_path', None, None), | ||||
| # IP Layer Parameters per Host | ||||
|     ('ip_forwarding_enabled', unpackbool, packbool), | ||||
|     ('non_local_source_routing_enabled', unpackbool, packbool), | ||||
|     ('policy_filer', None, None), | ||||
|     ('maximum_datagram_reassembly_size', shortunpack, shortpack), | ||||
|     ('default_ip_time_to_live', lambda data: data[0], lambda i: bytes([i])), | ||||
|     ('path_mtu_aging_timeout', None, None), | ||||
|     ('path_mtu_plateau_table', None, None), | ||||
| # IP Layer Parameters per Interface | ||||
|     ('interface_mtu', None, None), | ||||
|     ('all_subnets_are_local', unpackbool, packbool), | ||||
|     ('broadcast_address', inet_ntoa, inet_aton), | ||||
|     ('perform_mask_discovery', unpackbool, packbool), | ||||
|     ('mask_supplier', None, None), | ||||
|     ('perform_router_discovery', None, None), | ||||
|     ('router_solicitation_address', inet_ntoa, inet_aton), | ||||
|     ('static_route', None, None), | ||||
| # Link Layer Parameters per Interface | ||||
|     ('trailer_encapsulation_option', None, None), | ||||
|     ('arp_cache_timeout', None, None), | ||||
|     ('ethernet_encapsulation', None, None), | ||||
| # TCP Parameters | ||||
|     ('tcp_default_ttl', None, None), | ||||
|     ('tcp_keep_alive_interval', None, None), | ||||
|     ('tcp_keep_alive_garbage', None, None), | ||||
| # Application and Service Parameters Part 1 | ||||
|     ('network_information_service_domain', None, None), | ||||
|     ('network_informtaion_servers', inet_ntoaX, inet_atonX), | ||||
|     ('network_time_protocol_servers', inet_ntoaX, inet_atonX), | ||||
|     ('vendor_specific_information', None, None), | ||||
|     ('netbios_over_tcp_ip_name_server', inet_ntoaX, inet_atonX), | ||||
|     ('netbios_over_tcp_ip_datagram_distribution_server', inet_ntoaX, inet_atonX), | ||||
|     ('netbios_over_tcp_ip_node_type', None, None), | ||||
|     ('netbios_over_tcp_ip_scope', None, None), | ||||
|     ('x_window_system_font_server', inet_ntoaX, inet_atonX), | ||||
|     ('x_window_system_display_manager', inet_ntoaX, inet_atonX), | ||||
| # DHCP Extensions | ||||
|     ('requested_ip_address', inet_ntoa, inet_aton), | ||||
|     ('ip_address_lease_time', lambda d: struct.unpack('>I', d)[0], lambda i: struct.pack('>I', i)), | ||||
|     ('option_overload', None, None), | ||||
|     ('dhcp_message_type', lambda data: dhcp_message_types.get(data[0], data[0]), (lambda name: bytes([reversed_dhcp_message_types.get(name, name)]))), | ||||
|     ('server_identifier', inet_ntoa, inet_aton), | ||||
|     ('parameter_request_list', list, bytes), | ||||
|     ('message', None, None), | ||||
|     ('maximum_dhcp_message_size', shortunpack, shortpack), | ||||
|     ('renewal_time_value', None, None), | ||||
|     ('rebinding_time_value', None, None), | ||||
|     ('vendor_class_identifier', None, None), | ||||
|     ('client_identifier', macunpack, macpack), | ||||
|     ('tftp_server_name', None, None), | ||||
|     ('boot_file_name', None, None), | ||||
| # Application and Service Parameters Part 2 | ||||
|     ('network_information_service_domain', None, None), | ||||
|     ('network_information_servers', inet_ntoaX, inet_atonX), | ||||
|     ('', None, None), | ||||
|     ('', None, None), | ||||
|     ('mobile_ip_home_agent', inet_ntoaX, inet_atonX), | ||||
|     ('smtp_server', inet_ntoaX, inet_atonX), | ||||
|     ('pop_servers', inet_ntoaX, inet_atonX), | ||||
|     ('nntp_server', inet_ntoaX, inet_atonX), | ||||
|     ('default_www_server', inet_ntoaX, inet_atonX), | ||||
|     ('default_finger_server', inet_ntoaX, inet_atonX), | ||||
|     ('default_irc_server', inet_ntoaX, inet_atonX), | ||||
|     ('streettalk_server', inet_ntoaX, inet_atonX), | ||||
|     ('stda_server', inet_ntoaX, inet_atonX), | ||||
|     ] | ||||
|  | ||||
| assert options[18][0] == 'extensions_path', options[18][0] | ||||
| assert options[25][0] == 'path_mtu_plateau_table', options[25][0] | ||||
| assert options[33][0] == 'static_route', options[33][0] | ||||
| assert options[50][0] == 'requested_ip_address', options[50][0] | ||||
| assert options[64][0] == 'network_information_service_domain', options[64][0] | ||||
| assert options[76][0] == 'stda_server', options[76][0] | ||||
|  | ||||
|  | ||||
| class ReadBootProtocolPacket(object): | ||||
|  | ||||
|     for i, o in enumerate(options): | ||||
|         locals()[o[0]] = None | ||||
|         locals()['option_{0}'.format(i)] = None | ||||
|  | ||||
|     del i, o | ||||
|  | ||||
|     def __init__(self, data, address = ('0.0.0.0', 0)): | ||||
|         self.data = data | ||||
|         self.address = address | ||||
|         self.host = address[0] | ||||
|         self.port = address[1] | ||||
|  | ||||
|         # wireshark = wikipedia = data[...] | ||||
|          | ||||
|         self.message_type = self.OP =                data[0] | ||||
|         self.hardware_type = self.HTYPE =            data[1] | ||||
|         self.hardware_address_length = self.HLEN =   data[2] | ||||
|         self.hops = self.HOPS =                      data[3] | ||||
|  | ||||
|         self.XID = self.transaction_id = struct.unpack('>I', data[4:8])[0] | ||||
|  | ||||
|         self.seconds_elapsed = self.SECS = shortunpack(data[8:10]) | ||||
|         self.bootp_flags = self.FLAGS =    shortunpack(data[8:10]) | ||||
|  | ||||
|         self.client_ip_address = self.CIADDR = inet_ntoa(data[12:16]) | ||||
|         self.your_ip_address   = self.YIADDR = inet_ntoa(data[16:20]) | ||||
|         self.next_server_ip_address = self.SIADDR = inet_ntoa(data[20:24]) | ||||
|         self.relay_agent_ip_address = self.GIADDR = inet_ntoa(data[24:28]) | ||||
|  | ||||
|         self.client_mac_address = self.CHADDR = macunpack(data[28: 28 + self.hardware_address_length]) | ||||
|         index = 236 | ||||
|         self.magic_cookie = self.magic_cookie = inet_ntoa(data[index:index + 4]); index += 4 | ||||
|         self.options = dict() | ||||
|         self.named_options = dict() | ||||
|         while index < len(data): | ||||
|             option = data[index]; index += 1 | ||||
|             if option == 0: | ||||
|                 # padding | ||||
|                 # Can be used to pad other options so that they are aligned to the word boundary; is not followed by length byte | ||||
|                 continue | ||||
|             if option == 255: | ||||
|                 # end | ||||
|                 break | ||||
|             option_length = data[index]; index += 1 | ||||
|             option_data = data[index: index + option_length]; index += option_length | ||||
|             self.options[option] = option_data | ||||
|             if option < len(options): | ||||
|                 option_name, function, _ = options[option] | ||||
|                 if function: | ||||
|                     option_data = function(option_data) | ||||
|                 if option_name: | ||||
|                     setattr(self, option_name, option_data) | ||||
|                     self.named_options[option_name] = option_data | ||||
|             setattr(self, 'option_{}'.format(option), option_data) | ||||
|  | ||||
|     def __getitem__(self, key): | ||||
|         return getattr(self, key, None) | ||||
|  | ||||
|     def __contains__(self, key): | ||||
|         return key in self.__dict__ | ||||
|  | ||||
|     @property | ||||
|     def formatted_named_options(self): | ||||
|         return "\n".join("{}:\t{}".format(name.replace('_', ' '), value) for name, value in sorted(self.named_options.items())) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return """Message Type: {self.message_type} | ||||
| client MAC address: {self.client_mac_address} | ||||
| client IP address: {self.client_ip_address} | ||||
| your IP address: {self.your_ip_address} | ||||
| next server IP address: {self.next_server_ip_address} | ||||
| {self.formatted_named_options} | ||||
| """.format(self = self) | ||||
|  | ||||
|     def __gt__(self, other): | ||||
|         return id(self) < id(other) | ||||
|  | ||||
| data = base64.b16decode(b'02010600f7b41ad100000000c0a800640000000000000000000000007c7a914bca6c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000638253633501053604c0a800010104ffffff000304c0a800010604c0a80001ff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'.upper()) | ||||
| assert data[0] == 2 | ||||
| p = ReadBootProtocolPacket(data) | ||||
| assert p.message_type == 2 | ||||
| assert p.hardware_type == 1 | ||||
| assert p.hardware_address_length == 6 | ||||
| assert p.hops == 0 | ||||
| assert p.transaction_id == 4155775697 | ||||
| assert p.seconds_elapsed == 0 | ||||
| assert p.bootp_flags == 0 | ||||
| assert p.client_ip_address == '192.168.0.100' | ||||
| assert p.your_ip_address == '0.0.0.0' | ||||
| assert p.next_server_ip_address == '0.0.0.0' | ||||
| assert p.relay_agent_ip_address == '0.0.0.0' | ||||
| assert p.client_mac_address.lower() == '7c:7a:91:4b:ca:6c' | ||||
| assert p.magic_cookie == '99.130.83.99' | ||||
| assert p.dhcp_message_type == 'DHCPACK' | ||||
| assert p.options[53] == b'\x05' | ||||
| assert p.server_identifier == '192.168.0.1' | ||||
| assert p.subnet_mask == '255.255.255.0' | ||||
| assert p.router == ['192.168.0.1'] | ||||
| assert p.domain_name_server == ['192.168.0.1'] | ||||
| str(p) | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     s1 = socket(type = SOCK_DGRAM) | ||||
|     s1.setsockopt(SOL_IP, SO_REUSEADDR, 1) | ||||
|     s1.bind(('', 67)) | ||||
|     #s2 = socket(type = SOCK_DGRAM) | ||||
|     #s2.setsockopt(SOL_IP, SO_REUSEADDR, 1) | ||||
|     #s2.bind(('', 68)) | ||||
|     while 1: | ||||
|         reads = select.select([s1], [], [], 1)[0] | ||||
|         for s in reads: | ||||
|             packet = ReadBootProtocolPacket(*s.recvfrom(4096)) | ||||
|  | ||||
|  | ||||
| def get_host_ip_addresses(): | ||||
|     return gethostbyname_ex(gethostname())[2] | ||||
|  | ||||
|  | ||||
| class WriteBootProtocolPacket(object): | ||||
|  | ||||
|     message_type = 2 # 1 for client -> server 2 for server -> client | ||||
|     hardware_type = 1 | ||||
|     hardware_address_length = 6 | ||||
|     hops = 0 | ||||
|  | ||||
|     transaction_id = None | ||||
|  | ||||
|     seconds_elapsed = 0 | ||||
|     bootp_flags = 0 # unicast | ||||
|  | ||||
|     client_ip_address = '0.0.0.0' | ||||
|     your_ip_address = '0.0.0.0' | ||||
|     next_server_ip_address = '0.0.0.0' | ||||
|     relay_agent_ip_address = '0.0.0.0' | ||||
|  | ||||
|     client_mac_address = None | ||||
|     magic_cookie = '99.130.83.99' | ||||
|  | ||||
|     parameter_order = [] | ||||
|      | ||||
|     def __init__(self, configuration): | ||||
|         for i in range(256): | ||||
|             names = ['option_{}'.format(i)] | ||||
|             if i < len(options) and hasattr(configuration, options[i][0]): | ||||
|                 names.append(options[i][0]) | ||||
|             for name in names: | ||||
|                 if hasattr(configuration, name): | ||||
|                     setattr(self, name, getattr(configuration, name)) | ||||
|  | ||||
|     def to_bytes(self): | ||||
|         result = bytearray(236) | ||||
|          | ||||
|         result[0] = self.message_type | ||||
|         result[1] = self.hardware_type | ||||
|         result[2] = self.hardware_address_length | ||||
|         result[3] = self.hops | ||||
|  | ||||
|         result[4:8] = struct.pack('>I', self.transaction_id) | ||||
|  | ||||
|         result[ 8:10] = shortpack(self.seconds_elapsed) | ||||
|         result[10:12] = shortpack(self.bootp_flags) | ||||
|  | ||||
|         result[12:16] = inet_aton(self.client_ip_address) | ||||
|         result[16:20] = inet_aton(self.your_ip_address) | ||||
|         result[20:24] = inet_aton(self.next_server_ip_address) | ||||
|         result[24:28] = inet_aton(self.relay_agent_ip_address) | ||||
|  | ||||
|         result[28:28 + self.hardware_address_length] = macpack(self.client_mac_address) | ||||
|          | ||||
|         result += inet_aton(self.magic_cookie) | ||||
|  | ||||
|         for option in self.options: | ||||
|             value = self.get_option(option) | ||||
|             if value is None: | ||||
|                 continue | ||||
|             result += bytes([option, len(value)]) + value | ||||
|         result += bytes([255]) | ||||
|         return bytes(result) | ||||
|  | ||||
|     def get_option(self, option): | ||||
|         if option < len(options) and hasattr(self, options[option][0]): | ||||
|             value = getattr(self, options[option][0]) | ||||
|         elif hasattr(self, 'option_{}'.format(option)): | ||||
|             value = getattr(self, 'option_{}'.format(option)) | ||||
|         else: | ||||
|             return None | ||||
|         function = options[option][2] | ||||
|         if function and value is not None: | ||||
|             value = function(value) | ||||
|         return value | ||||
|      | ||||
|     @property | ||||
|     def options(self): | ||||
|         done = list() | ||||
|         # fulfill wishes | ||||
|         for option in self.parameter_order: | ||||
|             if option < len(options) and hasattr(self, options[option][0]) or hasattr(self, 'option_{}'.format(option)): | ||||
|                 # this may break with the specification because we must try to fulfill the wishes | ||||
|                 if option not in done: | ||||
|                     done.append(option) | ||||
|         # add my stuff | ||||
|         for option, o in enumerate(options): | ||||
|             if o[0] and hasattr(self, o[0]): | ||||
|                 if option not in done: | ||||
|                     done.append(option) | ||||
|         for option in range(256): | ||||
|             if hasattr(self, 'option_{}'.format(option)): | ||||
|                 if option not in done: | ||||
|                     done.append(option) | ||||
|         return done | ||||
|  | ||||
|     def __str__(self): | ||||
|         return str(ReadBootProtocolPacket(self.to_bytes())) | ||||
|  | ||||
| class DelayWorker(object): | ||||
|  | ||||
|     def __init__(self): | ||||
|         self.closed = False | ||||
|         self.queue = queue.PriorityQueue() | ||||
|         self.thread = threading.Thread(target = self._delay_response_thread) | ||||
|         self.thread.daemon = True | ||||
|         self.thread.start() | ||||
|  | ||||
|     def _delay_response_thread(self): | ||||
|         while not self.closed: | ||||
|             p = self.queue.get() | ||||
|             if self.closed: | ||||
|                 break | ||||
|             t, func, args, kw = p | ||||
|             now = time.time() | ||||
|             if now < t: | ||||
|                 time.sleep(0.01) | ||||
|                 self.queue.put(p) | ||||
|             else: | ||||
|                 func(*args, **kw) | ||||
|  | ||||
|     def do_after(self, seconds, func, args = (), kw = {}): | ||||
|         self.queue.put((time.time() + seconds, func, args, kw)) | ||||
|  | ||||
|     def close(self): | ||||
|         self.closed = True | ||||
|  | ||||
| class Transaction(object): | ||||
|  | ||||
|     def __init__(self, server): | ||||
|         self.server = server | ||||
|         self.configuration = server.configuration | ||||
|         self.packets = [] | ||||
|         self.done_time = time.time() + self.configuration.length_of_transaction | ||||
|         self.done = False | ||||
|         self.do_after = self.server.delay_worker.do_after | ||||
|  | ||||
|     def is_done(self): | ||||
|         return self.done or self.done_time < time.time() | ||||
|  | ||||
|     def close(self): | ||||
|         self.done = True | ||||
|  | ||||
|     def receive(self, packet): | ||||
|         # packet from client <-> packet.message_type == 1 | ||||
|         if packet.message_type == 1 and packet.dhcp_message_type == 'DHCPDISCOVER': | ||||
|             self.do_after(self.configuration.dhcp_offer_after_seconds, | ||||
|                           self.received_dhcp_discover, (packet,), ) | ||||
|         elif packet.message_type == 1 and packet.dhcp_message_type == 'DHCPREQUEST': | ||||
|             self.do_after(self.configuration.dhcp_acknowledge_after_seconds, | ||||
|                           self.received_dhcp_request, (packet,), ) | ||||
|         elif packet.message_type == 1 and packet.dhcp_message_type == 'DHCPINFORM': | ||||
|             self.received_dhcp_inform(packet) | ||||
|         else: | ||||
|             return False | ||||
|         return True | ||||
|  | ||||
|     def received_dhcp_discover(self, discovery): | ||||
|         if self.is_done(): return | ||||
|         self.configuration.debug('discover:\n {}'.format(str(discovery).replace('\n', '\n\t'))) | ||||
|         self.send_offer(discovery) | ||||
|  | ||||
|     def send_offer(self, discovery): | ||||
|         # https://tools.ietf.org/html/rfc2131 | ||||
|         offer = WriteBootProtocolPacket(self.configuration) | ||||
|         offer.parameter_order = discovery.parameter_request_list | ||||
|         mac = discovery.client_mac_address | ||||
|         ip = offer.your_ip_address = self.server.get_ip_address(discovery) | ||||
|         # offer.client_ip_address =  | ||||
|         offer.transaction_id = discovery.transaction_id | ||||
|         # offer.next_server_ip_address = | ||||
|         offer.relay_agent_ip_address = discovery.relay_agent_ip_address | ||||
|         offer.client_mac_address = mac | ||||
|         offer.client_ip_address = discovery.client_ip_address or '0.0.0.0' | ||||
|         offer.bootp_flags = discovery.bootp_flags | ||||
|         offer.dhcp_message_type = 'DHCPOFFER' | ||||
|         offer.client_identifier = mac | ||||
|         self.server.broadcast(offer) | ||||
|         ansiiprint.echo('DHCP server allocated {} to {} (hostname "{}")'.format(ip, mac, discovery.host_name), '', 'i') | ||||
|      | ||||
|     def received_dhcp_request(self, request): | ||||
|         if self.is_done(): return  | ||||
|         self.server.client_has_chosen(request) | ||||
|         self.acknowledge(request) | ||||
|         self.close() | ||||
|  | ||||
|     def acknowledge(self, request): | ||||
|         ack = WriteBootProtocolPacket(self.configuration) | ||||
|         ack.parameter_order = request.parameter_request_list | ||||
|         ack.transaction_id = request.transaction_id | ||||
|         # ack.next_server_ip_address = | ||||
|         ack.bootp_flags = request.bootp_flags | ||||
|         ack.relay_agent_ip_address = request.relay_agent_ip_address | ||||
|         mac = request.client_mac_address | ||||
|         ack.client_mac_address = mac | ||||
|         requested_ip_address = request.requested_ip_address | ||||
|         ack.client_ip_address = request.client_ip_address or '0.0.0.0' | ||||
|         ack.your_ip_address = self.server.get_ip_address(request) | ||||
|         ack.dhcp_message_type = 'DHCPACK' | ||||
|         self.server.broadcast(ack) | ||||
|  | ||||
|     def received_dhcp_inform(self, inform): | ||||
|         self.close() | ||||
|         self.server.client_has_chosen(inform) | ||||
|  | ||||
| class DHCPServerConfiguration(object): | ||||
|  | ||||
|     def __init__(self, zk_conn, ipaddr, iface, vni, network, router, dns_servers, start_addr, end_addr): | ||||
|         self.dhcp_offer_after_seconds = 1 | ||||
|         self.dhcp_acknowledge_after_seconds = 1 | ||||
|         self.length_of_transaction = 60 | ||||
|  | ||||
|         self.zk_conn = zk_conn | ||||
|  | ||||
|         self.ipaddr = ipaddr | ||||
|         self.iface = iface | ||||
|         self.vni = vni | ||||
|  | ||||
|         self.network_cidr = ipaddress.IPv4Network(network, False) | ||||
|         self.network = str(self.network_cidr.network_address) | ||||
|         self.broadcast_address = str(self.network_cidr.broadcast_address) | ||||
|         self.subnet_mask = str(self.network_cidr.netmask) | ||||
|  | ||||
|         self.router = router | ||||
|         self.domain_name_server = dns_servers | ||||
|  | ||||
|         self.start_addr = start_addr | ||||
|         self.end_addr = end_addr | ||||
|  | ||||
|         # 1 day is 86400 | ||||
|         self.ip_address_lease_time = 300 # seconds | ||||
|  | ||||
|         self.debug = lambda *args, **kw: None | ||||
|  | ||||
|     def all_ip_addresses(self): | ||||
|         ips = ip_addresses(self.network, self.subnet_mask, self.start_addr, self.end_addr) | ||||
|         return ips | ||||
|  | ||||
|     def network_filter(self): | ||||
|         return NETWORK(self.network, self.subnet_mask) | ||||
|  | ||||
| def ip_addresses(network, subnet_mask, start_addr, end_addr): | ||||
|     subnet_mask = struct.unpack('>I', inet_aton(subnet_mask))[0] | ||||
|     network = struct.unpack('>I', inet_aton(network))[0] | ||||
|     network = network & subnet_mask | ||||
|     start = struct.unpack('>I', inet_aton(start_addr))[0] | ||||
|     end = struct.unpack('>I', inet_aton(end_addr))[0] | ||||
|     return (inet_ntoa(struct.pack('>I', i)) for i in range(start, end)) | ||||
|  | ||||
| class ALL(object): | ||||
|     def __eq__(self, other): | ||||
|         return True | ||||
|     def __repr__(self): | ||||
|         return self.__class__.__name__ | ||||
| ALL = ALL() | ||||
|  | ||||
| class GREATER(object): | ||||
|     def __init__(self, value): | ||||
|         self.value = value | ||||
|     def __eq__(self, other): | ||||
|         return type(self.value)(other) > self.value | ||||
|  | ||||
| class NETWORK(object): | ||||
|     def __init__(self, network, subnet_mask): | ||||
|         self.subnet_mask = struct.unpack('>I', inet_aton(subnet_mask))[0] | ||||
|         self.network = struct.unpack('>I', inet_aton(network))[0] | ||||
|     def __eq__(self, other): | ||||
|         ip = struct.unpack('>I', inet_aton(other))[0] | ||||
|         return ip & self.subnet_mask == self.network and \ | ||||
|                ip - self.network and \ | ||||
|                ip - self.network != ~self.subnet_mask & 0xffffffff | ||||
|          | ||||
| class CASEINSENSITIVE(object): | ||||
|     def __init__(self, s): | ||||
|         self.s = s.lower() | ||||
|     def __eq__(self, other): | ||||
|         return self.s == other.lower() | ||||
|  | ||||
| class CSVDatabase(object): | ||||
|  | ||||
|     delimiter = ';' | ||||
|  | ||||
|     def __init__(self, file_name): | ||||
|         self.file_name = file_name | ||||
|         self.file('a').close() # create file | ||||
|  | ||||
|     def file(self, mode = 'r'): | ||||
|         return open(self.file_name, mode) | ||||
|  | ||||
|     def get(self, pattern): | ||||
|         pattern = list(pattern) | ||||
|         return [line for line in self.all() if pattern == line] | ||||
|  | ||||
|     def add(self, line): | ||||
|         with self.file('a') as f: | ||||
|             f.write(self.delimiter.join(line) + '\n') | ||||
|  | ||||
|     def delete(self, pattern): | ||||
|         lines = self.all() | ||||
|         lines_to_delete = self.get(pattern) | ||||
|         self.file('w').close() # empty file | ||||
|         for line in lines: | ||||
|             if line not in lines_to_delete: | ||||
|                 self.add(line) | ||||
|  | ||||
|     def all(self): | ||||
|         with self.file() as f: | ||||
|             return [list(line.strip().split(self.delimiter)) for line in f] | ||||
|  | ||||
|  | ||||
| class ZKDatabase(object): | ||||
|  | ||||
|     # Store DHCP leases in zookeeper | ||||
|     #  /networks/<VNI>/dhcp_leases/<MAC>:<timestamp>/{ipaddr,hostname} | ||||
|     # Line: | ||||
|     #  ['52:54:00:21:34:11', '10.10.10.6', 'test1', '1538287572'] | ||||
|  | ||||
|     def __init__(self, zk_conn, key): | ||||
|         self.zk_conn = zk_conn | ||||
|         self.key = key | ||||
|  | ||||
|     def get(self, pattern): | ||||
|         pattern = list(pattern) | ||||
|         return [line for line in self.all() if pattern[0] == line[0] and pattern[1] == line[1] and pattern[2] == line[2]] | ||||
|  | ||||
|     def isstatic(self, pattern): | ||||
|         macaddr = pattern[0] | ||||
|         try: | ||||
|             timestamp = zkhandler.readdata(self.zk_conn, '{}/{}'.format(self.key, macaddr)) | ||||
|             if timestamp == 'static': | ||||
|                 return True | ||||
|             else: | ||||
|                 return False | ||||
|         except Exception: | ||||
|             return False | ||||
|  | ||||
|     def add(self, line): | ||||
|         macaddr = line[0] | ||||
|         ipaddr = line[1] | ||||
|         hostname = line[2] | ||||
|         timestamp = line[3] | ||||
|  | ||||
|         zkhandler.writedata(self.zk_conn, { | ||||
|             '{}/{}'.format(self.key, macaddr): timestamp, | ||||
|             '{}/{}/ipaddr'.format(self.key, macaddr): ipaddr, | ||||
|             '{}/{}/hostname'.format(self.key, macaddr): hostname | ||||
|         }) | ||||
|  | ||||
|     def delete(self, pattern): | ||||
|         macaddr = pattern[0] | ||||
|         try: | ||||
|             zkhandler.delete(self.zk_conn, '{}/{}'.format(self.key, macaddr)) | ||||
|         except Exception: | ||||
|             pass | ||||
|  | ||||
|     def all(self): | ||||
|         leases = [] | ||||
|         mac_list = zkhandler.listchildren(self.zk_conn, self.key) | ||||
|         for macaddr in mac_list: | ||||
|             timestamp = zkhandler.readdata(self.zk_conn, '{}/{}'.format(self.key, macaddr)) | ||||
|             if timestamp == 'static': | ||||
|                 timestamp = 0 | ||||
|             ipaddr = zkhandler.readdata(self.zk_conn, '{}/{}/ipaddr'.format(self.key, macaddr)) | ||||
|             hostname = zkhandler.readdata(self.zk_conn, '{}/{}/hostname'.format(self.key, macaddr)) | ||||
|             leases.append([macaddr, ipaddr, hostname, timestamp]) | ||||
|         return leases | ||||
|          | ||||
|  | ||||
| class Host(object): | ||||
|  | ||||
|     def __init__(self, mac, ip, hostname, last_used): | ||||
|         self.mac = mac.upper() | ||||
|         self.ip = ip | ||||
|         self.hostname = hostname | ||||
|         self.last_used = int(last_used) | ||||
|  | ||||
|     @classmethod | ||||
|     def from_tuple(cls, line): | ||||
|         mac, ip, hostname, last_used = line | ||||
|         last_used = int(last_used) | ||||
|         return cls(mac, ip, hostname, last_used) | ||||
|  | ||||
|     @classmethod | ||||
|     def from_packet(cls, packet): | ||||
|         return cls(packet.client_mac_address, | ||||
|                    packet.requested_ip_address or packet.client_ip_address, | ||||
|                    packet.host_name or '', | ||||
|                    int(time.time())) | ||||
|  | ||||
|     @staticmethod | ||||
|     def get_pattern(mac = ALL, ip = ALL, hostname = ALL, last_used = ALL): | ||||
|         return [mac, ip, hostname, last_used] | ||||
|  | ||||
|     def to_tuple(self): | ||||
|         return [self.mac, self.ip, self.hostname, str(int(self.last_used))] | ||||
|  | ||||
|     def to_pattern(self): | ||||
|         return self.get_pattern(ip = self.ip, mac = self.mac) | ||||
|  | ||||
|     def __hash__(self): | ||||
|         return hash(self.key) | ||||
|  | ||||
|     def __eq__(self, other): | ||||
|         return self.to_tuple() == other.to_tuple() | ||||
|  | ||||
|     def has_valid_ip(self): | ||||
|         return self.ip and self.ip != '0.0.0.0' | ||||
|          | ||||
|  | ||||
| class HostDatabase(object): | ||||
|     def __init__(self, zk_conn, vni): | ||||
|         self.db = ZKDatabase(zk_conn, '/networks/{}/dhcp_leases'.format(vni)) | ||||
|  | ||||
|     def get(self, **kw): | ||||
|         pattern = Host.get_pattern(**kw) | ||||
|         return list(map(Host.from_tuple, self.db.get(pattern))) | ||||
|  | ||||
|     def add(self, host): | ||||
|         host_tuple = host.to_tuple() | ||||
|         self.db.add(host_tuple) | ||||
|  | ||||
|     def delete(self, host = None, **kw): | ||||
|         if host is None: | ||||
|             pattern = Host.get_pattern(**kw) | ||||
|         else: | ||||
|             pattern = host.to_pattern() | ||||
|         self.db.delete(pattern) | ||||
|  | ||||
|     def isstatic(self, host): | ||||
|         return self.db.isstatic(host.to_tuple()) | ||||
|  | ||||
|     def all(self): | ||||
|         return list(map(Host.from_tuple, self.db.all())) | ||||
|  | ||||
|     def replace(self, host): | ||||
|         if not self.isstatic(host): | ||||
|             self.delete(host) | ||||
|             self.add(host) | ||||
|          | ||||
| def sorted_hosts(hosts): | ||||
|     hosts = list(hosts) | ||||
|     hosts.sort(key = lambda host: (host.hostname.lower(), host.mac.lower(), host.ip.lower())) | ||||
|     return hosts | ||||
|  | ||||
| class DHCPServer(object): | ||||
|  | ||||
|     def __init__(self, configuration): | ||||
|         self.configuration = configuration | ||||
|         self.socket = socket(type=SOCK_DGRAM) | ||||
|         self.socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) | ||||
|         self.socket.setsockopt(SOL_SOCKET, 25, self.configuration.iface.encode('ascii')) | ||||
|         self.socket.bind(('<broadcast>', 67)) | ||||
|         self.delay_worker = DelayWorker() | ||||
|         self.closed = False | ||||
|         self.transactions = collections.defaultdict(lambda: Transaction(self)) # id: transaction | ||||
|         self.hosts = HostDatabase(self.configuration.zk_conn, self.configuration.vni) | ||||
|         self.time_started = time.time() | ||||
|  | ||||
|     def close(self): | ||||
|         self.socket.close() | ||||
|         self.closed = True | ||||
|         self.delay_worker.close() | ||||
|         for transaction in list(self.transactions.values()): | ||||
|             transaction.close() | ||||
|  | ||||
|     def update(self, timeout = 0): | ||||
|         try: | ||||
|             reads = select.select([self.socket], [], [], timeout)[0] | ||||
|         except ValueError as e: | ||||
|             # ValueError: file descriptor cannot be a negative integer (-1) | ||||
|             return | ||||
|         for socket in reads: | ||||
|             try: | ||||
|                 packet = ReadBootProtocolPacket(*socket.recvfrom(4096)) | ||||
|             except OSError as e: | ||||
|                 # OSError: [WinError 10038] An operation was attempted on something that is not a socket | ||||
|                 pass | ||||
|             else: | ||||
|                 self.received(packet) | ||||
|         for transaction_id, transaction in list(self.transactions.items()): | ||||
|             if transaction.is_done(): | ||||
|                 transaction.close() | ||||
|                 self.transactions.pop(transaction_id) | ||||
|  | ||||
|     def received(self, packet): | ||||
|         if not self.transactions[packet.transaction_id].receive(packet): | ||||
|             self.configuration.debug('received:\n {}'.format(str(packet).replace('\n', '\n\t'))) | ||||
|              | ||||
|     def client_has_chosen(self, packet): | ||||
|         self.configuration.debug('client_has_chosen:\n {}'.format(str(packet).replace('\n', '\n\t'))) | ||||
|         host = Host.from_packet(packet) | ||||
|         if not host.has_valid_ip(): | ||||
|             return | ||||
|         self.hosts.replace(host) | ||||
|  | ||||
|     def is_valid_client_address(self, address): | ||||
|         if address is None: | ||||
|             return False | ||||
|         a = address.split('.') | ||||
|         s = self.configuration.subnet_mask.split('.') | ||||
|         n = self.configuration.network.split('.') | ||||
|         return all(s[i] == '0' or a[i] == n[i] for i in range(4)) | ||||
|  | ||||
|     def get_ip_address(self, packet): | ||||
|         mac_address = packet.client_mac_address | ||||
|         requested_ip_address = packet.requested_ip_address | ||||
|         known_hosts = self.hosts.get(mac = CASEINSENSITIVE(mac_address)) | ||||
|         ip = None | ||||
|         if known_hosts: | ||||
|             # 1. choose known ip address (including static lease) | ||||
|             for host in known_hosts: | ||||
|                 if self.is_valid_client_address(host.ip): | ||||
|                     ip = host.ip | ||||
|         if ip is None and self.is_valid_client_address(requested_ip_address): | ||||
|             # 2. choose valid requested ip address | ||||
|             ip = requested_ip_address | ||||
|         if ip is None: | ||||
|             # 3. choose new, free ip address | ||||
|             chosen = False | ||||
|             network_hosts = self.hosts.get(ip = self.configuration.network_filter()) | ||||
|             for ip in self.configuration.all_ip_addresses(): | ||||
|                 if not any(host.ip == ip for host in network_hosts): | ||||
|                     chosen = True | ||||
|                     break | ||||
|             if not chosen: | ||||
|                 # 4. reuse old valid ip address | ||||
|                 network_hosts.sort(key = lambda host: host.last_used) | ||||
|                 ip = network_hosts[0].ip | ||||
|                 assert self.is_valid_client_address(ip) | ||||
|         if not any([host.ip == ip for host in known_hosts]): | ||||
|             self.hosts.replace(Host(mac_address, ip, packet.host_name or '', time.time())) | ||||
|         return ip | ||||
|  | ||||
|     @property | ||||
|     def server_identifiers(self): | ||||
|         return get_host_ip_addresses() | ||||
|  | ||||
|     def broadcast(self, packet): | ||||
|         self.configuration.debug('broadcasting:\n {}'.format(str(packet).replace('\n', '\n\t'))) | ||||
|         for addr in self.server_identifiers: | ||||
|             broadcast_socket = socket(type = SOCK_DGRAM) | ||||
|             broadcast_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) | ||||
|             broadcast_socket.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) | ||||
|             packet.server_identifier = addr | ||||
|             broadcast_socket.bind((self.configuration.ipaddr, 67)) | ||||
|             try: | ||||
|                 data = packet.to_bytes() | ||||
|                 broadcast_socket.sendto(data, ('255.255.255.255', 68)) | ||||
|                 broadcast_socket.sendto(data, (addr, 68)) | ||||
|             finally: | ||||
|                 broadcast_socket.close() | ||||
|  | ||||
|     def run(self): | ||||
|         while not self.closed: | ||||
|             try: | ||||
|                 self.update(1) | ||||
|             except KeyboardInterrupt: | ||||
|                 break | ||||
|             except: | ||||
|                 traceback.print_exc() | ||||
|  | ||||
|     def start(self): | ||||
|         self.thread = threading.Thread(target = self.run) | ||||
|         self.thread.daemon = True | ||||
|         self.thread.start() | ||||
|  | ||||
|     def debug_clients(self): | ||||
|         for line in self.ips.all(): | ||||
|             line = '\t'.join(line) | ||||
|             if line: | ||||
|                 self.configuration.debug(line) | ||||
|  | ||||
|     def get_all_hosts(self): | ||||
|         return sorted_hosts(self.hosts.get()) | ||||
|  | ||||
|     def get_current_hosts(self): | ||||
|         return sorted_hosts(self.hosts.get(last_used = GREATER(self.time_started))) | ||||
| @@ -70,6 +70,7 @@ def readConfig(pvcrd_config_file, myhostname): | ||||
|     o_config = configparser.ConfigParser() | ||||
|     o_config.read(pvcrd_config_file) | ||||
|     config = {} | ||||
|     config['pvcrd_config_file'] = pvcrd_config_file | ||||
|  | ||||
|     try: | ||||
|         entries = o_config[myhostname] | ||||
|   | ||||
		Reference in New Issue
	
	Block a user