blob: 2321aa7c094fb9c61a0b3fb240fdcdd1f92dcf96 [file] [log] [blame]
Eyal Baric86e5922017-07-02 18:33:16 +03001#!/usr/bin/env python
2""" L2BD ARP term Test """
3
4import unittest
5import random
6import copy
7
Eyal Bari20197482017-09-13 12:29:08 +03008from socket import AF_INET, AF_INET6
Eyal Bari758137a2017-07-05 14:31:30 +03009
Eyal Baric86e5922017-07-02 18:33:16 +030010from scapy.packet import Raw
11from scapy.layers.l2 import Ether, ARP
12from scapy.layers.inet import IP
Eyal Bari758137a2017-07-05 14:31:30 +030013from scapy.utils import inet_pton, inet_ntop
14from scapy.utils6 import in6_getnsma, in6_getnsmac, in6_ptop, in6_islladdr, \
15 in6_mactoifaceid, in6_ismaddr
16from scapy.layers.inet6 import IPv6, UDP, ICMPv6ND_NS, ICMPv6ND_RS, \
17 ICMPv6ND_RA, ICMPv6NDOptSrcLLAddr, getmacbyip6, ICMPv6MRD_Solicitation, \
18 ICMPv6NDOptMTU, ICMPv6NDOptSrcLLAddr, ICMPv6NDOptPrefixInfo, \
19 ICMPv6ND_NA, ICMPv6NDOptDstLLAddr, ICMPv6DestUnreach, icmp6types
Eyal Baric86e5922017-07-02 18:33:16 +030020
21from framework import VppTestCase, VppTestRunner
Ole Troan7f991832018-12-06 17:35:12 +010022from util import Host, ppp
Eyal Baric86e5922017-07-02 18:33:16 +030023
24
25class TestL2bdArpTerm(VppTestCase):
26 """ L2BD arp termination Test Case """
27
28 @classmethod
29 def setUpClass(cls):
30 """
31 Perform standard class setup (defined by class method setUpClass in
32 class VppTestCase) before running the test case, set test case related
33 variables and configure VPP.
34 """
35 super(TestL2bdArpTerm, cls).setUpClass()
36
37 try:
38 # Create pg interfaces
39 n_bd = 1
40 cls.ifs_per_bd = ifs_per_bd = 3
41 n_ifs = n_bd * ifs_per_bd
42 cls.create_pg_interfaces(range(n_ifs))
43
44 # Set up all interfaces
45 for i in cls.pg_interfaces:
46 i.admin_up()
47
48 cls.hosts = set()
49
50 except Exception:
51 super(TestL2bdArpTerm, cls).tearDownClass()
52 raise
53
54 def setUp(self):
55 """
56 Clear trace and packet infos before running each test.
57 """
58 self.reset_packet_infos()
59 super(TestL2bdArpTerm, self).setUp()
60
61 def tearDown(self):
62 """
63 Show various debug prints after each test.
64 """
65 super(TestL2bdArpTerm, self).tearDown()
66 if not self.vpp_dead:
67 self.logger.info(self.vapi.ppcli("show l2fib verbose"))
68 self.logger.info(self.vapi.ppcli("show bridge-domain 1 detail"))
69
Eyal Bari758137a2017-07-05 14:31:30 +030070 def add_del_arp_term_hosts(self, entries, bd_id=1, is_add=1, is_ipv6=0):
Eyal Baric86e5922017-07-02 18:33:16 +030071 for e in entries:
Neale Ranns4d5b9172018-10-24 02:57:49 -070072 ip = e.ip4 if is_ipv6 == 0 else e.ip6
Eyal Baric86e5922017-07-02 18:33:16 +030073 self.vapi.bd_ip_mac_add_del(bd_id=bd_id,
Ole Troan8006c6a2018-12-17 12:02:26 +010074 mac=e.mac,
Ole Troan0bcad322018-12-11 13:04:01 +010075 ip=ip,
Eyal Bari758137a2017-07-05 14:31:30 +030076 is_ipv6=is_ipv6,
Eyal Baric86e5922017-07-02 18:33:16 +030077 is_add=is_add)
78
79 @classmethod
80 def mac_list(cls, b6_range):
81 return ["00:00:ca:fe:00:%02x" % b6 for b6 in b6_range]
82
83 @classmethod
84 def ip4_host(cls, subnet, host, mac):
85 return Host(mac=mac,
86 ip4="172.17.1%02u.%u" % (subnet, host))
87
88 @classmethod
89 def ip4_hosts(cls, subnet, start, mac_list):
90 return {cls.ip4_host(subnet, start + j, mac_list[j])
91 for j in range(len(mac_list))}
92
93 @classmethod
Eyal Bari758137a2017-07-05 14:31:30 +030094 def ip6_host(cls, subnet, host, mac):
95 return Host(mac=mac,
96 ip6="fd01:%x::%x" % (subnet, host))
97
98 @classmethod
99 def ip6_hosts(cls, subnet, start, mac_list):
100 return {cls.ip6_host(subnet, start + j, mac_list[j])
101 for j in range(len(mac_list))}
102
103 @classmethod
Eyal Baric86e5922017-07-02 18:33:16 +0300104 def bd_swifs(cls, b):
105 n = cls.ifs_per_bd
106 start = (b - 1) * n
107 return [cls.pg_interfaces[j] for j in range(start, start + n)]
108
109 def bd_add_del(self, bd_id=1, is_add=1):
110 if is_add:
111 self.vapi.bridge_domain_add_del(bd_id=bd_id, is_add=is_add)
112 for swif in self.bd_swifs(bd_id):
113 swif_idx = swif.sw_if_index
114 self.vapi.sw_interface_set_l2_bridge(
115 swif_idx, bd_id=bd_id, enable=is_add)
116 if not is_add:
117 self.vapi.bridge_domain_add_del(bd_id=bd_id, is_add=is_add)
118
119 @classmethod
Eyal Bari20197482017-09-13 12:29:08 +0300120 def arp_req(cls, src_host, host):
Eyal Baric86e5922017-07-02 18:33:16 +0300121 return (Ether(dst="ff:ff:ff:ff:ff:ff", src=src_host.mac) /
122 ARP(op="who-has",
123 hwsrc=src_host.bin_mac,
124 pdst=host.ip4,
125 psrc=src_host.ip4))
126
127 @classmethod
128 def arp_reqs(cls, src_host, entries):
Eyal Bari20197482017-09-13 12:29:08 +0300129 return [cls.arp_req(src_host, e) for e in entries]
130
131 @classmethod
132 def garp_req(cls, host):
133 return cls.arp_req(host, host)
134
135 @classmethod
136 def garp_reqs(cls, entries):
137 return [cls.garp_req(e) for e in entries]
Eyal Baric86e5922017-07-02 18:33:16 +0300138
Eyal Bari758137a2017-07-05 14:31:30 +0300139 def arp_resp_host(self, src_host, arp_resp):
Eyal Baric86e5922017-07-02 18:33:16 +0300140 ether = arp_resp[Ether]
141 self.assertEqual(ether.dst, src_host.mac)
142
143 arp = arp_resp[ARP]
144 self.assertEqual(arp.hwtype, 1)
145 self.assertEqual(arp.ptype, 0x800)
146 self.assertEqual(arp.hwlen, 6)
147 self.assertEqual(arp.plen, 4)
148 arp_opts = {"who-has": 1, "is-at": 2}
149 self.assertEqual(arp.op, arp_opts["is-at"])
150 self.assertEqual(arp.hwdst, src_host.mac)
151 self.assertEqual(arp.pdst, src_host.ip4)
Eyal Bari758137a2017-07-05 14:31:30 +0300152 return Host(mac=arp.hwsrc, ip4=arp.psrc)
Eyal Baric86e5922017-07-02 18:33:16 +0300153
154 def arp_resp_hosts(self, src_host, pkts):
Eyal Bari758137a2017-07-05 14:31:30 +0300155 return {self.arp_resp_host(src_host, p) for p in pkts}
156
Paul Vinciguerra9db94452018-11-25 10:46:44 -0800157 @staticmethod
158 def inttoip4(ip):
Eyal Bari20197482017-09-13 12:29:08 +0300159 o1 = int(ip / 16777216) % 256
160 o2 = int(ip / 65536) % 256
161 o3 = int(ip / 256) % 256
162 o4 = int(ip) % 256
Paul Vinciguerra9db94452018-11-25 10:46:44 -0800163 return '%s.%s.%s.%s' % (o1, o2, o3, o4)
Eyal Bari20197482017-09-13 12:29:08 +0300164
165 def arp_event_host(self, e):
166 return Host(mac=':'.join(['%02x' % ord(char) for char in e.new_mac]),
167 ip4=self.inttoip4(e.address))
168
169 def arp_event_hosts(self, evs):
170 return {self.arp_event_host(e) for e in evs}
171
Eyal Baric125ecc2017-09-20 11:29:17 +0300172 def nd_event_host(self, e):
173 return Host(mac=':'.join(['%02x' % ord(char) for char in e.new_mac]),
174 ip6=inet_ntop(AF_INET6, e.address))
175
176 def nd_event_hosts(self, evs):
177 return {self.nd_event_host(e) for e in evs}
178
Eyal Bari758137a2017-07-05 14:31:30 +0300179 @classmethod
180 def ns_req(cls, src_host, host):
181 nsma = in6_getnsma(inet_pton(AF_INET6, "fd10::ffff"))
182 d = inet_ntop(AF_INET6, nsma)
183 return (Ether(dst="ff:ff:ff:ff:ff:ff", src=src_host.mac) /
184 IPv6(dst=d, src=src_host.ip6) /
185 ICMPv6ND_NS(tgt=host.ip6) /
186 ICMPv6NDOptSrcLLAddr(lladdr=src_host.mac))
187
188 @classmethod
Eyal Baric125ecc2017-09-20 11:29:17 +0300189 def ns_reqs_dst(cls, entries, dst_host):
190 return [cls.ns_req(e, dst_host) for e in entries]
191
192 @classmethod
193 def ns_reqs_src(cls, src_host, entries):
Eyal Bari758137a2017-07-05 14:31:30 +0300194 return [cls.ns_req(src_host, e) for e in entries]
195
196 def na_resp_host(self, src_host, rx):
197 self.assertEqual(rx[Ether].dst, src_host.mac)
198 self.assertEqual(in6_ptop(rx[IPv6].dst),
199 in6_ptop(src_host.ip6))
200
201 self.assertTrue(rx.haslayer(ICMPv6ND_NA))
202 self.assertTrue(rx.haslayer(ICMPv6NDOptDstLLAddr))
203
204 na = rx[ICMPv6ND_NA]
205 return Host(mac=na.lladdr, ip6=na.tgt)
206
207 def na_resp_hosts(self, src_host, pkts):
208 return {self.na_resp_host(src_host, p) for p in pkts}
Eyal Baric86e5922017-07-02 18:33:16 +0300209
210 def set_bd_flags(self, bd_id, **args):
211 """
212 Enable/disable defined feature(s) of the bridge domain.
213
214 :param int bd_id: Bridge domain ID.
215 :param list args: List of feature/status pairs. Allowed features: \
216 learn, forward, flood, uu_flood and arp_term. Status False means \
217 disable, status True means enable the feature.
218 :raise: ValueError in case of unknown feature in the input.
219 """
220 for flag in args:
221 if flag == "learn":
222 feature_bitmap = 1 << 0
223 elif flag == "forward":
224 feature_bitmap = 1 << 1
225 elif flag == "flood":
226 feature_bitmap = 1 << 2
227 elif flag == "uu_flood":
228 feature_bitmap = 1 << 3
229 elif flag == "arp_term":
230 feature_bitmap = 1 << 4
231 else:
232 raise ValueError("Unknown feature used: %s" % flag)
233 is_set = 1 if args[flag] else 0
234 self.vapi.bridge_flags(bd_id, is_set, feature_bitmap)
235 self.logger.info("Bridge domain ID %d updated" % bd_id)
236
237 def verify_arp(self, src_host, req_hosts, resp_hosts, bd_id=1):
238 reqs = self.arp_reqs(src_host, req_hosts)
239
240 for swif in self.bd_swifs(bd_id):
241 swif.add_stream(reqs)
242
243 self.pg_enable_capture(self.pg_interfaces)
244 self.pg_start()
245
246 for swif in self.bd_swifs(bd_id):
247 resp_pkts = swif.get_capture(len(resp_hosts))
248 resps = self.arp_resp_hosts(src_host, resp_pkts)
249 self.assertEqual(len(resps ^ resp_hosts), 0)
250
Eyal Bari758137a2017-07-05 14:31:30 +0300251 def verify_nd(self, src_host, req_hosts, resp_hosts, bd_id=1):
Eyal Baric125ecc2017-09-20 11:29:17 +0300252 reqs = self.ns_reqs_src(src_host, req_hosts)
Eyal Bari758137a2017-07-05 14:31:30 +0300253
254 for swif in self.bd_swifs(bd_id):
255 swif.add_stream(reqs)
256
257 self.pg_enable_capture(self.pg_interfaces)
258 self.pg_start()
259
260 for swif in self.bd_swifs(bd_id):
261 resp_pkts = swif.get_capture(len(resp_hosts))
262 resps = self.na_resp_hosts(src_host, resp_pkts)
263 self.assertEqual(len(resps ^ resp_hosts), 0)
264
Eyal Baric86e5922017-07-02 18:33:16 +0300265 def test_l2bd_arp_term_01(self):
266 """ L2BD arp term - add 5 hosts, verify arp responses
267 """
268 src_host = self.ip4_host(50, 50, "00:00:11:22:33:44")
269 self.bd_add_del(1, is_add=1)
270 self.set_bd_flags(1, arp_term=True, flood=False,
271 uu_flood=False, learn=False)
272 macs = self.mac_list(range(1, 5))
273 hosts = self.ip4_hosts(4, 1, macs)
274 self.add_del_arp_term_hosts(hosts, is_add=1)
Neale Ranns4d5b9172018-10-24 02:57:49 -0700275
Eyal Baric86e5922017-07-02 18:33:16 +0300276 self.verify_arp(src_host, hosts, hosts)
277 type(self).hosts = hosts
278
279 def test_l2bd_arp_term_02(self):
280 """ L2BD arp term - delete 3 hosts, verify arp responses
281 """
282 src_host = self.ip4_host(50, 50, "00:00:11:22:33:44")
283 macs = self.mac_list(range(1, 3))
284 deleted = self.ip4_hosts(4, 1, macs)
285 self.add_del_arp_term_hosts(deleted, is_add=0)
286 remaining = self.hosts - deleted
287 self.verify_arp(src_host, self.hosts, remaining)
288 type(self).hosts = remaining
289 self.bd_add_del(1, is_add=0)
290
291 def test_l2bd_arp_term_03(self):
292 """ L2BD arp term - recreate BD1, readd 3 hosts, verify arp responses
293 """
294 src_host = self.ip4_host(50, 50, "00:00:11:22:33:44")
295 self.bd_add_del(1, is_add=1)
296 self.set_bd_flags(1, arp_term=True, flood=False,
297 uu_flood=False, learn=False)
298 macs = self.mac_list(range(1, 3))
299 readded = self.ip4_hosts(4, 1, macs)
300 self.add_del_arp_term_hosts(readded, is_add=1)
301 self.verify_arp(src_host, self.hosts | readded, readded)
302 type(self).hosts = readded
303
304 def test_l2bd_arp_term_04(self):
305 """ L2BD arp term - 2 IP4 addrs per host
306 """
307 src_host = self.ip4_host(50, 50, "00:00:11:22:33:44")
308 macs = self.mac_list(range(1, 3))
309 sub5_hosts = self.ip4_hosts(5, 1, macs)
310 self.add_del_arp_term_hosts(sub5_hosts, is_add=1)
311 hosts = self.hosts | sub5_hosts
312 self.verify_arp(src_host, hosts, hosts)
313 type(self).hosts = hosts
314 self.bd_add_del(1, is_add=0)
315
316 def test_l2bd_arp_term_05(self):
317 """ L2BD arp term - create and update 10 IP4-mac pairs
318 """
319 src_host = self.ip4_host(50, 50, "00:00:11:22:33:44")
320 self.bd_add_del(1, is_add=1)
321 self.set_bd_flags(1, arp_term=True, flood=False,
322 uu_flood=False, learn=False)
323 macs1 = self.mac_list(range(10, 20))
324 hosts1 = self.ip4_hosts(5, 1, macs1)
325 self.add_del_arp_term_hosts(hosts1, is_add=1)
326 self.verify_arp(src_host, hosts1, hosts1)
327 macs2 = self.mac_list(range(20, 30))
328 hosts2 = self.ip4_hosts(5, 1, macs2)
329 self.add_del_arp_term_hosts(hosts2, is_add=1)
330 self.verify_arp(src_host, hosts1, hosts2)
331 self.bd_add_del(1, is_add=0)
332
Eyal Bari758137a2017-07-05 14:31:30 +0300333 def test_l2bd_arp_term_06(self):
334 """ L2BD arp/ND term - hosts with both ip4/ip6
335 """
336 src_host4 = self.ip4_host(50, 50, "00:00:11:22:33:44")
337 src_host6 = self.ip6_host(50, 50, "00:00:11:22:33:44")
338 self.bd_add_del(1, is_add=1)
339 # enable flood to make sure requests are not flooded
340 self.set_bd_flags(1, arp_term=True, flood=True,
341 uu_flood=False, learn=False)
342 macs = self.mac_list(range(10, 20))
343 hosts6 = self.ip6_hosts(5, 1, macs)
344 hosts4 = self.ip4_hosts(5, 1, macs)
345 self.add_del_arp_term_hosts(hosts4, is_add=1)
346 self.add_del_arp_term_hosts(hosts6, is_add=1, is_ipv6=1)
347 self.verify_arp(src_host4, hosts4, hosts4)
348 self.verify_nd(src_host6, hosts6, hosts6)
349 self.bd_add_del(1, is_add=0)
350
351 def test_l2bd_arp_term_07(self):
352 """ L2BD ND term - Add and Del hosts, verify ND replies
353 """
354 src_host6 = self.ip6_host(50, 50, "00:00:11:22:33:44")
355 self.bd_add_del(1, is_add=1)
356 self.set_bd_flags(1, arp_term=True, flood=False,
357 uu_flood=False, learn=False)
358 macs = self.mac_list(range(10, 20))
359 hosts6 = self.ip6_hosts(5, 1, macs)
360 self.add_del_arp_term_hosts(hosts6, is_add=1, is_ipv6=1)
361 self.verify_nd(src_host6, hosts6, hosts6)
362 del_macs = self.mac_list(range(10, 15))
363 deleted = self.ip6_hosts(5, 1, del_macs)
364 self.add_del_arp_term_hosts(deleted, is_add=0, is_ipv6=1)
365 self.verify_nd(src_host6, hosts6, hosts6 - deleted)
366 self.bd_add_del(1, is_add=0)
367
368 def test_l2bd_arp_term_08(self):
369 """ L2BD ND term - Add and update IP+mac, verify ND replies
370 """
371 src_host = self.ip6_host(50, 50, "00:00:11:22:33:44")
372 self.bd_add_del(1, is_add=1)
373 self.set_bd_flags(1, arp_term=True, flood=False,
374 uu_flood=False, learn=False)
375 macs1 = self.mac_list(range(10, 20))
376 hosts = self.ip6_hosts(5, 1, macs1)
377 self.add_del_arp_term_hosts(hosts, is_add=1, is_ipv6=1)
378 self.verify_nd(src_host, hosts, hosts)
379 macs2 = self.mac_list(range(20, 30))
380 updated = self.ip6_hosts(5, 1, macs2)
381 self.add_del_arp_term_hosts(updated, is_add=1, is_ipv6=1)
382 self.verify_nd(src_host, hosts, updated)
383 self.bd_add_del(1, is_add=0)
384
Eyal Bari20197482017-09-13 12:29:08 +0300385 def test_l2bd_arp_term_09(self):
386 """ L2BD arp term - send garps, verify arp event reports
387 """
388 self.vapi.want_ip4_arp_events()
389 self.bd_add_del(1, is_add=1)
390 self.set_bd_flags(1, arp_term=True, flood=False,
391 uu_flood=False, learn=False)
392 macs = self.mac_list(range(90, 95))
393 hosts = self.ip4_hosts(5, 1, macs)
394
395 garps = self.garp_reqs(hosts)
396 self.bd_swifs(1)[0].add_stream(garps)
397
398 self.pg_enable_capture(self.pg_interfaces)
399 self.pg_start()
400 evs = [self.vapi.wait_for_event(1, "ip4_arp_event")
401 for i in range(len(hosts))]
402 ev_hosts = self.arp_event_hosts(evs)
403 self.assertEqual(len(ev_hosts ^ hosts), 0)
404
405 def test_l2bd_arp_term_10(self):
406 """ L2BD arp term - send duplicate garps, verify suppression
407 """
408 macs = self.mac_list(range(70, 71))
409 hosts = self.ip4_hosts(6, 1, macs)
410
411 """ send the packet 5 times expect one event
412 """
413 garps = self.garp_reqs(hosts) * 5
414 self.bd_swifs(1)[0].add_stream(garps)
415
416 self.pg_enable_capture(self.pg_interfaces)
417 self.pg_start()
418 evs = [self.vapi.wait_for_event(1, "ip4_arp_event")
419 for i in range(len(hosts))]
420 ev_hosts = self.arp_event_hosts(evs)
421 self.assertEqual(len(ev_hosts ^ hosts), 0)
422
423 def test_l2bd_arp_term_11(self):
424 """ L2BD arp term - disable ip4 arp events,send garps, verify no events
425 """
426 self.vapi.want_ip4_arp_events(enable_disable=0)
427 macs = self.mac_list(range(90, 95))
428 hosts = self.ip4_hosts(5, 1, macs)
429
430 garps = self.garp_reqs(hosts)
431 self.bd_swifs(1)[0].add_stream(garps)
432
433 self.pg_enable_capture(self.pg_interfaces)
434 self.pg_start()
435 self.sleep(1)
436 self.assertEqual(len(self.vapi.collect_events()), 0)
437 self.bd_add_del(1, is_add=0)
438
Eyal Baric125ecc2017-09-20 11:29:17 +0300439 def test_l2bd_arp_term_12(self):
440 """ L2BD ND term - send NS packets verify reports
441 """
442 self.vapi.want_ip6_nd_events(address=inet_pton(AF_INET6, "::0"))
443 dst_host = self.ip6_host(50, 50, "00:00:11:22:33:44")
444 self.bd_add_del(1, is_add=1)
445 self.set_bd_flags(1, arp_term=True, flood=False,
446 uu_flood=False, learn=False)
447 macs = self.mac_list(range(10, 15))
448 hosts = self.ip6_hosts(5, 1, macs)
449 reqs = self.ns_reqs_dst(hosts, dst_host)
450 self.bd_swifs(1)[0].add_stream(reqs)
451
452 self.pg_enable_capture(self.pg_interfaces)
453 self.pg_start()
454 evs = [self.vapi.wait_for_event(2, "ip6_nd_event")
455 for i in range(len(hosts))]
456 ev_hosts = self.nd_event_hosts(evs)
457 self.assertEqual(len(ev_hosts ^ hosts), 0)
458
459 def test_l2bd_arp_term_13(self):
460 """ L2BD ND term - send duplicate ns, verify suppression
461 """
462 dst_host = self.ip6_host(50, 50, "00:00:11:22:33:44")
463 macs = self.mac_list(range(10, 11))
464 hosts = self.ip6_hosts(5, 1, macs)
465 reqs = self.ns_reqs_dst(hosts, dst_host) * 5
466 self.bd_swifs(1)[0].add_stream(reqs)
467
468 self.pg_enable_capture(self.pg_interfaces)
469 self.pg_start()
470 evs = [self.vapi.wait_for_event(2, "ip6_nd_event")
471 for i in range(len(hosts))]
472 ev_hosts = self.nd_event_hosts(evs)
473 self.assertEqual(len(ev_hosts ^ hosts), 0)
474
475 def test_l2bd_arp_term_14(self):
476 """ L2BD ND term - disable ip4 arp events,send ns, verify no events
477 """
478 self.vapi.want_ip6_nd_events(enable_disable=0,
479 address=inet_pton(AF_INET6, "::0"))
480 dst_host = self.ip6_host(50, 50, "00:00:11:22:33:44")
481 macs = self.mac_list(range(10, 15))
482 hosts = self.ip6_hosts(5, 1, macs)
483 reqs = self.ns_reqs_dst(hosts, dst_host)
484 self.bd_swifs(1)[0].add_stream(reqs)
485
486 self.pg_enable_capture(self.pg_interfaces)
487 self.pg_start()
488 self.sleep(1)
489 self.assertEqual(len(self.vapi.collect_events()), 0)
490 self.bd_add_del(1, is_add=0)
491
Eyal Baric86e5922017-07-02 18:33:16 +0300492
493if __name__ == '__main__':
494 unittest.main(testRunner=VppTestRunner)