diff options
-rw-r--r-- | junos-bootstrap/dhcpd/server_dhcp.py | 272 | ||||
-rw-r--r-- | junos-bootstrap/dhcpd/uninteresting_clidump.log | 14 |
2 files changed, 286 insertions, 0 deletions
diff --git a/junos-bootstrap/dhcpd/server_dhcp.py b/junos-bootstrap/dhcpd/server_dhcp.py new file mode 100644 index 0000000..7d7bfc0 --- /dev/null +++ b/junos-bootstrap/dhcpd/server_dhcp.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python + +#dhcpd.py pure python dhcp server pxe capable +#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 +def slicendice(msg,slices): #generator for each of the dhcp fields + for x in slices: + if str(type(x)) == "<type 'str'>": x=eval(x) #really dirty, deals with variable length options + yield msg[:x] + msg = msg[x:] + +# Convert hex IP to string with formated decimal IP +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 + +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)] + +def parse_options(raw): + print(' --> processing DHCP options') + raw = '3501013d13ffeb9a520f000100011c1b8324b827eb9a520f5000390205dc3c306468637063642d362e362e373a4c696e75782d332e31322e33352d312d415243483a61726d76366c3a42434d323730380c07616c61726d7069910101370e01792103060c0f1c2a33363a3b775223012164697374726f2d746573743a67652d302f302f31322e303a626f6f747374726170' + raw = '3501013c3c4a756e697065722d6578323230302d632d3132742d3267000000000000000000000000000000000000000000000000000000000000000000000000005222012064697374726f2d746573743a67652d302f302f302e303a626f6f747374726170ff' + chunked = chunk_hex(raw) + chunked_length = len(chunked) + next = 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)) + + 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') + break + return options + +def parse_option_82(raw): + print(' --> processing hook for option 82') + # print('RAW:', raw) + 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")) + +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] + #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) + 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): + 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') + # print(options) + print(' --> crafting response') + # print('parse_options: ', parse_options('x')) + #DHCPDiscover + #craft DHCPOffer + #DHCPOFFER creation: + #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' + return data + +def release(): #release a lease after timelimit has expired + for lease in leases: + if not lease[1]: + if time.time()+leasetime == leasetime: + continue + if lease[-1] > time.time()+leasetime: + print "Released",lease[0] + lease[1]=False + lease[2]='000000000000' + lease[3]=0 + +def getlease(hwaddr): #return the lease of mac address, or create if doesn't exist + global leases + for lease in leases: + if hwaddr == lease[2]: + return lease[0] + for lease in leases: + if not lease[1]: + lease[1]=True + lease[2]=hwaddr + lease[3]=time.time() + 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' + + leasetime=86400 #int + + leases=[] + #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 + 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)) + + 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 + except KeyboardInterrupt: + exit() + # except: + # continue diff --git a/junos-bootstrap/dhcpd/uninteresting_clidump.log b/junos-bootstrap/dhcpd/uninteresting_clidump.log new file mode 100644 index 0000000..439796a --- /dev/null +++ b/junos-bootstrap/dhcpd/uninteresting_clidump.log @@ -0,0 +1,14 @@ +j@lappie:~/git/tgmanage$ sudo python2 junos-bootstrap/dhcpd/server_dhcp.py -a 10.0.100.2 +DHCP Discover + --> Relay: 0.0.0.0 + --> Client MAC: b8:27:eb:9a:52:0f + --> processing DHCP options + --> option: 53 (length: 1) (next option starting on chunk 3) + --> option: 60 (length: 60) (next option starting on chunk 65) + --> option: 82 (length: 34) (next option starting on chunk 101) + --> processing hook for option 82 + --> suboption 1 found - value: " distro-test:ge-0/0/0.0:bootstrap" + --> finished processing options + --> crafting response +Leased: 10.0.100.100 + |