diff options
Diffstat (limited to 'junos-bootstrap/dhcpd/server_dhcp.py')
-rw-r--r-- | junos-bootstrap/dhcpd/server_dhcp.py | 433 |
1 files changed, 235 insertions, 198 deletions
diff --git a/junos-bootstrap/dhcpd/server_dhcp.py b/junos-bootstrap/dhcpd/server_dhcp.py index 7d7bfc0..8681a18 100644 --- a/junos-bootstrap/dhcpd/server_dhcp.py +++ b/junos-bootstrap/dhcpd/server_dhcp.py @@ -1,141 +1,228 @@ -#!/usr/bin/env python +#!/usr/bin/python +# -*- coding: utf-8 -*- -#dhcpd.py pure python dhcp server pxe capable -#psychomario - https://github.com/psychomario +# server_dhcp.py by Jonas "j" Lindstad for The Gathering tech:server 2015 +# Used to configure the Juniper EX2200 edge switches with Zero Touch Protocol +# License: GPLv2 +# Copyed/influcenced by the work of psychomario - https://github.com/psychomario import socket,binascii,time,IN from sys import exit from optparse import OptionParser + if not hasattr(IN,"SO_BINDTODEVICE"): IN.SO_BINDTODEVICE = 25 #http://stackoverflow.com/a/8437870/541038 + +options_raw = {} + +# Length of DHCP fields in octets, and their placement in packet. +# Ref: http://4.bp.blogspot.com/-IyYoFjAC4l8/UXuo16a3sII/AAAAAAAAAXQ/b6BojbYXoXg/s1600/DHCPTitle.JPG +# 0 OP - 1 +# 1 HTYPE - 1 +# 2 HLEN - 1 +# 3 HOPS - 1 +# 4 XID - 4 +# 5 SECS - 2 +# 6 FLAGS - 2 +# 7 CIADDR - 4 +# 8 YIADDR - 4 +# 9 SIADDR - 4 +# 10 GIADDR - 4 +# 11 CHADDR - 6 +# 12 MAGIC COOKIE - 10 +# 13 PADDING - 192 octets of 0's +# 14 MAGIC COOKIE - 4 +# 15 OPTIONS - variable length + +############# +# FUNCTIONS # +############# + def slicendice(msg,slices): #generator for each of the dhcp fields + # slicendice(message,dhcpfields) for x in slices: - if str(type(x)) == "<type 'str'>": x=eval(x) #really dirty, deals with variable length options + # if str(type(x)) == "<type 'str'>": x=eval(x) #really dirty, deals with variable length options + # print(x) + # print(msg) yield msg[:x] msg = msg[x:] -# Convert hex IP to string with formated decimal IP +# Splits a chunk of hex into a list of hex. (0123456789abcdef => ['01', '23', '45', '67', '89', 'ab', 'cd', 'ef']) +def chunk_hex(hex): + # return [hex[i:i+2].decode('utf-8') for i in range(0, len(hex), 2)] + return [hex[i:i+2] for i in range(0, len(hex), 2)] + +# Convert hex IP to string with formated decimal IP. (0a0000ff => 10.0.0.255) def hex_ip_to_str(hex_ip): - hex_pieces = [hex_ip[i:i+2] for i in range(0, len(hex_ip), 2)] # split hex string into hex chunks - bin_pieces = map(lambda x: int(x, 16), hex_pieces) # convert from hex to dec - return '.'.join(str(y) for y in bin_pieces) # cast int to str for join + return '.'.join(str(y) for y in map(lambda x: int(x, 16), chunk_hex(hex_ip))) # cast int to str for join +# formats a MAC address in the format "b827eb9a520f" to "b8:27:eb:9a:52:0f" def format_hex_mac(hex_mac): - hex_pieces = [hex_mac[i:i+2] for i in range(0, len(hex_mac), 2)] - return ':'.join(str(x) for x in hex_pieces) - -# Splits a chunk of hex into a list of hex. e.g. 0123456789abcdef = ['01', '23', '45', '67', '89', 'ab', 'cd', 'ef'] -def chunk_hex(hex): - return [hex[i:i+2] for i in range(0, len(hex), 2)] + return ':'.join(str(x) for x in chunk_hex(hex_mac)) + +# Parses DHCP options - raw = hex options def parse_options(raw): print(' --> processing DHCP options') - raw = '3501013d13ffeb9a520f000100011c1b8324b827eb9a520f5000390205dc3c306468637063642d362e362e373a4c696e75782d332e31322e33352d312d415243483a61726d76366c3a42434d323730380c07616c61726d7069910101370e01792103060c0f1c2a33363a3b775223012164697374726f2d746573743a67652d302f302f31322e303a626f6f747374726170' - raw = '3501013c3c4a756e697065722d6578323230302d632d3132742d3267000000000000000000000000000000000000000000000000000000000000000000000000005222012064697374726f2d746573743a67652d302f302f302e303a626f6f747374726170ff' + # print(type(raw)) + # raw = '3501013c3c4a756e697065722d6578323230302d632d3132742d3267000000000000000000000000000000000000000000000000000000000000000000000000005222012064697374726f2d746573743a67652d302f302f302e303a626f6f747374726170ff' chunked = chunk_hex(raw) + print(chunked) chunked_length = len(chunked) - next = 0 # counter - next option start + pointer = 0 # counter - next option start options = {} # options dataset - while True: - option = int(chunked[next], 16) # convert to dec - length = int(chunked[next+1], 16) # convert to dec - - options[option] = raw[((next+2)*2):((next+length+2)*2)].decode("hex") # getting the options - - next = next + length + 2 # length of option + length field (1 chunk) + option ID (1 chunk) - print(' --> option: %s (length: %s) (next option starting on chunk %s)' % (option, length, next)) + global options_raw + options_raw = {} + special_options = [53, 82] - if option is 82: - parse_option_82(binascii.hexlify(options[option])) # TODO: "ugly as fuck want to go to bed-hax" must be fixed before 02:21 AM - - if int(chunked[next], 16) == 255: - print(' --> finished processing options') + while True: + # print(chunked[pointer]) + option = int(chunked[pointer], 16) # option ID (0 => 255) + code = int(chunked[pointer], 16) # option code (0 => 255) + length = int(chunked[pointer+1], 16) # option length + option_payload = raw[((pointer+2)*2):((pointer+length+2)*2)] # Contains the payload of the option - without option ID and length + options_raw[code] = option_payload + ''' + # converts payload to ASCII and strips spaces in both ends, and removes repeating 0000s in the end of the string. + asciivalue = binascii.hexlify(option_payload.decode("hex").strip()).rstrip('0') + if len(asciivalue) % 2 == 1: + asciivalue = asciivalue + "0" + asciivalue = binascii.unhexlify(asciivalue) + ''' + asciivalue = binascii.unhexlify(option_payload) # should not contain unreadable characters + # print('option_payload:') + # print(option_payload) + # print('asciivalue:') + # print(asciivalue) + + if option in special_options: + if option is 82: + global option82_raw + option82_raw = option_payload + options[option] = parse_suboptions(option, option_payload) + 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)')) + else: + print(' --> option: %s: %s' % (option, asciivalue)) + + else: + options[option] = asciivalue + print(' --> option: %s: "%s"' % (option, asciivalue)) + + pointer = pointer + length + 2 # length of option + length field (1 chunk) + option ID (1 chunk) + if int(chunked[pointer], 16) is 255: # end of DHCP options + print(' --> finished processing options') break return options -def parse_option_82(raw): - print(' --> processing hook for option 82') - # print('RAW:', raw) +def parse_suboptions(option, raw): + print(' --> processing hook for option %s' % option) chunked = chunk_hex(raw) chunked_length = len(chunked) - if int(chunked[0], 16) is 1: # suboption 1 - subopt_1_length = int(chunked[2], 16) - print(' --> suboption 1 found - value: "%s"' % raw[2:(subopt_1_length+2)].decode("hex")) + dataset = {} + if int(chunked[0], 16) is 1: # suboption 1 - loop over suboptions + while True: + subopt_length = int(chunked[2], 16) + value = raw[2:(subopt_length+2)].strip() + print(' --> suboption 1 found - value: "%s"' % value) + dataset[int(chunked[0], 16)] = value + break; + return dataset def reqparse(message): #handles either DHCPDiscover or DHCPRequest - #using info from http://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Protocol - #the tables titled DHCPDISCOVER and DHCPOFFER - data=None - # Length of DHCP fields in octets, and their placement in packet. - # Ref: http://4.bp.blogspot.com/-IyYoFjAC4l8/UXuo16a3sII/AAAAAAAAAXQ/b6BojbYXoXg/s1600/DHCPTitle.JPG - # OP - 1 - # HTYPE - 1 - # HLEN - 1 - # HOPS - 1 - # XID - 4 - # SECS - 2 - # FLAGS - 2 - # CIADDR - 4 - # YIADDR - 4 - # SIADDR - 4 - # GIADDR - 4 - # CHADDR - ????? - # PADDING - 192 octets of 0's - # MAGIC COOKIE - 4 - # OPTIONS - Magic goes here - - - dhcpfields=[1,1,1,1,4,2,2,4,4,4,4,6,10,192,4,"msg.rfind('\xff')",1,None] + # dhcp_option_length = message.rfind(b'\xff') + # dhcpfields=[1,1,1,1,4,2,2,4,4,4,4,6,10,192,4,"msg.rfind('\xff')",1,None] + dhcpfields=[1,1,1,1,4,2,2,4,4,4,4,6,10,192,4,message.rfind(b'\xff'),1] #send: boolean as to whether to send data back, and data: data to send, if any #print len(message) hexmessage=binascii.hexlify(message) messagesplit=[binascii.hexlify(x) for x in slicendice(message,dhcpfields)] + print(messagesplit) # print(messagesplit) - dhcpopt=messagesplit[15][:6] #hope DHCP type is first. Should be. - if dhcpopt == '350101': # option 53 - identifies DHCP packet type - discover/request/offer/ack++ - print('DHCP Discover') - if(int(dhcpfields[10]) is not 0): + # dhcpopt=messagesplit[15][:6] # Checs first option, which should be DHCP type + if messagesplit[15][:6] == b'350101': # option 53 - identifies DHCP packet type - discover/request/offer/ack++ + print('\n\nDHCP DISCOVER - client MAC %s' % format_hex_mac(messagesplit[11])) + if int(messagesplit[10]) is not 0: print(' --> Relay: %s' % hex_ip_to_str(messagesplit[10])) - print(' --> Client MAC: %s' % format_hex_mac(messagesplit[11])) - # print('DHCP Options: ', messagesplit[15]) - options = parse_options('x') + else: + print(' --> Relay: none - direct request') + # options = parse_options('x') + options = parse_options(messagesplit[15]) # print(options) - print(' --> crafting response') - # print('parse_options: ', parse_options('x')) - #DHCPDiscover - #craft DHCPOffer - #DHCPOFFER creation: + + option43 = { + 'length': hex(30), + 'value': '01162f746731352d656467652f746573742e636f6e6669670304687474709' + } + + # + # Crafting DHCP OFFER + # + # {82: {1: 'distro-test:ge-0/0/0.0:bootstrap'}, 60: 'Juniper-ex2200-c-12t-2g', 53: 1} #options = \xcode \xlength \xdata - lease=getlease(messagesplit[11]) - print 'Leased:',lease - data='\x02\x01\x06\x00'+binascii.unhexlify(messagesplit[4])+'\x00\x04' - data+='\x80\x00'+'\x00'*4+socket.inet_aton(lease) - data+=socket.inet_aton(address)+'\x00'*4 - data+=binascii.unhexlify(messagesplit[11])+'\x00'*10+'\x00'*192 - data+='\x63\x82\x53\x63'+'\x35\x01\x02'+'\x01\x04' - data+=socket.inet_aton(netmask)+'\x36\x04'+socket.inet_aton(address) - data+='\x1c\x04'+socket.inet_aton(broadcast)+'\x03\x04' - data+=socket.inet_aton(gateway)+'\x06\x04'+socket.inet_aton(dns) - data+='\x33\x04'+binascii.unhexlify(hex(leasetime)[2:].rjust(8,'0')) - data+='\x42'+binascii.unhexlify(hex(len(tftp))[2:].rjust(2,'0'))+tftp - data+='\x43'+binascii.unhexlify(hex(len(pxefilename)+1)[2:].rjust(2,'0')) - data+=pxefilename+'\x00\xff' - elif dhcpopt == '350103': - print('DHCP ACK') - #DHCPRequest - #craft DHCPACK - data='\x02\x01\x06\x00'+binascii.unhexlify(messagesplit[4])+'\x00'*8 - data+=binascii.unhexlify(messagesplit[15][messagesplit[15].find('3204')+4:messagesplit[15].find('3204')+12]) - data+=socket.inet_aton(address)+'\x00'*4 - data+=binascii.unhexlify(messagesplit[11])+'\x00'*202 - data+='\x63\x82\x53\x63'+'\x35\x01\05'+'\x36\x04'+socket.inet_aton(address) - data+='\x01\x04'+socket.inet_aton(netmask)+'\x03\x04' - data+=socket.inet_aton(address)+'\x33\x04' - data+=binascii.unhexlify(hex(leasetime)[2:].rjust(8,'0')) - data+='\x42'+binascii.unhexlify(hex(len(tftp))[2:].rjust(2,'0')) - data+=tftp+'\x43'+binascii.unhexlify(hex(len(pxefilename)+1)[2:].rjust(2,'0')) - data+=pxefilename+'\x00\xff' + print(' --> crafting response') + lease=getlease(messagesplit[11].decode()) # Decodes MAC address + # print(binascii.unhexlify(messagesplit[4])) + # print('length: ' + str(len(binascii.unhexlify(messagesplit[4])))); + + # 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) # 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 + + # DHCP Options - ordered by pcapng "proof of concept" file + data += b'\x35\x01\x02' # Option 53 - DHCP OFFER + data += b'\x36\x04' + socket.inet_aton(address) # Option 54 - DHCP server identifier + data += b'\x33\x04' + binascii.unhexlify(b'00012340') # Option 51 - Lease time left padded with "0" + data += b'\x01\x04' + socket.inet_aton(netmask) # Option 1 - Subnet mask + data += b'\x03\x04' + binascii.unhexlify(messagesplit[10]) # Option 3 - Router (set to DHCP forwarders IP) + data += b'\x96\x04' + socket.inet_aton(address) # Option 150 - TFTP Server + # data += '\x2b' + option43['length'] + option43['value'] # Option 43 - Magic ZTP stuff + # data += '\x03\x04' + option82_raw # Option 82 - with suboptions + data += b'\xff' + + elif messagesplit[15][:6] == b'350103': + print('\n\nDHCP REQUEST - client MAC %s' % format_hex_mac(messagesplit[11])) + print(' --> crafting response') + + 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 += binascii.unhexlify(messagesplit[15][messagesplit[15].find('3204')+4:messagesplit[15].find('3204')+12]) + data += binascii.unhexlify(messagesplit[8]) # 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 + + # DHCP Options - ordered by pcapng "proof of concept" file + data += b'\x35\x01\05' # Option 53 - DHCP ACK + data += b'\x36\x04' + socket.inet_aton(address) # Option 54 - DHCP server identifier + data += b'\x33\x04' + binascii.unhexlify(b'00012340') # Option 51 - Lease time left padded with "0" + data += b'\x01\x04' + socket.inet_aton(netmask) # Option 1 - Subnet mask + data += b'\x03\x04' + binascii.unhexlify(messagesplit[10]) # Option 3 - Router (set to DHCP forwarders IP) + data += b'\x96\x04' + socket.inet_aton(address) # Option 150 - TFTP Server + data += b'\xff' return data def release(): #release a lease after timelimit has expired @@ -144,7 +231,7 @@ def release(): #release a lease after timelimit has expired if time.time()+leasetime == leasetime: continue if lease[-1] > time.time()+leasetime: - print "Released",lease[0] + print("Released" + lease[0]) lease[1]=False lease[2]='000000000000' lease[3]=0 @@ -162,111 +249,61 @@ def getlease(hwaddr): #return the lease of mac address, or create if doesn't exi return lease[0] if __name__ == "__main__": - parser = OptionParser(description='%prog - a simple DHCP server', usage='%prog [options]') - parser.add_option("-a", "--address", dest="address", action="store", help='server ip address (required).') - parser.add_option("-i", "--interface", dest="interface", action="store", help='network interface to use (default all interfaces).') - parser.add_option("-p", "--port", dest="port", action="store", help='server port to bind (default 67).') - parser.add_option("-f", "--from", dest="offerfrom", action="store", help='ip pool from (default x.x.x.100).') - parser.add_option("-t", "--to", dest="offerto", action="store", help='ip pool to (default x.x.x.150).') - parser.add_option("-b", "--broadcast", dest="broadcast", action="store", help='broadcast ip to reply (x.x.x.254).') - parser.add_option("-n", "--netmask", dest="netmask", action="store", help='netmask (default 255.255.255.0).') - parser.add_option("-s", "--tftp", dest="tftp", action="store", help='tftp ip address (default ip address provided).') - parser.add_option("-d", "--dns", dest="dns", action="store", help='dns ip address (default 8.8.8.8).') - parser.add_option("-g", "--gateway", dest="gateway", action="store", help='gateway ip address (default ip address provided).') - parser.add_option("-x", "--pxefilename", dest="pxefilename", action="store", help='pxe filename (default pxelinux.0).') - - (options, args) = parser.parse_args() - - if not (args or options.address): - parser.print_help() - exit(1) - - if options.interface: - interface = options.interface - else: - interface = '' # Symbolic name meaning all available interfaces - - if options.port: - port = options.port - else: - port = '67' - port = int(port) - - if options.address: - address = options.address - elements_in_address = address.split('.') - if len(elements_in_address) != 4: - sys.exit(os.path.basename(__file__) + ": invalid ip address") - else: - exit(1) - - if options.offerfrom: - offerfrom = options.offerfrom - else: - offerfrom = '.'.join(elements_in_address[0:3]) - offerfrom = offerfrom + '.100' - - if options.offerto: - offerto = options.offerto - else: - offerto = '.'.join(elements_in_address[0:3]) - offerto = offerto + '.150' - - if options.broadcast: - broadcast = options.broadcast - else: - broadcast = '.'.join(elements_in_address[0:3]) - broadcast = broadcast + '.254' - - if options.netmask: - netmask = options.netmask - else: - netmask = '255.255.255.0' - - if options.tftp: - tftp = options.tftp - else: - tftp = address - - if options.dns: - dns = options.dns - else: - dns = '8.8.8.8' - - if options.gateway: - gateway = options.gateway - else: - gateway = address - - if options.pxefilename: - pxefilename = options.pxefilename - else: - pxefilename = 'pxelinux.0' - + interface = 'eth0' + port = 67 + address = '10.0.100.2' + offerfrom = '10.0.0.100' + offerto = '10.0.0.150' + broadcast = '10.0.0.255' + netmask = '255.255.255.0' + tftp = address + dns = '8.8.8.8' + gateway = address + pxefilename = 'pxelinux.0' leasetime=86400 #int - leases=[] + leases=[] # leases database #next line creates the (blank) leases table. This probably isn't necessary. - for ip in ['.'.join(elements_in_address[0:3])+'.'+str(x) for x in range(int(offerfrom[offerfrom.rfind('.')+1:]),int(offerto[offerto.rfind('.')+1:])+1)]: - leases.append([ip,False,'000000000000',0]) - - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.setsockopt(socket.SOL_SOCKET,IN.SO_BINDTODEVICE,interface+'\0') #experimental + # for ip in ['.'.join(elements_in_address[0:3])+'.'+str(x) for x in range(int(offerfrom[offerfrom.rfind('.')+1:]),int(offerto[offerto.rfind('.')+1:])+1)]: + for octet in range(50): + leases.append(['10.0.0.' + str(octet), False, '000000000000', 0]) + # leases.append([ip,False,'000000000000',0]) + + # TODO: Support for binding to interface / IP + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # IPv4 UDP socket + # python 2.7: s.setsockopt(socket.SOL_SOCKET,IN.SO_BINDTODEVICE,interface+'\0') #experimental + # s.setsockopt(socket.SOL_SOCKET, IN.SO_BINDTODEVICE, interface+'\0') #experimental + # s.bind(address) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - s.bind(('',port)) - #s.sendto(data,(ip,port)) + s.setsockopt(socket.SOL_SOCKET, 25, b'eth0') + s.bind(('', 67)) + print('starting main loop') while 1: #main loop try: message, addressf = s.recvfrom(8192) - if not message.startswith('\x01') and not addressf[0] == '0.0.0.0': - continue #only serve if a dhcp request - data=reqparse(message) #handle request - if data: - s.sendto(data,('<broadcast>',68)) #reply - release() #update releases table + print('received something!') + 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') + reply_to = '<broadcast>' + else: + print('DHCP unicast - DHCP forwarding') + reply_to = addressf[0] + # print(message.decode('ISO-8859-1')) + data=reqparse(message) # Parse the DHCP request + if data: + # print(options_raw) + # data = str.encode(data) + print(' -- > replying to %s' % reply_to) + print(b'replying with UDP payload: ' + data) + s.sendto(data, ('<broadcast>', 68)) # Sends reply + # s.sendto(data,(reply_to,68)) # Sends reply + release() #update releases table + else: + print('not DHCP') except KeyboardInterrupt: exit() - # except: - # continue |