diff options
Diffstat (limited to 'junos-bootstrap/dhcpd/server_dhcp.py')
-rw-r--r-- | junos-bootstrap/dhcpd/server_dhcp.py | 224 |
1 files changed, 130 insertions, 94 deletions
diff --git a/junos-bootstrap/dhcpd/server_dhcp.py b/junos-bootstrap/dhcpd/server_dhcp.py index 4a4295f..d642917 100644 --- a/junos-bootstrap/dhcpd/server_dhcp.py +++ b/junos-bootstrap/dhcpd/server_dhcp.py @@ -15,7 +15,6 @@ Based on the work of psychomario - https://github.com/psychomario TODO - * only process if option 82 and GIADDR != '00000000' is set in discover/request * try/catch around each incomming packet - prevents DHCP-server from crashing if it receives a malformed packet ''' @@ -29,6 +28,7 @@ if not hasattr(IN,"SO_BINDTODEVICE"): options_raw = {} # TODO - not a nice way to do things option_82_1 = '' +client = '' # Length of DHCP fields in octets, and their placement in packet. # Ref: http://4.bp.blogspot.com/-IyYoFjAC4l8/UXuo16a3sII/AAAAAAAAAXQ/b6BojbYXoXg/s1600/DHCPTitle.JPG @@ -75,9 +75,13 @@ def format_hex_mac(hex_mac): def six_byte_mac_to_str(mac): return ':'.join('%02x' % byte for byte in mac) +# b'b827eb9a520f' => 'b8:27:eb:9a:52:0f' +def prettyprint_hex_as_str(hex): + return ':'.join('%02x' % byte for byte in binascii.unhexlify(hex)) + # Parses DHCP options - raw = hex options def parse_options(raw): - print(' --> processing DHCP options') + print('[%s] --> processing DHCP options' % client) chunked = chunk_hex(raw) chunked_length = len(chunked) pointer = 0 # counter - next option start @@ -104,24 +108,36 @@ def parse_options(raw): elif option is 53: # options[option] = 1 # Not adding DHCP DISCOVER to the options list, becouse it will not be used further on if int(chunked[pointer+2], 16) is 1: - print(' --> option: %s: %s' % (option, 'DHCP Discover (will not be used in reply)')) + print('[%s] --> option: %s: %s' % (client, option, 'DHCP Discover (will not be used in reply)')) else: - print(' --> option: %s: %s' % (option, asciivalue)) + print('[%s] --> option: %s: %s' % (client, option, asciivalue)) else: options[option] = asciivalue - print(' --> option: %s: %s' % (option, asciivalue)) + # TODO: Formating.... + try: + if len(asciivalue) > 30: + print('[%s] --> option: %s: %s' % (client, option, asciivalue[:26] + ' [...]')) + else: + print('[%s] --> option: %s: %s' % (client, option, asciivalue)) + except Exception: + if len(asciivalue) > 30: + print('[%s] --> option: %s: %s' % (client, option, prettyprint_hex_as_str(option_payload)[:26] + ' [...]')) + else: + print('[%s] --> option: %s: %s' % (client, option, prettyprint_hex_as_str(option_payload))) + pass + pointer = pointer + length + 2 # place pointer at the next options' option ID/code field if int(chunked[pointer], 16) is 255: # end of DHCP options - allways last field - print(' --> finished processing options') + print('[%s] --> Finished processing options' % client) break return options # Parses suboptions def parse_suboptions(option, raw): - print(' --> processing suboption hook for option %s' % option) + print('[%s] --> processing suboption hook for option %s' % (client, option)) chunked = chunk_hex(raw) chunked_length = len(chunked) pointer = 0 # counter - next option start @@ -137,11 +153,12 @@ def parse_suboptions(option, raw): if option is 82 and int(chunked[0], 16) is 1: # Option 82 - custom shit: Putting data in list option_82_1 = binascii.unhexlify(value).decode() - print(' --> suboption %s found - value: "%s"' % (int(chunked[0], 16), binascii.unhexlify(value).decode())) # will fail on non-ascii characters + print('[%s] --> suboption %s found - value: "%s"' % (client, int(chunked[0], 16), binascii.unhexlify(value).decode())) # will fail on non-ascii characters + dataset[int(chunked[0], 16)] = value pointer = pointer + length + 2 # place pointer at the next options' option ID/code field if pointer not in chunked: # end of DHCP options - allways last field - print(' --> finished processing suboption %s' % option) + print('[%s] --> Finished processing suboption %s' % (client, option)) break return dataset @@ -152,108 +169,126 @@ def reqparse(message): hexmessage=binascii.hexlify(message) messagesplit=[binascii.hexlify(x) for x in split_packet(message,dhcpfields)] - # Test parsing - options = parse_options(b'3501013c3c4a756e697065722d6578323230302d632d3132742d3267000000000000000000000000000000000000000000000000000000000000000000000000005222012064697374726f2d746573743a67652d302f302f302e303a626f6f747374726170ff') - # options = parse_options(messagesplit[15]) + global client + client = prettyprint_hex_as_str(messagesplit[11]) - # print(parse_suboptions('82', options[82])) + print('[%s] Parsing DHCP packet from client' % client) + + # + # Logical checks to decide to whether respond or reject + # - if 82 in options: # contains option 82 - was forwarded by a DHCP relay - print(' --> DHCP packet contains option 82 -> should be processed') - # print(options[82]) - else: - print(' --> DHCP packet does not contain option 82 -> should be dropped') + # DHCP request has been forwarded by DHCP relay + if int(messagesplit[10]) is 0: + print('[%s] Rejecting to process DHCP packet - not forwarded by DHCP relay' % client) + return False - if int(messagesplit[10]) is not 0: - print(' --> DHCP packet forwarded by relay %s' % hex_ip_to_str(messagesplit[10])) + # Process DHCP options + # Test data from EX2200 first boot up + options = parse_options(b'3501013c3c4a756e697065722d6578323230302d632d3132742d3267000000000000000000000000000000000000000000000000000000000000000000000000005222012064697374726f2d746573743a67652d302f302f302e303a626f6f747374726170ff') + # options = parse_options(messagesplit[15]) + + # Option 82 is set in the packet + if 82 not in options: + print('[%s] Rejecting to process DHCP packet - DHCP option 82 not set' % client) + return False + + # Check DHCP request type + if messagesplit[15][:6] == b'350101': + mode = 'dhcp_discover' + print('[%s] --> DHCP packet type: DHCP DISCOVER' % client) + elif messagesplit[15][:6] == b'350103': + mode = 'dhcp_request' + print('[%s] --> DHCP packet type: DHCP REQUEST' % client) else: - print(' --> DHCP packet not forwarded - direct request') - - # print(b'DHCP XID/Transaction ID:' + messagesplit[4]) + print('[%s] Rejecting to process DHCP packet - option 53 not first in DHCP request' % client) + return False + # + # Packet passes our requirements + # + print('[%s] --> DHCP packet contains option 82 - continues to process' % client) + print('[%s] --> DHCP packet forwarded by relay %s' % (client, hex_ip_to_str(messagesplit[10]))) + print('[%s] --> DHCP XID/Transaction ID: %s' % (client, prettyprint_hex_as_str(messagesplit[4]))) - - - # Testing - do DB lookup based on option 82 + # Handle DB request - do DB lookup based on option 82 + print('[%s] --> Looking up in the DB' % (client)) if len(option_82_1) > 0: (distro, phy, vlan) = option_82_1.split(':') + print('[%s] --> Query details: distro_name:%s, distro_phy_port:%s' % (client, distro, phy.split('.')[0])) - # lease.debug = True if lease({'distro_name': distro, 'distro_phy_port': phy.split('.')[0]}).get_dict() is not False: lease_details = lease({'distro_name': distro, 'distro_phy_port': phy[:-2]}).get_dict() - print(' --> Found match in DB with distro_name:' + distro + ' distro_phy_port:' + phy.split('.')[0]) + print('[%s] --> Data found, switch exists in DB - ready to craft response' % client) + else: + print('[%s] --> Data not found, switch does not exists in DB' % client) + return False - if messagesplit[15][:6] == b'350101': # option 53 (should allways be the first option in DISCOVER/REQUEST) - identifies DHCP packet type - discover/request/offer/ack++ - print('\n\nDHCP DISCOVER - client MAC %s' % six_byte_mac_to_str(messagesplit[11])) - print(' --> crafting DHCP OFFER response') - - # DHCP OFFER details - Options - data = b'\x02' # Message type - boot reply - data += b'\x01' # Hardware type - ethernet - data += b'\x06' # Hardware address length - 6 octets for MAC - data += b'\x00' # Hops - data += binascii.unhexlify(messagesplit[4]) # XID / Transaction ID - data += b'\x00\x01' # seconds elapsed - 1 second - data += b'\x80\x00' # BOOTP flags - broadcast (unicast: 0x0000) - data += b'\x00'*4 # Client IP address - data += socket.inet_aton(lease_details['mgmt_addr']) # New IP to client - data += socket.inet_aton(address) # Next server IP addres - self - data += binascii.unhexlify(messagesplit[10]) # Relay agent IP - DHCP forwarder - data += binascii.unhexlify(messagesplit[11]) # Client MAC - data += b'\x00'*202 # Client hardware address padding (10) + Server hostname (64) + Boot file name (128) - data += b'\x63\x82\x53\x63' # Magic cookie + if mode == 'dhcp_discover': + print('[%s] --> crafting DHCP OFFER response' % client) - # DHCP Options - ordered by pcapng "proof of concept" file - data += craft_option(53).raw_hex(b'\x02') # Option 53 - DHCP OFFER - - elif messagesplit[15][:6] == b'350103': - print('\n\nDHCP REQUEST - client MAC %s' % four_byte_mac_to_str(messagesplit[11])) - print(' --> crafting DHCP ACK response') + if mode == 'dhcp_request': + print('[%s] --> crafting DHCP ACK response' % client) - data = b'\x02' # Message type - boot reply - data += b'\x01' # Hardware type - ethernet - data += b'\x06' # Hardware address length - 6 octets for MAC - data += b'\x00' # Hops - data += binascii.unhexlify(messagesplit[4]) # XID / Transaction ID - data += b'\x00\x01' # seconds elapsed - 1 second - data += b'\x80\x00' # BOOTP flags - broadcast (unicast: 0x0000) - data += b'\x00'*4 # Client IP address - data += socket.inet_aton(lease_details['mgmt_addr']) # New IP to client <<<<<<<<<<<<<<<<<<<<<<<< ALL THE FUCKINGS IN THE WORLD - data += socket.inet_aton(address) # Next server IP addres - self - data += binascii.unhexlify(messagesplit[10]) # Relay agent IP - DHCP forwarder - data += binascii.unhexlify(messagesplit[11]) # Client MAC - data += b'\x00'*202 # Client hardware address padding (10) + Server hostname (64) + Boot file name (128) - data += b'\x63\x82\x53\x63' # Magic cookie - # DHCP Options - ordered by pcapng "proof of concept" file - data += b'\x35\x01\05' # Option 53 - DHCP ACK - else: - print('Unexpected DHCP option 53 - stopping processing request') - return None - + print('[%s] --> XID/Transaction ID: %s' % (client, prettyprint_hex_as_str(messagesplit[4]))) + print('[%s] --> Client IP: %s' % (client, lease_details['mgmt_addr'])) + print('[%s] --> Next server IP: %s' % (client, address)) + print('[%s] --> DHCP forwarder IP: %s' % (client, hex_ip_to_str(messagesplit[10]))) + print('[%s] --> Client MAC: %s' % (client, client)) + + data = b'\x02' # Message type - boot reply + data += b'\x01' # Hardware type - ethernet + data += b'\x06' # Hardware address length - 6 octets for MAC + data += b'\x00' # Hops + data += binascii.unhexlify(messagesplit[4]) # XID / Transaction ID + data += b'\x00\x01' # seconds elapsed - 1 second + data += b'\x80\x00' # BOOTP flags - broadcast (unicast: 0x0000) + data += b'\x00'*4 # Client IP address + data += socket.inet_aton(lease_details['mgmt_addr']) # New IP to client + data += socket.inet_aton(address) # Next server IP address + data += binascii.unhexlify(messagesplit[10]) # Relay agent IP - DHCP forwarder + data += binascii.unhexlify(messagesplit[11]) # Client MAC + data += b'\x00'*202 # Client hardware address padding (10) + Server hostname (64) + Boot file name (128) + data += b'\x63\x82\x53\x63' # Magic cookie + + # + # Craft DHCP options + # + print('[%s] --> Completed DHCP header structure, building DHCP options' % client) + if mode == 'dhcp_discover': + print('[%s] --> Option 53: DHCP OFFER (2)' % client) + data += craft_option(53).raw_hex(b'\x02') # Option 53 - DHCP OFFER - # common options for both DHCP REPLY and DHCP ACK - should be most of the options + if mode == 'dhcp_request': + print('[%s] --> Option 53: DHCP ACK (5)' % client) + data += craft_option(53).raw_hex(b'\x05') # Option 53 - DHCP ACK + data += craft_option(54).bytes(socket.inet_aton(address)) # Option 54 - DHCP server identifier - print(' --> Option 54 (DHCP server identifier): %s' % address) + print('[%s] --> Option 54 (DHCP server identifier): %s' % (client, address)) + data += craft_option(51).raw_hex(b'\x00\x00\xff\x00') # Option 51 - Lease time left padded with "0" - print(' --> Option 51 (Lease time): %s' % '???') + print('[%s] --> Option 51 (Lease time): %s' % (client, '65536')) + data += craft_option(1).ip(netmask) # Option 1 - Subnet mask - print(' --> Option 51 (Subnet mask): %s' % netmask) + print('[%s] --> Option 1 (subnet mask): %s' % (client, netmask)) + + data += craft_option(3).bytes(messagesplit[10]) # Option 3 - Default gateway (set to DHCP forwarders IP) + print('[%s] --> Option 3 (default gateway): %s' % (client, address)) # TODO - FIX BASED ON CIDR IN DB + + data += craft_option(150).bytes(socket.inet_aton(address)) # Option 150 - TFTP Server. Used as target for the Zero Touch Protocol + print('[%s] --> Option 150 (Cisco proprietary TFTP server(s)): %s' % (client, address)) # TODO - FIX BASED ON CIDR IN DB + + # http://www.juniper.net/documentation/en_US/junos13.2/topics/concept/software-image-and-configuration-automatic-provisioning-understanding.html + data += craft_option(43).bytes(craft_option(0).string('/junos/' + junos_file) + craft_option(1).string('/tg15-edge/' + lease_details['hostname']) + craft_option(3).string('http')) # Option 43 - ZTP + print('[%s] --> Option 43 (Vendor-specific option):' % client) + print('[%s] --> Suboption 0: %s' % (client, '/junos/' + junos_file)) + print('[%s] --> Suboption 1: %s' % (client, '/tg15-edge/' + lease_details['hostname'])) + print('[%s] --> Suboption 3: %s' % (client, 'http')) - # Set option 3 - default gateway. Only applicable if messagesplit[10] (DHCP forwarder (GIADDR)) is set - if messagesplit[10] == b'00000000': - data += craft_option(3).bytes(socket.inet_aton(address)) # Option 3 - Default gateway (set to DHCP servers IP) - print(' --> Option 3 (Default gateway): %s' % address) - else: - data += craft_option(3).bytes(messagesplit[10]) # Option 3 - Default gateway (set to DHCP forwarders IP) - data += craft_option(150).bytes(socket.inet_aton(address)) # Option 150 - TFTP Server - data += craft_option(43).bytes(craft_option(1).string('/tg15-edge/' + lease_details['hostname']) + craft_option(3).string('http')) # Option 43 - ZTP - print(' --> Option 43 (Vendor-specific option):') - print(' --> Suboption 1: %s' % '/tg15-edge/' + lease_details['hostname']) - print(' --> Suboption 3: %s' % 'http') # data += '\x03\x04' + option82_raw # Option 82 - with suboptions + data += b'\xff' - return data if __name__ == "__main__": @@ -263,7 +298,8 @@ if __name__ == "__main__": netmask = '255.255.255.0' tftp = address gateway = address - leasetime=86400 #int + leasetime = 86400 #int + junos_file = 'jinstall-ex-2200-12.3R6.6-domestic-signed.tgz' # Setting up the server, and how it will communicate s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # IPv4 UDP socket @@ -274,20 +310,20 @@ if __name__ == "__main__": # Starting the whole loop print('Starting main loop') - while 1: #main loop + while True: #main loop try: message, addressf = s.recvfrom(8192) # print(message) if message.startswith(b'\x01'): # UDP payload is DHCP request (discover, request, release) if addressf[0] == '0.0.0.0': - print('DHCP broadcast') + print('[%s] DHCP broadcast' % client) reply_to = '<broadcast>' else: - print('DHCP unicast - DHCP forwarding') + print('[%s] DHCP unicast - DHCP forwarding' % client) reply_to = addressf[0] data=reqparse(message) # Parse the DHCP request if data: - print(' --> replying to %s' % reply_to) + print('[%s] --> replying to %s' % (client, reply_to)) # print(b'replying with UDP payload: ' + data) s.sendto(data, ('<broadcast>', 68)) # Sends reply except KeyboardInterrupt: |