452a18049883d07f01028056091ed3c0a0ebab71
[vpp.git] / test / bfd.py
1 """ BFD protocol implementation """
2
3 from random import randint
4 from socket import AF_INET, AF_INET6, inet_pton
5 from scapy.all import bind_layers
6 from scapy.layers.inet import UDP
7 from scapy.packet import Packet
8 from scapy.fields import BitField, BitEnumField, XByteField, FlagsField,\
9     ConditionalField, StrField
10 from vpp_object import VppObject
11 from util import NumericConstant
12
13
14 class BFDDiagCode(NumericConstant):
15     """ BFD Diagnostic Code """
16     no_diagnostic = 0
17     control_detection_time_expired = 1
18     echo_function_failed = 2
19     neighbor_signaled_session_down = 3
20     forwarding_plane_reset = 4
21     path_down = 5
22     concatenated_path_down = 6
23     administratively_down = 7
24     reverse_concatenated_path_down = 8
25
26     desc_dict = {
27         no_diagnostic: "No diagnostic",
28         control_detection_time_expired: "Control Detection Time Expired",
29         echo_function_failed: "Echo Function Failed",
30         neighbor_signaled_session_down: "Neighbor Signaled Session Down",
31         forwarding_plane_reset: "Forwarding Plane Reset",
32         path_down: "Path Down",
33         concatenated_path_down: "Concatenated Path Down",
34         administratively_down: "Administratively Down",
35         reverse_concatenated_path_down: "Reverse Concatenated Path Down",
36     }
37
38     def __init__(self, value):
39         NumericConstant.__init__(self, value)
40
41
42 class BFDState(NumericConstant):
43     """ BFD State """
44     admin_down = 0
45     down = 1
46     init = 2
47     up = 3
48
49     desc_dict = {
50         admin_down: "AdminDown",
51         down: "Down",
52         init: "Init",
53         up: "Up",
54     }
55
56     def __init__(self, value):
57         NumericConstant.__init__(self, value)
58
59
60 class BFDAuthType(NumericConstant):
61     """ BFD Authentication Type """
62     no_auth = 0
63     simple_pwd = 1
64     keyed_md5 = 2
65     meticulous_keyed_md5 = 3
66     keyed_sha1 = 4
67     meticulous_keyed_sha1 = 5
68
69     desc_dict = {
70         no_auth: "No authentication",
71         simple_pwd: "Simple Password",
72         keyed_md5: "Keyed MD5",
73         meticulous_keyed_md5: "Meticulous Keyed MD5",
74         keyed_sha1: "Keyed SHA1",
75         meticulous_keyed_sha1: "Meticulous Keyed SHA1",
76     }
77
78     def __init__(self, value):
79         NumericConstant.__init__(self, value)
80
81
82 def bfd_is_auth_used(pkt):
83     """ is packet authenticated? """
84     return "A" in pkt.sprintf("%BFD.flags%")
85
86
87 def bfd_is_simple_pwd_used(pkt):
88     """ is simple password authentication used? """
89     return bfd_is_auth_used(pkt) and pkt.auth_type == BFDAuthType.simple_pwd
90
91
92 def bfd_is_sha1_used(pkt):
93     """ is sha1 authentication used? """
94     return bfd_is_auth_used(pkt) and pkt.auth_type in \
95         (BFDAuthType.keyed_sha1, BFDAuthType.meticulous_keyed_sha1)
96
97
98 def bfd_is_md5_used(pkt):
99     """ is md5 authentication used? """
100     return bfd_is_auth_used(pkt) and pkt.auth_type in \
101         (BFDAuthType.keyed_md5, BFDAuthType.meticulous_keyed_md5)
102
103
104 def bfd_is_md5_or_sha1_used(pkt):
105     """ is md5 or sha1 used? """
106     return bfd_is_md5_used(pkt) or bfd_is_sha1_used(pkt)
107
108
109 class BFD(Packet):
110     """ BFD protocol layer for scapy """
111
112     udp_dport = 3784  #: BFD destination port per RFC 5881
113     udp_dport_echo = 3785  # : BFD destination port for ECHO per RFC 5881
114     udp_sport_min = 49152  #: BFD source port min value per RFC 5881
115     udp_sport_max = 65535  #: BFD source port max value per RFC 5881
116     bfd_pkt_len = 24  # : length of BFD pkt without authentication section
117     sha1_auth_len = 28  # : length of authentication section if SHA1 used
118
119     name = "BFD"
120
121     fields_desc = [
122         BitField("version", 1, 3),
123         BitEnumField("diag", 0, 5, BFDDiagCode.desc_dict),
124         BitEnumField("state", 0, 2, BFDState.desc_dict),
125         FlagsField("flags", 0, 6, ['M', 'D', 'A', 'C', 'F', 'P']),
126         XByteField("detect_mult", 0),
127         BitField("length", bfd_pkt_len, 8),
128         BitField("my_discriminator", 0, 32),
129         BitField("your_discriminator", 0, 32),
130         BitField("desired_min_tx_interval", 0, 32),
131         BitField("required_min_rx_interval", 0, 32),
132         BitField("required_min_echo_rx_interval", 0, 32),
133         ConditionalField(
134             BitEnumField("auth_type", 0, 8, BFDAuthType.desc_dict),
135             bfd_is_auth_used),
136         ConditionalField(BitField("auth_len", 0, 8), bfd_is_auth_used),
137         ConditionalField(BitField("auth_key_id", 0, 8), bfd_is_auth_used),
138         ConditionalField(BitField("auth_reserved", 0, 8),
139                          bfd_is_md5_or_sha1_used),
140         ConditionalField(
141             BitField("auth_seq_num", 0, 32), bfd_is_md5_or_sha1_used),
142         ConditionalField(StrField("auth_key_hash", "0" * 16), bfd_is_md5_used),
143         ConditionalField(
144             StrField("auth_key_hash", "0" * 20), bfd_is_sha1_used),
145     ]
146
147     def mysummary(self):
148         return self.sprintf("BFD(my_disc=%BFD.my_discriminator%,"
149                             "your_disc=%BFD.your_discriminator%)")
150
151 # glue the BFD packet class to scapy parser
152 bind_layers(UDP, BFD, dport=BFD.udp_dport)
153
154
155 class BFD_vpp_echo(Packet):
156     """ BFD echo packet as used by VPP (non-rfc, as rfc doesn't define one) """
157
158     udp_dport = 3785  #: BFD echo destination port per RFC 5881
159     name = "BFD_VPP_ECHO"
160
161     fields_desc = [
162         BitField("discriminator", 0, 32),
163         BitField("expire_time_clocks", 0, 64),
164         BitField("checksum", 0, 64)
165     ]
166
167     def mysummary(self):
168         return self.sprintf(
169             "BFD_VPP_ECHO(disc=%BFD_VPP_ECHO.discriminator%,"
170             "expire_time_clocks=%BFD_VPP_ECHO.expire_time_clocks%)")
171
172 # glue the BFD echo packet class to scapy parser
173 bind_layers(UDP, BFD_vpp_echo, dport=BFD_vpp_echo.udp_dport)
174
175
176 class VppBFDAuthKey(VppObject):
177     """ Represents BFD authentication key in VPP """
178
179     def __init__(self, test, conf_key_id, auth_type, key):
180         self._test = test
181         self._key = key
182         self._auth_type = auth_type
183         test.assertIn(auth_type, BFDAuthType.desc_dict)
184         self._conf_key_id = conf_key_id
185
186     @property
187     def test(self):
188         """ Test which created this key """
189         return self._test
190
191     @property
192     def auth_type(self):
193         """ Authentication type for this key """
194         return self._auth_type
195
196     @property
197     def key(self):
198         """ key data """
199         return self._key
200
201     @key.setter
202     def key(self, value):
203         self._key = value
204
205     @property
206     def conf_key_id(self):
207         """ configuration key ID """
208         return self._conf_key_id
209
210     def add_vpp_config(self):
211         self.test.vapi.bfd_auth_set_key(
212             self._conf_key_id, self._auth_type, self._key)
213         self._test.registry.register(self, self.test.logger)
214
215     def get_bfd_auth_keys_dump_entry(self):
216         """ get the entry in the auth keys dump corresponding to this key """
217         result = self.test.vapi.bfd_auth_keys_dump()
218         for k in result:
219             if k.conf_key_id == self._conf_key_id:
220                 return k
221         return None
222
223     def query_vpp_config(self):
224         return self.get_bfd_auth_keys_dump_entry() is not None
225
226     def remove_vpp_config(self):
227         self.test.vapi.bfd_auth_del_key(self._conf_key_id)
228
229     def object_id(self):
230         return "bfd-auth-key-%s" % self._conf_key_id
231
232     def __str__(self):
233         return self.object_id()
234
235
236 class VppBFDUDPSession(VppObject):
237     """ Represents BFD UDP session in VPP """
238
239     def __init__(self, test, interface, peer_addr, local_addr=None, af=AF_INET,
240                  desired_min_tx=300000, required_min_rx=300000, detect_mult=3,
241                  sha1_key=None, bfd_key_id=None):
242         self._test = test
243         self._interface = interface
244         self._af = af
245         self._local_addr = local_addr
246         if local_addr is not None:
247             self._local_addr_n = inet_pton(af, local_addr)
248         else:
249             self._local_addr_n = None
250         self._peer_addr = peer_addr
251         self._peer_addr_n = inet_pton(af, peer_addr)
252         self._desired_min_tx = desired_min_tx
253         self._required_min_rx = required_min_rx
254         self._detect_mult = detect_mult
255         self._sha1_key = sha1_key
256         if bfd_key_id is not None:
257             self._bfd_key_id = bfd_key_id
258         else:
259             self._bfd_key_id = randint(0, 255)
260
261     @property
262     def test(self):
263         """ Test which created this session """
264         return self._test
265
266     @property
267     def interface(self):
268         """ Interface on which this session lives """
269         return self._interface
270
271     @property
272     def af(self):
273         """ Address family - AF_INET or AF_INET6 """
274         return self._af
275
276     @property
277     def local_addr(self):
278         """ BFD session local address (VPP address) """
279         if self._local_addr is None:
280             if self.af == AF_INET:
281                 return self._interface.local_ip4
282             elif self.af == AF_INET6:
283                 return self._interface.local_ip6
284             else:
285                 raise Exception("Unexpected af '%s'" % self.af)
286         return self._local_addr
287
288     @property
289     def local_addr_n(self):
290         """ BFD session local address (VPP address) - raw, suitable for API """
291         if self._local_addr is None:
292             if self.af == AF_INET:
293                 return self._interface.local_ip4n
294             elif self.af == AF_INET6:
295                 return self._interface.local_ip6n
296             else:
297                 raise Exception("Unexpected af '%s'" % self.af)
298         return self._local_addr_n
299
300     @property
301     def peer_addr(self):
302         """ BFD session peer address """
303         return self._peer_addr
304
305     @property
306     def peer_addr_n(self):
307         """ BFD session peer address - raw, suitable for API """
308         return self._peer_addr_n
309
310     def get_bfd_udp_session_dump_entry(self):
311         """ get the namedtuple entry from bfd udp session dump """
312         result = self.test.vapi.bfd_udp_session_dump()
313         for s in result:
314             self.test.logger.debug("session entry: %s" % str(s))
315             if s.sw_if_index == self.interface.sw_if_index:
316                 if self.af == AF_INET \
317                         and s.is_ipv6 == 0 \
318                         and self.interface.local_ip4n == s.local_addr[:4] \
319                         and self.interface.remote_ip4n == s.peer_addr[:4]:
320                     return s
321                 if self.af == AF_INET6 \
322                         and s.is_ipv6 == 1 \
323                         and self.interface.local_ip6n == s.local_addr \
324                         and self.interface.remote_ip6n == s.peer_addr:
325                     return s
326         return None
327
328     @property
329     def state(self):
330         """ BFD session state """
331         session = self.get_bfd_udp_session_dump_entry()
332         if session is None:
333             raise Exception("Could not find BFD session in VPP response")
334         return session.state
335
336     @property
337     def desired_min_tx(self):
338         """ desired minimum tx interval """
339         return self._desired_min_tx
340
341     @property
342     def required_min_rx(self):
343         """ required minimum rx interval """
344         return self._required_min_rx
345
346     @property
347     def detect_mult(self):
348         """ detect multiplier """
349         return self._detect_mult
350
351     @property
352     def sha1_key(self):
353         """ sha1 key """
354         return self._sha1_key
355
356     @property
357     def bfd_key_id(self):
358         """ bfd key id in use """
359         return self._bfd_key_id
360
361     def activate_auth(self, key, bfd_key_id=None, delayed=False):
362         """ activate authentication for this session """
363         self._bfd_key_id = bfd_key_id if bfd_key_id else randint(0, 255)
364         self._sha1_key = key
365         is_ipv6 = 1 if AF_INET6 == self.af else 0
366         conf_key_id = self._sha1_key.conf_key_id
367         is_delayed = 1 if delayed else 0
368         self.test.vapi.bfd_udp_auth_activate(self._interface.sw_if_index,
369                                              self.local_addr_n,
370                                              self.peer_addr_n,
371                                              is_ipv6=is_ipv6,
372                                              bfd_key_id=self._bfd_key_id,
373                                              conf_key_id=conf_key_id,
374                                              is_delayed=is_delayed)
375
376     def deactivate_auth(self, delayed=False):
377         """ deactivate authentication """
378         self._bfd_key_id = None
379         self._sha1_key = None
380         is_delayed = 1 if delayed else 0
381         is_ipv6 = 1 if AF_INET6 == self.af else 0
382         self.test.vapi.bfd_udp_auth_deactivate(self._interface.sw_if_index,
383                                                self.local_addr_n,
384                                                self.peer_addr_n,
385                                                is_ipv6=is_ipv6,
386                                                is_delayed=is_delayed)
387
388     def modify_parameters(self,
389                           detect_mult=None,
390                           desired_min_tx=None,
391                           required_min_rx=None):
392         """ modify session parameters """
393         if detect_mult:
394             self._detect_mult = detect_mult
395         if desired_min_tx:
396             self._desired_min_tx = desired_min_tx
397         if required_min_rx:
398             self._required_min_rx = required_min_rx
399         is_ipv6 = 1 if AF_INET6 == self.af else 0
400         self.test.vapi.bfd_udp_mod(self._interface.sw_if_index,
401                                    self.desired_min_tx,
402                                    self.required_min_rx,
403                                    self.detect_mult,
404                                    self.local_addr_n,
405                                    self.peer_addr_n,
406                                    is_ipv6=is_ipv6)
407
408     def add_vpp_config(self):
409         is_ipv6 = 1 if AF_INET6 == self.af else 0
410         bfd_key_id = self._bfd_key_id if self._sha1_key else None
411         conf_key_id = self._sha1_key.conf_key_id if self._sha1_key else None
412         self.test.vapi.bfd_udp_add(self._interface.sw_if_index,
413                                    self.desired_min_tx,
414                                    self.required_min_rx,
415                                    self.detect_mult,
416                                    self.local_addr_n,
417                                    self.peer_addr_n,
418                                    is_ipv6=is_ipv6,
419                                    bfd_key_id=bfd_key_id,
420                                    conf_key_id=conf_key_id)
421         self._test.registry.register(self, self.test.logger)
422
423     def query_vpp_config(self):
424         session = self.get_bfd_udp_session_dump_entry()
425         return session is not None
426
427     def remove_vpp_config(self):
428         is_ipv6 = 1 if AF_INET6 == self._af else 0
429         self.test.vapi.bfd_udp_del(self._interface.sw_if_index,
430                                    self.local_addr_n,
431                                    self.peer_addr_n,
432                                    is_ipv6=is_ipv6)
433
434     def object_id(self):
435         return "bfd-udp-%s-%s-%s-%s" % (self._interface.sw_if_index,
436                                         self.local_addr,
437                                         self.peer_addr,
438                                         self.af)
439
440     def __str__(self):
441         return self.object_id()
442
443     def admin_up(self):
444         """ set bfd session admin-up """
445         is_ipv6 = 1 if AF_INET6 == self._af else 0
446         self.test.vapi.bfd_udp_session_set_flags(1,
447                                                  self._interface.sw_if_index,
448                                                  self.local_addr_n,
449                                                  self.peer_addr_n,
450                                                  is_ipv6=is_ipv6)
451
452     def admin_down(self):
453         """ set bfd session admin-down """
454         is_ipv6 = 1 if AF_INET6 == self._af else 0
455         self.test.vapi.bfd_udp_session_set_flags(0,
456                                                  self._interface.sw_if_index,
457                                                  self.local_addr_n,
458                                                  self.peer_addr_n,
459                                                  is_ipv6=is_ipv6)