Skip to content

Commit

Permalink
Implement relay echo-request option handler
Browse files Browse the repository at this point in the history
  • Loading branch information
steffann committed Mar 7, 2017
1 parent 7b39b4d commit f80235e
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 5 deletions.
69 changes: 69 additions & 0 deletions dhcpkit/ipv6/server/extensions/relay_echo_request.py
@@ -0,0 +1,69 @@
"""
Implementation of Echo Request option handling as specified in :rfc:`4994`.
"""
from typing import List

from dhcpkit.ipv6.extensions.relay_echo_request import EchoRequestOption
from dhcpkit.ipv6.messages import RelayForwardMessage, RelayReplyMessage
from dhcpkit.ipv6.server.handlers import Handler, RelayHandler
from dhcpkit.ipv6.server.transaction_bundle import TransactionBundle


def create_cleanup_handlers() -> List[Handler]:
"""
Create handlers to clean up stuff in the transaction bundle
:return: Handlers to add to the handler chain
"""
return [RelayEchoRequestOptionHandler()]


class RelayEchoRequestOptionHandler(RelayHandler):
"""
When a server creates a Relay-Reply, it SHOULD perform ERO processing
after processing the ORO and other options processing. For each
option in the ERO:
a. If the option is already in the Relay-Reply, the server MUST
ignore that option and continue to process any remaining options
in the ERO.
b. If the option was not in the received Relay-Forward, the server
MUST ignore that option and continue to process any remaining
options in the ERO.
c. Otherwise, the server MUST copy the option, verbatim, from the
received Relay-Forward to the Relay-Reply, even if the server
does not otherwise recognize that option.
"""

def handle_relay(self, bundle: TransactionBundle,
relay_message_in: RelayForwardMessage, relay_message_out: RelayReplyMessage):
"""
Handle the options for each relay message pair.
:param bundle: The transaction bundle
:param relay_message_in: The incoming relay message
:param relay_message_out: Thr outgoing relay message
"""
# See if the relay message contains an ERO
ero = relay_message_in.get_option_of_type(EchoRequestOption)
if not ero:
# Nothing to do
return

for option_type in ero.requested_options:
# Don't do anything if the outgoing relay message already has this one
if any(option.option_type == option_type for option in relay_message_out.options):
continue

# Get the incoming options of the requested type
incoming_options = [option for option in relay_message_in.options if option.option_type == option_type]

for option in incoming_options:
# Make sure this option can go into this type of response
if not relay_message_out.may_contain(option):
return

# And append them to the outgoing message if possible
relay_message_out.options.append(option)
11 changes: 6 additions & 5 deletions dhcpkit/ipv6/server/message_handler.py
Expand Up @@ -4,6 +4,8 @@
import logging
import multiprocessing

from typing import Iterable, List, Optional

from dhcpkit.common.server.logging import DEBUG_HANDLING
from dhcpkit.ipv6.duids import DUID
from dhcpkit.ipv6.extensions.leasequery import LeasequeryMessage, LeasequeryReplyMessage, STATUS_MALFORMED_QUERY, \
Expand All @@ -26,7 +28,6 @@
from dhcpkit.ipv6.server.handlers.unicast import RejectUnwantedUnicastHandler
from dhcpkit.ipv6.server.statistics import StatisticsSet
from dhcpkit.ipv6.server.transaction_bundle import TransactionBundle
from typing import Iterable, List

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -205,7 +206,8 @@ def construct_plain_status_reply(self, bundle: TransactionBundle, option: Status
option
])

def construct_leasequery_status_reply(self, bundle: TransactionBundle, option: StatusCodeOption) -> ReplyMessage:
def construct_leasequery_status_reply(self, bundle: TransactionBundle,
option: StatusCodeOption) -> LeasequeryReplyMessage:
"""
Construct a leasequery reply message signalling a status code to the client.
Expand All @@ -219,7 +221,7 @@ def construct_leasequery_status_reply(self, bundle: TransactionBundle, option: S
option
])

def construct_use_multicast_reply(self, bundle: TransactionBundle) -> ReplyMessage:
def construct_use_multicast_reply(self, bundle: TransactionBundle) -> Optional[ReplyMessage]:
"""
Construct a message signalling to the client that they should have used multicast.
Expand All @@ -243,11 +245,10 @@ def handle(self, bundle: TransactionBundle, statistics: StatisticsSet):
: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
return

# Update the allow_rapid_commit flag
bundle.allow_rapid_commit = self.allow_rapid_commit
Expand Down
154 changes: 154 additions & 0 deletions dhcpkit/tests/ipv6/server/handlers/test_echo_request_option_handler.py
@@ -0,0 +1,154 @@
"""
Tests for a relay message handler
"""
import unittest
from ipaddress import IPv6Address

