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()
Example #2
0
    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)
Example #3
0
    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)
Example #4
0
    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()
Example #5
0
    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')
Example #6
0
    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)
Example #7
0
    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)
Example #8
0
    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)
Example #9
0
    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)
Example #10
0
    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)
Example #11
0
    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])
Example #12
0
    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])
Example #13
0
    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)
Example #14
0
    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)
Example #15
0
    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)
Example #16
0
    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)
Example #17
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_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)
Example #18
0
    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)
Example #19
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)
Example #21
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
Example #22
0
    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, [])
Example #23
0
 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))
Example #24
0
 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(),
     ]
Example #25
0
    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)
Example #26
0
    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)
Example #28
0
    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
Example #29
0
    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')
Example #30
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 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(),
     ]
Example #32
0
    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)
Example #33
0
    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')
Example #34
0
    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)
Example #35
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')
Example #36
0
    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))
Example #37
0
    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))
Example #38
0
    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)
Example #39
0
    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')
Example #40
0
    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('::'))
Example #42
0
    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")]
                    )
                )
Example #43
0
    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")
Example #44
0
    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")
                ]))
Example #45
0
    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