aboutsummaryrefslogtreecommitdiffstats
path: root/fap
diff options
context:
space:
mode:
authorJonas Lindstad <jonaslindstad@gmail.com>2015-03-16 23:09:40 +0100
committerJonas Lindstad <jonaslindstad@gmail.com>2015-03-16 23:09:40 +0100
commit939b606944b6892d26581bce598c66c790786de9 (patch)
tree6780dc26bd26c22ca8dce6a7832841b5f4efc37a /fap
parent88dfb934e546d02d1f4356032e91277f9f5b94f1 (diff)
rename + fixes
Diffstat (limited to 'fap')
-rw-r--r--fap/README.md31
-rw-r--r--fap/database/README.md111
-rw-r--r--fap/database/terminal_after_first_successfull_test14
-rw-r--r--fap/dhcpd/DHCP_protocol_breakdown.txt18
-rw-r--r--fap/dhcpd/module_craft_option.py79
-rw-r--r--fap/dhcpd/module_lease.py174
-rw-r--r--fap/dhcpd/server_dhcp.py323
-rw-r--r--fap/dhcpd/terminal.log146
-rw-r--r--fap/httpd/ex2200.template249
-rw-r--r--fap/httpd/server_http.py143
-rw-r--r--fap/httpd/terminal.log14
-rw-r--r--fap/proof_of_concepts/distro_during_testing.config355
-rw-r--r--fap/proof_of_concepts/tg15-tech82-poc1.tar.gzbin0 -> 4691 bytes
13 files changed, 1657 insertions, 0 deletions
diff --git a/fap/README.md b/fap/README.md
new file mode 100644
index 0000000..af91c1b
--- /dev/null
+++ b/fap/README.md
@@ -0,0 +1,31 @@
+# FAP - Fast and Agile Provisioning
+
+Tools (DHCP daemon + HTTP daemon + DB) for managing provisioning towards a large number of factory default Juniper switches (EX2200) using ZTP (Zero Touch Protocol) over DHCP relays.
+
+The project is built with Python (>3.4.0) and PostgreSQL (>9.3.5).
+
+Licensed under the GNU GPL, version 2. See the included COPYING file.
+
+
+
+## Usage
+Launch the python scripts for fap from tgmanage directory.
+
+
+### HTTPD
+ j@lappie:~/git/tgmanage$ sudo python3 fap/httpd/server_http.py
+
+Example: <a href="httpd/terminal.log">httpd/terminal.log</a>
+
+
+### DHCPD
+ j@lappie:~/git/tgmanage$ sudo python3 fap/dhcpd/server_dhcp.py
+
+Example: <a href="dhcpd/terminal.log">dhcpd/terminal.log</a>
+
+
+# TODO
+* Support for IPv6 management (in progress)
+* Support for only pushing JunOS image to switch - no config (for backup switches)
+* Try/catch on whole ethernet frame in DHCPD
+* Timestamps on each line in log both from DHCPD and HTTPD
diff --git a/fap/database/README.md b/fap/database/README.md
new file mode 100644
index 0000000..e49ab91
--- /dev/null
+++ b/fap/database/README.md
@@ -0,0 +1,111 @@
+# Database layout
+
+PostgreSQL
+
+**Tables**
+```
+bootstrap-> \dt
+ List of relations
+ Schema | Name | Type | Owner
+--------+----------+-------+-----------
+ public | switches | table | bootstrap
+```
+
+
+**Table structure**
+```
+bootstrap=> \d switches
+ Table "public.switches"
+ Column | Type | Modifiers
+-------------------+------------------------+-------------------------------------------------------
+ id | integer | not null default nextval('switches_id_seq'::regclass)
+ hostname | character varying(20) | not null
+ distro_name | character varying(100) | not null
+ distro_phy_port | character varying(100) | not null
+ mgmt_addr | character varying(15) | not null
+ mgmt_cidr | smallint | not null
+ mgmt_gw | character varying(15) | not null
+ mgmt_vlan | smallint | not null
+ last_config_fetch | integer |
+ current_mac | character varying(17) | default NULL::character varying
+ model | character varying(20) |
+ ztp_addr | character varying(15) |
+ ztp_cidr | smallint |
+ ztp_gw | character varying(15) |
+Indexes:
+ "switches_pkey" PRIMARY KEY, btree (id)
+```
+
+
+**Sample content in DB**
+```
+bootstrap=> select * from switches;
+ id | hostname | distro_name | distro_phy_port | mgmt_addr | mgmt_cidr | mgmt_gw | mgmt_vlan | last_config_fetch | current_mac | model | ztp_addr | ztp_cidr | ztp_gw | traffic_vlan
+----+----------+-------------+-----------------+-----------+-----------+----------+-----------+-------------------+-------------------+-------+----------+----------+--------+--------------
+ 23 | e-00-2 | rs1.sector0 | ge-0/0/2 | 10.0.0.12 | 24 | 10.0.0.1 | 666 | | | | | | | 102
+ 25 | e-00-4 | rs1.sector0 | ge-0/0/4 | 10.0.0.14 | 24 | 10.0.0.1 | 666 | | | | | | | 104
+ 27 | e-00-6 | rs1.sector0 | ge-0/0/6 | 10.0.0.16 | 24 | 10.0.0.1 | 666 | | | | | | | 106
+ 26 | e-00-5 | rs1.sector0 | ge-0/0/5 | 10.0.0.15 | 24 | 10.0.0.1 | 666 | 1426539826 | 44:f4:77:69:5e:c1 | | | | | 105
+ 24 | e-00-3 | rs1.sector0 | ge-0/0/3 | 10.0.0.13 | 24 | 10.0.0.1 | 666 | 1426535091 | 44:f4:77:69:49:81 | | | | | 103
+ 22 | e-00-1 | rs1.sector0 | ge-0/0/1 | 10.0.0.11 | 24 | 10.0.0.1 | 666 | 1426535243 | 44:f4:77:68:f7:c1 | | | | | 101
+ 30 | e-00-9 | rs1.sector0 | ge-0/0/9 | 10.0.0.19 | 24 | 10.0.0.1 | 666 | 1426539974 | 44:f4:77:68:b5:01 | | | | | 109
+ 21 | e-00-0 | rs1.sector0 | ge-0/0/0 | 10.0.0.10 | 24 | 10.0.0.1 | 666 | 1426540122 | 44:f4:77:69:4c:c1 | | | | | 100
+ 28 | e-00-7 | rs1.sector0 | ge-0/0/7 | 10.0.0.17 | 24 | 10.0.0.1 | 666 | 1426540272 | 44:f4:77:69:22:41 | | | | | 107
+ 29 | e-00-8 | rs1.sector0 | ge-0/0/8 | 10.0.0.18 | 24 | 10.0.0.1 | 666 | 1426540272 | 44:f4:77:69:4f:c1 | | | | | 108
+(10 rows)
+
+```
+
+
+**Connect to DB from CLI**
+```
+j@lappie:~/git/tgmanage$ psql -U bootstrap -d bootstrap -W
+Password for user bootstrap:
+psql (9.3.5)
+Type "help" for help.
+
+bootstrap=>
+```
+
+
+**Sample procedure to insert content to DB**
+```
+insert into switches
+(hostname, distro_name, distro_phy_port, mgmt_addr, mgmt_cidr, mgmt_gw, mgmt_vlan, traffic_vlan)
+values
+('e-00-0', 'rs1.sector0', 'ge-0/0/0', '10.0.0.10', '24', '10.0.0.1', '666', '100'),
+('e-00-1', 'rs1.sector0', 'ge-0/0/1', '10.0.0.11', '24', '10.0.0.1', '666', '101'),
+('e-00-2', 'rs1.sector0', 'ge-0/0/2', '10.0.0.12', '24', '10.0.0.1', '666', '102'),
+('e-00-3', 'rs1.sector0', 'ge-0/0/3', '10.0.0.13', '24', '10.0.0.1', '666', '103'),
+('e-00-4', 'rs1.sector0', 'ge-0/0/4', '10.0.0.14', '24', '10.0.0.1', '666', '104'),
+('e-00-5', 'rs1.sector0', 'ge-0/0/5', '10.0.0.15', '24', '10.0.0.1', '666', '105'),
+('e-00-6', 'rs1.sector0', 'ge-0/0/6', '10.0.0.16', '24', '10.0.0.1', '666', '106'),
+('e-00-7', 'rs1.sector0', 'ge-0/0/7', '10.0.0.17', '24', '10.0.0.1', '666', '107'),
+('e-00-8', 'rs1.sector0', 'ge-0/0/8', '10.0.0.18', '24', '10.0.0.1', '666', '108'),
+('e-00-9', 'rs1.sector0', 'ge-0/0/9', '10.0.0.19', '24', '10.0.0.1', '666', '109');
+```
+
+
+
+## Detailed description of table "switches" fields:
+* id: autoincreasing integer used to identify the database row
+* hostname: the unique edge switchs hostname - example: edge01
+* distro_name: the distro switch hostname - example: distro01
+* distro_phy_port: The distro switch's physical port - example: ge-3/1/0
+* mgmt_addr: The management IP - will be configured under vlan set in "mgmt_vlan" - example: 10.20.30.40
+* mgmt_cidr: CIDR mask on management subnet - example: 28
+* mgmt_vlan: VLAN id at the management VLAN - example: 100
+* last_config_fetch: unix timestamp of the last time the config were fetched by the switch - example: 11041551
+* current_mac: MAC address of the edge switch - example: 0f:1f:2f:3f:4f:5f
+* model: edge switch model - used to select template - example: ex2200
+
+
+
+## TODO
+ALTER TABLE bootstrap ADD mgmt_v6_cidr smallint;
+ALTER TABLE bootstrap ADD mgmt_v6_addr character varying(35);
+ALTER TABLE bootstrap ADD mgmt_v6_gw character varying(35);
+
+Rename v4 column names to follow v6 scheme
+
+Delete ztp_* columns
diff --git a/fap/database/terminal_after_first_successfull_test b/fap/database/terminal_after_first_successfull_test
new file mode 100644
index 0000000..68aee1c
--- /dev/null
+++ b/fap/database/terminal_after_first_successfull_test
@@ -0,0 +1,14 @@
+bootstrap=> select * from switches order by id;
+ id | hostname | distro_name | distro_phy_port | mgmt_addr | mgmt_cidr | mgmt_gw | mgmt_vlan | last_config_fetch | current_mac | model | ztp_addr | ztp_cidr | ztp_gw
+----+-------------+-----------------+-----------------+--------------+-----------+------------+-----------+-------------------+-------------------+-------+----------+----------+--------
+ 1 | e-00-0-test | distro-test | ge-0/0/0 | 10.0.200.2 | 24 | 10.0.200.1 | 300 | | | | | |
+ 2 | e-00-1-test | distro-test | ge-0/0/3 | 10.0.200.3 | 24 | 10.0.200.1 | 300 | | | | | |
+ 3 | e-00-2-test | distro-test | ge-0/0/6 | 10.0.200.4 | 24 | 10.0.200.1 | 300 | | | | | |
+ 4 | e-60-0-test | distro-test | ge-0/0/9 | 10.0.200.5 | 24 | 10.0.200.1 | 300 | | | | | |
+ 5 | e-01-1 | distro-test-new | ge-0/0/0 | 10.0.0.31 | 24 | 10.0.0.1 | 300 | 1424384091 | AA:BB:CC:DD:EE:FF | | | |
+ 6 | e-01-2 | distro-test-new | ge-0/0/3 | 10.0.0.32 | 24 | 10.0.0.1 | 300 | 1424311409 | | | | |
+ 8 | e-00-1 | distro0gw | ge-0/0/0 | 10.0.200.101 | 24 | 10.0.200.1 | 300 | 1424387906 | 40:b4:f0:cc:76:01 | | | |
+ 9 | e-00-2 | distro0gw | ge-0/0/3 | 10.0.200.102 | 24 | 10.0.200.1 | 300 | | | | | |
+ 10 | e-00-3 | distro0gw | ge-0/0/6 | 10.0.200.103 | 24 | 10.0.200.1 | 300 | | | | | |
+(9 rows)
+
diff --git a/fap/dhcpd/DHCP_protocol_breakdown.txt b/fap/dhcpd/DHCP_protocol_breakdown.txt
new file mode 100644
index 0000000..5af2bf2
--- /dev/null
+++ b/fap/dhcpd/DHCP_protocol_breakdown.txt
@@ -0,0 +1,18 @@
+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
diff --git a/fap/dhcpd/module_craft_option.py b/fap/dhcpd/module_craft_option.py
new file mode 100644
index 0000000..35e7328
--- /dev/null
+++ b/fap/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/fap/dhcpd/module_lease.py b/fap/dhcpd/module_lease.py
new file mode 100644
index 0000000..306f13a
--- /dev/null
+++ b/fap/dhcpd/module_lease.py
@@ -0,0 +1,174 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+'''
+ Created by Jonas 'j' Lindstad for The Gathering 2015
+ License: GPLv3
+
+ Class used to fetch data from the Postgres DB
+
+ Usage examples:
+ lease.debug = True
+ x = lease({'distro_name': 'distro-test', 'distro_phy_port': 'ge-0/0/6'}).get_dict()
+ print('key lookup - hostname: %s' % x['hostname'])
+'''
+
+import psycopg2
+import psycopg2.extras
+
+# settings
+settings = dict(
+ db = dict(
+ user = 'bootstrap',
+ password = 'asdf',
+ dbname = 'bootstrap',
+ host = 'localhost'
+ )
+)
+
+# connect to Postgres DB
+connect_params = ("dbname='%s' user='%s' host='%s' password='%s'" % (settings['db']['dbname'], settings['db']['user'], settings['db']['host'], settings['db']['password']))
+conn = psycopg2.connect(connect_params)
+cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
+
+class lease(object):
+ debug = False
+
+ def __init__(self, identifiers):
+ if len(identifiers) > 0: # 1 or more identifiers - we're good to go
+
+ # build query string
+ where_pieces = []
+ for x in identifiers.items():
+ where_pieces.append(str(x[0]) + " = '" + str(x[1]) + "'")
+ where = ' AND '.join(where_pieces)
+ select = "SELECT * FROM switches WHERE " + where + " LIMIT 1"
+
+ if self.debug is True:
+ print('Executing query: ' + select)
+
+ cur.execute(select)
+
+ rows = cur.fetchall()
+ if len(rows) is 1:
+ if self.debug is True:
+ print('returned from DB:')
+ for key, value in rows[0].items():
+ print('%s: %s' % (key, value))
+
+ self.row = rows[0]
+ else:
+ self.row = False
+ else:
+ print('Missing identifier parameter')
+ exit()
+
+ def get_ip(self):
+ if self.row is not False:
+ return self.row['ip']
+ else:
+ print('identifiers (%s) not found' % self.row)
+ return False
+
+ def get_config(self):
+ if self.row is not False:
+ return self.row['config']
+ else:
+ print('identifiers (%s) not found' % self.row)
+ return False
+
+ def get_dict(self):
+ if self.row is not False:
+ return self.row
+ else:
+ print('identifiers (%s) not found' % self.row)
+ return False
+
+
+#
+# TESTING - Bruker ID fra DB-en som identifier, og kjører en query per lease.get_x()
+#
+class lease2(object):
+ debug = False
+ hostname = False
+ identifiers = False
+
+ # identifiers = dict of field/values
+ def __init__(self, identifiers):
+ cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
+
+ if len(identifiers) > 0: # 1 or more identifiers - we're good to go
+ self.identifiers = identifiers # Used to debug if no match for the identifiers is given
+
+ # build query string
+ where_pieces = []
+ for identifier in identifiers.items():
+ where_pieces.append(str(identifier[0]) + " = '" + str(identifier[1]) + "'")
+ where = ' AND '.join(where_pieces)
+ select = "SELECT hostname FROM switches WHERE " + where + " LIMIT 1"
+
+ if self.debug is True:
+ print('Executing query: ' + select)
+
+ cur.execute(select)
+ rows = cur.fetchall()
+ cur.close()
+ if len(rows) is 1:
+ if self.debug is True:
+ print('returned from DB:')
+ print(rows[0][0])
+ self.hostname = rows[0][0]
+ else:
+ self.hostname = False
+ else:
+ print('Missing identifier parameter')
+ exit()
+
+ # Used to fetch fields from DB
+ def get(self, field):
+ if self.hostname is not False:
+ cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
+
+ query = "SELECT %s FROM switches WHERE hostname = '%s' LIMIT 1" % (field, self.hostname)
+ if self.debug is True:
+ print('Query: %s' % query)
+
+ try:
+ cur.execute(query)
+ rows = cur.fetchall()
+
+ if len(rows) is 1:
+ if self.debug is True:
+ print('returned from DB:')
+ print(rows[0][0])
+ return rows[0][0]
+ else:
+ if self.debug is True:
+ print('No data found - field: %s' % field)
+ return False
+ except psycopg2.ProgrammingError:
+ print('Field (%s) not found' % field)
+ conn.rollback() # Prevents DB from locking up the next queries - http://initd.org/psycopg/docs/connection.html#connection.rollback
+ return False
+ else:
+ print('identifiers (%s) not found' % self.identifiers)
+ return False
+
+ # Used to set fields in DB
+ def set(self, field, value):
+ if self.hostname is not False:
+ cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
+ query = "UPDATE switches SET %s = '%s' WHERE hostname = '%s'" % (field, value, self.hostname)
+ if self.debug is True:
+ print('Query: %s' % query)
+ try:
+ cur.execute(query)
+ conn.commit()
+ return True
+ except psycopg2.ProgrammingError:
+ print('Field (%s) not found' % field)
+ conn.rollback()
+ return False
+ else:
+ print('identifiers (%s) not found' % self.identifiers)
+ return False
diff --git a/fap/dhcpd/server_dhcp.py b/fap/dhcpd/server_dhcp.py
new file mode 100644
index 0000000..0c05505
--- /dev/null
+++ b/fap/dhcpd/server_dhcp.py
@@ -0,0 +1,323 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+'''
+server_dhcp.py by Jonas "j" Lindstad for The Gathering tech:server
+
+Used to configure the Juniper EX2200 edge switches with Zero Touch Protocol
+License: GPLv2
+
+Based on the work of psychomario - https://github.com/psychomario
+'''
+
+
+'''
+
+TODO
+
+ * try/catch around each incomming packet - prevents DHCP-server from crashing if it receives a malformed packet
+
+'''
+
+import socket, binascii, IN
+from module_craft_option import craft_option # Module that crafts DHCP options
+# from module_lease import lease # Module that fetches data from DB and provides data for the lease
+from module_lease import lease2 as lease # Module that fetches data from DB and provides data for the lease
+
+
+# Global options - not a pretty hack
+options_raw = {} # TODO - not a nice way to do things
+option_82_1 = ''
+client = ''
+
+
+#############
+# FUNCTIONS #
+#############
+
+# Generator for each of the dhcp fields
+def split_packet(msg,slices):
+ for x in slices:
+ 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] 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):
+ 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):
+ return ':'.join(str(x) for x in chunk_hex(hex_mac))
+
+# Formats a 6 byte MAC to a readable string (b'5e\x21\x00r3' => '35:65:21:00:72:33')
+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))
+
+# CIDR notation to subnet string ('25' => '255.255.255.128')
+def cidr_to_subnet(cidr):
+ mask = [0, 0, 0, 0]
+ for i in range(int(cidr)):
+ mask[i//8] = mask[i//8] + (1 << (7 - i % 8))
+ return '.'.join(str(x) for x in mask)
+
+# Parses DHCP options - raw = hex options
+def parse_options(raw):
+ print('[%s] --> processing DHCP options' % client)
+ chunked = chunk_hex(raw)
+ chunked_length = len(chunked)
+ pointer = 0 # counter - next option start
+ options = {} # options dataset
+
+ global options_raw
+ options_raw = {} # incomming request's options
+ special_options = [53, 82]
+
+ while True: # Loop over the DHCP options
+ option = int(chunked[pointer], 16) # option ID (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 # copying incomming request's options, directly usable in outgoing replies
+
+ asciivalue = binascii.unhexlify(option_payload) # should not contain unreadable characters
+
+ if option in special_options:
+ if option is 82:
+ option82_raw = option_payload
+ options[option] = parse_suboptions(option, option_payload)
+ elif option is 53:
+ options[option] = option_payload
+ # 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('[%s] --> option: %s: %s' % (client, option, 'DHCP Discover (will not be used in reply)'))
+ else:
+ print('[%s] --> option: %s: %s' % (client, option, asciivalue))
+
+ else:
+ options[option] = asciivalue
+ # TODO: Formating.... Also crap code
+ 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 - should allways last field
+ print('[%s] --> Finished processing options' % client)
+ break
+ return options
+
+# Parses suboptions
+def parse_suboptions(option, raw):
+ print('[%s] --> processing suboption hook for option %s' % (client, option))
+ chunked = chunk_hex(raw)
+ chunked_length = len(chunked)
+ pointer = 0 # counter - next option start
+ dataset = {}
+
+ if option is 82: # Option 82 - custom shit: Setting global variable to list
+ global option_82_1
+
+ while True:
+ length = int(chunked[pointer+1], 16) # option length in bytes
+ value = raw[4:(length*2)+(4)]
+
+ 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('[%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('[%s] --> Finished processing suboption %s' % (client, option))
+ break
+ return dataset
+
+# Parses and handles DHCP DISCOVER or DHCP REQUEST
+def reqparse(message):
+ data=None
+ dhcpfields=[1,1,1,1,4,2,2,4,4,4,4,6,10,192,4,message.rfind(b'\xff'),1]
+ hexmessage=binascii.hexlify(message)
+ messagesplit=[binascii.hexlify(x) for x in split_packet(message,dhcpfields)]
+
+ global client
+ client = prettyprint_hex_as_str(messagesplit[11])
+
+ print('[%s] Parsing DHCP packet from client' % client)
+
+ #
+ # Logical checks to decide to whether respond or reject
+ #
+ if int(hex_ip_to_str(messagesplit[10]).replace('.', '')) is 0: # DHCP request has been forwarded by DHCP relay - A bit haxxy..
+ print('[%s] Rejecting to process DHCP packet - not forwarded by DHCP relay' % client)
+ return False
+
+ # Process DHCP options
+ 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 options[53] == b'01':
+ mode = 'dhcp_discover'
+ print('[%s] --> DHCP packet type: DHCP DISCOVER' % client)
+ elif options[53] == b'03':
+ mode = 'dhcp_request'
+ print('[%s] --> DHCP packet type: DHCP REQUEST' % client)
+ else:
+ print('[%s] Rejecting to continue process DHCP packet - option 53 missing' % client) # Small sanity check
+ 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])))
+
+ # 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_identifiers = {'distro_name': distro, 'distro_phy_port': phy.split('.')[0]}
+ if lease(lease_identifiers).get('hostname') is not False:
+ l={
+ 'hostname': lease(lease_identifiers).get('hostname'),
+ 'mgmt_addr': lease(lease_identifiers).get('mgmt_addr'),
+ 'mgmt_gw': lease(lease_identifiers).get('mgmt_gw'),
+ 'mgmt_cidr': lease(lease_identifiers).get('mgmt_cidr')
+ }
+
+ # lease_details = lease({'distro_name': distro, 'distro_phy_port': phy[:-2]}).get_dict()
+ 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
+ else:
+ print('[%s] Rejecting to continue to process DHCP packet - option 82.1 is empty' % client)
+
+ if mode == 'dhcp_discover':
+ print('[%s] --> Crafting DHCP OFFER response' % client)
+
+ if mode == 'dhcp_request':
+ print('[%s] --> Crafting DHCP ACK response' % client)
+
+ print('[%s] --> XID/Transaction ID: %s' % (client, prettyprint_hex_as_str(messagesplit[4])))
+ print('[%s] --> Client IP: %s' % (client, l['mgmt_addr']))
+ print('[%s] --> DHCP forwarder IP: %s' % (client, l['mgmt_gw']))
+ 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'\x01' # Hops
+ data += binascii.unhexlify(messagesplit[4]) # XID / Transaction ID
+ data += b'\x00\x00' # seconds elapsed - 1 second
+ data += b'\x80\x00' # BOOTP flags - broadcast (unicast: 0x0000)
+ data += b'\x00'*4 # Client IP address
+ data += socket.inet_aton(l['mgmt_addr']) # New IP to client
+ data += socket.inet_aton(dhcp_server_address) # Next server IP address
+ data += socket.inet_aton(l['mgmt_gw']) # 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
+
+ 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(dhcp_server_address)) # Option 54 - DHCP server identifier
+ print('[%s] --> Option 54 (DHCP server identifier): %s' % (client, dhcp_server_address))
+
+ data += craft_option(51).raw_hex(b'\x00\x00\xa8\xc0') # Option 51 - Lease time left padded with "0"
+ print('[%s] --> Option 51 (Lease time): %s' % (client, '43200 (12 hours)'))
+
+ data += craft_option(1).ip(cidr_to_subnet(l['mgmt_cidr'])) # Option 1 - Subnet mask
+ print('[%s] --> Option 1 (subnet mask): %s' % (client, cidr_to_subnet(l['mgmt_cidr'])))
+
+ data += craft_option(3).ip(l['mgmt_gw']) # Option 3 - Default gateway (set to DHCP forwarders IP)
+ print('[%s] --> Option 3 (default gateway): %s' % (client, l['mgmt_gw']))
+
+ data += craft_option(150).bytes(socket.inet_aton(dhcp_server_address)) # Option 150 - TFTP Server. Used as target for the Zero Touch Protocol. Not necessarily TFTP protocol used.
+ print('[%s] --> Option 150 (Cisco proprietary TFTP server(s)): %s' % (client, dhcp_server_address))
+
+ # 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(target_junos_file) + craft_option(1).string('/tg-edge/' + l['hostname']) + craft_option(3).string('http')) # Option 43 - ZTP
+ print('[%s] --> Option 43 (Vendor-specific option):' % client)
+ print('[%s] --> Suboption 0: %s' % (client, target_junos_file))
+ print('[%s] --> Suboption 1: %s' % (client, '/tg-edge/' + l['hostname']))
+ print('[%s] --> Suboption 3: %s' % (client, 'http'))
+
+ data += b'\xff'
+
+ lease(lease_identifiers).set('current_mac', client) # updates MAC in DB
+
+ return data
+
+if __name__ == "__main__":
+ interface = b'eth0'
+ dhcp_server_address = '10.0.100.2'
+ # target_junos_file = '/files/jinstall-ex-2200-14.1X53-D15.2-domestic-signed.tgz'
+ target_junos_file = '/files/jinstall-ex-2200-14.1X53-D15.2-domestic-signed.tgz'
+
+ # Setting up the server, and how it will communicate
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # IPv4 UDP socket
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
+ s.setsockopt(socket.SOL_SOCKET, 25, interface)
+ s.bind(('', 67))
+
+ # Starting the whole loop
+ print('Starting 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('[%s] DHCP broadcast - unsupported' % client)
+ reply_to = '<broadcast>'
+ else:
+ print('[%s] DHCP unicast - DHCP forwarding' % client)
+ reply_to = addressf[0] # senders (DHCP forwarders) IP
+ # print(addressf[0])
+ # reply_to = '10.0.0.1'
+ data=reqparse(message) # Parse the DHCP request
+ if data:
+ print('[%s] --> replying to %s' % (client, reply_to))
+ s.sendto(data, (reply_to, 67)) # Sends reply
+ print('')
+ except KeyboardInterrupt:
+ exit()
diff --git a/fap/dhcpd/terminal.log b/fap/dhcpd/terminal.log
new file mode 100644
index 0000000..e865bd4
--- /dev/null
+++ b/fap/dhcpd/terminal.log
@@ -0,0 +1,146 @@
+j@lappie:~/git/tgmanage$ sudo python3 fap/dhcpd/server_dhcp.py
+Starting main loop
+[] DHCP unicast - DHCP forwarding
+[40:b4:f0:cc:76:01] Parsing DHCP packet from client
+[40:b4:f0:cc:76:01] --> processing DHCP options
+[40:b4:f0:cc:76:01] --> option: 53: DHCP Discover (will not be used in reply)
+[40:b4:f0:cc:76:01] --> option: 12: b'GP0212466317'
+[40:b4:f0:cc:76:01] --> option: 60: 4a:75:6e:69:70:65:72:2d:65 [...]
+[40:b4:f0:cc:76:01] --> processing suboption hook for option 82
+[40:b4:f0:cc:76:01] --> suboption 1 found - value: "distro0gw:ge-0/0/0.0:mgmt"
+[40:b4:f0:cc:76:01] --> Finished processing suboption 82
+[40:b4:f0:cc:76:01] --> Finished processing options
+[40:b4:f0:cc:76:01] --> DHCP packet type: DHCP DISCOVER
+[40:b4:f0:cc:76:01] --> DHCP packet contains option 82 - continues to process
+[40:b4:f0:cc:76:01] --> DHCP packet forwarded by relay 10.0.200.1
+[40:b4:f0:cc:76:01] --> DHCP XID/Transaction ID: 75:45:e1:46
+[40:b4:f0:cc:76:01] --> Looking up in the DB
+[40:b4:f0:cc:76:01] --> Query details: distro_name:distro0gw, distro_phy_port:ge-0/0/0
+[40:b4:f0:cc:76:01] --> Data found, switch exists in DB - ready to craft response
+[40:b4:f0:cc:76:01] --> Crafting DHCP OFFER response
+[40:b4:f0:cc:76:01] --> XID/Transaction ID: 75:45:e1:46
+[40:b4:f0:cc:76:01] --> Client IP: 10.0.200.101
+[40:b4:f0:cc:76:01] --> DHCP forwarder IP: 10.0.200.1
+[40:b4:f0:cc:76:01] --> Client MAC: 40:b4:f0:cc:76:01
+[40:b4:f0:cc:76:01] --> Completed DHCP header structure, building DHCP options
+[40:b4:f0:cc:76:01] --> Option 53 (DHCP OFFER): 2
+[40:b4:f0:cc:76:01] --> Option 54 (DHCP server identifier): 10.0.100.2
+[40:b4:f0:cc:76:01] --> Option 51 (Lease time): 43200 (12 hours)
+[40:b4:f0:cc:76:01] --> Option 1 (subnet mask): 255.255.255.0
+[40:b4:f0:cc:76:01] --> Option 3 (default gateway): 10.0.200.1
+[40:b4:f0:cc:76:01] --> Option 150 (Cisco proprietary TFTP server(s)): 10.0.100.2
+[40:b4:f0:cc:76:01] --> Option 43 (Vendor-specific option):
+[40:b4:f0:cc:76:01] --> Suboption 0: /files/jinstall-ex-2200-12.3R6.6-domestic-signed.tgz
+[40:b4:f0:cc:76:01] --> Suboption 1: /tg-edge/e-00-1
+[40:b4:f0:cc:76:01] --> Suboption 3: http
+[40:b4:f0:cc:76:01] --> replying to 10.0.100.1
+
+[40:b4:f0:cc:76:01] DHCP unicast - DHCP forwarding
+[40:b4:f0:cc:76:01] Parsing DHCP packet from client
+[40:b4:f0:cc:76:01] --> processing DHCP options
+[40:b4:f0:cc:76:01] --> option: 54: b'\n\x00d\x02'
+[40:b4:f0:cc:76:01] --> option: 55: b'\x033\x01\x0f\x06BC+\x96\x0c\x07*'
+[40:b4:f0:cc:76:01] --> option: 50: b'\n\x00\xc8e'
+[40:b4:f0:cc:76:01] --> option: 53: b'\x03'
+[40:b4:f0:cc:76:01] --> option: 12: b'GP0212466317'
+[40:b4:f0:cc:76:01] --> option: 60: 4a:75:6e:69:70:65:72:2d:65 [...]
+[40:b4:f0:cc:76:01] --> processing suboption hook for option 82
+[40:b4:f0:cc:76:01] --> suboption 1 found - value: "distro0gw:ge-0/0/0.0:mgmt"
+[40:b4:f0:cc:76:01] --> Finished processing suboption 82
+[40:b4:f0:cc:76:01] --> Finished processing options
+[40:b4:f0:cc:76:01] --> DHCP packet type: DHCP REQUEST
+[40:b4:f0:cc:76:01] --> DHCP packet contains option 82 - continues to process
+[40:b4:f0:cc:76:01] --> DHCP packet forwarded by relay 10.0.200.1
+[40:b4:f0:cc:76:01] --> DHCP XID/Transaction ID: 51:5f:00:7c
+[40:b4:f0:cc:76:01] --> Looking up in the DB
+[40:b4:f0:cc:76:01] --> Query details: distro_name:distro0gw, distro_phy_port:ge-0/0/0
+[40:b4:f0:cc:76:01] --> Data found, switch exists in DB - ready to craft response
+[40:b4:f0:cc:76:01] --> Crafting DHCP ACK response
+[40:b4:f0:cc:76:01] --> XID/Transaction ID: 51:5f:00:7c
+[40:b4:f0:cc:76:01] --> Client IP: 10.0.200.101
+[40:b4:f0:cc:76:01] --> DHCP forwarder IP: 10.0.200.1
+[40:b4:f0:cc:76:01] --> Client MAC: 40:b4:f0:cc:76:01
+[40:b4:f0:cc:76:01] --> Completed DHCP header structure, building DHCP options
+[40:b4:f0:cc:76:01] --> Option 53 (DHCP ACK): 5
+[40:b4:f0:cc:76:01] --> Option 54 (DHCP server identifier): 10.0.100.2
+[40:b4:f0:cc:76:01] --> Option 51 (Lease time): 43200 (12 hours)
+[40:b4:f0:cc:76:01] --> Option 1 (subnet mask): 255.255.255.0
+[40:b4:f0:cc:76:01] --> Option 3 (default gateway): 10.0.200.1
+[40:b4:f0:cc:76:01] --> Option 150 (Cisco proprietary TFTP server(s)): 10.0.100.2
+[40:b4:f0:cc:76:01] --> Option 43 (Vendor-specific option):
+[40:b4:f0:cc:76:01] --> Suboption 0: /files/jinstall-ex-2200-12.3R6.6-domestic-signed.tgz
+[40:b4:f0:cc:76:01] --> Suboption 1: /tg-edge/e-00-1
+[40:b4:f0:cc:76:01] --> Suboption 3: http
+[40:b4:f0:cc:76:01] --> replying to 10.0.100.1
+
+[40:b4:f0:cc:76:01] DHCP unicast - DHCP forwarding
+[40:b4:f0:cc:76:01] Parsing DHCP packet from client
+[40:b4:f0:cc:76:01] --> processing DHCP options
+[40:b4:f0:cc:76:01] --> option: 53: DHCP Discover (will not be used in reply)
+[40:b4:f0:cc:76:01] --> option: 60: 4a:75:6e:69:70:65:72:2d:65 [...]
+[40:b4:f0:cc:76:01] --> processing suboption hook for option 82
+[40:b4:f0:cc:76:01] --> suboption 1 found - value: "distro0gw:ge-0/0/0.0:mgmt"
+[40:b4:f0:cc:76:01] --> Finished processing suboption 82
+[40:b4:f0:cc:76:01] --> Finished processing options
+[40:b4:f0:cc:76:01] --> DHCP packet type: DHCP DISCOVER
+[40:b4:f0:cc:76:01] --> DHCP packet contains option 82 - continues to process
+[40:b4:f0:cc:76:01] --> DHCP packet forwarded by relay 10.0.200.1
+[40:b4:f0:cc:76:01] --> DHCP XID/Transaction ID: 64:3c:98:69
+[40:b4:f0:cc:76:01] --> Looking up in the DB
+[40:b4:f0:cc:76:01] --> Query details: distro_name:distro0gw, distro_phy_port:ge-0/0/0
+[40:b4:f0:cc:76:01] --> Data found, switch exists in DB - ready to craft response
+[40:b4:f0:cc:76:01] --> Crafting DHCP OFFER response
+[40:b4:f0:cc:76:01] --> XID/Transaction ID: 64:3c:98:69
+[40:b4:f0:cc:76:01] --> Client IP: 10.0.200.101
+[40:b4:f0:cc:76:01] --> DHCP forwarder IP: 10.0.200.1
+[40:b4:f0:cc:76:01] --> Client MAC: 40:b4:f0:cc:76:01
+[40:b4:f0:cc:76:01] --> Completed DHCP header structure, building DHCP options
+[40:b4:f0:cc:76:01] --> Option 53 (DHCP OFFER): 2
+[40:b4:f0:cc:76:01] --> Option 54 (DHCP server identifier): 10.0.100.2
+[40:b4:f0:cc:76:01] --> Option 51 (Lease time): 43200 (12 hours)
+[40:b4:f0:cc:76:01] --> Option 1 (subnet mask): 255.255.255.0
+[40:b4:f0:cc:76:01] --> Option 3 (default gateway): 10.0.200.1
+[40:b4:f0:cc:76:01] --> Option 150 (Cisco proprietary TFTP server(s)): 10.0.100.2
+[40:b4:f0:cc:76:01] --> Option 43 (Vendor-specific option):
+[40:b4:f0:cc:76:01] --> Suboption 0: /files/jinstall-ex-2200-12.3R6.6-domestic-signed.tgz
+[40:b4:f0:cc:76:01] --> Suboption 1: /tg-edge/e-00-1
+[40:b4:f0:cc:76:01] --> Suboption 3: http
+[40:b4:f0:cc:76:01] --> replying to 10.0.100.1
+
+[40:b4:f0:cc:76:01] DHCP unicast - DHCP forwarding
+[40:b4:f0:cc:76:01] Parsing DHCP packet from client
+[40:b4:f0:cc:76:01] --> processing DHCP options
+[40:b4:f0:cc:76:01] --> option: 54: b'\n\x00d\x02'
+[40:b4:f0:cc:76:01] --> option: 55: b'\x033\x01\x0f\x06BC+\x96\x0c\x07*'
+[40:b4:f0:cc:76:01] --> option: 50: b'\n\x00\xc8e'
+[40:b4:f0:cc:76:01] --> option: 53: b'\x03'
+[40:b4:f0:cc:76:01] --> option: 60: 4a:75:6e:69:70:65:72:2d:65 [...]
+[40:b4:f0:cc:76:01] --> processing suboption hook for option 82
+[40:b4:f0:cc:76:01] --> suboption 1 found - value: "distro0gw:ge-0/0/0.0:mgmt"
+[40:b4:f0:cc:76:01] --> Finished processing suboption 82
+[40:b4:f0:cc:76:01] --> Finished processing options
+[40:b4:f0:cc:76:01] --> DHCP packet type: DHCP REQUEST
+[40:b4:f0:cc:76:01] --> DHCP packet contains option 82 - continues to process
+[40:b4:f0:cc:76:01] --> DHCP packet forwarded by relay 10.0.200.1
+[40:b4:f0:cc:76:01] --> DHCP XID/Transaction ID: 66:33:48:73
+[40:b4:f0:cc:76:01] --> Looking up in the DB
+[40:b4:f0:cc:76:01] --> Query details: distro_name:distro0gw, distro_phy_port:ge-0/0/0
+[40:b4:f0:cc:76:01] --> Data found, switch exists in DB - ready to craft response
+[40:b4:f0:cc:76:01] --> Crafting DHCP ACK response
+[40:b4:f0:cc:76:01] --> XID/Transaction ID: 66:33:48:73
+[40:b4:f0:cc:76:01] --> Client IP: 10.0.200.101
+[40:b4:f0:cc:76:01] --> DHCP forwarder IP: 10.0.200.1
+[40:b4:f0:cc:76:01] --> Client MAC: 40:b4:f0:cc:76:01
+[40:b4:f0:cc:76:01] --> Completed DHCP header structure, building DHCP options
+[40:b4:f0:cc:76:01] --> Option 53 (DHCP ACK): 5
+[40:b4:f0:cc:76:01] --> Option 54 (DHCP server identifier): 10.0.100.2
+[40:b4:f0:cc:76:01] --> Option 51 (Lease time): 43200 (12 hours)
+[40:b4:f0:cc:76:01] --> Option 1 (subnet mask): 255.255.255.0
+[40:b4:f0:cc:76:01] --> Option 3 (default gateway): 10.0.200.1
+[40:b4:f0:cc:76:01] --> Option 150 (Cisco proprietary TFTP server(s)): 10.0.100.2
+[40:b4:f0:cc:76:01] --> Option 43 (Vendor-specific option):
+[40:b4:f0:cc:76:01] --> Suboption 0: /files/jinstall-ex-2200-12.3R6.6-domestic-signed.tgz
+[40:b4:f0:cc:76:01] --> Suboption 1: /tg-edge/e-00-1
+[40:b4:f0:cc:76:01] --> Suboption 3: http
+[40:b4:f0:cc:76:01] --> replying to 10.0.100.1
+
diff --git a/fap/httpd/ex2200.template b/fap/httpd/ex2200.template
new file mode 100644
index 0000000..aa62ff2
--- /dev/null
+++ b/fap/httpd/ex2200.template
@@ -0,0 +1,249 @@
+system {
+ host-name $hostname;
+ auto-snapshot;
+ time-zone Europe/Oslo;
+ authentication-order [ tacplus password ];
+ root-authentication {
+ encrypted-password "$1$v1xWD3zI$OhStP6PnpgIUO3RLtMmIJ/";
+ }
+ name-server {
+ 1.1.1.1;
+ 2.2.2.2;
+ }
+ login {
+ user technet{
+ uid 2000;
+ class super-user;
+ authentication {
+ encrypted-password "$1$v1xWD3zI$OhStP6PnpgIUO3RLtMmIJ/";
+ }
+ }
+ }
+ services {
+ ssh {
+ root-login deny;
+ }
+ netconf {
+ ssh;
+ }
+ }
+ syslog {
+ user * {
+ any emergency;
+ }
+ file messages {
+ any notice;
+ authorization info;
+ }
+ file interactive-commands {
+ interactive-commands any;
+ }
+ }
+ ntp {
+ server 1.2.3.4;
+ server 2.3.4.5;
+ }
+}
+
+chassis {
+ aggregated-devices {
+ ethernet {
+ device-count 1;
+ }
+ }
+}
+
+interfaces {
+ interface-range edge-ports {
+ member-range ge-0/0/0 to ge-0/0/43;
+ unit 0 {
+ family ethernet-switching {
+ port-mode access;
+ vlan {
+ members deltagere;
+ }
+ }
+ }
+ }
+ interface-range core-ports {
+ member-range ge-0/0/44 to ge-0/0/47;
+ ether-options {
+ 802.3ad ae0;
+ }
+ }
+ ae0 {
+ description "Til $distro_name $distro_phy_port";
+ aggregated-ether-options {
+ lacp {
+ active;
+ }
+ }
+ unit 0 {
+ family ethernet-switching {
+ port-mode trunk;
+ vlan {
+ members deltagere;
+ }
+ native-vlan-id mgmt;
+ }
+ }
+ }
+ vlan {
+ unit $mgmt_vlan {
+ description "MGMT L3 interface";
+ family inet {
+ filter {
+ input v4-mgmt;
+ }
+ address $mgmt_addr/$mgmt_cidr;
+ }
+ family inet6 {
+ filter {
+ input v6-mgmt;
+ }
+ }
+ }
+ }
+}
+firewall {
+ family inet {
+ filter v4-mgmt {
+ term accept-noc {
+ from {
+ source-address {
+ 0.0.0.0/0;
+ }
+ }
+ then accept;
+ }
+ term accept-icmp {
+ from {
+ protocol icmp;
+ }
+ then {
+ accept;
+ }
+ }
+ term reject-all {
+ then {
+ log;
+ syslog;
+ reject;
+ }
+ }
+ }
+ }
+ family inet6 {
+ filter v6-mgmt {
+ term accept-noc {
+ from {
+ source-address {
+ ::/0;
+ }
+ }
+ then accept;
+ }
+ term accept-icmp {
+ from {
+ next-header icmp6;
+ }
+ then {
+ accept;
+ }
+ }
+ term reject-all {
+ then {
+ log;
+ syslog;
+ reject;
+ }
+ }
+ }
+ }
+}
+
+ethernet-switching-options {
+ secure-access-port {
+ interface edge-ports {
+ no-dhcp-trusted;
+ }
+ vlan deltagere {
+ arp-inspection;
+ examine-dhcp;
+ examine-dhcpv6;
+ neighbor-discovery-inspection;
+ ip-source-guard;
+ ipv6-source-guard;
+ dhcp-option82;
+ dhcpv6-option18 {
+ use-option-82;
+ }
+ }
+ ipv6-source-guard-sessions {
+ max-number 128;
+ }
+ }
+}
+
+protocols {
+ sflow {
+ sample-rate {
+ ingress 10000;
+ egress 10000;
+ }
+ collector 91.209.30.12;
+ interfaces edge-ports;
+ interfaces core-ports;
+ }
+ igmp-snooping {
+ vlan all {
+ version 3;
+ immediate-leave;
+ }
+ }
+ mld-snooping {
+ vlan all {
+ version 2;
+ immediate-leave;
+ }
+ }
+ rstp {
+ bridge-priority 8k;
+ interface edge-ports {
+ edge;
+ no-root-port;
+ }
+ }
+ lldp {
+ interface ae0.0
+ }
+}
+
+vlans {
+ deltagere {
+ vlan-id $traffic_vlan;
+ }
+ mgmt {
+ vlan-id $mgmt_vlan;
+ l3-interface vlan.$mgmt_vlan;
+ }
+}
+
+routing-options {
+ rib inet.0 {
+ static {
+ route 0.0.0.0/0 {
+ next-hop $mgmt_gw;
+ }
+ }
+ }
+ rib inet6.0 {
+ static {
+ route ::/0 {
+ next-hop 20a0:dead::beef;
+ }
+ }
+ }
+}
+
+
diff --git a/fap/httpd/server_http.py b/fap/httpd/server_http.py
new file mode 100644
index 0000000..bc9ceea
--- /dev/null
+++ b/fap/httpd/server_http.py
@@ -0,0 +1,143 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+from http.server import BaseHTTPRequestHandler, HTTPServer
+from string import Template
+import time
+import psycopg2
+import psycopg2.extras
+import sys
+import os
+
+def main():
+ #
+ # Settings
+ #
+ settings = {
+ 'db': {
+ 'user': 'bootstrap',
+ 'password': 'asdf',
+ 'dbname': 'bootstrap',
+ 'host': 'localhost'
+ },
+ 'http': {
+ 'host': '0.0.0.0',
+ 'port': 80
+ }
+ }
+
+ #
+ # Connect to DB
+ #
+ try:
+ connect_params = ("dbname='%s' user='%s' host='%s' password='%s'" % (settings['db']['dbname'], settings['db']['user'], settings['db']['host'], settings['db']['password']))
+ conn = psycopg2.connect(connect_params)
+ cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
+ # cur.execute("""SELECT * from switches""")
+ # rows = cur.fetchall()
+ # print ("\nSwitches in DB during server_http.py startup:")
+ # for row in rows:
+ # print (" --> %s, connected to %s port %s" % (row['hostname'], row['distro_name'], row['distro_phy_port']))
+
+ except (psycopg2.DatabaseError, psycopg2.OperationalError) as e:
+ print ('Error: %s' % e)
+ sys.exit(1)
+
+ except:
+ print(sys.exc_info()[0])
+ sys.exit(1)
+
+ def template_get(model):
+ return open('fap/httpd/' + model + '.template').read()
+
+ def template_parse(template_src, hostname):
+ cur.execute("SELECT * FROM switches WHERE hostname = '%s'" % hostname)
+ if(cur.rowcount == 1):
+ row = cur.fetchall()[0]
+ d={
+ 'hostname': row['hostname'],
+ 'distro_name': row['distro_name'],
+ 'distro_phy_port': row['distro_phy_port'],
+ 'mgmt_addr': row['mgmt_addr'],
+ 'mgmt_cidr': row['mgmt_cidr'],
+ 'mgmt_gw': row['mgmt_gw'],
+ 'mgmt_vlan': row['mgmt_vlan'],
+ 'traffic_vlan': row['traffic_vlan']
+ }
+ cur.execute("UPDATE switches SET last_config_fetch = '%s' WHERE hostname = '%s'" % (str(time.time()).split('.')[0], hostname)) # updated DB with last config fetch
+ conn.commit()
+ return Template(template_src).safe_substitute(d)
+ else:
+ return False
+
+ class httpd(BaseHTTPRequestHandler):
+ def do_GET(self):
+ print('[%s] [%s] Incoming HTTP GET URI:%s ' % (self.client_address[0], time.asctime(), self.path))
+
+ # Client asks for the config file
+ if '/tg-edge/' in self.path:
+ hostname = self.path.split('/tg-edge/')[1]
+ if len(hostname) > 0:
+ print('[%s] --> Hostname "%s" accepted, fetching info from DB' % (self.client_address[0], hostname))
+ template_parsed = template_parse(template_get('ex2200'), hostname)
+ if template_parsed:
+ print('[%s] --> Template successfully populated' % self.client_address[0])
+ print('[%s] --> Sending response to client' % self.client_address[0])
+ self.send_response(200)
+ self.send_header("Content-type", "text/plain")
+ self.end_headers()
+ self.wfile.write(bytes(template_parsed, "utf-8"))
+ print('[%s] --> Success - %s bytes sent to client' % (self.client_address[0], len(template_parsed)))
+ else:
+ print('[%s] --> Error - could not find hostname "%s" in DB' % (self.client_address[0], hostname))
+ else:
+ print('[%s] --> Rejected due to missing hostname' % self.client_address[0])
+
+ # Client asks for a file download - most likely a JunOS file
+ elif '/files/' in self.path:
+ # It seems that "http.server" escapes nastiness from the URL - ("/files/../../../root_file" => "/files/root_file")
+ requested_file = self.path.split('/files/')[1]
+ files_dir = 'fap/httpd/files/'
+ print('[%s] --> File request for "%s" in "%s"' % (self.client_address[0], requested_file, files_dir))
+ if os.path.isfile(files_dir + requested_file):
+ print('[%s] --> File found' % self.client_address[0])
+ try:
+ f = open(files_dir + requested_file)
+ self.send_response(200)
+ self.send_header('Content-type', 'application/x-gzip') # correct content type for tar.gz
+ self.end_headers()
+ print('[%s] --> File transfer started' % self.client_address[0])
+ f = open(files_dir + requested_file, 'rb')
+ self.wfile.write(f.read())
+ f.close()
+ print('[%s] --> File transfer completed' % self.client_address[0])
+ return
+ except IOError:
+ self.send_error(404,'File Not Found: %s' % self.path)
+ print('[%s] --> ERROR 404 - File not found' % self.client_address[0])
+ pass
+ except:
+ print('[%s] --> Generic error during file reading' % self.client_address[0])
+ pass
+ else:
+ print('[%s] --> File request rejected due to nonexisting file' % self.client_address[0])
+ else:
+ print('[%s] --> rejected due to bad URI' % self.client_address[0])
+ # silence stderr from BaseHTTPRequestHandler
+ # source: http://stackoverflow.com/questions/3389305/how-to-silent-quiet-httpserver-and-basichttprequesthandlers-stderr-output
+ def log_message(self, format, *args):
+ return
+
+ httpd_instance = HTTPServer((settings['http']['host'], settings['http']['port']), httpd)
+ print("\n[%s] Server Starts - %s:%s" % (time.asctime(), settings['http']['host'], settings['http']['port']))
+
+ try:
+ httpd_instance.serve_forever()
+ except KeyboardInterrupt:
+ pass
+
+ httpd_instance.server_close()
+ print("\n\n[%s] HTTP Server stopped\n" % time.asctime())
+
+if __name__ == "__main__":
+ main()
diff --git a/fap/httpd/terminal.log b/fap/httpd/terminal.log
new file mode 100644
index 0000000..bedb829
--- /dev/null
+++ b/fap/httpd/terminal.log
@@ -0,0 +1,14 @@
+j@lappie:~/git/tgmanage$ sudo python3 fap/httpd/server_http.py
+
+[Thu Feb 19 23:15:45 2015] Server Starts - 0.0.0.0:80
+[10.0.200.101] [Fri Feb 20 00:18:25 2015] Incoming HTTP GET URI://tg-edge/e-00-1
+[10.0.200.101] --> Hostname "e-00-1" accepted, fetching info from DB
+[10.0.200.101] --> Template successfully populated
+[10.0.200.101] --> Sending response to client
+[10.0.200.101] --> Success - 1437 bytes sent to client
+[10.0.200.101] [Fri Feb 20 00:18:26 2015] Incoming HTTP GET URI://files/jinstall-ex-2200-12.3R6.6-domestic-signed.tgz
+[10.0.200.101] --> File request for "jinstall-ex-2200-12.3R6.6-domestic-signed.tgz" in "fap/httpd/files/"
+[10.0.200.101] --> File found
+[10.0.200.101] --> File transfer started
+[10.0.200.101] --> File transfer completed
+
diff --git a/fap/proof_of_concepts/distro_during_testing.config b/fap/proof_of_concepts/distro_during_testing.config
new file mode 100644
index 0000000..1b92507
--- /dev/null
+++ b/fap/proof_of_concepts/distro_during_testing.config
@@ -0,0 +1,355 @@
+root@distro0gw> show configuration
+## Last commit: 2013-05-03 23:06:23 UTC by root
+version 12.1R6.6;
+system {
+ host-name distro0gw;
+ root-authentication {
+ encrypted-password "$1$SPgmZ0Nq$u2R7lVJJAv1I3paV1Go0z0"; ## SECRET-DATA
+ }
+ login {
+ user tg15 {
+ uid 2003;
+ class super-user;
+ authentication {
+ encrypted-password "$1$Mhjp603p$t6F4RTyx8igBGDZ6zj7A3/"; ## SECRET-DATA
+ }
+ }
+ }
+ syslog {
+ user * {
+ any emergency;
+ }
+ file messages {
+ any notice;
+ authorization info;
+ }
+ file interactive-commands {
+ interactive-commands any;
+ }
+ }
+}
+chassis {
+ aggregated-devices {
+ ethernet {
+ device-count 10;
+ }
+ }
+}
+interfaces {
+ ge-0/0/0 {
+ description "ae0/access mgmt";
+ inactive: ether-options {
+ 802.3ad ae0;
+ }
+ unit 0 {
+ family ethernet-switching {
+ port-mode access;
+ vlan {
+ members mgmt;
+ }
+ }
+ }
+ }
+ ge-0/0/1 {
+ description ae0;
+ ether-options {
+ 802.3ad ae0;
+ }
+ }
+ ge-0/0/2 {
+ description ae0;
+ ether-options {
+ 802.3ad ae0;
+ }
+ }
+ ge-0/0/3 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/0/4 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/0/5 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/0/6 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/0/7 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/0/8 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/0/9 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/0/10 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/0/11 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/0/12 {
+ description "RPI dhcp-client test";
+ unit 0 {
+ family ethernet-switching {
+ port-mode access;
+ vlan {
+ members mgmt;
+ }
+ }
+ }
+ }
+ ge-0/0/13 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/0/14 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/0/15 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/0/16 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/0/17 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/0/18 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/0/19 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/0/20 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/0/21 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/0/22 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/0/23 {
+ unit 0 {
+ description "Til laptopen";
+ family inet {
+ address 10.0.100.1/24;
+ }
+ }
+ }
+ ge-0/1/0 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ xe-0/1/0 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/1/1 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ xe-0/1/1 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/1/2 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ xe-0/1/2 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ge-0/1/3 {
+ unit 0 {
+ family ethernet-switching;
+ }
+ }
+ ae0 {
+ description "Aggregation til EX2200";
+ aggregated-ether-options {
+ minimum-links 2;
+ lacp {
+ active;
+ }
+ }
+ unit 0 {
+ family ethernet-switching {
+ port-mode trunk;
+ vlan {
+ members [ deltagere mgmt ];
+ }
+ }
+ }
+ }
+ me0 {
+ unit 0 {
+ family inet;
+ }
+ }
+ vlan {
+ unit 200 {
+ description "Deltagere L3 interface";
+ }
+ unit 300 {
+ description "Management L3 interface";
+ family inet {
+ address 10.0.200.1/24;
+ }
+ }
+ }
+}
+forwarding-options {
+ helpers {
+ bootp {
+ interface {
+ vlan.200 {
+ description "Deltagere DHCP-server";
+ server 1.2.3.4;
+ }
+ vlan.300 {
+ description "MGMT + bootstrap";
+ server 10.0.100.2;
+ dhcp-option82 {
+ circuit-id {
+ prefix hostname;
+ use-interface-description;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+event-options {
+ policy ae0down {
+ events snmp_trap_link_down;
+ attributes-match {
+ snmp_trap_link_down.interface-name matches ae0;
+ }
+ then {
+ change-configuration {
+ retry count 5 interval 10;
+ commands {
+ "deactivate interface ge-0/0/0 ether-options";
+ "activate interface ge-0/0/0 unit 0";
+ }
+ user-name tg15;
+ commit-options {
+ log "deactivating 802.3 from ge-0/0/0 (ae0). Activating access port";
+ }
+ }
+ }
+ }
+ policy ae0up {
+ events snmp_trap_link_up;
+ attributes-match {
+ snmp_trap_link_up.interface-name matches ae0;
+ }
+ then {
+ change-configuration {
+ retry count 5 interval 10;
+ commands {
+ "deactivate interface ge-0/0/0 unit 0";
+ "activate interface ge-0/0/0 ether-options";
+ }
+ user-name tg15;
+ commit-options {
+ log "activating 802.3 at ge-0/0/0 (ae0). Deactivating access port";
+ }
+ }
+ }
+ }
+}
+protocols {
+ igmp-snooping {
+ vlan all;
+ }
+ rstp;
+ lldp {
+ interface all;
+ }
+ lldp-med {
+ interface all;
+ }
+}
+ethernet-switching-options {
+ analyzer dump {
+ input {
+ ingress {
+ interface ge-0/0/23.0;
+ }
+ egress {
+ interface ge-0/0/23.0;
+ }
+ }
+ output {
+ interface {
+ ge-0/0/22.0;
+ }
+ }
+ }
+ storm-control {
+ interface all;
+ }
+}
+vlans {
+ deltagere {
+ vlan-id 200;
+ l3-interface vlan.200;
+ }
+ mgmt {
+ vlan-id 300;
+ l3-interface vlan.300;
+ }
+}
+poe {
+ interface all;
+}
+
diff --git a/fap/proof_of_concepts/tg15-tech82-poc1.tar.gz b/fap/proof_of_concepts/tg15-tech82-poc1.tar.gz
new file mode 100644
index 0000000..2844b59
--- /dev/null
+++ b/fap/proof_of_concepts/tg15-tech82-poc1.tar.gz
Binary files differ