from dhcpkit.ipv6.duids import LinkLayerDUID, LinkLayerTimeDUID
from dhcpkit.ipv6.extensions.relay_echo_request import EchoRequestOption
from dhcpkit.ipv6.extensions.remote_id import OPTION_REMOTE_ID, RemoteIdOption
from dhcpkit.ipv6.extensions.subscriber_id import OPTION_SUBSCRIBER_ID
from dhcpkit.ipv6.messages import RelayForwardMessage, RelayReplyMessage, SolicitMessage
from dhcpkit.ipv6.options import ClientIdOption, ElapsedTimeOption, IANAOption, InterfaceIdOption, OPTION_INTERFACE_ID, \
RelayMessageOption, UnknownOption
from dhcpkit.ipv6.server.message_handler import MessageHandler
from dhcpkit.ipv6.server.statistics import StatisticsSet
from dhcpkit.ipv6.server.transaction_bundle import TransactionBundle


class RelayHandlerTestCase(unittest.TestCase):
def setUp(self):
# This is the DUID that is used in the message fixtures
self.duid = LinkLayerTimeDUID(hardware_type=1, time=488458703, link_layer_address=bytes.fromhex('00137265ca42'))

# Create a message handler
self.message_handler = MessageHandler(server_id=self.duid)

def test_empty_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=[]),
UnknownOption(option_type=65535),
InterfaceIdOption(interface_id=b'Fa2/3'),
RemoteIdOption(enterprise_number=9,
remote_id=bytes.fromhex('020023000001000a0003000100211c7d486e')),
]
)

bundle = TransactionBundle(incoming_message=relayed_solicit_message, received_over_multicast=True)
self.message_handler.handle(bundle, StatisticsSet())

self.assertIsInstance(bundle.outgoing_message, RelayReplyMessage)
self.assertEqual(len(bundle.outgoing_message.options), 2)
self.assertIsInstance(bundle.outgoing_message.options[0], InterfaceIdOption)
self.assertIsInstance(bundle.outgoing_message.options[1], RelayMessageOption)

def test_unnecessary_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_INTERFACE_ID]),
UnknownOption(option_type=65535),
InterfaceIdOption(interface_id=b'Fa2/3'),
RemoteIdOption(enterprise_number=9,
remote_id=bytes.fromhex('020023000001000a0003000100211c7d486e')),
]
)

bundle = TransactionBundle(incoming_message=relayed_solicit_message, received_over_multicast=True)
self.message_handler.handle(bundle, StatisticsSet())

self.assertIsInstance(bundle.outgoing_message, RelayReplyMessage)
self.assertEqual(len(bundle.outgoing_message.options), 2)
self.assertIsInstance(bundle.outgoing_message.options[0], InterfaceIdOption)
self.assertIsInstance(bundle.outgoing_message.options[1], RelayMessageOption)

def test_absent_option_echo_request(self):
relayed_solicit_message = RelayForwardMessage(
hop_count=1,
link_address=IPv6Address('2001:db8:ffff:1::1'),
peer_address=IPv6Address('fe80::3631:c4ff:fe3c:b2f1'),
options=[
RelayMessageOption(relayed_message=SolicitMessage(
transaction_id=bytes.fromhex('f350d6'),
options=[
ElapsedTimeOption(elapsed_time=0),
ClientIdOption(duid=LinkLayerDUID(hardware_type=1,
link_layer_address=bytes.fromhex('3431c43cb2f1'))),
IANAOption(iaid=bytes.fromhex('c43cb2f1')),
],
)),
EchoRequestOption(requested_options=[OPTION_SUBSCRIBER_ID]),
UnknownOption(option_type=65535),
InterfaceIdOption(interface_id=b'Fa2/3'),
RemoteIdOption(enterprise_number=9,
remote_id=bytes.fromhex('020023000001000a0003000100211c7d486e')),
]
)

bundle = TransactionBundle(incoming_message=relayed_solicit_message, received_over_multicast=True)
self.message_handler.handle(bundle, StatisticsSet())

self.assertIsInstance(bundle.outgoing_message, RelayReplyMessage)
self.assertEqual(len(bundle.outgoing_message.options), 2)
self.assertIsInstance(bundle.outgoing_message.options[0], InterfaceIdOption)
self.assertIsInstance(bundle.outgoing_message.options[1], RelayMessageOption)

def test_remote_id_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_REMOTE_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), 3)
self.assertIsInstance(bundle.outgoing_message.options[0], InterfaceIdOption)
self.assertIsInstance(bundle.outgoing_message.options[1], RelayMessageOption)
self.assertIsInstance(bundle.outgoing_message.options[2], RemoteIdOption)


if __name__ == '__main__':
unittest.main()
1 change: 1 addition & 0 deletions setup.py
Expand Up @@ -172,6 +172,7 @@ def read(filename):
'ntp = dhcpkit.ipv6.server.extensions.ntp',
'prefix-delegation = dhcpkit.ipv6.server.extensions.prefix_delegation',
'rate-limit = dhcpkit.ipv6.server.extensions.rate_limit',
'relay-echo-request = dhcpkit.ipv6.server.extensions.relay_echo_request',
'remote-id = dhcpkit.ipv6.server.extensions.remote_id',
'sip = dhcpkit.ipv6.server.extensions.sip_servers',
'sntp = dhcpkit.ipv6.server.extensions.sntp',
Expand Down

0 comments on commit f80235e

Please sign in to comment.