# Copyright (C) 2014 Xinguard, Inc.
# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Implementation of Bidirectional Forwarding Detection for IPv4 (Single Hop)

This module provides a simple way to let OSKen act like a daemon for running
IPv4 single hop BFD (RFC5881).

Please note that:

* Demand mode and echo function are not yet supported.
* Mechanism on negotiating L2/L3 addresses for an established
  session is not yet implemented.
* The interoperability of authentication support is not tested.
* Configuring a BFD session with too small interval may lead to
  full of event queue and congestion of Openflow channels.
  For deploying a low-latency configuration or with a large number
  of BFD sessions, use standalone BFD daemon instead.
"""


import logging
import time
import random

import six

from os_ken.base import app_manager
from os_ken.controller import event
from os_ken.controller import ofp_event
from os_ken.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER
from os_ken.controller.handler import set_ev_cls
from os_ken.exception import OSKenException
from os_ken.ofproto.ether import ETH_TYPE_IP, ETH_TYPE_ARP
from os_ken.ofproto import ofproto_v1_3
from os_ken.ofproto import inet
from os_ken.lib import hub
from os_ken.lib.packet import packet
from os_ken.lib.packet import ethernet
from os_ken.lib.packet import ipv4
from os_ken.lib.packet import udp
from os_ken.lib.packet import bfd
from os_ken.lib.packet import arp
from os_ken.lib.packet.arp import ARP_REQUEST, ARP_REPLY

LOG = logging.getLogger(__name__)

UINT16_MAX = (1 << 16) - 1
UINT32_MAX = (1 << 32) - 1

# RFC5881 Section 8
BFD_CONTROL_UDP_PORT = 3784
BFD_ECHO_UDP_PORT = 3785


class BFDSession(object):
    """BFD Session class.

    An instance maintains a BFD session.
    """

    def __init__(self, app, my_discr, dpid, ofport,
                 src_mac, src_ip, src_port,
                 dst_mac="FF:FF:FF:FF:FF:FF", dst_ip="255.255.255.255",
                 detect_mult=3,
                 desired_min_tx_interval=1000000,
                 required_min_rx_interval=1000000,
                 auth_type=0, auth_keys=None):
        """
        Initialize a BFD session.

        __init__ takes the corresponding args in this order.

        .. tabularcolumns:: |l|L|

        ========================= ============================================
        Argument                  Description
        ========================= ============================================
        app                       The instance of BFDLib.
        my_discr                  My Discriminator.
        dpid                      Datapath ID of the BFD interface.
        ofport                    Openflow port number of the BFD interface.
        src_mac                   Source MAC address of the BFD interface.
        src_ip                    Source IPv4 address of the BFD interface.
        dst_mac                   (Optional) Destination MAC address of the
                                  BFD interface.
        dst_ip                    (Optional) Destination IPv4 address of the
                                  BFD interface.
        detect_mult               (Optional) Detection time multiplier.
        desired_min_tx_interval   (Optional) Desired Min TX Interval.
                                  (in microseconds)
        required_min_rx_interval  (Optional) Required Min RX Interval.
                                  (in microseconds)
        auth_type                 (Optional) Authentication type.
        auth_keys                 (Optional) A dictionary of authentication
                                  key chain which key is an integer of
                                  *Auth Key ID* and value is a string of
                                  *Password* or *Auth Key*.
        ========================= ============================================

        Example::

            sess = BFDSession(app=self.bfdlib,
                              my_discr=1,
                              dpid=1,
                              ofport=1,
                              src_mac="01:23:45:67:89:AB",
                              src_ip="192.168.1.1",
                              dst_mac="12:34:56:78:9A:BC",
                              dst_ip="192.168.1.2",
                              detect_mult=3,
                              desired_min_tx_interval=1000000,
                              required_min_rx_interval=1000000,
                              auth_type=bfd.BFD_AUTH_KEYED_SHA1,
                              auth_keys={1: "secret key 1",
                                         2: "secret key 2"})
        """
        auth_keys = auth_keys if auth_keys else {}
        assert not (auth_type and len(auth_keys) == 0)

        # OSKenApp reference to BFDLib
        self.app = app

        # RFC5880 Section 6.8.1.
        # BFD Internal Variables
        self._session_state = bfd.BFD_STATE_DOWN
        self._remote_session_state = bfd.BFD_STATE_DOWN
        self._local_discr = my_discr
        self._remote_discr = 0
        self._local_diag = 0
        self._desired_min_tx_interval = 1000000
        self._required_min_rx_interval = required_min_rx_interval
        self._remote_min_rx_interval = -1
        # TODO: Demand mode is not yet supported.
        self._demand_mode = 0
        self._remote_demand_mode = 0
        self._detect_mult = detect_mult
        self._auth_type = auth_type
        self._auth_keys = auth_keys

        if self._auth_type in [bfd.BFD_AUTH_KEYED_MD5,
                               bfd.BFD_AUTH_METICULOUS_KEYED_MD5,
                               bfd.BFD_AUTH_KEYED_SHA1,
                               bfd.BFD_AUTH_METICULOUS_KEYED_SHA1]:
            self._rcv_auth_seq = 0
            self._xmit_auth_seq = random.randint(0, UINT32_MAX)
            self._auth_seq_known = 0

        # BFD Runtime Variables
        self._cfg_desired_min_tx_interval = desired_min_tx_interval
        self._cfg_required_min_echo_rx_interval = 0
        self._active_role = True
        self._detect_time = 0
        self._xmit_period = None
        self._update_xmit_period()
        self._is_polling = True
        self._pending_final = False
        # _enable_send indicates the switch of the periodic transmission of
        # BFD Control packets.
        self._enable_send = True
        self._lock = None

        # L2/L3/L4 Header fields
        self.src_mac = src_mac
        self.dst_mac = dst_mac
        self.src_ip = src_ip
        self.dst_ip = dst_ip
        self.ipv4_id = random.randint(0, UINT16_MAX)
        self.src_port = src_port
        self.dst_port = BFD_CONTROL_UDP_PORT

        if dst_mac == "FF:FF:FF:FF:FF:FF" or dst_ip == "255.255.255.255":
            self._remote_addr_config = False
        else:
            self._remote_addr_config = True

        # Switch and port associated to this BFD session.
        self.dpid = dpid
        self.datapath = None
        self.ofport = ofport

        # Spawn a periodic transmission loop for BFD Control packets.
        hub.spawn(self._send_loop)

        LOG.info("[BFD][%s][INIT] BFD Session initialized.",
                 hex(self._local_discr))

    @property
    def my_discr(self):
        """
        Returns My Discriminator of the BFD session.
        """
        return self._local_discr

    @property
    def your_discr(self):
        """
        Returns Your Discriminator of the BFD session.
        """
        return self._remote_discr

    def set_remote_addr(self, dst_mac, dst_ip):
        """
        Configure remote ethernet and IP addresses.
        """
        self.dst_mac = dst_mac
        self.dst_ip = dst_ip

        if not (dst_mac == "FF:FF:FF:FF:FF:FF" or dst_ip == "255.255.255.255"):
            self._remote_addr_config = True

        LOG.info("[BFD][%s][REMOTE] Remote address configured: %s, %s.",
                 hex(self._local_discr), self.dst_ip, self.dst_mac)

    def recv(self, bfd_pkt):
        """
        BFD packet receiver.
        """
        LOG.debug("[BFD][%s][RECV] BFD Control received: %s",
                  hex(self._local_discr), six.binary_type(bfd_pkt))
        self._remote_discr = bfd_pkt.my_discr
        self._remote_state = bfd_pkt.state
        self._remote_demand_mode = bfd_pkt.flags & bfd.BFD_FLAG_DEMAND

        if self._remote_min_rx_interval != bfd_pkt.required_min_rx_interval:
            self._remote_min_rx_interval = bfd_pkt.required_min_rx_interval
            # Update transmit interval (RFC5880 Section 6.8.2.)
            self._update_xmit_period()

        # TODO: Echo function (RFC5880 Page 35)

        if bfd_pkt.flags & bfd.BFD_FLAG_FINAL and self._is_polling:
            self._is_polling = False

        # Check and update the session state (RFC5880 Page 35)
        if self._session_state == bfd.BFD_STATE_ADMIN_DOWN:
            return

        if bfd_pkt.state == bfd.BFD_STATE_ADMIN_DOWN:
            if self._session_state != bfd.BFD_STATE_DOWN:
                self._set_state(bfd.BFD_STATE_DOWN,
                                bfd.BFD_DIAG_NEIG_SIG_SESS_DOWN)
        else:
            if self._session_state == bfd.BFD_STATE_DOWN:
                if bfd_pkt.state == bfd.BFD_STATE_DOWN:
                    self._set_state(bfd.BFD_STATE_INIT)
                elif bfd_pkt.state == bfd.BFD_STATE_INIT:
                    self._set_state(bfd.BFD_STATE_UP)

            elif self._session_state == bfd.BFD_STATE_INIT:
                if bfd_pkt.state in [bfd.BFD_STATE_INIT, bfd.BFD_STATE_UP]:
                    self._set_state(bfd.BFD_STATE_UP)

            else:
                if bfd_pkt.state == bfd.BFD_STATE_DOWN:
                    self._set_state(bfd.BFD_STATE_DOWN,
                                    bfd.BFD_DIAG_NEIG_SIG_SESS_DOWN)

        # TODO: Demand mode support.

        if self._remote_demand_mode and \
                self._session_state == bfd.BFD_STATE_UP and \
                self._remote_session_state == bfd.BFD_STATE_UP:
            self._enable_send = False

        if not self._remote_demand_mode or \
                self._session_state != bfd.BFD_STATE_UP or \
                self._remote_session_state != bfd.BFD_STATE_UP:
            if not self._enable_send:
                self._enable_send = True
                hub.spawn(self._send_loop)

        # Update the detection time (RFC5880 Section 6.8.4.)
        if self._detect_time == 0:
            self._detect_time = bfd_pkt.desired_min_tx_interval * \
                bfd_pkt.detect_mult / 1000000.0
            # Start the timeout loop.
            hub.spawn(self._recv_timeout_loop)

        if bfd_pkt.flags & bfd.BFD_FLAG_POLL:
            self._pending_final = True
            self._detect_time = bfd_pkt.desired_min_tx_interval * \
                bfd_pkt.detect_mult / 1000000.0

        # Update the remote authentication sequence number.
        if self._auth_type in [bfd.BFD_AUTH_KEYED_MD5,
                               bfd.BFD_AUTH_METICULOUS_KEYED_MD5,
                               bfd.BFD_AUTH_KEYED_SHA1,
                               bfd.BFD_AUTH_METICULOUS_KEYED_SHA1]:
            self._rcv_auth_seq = bfd_pkt.auth_cls.seq
            self._auth_seq_known = 1

        # Set the lock.
        if self._lock is not None:
            self._lock.set()

    def _set_state(self, new_state, diag=None):
        """
        Set the state of the BFD session.
        """
        old_state = self._session_state

        LOG.info("[BFD][%s][STATE] State changed from %s to %s.",
                 hex(self._local_discr),
                 bfd.BFD_STATE_NAME[old_state],
                 bfd.BFD_STATE_NAME[new_state])
        self._session_state = new_state

        if new_state == bfd.BFD_STATE_DOWN:
            if diag is not None:
                self._local_diag = diag
            self._desired_min_tx_interval = 1000000
            self._is_polling = True
            self._update_xmit_period()
        elif new_state == bfd.BFD_STATE_UP:
            self._desired_min_tx_interval = self._cfg_desired_min_tx_interval
            self._is_polling = True
            self._update_xmit_period()

        self.app.send_event_to_observers(
            EventBFDSessionStateChanged(self, old_state, new_state))

    def _recv_timeout_loop(self):
        """
        A loop to check timeout of receiving remote BFD packet.
        """
        while self._detect_time:
            last_wait = time.time()
            self._lock = hub.Event()

            self._lock.wait(timeout=self._detect_time)

            if self._lock.is_set():
                # Authentication variable check (RFC5880 Section 6.8.1.)
                if getattr(self, "_auth_seq_known", 0):
                    if last_wait > time.time() + 2 * self._detect_time:
                        self._auth_seq_known = 0

            else:
                # Check Detection Time expiration (RFC5880 section 6.8.4.)
                LOG.info("[BFD][%s][RECV] BFD Session timed out.",
                         hex(self._local_discr))
                if self._session_state not in [bfd.BFD_STATE_DOWN,
                                               bfd.BFD_STATE_ADMIN_DOWN]:
                    self._set_state(bfd.BFD_STATE_DOWN,
                                    bfd.BFD_DIAG_CTRL_DETECT_TIME_EXPIRED)

                # Authentication variable check (RFC5880 Section 6.8.1.)
                if getattr(self, "_auth_seq_known", 0):
                    self._auth_seq_known = 0

    def _update_xmit_period(self):
        """
        Update transmission period of the BFD session.
        """
        # RFC5880 Section 6.8.7.
        if self._desired_min_tx_interval > self._remote_min_rx_interval:
            xmit_period = self._desired_min_tx_interval
        else:
            xmit_period = self._remote_min_rx_interval

        # This updates the transmission period of BFD Control packets.
        # (RFC5880 Section 6.8.2 & 6.8.3.)
        if self._detect_mult == 1:
            xmit_period *= random.randint(75, 90) / 100.0
        else:
            xmit_period *= random.randint(75, 100) / 100.0

        self._xmit_period = xmit_period / 1000000.0
        LOG.info("[BFD][%s][XMIT] Transmission period changed to %f",
                 hex(self._local_discr), self._xmit_period)

    def _send_loop(self):
        """
        A loop to proceed periodic BFD packet transmission.
        """
        while self._enable_send:
            hub.sleep(self._xmit_period)

            # Send BFD packet. (RFC5880 Section 6.8.7.)

            if self._remote_discr == 0 and not self._active_role:
                continue

            if self._remote_min_rx_interval == 0:
                continue

            if self._remote_demand_mode and \
                    self._session_state == bfd.BFD_STATE_UP and \
                    self._remote_session_state == bfd.BFD_STATE_UP and \
                    not self._is_polling:
                continue

            self._send()

    def _send(self):
        """
        BFD packet sender.
        """
        # If the switch was not connected to controller, exit.
        if self.datapath is None:
            return

        # BFD Flags Setup
        flags = 0

        if self._pending_final:
            flags |= bfd.BFD_FLAG_FINAL
            self._pending_final = False
            self._is_polling = False

        if self._is_polling:
            flags |= bfd.BFD_FLAG_POLL

        # Authentication Section
        auth_cls = None
        if self._auth_type:
            auth_key_id = list(self._auth_keys.keys())[
                random.randint(0, len(list(self._auth_keys.keys())) - 1)]
            auth_key = self._auth_keys[auth_key_id]

            if self._auth_type == bfd.BFD_AUTH_SIMPLE_PASS:
                auth_cls = bfd.SimplePassword(auth_key_id=auth_key_id,
                                              password=auth_key)

            if self._auth_type in [bfd.BFD_AUTH_KEYED_MD5,
                                   bfd.BFD_AUTH_METICULOUS_KEYED_MD5,
                                   bfd.BFD_AUTH_KEYED_SHA1,
                                   bfd.BFD_AUTH_METICULOUS_KEYED_SHA1]:
                if self._auth_type in [bfd.BFD_AUTH_KEYED_MD5,
                                       bfd.BFD_AUTH_KEYED_SHA1]:
                    if random.randint(0, 1):
                        self._xmit_auth_seq = \
                            (self._xmit_auth_seq + 1) & UINT32_MAX
                else:
                    self._xmit_auth_seq = \
                        (self._xmit_auth_seq + 1) & UINT32_MAX

                auth_cls = bfd.bfd._auth_parsers[self._auth_type](
                    auth_key_id=auth_key_id,
                    seq=self._xmit_auth_seq,
                    auth_key=auth_key)

        if auth_cls is not None:
            flags |= bfd.BFD_FLAG_AUTH_PRESENT

        if self._demand_mode and \
                self._session_state == bfd.BFD_STATE_UP and \
                self._remote_session_state == bfd.BFD_STATE_UP:
            flags |= bfd.BFD_FLAG_DEMAND

        diag = self._local_diag
        state = self._session_state
        detect_mult = self._detect_mult
        my_discr = self._local_discr
        your_discr = self._remote_discr
        desired_min_tx_interval = self._desired_min_tx_interval
        required_min_rx_interval = self._required_min_rx_interval
        required_min_echo_rx_interval = self._cfg_required_min_echo_rx_interval

        # Prepare for Ethernet/IP/UDP header fields
        src_mac = self.src_mac
        dst_mac = self.dst_mac
        src_ip = self.src_ip
        dst_ip = self.dst_ip
        self.ipv4_id = (self.ipv4_id + 1) & UINT16_MAX
        ipv4_id = self.ipv4_id
        src_port = self.src_port
        dst_port = self.dst_port

        # Construct BFD Control packet
        data = BFDPacket.bfd_packet(
            src_mac=src_mac, dst_mac=dst_mac,
            src_ip=src_ip, dst_ip=dst_ip, ipv4_id=ipv4_id,
            src_port=src_port, dst_port=dst_port,
            diag=diag, state=state, flags=flags, detect_mult=detect_mult,
            my_discr=my_discr, your_discr=your_discr,
            desired_min_tx_interval=desired_min_tx_interval,
            required_min_rx_interval=required_min_rx_interval,
            required_min_echo_rx_interval=required_min_echo_rx_interval,
            auth_cls=auth_cls)

        # Prepare for a datapath
        datapath = self.datapath
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser

        actions = [parser.OFPActionOutput(self.ofport)]

        out = parser.OFPPacketOut(datapath=datapath,
                                  buffer_id=ofproto.OFP_NO_BUFFER,
                                  in_port=ofproto.OFPP_CONTROLLER,
                                  actions=actions,
                                  data=data)

        datapath.send_msg(out)
        LOG.debug("[BFD][%s][SEND] BFD Control sent.", hex(self._local_discr))


class BFDPacket(object):
    """
    BFDPacket class for parsing raw BFD packet, and generating BFD packet with
    Ethernet, IPv4, and UDP headers.
    """

    class BFDUnknownFormat(OSKenException):
        message = '%(msg)s'

    @staticmethod
    def bfd_packet(src_mac, dst_mac, src_ip, dst_ip, ipv4_id,
                   src_port, dst_port,
                   diag=0, state=0, flags=0, detect_mult=0,
                   my_discr=0, your_discr=0, desired_min_tx_interval=0,
                   required_min_rx_interval=0,
                   required_min_echo_rx_interval=0,
                   auth_cls=None):
        """
        Generate BFD packet with Ethernet/IPv4/UDP encapsulated.
        """
        # Generate ethernet header first.
        pkt = packet.Packet()
        eth_pkt = ethernet.ethernet(dst_mac, src_mac, ETH_TYPE_IP)
        pkt.add_protocol(eth_pkt)

        # IPv4 encapsulation
        # set ToS to 192 (Network control/CS6)
        # set TTL to 255 (RFC5881 Section 5.)
        ipv4_pkt = ipv4.ipv4(proto=inet.IPPROTO_UDP, src=src_ip, dst=dst_ip,
                             tos=192, identification=ipv4_id, ttl=255)
        pkt.add_protocol(ipv4_pkt)

        # UDP encapsulation
        udp_pkt = udp.udp(src_port=src_port, dst_port=dst_port)
        pkt.add_protocol(udp_pkt)

        # BFD payload
        bfd_pkt = bfd.bfd(
            ver=1, diag=diag, state=state, flags=flags,
            detect_mult=detect_mult,
            my_discr=my_discr, your_discr=your_discr,
            desired_min_tx_interval=desired_min_tx_interval,
            required_min_rx_interval=required_min_rx_interval,
            required_min_echo_rx_interval=required_min_echo_rx_interval,
            auth_cls=auth_cls)
        pkt.add_protocol(bfd_pkt)

        pkt.serialize()
        return pkt.data

    @staticmethod
    def bfd_parse(data):
        """
        Parse raw packet and return BFD class from packet library.
        """
        pkt = packet.Packet(data)
        i = iter(pkt)
        eth_pkt = next(i)

        assert isinstance(eth_pkt, ethernet.ethernet)

        ipv4_pkt = next(i)
        assert isinstance(ipv4_pkt, ipv4.ipv4)

        udp_pkt = next(i)
        assert isinstance(udp_pkt, udp.udp)

        udp_payload = next(i)

        return bfd.bfd.parser(udp_payload)[0]


class ARPPacket(object):
    """
    ARPPacket class for parsing raw ARP packet, and generating ARP packet with
    Ethernet header.
    """

    class ARPUnknownFormat(OSKenException):
        message = '%(msg)s'

    @staticmethod
    def arp_packet(opcode, src_mac, src_ip, dst_mac, dst_ip):
        """
        Generate ARP packet with ethernet encapsulated.
        """
        # Generate ethernet header first.
        pkt = packet.Packet()
        eth_pkt = ethernet.ethernet(dst_mac, src_mac, ETH_TYPE_ARP)
        pkt.add_protocol(eth_pkt)

        # Use IPv4 ARP wrapper from packet library directly.
        arp_pkt = arp.arp_ip(opcode, src_mac, src_ip, dst_mac, dst_ip)
        pkt.add_protocol(arp_pkt)

        pkt.serialize()
        return pkt.data

    @staticmethod
    def arp_parse(data):
        """
        Parse ARP packet, return ARP class from packet library.
        """
        # Iteratize pkt
        pkt = packet.Packet(data)
        i = iter(pkt)
        eth_pkt = next(i)
        # Ensure it's an ethernet frame.
        assert isinstance(eth_pkt, ethernet.ethernet)

        arp_pkt = next(i)
        if not isinstance(arp_pkt, arp.arp):
            raise ARPPacket.ARPUnknownFormat()

        if arp_pkt.opcode not in (ARP_REQUEST, ARP_REPLY):
            raise ARPPacket.ARPUnknownFormat(
                msg='unsupported opcode %d' % arp_pkt.opcode)

        if arp_pkt.proto != ETH_TYPE_IP:
            raise ARPPacket.ARPUnknownFormat(
                msg='unsupported arp ethtype 0x%04x' % arp_pkt.proto)

        return arp_pkt


class EventBFDSessionStateChanged(event.EventBase):
    """
    An event class that notifies the state change of a BFD session.
    """

    def __init__(self, session, old_state, new_state):
        super(EventBFDSessionStateChanged, self).__init__()
        self.session = session
        self.old_state = old_state
        self.new_state = new_state


class BFDLib(app_manager.OSKenApp):
    """
    BFD daemon library.

    Add this library as a context in your app and use ``add_bfd_session``
    function to establish a BFD session.

    Example::

        from os_ken.base import app_manager
        from os_ken.controller.handler import set_ev_cls
        from os_ken.ofproto import ofproto_v1_3
        from os_ken.lib import bfdlib
        from os_ken.lib.packet import bfd

        class Foo(app_manager.OSKenApp):
            OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]

            _CONTEXTS = {
                'bfdlib': bfdlib.BFDLib
            }

            def __init__(self, *args, **kwargs):
                super(Foo, self).__init__(*args, **kwargs)
                self.bfdlib = kwargs['bfdlib']
                self.my_discr = \
                    self.bfdlib.add_bfd_session(dpid=1,
                                                ofport=1,
                                                src_mac="00:23:45:67:89:AB",
                                                src_ip="192.168.1.1")

            @set_ev_cls(bfdlib.EventBFDSessionStateChanged)
            def bfd_state_handler(self, ev):
                if ev.session.my_discr != self.my_discr:
                    return

                if ev.new_state == bfd.BFD_STATE_DOWN:
                    print "BFD Session=%d is DOWN!" % ev.session.my_discr
                elif ev.new_state == bfd.BFD_STATE_UP:
                    print "BFD Session=%d is UP!" % ev.session.my_discr
    """
    OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]

    _EVENTS = [EventBFDSessionStateChanged]

    def __init__(self, *args, **kwargs):
        super(BFDLib, self).__init__(*args, **kwargs)

        # BFD Session Dictionary
        # key: My Discriminator
        # value: BFDSession object
        self.session = {}

    def close(self):
        pass

    @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
    def switch_features_handler(self, ev):
        datapath = ev.msg.datapath
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser

        # Update datapath object in BFD sessions
        for s in self.session.values():
            if s.dpid == datapath.id:
                s.datapath = datapath

        # Install default flows for capturing ARP & BFD packets.
        match = parser.OFPMatch(eth_type=ETH_TYPE_ARP)
        actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
                                          ofproto.OFPCML_NO_BUFFER)]
        self.add_flow(datapath, 0xFFFF, match, actions)

        match = parser.OFPMatch(eth_type=ETH_TYPE_IP,
                                ip_proto=inet.IPPROTO_UDP,
                                udp_dst=3784)
        actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
                                          ofproto.OFPCML_NO_BUFFER)]
        self.add_flow(datapath, 0xFFFF, match, actions)

    def add_flow(self, datapath, priority, match, actions):
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser

        inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS,
                                             actions)]

        mod = parser.OFPFlowMod(datapath=datapath, priority=priority,
                                match=match, instructions=inst)
        datapath.send_msg(mod)

    # Packet-In Handler, only for BFD packets.
    @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
    def _packet_in_handler(self, ev):
        msg = ev.msg
        datapath = msg.datapath
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
        in_port = msg.match['in_port']

        pkt = packet.Packet(msg.data)

        # If there's someone asked for an IP address associated
        # with a BFD session, generate an ARP reply for it.
        if arp.arp in pkt:
            arp_pkt = ARPPacket.arp_parse(msg.data)
            if arp_pkt.opcode == ARP_REQUEST:
                for s in self.session.values():
                    if s.dpid == datapath.id and \
                            s.ofport == in_port and \
                            s.src_ip == arp_pkt.dst_ip:

                        ans = ARPPacket.arp_packet(
                            ARP_REPLY,
                            s.src_mac, s.src_ip,
                            arp_pkt.src_mac, arp_pkt.src_ip)

                        actions = [parser.OFPActionOutput(in_port)]
                        out = parser.OFPPacketOut(
                            datapath=datapath,
                            buffer_id=ofproto.OFP_NO_BUFFER,
                            in_port=ofproto.OFPP_CONTROLLER,
                            actions=actions, data=ans)

                        datapath.send_msg(out)
                        return
            return

        # Check whether it's BFD packet or not.
        if ipv4.ipv4 not in pkt or udp.udp not in pkt:
            return

        udp_hdr = pkt.get_protocols(udp.udp)[0]
        if udp_hdr.dst_port != BFD_CONTROL_UDP_PORT:
            return

        # Parse BFD packet here.
        self.recv_bfd_pkt(datapath, in_port, msg.data)

    def add_bfd_session(self, dpid, ofport, src_mac, src_ip,
                        dst_mac="FF:FF:FF:FF:FF:FF", dst_ip="255.255.255.255",
                        auth_type=0, auth_keys=None):
        """
        Establish a new BFD session and return My Discriminator of new session.

        Configure the BFD session with the following arguments.

        ================ ======================================================
        Argument         Description
        ================ ======================================================
        dpid             Datapath ID of the BFD interface.
        ofport           Openflow port number of the BFD interface.
        src_mac          Source MAC address of the BFD interface.
        src_ip           Source IPv4 address of the BFD interface.
        dst_mac          (Optional) Destination MAC address of the BFD
                         interface.
        dst_ip           (Optional) Destination IPv4 address of the BFD
                         interface.
        auth_type        (Optional) Authentication type.
        auth_keys        (Optional) A dictionary of authentication key chain
                         which key is an integer of *Auth Key ID* and value
                         is a string of *Password* or *Auth Key*.
        ================ ======================================================

        Example::

            add_bfd_session(dpid=1,
                            ofport=1,
                            src_mac="01:23:45:67:89:AB",
                            src_ip="192.168.1.1",
                            dst_mac="12:34:56:78:9A:BC",
                            dst_ip="192.168.1.2",
                            auth_type=bfd.BFD_AUTH_KEYED_SHA1,
                            auth_keys={1: "secret key 1",
                                       2: "secret key 2"})
        """
        auth_keys = auth_keys if auth_keys else {}
        # Generate a unique discriminator
        while True:
            # Generate My Discriminator
            my_discr = random.randint(1, UINT32_MAX)

            # Generate an UDP destination port according to RFC5881 Section 4.
            src_port = random.randint(49152, 65535)

            # Ensure generated discriminator and UDP port are unique.
            if my_discr in self.session:
                continue

            unique_flag = True

            for s in self.session.values():
                if s.your_discr == my_discr or s.src_port == src_port:
                    unique_flag = False
                    break

            if unique_flag:
                break

        sess = BFDSession(app=self, my_discr=my_discr,
                          dpid=dpid, ofport=ofport,
                          src_mac=src_mac, src_ip=src_ip, src_port=src_port,
                          dst_mac=dst_mac, dst_ip=dst_ip,
                          auth_type=auth_type, auth_keys=auth_keys)

        self.session[my_discr] = sess

        return my_discr

    def recv_bfd_pkt(self, datapath, in_port, data):
        pkt = packet.Packet(data)
        eth = pkt.get_protocols(ethernet.ethernet)[0]

        if eth.ethertype != ETH_TYPE_IP:
            return

        ip_pkt = pkt.get_protocols(ipv4.ipv4)[0]

        # Discard it if TTL != 255 for single hop bfd. (RFC5881 Section 5.)
        if ip_pkt.ttl != 255:
            return

        # Parse BFD packet here.
        bfd_pkt = BFDPacket.bfd_parse(data)

        if not isinstance(bfd_pkt, bfd.bfd):
            return

        # BFD sanity checks
        # RFC 5880 Section 6.8.6.
        if bfd_pkt.ver != 1:
            return

        if bfd_pkt.flags & bfd.BFD_FLAG_AUTH_PRESENT:
            if bfd_pkt.length < 26:
                return
        else:
            if bfd_pkt.length < 24:
                return

        if bfd_pkt.detect_mult == 0:
            return

        if bfd_pkt.flags & bfd.BFD_FLAG_MULTIPOINT:
            return

        if bfd_pkt.my_discr == 0:
            return

        if bfd_pkt.your_discr != 0 and bfd_pkt.your_discr not in self.session:
            return

        if bfd_pkt.your_discr == 0 and \
                bfd_pkt.state not in [bfd.BFD_STATE_ADMIN_DOWN,
                                      bfd.BFD_STATE_DOWN]:
            return

        sess_my_discr = None

        if bfd_pkt.your_discr == 0:
            # Select session (Page 34)
            for s in self.session.values():
                if s.dpid == datapath.id and s.ofport == in_port:
                    sess_my_discr = s.my_discr
                    break

            # BFD Session not found.
            if sess_my_discr is None:
                return
        else:
            sess_my_discr = bfd_pkt.your_discr

        sess = self.session[sess_my_discr]

        if bfd_pkt.flags & bfd.BFD_FLAG_AUTH_PRESENT and sess._auth_type == 0:
            return

        if bfd_pkt.flags & bfd.BFD_FLAG_AUTH_PRESENT == 0 and \
                sess._auth_type != 0:
            return

        # Authenticate the session (Section 6.7.)
        if bfd_pkt.flags & bfd.BFD_FLAG_AUTH_PRESENT:
            if sess._auth_type == 0:
                return

            if bfd_pkt.auth_cls.auth_type != sess._auth_type:
                return

            # Check authentication sequence number to defend replay attack.
            if sess._auth_type in [bfd.BFD_AUTH_KEYED_MD5,
                                   bfd.BFD_AUTH_METICULOUS_KEYED_MD5,
                                   bfd.BFD_AUTH_KEYED_SHA1,
                                   bfd.BFD_AUTH_METICULOUS_KEYED_SHA1]:
                if sess._auth_seq_known:
                    if bfd_pkt.auth_cls.seq < sess._rcv_auth_seq:
                        return

                    if sess._auth_type in [bfd.BFD_AUTH_METICULOUS_KEYED_MD5,
                                           bfd.BFD_AUTH_METICULOUS_KEYED_SHA1]:
                        if bfd_pkt.auth_cls.seq <= sess._rcv_auth_seq:
                            return

                    if bfd_pkt.auth_cls.seq > sess._rcv_auth_seq \
                            + 3 * sess._detect_mult:
                        return

            if not bfd_pkt.authenticate(sess._auth_keys):
                LOG.debug("[BFD][%s][AUTH] BFD Control authentication failed.",
                          hex(sess._local_discr))
                return

        # Sanity check passed, proceed.
        if sess is not None:
            # Check whether L2/L3 addresses were configured or not.
            # TODO: L2/L3 addresses negotiation for an established session.
            if not sess._remote_addr_config:
                sess.set_remote_addr(eth.src, ip_pkt.src)
            # Proceed to session update.
            sess.recv(bfd_pkt)
