1# Copyright (C) 2014 Xinguard, Inc.
2# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#    http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13# implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""
18Implementation of Bidirectional Forwarding Detection for IPv4 (Single Hop)
19
20This module provides a simple way to let Ryu act like a daemon for running
21IPv4 single hop BFD (RFC5881).
22
23Please note that:
24
25* Demand mode and echo function are not yet supported.
26* Mechanism on negotiating L2/L3 addresses for an established
27  session is not yet implemented.
28* The interoperability of authentication support is not tested.
29* Configuring a BFD session with too small interval may lead to
30  full of event queue and congestion of Openflow channels.
31  For deploying a low-latency configuration or with a large number
32  of BFD sessions, use standalone BFD daemon instead.
33"""
34
35
36import logging
37import time
38import random
39
40import six
41
42from ryu.base import app_manager
43from ryu.controller import event
44from ryu.controller import ofp_event
45from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER
46from ryu.controller.handler import set_ev_cls
47from ryu.exception import RyuException
48from ryu.ofproto.ether import ETH_TYPE_IP, ETH_TYPE_ARP
49from ryu.ofproto import ofproto_v1_3
50from ryu.ofproto import inet
51from ryu.lib import hub
52from ryu.lib.packet import packet
53from ryu.lib.packet import ethernet
54from ryu.lib.packet import ipv4
55from ryu.lib.packet import udp
56from ryu.lib.packet import bfd
57from ryu.lib.packet import arp
58from ryu.lib.packet.arp import ARP_REQUEST, ARP_REPLY
59
60LOG = logging.getLogger(__name__)
61
62UINT16_MAX = (1 << 16) - 1
63UINT32_MAX = (1 << 32) - 1
64
65# RFC5881 Section 8
66BFD_CONTROL_UDP_PORT = 3784
67BFD_ECHO_UDP_PORT = 3785
68
69
70class BFDSession(object):
71    """BFD Session class.
72
73    An instance maintains a BFD session.
74    """
75
76    def __init__(self, app, my_discr, dpid, ofport,
77                 src_mac, src_ip, src_port,
78                 dst_mac="FF:FF:FF:FF:FF:FF", dst_ip="255.255.255.255",
79                 detect_mult=3,
80                 desired_min_tx_interval=1000000,
81                 required_min_rx_interval=1000000,
82                 auth_type=0, auth_keys=None):
83        """
84        Initialize a BFD session.
85
86        __init__ takes the corresponding args in this order.
87
88        .. tabularcolumns:: |l|L|
89
90        ========================= ============================================
91        Argument                  Description
92        ========================= ============================================
93        app                       The instance of BFDLib.
94        my_discr                  My Discriminator.
95        dpid                      Datapath ID of the BFD interface.
96        ofport                    Openflow port number of the BFD interface.
97        src_mac                   Source MAC address of the BFD interface.
98        src_ip                    Source IPv4 address of the BFD interface.
99        dst_mac                   (Optional) Destination MAC address of the
100                                  BFD interface.
101        dst_ip                    (Optional) Destination IPv4 address of the
102                                  BFD interface.
103        detect_mult               (Optional) Detection time multiplier.
104        desired_min_tx_interval   (Optional) Desired Min TX Interval.
105                                  (in microseconds)
106        required_min_rx_interval  (Optional) Required Min RX Interval.
107                                  (in microseconds)
108        auth_type                 (Optional) Authentication type.
109        auth_keys                 (Optional) A dictionary of authentication
110                                  key chain which key is an integer of
111                                  *Auth Key ID* and value is a string of
112                                  *Password* or *Auth Key*.
113        ========================= ============================================
114
115        Example::
116
117            sess = BFDSession(app=self.bfdlib,
118                              my_discr=1,
119                              dpid=1,
120                              ofport=1,
121                              src_mac="01:23:45:67:89:AB",
122                              src_ip="192.168.1.1",
123                              dst_mac="12:34:56:78:9A:BC",
124                              dst_ip="192.168.1.2",
125                              detect_mult=3,
126                              desired_min_tx_interval=1000000,
127                              required_min_rx_interval=1000000,
128                              auth_type=bfd.BFD_AUTH_KEYED_SHA1,
129                              auth_keys={1: "secret key 1",
130                                         2: "secret key 2"})
131        """
132        auth_keys = auth_keys if auth_keys else {}
133        assert not (auth_type and len(auth_keys) == 0)
134
135        # RyuApp reference to BFDLib
136        self.app = app
137
138        # RFC5880 Section 6.8.1.
139        # BFD Internal Variables
140        self._session_state = bfd.BFD_STATE_DOWN
141        self._remote_session_state = bfd.BFD_STATE_DOWN
142        self._local_discr = my_discr
143        self._remote_discr = 0
144        self._local_diag = 0
145        self._desired_min_tx_interval = 1000000
146        self._required_min_rx_interval = required_min_rx_interval
147        self._remote_min_rx_interval = -1
148        # TODO: Demand mode is not yet supported.
149        self._demand_mode = 0
150        self._remote_demand_mode = 0
151        self._detect_mult = detect_mult
152        self._auth_type = auth_type
153        self._auth_keys = auth_keys
154
155        if self._auth_type in [bfd.BFD_AUTH_KEYED_MD5,
156                               bfd.BFD_AUTH_METICULOUS_KEYED_MD5,
157                               bfd.BFD_AUTH_KEYED_SHA1,
158                               bfd.BFD_AUTH_METICULOUS_KEYED_SHA1]:
159            self._rcv_auth_seq = 0
160            self._xmit_auth_seq = random.randint(0, UINT32_MAX)
161            self._auth_seq_known = 0
162
163        # BFD Runtime Variables
164        self._cfg_desired_min_tx_interval = desired_min_tx_interval
165        self._cfg_required_min_echo_rx_interval = 0
166        self._active_role = True
167        self._detect_time = 0
168        self._xmit_period = None
169        self._update_xmit_period()
170        self._is_polling = True
171        self._pending_final = False
172        # _enable_send indicates the switch of the periodic transmission of
173        # BFD Control packets.
174        self._enable_send = True
175        self._lock = None
176
177        # L2/L3/L4 Header fields
178        self.src_mac = src_mac
179        self.dst_mac = dst_mac
180        self.src_ip = src_ip
181        self.dst_ip = dst_ip
182        self.ipv4_id = random.randint(0, UINT16_MAX)
183        self.src_port = src_port
184        self.dst_port = BFD_CONTROL_UDP_PORT
185
186        if dst_mac == "FF:FF:FF:FF:FF:FF" or dst_ip == "255.255.255.255":
187            self._remote_addr_config = False
188        else:
189            self._remote_addr_config = True
190
191        # Switch and port associated to this BFD session.
192        self.dpid = dpid
193        self.datapath = None
194        self.ofport = ofport
195
196        # Spawn a periodic transmission loop for BFD Control packets.
197        hub.spawn(self._send_loop)
198
199        LOG.info("[BFD][%s][INIT] BFD Session initialized.",
200                 hex(self._local_discr))
201
202    @property
203    def my_discr(self):
204        """
205        Returns My Discriminator of the BFD session.
206        """
207        return self._local_discr
208
209    @property
210    def your_discr(self):
211        """
212        Returns Your Discriminator of the BFD session.
213        """
214        return self._remote_discr
215
216    def set_remote_addr(self, dst_mac, dst_ip):
217        """
218        Configure remote ethernet and IP addresses.
219        """
220        self.dst_mac = dst_mac
221        self.dst_ip = dst_ip
222
223        if not (dst_mac == "FF:FF:FF:FF:FF:FF" or dst_ip == "255.255.255.255"):
224            self._remote_addr_config = True
225
226        LOG.info("[BFD][%s][REMOTE] Remote address configured: %s, %s.",
227                 hex(self._local_discr), self.dst_ip, self.dst_mac)
228
229    def recv(self, bfd_pkt):
230        """
231        BFD packet receiver.
232        """
233        LOG.debug("[BFD][%s][RECV] BFD Control received: %s",
234                  hex(self._local_discr), six.binary_type(bfd_pkt))
235        self._remote_discr = bfd_pkt.my_discr
236        self._remote_state = bfd_pkt.state
237        self._remote_demand_mode = bfd_pkt.flags & bfd.BFD_FLAG_DEMAND
238
239        if self._remote_min_rx_interval != bfd_pkt.required_min_rx_interval:
240            self._remote_min_rx_interval = bfd_pkt.required_min_rx_interval
241            # Update transmit interval (RFC5880 Section 6.8.2.)
242            self._update_xmit_period()
243
244        # TODO: Echo function (RFC5880 Page 35)
245
246        if bfd_pkt.flags & bfd.BFD_FLAG_FINAL and self._is_polling:
247            self._is_polling = False
248
249        # Check and update the session state (RFC5880 Page 35)
250        if self._session_state == bfd.BFD_STATE_ADMIN_DOWN:
251            return
252
253        if bfd_pkt.state == bfd.BFD_STATE_ADMIN_DOWN:
254            if self._session_state != bfd.BFD_STATE_DOWN:
255                self._set_state(bfd.BFD_STATE_DOWN,
256                                bfd.BFD_DIAG_NEIG_SIG_SESS_DOWN)
257        else:
258            if self._session_state == bfd.BFD_STATE_DOWN:
259                if bfd_pkt.state == bfd.BFD_STATE_DOWN:
260                    self._set_state(bfd.BFD_STATE_INIT)
261                elif bfd_pkt.state == bfd.BFD_STATE_INIT:
262                    self._set_state(bfd.BFD_STATE_UP)
263
264            elif self._session_state == bfd.BFD_STATE_INIT:
265                if bfd_pkt.state in [bfd.BFD_STATE_INIT, bfd.BFD_STATE_UP]:
266                    self._set_state(bfd.BFD_STATE_UP)
267
268            else:
269                if bfd_pkt.state == bfd.BFD_STATE_DOWN:
270                    self._set_state(bfd.BFD_STATE_DOWN,
271                                    bfd.BFD_DIAG_NEIG_SIG_SESS_DOWN)
272
273        # TODO: Demand mode support.
274
275        if self._remote_demand_mode and \
276                self._session_state == bfd.BFD_STATE_UP and \
277                self._remote_session_state == bfd.BFD_STATE_UP:
278            self._enable_send = False
279
280        if not self._remote_demand_mode or \
281                self._session_state != bfd.BFD_STATE_UP or \
282                self._remote_session_state != bfd.BFD_STATE_UP:
283            if not self._enable_send:
284                self._enable_send = True
285                hub.spawn(self._send_loop)
286
287        # Update the detection time (RFC5880 Section 6.8.4.)
288        if self._detect_time == 0:
289            self._detect_time = bfd_pkt.desired_min_tx_interval * \
290                bfd_pkt.detect_mult / 1000000.0
291            # Start the timeout loop.
292            hub.spawn(self._recv_timeout_loop)
293
294        if bfd_pkt.flags & bfd.BFD_FLAG_POLL:
295            self._pending_final = True
296            self._detect_time = bfd_pkt.desired_min_tx_interval * \
297                bfd_pkt.detect_mult / 1000000.0
298
299        # Update the remote authentication sequence number.
300        if self._auth_type in [bfd.BFD_AUTH_KEYED_MD5,
301                               bfd.BFD_AUTH_METICULOUS_KEYED_MD5,
302                               bfd.BFD_AUTH_KEYED_SHA1,
303                               bfd.BFD_AUTH_METICULOUS_KEYED_SHA1]:
304            self._rcv_auth_seq = bfd_pkt.auth_cls.seq
305            self._auth_seq_known = 1
306
307        # Set the lock.
308        if self._lock is not None:
309            self._lock.set()
310
311    def _set_state(self, new_state, diag=None):
312        """
313        Set the state of the BFD session.
314        """
315        old_state = self._session_state
316
317        LOG.info("[BFD][%s][STATE] State changed from %s to %s.",
318                 hex(self._local_discr),
319                 bfd.BFD_STATE_NAME[old_state],
320                 bfd.BFD_STATE_NAME[new_state])
321        self._session_state = new_state
322
323        if new_state == bfd.BFD_STATE_DOWN:
324            if diag is not None:
325                self._local_diag = diag
326            self._desired_min_tx_interval = 1000000
327            self._is_polling = True
328            self._update_xmit_period()
329        elif new_state == bfd.BFD_STATE_UP:
330            self._desired_min_tx_interval = self._cfg_desired_min_tx_interval
331            self._is_polling = True
332            self._update_xmit_period()
333
334        self.app.send_event_to_observers(
335            EventBFDSessionStateChanged(self, old_state, new_state))
336
337    def _recv_timeout_loop(self):
338        """
339        A loop to check timeout of receiving remote BFD packet.
340        """
341        while self._detect_time:
342            last_wait = time.time()
343            self._lock = hub.Event()
344
345            self._lock.wait(timeout=self._detect_time)
346
347            if self._lock.is_set():
348                # Authentication variable check (RFC5880 Section 6.8.1.)
349                if getattr(self, "_auth_seq_known", 0):
350                    if last_wait > time.time() + 2 * self._detect_time:
351                        self._auth_seq_known = 0
352
353            else:
354                # Check Detection Time expiration (RFC5880 section 6.8.4.)
355                LOG.info("[BFD][%s][RECV] BFD Session timed out.",
356                         hex(self._local_discr))
357                if self._session_state not in [bfd.BFD_STATE_DOWN,
358                                               bfd.BFD_STATE_ADMIN_DOWN]:
359                    self._set_state(bfd.BFD_STATE_DOWN,
360                                    bfd.BFD_DIAG_CTRL_DETECT_TIME_EXPIRED)
361
362                # Authentication variable check (RFC5880 Section 6.8.1.)
363                if getattr(self, "_auth_seq_known", 0):
364                    self._auth_seq_known = 0
365
366    def _update_xmit_period(self):
367        """
368        Update transmission period of the BFD session.
369        """
370        # RFC5880 Section 6.8.7.
371        if self._desired_min_tx_interval > self._remote_min_rx_interval:
372            xmit_period = self._desired_min_tx_interval
373        else:
374            xmit_period = self._remote_min_rx_interval
375
376        # This updates the transmission period of BFD Control packets.
377        # (RFC5880 Section 6.8.2 & 6.8.3.)
378        if self._detect_mult == 1:
379            xmit_period *= random.randint(75, 90) / 100.0
380        else:
381            xmit_period *= random.randint(75, 100) / 100.0
382
383        self._xmit_period = xmit_period / 1000000.0
384        LOG.info("[BFD][%s][XMIT] Transmission period changed to %f",
385                 hex(self._local_discr), self._xmit_period)
386
387    def _send_loop(self):
388        """
389        A loop to proceed periodic BFD packet transmission.
390        """
391        while self._enable_send:
392            hub.sleep(self._xmit_period)
393
394            # Send BFD packet. (RFC5880 Section 6.8.7.)
395
396            if self._remote_discr == 0 and not self._active_role:
397                continue
398
399            if self._remote_min_rx_interval == 0:
400                continue
401
402            if self._remote_demand_mode and \
403                    self._session_state == bfd.BFD_STATE_UP and \
404                    self._remote_session_state == bfd.BFD_STATE_UP and \
405                    not self._is_polling:
406                continue
407
408            self._send()
409
410    def _send(self):
411        """
412        BFD packet sender.
413        """
414        # If the switch was not connected to controller, exit.
415        if self.datapath is None:
416            return
417
418        # BFD Flags Setup
419        flags = 0
420
421        if self._pending_final:
422            flags |= bfd.BFD_FLAG_FINAL
423            self._pending_final = False
424            self._is_polling = False
425
426        if self._is_polling:
427            flags |= bfd.BFD_FLAG_POLL
428
429        # Authentication Section
430        auth_cls = None
431        if self._auth_type:
432            auth_key_id = list(self._auth_keys.keys())[
433                random.randint(0, len(list(self._auth_keys.keys())) - 1)]
434            auth_key = self._auth_keys[auth_key_id]
435
436            if self._auth_type == bfd.BFD_AUTH_SIMPLE_PASS:
437                auth_cls = bfd.SimplePassword(auth_key_id=auth_key_id,
438                                              password=auth_key)
439
440            if self._auth_type in [bfd.BFD_AUTH_KEYED_MD5,
441                                   bfd.BFD_AUTH_METICULOUS_KEYED_MD5,
442                                   bfd.BFD_AUTH_KEYED_SHA1,
443                                   bfd.BFD_AUTH_METICULOUS_KEYED_SHA1]:
444                if self._auth_type in [bfd.BFD_AUTH_KEYED_MD5,
445                                       bfd.BFD_AUTH_KEYED_SHA1]:
446                    if random.randint(0, 1):
447                        self._xmit_auth_seq = \
448                            (self._xmit_auth_seq + 1) & UINT32_MAX
449                else:
450                    self._xmit_auth_seq = \
451                        (self._xmit_auth_seq + 1) & UINT32_MAX
452
453                auth_cls = bfd.bfd._auth_parsers[self._auth_type](
454                    auth_key_id=auth_key_id,
455                    seq=self._xmit_auth_seq,
456                    auth_key=auth_key)
457
458        if auth_cls is not None:
459            flags |= bfd.BFD_FLAG_AUTH_PRESENT
460
461        if self._demand_mode and \
462                self._session_state == bfd.BFD_STATE_UP and \
463                self._remote_session_state == bfd.BFD_STATE_UP:
464            flags |= bfd.BFD_FLAG_DEMAND
465
466        diag = self._local_diag
467        state = self._session_state
468        detect_mult = self._detect_mult
469        my_discr = self._local_discr
470        your_discr = self._remote_discr
471        desired_min_tx_interval = self._desired_min_tx_interval
472        required_min_rx_interval = self._required_min_rx_interval
473        required_min_echo_rx_interval = self._cfg_required_min_echo_rx_interval
474
475        # Prepare for Ethernet/IP/UDP header fields
476        src_mac = self.src_mac
477        dst_mac = self.dst_mac
478        src_ip = self.src_ip
479        dst_ip = self.dst_ip
480        self.ipv4_id = (self.ipv4_id + 1) & UINT16_MAX
481        ipv4_id = self.ipv4_id
482        src_port = self.src_port
483        dst_port = self.dst_port
484
485        # Construct BFD Control packet
486        data = BFDPacket.bfd_packet(
487            src_mac=src_mac, dst_mac=dst_mac,
488            src_ip=src_ip, dst_ip=dst_ip, ipv4_id=ipv4_id,
489            src_port=src_port, dst_port=dst_port,
490            diag=diag, state=state, flags=flags, detect_mult=detect_mult,
491            my_discr=my_discr, your_discr=your_discr,
492            desired_min_tx_interval=desired_min_tx_interval,
493            required_min_rx_interval=required_min_rx_interval,
494            required_min_echo_rx_interval=required_min_echo_rx_interval,
495            auth_cls=auth_cls)
496
497        # Prepare for a datapath
498        datapath = self.datapath
499        ofproto = datapath.ofproto
500        parser = datapath.ofproto_parser
501
502        actions = [parser.OFPActionOutput(self.ofport)]
503
504        out = parser.OFPPacketOut(datapath=datapath,
505                                  buffer_id=ofproto.OFP_NO_BUFFER,
506                                  in_port=ofproto.OFPP_CONTROLLER,
507                                  actions=actions,
508                                  data=data)
509
510        datapath.send_msg(out)
511        LOG.debug("[BFD][%s][SEND] BFD Control sent.", hex(self._local_discr))
512
513
514class BFDPacket(object):
515    """
516    BFDPacket class for parsing raw BFD packet, and generating BFD packet with
517    Ethernet, IPv4, and UDP headers.
518    """
519
520    class BFDUnknownFormat(RyuException):
521        message = '%(msg)s'
522
523    @staticmethod
524    def bfd_packet(src_mac, dst_mac, src_ip, dst_ip, ipv4_id,
525                   src_port, dst_port,
526                   diag=0, state=0, flags=0, detect_mult=0,
527                   my_discr=0, your_discr=0, desired_min_tx_interval=0,
528                   required_min_rx_interval=0,
529                   required_min_echo_rx_interval=0,
530                   auth_cls=None):
531        """
532        Generate BFD packet with Ethernet/IPv4/UDP encapsulated.
533        """
534        # Generate ethernet header first.
535        pkt = packet.Packet()
536        eth_pkt = ethernet.ethernet(dst_mac, src_mac, ETH_TYPE_IP)
537        pkt.add_protocol(eth_pkt)
538
539        # IPv4 encapsulation
540        # set ToS to 192 (Network control/CS6)
541        # set TTL to 255 (RFC5881 Section 5.)
542        ipv4_pkt = ipv4.ipv4(proto=inet.IPPROTO_UDP, src=src_ip, dst=dst_ip,
543                             tos=192, identification=ipv4_id, ttl=255)
544        pkt.add_protocol(ipv4_pkt)
545
546        # UDP encapsulation
547        udp_pkt = udp.udp(src_port=src_port, dst_port=dst_port)
548        pkt.add_protocol(udp_pkt)
549
550        # BFD payload
551        bfd_pkt = bfd.bfd(
552            ver=1, diag=diag, state=state, flags=flags,
553            detect_mult=detect_mult,
554            my_discr=my_discr, your_discr=your_discr,
555            desired_min_tx_interval=desired_min_tx_interval,
556            required_min_rx_interval=required_min_rx_interval,
557            required_min_echo_rx_interval=required_min_echo_rx_interval,
558            auth_cls=auth_cls)
559        pkt.add_protocol(bfd_pkt)
560
561        pkt.serialize()
562        return pkt.data
563
564    @staticmethod
565    def bfd_parse(data):
566        """
567        Parse raw packet and return BFD class from packet library.
568        """
569        pkt = packet.Packet(data)
570        i = iter(pkt)
571        eth_pkt = next(i)
572
573        assert isinstance(eth_pkt, ethernet.ethernet)
574
575        ipv4_pkt = next(i)
576        assert isinstance(ipv4_pkt, ipv4.ipv4)
577
578        udp_pkt = next(i)
579        assert isinstance(udp_pkt, udp.udp)
580
581        udp_payload = next(i)
582
583        return bfd.bfd.parser(udp_payload)[0]
584
585
586class ARPPacket(object):
587    """
588    ARPPacket class for parsing raw ARP packet, and generating ARP packet with
589    Ethernet header.
590    """
591
592    class ARPUnknownFormat(RyuException):
593        message = '%(msg)s'
594
595    @staticmethod
596    def arp_packet(opcode, src_mac, src_ip, dst_mac, dst_ip):
597        """
598        Generate ARP packet with ethernet encapsulated.
599        """
600        # Generate ethernet header first.
601        pkt = packet.Packet()
602        eth_pkt = ethernet.ethernet(dst_mac, src_mac, ETH_TYPE_ARP)
603        pkt.add_protocol(eth_pkt)
604
605        # Use IPv4 ARP wrapper from packet library directly.
606        arp_pkt = arp.arp_ip(opcode, src_mac, src_ip, dst_mac, dst_ip)
607        pkt.add_protocol(arp_pkt)
608
609        pkt.serialize()
610        return pkt.data
611
612    @staticmethod
613    def arp_parse(data):
614        """
615        Parse ARP packet, return ARP class from packet library.
616        """
617        # Iteratize pkt
618        pkt = packet.Packet(data)
619        i = iter(pkt)
620        eth_pkt = next(i)
621        # Ensure it's an ethernet frame.
622        assert isinstance(eth_pkt, ethernet.ethernet)
623
624        arp_pkt = next(i)
625        if not isinstance(arp_pkt, arp.arp):
626            raise ARPPacket.ARPUnknownFormat()
627
628        if arp_pkt.opcode not in (ARP_REQUEST, ARP_REPLY):
629            raise ARPPacket.ARPUnknownFormat(
630                msg='unsupported opcode %d' % arp_pkt.opcode)
631
632        if arp_pkt.proto != ETH_TYPE_IP:
633            raise ARPPacket.ARPUnknownFormat(
634                msg='unsupported arp ethtype 0x%04x' % arp_pkt.proto)
635
636        return arp_pkt
637
638
639class EventBFDSessionStateChanged(event.EventBase):
640    """
641    An event class that notifies the state change of a BFD session.
642    """
643
644    def __init__(self, session, old_state, new_state):
645        super(EventBFDSessionStateChanged, self).__init__()
646        self.session = session
647        self.old_state = old_state
648        self.new_state = new_state
649
650
651class BFDLib(app_manager.RyuApp):
652    """
653    BFD daemon library.
654
655    Add this library as a context in your app and use ``add_bfd_session``
656    function to establish a BFD session.
657
658    Example::
659
660        from ryu.base import app_manager
661        from ryu.controller.handler import set_ev_cls
662        from ryu.ofproto import ofproto_v1_3
663        from ryu.lib import bfdlib
664        from ryu.lib.packet import bfd
665
666        class Foo(app_manager.RyuApp):
667            OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
668
669            _CONTEXTS = {
670                'bfdlib': bfdlib.BFDLib
671            }
672
673            def __init__(self, *args, **kwargs):
674                super(Foo, self).__init__(*args, **kwargs)
675                self.bfdlib = kwargs['bfdlib']
676                self.my_discr = \
677                    self.bfdlib.add_bfd_session(dpid=1,
678                                                ofport=1,
679                                                src_mac="00:23:45:67:89:AB",
680                                                src_ip="192.168.1.1")
681
682            @set_ev_cls(bfdlib.EventBFDSessionStateChanged)
683            def bfd_state_handler(self, ev):
684                if ev.session.my_discr != self.my_discr:
685                    return
686
687                if ev.new_state == bfd.BFD_STATE_DOWN:
688                    print "BFD Session=%d is DOWN!" % ev.session.my_discr
689                elif ev.new_state == bfd.BFD_STATE_UP:
690                    print "BFD Session=%d is UP!" % ev.session.my_discr
691    """
692    OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
693
694    _EVENTS = [EventBFDSessionStateChanged]
695
696    def __init__(self, *args, **kwargs):
697        super(BFDLib, self).__init__(*args, **kwargs)
698
699        # BFD Session Dictionary
700        # key: My Discriminator
701        # value: BFDSession object
702        self.session = {}
703
704    def close(self):
705        pass
706
707    @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
708    def switch_features_handler(self, ev):
709        datapath = ev.msg.datapath
710        ofproto = datapath.ofproto
711        parser = datapath.ofproto_parser
712
713        # Update datapath object in BFD sessions
714        for s in self.session.values():
715            if s.dpid == datapath.id:
716                s.datapath = datapath
717
718        # Install default flows for capturing ARP & BFD packets.
719        match = parser.OFPMatch(eth_type=ETH_TYPE_ARP)
720        actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
721                                          ofproto.OFPCML_NO_BUFFER)]
722        self.add_flow(datapath, 0xFFFF, match, actions)
723
724        match = parser.OFPMatch(eth_type=ETH_TYPE_IP,
725                                ip_proto=inet.IPPROTO_UDP,
726                                udp_dst=3784)
727        actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
728                                          ofproto.OFPCML_NO_BUFFER)]
729        self.add_flow(datapath, 0xFFFF, match, actions)
730
731    def add_flow(self, datapath, priority, match, actions):
732        ofproto = datapath.ofproto
733        parser = datapath.ofproto_parser
734
735        inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS,
736                                             actions)]
737
738        mod = parser.OFPFlowMod(datapath=datapath, priority=priority,
739                                match=match, instructions=inst)
740        datapath.send_msg(mod)
741
742    # Packet-In Handler, only for BFD packets.
743    @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
744    def _packet_in_handler(self, ev):
745        msg = ev.msg
746        datapath = msg.datapath
747        ofproto = datapath.ofproto
748        parser = datapath.ofproto_parser
749        in_port = msg.match['in_port']
750
751        pkt = packet.Packet(msg.data)
752
753        # If there's someone asked for an IP address associated
754        # with a BFD session, generate an ARP reply for it.
755        if arp.arp in pkt:
756            arp_pkt = ARPPacket.arp_parse(msg.data)
757            if arp_pkt.opcode == ARP_REQUEST:
758                for s in self.session.values():
759                    if s.dpid == datapath.id and \
760                            s.ofport == in_port and \
761                            s.src_ip == arp_pkt.dst_ip:
762
763                        ans = ARPPacket.arp_packet(
764                            ARP_REPLY,
765                            s.src_mac, s.src_ip,
766                            arp_pkt.src_mac, arp_pkt.src_ip)
767
768                        actions = [parser.OFPActionOutput(in_port)]
769                        out = parser.OFPPacketOut(
770                            datapath=datapath,
771                            buffer_id=ofproto.OFP_NO_BUFFER,
772                            in_port=ofproto.OFPP_CONTROLLER,
773                            actions=actions, data=ans)
774
775                        datapath.send_msg(out)
776                        return
777            return
778
779        # Check whether it's BFD packet or not.
780        if ipv4.ipv4 not in pkt or udp.udp not in pkt:
781            return
782
783        udp_hdr = pkt.get_protocols(udp.udp)[0]
784        if udp_hdr.dst_port != BFD_CONTROL_UDP_PORT:
785            return
786
787        # Parse BFD packet here.
788        self.recv_bfd_pkt(datapath, in_port, msg.data)
789
790    def add_bfd_session(self, dpid, ofport, src_mac, src_ip,
791                        dst_mac="FF:FF:FF:FF:FF:FF", dst_ip="255.255.255.255",
792                        auth_type=0, auth_keys=None):
793        """
794        Establish a new BFD session and return My Discriminator of new session.
795
796        Configure the BFD session with the following arguments.
797
798        ================ ======================================================
799        Argument         Description
800        ================ ======================================================
801        dpid             Datapath ID of the BFD interface.
802        ofport           Openflow port number of the BFD interface.
803        src_mac          Source MAC address of the BFD interface.
804        src_ip           Source IPv4 address of the BFD interface.
805        dst_mac          (Optional) Destination MAC address of the BFD
806                         interface.
807        dst_ip           (Optional) Destination IPv4 address of the BFD
808                         interface.
809        auth_type        (Optional) Authentication type.
810        auth_keys        (Optional) A dictionary of authentication key chain
811                         which key is an integer of *Auth Key ID* and value
812                         is a string of *Password* or *Auth Key*.
813        ================ ======================================================
814
815        Example::
816
817            add_bfd_session(dpid=1,
818                            ofport=1,
819                            src_mac="01:23:45:67:89:AB",
820                            src_ip="192.168.1.1",
821                            dst_mac="12:34:56:78:9A:BC",
822                            dst_ip="192.168.1.2",
823                            auth_type=bfd.BFD_AUTH_KEYED_SHA1,
824                            auth_keys={1: "secret key 1",
825                                       2: "secret key 2"})
826        """
827        auth_keys = auth_keys if auth_keys else {}
828        # Generate a unique discriminator
829        while True:
830            # Generate My Discriminator
831            my_discr = random.randint(1, UINT32_MAX)
832
833            # Generate an UDP destination port according to RFC5881 Section 4.
834            src_port = random.randint(49152, 65535)
835
836            # Ensure generated discriminator and UDP port are unique.
837            if my_discr in self.session:
838                continue
839
840            unique_flag = True
841
842            for s in self.session.values():
843                if s.your_discr == my_discr or s.src_port == src_port:
844                    unique_flag = False
845                    break
846
847            if unique_flag:
848                break
849
850        sess = BFDSession(app=self, my_discr=my_discr,
851                          dpid=dpid, ofport=ofport,
852                          src_mac=src_mac, src_ip=src_ip, src_port=src_port,
853                          dst_mac=dst_mac, dst_ip=dst_ip,
854                          auth_type=auth_type, auth_keys=auth_keys)
855
856        self.session[my_discr] = sess
857
858        return my_discr
859
860    def recv_bfd_pkt(self, datapath, in_port, data):
861        pkt = packet.Packet(data)
862        eth = pkt.get_protocols(ethernet.ethernet)[0]
863
864        if eth.ethertype != ETH_TYPE_IP:
865            return
866
867        ip_pkt = pkt.get_protocols(ipv4.ipv4)[0]
868
869        # Discard it if TTL != 255 for single hop bfd. (RFC5881 Section 5.)
870        if ip_pkt.ttl != 255:
871            return
872
873        # Parse BFD packet here.
874        bfd_pkt = BFDPacket.bfd_parse(data)
875
876        if not isinstance(bfd_pkt, bfd.bfd):
877            return
878
879        # BFD sanity checks
880        # RFC 5880 Section 6.8.6.
881        if bfd_pkt.ver != 1:
882            return
883
884        if bfd_pkt.flags & bfd.BFD_FLAG_AUTH_PRESENT:
885            if bfd_pkt.length < 26:
886                return
887        else:
888            if bfd_pkt.length < 24:
889                return
890
891        if bfd_pkt.detect_mult == 0:
892            return
893
894        if bfd_pkt.flags & bfd.BFD_FLAG_MULTIPOINT:
895            return
896
897        if bfd_pkt.my_discr == 0:
898            return
899
900        if bfd_pkt.your_discr != 0 and bfd_pkt.your_discr not in self.session:
901            return
902
903        if bfd_pkt.your_discr == 0 and \
904                bfd_pkt.state not in [bfd.BFD_STATE_ADMIN_DOWN,
905                                      bfd.BFD_STATE_DOWN]:
906            return
907
908        sess_my_discr = None
909
910        if bfd_pkt.your_discr == 0:
911            # Select session (Page 34)
912            for s in self.session.values():
913                if s.dpid == datapath.id and s.ofport == in_port:
914                    sess_my_discr = s.my_discr
915                    break
916
917            # BFD Session not found.
918            if sess_my_discr is None:
919                return
920        else:
921            sess_my_discr = bfd_pkt.your_discr
922
923        sess = self.session[sess_my_discr]
924
925        if bfd_pkt.flags & bfd.BFD_FLAG_AUTH_PRESENT and sess._auth_type == 0:
926            return
927
928        if bfd_pkt.flags & bfd.BFD_FLAG_AUTH_PRESENT == 0 and \
929                sess._auth_type != 0:
930            return
931
932        # Authenticate the session (Section 6.7.)
933        if bfd_pkt.flags & bfd.BFD_FLAG_AUTH_PRESENT:
934            if sess._auth_type == 0:
935                return
936
937            if bfd_pkt.auth_cls.auth_type != sess._auth_type:
938                return
939
940            # Check authentication sequence number to defend replay attack.
941            if sess._auth_type in [bfd.BFD_AUTH_KEYED_MD5,
942                                   bfd.BFD_AUTH_METICULOUS_KEYED_MD5,
943                                   bfd.BFD_AUTH_KEYED_SHA1,
944                                   bfd.BFD_AUTH_METICULOUS_KEYED_SHA1]:
945                if sess._auth_seq_known:
946                    if bfd_pkt.auth_cls.seq < sess._rcv_auth_seq:
947                        return
948
949                    if sess._auth_type in [bfd.BFD_AUTH_METICULOUS_KEYED_MD5,
950                                           bfd.BFD_AUTH_METICULOUS_KEYED_SHA1]:
951                        if bfd_pkt.auth_cls.seq <= sess._rcv_auth_seq:
952                            return
953
954                    if bfd_pkt.auth_cls.seq > sess._rcv_auth_seq \
955                            + 3 * sess._detect_mult:
956                        return
957
958            if not bfd_pkt.authenticate(sess._auth_keys):
959                LOG.debug("[BFD][%s][AUTH] BFD Control authentication failed.",
960                          hex(sess._local_discr))
961                return
962
963        # Sanity check passed, proceed.
964        if sess is not None:
965            # Check whether L2/L3 addresses were configured or not.
966            # TODO: L2/L3 addresses negotiation for an established session.
967            if not sess._remote_addr_config:
968                sess.set_remote_addr(eth.src, ip_pkt.src)
969            # Proceed to session update.
970            sess.recv(bfd_pkt)
971