1#!/usr/bin/env python3
2#
3#  Copyright (c) 2022, The OpenThread Authors.
4#  All rights reserved.
5#
6#  Redistribution and use in source and binary forms, with or without
7#  modification, are permitted provided that the following conditions are met:
8#  1. Redistributions of source code must retain the above copyright
9#     notice, this list of conditions and the following disclaimer.
10#  2. Redistributions in binary form must reproduce the above copyright
11#     notice, this list of conditions and the following disclaimer in the
12#     documentation and/or other materials provided with the distribution.
13#  3. Neither the name of the copyright holder nor the
14#     names of its contributors may be used to endorse or promote products
15#     derived from this software without specific prior written permission.
16#
17#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20#  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21#  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27#  POSSIBILITY OF SUCH DAMAGE.
28
29from cli import verify
30from cli import verify_within
31import cli
32import time
33
34# -----------------------------------------------------------------------------------------------------------------------
35# Test description: Address Cache Table
36#
37# This test verifies the behavior of `AddressResolver` and how the cache
38# table is managed. In particular it verifies behavior query timeout and
39# query retry and snoop optimization.
40#
41# Build network topology
42#
43#  r3 ---- r1 ---- r2
44#  |               |
45#  |               |
46#  c3              c2
47#
48
49test_name = __file__[:-3] if __file__.endswith('.py') else __file__
50print('-' * 120)
51print('Starting \'{}\''.format(test_name))
52
53# -----------------------------------------------------------------------------------------------------------------------
54# Creating `cli.Node` instances
55
56speedup = 10
57cli.Node.set_time_speedup_factor(speedup)
58
59r1 = cli.Node()
60r2 = cli.Node()
61r3 = cli.Node()
62c2 = cli.Node()
63c3 = cli.Node()
64
65# -----------------------------------------------------------------------------------------------------------------------
66# Form topology
67
68r1.allowlist_node(r2)
69r1.allowlist_node(r3)
70
71r2.allowlist_node(r1)
72r2.allowlist_node(c2)
73
74r3.allowlist_node(r1)
75r3.allowlist_node(c3)
76
77c2.allowlist_node(r2)
78c3.allowlist_node(r3)
79
80r1.form('addrrslvr')
81
82prefix = 'fd00:abba::'
83r1.add_prefix(prefix + '/64', 'pos', 'med')
84r1.register_netdata()
85
86r2.join(r1)
87r3.join(r1)
88c2.join(r1, cli.JOIN_TYPE_END_DEVICE)
89c3.join(r1, cli.JOIN_TYPE_SLEEPY_END_DEVICE)
90c3.set_pollperiod(400)
91
92verify(r1.get_state() == 'leader')
93verify(r2.get_state() == 'router')
94verify(r3.get_state() == 'router')
95verify(c2.get_state() == 'child')
96verify(c3.get_state() == 'child')
97
98# -----------------------------------------------------------------------------------------------------------------------
99# Test Implementation
100
101# Wait till first router has either established a link or
102# has a valid "next hop" towards all other routers.
103
104r1_rloc16 = int(r1.get_rloc16(), 16)
105
106
107def check_r1_router_table():
108    table = r1.get_router_table()
109    verify(len(table) == 3)
110    for entry in table:
111        verify(int(entry['RLOC16'], 0) == r1_rloc16 or int(entry['Link']) == 1 or int(entry['Next Hop']) != 63)
112
113
114verify_within(check_r1_router_table, 120)
115
116# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
117
118r1_rloc = int(r1.get_rloc16(), 16)
119r2_rloc = int(r2.get_rloc16(), 16)
120r3_rloc = int(r3.get_rloc16(), 16)
121c2_rloc = int(c2.get_rloc16(), 16)
122c3_rloc = int(c3.get_rloc16(), 16)
123
124# AddressResolver constants:
125
126max_cache_entries = 16
127max_snooped_non_evictable = 2
128
129# Add IPv6 addresses matching the on-mesh prefix on all nodes
130
131r1.add_ip_addr(prefix + '1')
132
133num_addresses = 4  # Number of addresses to add on r2, r3, c2, and c3
134
135for num in range(num_addresses):
136    r2.add_ip_addr(prefix + "2:" + str(num))
137    r3.add_ip_addr(prefix + "3:" + str(num))
138    c2.add_ip_addr(prefix + "c2:" + str(num))
139    c3.add_ip_addr(prefix + "c3:" + str(num))
140
141# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
142
143# From r1 send msg to a group of addresses that are not provided by
144# any nodes in network.
145
146num_queries = 5
147stagger_interval = 1.2
148port = 1234
149initial_retry_delay = 8
150
151r1.udp_open()
152
153for num in range(num_queries):
154    r1.udp_send(prefix + '800:' + str(num), port, 'hi_nobody')
155    # Wait before next tx to stagger the address queries
156    # request ensuring different timeouts
157    time.sleep(stagger_interval / (num_queries * speedup))
158
159# Verify that we do see entries in cache table for all the addresses
160# and all are in "query" state
161
162cache_table = r1.get_eidcache()
163verify(len(cache_table) == num_queries)
164for entry in cache_table:
165    fields = entry.strip().split(' ')
166    verify(fields[2] == 'query')
167    verify(fields[3] == 'canEvict=0')
168    verify(fields[4].startswith('timeout='))
169    verify(int(fields[4].split('=')[1]) > 0)
170
171# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
172# Check the retry-query behavior
173#
174# Wait till all the address queries time out and verify they
175# enter "retry-query" state.
176
177
178def check_cache_entry_switch_to_retry_state():
179    cache_table = r1.get_eidcache()
180    for entry in cache_table:
181        fields = entry.strip().split(' ')
182        verify(fields[2] == 'retry')
183        verify(fields[3] == 'canEvict=1')
184        verify(fields[4].startswith('timeout='))
185        verify(int(fields[4].split('=')[1]) >= 0)
186        verify(fields[5].startswith('retryDelay='))
187        verify(int(fields[5].split('=')[1]) == initial_retry_delay)
188
189
190verify_within(check_cache_entry_switch_to_retry_state, 20)
191
192# Try sending again to same addresses which are all in "retry" state.
193
194for num in range(num_queries):
195    r1.udp_send(prefix + '800:' + str(num), port, 'hi_nobody')
196
197# Make sure the entries stayed in retry-query state as before.
198
199verify_within(check_cache_entry_switch_to_retry_state, 20)
200
201# Now wait for all entries to reach zero timeout.
202
203
204def check_cache_entry_in_retry_state_to_enter_rampdown():
205    cache_table = r1.get_eidcache()
206    for entry in cache_table:
207        fields = entry.strip().split(' ')
208        verify(fields[2] == 'retry')
209        verify(fields[3] == 'canEvict=1')
210        verify(fields[4].startswith('timeout='))
211        verify(fields[5].startswith('retryDelay='))
212        verify(fields[6] == 'rampDown=1')
213
214
215verify_within(check_cache_entry_in_retry_state_to_enter_rampdown, 20)
216
217# Now send again to the same addresses.
218
219for num in range(num_queries):
220    r1.udp_send(prefix + '800:' + str(num), port, 'hi_nobody')
221
222# We expect now after the delay to see retries for same addresses.
223
224
225def check_cache_entry_switch_to_query_state():
226    cache_table = r1.get_eidcache()
227    for entry in cache_table:
228        fields = entry.strip().split(' ')
229        verify(fields[2] == 'query')
230        verify(fields[3] == 'canEvict=1')
231
232
233verify_within(check_cache_entry_switch_to_query_state, 20)
234
235
236def check_cache_entry_switch_to_retry_state_with_double_retry_delay():
237    cache_table = r1.get_eidcache()
238    for entry in cache_table:
239        fields = entry.strip().split(' ')
240        verify(fields[2] == 'retry')
241        verify(fields[3] == 'canEvict=1')
242        verify(fields[4].startswith('timeout='))
243        verify(fields[5].startswith('retryDelay='))
244        verify(int(fields[5].split('=')[1]) == 2 * initial_retry_delay)
245
246
247verify_within(check_cache_entry_switch_to_retry_state_with_double_retry_delay, 40)
248
249verify_within(check_cache_entry_in_retry_state_to_enter_rampdown, 40)
250
251
252def check_cache_entry_ramp_down_to_initial_retry_delay():
253    cache_table = r1.get_eidcache()
254    for entry in cache_table:
255        fields = entry.strip().split(' ')
256        verify(fields[2] == 'retry')
257        verify(fields[3] == 'canEvict=1')
258        verify(fields[4].startswith('timeout='))
259        verify(fields[5].startswith('retryDelay='))
260        verify(int(fields[5].split('=')[1]) == initial_retry_delay)
261        verify(fields[6] == 'rampDown=1')
262
263
264verify_within(check_cache_entry_ramp_down_to_initial_retry_delay, 60)
265
266# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
267# Verify snoop optimization behavior.
268
269# Send to r1 from all addresses on r2.
270
271r2.udp_open()
272for num in range(num_addresses):
273    r2.udp_bind(prefix + '2:' + str(num), port)
274    r2.udp_send(prefix + '1', port, 'hi_r1_from_r2_snoop_me')
275
276# Verify that we see all addresses from r2 as snooped in cache table.
277# At most two of them should be marked as non-evictable.
278
279
280def check_cache_entry_contains_snooped_entries():
281    cache_table = r1.get_eidcache()
282    verify(len(cache_table) >= num_addresses)
283    snooped_count = 0
284    snooped_non_evictable = 0
285    for entry in cache_table:
286        fields = entry.strip().split(' ')
287        if fields[2] == 'snoop':
288            verify(fields[0].startswith('fd00:abba:0:0:0:0:2:'))
289            verify(int(fields[1], 16) == r2_rloc)
290            snooped_count = snooped_count + 1
291            if fields[3] == 'canEvict=0':
292                snooped_non_evictable = snooped_non_evictable + 1
293    verify(snooped_count == num_addresses)
294    verify(snooped_non_evictable == max_snooped_non_evictable)
295
296
297verify_within(check_cache_entry_contains_snooped_entries, 20)
298
299# Now we use the snooped entries by sending from r1 to r2 using
300# all its addresses.
301
302for num in range(num_addresses):
303    r1.udp_send(prefix + '2:' + str(num), port, 'hi_back_r2_from_r1')
304
305time.sleep(0.1)
306
307# We expect to see the entries to be in "cached" state now.
308
309cache_table = r1.get_eidcache()
310verify(len(cache_table) >= num_addresses)
311match_count = 0
312for entry in cache_table:
313    fields = entry.strip().split(' ')
314    if fields[0].startswith('fd00:abba:0:0:0:0:2:'):
315        verify(fields[2] == 'cache')
316        verify(fields[3] == 'canEvict=1')
317        match_count = match_count + 1
318verify(match_count == num_addresses)
319
320# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
321# Check query requests and last transaction time
322
323# Send from r1 to all addresses on r3. Check entries
324# for r3 are at the top of cache table list.
325
326for num in range(num_addresses):
327    r1.udp_send(prefix + '3:' + str(num), port, 'hi_r3_from_r1')
328
329
330def check_cache_entry_contains_r3_entries():
331    cache_table = r1.get_eidcache()
332    for num in range(num_addresses):
333        entry = cache_table[num]
334        fields = entry.strip().split(' ')
335        verify(fields[0].startswith('fd00:abba:0:0:0:0:3:'))
336        verify(int(fields[1], 16) == r3_rloc)
337        verify(fields[2] == 'cache')
338        verify(fields[3] == 'canEvict=1')
339        verify(fields[4] == 'transTime=0')
340
341
342verify_within(check_cache_entry_contains_r3_entries, 20)
343
344# Send from r1 to all addresses of c3 (sleepy child of r3)
345
346for num in range(num_addresses):
347    r1.udp_send(prefix + 'c3:' + str(num), port, 'hi_c3_from_r1')
348
349
350def check_cache_entry_contains_c3_entries():
351    cache_table = r1.get_eidcache()
352    for num in range(num_addresses):
353        entry = cache_table[num]
354        fields = entry.strip().split(' ')
355        verify(fields[0].startswith('fd00:abba:0:0:0:0:c3:'))
356        verify(int(fields[1], 16) == r3_rloc)
357        verify(fields[2] == 'cache')
358        verify(fields[3] == 'canEvict=1')
359        verify(fields[4] == 'transTime=0')
360
361
362verify_within(check_cache_entry_contains_c3_entries, 20)
363
364# Send again to r2. This should cause the related cache entries to
365# be moved to top of the list.
366
367for num in range(num_addresses):
368    r1.udp_send(prefix + '2:' + str(num), port, 'hi_again_r2_from_r1')
369
370
371def check_cache_entry_contains_r2_entries():
372    cache_table = r1.get_eidcache()
373    for num in range(num_addresses):
374        entry = cache_table[num]
375        fields = entry.strip().split(' ')
376        verify(fields[0].startswith('fd00:abba:0:0:0:0:2:'))
377        verify(int(fields[1], 16) == r2_rloc)
378        verify(fields[2] == 'cache')
379        verify(fields[3] == 'canEvict=1')
380
381
382verify_within(check_cache_entry_contains_r2_entries, 20)
383
384# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
385# Check behavior when address cache table is full.
386
387cache_table = r1.get_eidcache()
388verify(len(cache_table) == max_cache_entries)
389
390# From r1 send to non-existing addresses.
391
392for num in range(num_queries):
393    r1.udp_send(prefix + '900:' + str(num), port, 'hi_nobody!')
394
395cache_table = r1.get_eidcache()
396verify(len(cache_table) == max_cache_entries)
397
398# Send from c2 to r1 and verify that snoop optimization uses at most
399# `max_snooped_non_evictable` entries
400
401c2.udp_open()
402
403for num in range(num_addresses):
404    c2.udp_bind(prefix + 'c2:' + str(num), port)
405    c2.udp_send(prefix + '1', port, 'hi_r1_from_c2_snoop_me')
406
407
408def check_cache_entry_contains_max_allowed_snopped():
409    cache_table = r1.get_eidcache()
410    snooped_non_evictable = 0
411    for entry in cache_table:
412        fields = entry.strip().split(' ')
413        if fields[2] == 'snoop':
414            verify(fields[0].startswith('fd00:abba:0:0:0:0:c2:'))
415            verify(fields[3] == 'canEvict=0')
416            snooped_non_evictable = snooped_non_evictable + 1
417    verify(snooped_non_evictable == max_snooped_non_evictable)
418
419
420verify_within(check_cache_entry_contains_max_allowed_snopped, 20)
421
422# Now send from r1 to c2, the snooped entries would be used
423# some other addresses will  go through full address query.
424
425for num in range(num_addresses):
426    r1.udp_send(prefix + 'c2:' + str(num), port, 'hi_c2_from_r1')
427
428
429def check_cache_entry_contains_c2_entries():
430    cache_table = r1.get_eidcache()
431    for num in range(num_addresses):
432        entry = cache_table[num]
433        fields = entry.strip().split(' ')
434        verify(fields[0].startswith('fd00:abba:0:0:0:0:c2:'))
435        verify(int(fields[1], 16) == r2_rloc)
436        verify(fields[2] == 'cache')
437        verify(fields[3] == 'canEvict=1')
438
439
440verify_within(check_cache_entry_contains_c2_entries, 20)
441
442# -----------------------------------------------------------------------------------------------------------------------
443# Test finished
444
445cli.Node.finalize_all_nodes()
446
447print('\'{}\' passed.'.format(test_name))
448