def setUp(self): self.packet_fixture = bytes.fromhex( '0e' # Message type Leasequery 'e86f0c' # Transaction ID '0001' # Option type 1: OPTION_CLIENT_ID '000a' # Option length: 10 '0003' # DUID type: DUID_LL '0001' # Hardware type: Ethernet '001ee6f77d00' # MAC Address '002c' # Option type 44: OPTION_LQ_QUERY '0017' # Option length: 23 '01' # Query type: QUERY_BY_ADDRESS 'fe800000000000000000000000000001' # Link address: fe80::1 '0006' # Option type: OPTION_ORO '0002' # Option length: 2 '002f' # Requested option: OPTION_LQ_RELAY_DATA ) self.message_fixture = LeasequeryMessage( transaction_id=bytes.fromhex('e86f0c'), options=[ ClientIdOption(duid=LinkLayerDUID( hardware_type=1, link_layer_address=bytes.fromhex('001ee6f77d00'))), LQQueryOption( query_type=QUERY_BY_ADDRESS, link_address=IPv6Address('fe80::1'), options=[ OptionRequestOption( requested_options=[OPTION_LQ_RELAY_DATA]), ]), ]) self.parse_packet()
def test_query_messed_up_prefix(self): bundle = TransactionBundle(self.relayed_solicit_message, received_over_multicast=False) bundle.response = reply_message ia_na_option = bundle.response.get_option_of_type(IANAOption) ia_address = ia_na_option.get_option_of_type(IAAddressOption) query = LQQueryOption(QUERY_BY_ADDRESS, options=[ia_address]) # Messed-up data, a log message should appear with TemporaryDirectory() as tmp_dir_name: store = LeasequerySqliteStore(os.path.join(tmp_dir_name, 'lq.sqlite')) store.worker_init([]) store.remember_lease(bundle) # Mess up the data in our poor database db = sqlite3.connect(store.sqlite_filename) db.row_factory = sqlite3.Row db.execute("UPDATE prefixes SET first_address='2001:0db8:0000:0000:0000:0000:0000:0000'") db.commit() with self.assertLogs('', 'NOTSET')as cm: nr_found, results = store.find_leases(query) results = list(results) self.assertEqual(nr_found, 1) self.assertEqual(len(results), 1) self.assertEqual(len(cm.output), 1) self.assertRegex(cm.output[0], 'Ignoring invalid prefix range')
def find_leases( self, query: LQQueryOption ) -> Tuple[int, Iterable[Tuple[IPv6Address, ClientDataOption]]]: """ Find all leases that match the given query. :param query: The query :return: The number of leases and an iterator over tuples of link-address and corresponding client data """ # Run everything in one transaction with self.db: if query.query_type == QUERY_BY_ADDRESS: client_row_ids = self.find_client_by_address(query) elif query.query_type == QUERY_BY_CLIENT_ID: client_row_ids = self.find_client_by_client_id(query) elif query.query_type == QUERY_BY_RELAY_ID: client_row_ids = self.find_client_by_relay_id(query) elif query.query_type == QUERY_BY_LINK_ADDRESS: client_row_ids = self.find_client_by_link_address(query) elif query.query_type == QUERY_BY_REMOTE_ID: client_row_ids = self.find_client_by_remote_id(query) else: # We can't handle this query return -1, [] if not client_row_ids: # None found return 0, [] # Generate records for these client IDs oro = query.get_option_of_type(OptionRequestOption) requested_options = oro.requested_options if oro else [] return len(client_row_ids), self.generate_client_data_options( client_row_ids, requested_options)
def find_client_by_address(self, query: LQQueryOption) -> List[int]: """ Get the row ids of the clients we want to return. :param query: The query :return: A list of row ids """ # Get the requested address from the query address_option = query.get_option_of_type(IAAddressOption) if not address_option: raise ReplyWithLeasequeryError( STATUS_MALFORMED_QUERY, "Address queries must contain an address") address = address_option.address.exploded if query.link_address.is_unspecified: cur = self.db.execute( "SELECT client_fk FROM addresses WHERE address=?" " UNION " "SELECT client_fk FROM prefixes WHERE ? BETWEEN first_address AND last_address", (address, address)) return [row['client_fk'] for row in cur] else: cur = self.db.execute( "SELECT id FROM clients WHERE link_address=? AND (" "id IN (SELECT client_fk FROM addresses WHERE address=?)" " OR " "id IN (SELECT client_fk FROM prefixes WHERE ? BETWEEN first_address AND last_address)" ")", (query.link_address.exploded, address, address)) return [row['id'] for row in cur]
def find_client_by_client_id(self, query: LQQueryOption) -> List[int]: """ Get the row ids of the clients we want to return. :param query: The query :return: A list of row ids """ # Get the requested client ID from the query client_id_option = query.get_option_of_type(ClientIdOption) if not client_id_option: raise ReplyWithLeasequeryError( STATUS_MALFORMED_QUERY, "Client-ID queries must contain a client ID") client_id_str = self.encode_duid(client_id_option.duid) if query.link_address.is_unspecified: cur = self.db.execute("SELECT id FROM clients WHERE client_id=?", (client_id_str, )) else: cur = self.db.execute( "SELECT id FROM clients WHERE client_id=? AND link_address=?", (client_id_str, query.link_address.exploded)) return [row['id'] for row in cur]
def find_client_by_remote_id(self, query: LQQueryOption) -> List[int]: """ Get the row ids of the clients we want to return. :param query: The query :return: A list of row ids """ # Get the requested remote ID from the query remote_id_option = query.get_option_of_type(RemoteIdOption) if not remote_id_option: raise ReplyWithLeasequeryError( STATUS_MALFORMED_QUERY, "Remote-ID queries must contain a remote ID") remote_id_str = self.encode_remote_id(remote_id_option) if query.link_address.is_unspecified: cur = self.db.execute( "SELECT client_fk FROM relay_ids WHERE relay_id=?", (remote_id_str, )) return [row['client_fk'] for row in cur] else: cur = self.db.execute( "SELECT id FROM clients " "WHERE link_address=? AND id IN (SELECT client_fk FROM relay_ids WHERE relay_id=?)", (query.link_address.exploded, remote_id_str)) return [row['id'] for row in cur]
def test_query_by_unspecified_link_address(self): bundle = TransactionBundle(self.relayed_solicit_message, received_over_multicast=False) bundle.response = reply_message query = LQQueryOption(QUERY_BY_LINK_ADDRESS, link_address=IPv6Address('::')) self.query(bundle, query)
def test_query_by_unknown(self): bundle = TransactionBundle(self.relayed_solicit_message, received_over_multicast=False) bundle.response = reply_message query = LQQueryOption(-1) # No valid query provided, no data self.query_empty(bundle, query, invalid=True)
def test_query_by_address_malformed(self): bundle = TransactionBundle(self.relayed_solicit_message, received_over_multicast=False) bundle.response = reply_message query = LQQueryOption(QUERY_BY_ADDRESS) with self.assertRaisesRegex(ReplyWithLeasequeryError, 'Address queries must contain an address'): self.query(bundle, query)
def test_query_by_remote_id_malformed(self): bundle = TransactionBundle(self.relayed_solicit_message, received_over_multicast=False) bundle.response = reply_message query = LQQueryOption(QUERY_BY_REMOTE_ID) with self.assertRaisesRegex(ReplyWithLeasequeryError, 'Remote-ID queries must contain a remote ID'): self.query(bundle, query)
def create_link_address_query(options) -> LQQueryOption: """ Create query option for link-address query. :param options: Options from the main argument parser :return: The Leasequery """ return LQQueryOption(QUERY_BY_LINK_ADDRESS, options.link_address)
def test_query_by_client_id_on_wrong_link(self): bundle = TransactionBundle(self.relayed_solicit_message, received_over_multicast=False) bundle.response = reply_message client_id_option = bundle.response.get_option_of_type(ClientIdOption) query = LQQueryOption(QUERY_BY_CLIENT_ID, link_address=IPv6Address('3ffe::'), options=[client_id_option]) self.query_empty(bundle, query)
def test_query_by_relay_id_on_link(self): bundle = TransactionBundle(self.relayed_solicit_message, received_over_multicast=False) bundle.response = reply_message relay_id_option = bundle.incoming_relay_messages[-1].get_option_of_type(RelayIdOption) query = LQQueryOption(QUERY_BY_RELAY_ID, link_address=bundle.link_address, options=[relay_id_option]) self.query(bundle, query)
def create_relay_id_query(options) -> LQQueryOption: """ Create query option for relay-id query. :param options: Options from the main argument parser :return: The Leasequery """ return LQQueryOption(QUERY_BY_RELAY_ID, options.link_address, [RelayIdOption(parse_duid(options.duid))])
def create_client_id_query(options) -> LQQueryOption: """ Create query option for client-id query. :param options: Options from the main argument parser :return: The Leasequery """ return LQQueryOption(QUERY_BY_CLIENT_ID, options.link_address, [ClientIdOption(parse_duid(options.duid))])
def create_client_address_query(options) -> LQQueryOption: """ Create query option for address query. :param options: Options from the main argument parser :return: The Leasequery """ return LQQueryOption(QUERY_BY_ADDRESS, options.link_address, [IAAddressOption(options.address)])
def test_query_by_address_with_extra_data(self): bundle = TransactionBundle(self.relayed_solicit_message, received_over_multicast=False) bundle.response = reply_message ia_na_option = bundle.response.get_option_of_type(IANAOption) ia_address = ia_na_option.get_option_of_type(IAAddressOption) query = LQQueryOption(QUERY_BY_ADDRESS, options=[ia_address, OptionRequestOption([OPTION_DNS_SERVERS])]) self.query(bundle, query, [RecursiveNameServersOption])
def test_query_by_address_with_relay_data(self): bundle = TransactionBundle(self.relayed_solicit_message, received_over_multicast=False) bundle.response = reply_message ia_na_option = bundle.response.get_option_of_type(IANAOption) ia_address = ia_na_option.get_option_of_type(IAAddressOption) query = LQQueryOption(QUERY_BY_ADDRESS, options=[ia_address, OptionRequestOption([OPTION_LQ_RELAY_DATA])]) self.query(bundle, query, [LQRelayDataOption])
def test_query_by_address_on_wrong_link(self): bundle = TransactionBundle(self.relayed_solicit_message, received_over_multicast=False) bundle.response = reply_message ia_na_option = bundle.response.get_option_of_type(IANAOption) ia_address = ia_na_option.get_option_of_type(IAAddressOption) query = LQQueryOption(QUERY_BY_ADDRESS, link_address=IPv6Address('3ffe::'), options=[ia_address]) self.query_empty(bundle, query)
def test_query_by_remote_id_on_wrong_link(self): bundle = TransactionBundle(self.relayed_solicit_message, received_over_multicast=False) bundle.response = reply_message # Test every remote-id for relay_message in bundle.incoming_relay_messages: for option in relay_message.get_options_of_type(RemoteIdOption): with self.subTest(msg="{}:{}".format(option.enterprise_number, normalise_hex(option.remote_id))): query = LQQueryOption(QUERY_BY_REMOTE_ID, link_address=IPv6Address('3ffe::'), options=[option]) self.query_empty(bundle, query)
def create_remote_id_query(options) -> LQQueryOption: """ Create query option for remote-id query. :param options: Options from the main argument parser :return: The Leasequery """ return LQQueryOption(QUERY_BY_REMOTE_ID, options.link_address, [ RemoteIdOption(int(options.enterprise_nr), bytes.fromhex(options.remote_id)) ])
def test_query_by_relay_id_on_wrong_link(self): bundle = TransactionBundle(self.relayed_solicit_message, received_over_multicast=False) bundle.response = reply_message client_id_option = bundle.response.get_option_of_type(ClientIdOption) query = LQQueryOption(QUERY_BY_RELAY_ID, link_address=IPv6Address('3ffe::'), options=[RelayIdOption(duid=client_id_option.duid)]) # Our test data doesn't have a relay-id, so no results expected self.query_empty(bundle, query)
def test_bad_option_length(self): with self.assertRaisesRegex(ValueError, 'shorter than the minimum length'): LQQueryOption.parse(bytes.fromhex('002c001001fe800000000000000000000000000001')) with self.assertRaisesRegex(ValueError, 'longer than the available buffer'): LQQueryOption.parse(bytes.fromhex('002c001201fe800000000000000000000000000001')) with self.assertRaisesRegex(ValueError, 'length does not match'): LQQueryOption.parse(bytes.fromhex('002c001601fe80000000000000000000000000000100060002002f'))
def setUp(self): self.option_bytes = bytes.fromhex( '002c' # Option type 44: OPTION_LQ_QUERY '0017' # Option length: 23 '01' # Query type: QUERY_BY_ADDRESS 'fe800000000000000000000000000001' # Link address: fe80::1 '0006' # Option type: OPTION_ORO '0002' # Option length: 2 '002f' # Requested option: OPTION_LQ_RELAY_DATA ) self.option_object = LQQueryOption( query_type=QUERY_BY_ADDRESS, link_address=IPv6Address('fe80::1'), options=[ OptionRequestOption(requested_options=[OPTION_LQ_RELAY_DATA]), ]) self.parse_option()
def test_bad_option_length(self): with self.assertRaisesRegex(ValueError, 'shorter than the minimum length'): LQQueryOption.parse( bytes.fromhex('002c001001fe800000000000000000000000000001')) with self.assertRaisesRegex(ValueError, 'longer than the available buffer'): LQQueryOption.parse( bytes.fromhex('002c001201fe800000000000000000000000000001')) with self.assertRaisesRegex(ValueError, 'length does not match'): LQQueryOption.parse( bytes.fromhex( '002c001601fe80000000000000000000000000000100060002002f'))
def test_parse_wrong_type(self): with self.assertRaisesRegex(ValueError, 'does not contain LQQueryOption data'): option = LQQueryOption() option.load_from(b'00020010ff12000000000000000000000000abcd')