def setUp(self): self.bundle = TransactionBundle(relayed_solicit_message, received_over_multicast=False) self.shallow_bundle = TransactionBundle(solicit_message, received_over_multicast=True) self.deep_bundle = TransactionBundle(RelayForwardMessage( hop_count=0, link_address=IPv6Address('2001:db8:ffff:2::1'), peer_address=IPv6Address('fe80::3631:c4ff:fe3c:b2f1'), options=[ RelayMessageOption(relayed_message=relayed_solicit_message), ]), received_over_multicast=False, marks=['some', 'marks']) self.ia_bundle = TransactionBundle(SolicitMessage(options=[ IANAOption(b'0001'), IANAOption(b'0002'), IATAOption(b'0003'), IATAOption(b'0004'), IAPDOption(b'0005'), IAPDOption(b'0006'), ]), received_over_multicast=False) self.option_handlers = [ InterfaceIdOptionHandler(), ]
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_absent_option_echo_request(self): relayed_solicit_message = RelayForwardMessage( hop_count=1, link_address=IPv6Address('2001:db8:ffff:1::1'), peer_address=IPv6Address('fe80::3631:c4ff:fe3c:b2f1'), options=[ RelayMessageOption(relayed_message=SolicitMessage( transaction_id=bytes.fromhex('f350d6'), options=[ ElapsedTimeOption(elapsed_time=0), ClientIdOption(duid=LinkLayerDUID(hardware_type=1, link_layer_address=bytes.fromhex('3431c43cb2f1'))), IANAOption(iaid=bytes.fromhex('c43cb2f1')), ], )), EchoRequestOption(requested_options=[OPTION_SUBSCRIBER_ID]), UnknownOption(option_type=65535), InterfaceIdOption(interface_id=b'Fa2/3'), RemoteIdOption(enterprise_number=9, remote_id=bytes.fromhex('020023000001000a0003000100211c7d486e')), ] ) bundle = TransactionBundle(incoming_message=relayed_solicit_message, received_over_multicast=True) self.message_handler.handle(bundle, StatisticsSet()) self.assertIsInstance(bundle.outgoing_message, RelayReplyMessage) self.assertEqual(len(bundle.outgoing_message.options), 2) self.assertIsInstance(bundle.outgoing_message.options[0], InterfaceIdOption) self.assertIsInstance(bundle.outgoing_message.options[1], RelayMessageOption)
def test_rapid_solicit_message(self): bundle = TransactionBundle(incoming_message=solicit_message, received_over_multicast=True, marks=['one', 'two']) self.rapid_message_handler.handle(bundle, StatisticsSet()) result = bundle.outgoing_message self.assertIsInstance(result, AdvertiseMessage) self.assertEqual(result.transaction_id, solicit_message.transaction_id) self.assertEqual(result.get_option_of_type(ClientIdOption), solicit_message.get_option_of_type(ClientIdOption)) self.assertEqual( result.get_option_of_type(ServerIdOption).duid, self.duid) self.assertEqual( result.get_option_of_type(IANAOption).get_option_of_type( StatusCodeOption).status_code, STATUS_NO_ADDRS_AVAIL) self.assertEqual( result.get_option_of_type(IAPDOption).get_option_of_type( StatusCodeOption).status_code, STATUS_NO_PREFIX_AVAIL) # Check if the handlers are called correctly for method_name in ['pre', 'handle', 'post']: method = getattr(self.dummy_handler, method_name) self.assertEqual(method.call_count, 1) args, kwargs = method.call_args self.assertEqual(len(args), 1) self.assertEqual(len(kwargs), 0) self.assertIsInstance(args[0], TransactionBundle) # Check the types and values at various stages # In the pre phase there is no response yet bundle = self.dummy_handler.pre.call_args[0][0] self.assertEqual(bundle.request, solicit_message) self.assertEqual(bundle.incoming_relay_messages, []) self.assertEqual(bundle.marks, {'one', 'two', 'pre-setup'}) self.assertIsNone(bundle.response) self.assertIsNone(bundle.outgoing_relay_messages) # In the handle phase there is an AdvertiseMessage bundle = self.dummy_handler.handle.call_args[0][0] self.assertEqual(bundle.request, solicit_message) self.assertEqual(bundle.incoming_relay_messages, []) self.assertEqual( bundle.marks, {'one', 'two', 'pre-setup', 'pre-cleanup', 'handle-setup'}) self.assertIsInstance(bundle.response, AdvertiseMessage) self.assertEqual(bundle.outgoing_relay_messages, []) # In the post phase there is still an AdvertiseMessage (rapid commit, but no rapid commit rejections) bundle = self.dummy_handler.post.call_args[0][0] self.assertEqual(bundle.request, solicit_message) self.assertEqual(bundle.incoming_relay_messages, []) self.assertEqual( bundle.marks, { 'one', 'two', 'pre-setup', 'pre-cleanup', 'handle-setup', 'handle-cleanup', 'post-setup' }) self.assertIsInstance(bundle.response, AdvertiseMessage) self.assertEqual(bundle.outgoing_relay_messages, [])
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 test_remember_lease_differently(self): bundle = TransactionBundle(self.relayed_solicit_message, received_over_multicast=False) bundle.response = reply_message client_id_option = bundle.request.get_option_of_type(ClientIdOption) ia_na_option = bundle.response.get_option_of_type(IANAOption) ia_address = ia_na_option.get_option_of_type(IAAddressOption) ia_pd_option = bundle.response.get_option_of_type(IAPDOption) ia_prefix = ia_pd_option.get_option_of_type(IAPrefixOption) remote_ids = set() for relay_message in bundle.incoming_relay_messages: for option in relay_message.get_options_of_type(RemoteIdOption): remote_ids.add("{}:{}".format(option.enterprise_number, normalise_hex(option.remote_id))) with TemporaryDirectory() as tmp_dir_name: store = LeasequerySqliteStore(os.path.join(tmp_dir_name, 'lq.sqlite')) store.worker_init([]) store.remember_lease(bundle) store.remember_lease(bundle) # Check that the data ended up in the database db = sqlite3.connect(store.sqlite_filename) db.row_factory = sqlite3.Row rows = list(db.execute("SELECT * FROM clients")) self.assertEqual(len(rows), 1) row = rows[0] client_row = row['id'] self.assertEqual(row['client_id'], normalise_hex(client_id_option.duid.save())) self.assertEqual(row['link_address'], bundle.link_address.exploded) self.assertAlmostEqual(row['last_interaction'], time.time(), delta=5) # print({key: row[key] for key in rows[0].keys()}) rows = list(db.execute("SELECT * FROM addresses")) self.assertEqual(len(rows), 1) row = rows[0] self.assertEqual(row['client_fk'], client_row) self.assertEqual(row['address'], ia_address.address.exploded) self.assertAlmostEqual(row['preferred_lifetime_end'], time.time() + ia_address.preferred_lifetime, delta=5) self.assertAlmostEqual(row['valid_lifetime_end'], time.time() + ia_address.valid_lifetime, delta=5) self.assertEqual(row['options'], b'') rows = list(db.execute("SELECT * FROM prefixes")) self.assertEqual(len(rows), 1) row = rows[0] self.assertEqual(row['client_fk'], client_row) self.assertEqual(row['first_address'], ia_prefix.prefix[0].exploded) self.assertEqual(row['last_address'], ia_prefix.prefix[-1].exploded) self.assertAlmostEqual(row['preferred_lifetime_end'], time.time() + ia_address.preferred_lifetime, delta=5) self.assertAlmostEqual(row['valid_lifetime_end'], time.time() + ia_address.valid_lifetime, delta=5) self.assertEqual(row['options'], b'') rows = list(db.execute("SELECT * FROM remote_ids")) self.assertEqual(len(rows), len(remote_ids)) self.assertSetEqual({row['remote_id'] for row in rows}, remote_ids) rows = list(db.execute("SELECT * FROM relay_ids")) self.assertEqual(len(rows), 1)
def test_accept_unicast_message(self): bundle = TransactionBundle(incoming_message=solicit_message, received_over_multicast=False, marks=['unicast-me']) self.message_handler.handle(bundle, StatisticsSet()) result = bundle.outgoing_message self.assertIsInstance(result, AdvertiseMessage) self.assertIsNone(result.get_option_of_type(StatusCodeOption))
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_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_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 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 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_empty_confirm_message(self): bundle = TransactionBundle( incoming_message=ConfirmMessage(transaction_id=b'abcd'), received_over_multicast=True, marks=['one']) self.message_handler.handle(bundle, StatisticsSet()) result = bundle.outgoing_message # ConfirmMessage without IANAOption/IATAOption/IAPDOption must be ignored self.assertIsNone(result)
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 test_empty_message(self): with self.assertLogs(level=logging.WARNING) as cm: bundle = TransactionBundle(incoming_message=RelayForwardMessage(), received_over_multicast=True) result = self.message_handler.handle(bundle, StatisticsSet()) self.assertIsNone(result) self.assertEqual(len(cm.output), 1) self.assertRegex(cm.output[0], '^WARNING:.*:A server should not receive')
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_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_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_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_remember_lease_non_interesting(self): bundle = TransactionBundle(confirm_message, received_over_multicast=False) bundle.response = reply_message with TemporaryDirectory() as tmp_dir_name: store = LeasequerySqliteStore(os.path.join(tmp_dir_name, 'lq.sqlite')) store.worker_init([]) store.remember_lease(bundle) # Check that nothing ended up in the database db = sqlite3.connect(store.sqlite_filename) rows = list(db.execute("SELECT 1 FROM clients")) self.assertEqual(len(rows), 0)
def test_badly_rejected_multicast_message(self): with self.assertLogs(level=logging.DEBUG) as cm: bundle = TransactionBundle(incoming_message=solicit_message, received_over_multicast=True, marks=['reject-me']) self.message_handler.handle(bundle, StatisticsSet()) result = bundle.outgoing_message self.assertIsNone(result) self.assertEqual(len(cm.output), 3) self.assertRegex(cm.output[0], '^DEBUG:.*:Handling SolicitMessage') self.assertRegex(cm.output[1], '^DEBUG:.*:.*multicast is required') self.assertRegex(cm.output[2], '^ERROR:.*:Not telling client to use multicast')
def test_ignorable_multicast_message(self): with self.assertLogs(level=logging.DEBUG) as cm: bundle = TransactionBundle(incoming_message=solicit_message, received_over_multicast=True, marks=['ignore-me']) self.message_handler.handle(bundle, StatisticsSet()) result = bundle.outgoing_message self.assertIsNone(result) self.assertEqual(len(cm.output), 3) self.assertRegex(cm.output[0], '^DEBUG:.*:Handling SolicitMessage') self.assertRegex(cm.output[1], '^INFO:.*:Configured to ignore SolicitMessage') self.assertRegex(cm.output[2], '^WARNING:.*:.*ignoring')
def test_empty_confirm_message(self): with self.assertLogs() as cm: bundle = TransactionBundle( incoming_message=ConfirmMessage(transaction_id=b'abcd'), received_over_multicast=True, marks=['one']) self.message_handler.handle(bundle, StatisticsSet()) result = bundle.outgoing_message self.assertEqual(len(cm.output), 1) self.assertRegex(cm.output[0], '^WARNING:.*:No IAs present in confirm reply') # ConfirmMessage without IANAOption/IATAOption/IAPDOption must be ignored self.assertIsNone(result)
def test_reject_unicast_message(self): with self.assertLogs(level=logging.DEBUG) as cm: bundle = TransactionBundle(incoming_message=solicit_message, received_over_multicast=False) self.message_handler.handle(bundle, StatisticsSet()) result = bundle.outgoing_message self.assertIsInstance(result, ReplyMessage) self.assertEqual( result.get_option_of_type(StatusCodeOption).status_code, STATUS_USE_MULTICAST) self.assertEqual(len(cm.output), 3) self.assertRegex(cm.output[0], '^DEBUG:.*:Handling SolicitMessage') self.assertRegex(cm.output[1], '^INFO:.*:Rejecting unicast SolicitMessage') self.assertRegex(cm.output[2], '^DEBUG:.*:.*multicast is required')
def test_not_implemented_message(self): class NotImplementedMessage(ClientServerMessage): message_type = 255 from_client_to_server = True with self.assertLogs() as cm: bundle = TransactionBundle( incoming_message=NotImplementedMessage(transaction_id=b'abcd'), received_over_multicast=True, marks=['one']) self.message_handler.handle(bundle, StatisticsSet()) result = bundle.outgoing_message self.assertEqual(len(cm.output), 1) self.assertRegex(cm.output[0], '^WARNING:.*:Do not know how to reply') self.assertIsNone(result)
def test_request_message(self): bundle = TransactionBundle(incoming_message=request_message, received_over_multicast=True, marks=['one']) self.message_handler.handle(bundle, StatisticsSet()) result = bundle.outgoing_message self.assertIsInstance(result, ReplyMessage) self.assertEqual(result.transaction_id, solicit_message.transaction_id) self.assertEqual(result.get_option_of_type(ClientIdOption), solicit_message.get_option_of_type(ClientIdOption)) self.assertEqual( result.get_option_of_type(ServerIdOption).duid, self.duid) self.assertEqual( result.get_option_of_type(IANAOption).get_option_of_type( StatusCodeOption).status_code, STATUS_NO_ADDRS_AVAIL) self.assertEqual( result.get_option_of_type(IAPDOption).get_option_of_type( StatusCodeOption).status_code, STATUS_NO_PREFIX_AVAIL)
def test_confirm_message(self): with self.assertLogs() as cm: bundle = TransactionBundle(incoming_message=confirm_message, received_over_multicast=True, marks=['one']) self.message_handler.handle(bundle, StatisticsSet()) result = bundle.outgoing_message self.assertEqual(len(cm.output), 1) self.assertRegex(cm.output[0], '^WARNING:.*:No handler confirmed') self.assertIsInstance(result, ReplyMessage) self.assertEqual(result.transaction_id, request_message.transaction_id) self.assertEqual(result.get_option_of_type(ClientIdOption), solicit_message.get_option_of_type(ClientIdOption)) self.assertEqual( result.get_option_of_type(ServerIdOption).duid, self.duid) self.assertEqual( result.get_option_of_type(StatusCodeOption).status_code, STATUS_NOT_ON_LINK)
def parse_incoming_request( incoming_packet: IncomingPacketBundle) -> TransactionBundle: """ Parse the incoming packet and add a RelayServerMessage around it containing the meta-data received from the listener. :param incoming_packet: The received packet :return: The parsed message in a transaction bundle """ # Parse message and validate length, incoming_message = Message.parse(incoming_packet.data) incoming_message.validate() # Determine the next hop count and construct useful log messages if isinstance(incoming_message, RelayForwardMessage): next_hop_count = incoming_message.hop_count + 1 else: next_hop_count = 0 # Collect the relay options relay_options = [] """:type: List[Option]""" relay_options.append( InterfaceIdOption( interface_id=incoming_packet.interface_name.encode('utf-8'))) relay_options.extend(incoming_packet.extra_options) relay_options.append(RelayMessageOption(relayed_message=incoming_message)) # Pretend to be an internal relay and wrap the message like a relay would wrapped_message = RelayForwardMessage( hop_count=next_hop_count, link_address=incoming_packet.link_address, peer_address=incoming_packet.sender, options=relay_options) # Create the transaction bundle return TransactionBundle( incoming_message=wrapped_message, received_over_multicast=incoming_packet.received_over_multicast, marks=incoming_packet.marks)
def test_unknown_message(self): with self.assertLogs() as cm: TransactionBundle(UnknownMessage(1608, b'Unknown'), False) self.assertEqual(len(cm.output), 1) self.assertRegex(cm.output[0], 'unrecognised message')
def test_wrong_way(self): with self.assertLogs() as cm: TransactionBundle(ReplyMessage(), False) self.assertEqual(len(cm.output), 1) self.assertRegex(cm.output[0], 'server should not receive')