def setUp(self):
     self.bundle = TransactionBundle(relayed_solicit_message, True)
     self.ia_bundle = TransactionBundle(SolicitMessage(options=[
         IANAOption(b'0001'),
         IANAOption(b'0002'),
         IATAOption(b'0003'),
         IATAOption(b'0004'),
         IAPDOption(b'0005'),
         IAPDOption(b'0006'),
     ]), False)
     self.option_handlers = [
         InterfaceIdOptionHandler(),
     ]
Esempio n. 2
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_unanswered_ia_options() or bundle.get_unanswered_iapd_options():
                # 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)
Esempio n. 3
0
    def handle_confirm(self, bundle: TransactionBundle):
        """
        Handle a client requesting confirmation

        :param bundle: The request bundle
        """
        # Make sure we are responsible for this link
        link_address = bundle.get_link_address()
        if not address_in_prefixes(link_address, self.responsible_for_links):
            logger.debug("Not confirming to link {}: "
                         "doesn't match {}".format(link_address, ', '.join(map(str, self.responsible_for_links))))
            return

        # Get the assignment
        assignment = self.get_assignment(bundle)

        # Collect unanswered options
        unanswered_iana_options = bundle.get_unanswered_iana_options()

        # 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

                if address_in_prefixes(suboption.address, self.responsible_for_links):
                    # Oops, an address on a link that I am responsible for, but it's the wrong one...
                    force_status(bundle.response.options,
                                 StatusCodeOption(STATUS_NOTONLINK,
                                                  "{} is not assigned to you".format(suboption.address)))
                    bundle.mark_handled(option)
                    return
Esempio n. 4
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)

        # Client ID for logging
        client_id_option = bundle.request.get_option_of_type(ClientIdOption)

        # Collect unanswered options
        unanswered_iana_options = bundle.get_unanswered_iana_options()
        unanswered_iapd_options = bundle.get_unanswered_iapd_options()

        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.info("Renewing {} for {!r}".format(assignment.prefix, client_id_option.duid))
                        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.info("Withdrawing {} from {!r}".format(suboption.prefix, client_id_option.duid))
                        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:
            if any([address_in_prefixes(address, self.responsible_for_links) for address in option.get_addresses()]):
                # Overlap with our addresses: take responsibility
                response_suboptions = []
                for suboption in option.get_options_of_type(IAAddressOption):
                    if suboption.address == assignment.address:
                        # This is the correct option, renew it
                        logger.info("Renewing {} for {!r}".format(assignment.address, client_id_option.duid))
                        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.info("Withdrawing {} from {!r}".format(suboption.address, client_id_option.duid))
                        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)
Esempio n. 5
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)

        # Collect unanswered options
        unanswered_iana_options = bundle.get_unanswered_iana_options()
        unanswered_iapd_options = bundle.get_unanswered_iapd_options()

        # Try to assign the prefix first: it's not dependent on the link
        if assignment.prefix:
            found_option = self.find_iapd_option_for_prefix(unanswered_iapd_options, assignment.prefix)
            if found_option:
                # Answer to this option
                logger.info("Assigning {} to {!r}".format(assignment.prefix,
                                                          bundle.request.get_option_of_type(ClientIdOption).duid))
                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)

        # Make sure we are responsible for this link
        link_address = bundle.get_link_address()
        if not address_in_prefixes(link_address, self.responsible_for_links):
            logger.debug("Not assigning to link {}: "
                         "doesn't match {}".format(link_address, ', '.join(map(str, self.responsible_for_links))))
            return

        if assignment.address:
            found_option = self.find_iana_option_for_address(unanswered_iana_options, assignment.address)
            if found_option:
                # Answer to this option
                logger.info("Assigning {} to {!r}".format(assignment.address,
                                                          bundle.request.get_option_of_type(ClientIdOption).duid))
                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)
Esempio n. 6
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
        fixed. 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_unanswered_iana_options()
        unanswered_iapd_options = bundle.get_unanswered_iapd_options()

        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 any([address_in_prefixes(address, self.responsible_for_links) for address in option.get_addresses()]):
                # Overlap with our addresses: take responsibility
                bundle.mark_handled(option)
Esempio n. 7
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_unanswered_ia_options():
            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([str(suboption.address)
                                       for suboption in option.get_options_of_type(IAAddressOption)])
                logger.warning("No handler confirmed {} for {}: "
                               "sending NotOnLink status".format(addresses, bundle.get_link_address()))

                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([str(suboption.address)
                                       for suboption in option.get_options_of_type(IAAddressOption)])

                if self.authoritative:
                    logger.warning("No handler renewed {} for {}: "
                                   "withdrawing addresses".format(addresses, bundle.get_link_address()))

                    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 {} for {}: "
                                   "sending NoBinding status".format(addresses, bundle.get_link_address()))

                    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([str(suboption.address)
                                       for suboption in option.get_options_of_type(IAAddressOption)])
                logger.warning("No handler answered rebind of {} for {}: "
                               "withdrawing addresses".format(addresses, bundle.get_link_address()))

                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")
                ]))
Esempio n. 8
0
    def handle(self, bundle: TransactionBundle):
        """
        Make sure that every :class:`.IAPDOption` is answered.

        :param bundle: The transaction bundle
        """
        for option in bundle.get_unanswered_iapd_options():
            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_NOPREFIXAVAIL, "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([str(suboption.prefix)
                                      for suboption in option.get_options_of_type(IAPrefixOption)])
                logger.warning("No handler renewed {} for {}: "
                               "sending NoBinding status".format(prefixes, bundle.get_link_address()))

                bundle.response.options.append(IAPDOption(option.iaid, options=[
                    StatusCodeOption(STATUS_NOBINDING, "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

                prefixes = ', '.join([str(suboption.prefix)
                                      for suboption in option.get_options_of_type(IAPrefixOption)])
                logger.warning("No handler answered rebind of {} for {}: "
                               "withdrawing prefixes".format(prefixes, bundle.get_link_address()))

                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_NOBINDING, "No prefixes assigned to you")
                ]))
class TransactionBundleTestCase(unittest.TestCase):
    def setUp(self):
        self.bundle = TransactionBundle(relayed_solicit_message, True)
        self.ia_bundle = TransactionBundle(SolicitMessage(options=[
            IANAOption(b'0001'),
            IANAOption(b'0002'),
            IATAOption(b'0003'),
            IATAOption(b'0004'),
            IAPDOption(b'0005'),
            IAPDOption(b'0006'),
        ]), False)
        self.option_handlers = [
            InterfaceIdOptionHandler(),
        ]

    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_unanswered_ia_options(self):
        unanswered_options = self.ia_bundle.get_unanswered_ia_options()
        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_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_unanswered_ia_options()
        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_unanswered_iana_options()
        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_unanswered_iata_options()
        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_unanswered_iapd_options()
        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.get_link_address(), IPv6Address('2001:db8:ffff:1::1'))
        self.assertEqual(self.ia_bundle.get_link_address(), IPv6Address('::'))