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_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_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 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 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_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 handle_renew_rebind(self, bundle: TransactionBundle): """ Handle a client renewing/rebinding addresses :param bundle: The request bundle """ # Get the assignment assignment = self.get_assignment(bundle) # Collect unanswered options unanswered_iana_options = bundle.get_unhandled_options(IANAOption) unanswered_iapd_options = bundle.get_unhandled_options(IAPDOption) for option in unanswered_iapd_options: if assignment.prefix and prefix_overlaps_prefixes(assignment.prefix, option.get_prefixes()): # Overlap with our assigned prefix: take responsibility response_suboptions = [] for suboption in option.get_options_of_type(IAPrefixOption): if suboption.prefix == assignment.prefix: # This is the correct option, renew it logger.log(DEBUG_HANDLING, "Renewing prefix {}".format(assignment.prefix)) response_suboptions.append(IAPrefixOption(prefix=assignment.prefix, preferred_lifetime=self.prefix_preferred_lifetime, valid_lifetime=self.prefix_valid_lifetime)) else: # This isn't right logger.log(DEBUG_HANDLING, "Withdrawing prefix {}".format(suboption.prefix)) response_suboptions.append(IAPrefixOption(prefix=suboption.prefix, preferred_lifetime=0, valid_lifetime=0)) response_option = IAPDOption(option.iaid, options=response_suboptions) bundle.response.options.append(response_option) bundle.mark_handled(option) for option in unanswered_iana_options: response_suboptions = [] for suboption in option.get_options_of_type(IAAddressOption): if suboption.address == assignment.address: # This is the correct option, renew it logger.log(DEBUG_HANDLING, "Renewing address {}".format(assignment.address)) response_suboptions.append(IAAddressOption(address=assignment.address, preferred_lifetime=self.address_preferred_lifetime, valid_lifetime=self.address_valid_lifetime)) else: # This isn't right logger.log(DEBUG_HANDLING, "Withdrawing address {}".format(suboption.address)) response_suboptions.append(IAAddressOption(address=suboption.address, preferred_lifetime=0, valid_lifetime=0)) response_option = IANAOption(option.iaid, options=response_suboptions) bundle.response.options.append(response_option) bundle.mark_handled(option)
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 pre(self, bundle: TransactionBundle): """ Set flag to let the server know that unicast is ok, otherwise RejectUnwantedUnicastHandler will reject it later. :param bundle: The transaction bundle """ bundle.allow_unicast = True
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_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 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 analyse_pre(self, bundle: TransactionBundle): """ Start building the Kafka message. :param bundle: The transaction bundle """ bundle.handler_data[self] = DHCPKafkaMessage(server_name=self.server_name, timestamp_in=time.time(), message_in=bundle.incoming_message)
def handle_confirm(self, bundle: TransactionBundle): """ Handle a client requesting confirmation :param bundle: The request bundle """ # Get the assignment assignment = self.get_assignment(bundle) # Collect unanswered options unanswered_iana_options = bundle.get_unhandled_options(IANAOption) # See if there are any addresses on a link that I am responsible for for option in unanswered_iana_options: for suboption in option.get_options_of_type(IAAddressOption): if suboption.address == assignment.address: # This is the address from the assignment: it's ok bundle.mark_handled(option) continue
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_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 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 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 handle_release_decline(self, bundle: TransactionBundle): """ Handle a client releasing or declining resources. Doesn't really need to do anything because assignments are static. Just mark the right options as handled. :param bundle: The request bundle """ # Get the assignment assignment = self.get_assignment(bundle) # Collect unanswered options unanswered_iana_options = bundle.get_unhandled_options(IANAOption) unanswered_iapd_options = bundle.get_unhandled_options(IAPDOption) for option in unanswered_iapd_options: if assignment.prefix and prefix_overlaps_prefixes(assignment.prefix, option.get_prefixes()): # Overlap with our assigned prefix: take responsibility bundle.mark_handled(option) for option in unanswered_iana_options: if assignment.address in option.get_addresses(): bundle.mark_handled(option)
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 handle_release_decline(self, bundle: TransactionBundle): """ Handle a client releasing or declining resources. Doesn't really need to do anything because assignments are static. Just mark the right options as handled. :param bundle: The request bundle """ # Get the assignment assignment = self.get_assignment(bundle) # Collect unanswered options unanswered_iana_options = bundle.get_unhandled_options(IANAOption) unanswered_iapd_options = bundle.get_unhandled_options(IAPDOption) for option in unanswered_iapd_options: if assignment.prefix and prefix_overlaps_prefixes( assignment.prefix, option.get_prefixes()): # Overlap with our assigned prefix: take responsibility bundle.mark_handled(option) for option in unanswered_iana_options: if assignment.address in option.get_addresses(): bundle.mark_handled(option)
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 handle_request(self, bundle: TransactionBundle): """ Handle a client requesting addresses (also handles SolicitMessage) :param bundle: The request bundle """ # Get the assignment assignment = self.get_assignment(bundle) # Try to assign the prefix first: it's not dependent on the link if assignment.prefix: unanswered_iapd_options = bundle.get_unhandled_options(IAPDOption) found_option = self.find_iapd_option_for_prefix( unanswered_iapd_options, assignment.prefix) if found_option: # Answer to this option logger.log(DEBUG_HANDLING, "Assigning prefix {}".format(assignment.prefix)) response_option = IAPDOption( found_option.iaid, options=[ IAPrefixOption( prefix=assignment.prefix, preferred_lifetime=self.prefix_preferred_lifetime, valid_lifetime=self.prefix_valid_lifetime) ]) bundle.response.options.append(response_option) bundle.mark_handled(found_option) else: logger.log( DEBUG_HANDLING, "Prefix {} reserved, but client did not ask for it".format( assignment.prefix)) if assignment.address: unanswered_iana_options = bundle.get_unhandled_options(IANAOption) found_option = self.find_iana_option_for_address( unanswered_iana_options, assignment.address) if found_option: # Answer to this option logger.log(DEBUG_HANDLING, "Assigning address {}".format(assignment.address)) response_option = IANAOption( found_option.iaid, options=[ IAAddressOption( address=assignment.address, preferred_lifetime=self.address_preferred_lifetime, valid_lifetime=self.address_valid_lifetime) ]) bundle.response.options.append(response_option) bundle.mark_handled(found_option) else: logger.log( DEBUG_HANDLING, "Address {} reserved, but client did not ask for it". format(assignment.address))
def post(self, bundle: TransactionBundle): """ Check for unhandled leasequeries. :param bundle: The transaction bundle """ if not isinstance(bundle.request, LeasequeryMessage): # Only leasequeries are relevant return unhandled_queries = bundle.get_unhandled_options(LQQueryOption) if unhandled_queries: query = unhandled_queries[0] raise ReplyWithLeasequeryError(STATUS_UNKNOWN_QUERY_TYPE, "This server can't handle query type {}".format(query.query_type))
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 handle_request(self, bundle: TransactionBundle): """ Handle a client requesting addresses (also handles SolicitMessage) :param bundle: The request bundle """ # Get the assignment assignment = self.get_assignment(bundle) # Try to assign the prefix first: it's not dependent on the link if assignment.prefix: unanswered_iapd_options = bundle.get_unhandled_options(IAPDOption) found_option = self.find_iapd_option_for_prefix(unanswered_iapd_options, assignment.prefix) if found_option: # Answer to this option logger.log(DEBUG_HANDLING, "Assigning prefix {}".format(assignment.prefix)) response_option = IAPDOption(found_option.iaid, options=[ IAPrefixOption(prefix=assignment.prefix, preferred_lifetime=self.prefix_preferred_lifetime, valid_lifetime=self.prefix_valid_lifetime) ]) bundle.response.options.append(response_option) bundle.mark_handled(found_option) else: logger.log(DEBUG_HANDLING, "Prefix {} reserved, but client did not ask for it".format(assignment.prefix)) if assignment.address: unanswered_iana_options = bundle.get_unhandled_options(IANAOption) found_option = self.find_iana_option_for_address(unanswered_iana_options, assignment.address) if found_option: # Answer to this option logger.log(DEBUG_HANDLING, "Assigning address {}".format(assignment.address)) response_option = IANAOption(found_option.iaid, options=[ IAAddressOption(address=assignment.address, preferred_lifetime=self.address_preferred_lifetime, valid_lifetime=self.address_valid_lifetime) ]) bundle.response.options.append(response_option) bundle.mark_handled(found_option) else: logger.log(DEBUG_HANDLING, "Address {} reserved, but client did not ask for it".format(assignment.address))
class TransactionBundleTestCase(unittest.TestCase): 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_str(self): bundle_str = str(self.bundle) self.assertEqual(bundle_str, "SolicitMessage from 0001000a000300013431c43cb2f1 at fe80::3631:c4ff:fe3c:b2f1 " "via 2001:db8:ffff:1::1") bundle_str = str(self.shallow_bundle) self.assertEqual(bundle_str, "SolicitMessage from 0001000a000300013431c43cb2f1") bundle_str = str(self.deep_bundle) self.assertRegex(bundle_str, "^SolicitMessage from 0001000a000300013431c43cb2f1 at fe80::3631:c4ff:fe3c:b2f1 " "via 2001:db8:ffff:1::1 -> 2001:db8:ffff:2::1 with marks .*$") bundle_str = str(self.ia_bundle) self.assertEqual(bundle_str, "SolicitMessage from unknown") def test_shallow_bundle(self): self.shallow_bundle.response = advertise_message self.shallow_bundle.create_outgoing_relay_messages() self.assertEqual(self.shallow_bundle.outgoing_message, advertise_message) self.assertEqual(self.shallow_bundle.outgoing_relay_messages, []) def test_request(self): self.assertEqual(self.bundle.request, solicit_message) def test_incoming_relay_messages(self): self.assertEqual(len(self.bundle.incoming_relay_messages), 2) self.assertEqual(self.bundle.incoming_relay_messages[0].hop_count, 0) self.assertEqual(self.bundle.incoming_relay_messages[1].hop_count, 1) def test_no_response(self): self.assertRaisesRegex(ValueError, 'Cannot create outgoing', self.bundle.create_outgoing_relay_messages) def test_bad_response(self): self.bundle.response = SolicitMessage() with self.assertLogs() as cm: self.assertIsNone(self.bundle.outgoing_message) self.assertEqual(len(cm.output), 1) self.assertRegex(cm.output[0], 'server should not send') def test_outgoing_message(self): # Set the response and let the option handlers do their work # Which in this case is copy the InterfaceId to the response self.bundle.response = advertise_message self.bundle.create_outgoing_relay_messages() for option_handler in self.option_handlers: option_handler.handle(self.bundle) self.assertEqual(self.bundle.outgoing_message, relayed_advertise_message) def test_direct_outgoing_message(self): self.ia_bundle.response = advertise_message self.assertEqual(self.ia_bundle.outgoing_message, advertise_message) def test_auto_create_outgoing_relay_messages(self): self.bundle.response = advertise_message self.assertIsInstance(self.bundle.outgoing_message, RelayReplyMessage) def test_no_outgoing_message(self): self.assertIsNone(self.bundle.outgoing_message) def test_get_unhandled_options(self): unanswered_options = self.ia_bundle.get_unhandled_options((IANAOption, IATAOption)) self.assertEqual(len(unanswered_options), 4) self.assertIn(IANAOption(b'0001'), unanswered_options) self.assertIn(IANAOption(b'0002'), unanswered_options) self.assertIn(IATAOption(b'0003'), unanswered_options) self.assertIn(IATAOption(b'0004'), unanswered_options) def test_marks(self): self.assertEqual(self.bundle.marks, set()) self.bundle.marks.add('one') self.bundle.marks.add('two') self.assertEqual(self.bundle.marks, {'one', 'two'}) self.bundle.marks.add('two') self.assertEqual(self.bundle.marks, {'one', 'two'}) def test_mark_handled(self): self.ia_bundle.mark_handled(IANAOption(b'0001')) self.ia_bundle.mark_handled(IATAOption(b'0004')) unanswered_options = self.ia_bundle.get_unhandled_options((IANAOption, IATAOption)) self.assertEqual(len(unanswered_options), 2) self.assertIn(IANAOption(b'0002'), unanswered_options) self.assertIn(IATAOption(b'0003'), unanswered_options) def test_unanswered_iana_options(self): unanswered_options = self.ia_bundle.get_unhandled_options(IANAOption) self.assertEqual(len(unanswered_options), 2) self.assertIn(IANAOption(b'0001'), unanswered_options) self.assertIn(IANAOption(b'0002'), unanswered_options) def test_unanswered_iata_options(self): unanswered_options = self.ia_bundle.get_unhandled_options(IATAOption) self.assertEqual(len(unanswered_options), 2) self.assertIn(IATAOption(b'0003'), unanswered_options) self.assertIn(IATAOption(b'0004'), unanswered_options) def test_unanswered_iapd_options(self): unanswered_options = self.ia_bundle.get_unhandled_options(IAPDOption) self.assertEqual(len(unanswered_options), 2) self.assertIn(IAPDOption(b'0005'), unanswered_options) self.assertIn(IAPDOption(b'0006'), unanswered_options) 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') def test_link_address(self): self.assertEqual(self.bundle.link_address, IPv6Address('2001:db8:ffff:1::1')) self.assertEqual(self.ia_bundle.link_address, IPv6Address('::'))
def handle(self, bundle: TransactionBundle): """ Make sure that every :class:`.IAPDOption` is answered. :param bundle: The transaction bundle """ for option in bundle.get_unhandled_options(IAPDOption): if isinstance(bundle.request, (SolicitMessage, RequestMessage)): # If the delegating router will not assign any prefixes to any IA_PDs in a subsequent Request from the # requesting router, the delegating router MUST send an Advertise message to the requesting router that # includes the IA_PD with no prefixes in the IA_PD and a Status Code option in the IA_PD containing # status code NoPrefixAvail and a status message for the user # # We do the same for unanswered requests bundle.response.options.append( IAPDOption(option.iaid, options=[StatusCodeOption(STATUS_NO_PREFIX_AVAIL, "No prefixes available")]) ) elif isinstance(bundle.request, RenewMessage): # Renew message: If the delegating router cannot find a binding for the requesting router's IA_PD the # delegating router returns the IA_PD containing no prefixes with a Status Code option set to # NoBinding in the Reply message. prefixes = ", ".join(map(str, option.get_prefixes())) logger.warning("No handler renewed {}: sending NoBinding status".format(prefixes)) bundle.response.options.append( IAPDOption( option.iaid, options=[StatusCodeOption(STATUS_NO_BINDING, "No prefixes assigned to you")] ) ) elif isinstance(bundle.request, RebindMessage): # Rebind message: If the delegating router cannot find a binding for the requesting router's IA_PD and # the delegating router determines that the prefixes in the IA_PD are not appropriate for the link to # which the requesting router's interface is attached according to the delegating routers explicit # configuration, the delegating router MAY send a Reply message to the requesting router containing # the IA_PD with the lifetimes of the prefixes in the IA_PD set to zero. This Reply constitutes an # explicit notification to the requesting router that the prefixes in the IA_PD are no longer valid. # # If the delegating router is unable to determine if the prefix is not appropriate for the link, the # Rebind message is discarded. # # The authoritative flag indicates whether this option may claim whether it is able to determine if a # prefix is appropriate for the link. if not self.authoritative: raise CannotRespondError("Server is not authoritative and cannot reject rebind") prefixes = ", ".join(map(str, option.get_prefixes())) logger.warning("No handler answered rebind of {}: withdrawing prefixes".format(prefixes)) reply_suboptions = [] for suboption in option.get_options_of_type(IAPrefixOption): reply_suboptions.append(IAPrefixOption(suboption.prefix, preferred_lifetime=0, valid_lifetime=0)) bundle.response.options.append(IAPDOption(option.iaid, options=reply_suboptions)) elif isinstance(bundle.request, ReleaseMessage): # For each IA in the Release message for which the server has no binding information, the server adds an # IA option using the IAID from the Release message, and includes a Status Code option with the value # NoBinding in the IA option. No other options are included in the IA option. bundle.response.options.append( IAPDOption( option.iaid, options=[StatusCodeOption(STATUS_NO_BINDING, "No prefixes assigned to you")] ) )
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): """ Make sure that every :class:`.IANAOption` and :class:`.IATAOption` is answered. :param bundle: The transaction bundle """ for option in bundle.get_unhandled_options((IANAOption, IATAOption)): ia_class = type(option) if isinstance(bundle.request, (SolicitMessage, RequestMessage)): # If the server will not assign any addresses to any IAs in a subsequent Request from the client, the # server MUST send an Advertise message to the client that includes only a Status Code option with code # NoAddrsAvail and a status message for the user # # We do the same for unanswered requests bundle.response.options.append(ia_class(option.iaid, options=[ StatusCodeOption(STATUS_NOADDRSAVAIL, "No addresses available") ])) elif isinstance(bundle.request, ConfirmMessage): # When the server receives a Confirm message, the server determines whether the addresses in the # Confirm message are appropriate for the link to which the client is attached. If all of the # addresses in the Confirm message pass this test, the server returns a status of Success. If any of # the addresses do not pass this test, the server returns a status of NotOnLink. If the server is # unable to perform this test (for example, the server does not have information about prefixes on the # link to which the client is connected), or there were no addresses in any of the IAs sent by the # client, the server MUST NOT send a reply to the client. # # The "there were no addresses in any of the IAs sent by the client" check is done by the message # handler. if not self.authoritative: raise CannotRespondError addresses = ', '.join(map(str, option.get_addresses())) logger.warning("No handler confirmed {}: sending NotOnLink status".format(addresses)) force_status(bundle.response.options, StatusCodeOption(STATUS_NOTONLINK, "Those addresses are not appropriate on this link")) elif isinstance(bundle.request, RenewMessage): # If the server cannot find a client entry for the IA the server returns the IA containing no addresses # with a Status Code option set to NoBinding in the Reply message. # # If the server finds that any of the addresses are not appropriate for the link to which the client is # attached, the server returns the address to the client with lifetimes of 0. addresses = ', '.join(map(str, option.get_addresses())) if self.authoritative: logger.warning("No handler renewed {}: withdrawing addresses".format(addresses)) reply_suboptions = [] for suboption in option.get_options_of_type(IAAddressOption): reply_suboptions.append(IAAddressOption(suboption.address, preferred_lifetime=0, valid_lifetime=0)) bundle.response.options.append(ia_class(option.iaid, options=reply_suboptions)) else: logger.warning("No handler renewed {}: sending NoBinding status".format(addresses)) bundle.response.options.append(ia_class(option.iaid, options=[ StatusCodeOption(STATUS_NOBINDING, "No addresses assigned to you") ])) elif isinstance(bundle.request, RebindMessage): # If the server cannot find a client entry for the IA and the server determines that the addresses in # the IA are not appropriate for the link to which the client's interface is attached according to the # server's explicit configuration information, the server MAY send a Reply message to the client # containing the client's IA, with the lifetimes for the addresses in the IA set to zero. This Reply # constitutes an explicit notification to the client that the addresses in the IA are no longer valid. # In this situation, if the server does not send a Reply message it silently discards the Rebind # message. # # If the server finds that any of the addresses are no longer appropriate for the link to which the # client is attached, the server returns the address to the client with lifetimes of 0. if not self.authoritative: raise CannotRespondError addresses = ', '.join(map(str, option.get_addresses())) logger.warning("No handler answered rebind of {}: withdrawing addresses".format(addresses)) reply_suboptions = [] for suboption in option.get_options_of_type(IAAddressOption): reply_suboptions.append(IAAddressOption(suboption.address, preferred_lifetime=0, valid_lifetime=0)) bundle.response.options.append(ia_class(option.iaid, options=reply_suboptions)) elif isinstance(bundle.request, DeclineMessage): # For each IA in the Decline message for which the server has no binding information, the server adds # an IA option using the IAID from the Release message and includes a Status Code option with the value # NoBinding in the IA option. No other options are included in the IA option. bundle.response.options.append(ia_class(option.iaid, options=[ StatusCodeOption(STATUS_NOBINDING, "No addresses assigned to you") ])) elif isinstance(bundle.request, ReleaseMessage): # For each IA in the Release message for which the server has no binding information, the server adds an # IA option using the IAID from the Release message, and includes a Status Code option with the value # NoBinding in the IA option. No other options are included in the IA option. bundle.response.options.append(ia_class(option.iaid, options=[ StatusCodeOption(STATUS_NOBINDING, "No addresses assigned to you") ]))
def handle(self, bundle: TransactionBundle): """ Perform leasequery if requested. :param bundle: The transaction bundle """ if not isinstance(bundle.request, LeasequeryMessage): # Not a leasequery, not our business return # Extract the query queries = bundle.get_unhandled_options(LQQueryOption) if not queries: # No unhandled queries return query = queries[0] # Get the leases from the store lease_count, leases = self.store.find_leases(query) # A count of -1 means unsupported query, so we stop handling if lease_count < 0: return # Otherwise mark this query as handled bundle.mark_handled(query) # What we do now depends on the protocol if bundle.received_over_tcp: try: if lease_count > 0: # We're doing bulk leasequery, return all the records in separate messages leases_iterator = iter(leases) first_link_address, first_data_option = next(leases_iterator) first_message = bundle.response first_message.options.append(first_data_option) bundle.responses = MessagesList(first_message, self.generate_data_messages(first_message.transaction_id, leases_iterator)) else: # If the server does not find any bindings satisfying a query, it # SHOULD send a LEASEQUERY-REPLY without an OPTION_STATUS_CODE option # and without any OPTION_CLIENT_DATA option. pass except: # Something went wrong (database changes while reading?), abort logger.exception("Error while building bulk leasequery response") raise ReplyWithLeasequeryError(STATUS_QUERY_TERMINATED, "Error constructing your reply, please try again") else: try: if lease_count == 1: # One entry found, return it leases_iterator = iter(leases) first_link_address, first_data_option = next(leases_iterator) bundle.response.options.append(first_data_option) elif lease_count > 1: # The Client Link option is used only in a LEASEQUERY-REPLY message and # identifies the links on which the client has one or more bindings. # It is used in reply to a query when no link-address was specified and # the client is found to be on more than one link. link_addresses = set([link_address for link_address, data_option in leases]) bundle.response.options.append(LQClientLink(link_addresses)) except: # Something went wrong (database changes while reading?), abort logger.exception("Error while building leasequery response") raise ReplyWithLeasequeryError(STATUS_UNSPEC_FAIL, "Error constructing your reply, please try again")
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