aboutsummaryrefslogtreecommitdiffstats
path: root/junos-bootstrap
diff options
context:
space:
mode:
Diffstat (limited to 'junos-bootstrap')
-rw-r--r--junos-bootstrap/dhcpd/module_craft_option.py79
-rw-r--r--junos-bootstrap/dhcpd/server_dhcp.py191
2 files changed, 160 insertions, 110 deletions
diff --git a/junos-bootstrap/dhcpd/module_craft_option.py b/junos-bootstrap/dhcpd/module_craft_option.py
new file mode 100644
index 0000000..35e7328
--- /dev/null
+++ b/junos-bootstrap/dhcpd/module_craft_option.py
@@ -0,0 +1,79 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+'''
+ Created by Jonas 'j' Lindstad for The Gathering 2015
+ License: GPLv3
+
+ Class used to craft byte hex encoded DHCP options
+
+ NB: No direct support for suboptions. Should be possible to craft suboptions as
+ options, and inject them with craft_option(<option>).raw_hes(<conconcatenated options>)
+
+ Usage examples:
+ craft_option.debug = True
+ print(craft_option(1).string('vg.no'))
+ print(craft_option(2).bytes(b'abcd'))
+ print(craft_option(3).bytes(socket.inet_aton('192.168.0.55')))
+ print(craft_option(4).bytes(b'\xde\xad\xbe\xef\xfe\xed'))
+ print(craft_option(5).raw_hex(b'\x72\x78'))
+ print(craft_option(6).ip('255.255.128.0'))
+'''
+
+from binascii import hexlify, unhexlify
+
+class craft_option(object):
+ # content = b'' # content will be stored as hex values like hex(10) + hex(255) = 0aff
+ debug = False
+ def __init__(self, code):
+ self.code = self.__int_to_pad_byte(code)
+
+ # Works as intended
+ # internal function. Converts int(3) to str('03'), int('11') to str('0b'), int(255) to str('ff')
+ def __int_to_pad_byte(self, integer):
+ return hex(integer).split('x')[1].rjust(2, '0').encode()
+
+ # Works as intended
+ def string(self, string):
+ self.method = 'string'
+ self.content = hexlify(string.encode())
+ return self.process()
+
+ # Works as intended
+ def bytes(self, bytes):
+ self.method = 'bytes'
+ self.content = hexlify(bytes)
+ return self.process()
+
+ # Works as intended
+ # str('10.20.30.40') to b'\x10\x20\x30\x40'
+ def ip(self, ip):
+ self.method = 'ip'
+ self.content = ''.join([hex(int(i))[2:].rjust(2, '0') for i in ip.split('.')]).encode()
+ return self.process()
+
+ # Works as intended
+ # string like '\x72\x78' for 'rx'
+ def raw_hex(self, raw_hex):
+ self.method = 'raw_hex'
+ self.content = hexlify(raw_hex)
+ return self.process()
+
+
+
+ # TODO Does not work as intended
+ # int(666) to b'\x02\x9A'
+ def integer(self, integer):
+ self.method = 'integer'
+ self.content = ''.join([hex(int(i))[2:].rjust(2, '0') for i in ip.split('.')])
+ return self.process()
+
+ def process(self):
+ length = self.__int_to_pad_byte(len(unhexlify(self.content)))
+ if self.debug is True:
+ print('----------')
+ print(self.method + '():')
+ print(self.code + length)
+ print(b'content: ' + self.content)
+ print(unhexlify(self.content))
+ return unhexlify(self.code + length + self.content)
diff --git a/junos-bootstrap/dhcpd/server_dhcp.py b/junos-bootstrap/dhcpd/server_dhcp.py
index 8681a18..6d9afd3 100644
--- a/junos-bootstrap/dhcpd/server_dhcp.py
+++ b/junos-bootstrap/dhcpd/server_dhcp.py
@@ -4,16 +4,17 @@
# 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
+# Based on the work of psychomario - https://github.com/psychomario
-import socket,binascii,time,IN
-from sys import exit
-from optparse import OptionParser
+import socket, binascii, time, IN, sys
+from module_craft_option import craft_option
+# 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 = {}
+options_raw = {} # TODO - not a nice way to do things
# Length of DHCP fields in octets, and their placement in packet.
# Ref: http://4.bp.blogspot.com/-IyYoFjAC4l8/UXuo16a3sII/AAAAAAAAAXQ/b6BojbYXoXg/s1600/DHCPTitle.JPG
@@ -38,18 +39,14 @@ options_raw = {}
# FUNCTIONS #
#############
-def slicendice(msg,slices): #generator for each of the dhcp fields
- # slicendice(message,dhcpfields)
+#generator for each of the dhcp fields
+def slicendice(msg,slices):
for x in slices:
- # 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:]
# 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)
@@ -58,46 +55,32 @@ def hex_ip_to_str(hex_ip):
# formats a MAC address in the format "b827eb9a520f" to "b8:27:eb:9a:52:0f"
def format_hex_mac(hex_mac):
- return ':'.join(str(x) for x in chunk_hex(hex_mac))
-
+ 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')
- # print(type(raw))
- # raw = '3501013c3c4a756e697065722d6578323230302d632d3132742d3267000000000000000000000000000000000000000000000000000000000000000000000000005222012064697374726f2d746573743a67652d302f302f302e303a626f6f747374726170ff'
chunked = chunk_hex(raw)
- print(chunked)
chunked_length = len(chunked)
pointer = 0 # counter - next option start
options = {} # options dataset
- global options_raw
- options_raw = {}
+
+ global options_raw
+ options_raw = {} # incomming request's options
special_options = [53, 82]
while True:
- # print(chunked[pointer])
option = int(chunked[pointer], 16) # option ID (0 => 255)
- code = int(chunked[pointer], 16) # option code (0 => 255)
+ code = int(chunked[pointer], 16) # option code (0 => 255) # New int for options' ID with correct name. Replaces $option
+
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)
- '''
+ options_raw[code] = option_payload # copying incomming request's options, directly usable in outgoing replies
+
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:
@@ -109,65 +92,60 @@ def parse_options(raw):
else:
options[option] = asciivalue
- print(' --> option: %s: "%s"' % (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
+ 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')
break
return options
+# Parses suboptions
def parse_suboptions(option, raw):
- print(' --> processing hook for option %s' % option)
+ print(' --> processing suboption hook for option %s' % option)
chunked = chunk_hex(raw)
chunked_length = len(chunked)
+ pointer = 0 # counter - next option start
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;
+ while True:
+ length = int(chunked[pointer+1], 16) # option length
+ value = raw[2:(length+2)].strip()
+ print(' --> suboption %s found - value: "%s"' % (int(chunked[0], 16), value))
+ 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)
+ break
return dataset
-def reqparse(message): #handles either DHCPDiscover or DHCPRequest
+# Parses and handles DHCP DISCOVER or DHCP REQUEST
+def reqparse(message):
data=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] # 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]))
- else:
- print(' --> Relay: none - direct request')
- # options = parse_options('x')
- options = parse_options(messagesplit[15])
- # print(options)
-
- option43 = {
- 'length': hex(30),
- 'value': '01162f746731352d656467652f746573742e636f6e6669670304687474709'
- }
+
+ # hard coded option 43 - for testing purposes
+ option43 = {
+ 'length': hex(30),
+ 'value': '01162f746731352d656467652f746573742e636f6e666967030468747470'
+ }
+
+ # Test parsing
+ options = parse_options(b'3501013c3c4a756e697065722d6578323230302d632d3132742d3267000000000000000000000000000000000000000000000000000000000000000000000000005222012064697374726f2d746573743a67652d302f302f302e303a626f6f747374726170ff')
+
+ if int(messagesplit[10]) is not 0:
+ print('DHCP packet forwarded by relay %s' % hex_ip_to_str(messagesplit[10]))
+ else:
+ print('DHCP packet not forwarded - direct request')
- #
- # 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
- print(' --> crafting response')
- lease=getlease(messagesplit[11].decode()) # Decodes MAC address
- # print(binascii.unhexlify(messagesplit[4]))
- # print('length: ' + str(len(binascii.unhexlify(messagesplit[4]))));
+ 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' % format_hex_mac(messagesplit[11]))
+ print(' --> crafting DHCP OFFER response')
+ lease = getlease(messagesplit[11].decode()) # Decodes MAC address
+
# DHCP OFFER details - Options
data = b'\x02' # Message type - boot reply
data += b'\x01' # Hardware type - ethernet
@@ -185,19 +163,11 @@ def reqparse(message): #handles either DHCPDiscover or DHCPRequest
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'
+ 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' % format_hex_mac(messagesplit[11]))
- print(' --> crafting response')
+ print(' --> crafting DHCP ACK response')
data = b'\x02' # Message type - boot reply
data += b'\x01' # Hardware type - ethernet
@@ -207,7 +177,6 @@ def reqparse(message): #handles either DHCPDiscover or DHCPRequest
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
@@ -217,12 +186,27 @@ def reqparse(message): #handles either DHCPDiscover or DHCPRequest
# 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'
+ else:
+ print('Unexpected DHCP option 53 - stopping processing request')
+ return None
+
+
+ # common options for both DHCP REPLY and DHCP ACK - should be most of the options
+ data += craft_option(54).bytes(socket.inet_aton(address)) # Option 54 - DHCP server identifier
+ data += craft_option(51).raw_hex(b'\x00\x00\xff\x00') # Option 51 - Lease time left padded with "0"
+ data += craft_option(1).ip(netmask) # Option 1 - Subnet mask
+
+ # Set option 3 - default gateway. Only applicable if messagesplit[10] (DHCP forwarder (GIADDR)) is set
+ if messagesplit[10] is not b'00000000':
+ data += craft_option(3).bytes(messagesplit[10]) # Option 3 - Default gateway (set to DHCP forwarders IP)
+ else:
+ data += craft_option(3).bytes(socket.inet_aton(address)) # Option 3 - Default gateway (set to DHCP servers IP)
+
+ data += craft_option(43).raw_hex(binascii.unhexlify(option43['value'])) # Option 43 - ZTP
+ data += craft_option(150).bytes(socket.inet_aton(address)) # Option 150 - TFTP Server
+ # data += '\x03\x04' + option82_raw # Option 82 - with suboptions
+ data += b'\xff'
+
return data
def release(): #release a lease after timelimit has expired
@@ -249,7 +233,7 @@ def getlease(hwaddr): #return the lease of mac address, or create if doesn't exi
return lease[0]
if __name__ == "__main__":
- interface = 'eth0'
+ interface = b'eth0'
port = 67
address = '10.0.100.2'
offerfrom = '10.0.0.100'
@@ -259,7 +243,6 @@ if __name__ == "__main__":
tftp = address
dns = '8.8.8.8'
gateway = address
- pxefilename = 'pxelinux.0'
leasetime=86400 #int
leases=[] # leases database
@@ -269,23 +252,17 @@ if __name__ == "__main__":
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.setsockopt(socket.SOL_SOCKET, 25, b'eth0')
+ s.setsockopt(socket.SOL_SOCKET, 25, interface)
s.bind(('', 67))
print('starting main loop')
while 1: #main loop
try:
message, addressf = s.recvfrom(8192)
- print('received something!')
- print(message)
-
+ # 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')
@@ -293,17 +270,11 @@ if __name__ == "__main__":
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)
+ # 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')
+ release() # update releases table
except KeyboardInterrupt:
exit()