def init_response(bundle: TransactionBundle): """ Create the message object in bundle.response :param bundle: The transaction bundle """ # Start building the response if isinstance(bundle.request, SolicitMessage): bundle.response = AdvertiseMessage(bundle.request.transaction_id) elif isinstance(bundle.request, (RequestMessage, RenewMessage, RebindMessage, ReleaseMessage, DeclineMessage, InformationRequestMessage)): bundle.response = ReplyMessage(bundle.request.transaction_id) elif isinstance(bundle.request, ConfirmMessage): # Receipt of Confirm Messages: If [...] there were no addresses in any of the IAs sent by the client, the # server MUST NOT send a reply to the client. for option in bundle.request.get_options_of_type((IANAOption, IATAOption, IAPDOption)): if option.get_options_of_type((IAAddressOption, IAPrefixOption)): # Found an address or prefix option break else: # Not found: ignore request raise CannotRespondError("No IAs present in confirm reply") bundle.response = ReplyMessage(bundle.request.transaction_id) else: raise CannotRespondError("Do not know how to reply to {}".format(type(bundle.request).__name__)) # Build the plain chain of relay reply messages bundle.create_outgoing_relay_messages()
def init_response(bundle: TransactionBundle): """ Create the message object in bundle.response :param bundle: The transaction bundle """ # Start building the response if isinstance(bundle.request, SolicitMessage): bundle.response = AdvertiseMessage(bundle.request.transaction_id) elif isinstance(bundle.request, (RequestMessage, RenewMessage, RebindMessage, ReleaseMessage, DeclineMessage, InformationRequestMessage)): bundle.response = ReplyMessage(bundle.request.transaction_id) elif isinstance(bundle.request, ConfirmMessage): # Receipt of Confirm Messages: If [...] there were no addresses in any of the IAs sent by the client, the # server MUST NOT send a reply to the client. for option in bundle.request.get_options_of_type((IANAOption, IATAOption, IAPDOption)): if option.get_options_of_type((IAAddressOption, IAPrefixOption)): # Found an address or prefix option break else: # Not found: ignore request raise CannotRespondError bundle.response = ReplyMessage(bundle.request.transaction_id) else: logger.warning("Do not know how to reply to {}".format(type(bundle.request).__name__)) raise CannotRespondError # Build the plain chain of relay reply messages bundle.create_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_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_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_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_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_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_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_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 post(self, bundle: TransactionBundle): """ Upgrade the response from a AdvertiseMessage to a ReplyMessage if appropriate :param bundle: The transaction bundle """ # Does this transaction even allow rapid commit? if not bundle.allow_rapid_commit: return # We only look for SolicitMessages that have a RapidCommitOption if not isinstance( bundle.request, SolicitMessage ) or not bundle.request.get_option_of_type(RapidCommitOption): return # And only if the current response is an AdvertiseMessage if not isinstance(bundle.response, AdvertiseMessage): return # Ok, this looks promising, do extra checks if requested if not self.rapid_commit_rejections: # Ok, we don't want to rapid-commit rejections. Check for them. if bundle.get_unhandled_options( (IANAOption, IATAOption, IAPDOption)): # Unhandled options. We are post-processing, so they are not going to be answered anymore return # Did we already refuse anything? ia_options = [ option for option in bundle.response.options if isinstance(option, (IANAOption, IATAOption)) ] for option in ia_options: status = option.get_option_of_type(StatusCodeOption) if status and status.status_code == STATUS_NO_ADDRS_AVAIL: # Refusal: don't do anything return iapd_options = [ option for option in bundle.response.options if isinstance(option, IAPDOption) ] for option in iapd_options: status = option.get_option_of_type(StatusCodeOption) if status and status.status_code == STATUS_NO_PREFIX_AVAIL: # Refusal: don't do anything return # It seems the request and response qualify: upgrade to ReplyMessage bundle.response = ReplyMessage(bundle.response.transaction_id, [RapidCommitOption()] + bundle.response.options)
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 post(self, bundle: TransactionBundle): """ Upgrade the response from a AdvertiseMessage to a ReplyMessage if appropriate :param bundle: The transaction bundle """ # Does this transaction even allow rapid commit? if not bundle.allow_rapid_commit: return # We only look for SolicitMessages that have a RapidCommitOption if not isinstance(bundle.request, SolicitMessage) or not bundle.request.get_option_of_type(RapidCommitOption): return # And only if the current response is an AdvertiseMessage if not isinstance(bundle.response, AdvertiseMessage): return # Ok, this looks promising, do extra checks if requested if not self.rapid_commit_rejections: # Ok, we don't want to rapid-commit rejections. Check for them. if bundle.get_unhandled_options((IANAOption, IATAOption, IAPDOption)): # Unhandled options. We are post-processing, so they are not going to be answered anymore return # Did we already refuse anything? ia_options = [option for option in bundle.response.options if isinstance(option, (IANAOption, IATAOption))] for option in ia_options: status = option.get_option_of_type(StatusCodeOption) if status and status.status_code == STATUS_NOADDRSAVAIL: # Refusal: don't do anything return iapd_options = [option for option in bundle.response.options if isinstance(option, IAPDOption)] for option in iapd_options: status = option.get_option_of_type(StatusCodeOption) if status and status.status_code == STATUS_NOPREFIXAVAIL: # Refusal: don't do anything return # It seems the request and response qualify: upgrade to ReplyMessage bundle.response = ReplyMessage(bundle.response.transaction_id, [RapidCommitOption()] + bundle.response.options)
def handle(self, incoming_message: Message, received_over_multicast: bool, marks: Iterable[str] = None) -> Optional[Message]: """ The main dispatcher for incoming messages. :param incoming_message: The parsed incoming request :param received_over_multicast: Whether the request was received over multicast :param marks: Marks to add to the transaction bundle, usually set by the listener :returns: The message to reply with """ # Create the transaction bundle = TransactionBundle(incoming_message=incoming_message, received_over_multicast=received_over_multicast, allow_rapid_commit=self.allow_rapid_commit) if not bundle.request: # Nothing to do... return None # Add the marks so the filters can take them into account if marks: bundle.marks.update(marks) # Log what we are doing (low-detail, so not DEBUG_HANDLING here) logger.debug("Handling {}".format(bundle)) # Collect the handlers handlers = self.get_handlers(bundle) try: # Pre-process the request for handler in handlers: handler.pre(bundle) # Init the response self.init_response(bundle) # Process the request for handler in handlers: logger.log(DEBUG_HANDLING, "Applying {}".format(handler)) handler.handle(bundle) # Post-process the request for handler in handlers: handler.post(bundle) except CannotRespondError: logger.debug("Cannot respond to this message: ignoring") bundle.response = None except UseMulticastError: logger.debug("Unicast request received when multicast is required: informing client") bundle.response = self.construct_use_multicast_reply(bundle) if bundle.response: logger.log(DEBUG_HANDLING, "Responding with {}".format(bundle.response.__class__.__name__)) else: logger.log(DEBUG_HANDLING, "Not responding") return bundle.outgoing_message
def handle(self, bundle: TransactionBundle, statistics: StatisticsSet): """ The main dispatcher for incoming messages. :param bundle: The transaction bundle :param statistics: Container for shared memory with statistics counters :returns: The message to reply with """ if not bundle.request: # Nothing to do... return None # Update the allow_rapid_commit flag bundle.allow_rapid_commit = self.allow_rapid_commit # Count the incoming message type statistics.count_message_in(bundle.request.message_type) # Log what we are doing (low-detail, so not DEBUG_HANDLING here) logger.debug("Handling {}".format(bundle)) # Collect the handlers handlers = self.get_handlers(bundle) # Analyse pre for handler in handlers: # noinspection PyBroadException try: handler.analyse_pre(bundle) except: # Ignore all errors, analysis isn't that important pass try: # Pre-process the request for handler in handlers: handler.pre(bundle) # Init the response self.init_response(bundle) # Process the request for handler in handlers: logger.log(DEBUG_HANDLING, "Applying {}".format(handler)) handler.handle(bundle) # Post-process the request for handler in handlers: handler.post(bundle) except ForOtherServerError as e: # Specific form of CannotRespondError that should have its own log message message = str(e) or 'Message is for another server' logger.debug("{}: ignoring".format(message)) statistics.count_for_other_server() bundle.response = None except CannotRespondError as e: message = str(e) or 'Cannot respond to this message' logger.warning("{}: ignoring".format(message)) statistics.count_do_not_respond() bundle.response = None except UseMulticastError: logger.debug("Unicast request received when multicast is required: informing client") statistics.count_use_multicast() bundle.response = self.construct_use_multicast_reply(bundle) # Analyse post for handler in handlers: # noinspection PyBroadException try: handler.analyse_post(bundle) except: # Ignore all errors, analysis isn't that important pass if bundle.response: logger.log(DEBUG_HANDLING, "Responding with {}".format(bundle.response.__class__.__name__)) # Count the outgoing message type statistics.count_message_out(bundle.response.message_type) else: logger.log(DEBUG_HANDLING, "Not responding")
def handle(self, bundle: TransactionBundle, statistics: StatisticsSet): """ The main dispatcher for incoming messages. :param bundle: The transaction bundle :param statistics: Container for shared memory with statistics counters :returns: The message to reply with """ if not bundle.request: # Nothing to do... return None # Update the allow_rapid_commit flag bundle.allow_rapid_commit = self.allow_rapid_commit # Count the incoming message type statistics.count_message_in(bundle.request.message_type) # Log what we are doing (low-detail, so not DEBUG_HANDLING here) logger.debug("Handling {}".format(bundle)) # Collect the handlers handlers = self.get_handlers(bundle) # Analyse pre for handler in handlers: # noinspection PyBroadException try: handler.analyse_pre(bundle) except: # Ignore all errors, analysis isn't that important logger.exception("{} pre analysis failed".format(handler.__class__.__name__)) try: # Pre-process the request for handler in handlers: handler.pre(bundle) # Init the response self.init_response(bundle) # Process the request for handler in handlers: logger.log(DEBUG_HANDLING, "Applying {}".format(handler)) handler.handle(bundle) # Post-process the request for handler in handlers: handler.post(bundle) except ForOtherServerError as e: # Specific form of CannotRespondError that should have its own log message message = str(e) or 'Message is for another server' logger.debug("{}: ignoring".format(message)) statistics.count_for_other_server() bundle.response = None except CannotRespondError as e: message = str(e) or 'Cannot respond to this message' logger.warning("{}: ignoring".format(message)) statistics.count_do_not_respond() bundle.response = None except UseMulticastError: logger.debug("Unicast request received when multicast is required: informing client") statistics.count_use_multicast() bundle.response = self.construct_use_multicast_reply(bundle) except ReplyWithStatusError as e: # Leasequery has its own reply message type if isinstance(e, ReplyWithLeasequeryError): bundle.response = self.construct_leasequery_status_reply(bundle, e.option) else: bundle.response = self.construct_plain_status_reply(bundle, e.option) logger.warning("Replying with {}".format(e)) # Update the right counter based on the status code if e.option.status_code == STATUS_UNKNOWN_QUERY_TYPE: statistics.count_unknown_query_type() elif e.option.status_code == STATUS_MALFORMED_QUERY: statistics.count_malformed_query() elif e.option.status_code == STATUS_NOT_ALLOWED: statistics.count_not_allowed() else: statistics.count_other_error() # Analyse post for handler in handlers: # noinspection PyBroadException try: handler.analyse_post(bundle) except: # Ignore all errors, analysis isn't that important logger.exception("{} post analysis failed".format(handler.__class__.__name__)) if bundle.response: logger.log(DEBUG_HANDLING, "Responding with {}".format(bundle.response.__class__.__name__)) # Count the outgoing message type statistics.count_message_out(bundle.response.message_type) else: logger.log(DEBUG_HANDLING, "Not responding")
def handle(self, bundle: TransactionBundle, statistics: StatisticsSet): """ The main dispatcher for incoming messages. :param bundle: The transaction bundle :param statistics: Container for shared memory with statistics counters """ if not bundle.request: # Nothing to do... return # Update the allow_rapid_commit flag bundle.allow_rapid_commit = self.allow_rapid_commit # Count the incoming message type statistics.count_message_in(bundle.request.message_type) # Log what we are doing (low-detail, so not DEBUG_HANDLING here) logger.debug("Handling {}".format(bundle)) # Collect the handlers handlers = self.get_handlers(bundle) # Analyse pre for handler in handlers: # noinspection PyBroadException try: handler.analyse_pre(bundle) except: # Ignore all errors, analysis isn't that important logger.exception("{} pre analysis failed".format( handler.__class__.__name__)) try: # Pre-process the request for handler in handlers: handler.pre(bundle) # Init the response self.init_response(bundle) # Process the request for handler in handlers: logger.log(DEBUG_HANDLING, "Applying {}".format(handler)) handler.handle(bundle) # Post-process the request for handler in handlers: handler.post(bundle) except ForOtherServerError as e: # Specific form of CannotRespondError that should have its own log message message = str(e) or 'Message is for another server' logger.debug("{}: ignoring".format(message)) statistics.count_for_other_server() bundle.response = None except CannotRespondError as e: message = str(e) or 'Cannot respond to this message' logger.warning("{}: ignoring".format(message)) statistics.count_do_not_respond() bundle.response = None except UseMulticastError: logger.debug( "Unicast request received when multicast is required: informing client" ) statistics.count_use_multicast() bundle.response = self.construct_use_multicast_reply(bundle) except ReplyWithStatusError as e: # Leasequery has its own reply message type if isinstance(e, ReplyWithLeasequeryError): bundle.response = self.construct_leasequery_status_reply( bundle, e.option) else: bundle.response = self.construct_plain_status_reply( bundle, e.option) logger.warning("Replying with {}".format(e)) # Update the right counter based on the status code if e.option.status_code == STATUS_UNKNOWN_QUERY_TYPE: statistics.count_unknown_query_type() elif e.option.status_code == STATUS_MALFORMED_QUERY: statistics.count_malformed_query() elif e.option.status_code == STATUS_NOT_ALLOWED: statistics.count_not_allowed() else: statistics.count_other_error() # Analyse post for handler in handlers: # noinspection PyBroadException try: handler.analyse_post(bundle) except: # Ignore all errors, analysis isn't that important logger.exception("{} post analysis failed".format( handler.__class__.__name__)) if bundle.response: logger.log( DEBUG_HANDLING, "Responding with {}".format( bundle.response.__class__.__name__)) # Count the outgoing message type statistics.count_message_out(bundle.response.message_type) else: logger.log(DEBUG_HANDLING, "Not responding")