diff options
author | Jonas Lindstad <jonaslindstad@gmail.com> | 2015-03-16 23:09:40 +0100 |
---|---|---|
committer | Jonas Lindstad <jonaslindstad@gmail.com> | 2015-03-16 23:09:40 +0100 |
commit | 939b606944b6892d26581bce598c66c790786de9 (patch) | |
tree | 6780dc26bd26c22ca8dce6a7832841b5f4efc37a /fap | |
parent | 88dfb934e546d02d1f4356032e91277f9f5b94f1 (diff) |
rename + fixes
Diffstat (limited to 'fap')
-rw-r--r-- | fap/README.md | 31 | ||||
-rw-r--r-- | fap/database/README.md | 111 | ||||
-rw-r--r-- | fap/database/terminal_after_first_successfull_test | 14 | ||||
-rw-r--r-- | fap/dhcpd/DHCP_protocol_breakdown.txt | 18 | ||||
-rw-r--r-- | fap/dhcpd/module_craft_option.py | 79 | ||||
-rw-r--r-- | fap/dhcpd/module_lease.py | 174 | ||||
-rw-r--r-- | fap/dhcpd/server_dhcp.py | 323 | ||||
-rw-r--r-- | fap/dhcpd/terminal.log | 146 | ||||
-rw-r--r-- | fap/httpd/ex2200.template | 249 | ||||
-rw-r--r-- | fap/httpd/server_http.py | 143 | ||||
-rw-r--r-- | fap/httpd/terminal.log | 14 | ||||
-rw-r--r-- | fap/proof_of_concepts/distro_during_testing.config | 355 | ||||
-rw-r--r-- | fap/proof_of_concepts/tg15-tech82-poc1.tar.gz | bin | 0 -> 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 Binary files differnew file mode 100644 index 0000000..2844b59 --- /dev/null +++ b/fap/proof_of_concepts/tg15-tech82-poc1.tar.gz |