mirror of
https://github.com/espressif/openthread.git
synced 2026-07-20 19:14:07 +00:00
[mlr] add MulticastListenersTable on BBR (#5292)
This commit adds the Multicast Listener Table for BBR:
- Implements Multicast Listener Table on BBR
- Adds registered MAs to MulticastListenerTable
- Reject when the table is full or the multicast address is invalid
and return failed status
- Respond with unsuccessful status along with failed addresses
- Expire Listeners from MulticastListenerTable per second (using
MinHeap for better performance)
- MLR enhancement
- Re-register only failed multicast addresses (used to register all
registering multicast addresses)
- Reference Device only APIs
- Some APIs are defined for reference devices and tests.
- Add tests
- Add unit test: test_multicast_listener_table.cpp (runs for 1.2-bbr
only)
- Add functional tests
This commit is contained in:
@@ -40,6 +40,8 @@ import time
|
||||
import unittest
|
||||
import binascii
|
||||
|
||||
from typing import Union
|
||||
|
||||
|
||||
class Node:
|
||||
|
||||
@@ -528,6 +530,35 @@ class Node:
|
||||
self.send_command(cmd)
|
||||
self._expect('Done')
|
||||
|
||||
def multicast_listener_list(self):
|
||||
cmd = 'bbr mgmt mlr listener'
|
||||
self.send_command(cmd)
|
||||
|
||||
table = {}
|
||||
for line in self._expect_results("\S+ \d+"):
|
||||
line = line.split()
|
||||
assert len(line) == 2, line
|
||||
ip = ipaddress.IPv6Address(line[0])
|
||||
timeout = int(line[1])
|
||||
assert ip not in table
|
||||
|
||||
table[ip] = timeout
|
||||
|
||||
return table
|
||||
|
||||
def multicast_listener_clear(self):
|
||||
cmd = f'bbr mgmt mlr listener clear'
|
||||
self.send_command(cmd)
|
||||
self._expect("Done")
|
||||
|
||||
def multicast_listener_add(self, ip: Union[ipaddress.IPv6Address, str], timeout: int = 0):
|
||||
if not isinstance(ip, ipaddress.IPv6Address):
|
||||
ip = ipaddress.IPv6Address(ip)
|
||||
|
||||
cmd = f'bbr mgmt mlr listener add {ip.compressed} {timeout}'
|
||||
self.send_command(cmd)
|
||||
self._expect(r"(Done|Error .*)")
|
||||
|
||||
def set_link_quality(self, addr, lqi):
|
||||
cmd = 'macfilter rss add-lqi %s %s' % (addr, lqi)
|
||||
self.send_command(cmd)
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
#
|
||||
|
||||
import ipaddress
|
||||
import logging
|
||||
import time
|
||||
import unittest
|
||||
from typing import Union, List
|
||||
|
||||
@@ -35,6 +37,8 @@ import config
|
||||
import network_layer
|
||||
import thread_cert
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
_, BBR_1, BBR_2, ROUTER_1_2, ROUTER_1_1, SED_1, MED_1, MED_2, FED_1 = range(9)
|
||||
|
||||
WAIT_ATTACH = 5
|
||||
@@ -124,6 +128,7 @@ class TestMulticastListenerRegistration(thread_cert.TestCase):
|
||||
assert (turn_on_bbr_2 or not turn_on_router_1_1) # ROUTER_1_1 needs BBR_2
|
||||
|
||||
# starting context id
|
||||
t0 = time.time()
|
||||
context_id = 1
|
||||
|
||||
# Bring up BBR_1, BBR_1 becomes Leader and Primary Backbone Router
|
||||
@@ -142,7 +147,8 @@ class TestMulticastListenerRegistration(thread_cert.TestCase):
|
||||
self.simulator.go(WAIT_TIME)
|
||||
self.assertEqual(self.nodes[BBR_1].get_backbone_router_state(), 'Primary')
|
||||
|
||||
self.pbbr_seq = 1
|
||||
self.pbbr_seq = 1
|
||||
self.pbbr_id = BBR_1
|
||||
|
||||
if turn_on_bbr_2:
|
||||
# Bring up BBR_2, BBR_2 becomes Router and Secondary Backbone Router
|
||||
@@ -197,6 +203,8 @@ class TestMulticastListenerRegistration(thread_cert.TestCase):
|
||||
self.simulator.go(WAIT_ATTACH)
|
||||
self.assertEqual(self.nodes[SED_1].get_state(), 'child')
|
||||
|
||||
logging.info("bootstrap takes %f seconds", time.time() - t0)
|
||||
|
||||
def test(self):
|
||||
self._bootstrap()
|
||||
|
||||
@@ -257,11 +265,171 @@ class TestMulticastListenerRegistration(thread_cert.TestCase):
|
||||
# Expect MLR.req sent by ROUTER_1_2 and FED_1
|
||||
self.__check_send_mlr_req(ROUTER_1_2, router_reg, should_send=True, expect_mlr_rsp=True)
|
||||
|
||||
def testMulticastListenersTableAdd(self):
|
||||
self._bootstrap()
|
||||
self.__test_multicast_listeners_table_add()
|
||||
|
||||
def testMulticastListenersTableExpire(self):
|
||||
self._bootstrap()
|
||||
self.__test_multicast_listeners_table_expire()
|
||||
|
||||
def testMulticastListenersTableFull(self):
|
||||
self._bootstrap()
|
||||
self.__test_multicast_listeners_table_full()
|
||||
|
||||
def testMulticastListenersTableTwoFreeSlot(self):
|
||||
self._bootstrap()
|
||||
self.__test_multicast_listeners_table_two_free_slots()
|
||||
|
||||
def testMulticastListenerTableAPI(self):
|
||||
self._bootstrap()
|
||||
self.__test_multicast_listeners_table_api()
|
||||
|
||||
def __test_multicast_listeners_table_api(self):
|
||||
self.assertTrue(self.nodes[BBR_1].multicast_listener_list() == {})
|
||||
|
||||
self.nodes[BBR_1].multicast_listener_add("ff04::1")
|
||||
self.assertEqual(1, len(self.nodes[BBR_1].multicast_listener_list()))
|
||||
self.nodes[BBR_1].multicast_listener_add("ff04::2", 300)
|
||||
self.assertEqual(2, len(self.nodes[BBR_1].multicast_listener_list()))
|
||||
self.nodes[BBR_1].multicast_listener_clear()
|
||||
self.assertTrue(self.nodes[BBR_1].multicast_listener_list() == {})
|
||||
|
||||
def __test_multicast_listeners_table_add(self):
|
||||
self.assertTrue(self.nodes[BBR_1].multicast_listener_list() == {})
|
||||
|
||||
all_mas = set()
|
||||
|
||||
CHECK_LIST = [(ROUTER_1_2, "ff04::1"), (FED_1, "ff04::2"), (MED_1, "ff04::3"), (SED_1, "ff04::4")]
|
||||
|
||||
for id, ip in CHECK_LIST:
|
||||
self.nodes[id].add_ipmaddr(ip)
|
||||
self.simulator.go(PARENT_AGGREGATE_DELAY + WAIT_REDUNDANCE)
|
||||
all_mas.add(ipaddress.IPv6Address(ip))
|
||||
self.assertEqual(all_mas, set(self.nodes[BBR_1].multicast_listener_list().keys()))
|
||||
|
||||
# restore
|
||||
for id, ip in CHECK_LIST:
|
||||
self.nodes[id].del_ipmaddr(ip)
|
||||
|
||||
self.simulator.go(WAIT_REDUNDANCE)
|
||||
|
||||
def __test_multicast_listeners_table_expire(self):
|
||||
self.assertEqual({}, self.nodes[BBR_1].multicast_listener_list())
|
||||
|
||||
all_mas = set()
|
||||
|
||||
CHECK_LIST = [(ROUTER_1_2, "ff04::1"), (FED_1, "ff04::2"), (MED_1, "ff04::3"), (SED_1, "ff04::4")]
|
||||
|
||||
for id, ip in CHECK_LIST:
|
||||
self.nodes[id].add_ipmaddr(ip)
|
||||
self.simulator.go(PARENT_AGGREGATE_DELAY + WAIT_REDUNDANCE)
|
||||
all_mas.add(ipaddress.IPv6Address(ip))
|
||||
self.assertEqual(all_mas, set(self.nodes[BBR_1].multicast_listener_list().keys()))
|
||||
|
||||
# remove MAs from nodes, and wait for Multicast Listeners to expire on BBR_1
|
||||
for id, ip in CHECK_LIST:
|
||||
self.nodes[id].del_ipmaddr(ip)
|
||||
|
||||
# Wait for MLR_TIMEOUT/3, and expect Multicast Listeners not to expire.
|
||||
self.simulator.go(MLR_TIMEOUT / 3)
|
||||
self.assertEqual(all_mas, set(self.nodes[BBR_1].multicast_listener_list().keys()))
|
||||
|
||||
# Wait for MLR_TIMEOUT*2/3, and expect all Multicast Listeners to expire.
|
||||
self.simulator.go(MLR_TIMEOUT * 2 / 3 + WAIT_REDUNDANCE)
|
||||
self.assertEqual({}, self.nodes[BBR_1].multicast_listener_list())
|
||||
|
||||
def __test_multicast_listeners_table_full(self):
|
||||
self.assertTrue(self.nodes[BBR_1].multicast_listener_list() == {})
|
||||
|
||||
table = set()
|
||||
|
||||
# Add to Multicast Listeners Table until it's full
|
||||
for i in range(1, 76):
|
||||
self.nodes[BBR_1].multicast_listener_add(f"ff04::{i}")
|
||||
table.add(ipaddress.IPv6Address(f"ff04::{i}"))
|
||||
self.assertEqual(table, set(self.nodes[BBR_1].multicast_listener_list().keys()))
|
||||
|
||||
# Add when Multicast Listeners Table is full should not succeed
|
||||
self.nodes[BBR_1].multicast_listener_add(f"ff05::1")
|
||||
self.assertEqual(table, set(self.nodes[BBR_1].multicast_listener_list().keys()))
|
||||
|
||||
self.flush_all()
|
||||
|
||||
# Expect PBBR to respond with MLR_NO_RESOURCES Multicast Listeners Table when it's full
|
||||
self.nodes[ROUTER_1_2].add_ipmaddr("ff06::1")
|
||||
self.simulator.go(WAIT_REDUNDANCE)
|
||||
|
||||
self.__check_send_mlr_req(ROUTER_1_2,
|
||||
"ff06::1",
|
||||
should_send=True,
|
||||
expect_mlr_rsp=True,
|
||||
expect_mlr_rsp_status=4)
|
||||
|
||||
self.assertEqual(table, set(self.nodes[BBR_1].multicast_listener_list().keys()))
|
||||
|
||||
self.nodes[MED_1].add_ipmaddr("ff06::2")
|
||||
self.simulator.go(PARENT_AGGREGATE_DELAY + WAIT_REDUNDANCE)
|
||||
|
||||
self.__check_send_mlr_req(ROUTER_1_2,
|
||||
"ff06::2",
|
||||
should_send=True,
|
||||
expect_mlr_rsp=True,
|
||||
expect_mlr_rsp_status=4)
|
||||
|
||||
self.assertEqual(table, set(self.nodes[BBR_1].multicast_listener_list().keys()))
|
||||
|
||||
# the ROUTER_1_2 should be resending both ff06::1 and ff06::2
|
||||
for i in range(3):
|
||||
self.simulator.go(REREG_DELAY + WAIT_REDUNDANCE)
|
||||
self.__check_send_mlr_req(ROUTER_1_2, ['ff06::1', 'ff06::2'],
|
||||
should_send=True,
|
||||
expect_mlr_rsp=True,
|
||||
expect_mlr_rsp_status=4)
|
||||
|
||||
# Restore
|
||||
self.nodes[ROUTER_1_2].del_ipmaddr("ff06::1")
|
||||
self.nodes[MED_1].del_ipmaddr("ff06::2")
|
||||
self.simulator.go(WAIT_REDUNDANCE)
|
||||
|
||||
def __test_multicast_listeners_table_two_free_slots(self):
|
||||
|
||||
# Add to Multicast Listeners Table until there is only two free slots
|
||||
for i in range(1, 74):
|
||||
self.nodes[BBR_1].multicast_listener_add(f"ff04::{i}")
|
||||
|
||||
self.assertEqual(73, len(self.nodes[BBR_1].multicast_listener_list()))
|
||||
|
||||
self.nodes[MED_1].add_ipmaddr("ff05::1")
|
||||
self.nodes[MED_1].add_ipmaddr("ff05::2")
|
||||
self.nodes[MED_2].add_ipmaddr("ff05::3")
|
||||
self.nodes[MED_2].add_ipmaddr("ff05::4")
|
||||
self.nodes[SED_1].add_ipmaddr("ff05::5")
|
||||
self.nodes[SED_1].add_ipmaddr("ff05::6")
|
||||
|
||||
self.simulator.go(PARENT_AGGREGATE_DELAY + WAIT_REDUNDANCE)
|
||||
self.__check_send_mlr_req(ROUTER_1_2, ["ff05::1", "ff05::2", "ff05::3", "ff05::4", "ff05::5", "ff05::6"])
|
||||
|
||||
self.simulator.go(PARENT_AGGREGATE_DELAY + WAIT_REDUNDANCE)
|
||||
self.flush_all()
|
||||
# two addresses should be registered, others can not
|
||||
for i in range(3):
|
||||
self.simulator.go(PARENT_AGGREGATE_DELAY + WAIT_REDUNDANCE)
|
||||
self.assertEqual(4, len(set(self.__get_registered_MAs(ROUTER_1_2))))
|
||||
|
||||
# Restore
|
||||
self.nodes[MED_1].del_ipmaddr("ff05::1")
|
||||
self.nodes[MED_1].del_ipmaddr("ff05::2")
|
||||
self.nodes[MED_2].del_ipmaddr("ff05::3")
|
||||
self.nodes[MED_2].del_ipmaddr("ff05::4")
|
||||
self.nodes[SED_1].del_ipmaddr("ff05::5")
|
||||
self.nodes[SED_1].del_ipmaddr("ff05::6")
|
||||
|
||||
def __check_mlr_ok(self, id, is_ftd, is_parent_1p1=False):
|
||||
"""Check if MLR works for the node"""
|
||||
# Add MA1 and send MLR.req
|
||||
print("======== checking MLR: Node%d (%s), Parent=%s ========" %
|
||||
(id, 'FTD' if is_ftd else 'MTD', '1.1' if is_parent_1p1 else '1.2'))
|
||||
logging.info("======== checking MLR: Node%d (%s), Parent=%s ========" %
|
||||
(id, 'FTD' if is_ftd else 'MTD', '1.1' if is_parent_1p1 else '1.2'))
|
||||
expect_mlr_req = is_ftd or is_parent_1p1
|
||||
|
||||
if id == ROUTER_1_2:
|
||||
@@ -278,11 +446,11 @@ class TestMulticastListenerRegistration(thread_cert.TestCase):
|
||||
|
||||
for addr in [MA5, MA6]:
|
||||
self.__check_ipmaddr_add(id, parent_id, addr, expect_mlr_req=False, expect_mlr_req_proxied=False)
|
||||
print('=' * 120)
|
||||
logging.info('=' * 120)
|
||||
|
||||
def __check_ipmaddr_add(self, id, parent_id, addr, expect_mlr_req=True, expect_mlr_req_proxied=False):
|
||||
"""Check MLR works for the added multicast address"""
|
||||
print("Node %d: ipmaddr %s" % (id, addr))
|
||||
logging.info("Node %d: ipmaddr %s" % (id, addr))
|
||||
self.flush_all()
|
||||
self.nodes[id].add_ipmaddr(addr)
|
||||
self.assertTrue(self.nodes[id].has_ipmaddr(addr))
|
||||
@@ -315,12 +483,14 @@ class TestMulticastListenerRegistration(thread_cert.TestCase):
|
||||
addrs: Union[List[str], str],
|
||||
should_send=True,
|
||||
expect_mlr_rsp=False,
|
||||
expect_mlr_rsp_status=0,
|
||||
expect_mlr_req_num=None,
|
||||
expect_unique_reg=False):
|
||||
if isinstance(addrs, str):
|
||||
addrs = [addrs]
|
||||
|
||||
reg_mas = self.__get_registered_MAs(id, expect_mlr_req_num=expect_mlr_req_num)
|
||||
message_ids = []
|
||||
reg_mas = self.__get_registered_MAs(id, expect_mlr_req_num=expect_mlr_req_num, message_ids=message_ids)
|
||||
if should_send:
|
||||
for addr in addrs:
|
||||
self.assertIn(ipaddress.IPv6Address(addr), reg_mas)
|
||||
@@ -329,13 +499,37 @@ class TestMulticastListenerRegistration(thread_cert.TestCase):
|
||||
|
||||
# BBR should send MLR.rsp ACK
|
||||
if expect_mlr_rsp:
|
||||
messages = self.simulator.get_messages_sent_by(BBR_1)
|
||||
messages.next_coap_message('2.04')
|
||||
message_id = message_ids[0]
|
||||
rsp = self.__expect_MLR_rsp(message_id)
|
||||
|
||||
logging.info('MLR.rsp %s uri_path=%s, payload=%s', rsp, rsp.coap.uri_path, rsp.coap.payload)
|
||||
|
||||
statusTlv = None
|
||||
for tlv in rsp.coap.payload:
|
||||
if isinstance(tlv, network_layer.Status):
|
||||
statusTlv = tlv
|
||||
break
|
||||
|
||||
self.assertIsNotNone(statusTlv)
|
||||
self.assertEqual(expect_mlr_rsp_status, statusTlv.status)
|
||||
|
||||
else:
|
||||
for addr in addrs:
|
||||
self.assertNotIn(ipaddress.IPv6Address(addr), reg_mas)
|
||||
|
||||
def __get_registered_MAs(self, id, expect_mlr_req_num=None):
|
||||
def __expect_MLR_rsp(self, message_id):
|
||||
logging.info("Expecting MLR.rsp with message ID = %s", message_id)
|
||||
messages = self.simulator.get_messages_sent_by(self.pbbr_id)
|
||||
logging.info("PBBR %d messages: %s", self.pbbr_id, messages)
|
||||
|
||||
while True:
|
||||
msg = messages.next_coap_message('2.04')
|
||||
logging.info('Check ACK for %s: %s, %s, %s, %s', message_id, msg, msg.coap.message_id, msg.coap.uri_path,
|
||||
msg.coap.payload)
|
||||
if msg.coap.message_id == message_id:
|
||||
return msg
|
||||
|
||||
def __get_registered_MAs(self, id, expect_mlr_req_num=None, message_ids=None):
|
||||
"""Get MAs registered via MLR.req by the node"""
|
||||
messages = self.simulator.get_messages_sent_by(id)
|
||||
reg_mas = []
|
||||
@@ -343,10 +537,14 @@ class TestMulticastListenerRegistration(thread_cert.TestCase):
|
||||
msg = messages.next_coap_message('0.02', '/n/mr', assert_enabled=False)
|
||||
if not msg:
|
||||
break
|
||||
|
||||
logging.info("MLR.req: %s %s" % (msg.coap.message_id, msg.coap.payload))
|
||||
addrs = msg.get_coap_message_tlv(network_layer.IPv6Addresses)
|
||||
reg_mas.append(addrs)
|
||||
if message_ids is not None:
|
||||
message_ids.append(msg.coap.message_id)
|
||||
|
||||
print('Node %d registered MAs: %s' % (id, reg_mas))
|
||||
logging.info('Node %d registered MAs: %s' % (id, reg_mas))
|
||||
|
||||
if expect_mlr_req_num is not None:
|
||||
self.assertEqual(len(reg_mas), expect_mlr_req_num)
|
||||
@@ -358,6 +556,7 @@ class TestMulticastListenerRegistration(thread_cert.TestCase):
|
||||
|
||||
def __check_renewing(self, id, parent_id, addr, expect_mlr_req=True, expect_mlr_req_proxied=False):
|
||||
"""Check if MLR works that a node can renew it's registered MAs"""
|
||||
assert self.pbbr_id == BBR_1
|
||||
self.flush_all()
|
||||
self.simulator.go(MLR_TIMEOUT + WAIT_REDUNDANCE)
|
||||
|
||||
@@ -401,10 +600,14 @@ class TestMulticastListenerRegistration(thread_cert.TestCase):
|
||||
def __check_rereg_pbbr_change(self, id, parent_id, addr, expect_mlr_req=True, expect_mlr_req_proxied=False):
|
||||
"""Check if MLR works that a node can do MLR reregistration when PBBR changes"""
|
||||
# Make BBR_2 to be Primary and expect MLR.req within REREG_DELAY
|
||||
assert self.pbbr_id == BBR_1
|
||||
|
||||
self.flush_all()
|
||||
self.nodes[BBR_1].disable_backbone_router()
|
||||
self.simulator.go(BBR_REGISTRATION_JITTER + WAIT_REDUNDANCE)
|
||||
self.assertEqual(self.nodes[BBR_2].get_backbone_router_state(), 'Primary')
|
||||
self.pbbr_id = BBR_2
|
||||
|
||||
self.simulator.go(REREG_DELAY + WAIT_REDUNDANCE)
|
||||
|
||||
self.__check_send_mlr_req(id, addr, should_send=expect_mlr_req, expect_mlr_rsp=expect_mlr_req)
|
||||
@@ -423,11 +626,12 @@ class TestMulticastListenerRegistration(thread_cert.TestCase):
|
||||
self.nodes[BBR_2].enable_backbone_router()
|
||||
self.simulator.go(BBR_REGISTRATION_JITTER + WAIT_REDUNDANCE)
|
||||
self.assertEqual(self.nodes[BBR_2].get_backbone_router_state(), 'Secondary')
|
||||
self.pbbr_id = BBR_1
|
||||
|
||||
def __switch_to_1_1_parent(self):
|
||||
"""Check if MLR works when nodes are switching to a 1.1 parent"""
|
||||
# Add MA1 to EDs to prepare for parent switching
|
||||
print("=" * 10, "switching to 1.1 parent", '=' * 10)
|
||||
logging.info("=" * 10 + " switching to 1.1 parent " + '=' * 10)
|
||||
|
||||
self.flush_all()
|
||||
|
||||
@@ -468,7 +672,7 @@ class TestMulticastListenerRegistration(thread_cert.TestCase):
|
||||
def __switch_to_1_2_parent(self):
|
||||
"""Check if MLR works when nodes are switching to a 1.2 parent"""
|
||||
# Add MA1 to EDs to prepare for parent switching
|
||||
print("=" * 10, "switching to 1.2 parent", '=' * 10)
|
||||
logging.info("=" * 10, "switching to 1.2 parent", '=' * 10)
|
||||
|
||||
self.flush_all()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user