aboutsummaryrefslogtreecommitdiffstats
path: root/junos-bootstrap/dhcpd/server_dhcp.py
diff options
context:
space:
mode:
authorJonas Lindstad <jonaslindstad@gmail.com>2015-01-30 02:30:59 +0100
committerJonas Lindstad <jonaslindstad@gmail.com>2015-01-30 02:30:59 +0100
commitd502e89535a766d103e0a5c7462aae0dc57f20cd (patch)
treef5e69f09a3316770696d3b6ca77329defafdcc1f /junos-bootstrap/dhcpd/server_dhcp.py
parent9f515b2ec00c36aab68845b4495165d560b6da2c (diff)
Started playing around parsing DHCP packets. Nothing usable/interesting yet
Diffstat (limited to 'junos-bootstrap/dhcpd/server_dhcp.py')
-rw-r--r--junos-bootstrap/dhcpd/server_dhcp.py272
1 files changed, 272 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