#! /usr/bin/python2

#-------------------------------------------------------------------------------
#
# Copyright 2017, 2018, 2019 Cumulus Networks, inc  all rights reserved
#
# License: Cumulus Networks Proprietary
#  All Rights reserved.
#  .
#  The Cumulus Networks End User License Agreement can be found at the
#  following locations.
#  .
#  Internet: https://cumulusnetworks.com/downloads/eula/latest/view/
#  Cumulus Linux systems: /usr/share/cumulus/EULA.txt
#-------------------------------------------------------------------------------

"""
This script has three functions: neighbor refresh, VRR MAC address refresh and
VRR neighbor sync. Two of these three functions run as separate threads. The
neighbor refreshes are important to keep those entries in the "reachable" state,
and not have them churn between "reachable" and "stale" or other states which
can cause lots of Netlink messages and significant work by the receipients of
those netlink messages, like switchd.

Neighbor Refresh - The goal is to keep neighbors which are still alive from
transitioning out of the reachable state. So this thread periodically sends
arp requests (for IPv4 neighbors) or neighbor solictations (for IPv6 neighbors)
to each of the neighbors.

VRR MAC address refresh - Although servers send data to the VRR MAC address,
the switch never sends data using the VRR MAC address in the source address
field of a packet. Therefore, any switches between the server and this switch
would flood packets to the VRR MAC address. This thread sends gratuitous ARPs
with the VRR MAC as the source address, to keep the MACs in the forwarding
database of those switches.

VRR Neighbor Sync - This is not a separate thread but is done whenever a
neighbor table update for a VRR interface which changes the MAC address, an ARP
request is sent from the corresponding individual (lower) interface for that
same neighbor. This will cause the individual interface's neighbor table entry
to be updated if it is out of sync. The same thing is done for IPv6 entries,
except neighbor solicitation requests are sent instead of ARP requests.

Neigh Work - This is a separate thread that performs work on a neigh in
the background and in a staggered fashion. It wakes up every one second
does _NEIGH_WORKQ_PROC_MAX amount of work and goes back to sleep. For
more details see neighWorkQ and neighWorkQDedup.

EVPN NTF_EXT_LEARNED neigh entries -
Regular dynamic neigh entries are learned via ARP requests/responses. But the
EVPN neigh entries are installed by FRR on unnumbered bridge interfaces or
SVIs and tagged with the EXT_LEARNED flag. Sample -
root@l1:/media/node# ip -4 neighbor |grep bridge.200
60.1.1.31 dev bridge.200 lladdr 00:02:00:00:00:05 ext_learned REACHABLE >>>>>
60.1.1.22 dev bridge.200 lladdr 00:02:00:00:00:04 ext_learned REACHABLE >>>>>
60.1.1.11 dev bridge.200 lladdr 00:02:00:00:00:01 REACHABLE
60.1.1.12 dev bridge.200 lladdr 00:02:00:00:00:02 REACHABLE
>>>>>>>>>>>>> SNIP>>>>>>>>>>
1. Such entries should NOT be refreshed by the NeighborRefresh mechanism.
2. Also such entries need to be announced via gratuituous ARP requests to the
access ports in the bridge. neighmgrd simply sends the packet (with a
broadcast DMAC) out the bridge interface or SVI and the kernel ensures that
they are only flooded out the access ports (and suppressed on the network
port).
"""

#-------------------------------------------------------------------------------
#
#   Import the necessary modules
#
#-------------------------------------------------------------------------------

try:
    import argparse
    import copy
    import ConfigParser
    import ipaddr
    import logging
    import logging.handlers
    import nlmanager.nllistener
    import nlmanager.nlpacket
    import Queue
    import signal
    import socket
    import struct
    import sys
    import threading
    import json
    import neighmgr_cli
    import neighmgr_pkt as nm_pkt
    import neighmgr_snoop
    from subprocess import check_output, CalledProcessError
    from errno import ENOENT
except ImportError, e:
    raise ImportError (str(e) + "- required module not found")


#-------------------------------------------------------------------------------
#
#   Define constants
#
#-------------------------------------------------------------------------------

_ETH_BROADCAST      = "\xFF\xFF\xFF\xFF\xFF\xFF"
_ETH_NULL           = "\x00\x00\x00\x00\x00\x00"

_ARP_ETHTYPE        = 0x0806

_ARP_HW_TYPE_ETH    = 0x0001
_ARP_PROT_TYPE_IP   = 0x0800

_ARP_HW_ETH_LEN     = 6
_ARP_PROT_IP_LEN    = 4

_ARP_REQUEST        = 1
_ARP_REPLY          = 2

_IPV4_UNSPEC_ADDR   = "\x00\x00\x00\x00"
_IPV6_UNSPEC_ADDR   = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
_IPV6_SN_MCAST_IP   = "\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff"

_IPV4_BASE_REACHABLE_MS = "/proc/sys/net/ipv4/neigh/default/base_reachable_time_ms"
_IPV6_BASE_REACHABLE_MS = "/proc/sys/net/ipv6/neigh/default/base_reachable_time_ms"

_IPV6_ETHTYPE = 0x86DD
#max neighs processed in one run of the neigh workQ handler
_NEIGH_WORKQ_PROC_MAX = 100
_NEIGH_REACH_DAMPEN_THRESHOLD = 10
_NEIGH_REACH_DAMPEN_TIMEOUT = 60*10 #10 minutes

_NEIGHMGR_CONF_FILE = "/etc/cumulus/neighmgr.conf"

TC_PROTO  = 'all'
TC_PREF   = 1
# set only a single bit (0x08000000) to minimize overlap with future users
TC_FWMARK = 1 << 27
TC_FTYPE  = 'fw'
TC_QINFO  = 'clsact'
TC_FINFO  = 'action drop'
TC_Q_CMD  = "tc qdisc %s dev %s %s"
TC_F_CMD  = "tc filter %s dev %s egress protocol {} pref {} handle {} {} %s"\
             .format(TC_PROTO, TC_PREF, TC_FWMARK, TC_FTYPE)
# python2's socket module doesn't have a constant for SO_MARK, so make our own
SO_MARK = 36

#-------------------------------------------------------------------------------
#
#   Global Variables
#
#-------------------------------------------------------------------------------

# The handle used for logging messages
logger = logging.getLogger("neighmgrd")

# When set, exit all threads and get out.
stopEvent = threading.Event()

# The netlink manager
nlm = None

# ARP snooper
arpsnoop = None

# Socket used for sending ARP packets
arpSock = None

# Socket used for sending proxy ARP/NDP announcements for remote neighs
remoteArpSock = None

# Used for reading neighmgr conf file
confParser = ConfigParser.SafeConfigParser()

#-------------------------------------------------------------------------------
#
#   Functions
#
#-------------------------------------------------------------------------------

def ParseCmdLine():
    """
    Parse the command line options using the standard argparse library.
    """
    # The default values for the arguments
    _DEFAULT_LOG_LEVEL  = "WARNING"


    parser = argparse.ArgumentParser(description="Send ARP request to a list of hosts")
    parser.add_argument("--loglevel", "-l", default=_DEFAULT_LOG_LEVEL,
                        help="The logging level for output")
    parser.add_argument("--disable-snooper", "-d", action='store_true',
                        help="Disable ARP snooping function")
    args = parser.parse_args()
    return(args)

def GetConfigValue(section, option, defaultVal):
    try:
        val = confParser.get(section, option)
    except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
        val = defaultVal

    return val

def InitLogging(cmdargs):
    '''
    Set up the logging according to the command line parameters provided.
    '''
    logCfg = GetConfigValue('main', 'logging', cmdargs.loglevel)
    facility = logging.handlers.SysLogHandler.LOG_DAEMON
    handler = logging.handlers.SysLogHandler(address="/dev/log", facility=facility)
    formatter = logging.Formatter("neighmgrd[%(process)d]: %(message)s", None)
    handler.setFormatter(formatter)
    logger.addHandler(handler)

    logLevel = getattr(logging, logCfg.upper(), None)
    if not isinstance(logLevel, int):
        raise ValueError("Invalid log level: %s" % cmdargs.loglevel)
        exit(-1)
    logger.setLevel(logLevel)

def StopEventSet():
    stopEvent.set()
    # wakeup the snooper
    if arpsnoop:
        arpsnoop.wakeup_arprx()

def SignalHandler(signum, frame):
    """
    Handles any signals registered for and received while code is running.
    """
    if signum in [signal.SIGTERM, signal.SIGINT]:
        StopEventSet()
        if nlm:
            nlm.workq.put(('SHUTDOWN', None))
            if signum == signal.SIGTERM:
                nlm.signal_term_handler(signum, frame)
            elif signum == signal.SIGINT:
                nlm.signal_int_handler(signum, frame)
            nlm.listener.join()

def GetIpv4BaseReachableMs():
    try:
        with open(_IPV4_BASE_REACHABLE_MS) as f:
            return int(f.readline().strip())
    except (IOError, ValueError, TypeError):
        return 4*60*60*1000

def GetIpv6BaseReachableMs():
    try:
        with open(_IPV6_BASE_REACHABLE_MS) as f:
            return int(f.readline().strip())
    except (IOError, ValueError, TypeError):
        return 4*60*60*1000

def checksum(pkt):
    """
    Calculates the traditional one's compliment checksum used in many IETF
    protocols.
    """
    if len(pkt) % 2 == 1:
        pkt += "\0"
    s = (sum([ord(x) for x in pkt[0::2]]) << 8) + sum([ord(x) for x in pkt[1::2]])
    s = (s >> 16) + (s & 0xffff)
    s += s >> 16
    s = ~s
    return s & 0xffff

def Icmp6GratNASend(ifIndex, addr, lladdr, isRouter):
    if remoteArpSock is None:
        return False

    packet = nm_pkt.Icmp6GratNAGen(addr, lladdr, isRouter)
    if logger.isEnabledFor(logging.DEBUG):
        logger.debug("sending packet to ifI %d (%s)" % (ifIndex, nm_pkt.toHexStr(packet),))
    sent = False
    try:
        nm_pkt.sendmsg(arpSock, ifIndex, _IPV6_ETHTYPE, packet, len(packet))
        sent=True
    except socket.error as e:
        if logger.isEnabledFor(logging.INFO):
            logger.info("unable to send packet to ifI %d: %s (%s)" % (ifIndex, nm_pkt.toHexStr(packet), str(e)))
    return sent


def NsSend(ifIndex, smac, sip, dip):
    """
    Sends a Neighbor solicitation packet
    """
    if arpSock is None:
        return False

    #           Type     Code     Checksum     Reserved
    icmp6     = '\x87' + '\x00' + '\x00\x00' + '\x00\x00\x00\x00' + dip
    #           Type     Length
    icmp6opt  = '\x01' + '\x01' + smac
    icmp6len  = struct.pack('!H', len(icmp6) + len(icmp6opt))

    pseudo    = sip + _IPV6_SN_MCAST_IP + dip[13:16] + '\x00\x00' + icmp6len + \
                '\x00\x00\x00' + '\x3a'
    icmp6csum = struct.pack('!H', checksum(pseudo + icmp6 + icmp6opt))
    #           Type     Code                 Reserved
    icmp6     = '\x87' + '\x00' + icmp6csum + '\x00\x00\x00\x00' + dip

    #     Ver, TC, Flow                  Next Hdr  Hop
    ip6 = '\x60\x00\x00\x00' + icmp6len + '\x3a' + '\xff' + sip + \
           _IPV6_SN_MCAST_IP + dip[13:16]
    #     DMAC                                 EthType
    eth = '\x33\x33\xff' + dip[13:16] + smac + '\x86\xdd'

    packet = eth + ip6 + icmp6 + icmp6opt

    sent=False
    if logger.isEnabledFor(logging.DEBUG):
        logger.debug("sending packet to ifI %d (%s)" % (ifIndex, nm_pkt.toHexStr(packet),))
    try:
        nm_pkt.sendmsg(arpSock, ifIndex, _IPV6_ETHTYPE, packet, len(packet))
        sent=True
    except socket.error as e:
        if logger.isEnabledFor(logging.INFO):
            logger.info("failed to send packet to ifI %d: %s (%s)" % (ifIndex, nm_pkt.toHexStr(packet), str(e)))
    return sent

def ArpSend(op, ifIndex, smac, sip, dmac, dip, isRemote):
    """
    Sends an ARP packet.
    """
    sock = remoteArpSock if isRemote else arpSock
    if sock is None:
        return False

    tmac = _ETH_NULL if dmac == _ETH_BROADCAST else dmac
    packet = dmac + smac + \
             struct.pack('!HHHBBH', _ARP_ETHTYPE, _ARP_HW_TYPE_ETH,
                         _ARP_PROT_TYPE_IP, _ARP_HW_ETH_LEN,
                         _ARP_PROT_IP_LEN, op) + \
             smac + sip + tmac + dip

    sent=False
    if logger.isEnabledFor(logging.DEBUG):
        logger.debug("sending packet to ifI %d (%s)" % (ifIndex, nm_pkt.toHexStr(packet),))
    try:
        nm_pkt.sendmsg(sock, ifIndex, _ARP_ETHTYPE, packet, len(packet))
        sent=True
    except socket.error as e:
        if logger.isEnabledFor(logging.INFO):
            logger.info("failed to send packet to ifI %d: %s (%s)" % (ifIndex, nm_pkt.toHexStr(packet), str(e)))
    return sent

def ArpReqNsSend(ifName, ifIndex, smac, sipv4, sipv6, dip):
    if not ifName or not smac or ifName.startswith("eth"):
        logger.debug("arp req send skipped for %s on interface %s/%s" %\
                     (str(dip), ifName, smac))
        return

    smacPacked = nm_pkt.mactobinary(smac)
    if dip.version == 4:
        logger.debug("sending arp req for new neigh %s on %s" %\
                     (str(dip), ifName))
        sip = _IPV4_UNSPEC_ADDR if sipv4 is None else sipv4.packed
        ArpSend(_ARP_REQUEST, ifIndex, smacPacked, sip, _ETH_BROADCAST, dip.packed, False)
    elif dip.version == 6:
        logger.debug("sending ns for new neigh %s on %s" %\
                     (str(dip), ifName))
        sip = _IPV6_UNSPEC_ADDR if sipv6 is None else sipv6.packed
        NsSend(ifIndex, smacPacked, sip, dip.packed)

def macFormat1ToFormat2(mac):
    '''
    Format1: 1122.3344.5566
    Format2: 11:22:33:44:55:66
    This api converst a mac string of Format1 to a mac string of Format2
    '''
    if not ':' in mac:
        mac = mac.replace('.', '')
        mac = ':'.join([mac[i:i+2] for i in range(0, len(mac), 2)])
    return mac

#-------------------------------------------------------------------------------
#
#   Threads
#
#-------------------------------------------------------------------------------

class NeighborListener(nlmanager.nllistener.NetlinkManagerWithListener):
    IFINFO_IDX_KIND = 0
    IFINFO_IDX_IFNAME = 1
    IFINFO_IDX_LOWER = 2
    IFINFO_IDX_MAC = 3
    IFINFO_IDX_IPv4 = 4
    IFINFO_IDX_IPv6 = 5
    IFINFO_IDX_MASTER = 6
    IFINFO_IDX_BRPORT_ARP_SUP = 7
    IFINFO_IDX_VLAN = 8 # SVI vlan
    IFINFO_IDX_IS_BRPORT = 9 # set if AF_BRIDGE add is rxed
    IFINFO_IDX_BRPORT_PVID = 10
    IFINFO_IDX_ATLINK = 11 # bridge associated with SVI
    IFINFO_IDX_VLAN_FILTER = 12
    IFINFO_IDX_OPSTATE = 13
    IFINFO_IDX_BRPORT_PEER_LINK = 14
    IFINFO_IDX_IPv4_NETWORKS = 15
    IFINFO_IDX_IPv6_NETWORKS = 16
    IFINFO_IDX_REFRESH_PENDING = 17
    IFINFO_IDX_PENDING_NEIGH_COUNT = 18

    # increment this everytime a new field is added and update
    # intfFieldStr
    IFINFO_TUP_LEN = 19

    intfFieldStr = {IFINFO_IDX_KIND: "kind",
                    IFINFO_IDX_IFNAME: "ifName",
                    IFINFO_IDX_LOWER: "lowerLink",
                    IFINFO_IDX_MAC: "mac",
                    IFINFO_IDX_IPv4: "ipV4Addr",
                    IFINFO_IDX_IPv6: "ipV6Addr",
                    IFINFO_IDX_MASTER: "master",
                    IFINFO_IDX_BRPORT_ARP_SUP: "brPortArpSuppress",
                    IFINFO_IDX_VLAN: "vlan",
                    IFINFO_IDX_IS_BRPORT: "isBrPort",
                    IFINFO_IDX_BRPORT_PVID: "brPortPvid",
                    IFINFO_IDX_ATLINK: "atLink",
                    IFINFO_IDX_VLAN_FILTER: "vlanFiltering",
                    IFINFO_IDX_OPSTATE: "opState",
                    IFINFO_IDX_BRPORT_PEER_LINK: "brPortPeerLink",
                    IFINFO_IDX_IPv4_NETWORKS: "ipV4Networks",
                    IFINFO_IDX_IPv6_NETWORKS: "ipV6Networks",
                    IFINFO_IDX_REFRESH_PENDING: "refreshPending",
                    IFINFO_IDX_PENDING_NEIGH_COUNT: "pendingNeighCount",
                   }

    NEIGH_WORKQ_IDX_NONE = 0
    NEIGH_WORKQ_IDX_STALE_REFRESH = 1

    NEIGH_INFO_IDX_MAC = 0
    NEIGH_INFO_IDX_IS_EXT = 1
    NEIGH_INFO_IDX_REACH_RETRIES = 2 # number of times we have tried to make
                                     # an entry reachable outside of the
                                     # periodic refreshes
    NEIGH_INFO_IDX_IS_ROUTER = 3
    NEIGH_INFO_IDX_STATE = 4
    NEIGH_INFO_PENDING_REFRESH = 5
    NEIGH_INFO_LIST_LEN = 6 # increment this everytime you add a field to
                           # neighInfo

    STAT_IDX_NEIGH_WORK_TOTAL = 0
    STAT_IDX_NEIGH_WORK_MAX = 1
    STAT_IDX_NEIGH_WORKQ_MAX_LEN = 2
    STAT_IDX_ADD_NEIGHv4 = 3
    STAT_IDX_ADD_NEIGHv6 = 4
    STAT_IDX_MOD_NEIGHv4 = 5
    STAT_IDX_MOD_NEIGHv6 = 6
    STAT_IDX_DEL_NEIGHv4 = 7
    STAT_IDX_DEL_NEIGHv6 = 8
    STAT_IDX_ADD_EXT_NEIGHv4 = 9
    STAT_IDX_ADD_EXT_NEIGHv6 = 10
    STAT_IDX_MOD_EXT_NEIGHv4 = 11
    STAT_IDX_MOD_EXT_NEIGHv6 = 12
    STAT_IDX_DEL_EXT_NEIGHv4 = 13
    STAT_IDX_DEL_EXT_NEIGHv6 = 14
    STAT_IDX_ADD_VRR_NEIGHv4 = 15
    STAT_IDX_ADD_VRR_NEIGHv6 = 16
    STAT_IDX_MOD_VRR_NEIGHv4 = 17
    STAT_IDX_MOD_VRR_NEIGHv6 = 18
    STAT_IDX_DEL_VRR_NEIGHv4 = 19
    STAT_IDX_DEL_VRR_NEIGHv6 = 20
    STAT_IDX_NEIGHv4 = 21
    STAT_IDX_NEIGHv6 = 22
    STAT_IDX_EXT_NEIGHv4 = 23
    STAT_IDX_EXT_NEIGHv6 = 24
    STAT_IDX_VRR_NEIGHv4 = 25
    STAT_IDX_VRR_NEIGHv6 = 26
    STAT_IDX_NEIGH_MOD_DAMPENS = 27
    STAT_IDX_RESYNCS = 28
    STAT_IDX_CONFIG_RELOADS = 29
    STAT_IDX_DEFERRED_V6_REFRESHES = 30
    # update STAT_IDX_NUM and statInfo when you add a new stat
    STAT_IDX_NUM = 31

    STAT_INFO_IDX_STR = 0
    STAT_INFO_IDX_CLEAR_OK = 1 # current entry counts cannot be cleared by
                               # the admin
    # key=STAT_IDX_XXX info=(desc, clear_ok)
    statInfo = {STAT_IDX_NEIGH_WORK_TOTAL: ("neighWorkTotal", False),
                STAT_IDX_NEIGH_WORK_MAX: ("neighWorkMax", False),
                STAT_IDX_NEIGH_WORKQ_MAX_LEN: ("neighWorkQMaxLen", False),
                STAT_IDX_ADD_NEIGHv4: ("neighAddIpv4", True),
                STAT_IDX_ADD_NEIGHv6: ("neighAddIpv6", True),
                STAT_IDX_MOD_NEIGHv4: ("neighModIpv4", True),
                STAT_IDX_MOD_NEIGHv6: ("neighModIpv6", True),
                STAT_IDX_DEL_NEIGHv4: ("neighDelIpv4", True),
                STAT_IDX_DEL_NEIGHv6: ("neighDelIpv6", True),
                STAT_IDX_ADD_EXT_NEIGHv4: ("neighAddExtIpv4", True),
                STAT_IDX_ADD_EXT_NEIGHv6: ("neighAddExtIpv6", True),
                STAT_IDX_MOD_EXT_NEIGHv4: ("neighModExtIpv4", True),
                STAT_IDX_MOD_EXT_NEIGHv6: ("neighModExtIpv6", True),
                STAT_IDX_DEL_EXT_NEIGHv4: ("neighDelExtIpv4", True),
                STAT_IDX_DEL_EXT_NEIGHv6: ("neighDelExtIpv6", True),
                STAT_IDX_ADD_VRR_NEIGHv4: ("neighAddVrrIpv4", True),
                STAT_IDX_ADD_VRR_NEIGHv6: ("neighAddVrrIpv6", True),
                STAT_IDX_MOD_VRR_NEIGHv4: ("neighModVrrIpv4", True),
                STAT_IDX_MOD_VRR_NEIGHv6: ("neighModVrrIpv6", True),
                STAT_IDX_DEL_VRR_NEIGHv4: ("neighDelVrrIpv4", True),
                STAT_IDX_DEL_VRR_NEIGHv6: ("neighDelVrrIpv6", True),
                STAT_IDX_NEIGHv4: ("neighIpv4", False),
                STAT_IDX_NEIGHv6: ("neighIpv6", False),
                STAT_IDX_EXT_NEIGHv4: ("neighExtIpv4", False),
                STAT_IDX_EXT_NEIGHv6: ("neighExtIpv6", False),
                STAT_IDX_VRR_NEIGHv4: ("neighVrrIpv4", False),
                STAT_IDX_VRR_NEIGHv6: ("neighVrrIpv6", False),
                STAT_IDX_NEIGH_MOD_DAMPENS: ("neighModDampens", True),
                STAT_IDX_RESYNCS: ("resyncs", True),
                STAT_IDX_CONFIG_RELOADS: ("configReloads", True),
                STAT_IDX_DEFERRED_V6_REFRESHES: ("deferredIpv6Refreshes", True),
               }

    '''
    This class overrides the Netlink Manager's NetlinkManagerWithListener class
    to provide customizations for this script.
    '''
    def __init__(self, groups, cmdargs, use_color=True):
        self.groups = groups
        self.use_color = use_color
        self.cmdargs = cmdargs
        super(NeighborListener, self).__init__(groups, use_color=use_color, error_notification=True, rcvbuf_sz=16000000)

        # stats STAT_IDX_XXX:counter
        self.stats = {}
        for idx in range(self.STAT_IDX_NUM):
            self.stats[idx] = 0

        self.ifInfoLock = threading.Lock()
        # update IFINFO_IDX_XXX and intfFieldStr if you add new element to
        # this tuple
        self.ifinfo_by_index  = {} # { ifindex : (kind, ifname, lower, mac, ipv4, ipv6, master, arpSup, vlan, isBrPort, pvid, atLink, vlanFilter), ... }

        self.neighInfoLock = threading.Lock()
        # update NEIGH_INFO_IDX_XXX and you add new element to this value list
        self.neigh_by_index_ip = {} # { (ifindex, ipaddr) : [mac, isExt, reachTries], ... }
        # database of neighbors present in the kernel but in an !NUD_VALID
        # state i.e. cannot be used for packet forwarding
        self.neigh_invalid_db = {} # { (ifindex, ipaddr) : state }

        # neighs can be added to a workQ for staggered processing - neighWorkQ
        # This is a simply a queue of keys with work pending against them
        # (ifindex, ipaddr).
        #
        # To prevent the workQ from growing in an unconstrained fashion a dict
        # is maintained for de-dup - neighWorkQDedup. key: (ifIndex, ipaddr).
        # Each entry in this dict is a list of work items pending against the
        # neigh where each work-item is one of NEIGH_WORKQ_IDX_XX.
        # So if a entry changes (MAC mod) back-to-back or if a neigh add/del/add
        # happens back-to-back you will only have a single item in the
        # neighWorkQ for that neigh (instead of two). This is particularly
        # important in a scale setup where OOM can happen because of unbounded
        # Queue build ups.
        #
        # Note: This means that work across neighs will not be processed in
        # order but an atempt at "fairness" will be made by processing the
        # neigh with the "oldest" work first.
        #
        # Instead of using a Queue+Dictionary I could have used an ordered dict.
        # But insertion into an ordered dictionary in python2.x is
        # way more expensive than insertion into a regular-dict+Queue.
        self.neighWorkRate = _NEIGH_WORKQ_PROC_MAX
        self.neighWorkQ = Queue.Queue() #queue of (ifIndex, ipaddr)
        self.neighWorkQDedup = {} #{(ifindex, ipaddr) : [work-list]}

        self.neighReachDampenTh = _NEIGH_REACH_DAMPEN_THRESHOLD
        self.neighReachDampenTout = _NEIGH_REACH_DAMPEN_TIMEOUT

        # if interface has no ip, instead of using unspec ip as sip, use a valid one
        # this option is configurable
        self.srcIPv4 = None
        # apply initial config
        self.apply_config()
        self.resync_in_prog = False

    def main(self):
        '''
        The main listener processing loop. Takes entries off the work queue and
        process them.
        '''

        # This loop has two jobs:
        # - process items on our workq
        # - process netlink messages on our netlinkq, messages are placed there via our NetlinkListener
        while True:

            try:
                (event, options) = self.workq.get(True, 60*60*24) # If timeout is None, signals are blocked
            except Queue.Empty:
                continue

            if event == 'SHUTDOWN':
                logger.info("NeighborListener: shutting things down")
                break
            elif event == 'GET_ALL_ADDRESSES':
                self.get_all_addresses()
            elif event == 'GET_ALL_LINKS':
                self.get_all_links()
                self.get_all_br_links()
            elif event == 'GET_ALL_NEIGHBORS':
                self.get_all_neighbors()
            elif event == 'GET_ALL_ROUTES':
                self.get_all_routes()
            elif event == 'SERVICE_NETLINK_QUEUE':
                self.service_netlinkq()
            elif event == 'SERVICE_ERROR':
                if "OVERFLOW" in options:
                    shutdown = self.service_overflow()
                    if shutdown:
                        # we caught a shutdown while processing overflow
                        return
            else:
                logger.error('rx unknown workq event %s' % event)

        self.listener.shutdown_event.set()
        self.listener.join()

    def summary(self):
        '''
        Summary of the neighmgr state
        '''
        summary = {}
        summary["neighWorkQLen"] = nlm.neighWorkQ.qsize()
        for idx in range(self.STAT_IDX_NUM):
            info = self.statInfo.get(idx)
            key = info[self.STAT_INFO_IDX_STR] if info else "stat_%d" % idx
            summary[key] = self.stats[idx]

        summary["dampenLimit"] = self.neighReachDampenTh
        summary["dampenTimeout"] = self.neighReachDampenTout
        summary["subnetChecks"] = self.subnetChecks
        summary["srcIPv4"] = str(self.srcIPv4)
        return json.dumps(summary, indent=4)

    def neigh_invalid_cache_json_str(self):
        '''
        Return a copy of the current contents of the neigh_invalid_db as a
        json str
        '''
        neighs = []
        for (neighIfindex, neighIpaddr), state in nlm.get_neigh_invalid_cache().iteritems():
            neigh = {}
            neigh['ip'] = str(neighIpaddr)
            ifName = self.ifinfo_ifindex2name(neighIfindex)
            neigh['ifName'] = ifName if ifName else str(neighIfindex)
            neigh['state'] = state
            neighs.append(neigh)
        return json.dumps(neighs, indent=4)

    def neigh_cache_json_str(self):
        '''
        Return a copy of the current contents of the neighbor cache as a json
        str
        '''
        neighs = []
        for (neighIfindex, neighIpaddr), neighInfo in nlm.get_neigh_cache().iteritems():
            neigh = {}
            neigh['ip'] = str(neighIpaddr)
            ifName = self.ifinfo_ifindex2name(neighIfindex)
            neigh['ifName'] = ifName if ifName else str(neighIfindex)
            neighMac = neighInfo[self.NEIGH_INFO_IDX_MAC]
            neigh['mac'] = macFormat1ToFormat2(neighMac) if neighMac else None
            neigh['isExt'] = neighInfo[self.NEIGH_INFO_IDX_IS_EXT]
            neigh['isRouter'] = neighInfo[self.NEIGH_INFO_IDX_IS_ROUTER]
            neigh['state'] = neighInfo[self.NEIGH_INFO_IDX_STATE]
            neigh['reachRetries'] = neighInfo[self.NEIGH_INFO_IDX_REACH_RETRIES]
            neigh['isDampened'] = True if neighInfo[self.NEIGH_INFO_IDX_REACH_RETRIES] >= self.neighReachDampenTh else False
            neighs.append(neigh)
        return json.dumps(neighs, indent=4)

    def ifaddr_cache_json_str(self):
        '''
        Return a copy of the current contents of the neighbor cache as a json
        str
        '''
        intfs = []
        for ifIndex, ifInfo in self.get_ifinfo_cache().iteritems():
            addr = {}
            addr['ifName'] = ifInfo[self.IFINFO_IDX_IFNAME]
            addr['ipv4Networks'] = []
            addr['ipv6Networks'] = []
            idx = self.IFINFO_IDX_IPv4_NETWORKS
            if ifInfo[idx]:
                for network in ifInfo[idx]:
                    addr['ipv4Networks'].append(str(network))
            idx = self.IFINFO_IDX_IPv6_NETWORKS
            if ifInfo[idx]:
                for network in ifInfo[idx]:
                    addr['ipv6Networks'].append(str(network))
            intfs.append(addr)
        return json.dumps(intfs, indent=4)

    def set_dampen_limit(self, limit):
        self.neighReachDampenTh = limit

    def set_dampen_tout(self, tout):
        self.neighReachDampenTout = tout

    def clear_stat(self, curr_cnt=False):
        for idx in range(self.STAT_IDX_NUM):
            info = self.statInfo.get(idx)
            if curr_cnt or (info and info[self.STAT_INFO_IDX_CLEAR_OK]):
                self.stats[idx] = 0

    def get_neigh_cache(self):
        '''
        Return a copy of the current contents of the neighbor cache
        '''
        self.neighInfoLock.acquire()
        retVal = copy.deepcopy(self.neigh_by_index_ip)
        self.neighInfoLock.release()
        return retVal

    def get_neigh_cache_entry(self, ifIndex, addr):
        '''
        Return a copy of the current neigh information for the specified ifindex and ipaddr
        '''
        self.neighInfoLock.acquire()
        neighInfo = self.neigh_by_index_ip.get((ifIndex, addr), [None,]*self.NEIGH_INFO_LIST_LEN)
        neighInvalid = self.neigh_invalid_db.get((ifIndex, addr))
        self.neighInfoLock.release()
        return neighInfo, neighInvalid

    def get_neigh_invalid_cache(self):
        '''
        Return a copy of the current contents of the neigh_invalid_db
        '''
        self.neighInfoLock.acquire()
        retVal = copy.deepcopy(self.neigh_invalid_db)
        self.neighInfoLock.release()
        return retVal

    def ifinfo_ifindex2name(self, ifIndex):
        ifInfo = self.get_ifinfo_cache_entry(ifIndex)
        return ifInfo[self.IFINFO_IDX_IFNAME]

    def ifinfo_cache_json_str(self):
        '''
        Return a copy of the current contents of the interface cache as a json
        str
        '''
        intfs = []
        ifCache = self.get_ifinfo_cache()
        for ifIndex in ifCache:
            ifInfo = ifCache[ifIndex]
            ifJInfo = {}
            ifJInfo["ifIndex"] = ifIndex
            for idx in range(self.IFINFO_TUP_LEN):
                if ifInfo[idx]:
                    ifJInfo[self.intfFieldStr[idx]] = ifInfo[idx]

            # some fields require special handling
            ipv4 = ifInfo[self.IFINFO_IDX_IPv4]
            if ipv4:
                ifJInfo[self.intfFieldStr[self.IFINFO_IDX_IPv4]] = str(ipv4)

            ipv6 = ifInfo[self.IFINFO_IDX_IPv6]
            if ipv6:
                ifJInfo[self.intfFieldStr[self.IFINFO_IDX_IPv6]] = str(ipv6)

            # networks are displayed separately
            try:
                del ifJInfo[self.intfFieldStr[self.IFINFO_IDX_IPv4_NETWORKS]]
            except KeyError:
                pass
            try:
                del ifJInfo[self.intfFieldStr[self.IFINFO_IDX_IPv6_NETWORKS]]
            except KeyError:
                pass

            # convert master, atLink and lower to ifNames
            master = ifInfo[self.IFINFO_IDX_MASTER]
            if master:
                name = self.ifinfo_ifindex2name(master)
                if name:
                    ifJInfo[self.intfFieldStr[self.IFINFO_IDX_MASTER]] = name

            lower = ifInfo[self.IFINFO_IDX_LOWER]
            if lower:
                name = self.ifinfo_ifindex2name(lower)
                if name:
                    ifJInfo[self.intfFieldStr[self.IFINFO_IDX_LOWER]] = name

            atLink = ifInfo[self.IFINFO_IDX_ATLINK]
            if atLink:
                name = self.ifinfo_ifindex2name(atLink)
                if name:
                    ifJInfo[self.intfFieldStr[self.IFINFO_IDX_ATLINK]] = name

            mac = ifInfo[self.IFINFO_IDX_MAC]
            ifJInfo[self.intfFieldStr[self.IFINFO_IDX_MAC]] =\
                             macFormat1ToFormat2(mac) if mac else None
            intfs.append(ifJInfo)
        return json.dumps(intfs, indent=4)

    def get_ifinfo_for_arp_req(self, ifIndex):
        self.ifInfoLock.acquire()
        ifInfo = self.ifinfo_by_index.get(ifIndex, (None,)*self.IFINFO_TUP_LEN)
        ifName = ifInfo[self.IFINFO_IDX_IFNAME]
        ifKind = ifInfo[self.IFINFO_IDX_KIND]

        # For VRR interfaces, send ARP/NS through the lower interface
        if ifName is not None and ifKind == 'macvlan':
            (base, sep, idx) = ifName.partition("-v")
            ifLower = ifInfo[self.IFINFO_IDX_LOWER]
            loInfo = self.ifinfo_by_index.get(ifLower, (None,)*self.IFINFO_TUP_LEN)
            loName = loInfo[self.IFINFO_IDX_IFNAME]
            if loName is not None and base == loName.replace(".", "-") and idx.isdigit():
                ifInfo = loInfo

        self.ifInfoLock.release()
        return ifInfo

    def get_neigh_work(self):
        self.neighInfoLock.acquire()
        try:
            key = self.neighWorkQ.get_nowait()
            workList = self.neighWorkQDedup.pop(key, [])
        except Queue.Empty:
            self.neighInfoLock.release()
            return (None,)*5

        neighInfo = self.neigh_by_index_ip.get(key)
        self.neighInfoLock.release()

        if not neighInfo:
            # entry not present flush the worklist
            workList = []
            neighInfo = [None,]*self.NEIGH_INFO_LIST_LEN
        logger.debug("do work %r for neigh %d:%s" %\
                     (workList, key[0], str(key[1])))
        return(key[0], key[1], workList, neighInfo[self.NEIGH_INFO_IDX_MAC],\
                     neighInfo[self.NEIGH_INFO_IDX_IS_EXT])

    def add_neigh_work(self, ifIndex, ipAddr, work, reasonStr, ifName):
        '''
        Queueup a work item for a neigh.
        Called must acquire neighInfoLock before calling this API
        '''
        logger.debug("add work %d/%s for neigh %s:%s" %\
                       (work, reasonStr, ifName, str(ipAddr)))
        key = (ifIndex, ipAddr)
        wl = self.neighWorkQDedup.get(key)
        if wl:
            if work not in wl:
                wl.append(work)
        else:
            self.neighWorkQDedup[key] = [work]
            self.neighWorkQ.put(key)
            qlen = nlm.neighWorkQ.qsize()
            if qlen > self.stats[self.STAT_IDX_NEIGH_WORKQ_MAX_LEN]:
                self.stats[self.STAT_IDX_NEIGH_WORKQ_MAX_LEN] = qlen

    def del_neigh_work(self, ifIndex, ipAddr, work):
        '''
        Remove a neigh work item; the neigh itself will be left in the
        workQ even if there is no work pending against it. Only NeighWork
        thread can remove neighs from the neighWorkQ and neighWorkQDedup.
        Caller must acquire neighInfoLock before calling this API.
        '''
        key = (ifIndex, ipAddr)
        removed = False
        wl = self.neighWorkQDedup.get(key)
        if wl:
            logger.debug("del work %d for neigh %d:%s" %\
                           (work, ifIndex, str(ipAddr)))
            try:
                wl.remove(work)
            except ValueError:
                removed = False
            else:
                # try block succeeds
                removed = True
        return removed

    def set_neigh_info_fields(self, ifIndex, addr, fieldVals):
        '''
        Changes one or more fields in the neighInfoLock entry. Rest of the fields
        in the tuple are left unaltered
        fieldVals: array of (fieldIdx, fieldVal)
        NOTE: CALLER IS RESPONSIBLE FOR ACQUIRING THE neighInfoLock
        '''
        key = (ifIndex, addr)
        try:
            neighInfo = self.neigh_by_index_ip.get(key)
            # update some fields to the new values
            for (fieldIdx, fieldVal) in fieldVals:
                neighInfo[fieldIdx] = fieldVal
        except:
            pass

    def undampen_neighs(self):
        cnt = 0
        self.neighInfoLock.acquire()
        for (neighIfindex, neighIpaddr), neighInfo in self.neigh_by_index_ip.iteritems():
            if neighInfo[self.NEIGH_INFO_IDX_REACH_RETRIES]:
                if neighInfo[self.NEIGH_INFO_IDX_REACH_RETRIES]\
                                         >= self.neighReachDampenTh:
                    cnt += 1
                    logger.info("undampened %d %s" % (neighIfindex, str(neighIpaddr)))
                neighInfo[self.NEIGH_INFO_IDX_REACH_RETRIES] = 0
        self.neighInfoLock.release()
        logger.info("undampened %d neighs" % cnt)

    def set_ifinfo_fields(self, ifIndex, fieldVals):
        '''
        Changes one or more fields in the ifinfo entry. Rest of the fields
        in the tuple are left unaltered
        fieldVals: array of (fieldIdx, fieldVal)
        NOTE: CALLER IS RESPONSIBLE FOR ACQUIRING THE ifInfoLock
        '''
        ifInfo = self.ifinfo_by_index.get(ifIndex, (None,)*self.IFINFO_TUP_LEN)
        # convert the tuple to a mutable list for easy updating
        ifInfoList = list(ifInfo)
        # update some fields to the new values
        for (fieldIdx, fieldVal) in fieldVals:
            ifInfoList[fieldIdx] = fieldVal
        # convert back to tuple and store in the dict
        self.ifinfo_by_index[ifIndex] = tuple(ifInfoList)

    def set_ifinfo_refresh_pending(self, ifIndex, pending):
        self.ifInfoLock.acquire()
        fieldVals = []
        fieldVals.append((self.IFINFO_IDX_REFRESH_PENDING, pending))
        self.set_ifinfo_fields(ifIndex, fieldVals)
        self.ifInfoLock.release()

    def inc_ifinfo_pending_neigh_count(self, ifIndex):
        self.ifInfoLock.acquire()
        ifInfo = self.ifinfo_by_index.get(ifIndex)
        count = ifInfo[self.IFINFO_IDX_PENDING_NEIGH_COUNT]
        fieldVals = []
        fieldVals.append((self.IFINFO_IDX_PENDING_NEIGH_COUNT, count + 1))
        self.set_ifinfo_fields(ifIndex, fieldVals)
        logger.debug('Increment neigh count %d for ifindex %d' % (count, ifIndex))
        self.ifInfoLock.release()

    def dec_ifinfo_pending_neigh_count(self, ifIndex):
        self.ifInfoLock.acquire()
        ifInfo = self.ifinfo_by_index.get(ifIndex)
        count = ifInfo[self.IFINFO_IDX_PENDING_NEIGH_COUNT]
        if count is 0:
            logger.debug('Decrement for no pending neigh for ifindex %d' % ifIndex)
            self.ifInfoLock.release()
            return
        fieldVals = []
        fieldVals.append((self.IFINFO_IDX_PENDING_NEIGH_COUNT, count - 1))
        self.set_ifinfo_fields(ifIndex, fieldVals)
        logger.debug('Decrement neigh count %d for ifindex %d' % (count, ifIndex))
        self.ifInfoLock.release()

    def get_ifinfo_cache(self):
        '''
        Return a copy of the current contents of the interface cache
        '''
        self.ifInfoLock.acquire()
        retVal = copy.deepcopy(self.ifinfo_by_index)
        self.ifInfoLock.release()
        return retVal

    def get_ifinfo_cache_entry(self, ifindex):
        '''
        Return a copy of the current interface information for the specified ifindex
        '''
        self.ifInfoLock.acquire()
        retVal = self.ifinfo_by_index.get(ifindex, (None,)*self.IFINFO_TUP_LEN)
        self.ifInfoLock.release()
        return retVal

    def get_svi_from_br_port_and_vlan(self, brPort, pkt_vlan):
        '''
        Locates SVI using the bridge member and vlan as key. Returns SVI or
        bridge interface, kind of the bridge port and svi opState.
        '''
        svi = sviState = None

        self.ifInfoLock.acquire()
        # get bridge from brPort (master)
        brPortInfo = self.ifinfo_by_index.get(brPort, (None,)*self.IFINFO_TUP_LEN)
        master = brPortInfo[self.IFINFO_IDX_MASTER]
        if master:
            # find bridge
            brInfo = self.ifinfo_by_index.get(master, (None,)*self.IFINFO_TUP_LEN)
            vlanFilter = brInfo[self.IFINFO_IDX_VLAN_FILTER]
            if vlanFilter:
                # for vlan aware we need locate an SVI@bridge that matches
                # the vlan
                if not pkt_vlan:
                    # use pvid from brPort
                    pkt_vlan = brPortInfo[self.IFINFO_IDX_BRPORT_PVID]
                for ifIndex in self.ifinfo_by_index:
                    ifInfo = self.ifinfo_by_index[ifIndex]
                    kind = ifInfo[self.IFINFO_IDX_KIND]
                    vlan = ifInfo[self.IFINFO_IDX_VLAN]
                    atLink = ifInfo[self.IFINFO_IDX_ATLINK]
                    if kind == 'vlan' and atLink == master and vlan == pkt_vlan:
                        svi = ifIndex
                        sviState = ifInfo[self.IFINFO_IDX_OPSTATE]
                        break
            else:
                kind = brPortInfo[self.IFINFO_IDX_KIND]
                vlan = brPortInfo[self.IFINFO_IDX_VLAN]
                if not pkt_vlan or (kind == 'vlan' and vlan == pkt_vlan):
                    svi = master
                    sviState = brInfo[self.IFINFO_IDX_OPSTATE]
        self.ifInfoLock.release()

        return svi, brPortInfo[self.IFINFO_IDX_KIND], sviState

    def is_addr_in_subnet(self, ifindex, addr):
        if not self.subnetChecks:
            return True

        self.ifInfoLock.acquire()
        ifInfo = self.ifinfo_by_index.get(ifindex)
        self.ifInfoLock.release()
        if not ifInfo:
            return False

        if addr.version is 4:
            idx = self.IFINFO_IDX_IPv4_NETWORKS
        else:
            idx = self.IFINFO_IDX_IPv6_NETWORKS

        if not ifInfo[idx]:
            return False

        for network in ifInfo[idx]:
            if addr in network:
                return True

        return False

    def get_matching_subnet_addr(self, ifindex, ifinfo, addr):
        ifInfo = ifinfo
        # if we already have ifinfo, don't lookup
        if not ifInfo:
            self.ifInfoLock.acquire()
            ifInfo = self.ifinfo_by_index.get(ifindex)
            self.ifInfoLock.release()

        if addr.version is 4:
            idx = self.IFINFO_IDX_IPv4_NETWORKS
        else:
            idx = self.IFINFO_IDX_IPv6_NETWORKS

        sip = None
        if not ifInfo:
            logger.error('ifinfo not found for addr %s' % str(addr))
            return sip

        if not ifInfo[idx]:
            return sip

        for network in ifInfo[idx]:
            if addr in network:
                sip = network.ip
                return sip

        return sip

    def get_intf_link_local_addr(self, ifindex, ifinfo):
        ifInfo = ifinfo
        # if we already have ifinfo, don't lookup
        if not ifInfo:
            self.ifInfoLock.acquire()
            ifInfo = self.ifinfo_by_index.get(ifindex)
            self.ifInfoLock.release()

        lla = None
        if not ifInfo:
            logger.error('ifinfo not found for ifindex %d' % ifindex)
            return lla

        if not ifInfo[self.IFINFO_IDX_IPv6_NETWORKS]:
            return lla

        for network in ifInfo[self.IFINFO_IDX_IPv6_NETWORKS]:
            if network.is_link_local:
                lla = network.ip
                return lla

        return lla

    def get_probe_src_addr(self, ifindex, ifinfo, addr):
        ifInfo = ifinfo
        # if we already have ifinfo, don't lookup
        if not ifInfo:
            self.ifInfoLock.acquire()
            ifInfo = self.ifinfo_by_index.get(ifindex)
            self.ifInfoLock.release()

        # If we have only 1 addr and that is an LLA, use that
        # as SIP while sending NS out. This can happen when neigh
        # gets added to neighmgrd before LLA is added. Also, skip
        # the subnet check as neigh with GUA will not match subnet
        # of an LLA netowrk when there is no GUA network on the
        # interface. Use SIP as LLA in that case.
        networks = []
        sip = None
        if addr.version is 6:
            if not ifInfo[self.IFINFO_IDX_IPv6_NETWORKS]:
                logger.info('no network on interface for %s' % str(addr))
                return sip
            networks = ifInfo[self.IFINFO_IDX_IPv6_NETWORKS]
            # if only a LLA, use it always as SIP
            if len(networks) == 1 and networks[0].is_link_local:
                sip = networks[0].ip
            else:
                # Can we ever have more than 1 networks on the intf?
                # It can because of a separate
                # thread. Thread got its turn after we had 2+ networks
                # on the interface
                # Find matching network and use it as src ip
                sip = self.get_matching_subnet_addr(0, ifInfo, addr)
                # if subnet match fails, should we use LLA as SIP?
                if sip == None:
                    sip = self.get_intf_link_local_addr(0, ifInfo)
                    if sip == None:
                        # dont send a NS probe
                        logger.info('No matching subnet and LLA for %s.' % str(addr))
        else:
            # Find matching network and use it as src ip for ipv4
            sip = self.get_matching_subnet_addr(0, ifInfo, addr)

        # Even now we couldn't find a sip
        # if interface has no ip, instead of using unspec ip as sip, use a valid one
        # this option is configurable
        if sip is None and addr.version is 4 and self.srcIPv4 is not None:
            sip = self.srcIPv4

        return sip

    def is_arp_sup_bridge(self, ifindex):
        self.ifInfoLock.acquire()
        ifInfo = self.ifinfo_by_index.get(ifindex, (None,)*self.IFINFO_TUP_LEN)
        self.ifInfoLock.release()
        kind = ifInfo[self.IFINFO_IDX_KIND]
        arpSup = ifInfo[self.IFINFO_IDX_BRPORT_ARP_SUP] if kind is 'bridge' else None
        return arpSup

    def get_bridge_mbrs(self, bridgeIfIndex):
        mbrs = []
        self.ifInfoLock.acquire()
        for ifIndex in self.ifinfo_by_index:
            ifInfo = self.ifinfo_by_index[ifIndex]
            master = ifInfo[self.IFINFO_IDX_MASTER]
            isBrPort = ifInfo[self.IFINFO_IDX_IS_BRPORT]
            if master == bridgeIfIndex and isBrPort:
                mbrs.append(ifIndex)
        self.ifInfoLock.release()
        return mbrs

    def bridge_arp_snoop_reeval(self, bridgeIfIndex):
        if not bridgeIfIndex:
            return

        self.ifInfoLock.acquire()
        brInfo = self.ifinfo_by_index.get(bridgeIfIndex, (None,)*self.IFINFO_TUP_LEN)
        if brInfo[self.IFINFO_IDX_KIND] != 'bridge':
            # nothing to do in that case
            self.ifInfoLock.release()
            return
        oldBrArpSup = brInfo[self.IFINFO_IDX_BRPORT_ARP_SUP]

        # look for all members in the bridge
        arp_sup_ports = []
        non_arp_sup_ports = []
        # snooping cannot be enabled on some bridge ports
        snoop_black_list = []
        for ifIndex in self.ifinfo_by_index:
            ifInfo = self.ifinfo_by_index[ifIndex]
            ifName = ifInfo[self.IFINFO_IDX_IFNAME]
            master = ifInfo[self.IFINFO_IDX_MASTER]
            isBrPort = ifInfo[self.IFINFO_IDX_IS_BRPORT]
            if not isBrPort or master != bridgeIfIndex:
                continue
            # Enable arpSup on the bridge if it has any port with
            # arpSup enabled
            arpSup = ifInfo[self.IFINFO_IDX_BRPORT_ARP_SUP]
            if arpSup:
                arp_sup_ports.append((ifName,ifIndex))
            else:
                non_arp_sup_ports.append((ifName,ifIndex))
            if ifInfo[self.IFINFO_IDX_BRPORT_PEER_LINK]:
                # snooping cannot be enabled on the peer link
                snoop_black_list.append(ifIndex)

        newBrArpSup = True if arp_sup_ports else False
        if oldBrArpSup != newBrArpSup:
            # update the entry with the new suppress flag
            self.set_ifinfo_fields(bridgeIfIndex, [(self.IFINFO_IDX_BRPORT_ARP_SUP, newBrArpSup)])
        self.ifInfoLock.release()

        if newBrArpSup:
            for (ifName, ifIndex) in arp_sup_ports:
                arpsnoop.del_intf(ifIndex)
            for (ifName, ifIndex) in non_arp_sup_ports:
                if ifIndex not in snoop_black_list:
                    arpsnoop.add_intf(ifIndex, ifName)
                else:
                    arpsnoop.del_intf(ifIndex)
        else:
            # just withdraw all members from the snooper
            for (ifName,ifIndex) in arp_sup_ports + non_arp_sup_ports:
                arpsnoop.del_intf(ifIndex)

    def exec_tc_cmd(self, tc_cmd, op, dev, info=''):
        rc = 0
        out = ''
        cmd = tc_cmd % (op, dev, info)
        try:
            logger.debug("executing cmd '%s'" % (cmd))
            with open("/dev/null", "w") as DEV_NULL:
                out = check_output(cmd.split(), stderr=DEV_NULL)
        except CalledProcessError as e:
            rc = e.returncode
            out = e.output
            # we expect the kernel to return ENOENT if 'get' fails
            if not (op == "get" and rc == ENOENT):
                logger.info("cmd '%s' failed: %d (%s)" % (cmd, rc, out))
        return(rc, out)

    def add_tc_qdisc(self, ifname):
        # check for 'root htb' qdisc with handle 1111:
        rc, out = self.exec_tc_cmd(TC_Q_CMD, "show", ifname)
        if not rc and out and TC_QINFO in out:
            # handle exists
            logger.debug("tc qdisc already set for %s" % ifname)
        else:
            logger.debug("adding tc qdisc for %s" % ifname)
            self.exec_tc_cmd(TC_Q_CMD, "add", ifname, TC_QINFO)

    def add_tc_filter(self, ifname):
        # check for 'fw' filter matching fwmark 1 ('handle 1')
        rc, out = self.exec_tc_cmd(TC_F_CMD, "get", ifname)
        if rc:
            logger.debug("adding tc filter for %s" % ifname)
            self.exec_tc_cmd(TC_F_CMD, "add", ifname, TC_FINFO)
        else:
            logger.debug("tc filter already set for %s" % ifname)

    def add_vxlan_remote_announce_filter(self, ifname):
        self.add_tc_qdisc(ifname)
        self.add_tc_filter(ifname)

    def rx_rtm_newlink_bridge(self, msg):
        self.ifInfoLock.acquire()
        ifInfo = self.ifinfo_by_index.get(msg.ifindex,\
                                 (None,)*self.IFINFO_TUP_LEN)
        fieldVals = []

        oldArpSup = ifInfo[self.IFINFO_IDX_BRPORT_ARP_SUP]
        oldIsBrPort = ifInfo[self.IFINFO_IDX_IS_BRPORT]
        oldIsPeerLink = ifInfo[self.IFINFO_IDX_BRPORT_PEER_LINK]

        # update arpSup and pvid and isBrPort
        isBrPort = True

        protInfo = msg.get_attribute_value(msg.IFLA_PROTINFO, {})
        arpSup = protInfo.get(msg.IFLA_BRPORT_NEIGH_SUPPRESS, oldArpSup)
        isPeerLink = protInfo.get(msg.IFLA_BRPORT_PEER_LINK, oldIsPeerLink)

        afSpec = msg.get_attribute_value(msg.IFLA_AF_SPEC, {})
        vlanInfo = afSpec.get(msg.IFLA_BRIDGE_VLAN_INFO, [])
        # retain old pvid if AF_SPEC attribute was not sent by the kernel
        pvid = None if afSpec else ifInfo[self.IFINFO_IDX_BRPORT_PVID]
        for flags, vlanId in vlanInfo:
            if flags & nlmanager.nlpacket.Link.BRIDGE_VLAN_INFO_PVID:
                pvid = vlanId
                break

        fieldVals.append((self.IFINFO_IDX_BRPORT_ARP_SUP, arpSup))
        fieldVals.append((self.IFINFO_IDX_IS_BRPORT, isBrPort))
        fieldVals.append((self.IFINFO_IDX_BRPORT_PVID, pvid))
        fieldVals.append((self.IFINFO_IDX_BRPORT_PEER_LINK, isPeerLink))
        self.set_ifinfo_fields(msg.ifindex, fieldVals)
        newIfInfo = self.ifinfo_by_index.get(msg.ifindex, (None,)*self.IFINFO_TUP_LEN)
        self.ifInfoLock.release()

        logger.debug("rx af_bridge rtm_newlink %s (%d): %s" %
                     (str(ifInfo[self.IFINFO_IDX_IFNAME]), msg.ifindex, str(newIfInfo)))

        # when a new brPort gets added or arpSup  changes we will re-eval
        # snooping on the bridge
        if oldArpSup != arpSup or oldIsBrPort != isBrPort or oldIsPeerLink != isPeerLink:
            self.bridge_arp_snoop_reeval(newIfInfo[self.IFINFO_IDX_MASTER])

    def rx_rtm_newlink_unspec(self, msg):
        '''
        Create or update information about this interface.
        '''
        self.ifInfoLock.acquire()
        ifInfo = self.ifinfo_by_index.get(msg.ifindex,\
                         (None,)*self.IFINFO_TUP_LEN)
        fieldVals = []
        opState = True if (msg.flags & msg.IFF_LOWER_UP) else False
        kind = ifInfo[self.IFINFO_IDX_KIND]
        vlan = ifInfo[self.IFINFO_IDX_VLAN]
        atLink = ifInfo[self.IFINFO_IDX_ATLINK]
        vlanFilter = ifInfo[self.IFINFO_IDX_VLAN_FILTER]
        oldMaster = ifInfo[self.IFINFO_IDX_MASTER]
        isBrPort = ifInfo[self.IFINFO_IDX_IS_BRPORT]

        # update non-brPort and non-addr attributes
        ifname = msg.get_attribute_value(msg.IFLA_IFNAME)
        lower = msg.get_attribute_value(msg.IFLA_LINK)
        mac = msg.get_attribute_value(msg.IFLA_ADDRESS)
        master = msg.get_attribute_value(msg.IFLA_MASTER)
        atLink = msg.get_attribute_value(msg.IFLA_LINK, None)

        oldKind = kind
        linkinfo = msg.get_attribute_value(msg.IFLA_LINKINFO, {})
        kind = linkinfo.get(msg.IFLA_INFO_KIND, oldKind)

        infoData = linkinfo.get(msg.IFLA_INFO_DATA, {})
        vlan = infoData.get(msg.IFLA_VLAN_ID, vlan)
        vlanFilter = infoData.get(msg.IFLA_BR_VLAN_FILTERING, vlanFilter)

        fieldVals.append((self.IFINFO_IDX_KIND, kind))
        fieldVals.append((self.IFINFO_IDX_IFNAME, ifname))
        fieldVals.append((self.IFINFO_IDX_LOWER, lower))
        fieldVals.append((self.IFINFO_IDX_MAC, mac))
        fieldVals.append((self.IFINFO_IDX_MASTER, master))
        fieldVals.append((self.IFINFO_IDX_VLAN, vlan))
        fieldVals.append((self.IFINFO_IDX_ATLINK, atLink))
        fieldVals.append((self.IFINFO_IDX_VLAN_FILTER, vlanFilter))
        fieldVals.append((self.IFINFO_IDX_OPSTATE, opState))
        if ifInfo[self.IFINFO_IDX_PENDING_NEIGH_COUNT] is None:
            fieldVals.append((self.IFINFO_IDX_PENDING_NEIGH_COUNT, 0))
        self.set_ifinfo_fields(msg.ifindex, fieldVals)
        newIfInfo = self.ifinfo_by_index.get(msg.ifindex, (None,)*self.IFINFO_TUP_LEN)
        self.ifinfo_by_index[msg.ifindex] = newIfInfo
        self.ifInfoLock.release()
        logger.debug("rx af_unspec rtm_newlink %s (%d): %s" %
                     (str(ifname), msg.ifindex, str(newIfInfo)))

        if kind == 'bridge':
            # when a bridge is added reeval snooping on it
            if oldKind != kind:
                self.bridge_arp_snoop_reeval(msg.ifindex)
        elif isBrPort or kind == 'vxlan':
            if isBrPort:
                # if master changes we need to reeval on old and new masters
                # if they are bridges. XXX - do we really need to handle this
                # separately?
                if oldMaster != master:
                    self.bridge_arp_snoop_reeval(oldMaster)
                    # if the new master goes to None I also expect a AF_BRIDGE
                    # del. we will withdraw the interface from the snooper at
                    # that point.
                    self.bridge_arp_snoop_reeval(master)
            if kind == 'vxlan' and opState == True:
                # if we've received an rtm_newlink for a vxlan interface, we
                # need to set a tc filter to drop proxy ARP/NDP announcements.
                self.add_vxlan_remote_announce_filter(ifname)

    def rx_rtm_newlink(self, msg):
        if msg.family == socket.AF_UNSPEC:
            self.rx_rtm_newlink_unspec(msg)
        elif msg.family == socket.AF_BRIDGE:
            self.rx_rtm_newlink_bridge(msg)
        else:
            logger.info("rx rtm_newlink with unknown family %d" % msg.family)

    def rx_rtm_dellink_bridge(self, msg):
        '''
        Remove brPort attributes from the ifinfo cache
        '''
        self.ifInfoLock.acquire()
        ifInfo = self.ifinfo_by_index.get(msg.ifindex, (None,)*self.IFINFO_TUP_LEN)
        arpSup = ifInfo[self.IFINFO_IDX_BRPORT_ARP_SUP]
        if msg.ifindex in self.ifinfo_by_index:
            # clear all the bridge port attributes
            fieldVals = []
            fieldVals.append((self.IFINFO_IDX_BRPORT_ARP_SUP, None))
            fieldVals.append((self.IFINFO_IDX_IS_BRPORT, None))
            fieldVals.append((self.IFINFO_IDX_BRPORT_PVID, None))
            fieldVals.append((self.IFINFO_IDX_BRPORT_PEER_LINK, None))
            self.set_ifinfo_fields(msg.ifindex, fieldVals)
            ifInfo = self.ifinfo_by_index.get(msg.ifindex, (None,)*self.IFINFO_TUP_LEN)
        self.ifInfoLock.release()
        logger.debug("rx af_bridge rtm_dellink %s (%d): %s" %
                    (ifInfo[self.IFINFO_IDX_IFNAME],
                    msg.ifindex, str(ifInfo)))

        if arpSup:
            self.bridge_arp_snoop_reeval(ifInfo[self.IFINFO_IDX_MASTER])
        # blanket call to cover all cases. if a brPort doesn't exist it
        # shouldn't be registered with the snooper.
        arpsnoop.del_intf(msg.ifindex)

    def rx_rtm_dellink_unspec(self, msg):
        '''
        Remove information about this interface.
        '''
        self.ifInfoLock.acquire()
        ifInfo = self.ifinfo_by_index.get(msg.ifindex, (None,)*self.IFINFO_TUP_LEN)
        ifDelDone = False
        if msg.ifindex in self.ifinfo_by_index:
            ifDelDone = True
            del self.ifinfo_by_index[msg.ifindex]
        self.ifInfoLock.release()
        logger.debug("rx af_unspec rtm_dellink %s (%d): %s" %
                     (ifInfo[self.IFINFO_IDX_IFNAME], msg.ifindex,
                     str(ifInfo)))

        if ifDelDone:
            kind = ifInfo[self.IFINFO_IDX_KIND]
            arpSup = ifInfo[self.IFINFO_IDX_BRPORT_ARP_SUP]
            isBrPort = ifInfo[self.IFINFO_IDX_IS_BRPORT]
            master = ifInfo[self.IFINFO_IDX_MASTER]

            if kind == 'bridge':
                # This is not possible i.e. a bridge cannot be deleted while
                # there are br members referencing it but just in case
                if arpSup:
                    logger.warning("del arp-sup bridge link %d: %s" %
                                        (msg.ifindex, str(ifInfo)))
                    mbrs =  self.get_bridge_mbrs(msg.ifindex)
                    for mbr in mbrs:
                        arpsnoop.del_intf(mbr)
            elif isBrPort:
                if arpSup:
                    self.bridge_arp_snoop_reeval(master)

            # blanket call to cover all cases. if a brPort doesn't exist it
            # shouldn't be registered with the snooper.
            arpsnoop.del_intf(msg.ifindex)

    def rx_rtm_dellink(self, msg):
        if msg.family == socket.AF_UNSPEC:
            self.rx_rtm_dellink_unspec(msg)
        elif msg.family == socket.AF_BRIDGE:
            self.rx_rtm_dellink_bridge(msg)
        else:
            logger.info("rx rtm_dellink with unknown family %d" % msg.family)

    def add_network(self, msg):
        self.ifInfoLock.acquire()
        ifInfo = self.ifinfo_by_index.get(msg.ifindex)
        if not ifInfo:
            self.ifInfoLock.release()
            return

        # in case of a peer address (point-to-point or unnumbered) IFA_LOCAL
        # will contain local intf address and IFA_ADDRESS will contain peer
        # address. In normal case without peer addr, IFA_ADDRESS will contain
        # local interface address
        addr = msg.get_attribute_value(msg.IFA_LOCAL)
        peer = msg.get_attribute_value(msg.IFA_ADDRESS)
        # logic copied from iproute2/ip/ipaddress.c:print_addrinfo()
        if not addr:
            addr = peer
        if msg.family == socket.AF_INET:
            idx = self.IFINFO_IDX_IPv4_NETWORKS
        elif msg.family == socket.AF_INET6:
            idx = self.IFINFO_IDX_IPv6_NETWORKS

        logger.debug("add_network for %s (%d): %s/%d" %
                     (ifInfo[self.IFINFO_IDX_IFNAME], msg.ifindex,
                     addr, msg.prefixlen))
        key = -1
        newNet = ipaddr.IPNetwork(str(addr) + ("/%d"%msg.prefixlen))
        found = False
        if ifInfo[idx] is None:
            fieldVals = []
            fieldVals.append((idx, []))
            self.set_ifinfo_fields(msg.ifindex, fieldVals)
            ifInfo = self.ifinfo_by_index.get(msg.ifindex)

        # check if we have a connected network with the same address
        for net in ifInfo[idx]:
            key += 1
            if addr in net:
                found = True
                break

        if found:
            if ifInfo[idx][key] != newNet:
                # if addr exists but prefixlen is different
                ifInfo[idx][key] = newNet
        else:
            # if addr does not exist
            ifInfo[idx].append(newNet)

        self.ifInfoLock.release()

    def del_network(self, msg):
        self.ifInfoLock.acquire()
        ifInfo = self.ifinfo_by_index.get(msg.ifindex)
        if not ifInfo:
            self.ifInfoLock.release()
            return

        # in case of a peer address (point-to-point or unnumbered) IFA_LOCAL
        # will contain local intf address and IFA_ADDRESS will contain peer
        # address. In normal case without peer addr, IFA_ADDRESS will contain
        # local interface address
        addr = msg.get_attribute_value(msg.IFA_LOCAL)
        peer = msg.get_attribute_value(msg.IFA_ADDRESS)
        # logic copied from iproute2/ip/ipaddress.c:print_addrinfo()
        if not addr:
            addr = peer
        if msg.family == socket.AF_INET:
            idx = self.IFINFO_IDX_IPv4_NETWORKS
        elif msg.family == socket.AF_INET6:
            idx = self.IFINFO_IDX_IPv6_NETWORKS

        logger.debug("del_network for %s (%d): %s/%d" %
                     (ifInfo[self.IFINFO_IDX_IFNAME], msg.ifindex,
                     addr, msg.prefixlen))
        key = -1
        found = False
        # check if we have a connected network with the same address
        if ifInfo[idx]:
            for net in ifInfo[idx]:
                key += 1
                if addr == net.ip:
                    found = True
                    break

        if found:
            ifInfo[idx].pop(key)
        self.ifInfoLock.release()

    def proc_pending_neigh_refreshes(self, ifindex, ifInfo):
        #if not ifInfo[self.IFINFO_IDX_REFRESH_PENDING]:
        #    return
        if ifInfo[self.IFINFO_IDX_PENDING_NEIGH_COUNT] is 0:
            return

        ifName = ifInfo[self.IFINFO_IDX_IFNAME]
        logger.info("process deferred v6 neigh refresh on %s" % ifName)
        # Meaning of this stat has changed after pending neigh count usage
        self.stats[self.STAT_IDX_DEFERRED_V6_REFRESHES] +=1
        #self.set_ifinfo_refresh_pending(ifindex, pending=False)
        self.neighInfoLock.acquire()
        for (neighIfindex, neighIpaddr), neighInfo in self.neigh_by_index_ip.iteritems():
            if neighIfindex != ifindex:
                continue
            if neighIpaddr.version is 4:
                continue
            isExt = neighInfo[self.NEIGH_INFO_IDX_IS_EXT]
            state = neighInfo[self.NEIGH_INFO_IDX_STATE]
            if not isExt\
                and not (state & nlmanager.nlpacket.Neighbor.NUD_REACHABLE)\
                and ifName and not ifName.startswith("eth"):
                fieldIdx = self.NEIGH_INFO_IDX_REACH_RETRIES
                retries = neighInfo[fieldIdx]
                if retries < self.neighReachDampenTh:
                    self.set_neigh_info_fields(ifindex, neighIpaddr,\
                                              [(fieldIdx, retries+1)])
                    self.add_neigh_work(ifindex, neighIpaddr,\
                                    self.NEIGH_WORKQ_IDX_STALE_REFRESH,\
                                    "v6-addr-add", ifName)
                else:
                    self.stats[self.STAT_IDX_NEIGH_MOD_DAMPENS] +=1
        self.neighInfoLock.release()

    def rx_rtm_newaddr(self, msg):
        '''
        Add information about the address to this interface.
        '''
        self.ifInfoLock.acquire()
        ifInfo = self.ifinfo_by_index.get(msg.ifindex, (None,)*self.IFINFO_TUP_LEN)

        ipv4 = ifInfo[self.IFINFO_IDX_IPv4]
        ipv6 = ifInfo[self.IFINFO_IDX_IPv6]
        # in case of a peer address (point-to-point or unnumbered) IFA_LOCAL
        # will contain local intf address and IFA_ADDRESS will contain peer
        # address. In normal case without peer addr, IFA_ADDRESS will contain
        # local interface address
        addr = msg.get_attribute_value(msg.IFA_LOCAL)
        peer = msg.get_attribute_value(msg.IFA_ADDRESS)
        # logic copied from iproute2/ip/ipaddress.c:print_addrinfo()
        if not addr:
            addr = peer
        if msg.family == socket.AF_INET:
            ipv4 = addr
        elif msg.family == socket.AF_INET6:
            ipv6 = addr
        fieldVals = []
        fieldVals.append((self.IFINFO_IDX_IPv4, ipv4))
        fieldVals.append((self.IFINFO_IDX_IPv6, ipv6))
        self.set_ifinfo_fields(msg.ifindex, fieldVals)
        newIfInfo = self.ifinfo_by_index.get(msg.ifindex, (None,)*self.IFINFO_TUP_LEN)
        self.ifInfoLock.release()
        logger.debug("rx rtm_newaddr for %s (%d): %s" %
                     (newIfInfo[self.IFINFO_IDX_IFNAME], msg.ifindex, str(newIfInfo)))
        self.add_network(msg)
        # if new ipv6 addr, check and refresh any pending nighs. This can
        # happen when ip on intf getting received at neighmgrd after a
        # neigh getting added. Ip addrs also can get received in any order.
        # Hence on every new addr add check if any pending neigh (matching
        # subnet with this addr) and refresh those
        if ipv6:
            self.proc_pending_neigh_refreshes(msg.ifindex, newIfInfo)

    def rx_rtm_deladdr(self, msg):
        '''
        Remove information about the address from this interface.
        '''
        self.ifInfoLock.acquire()
        ifInfo = self.ifinfo_by_index.get(msg.ifindex, (None,)*self.IFINFO_TUP_LEN)
        ipv4 = ifInfo[self.IFINFO_IDX_IPv4]
        ipv6 = ifInfo[self.IFINFO_IDX_IPv6]
        # in case of a peer address (point-to-point or unnumbered) IFA_LOCAL
        # will contain local intf address and IFA_ADDRESS will contain peer
        # address. In normal case without peer addr, IFA_ADDRESS will contain
        # local interface address
        ipaddr = msg.get_attribute_value(msg.IFA_LOCAL)
        peer = msg.get_attribute_value(msg.IFA_ADDRESS)
        # logic copied from iproute2/ip/ipaddress.c:print_addrinfo()
        if not ipaddr:
            ipaddr = peer
        if msg.family == socket.AF_INET and ipaddr == ipv4:
            ipv4 = None
        elif msg.family == socket.AF_INET6 and ipaddr == ipv6:
            ipv6 = None
        fieldVals = []
        fieldVals.append((self.IFINFO_IDX_IPv4, ipv4))
        fieldVals.append((self.IFINFO_IDX_IPv6, ipv6))
        self.set_ifinfo_fields(msg.ifindex, fieldVals)
        newIfInfo = self.ifinfo_by_index.get(msg.ifindex, (None,)*self.IFINFO_TUP_LEN)
        self.ifInfoLock.release()
        logger.debug("rx rtm_deladdr for %s (%d): %s" %
                     (newIfInfo[self.IFINFO_IDX_IFNAME], msg.ifindex, str(newIfInfo)))
        self.del_network(msg)

    def stat_update_neigh_add(self, version, isVrr, isExt, mod, isExtMod):
        statIdx1 = -1
        statIdx2 = -1
        if version == 4:
            if isVrr:
                if mod:
                    statIdx = self.STAT_IDX_MOD_VRR_NEIGHv4
                else:
                    statIdx = self.STAT_IDX_ADD_VRR_NEIGHv4
                    statIdx1 = self.STAT_IDX_VRR_NEIGHv4
            elif isExt:
                if mod:
                    statIdx = self.STAT_IDX_MOD_EXT_NEIGHv4
                    if isExtMod:
                        # change from local to ext
                        # add to ext
                        statIdx1 = self.STAT_IDX_EXT_NEIGHv4
                        # remove from ext
                        statIdx2 = self.STAT_IDX_NEIGHv4
                else:
                    statIdx = self.STAT_IDX_ADD_EXT_NEIGHv4
                    statIdx1 = self.STAT_IDX_EXT_NEIGHv4
            else:
                if mod:
                    statIdx = self.STAT_IDX_MOD_NEIGHv4
                    if isExtMod:
                        # change from ext to local
                        # add to local
                        statIdx1 = self.STAT_IDX_NEIGHv4
                        # remove from ext
                        statIdx2 = self.STAT_IDX_EXT_NEIGHv4
                else:
                    statIdx = self.STAT_IDX_ADD_NEIGHv4
                    statIdx1 = self.STAT_IDX_NEIGHv4
        else:
            if isVrr:
                if mod:
                    statIdx = self.STAT_IDX_MOD_VRR_NEIGHv6
                else:
                    statIdx = self.STAT_IDX_ADD_VRR_NEIGHv6
                    statIdx1 = self.STAT_IDX_VRR_NEIGHv6
            elif isExt:
                if mod:
                    statIdx = self.STAT_IDX_MOD_EXT_NEIGHv6
                    if isExtMod:
                        # change from local to ext
                        # add to ext
                        statIdx1 = self.STAT_IDX_EXT_NEIGHv4
                        # remove from ext
                        statIdx2 = self.STAT_IDX_NEIGHv4
                else:
                    statIdx = self.STAT_IDX_ADD_EXT_NEIGHv6
                    statIdx1 = self.STAT_IDX_EXT_NEIGHv6
            else:
                if mod:
                    statIdx = self.STAT_IDX_MOD_NEIGHv6
                    if isExtMod:
                        # change from ext to local
                        # add to local
                        statIdx1 = self.STAT_IDX_NEIGHv6
                        # remove from ext
                        statIdx2 = self.STAT_IDX_EXT_NEIGHv6
                else:
                    statIdx = self.STAT_IDX_ADD_NEIGHv6
                    statIdx1 = self.STAT_IDX_NEIGHv6
        self.stats[statIdx] += 1
        if statIdx1 != -1:
            self.stats[statIdx1] += 1
        if statIdx2 != -1 and self.stats[statIdx2] > 0:
            self.stats[statIdx2] -= 1

    def stat_update_neigh_del(self, version, isVrr, isExt):
        if version == 4:
            if isVrr:
                statIdx = self.STAT_IDX_DEL_VRR_NEIGHv4
                statIdx1 = self.STAT_IDX_VRR_NEIGHv4
            elif isExt:
                statIdx = self.STAT_IDX_DEL_EXT_NEIGHv4
                statIdx1 = self.STAT_IDX_EXT_NEIGHv4
            else:
                statIdx = self.STAT_IDX_DEL_NEIGHv4
                statIdx1 = self.STAT_IDX_NEIGHv4
        else:
            if isVrr:
                statIdx = self.STAT_IDX_DEL_VRR_NEIGHv6
                statIdx1 = self.STAT_IDX_VRR_NEIGHv6
            elif isExt:
                statIdx = self.STAT_IDX_DEL_EXT_NEIGHv6
                statIdx1 = self.STAT_IDX_EXT_NEIGHv6
            else:
                statIdx = self.STAT_IDX_DEL_NEIGHv6
                statIdx1 = self.STAT_IDX_NEIGHv6
        self.stats[statIdx] += 1
        if self.stats[statIdx1] > 0:
            self.stats[statIdx1] -= 1

    def rx_rtm_newneigh(self, msg):
        '''
        Add information about the neighbor. Send an ARP/NS if necessary.
        '''
        neighIpaddr = msg.get_attribute_value(msg.NDA_DST)
        neighNewmac = msg.get_attribute_value(msg.NDA_LLADDR)
        interestingState = (msg.state & (msg.NUD_STALE|msg.NUD_PROBE|\
                            msg.NUD_DELAY|msg.NUD_REACHABLE|msg.NUD_NOARP))\
                            and not (msg.state & msg.NUD_PERMANENT)
        isExt = True if (msg.flags & msg.NTF_EXT_LEARNED) else False
        if neighIpaddr is not None and neighNewmac is not None and interestingState:
            self.ifInfoLock.acquire()
            # XXX - access of ifinfo_by_index fields should be via IFINFO_IDX_xxx
            (upKind, upIfname, upLower, upMac, upIpv4, upIpv6) = self.ifinfo_by_index.get(msg.ifindex, (None,)*self.IFINFO_TUP_LEN)[:6]
            (loKind, loIfname, loLower, loMac, loIpv4, loIpv6) = self.ifinfo_by_index.get(upLower, (None,)*self.IFINFO_TUP_LEN)[:6]
            self.ifInfoLock.release()
            logger.debug("rx rtm_newneigh %s (%d): (%s %s ext:%s 0x%x)" %
                         (str(upIfname), msg.ifindex, str(neighIpaddr),\
                         str(neighNewmac), isExt, msg.state))
            if upIfname is "lo" or upIfname is "mgmt":
                logger.info("rx_rtm_newneigh ignoring neigh %s mac %s on reserved dev %s " %
                        (str(neighIpaddr), str(neighNewmac), upIfname))
                return
            self.neighInfoLock.acquire()
            self.del_neigh_invalid(msg.ifindex, neighIpaddr)
            neighInfo = self.neigh_by_index_ip.get((msg.ifindex, neighIpaddr),\
                            [None,]*self.NEIGH_INFO_LIST_LEN)
            neighOldmac = neighInfo[self.NEIGH_INFO_IDX_MAC]
            oldExt = neighInfo[self.NEIGH_INFO_IDX_IS_EXT]
            neighInfo[self.NEIGH_INFO_IDX_MAC] = neighNewmac
            neighInfo[self.NEIGH_INFO_IDX_IS_EXT] = isExt
            isRouter = True if (msg.flags & msg.NTF_ROUTER) else False
            neighInfo[self.NEIGH_INFO_IDX_IS_ROUTER] = isRouter
            neighInfo[self.NEIGH_INFO_IDX_STATE] = msg.state
            if neighInfo[self.NEIGH_INFO_IDX_REACH_RETRIES] is None:
                neighInfo[self.NEIGH_INFO_IDX_REACH_RETRIES] = 0
            self.neigh_by_index_ip[(msg.ifindex, neighIpaddr)] = neighInfo
            neighLoInfo = self.neigh_by_index_ip.get((upLower, neighIpaddr), [None,]*self.NEIGH_INFO_LIST_LEN)
            # Is the upper interface a macvlan, and do we have interface names
            # for both the upper and lower interfaces?
            if upKind == 'macvlan' and upIfname is not None and loIfname is not None:
                # if neigh on lower(svi) intf was not existing, we will need to create it and
                # store in the neigh dict so that flags like NEIGH_INFO_PENDING_REFRESH can be
                # stored. This flag is used when a neigh is learnt on VRR intf and it is also
                # refreshed on lower svi. If svi doesn't have an ipv6 addr, a pending flag
                # needs to persist so that neigh can be refreshed on ipv6 addr addition.
                # fill in some mandatory dumy details from neigh on upper interface
                if neighLoInfo[self.NEIGH_INFO_IDX_IS_EXT] is None:
                    neighLoInfo[self.NEIGH_INFO_IDX_IS_EXT] = isExt
                if neighLoInfo[self.NEIGH_INFO_IDX_IS_ROUTER] is None:
                    neighLoInfo[self.NEIGH_INFO_IDX_IS_ROUTER] = isRouter
                if neighLoInfo[self.NEIGH_INFO_IDX_STATE] is None:
                    neighLoInfo[self.NEIGH_INFO_IDX_STATE] = msg.state
                if neighLoInfo[self.NEIGH_INFO_IDX_REACH_RETRIES] is None:
                    neighLoInfo[self.NEIGH_INFO_IDX_REACH_RETRIES] = 0
                self.neigh_by_index_ip[(upLower, neighIpaddr)] = neighLoInfo
            neighLomac = neighLoInfo[self.NEIGH_INFO_IDX_MAC]
            # refresh local entries that are not-reachable yet if -
            # 1. new (or)
            # 2. there is a mac address change (or)
            # 3. entry moved from remote to local
            if not isExt\
                and ((neighOldmac != neighNewmac) or oldExt)\
                and not (msg.state & msg.NUD_REACHABLE)\
                and upIfname and not upIfname.startswith("eth"):
                # what if we get an LLA neigh and we have not yet received the LLA
                # on the interface? Defer refresh to the receipt of LLA
                matchingSip = self.get_probe_src_addr(msg.ifindex, None, neighIpaddr)
                # ipv6 neighbors cannot be refreshed without an ipv6 address
                if neighIpaddr.version == 6 and not matchingSip and\
                    not upKind == "macvlan":
                    logger.info("refresh %s on interface %s deferred;"\
                             " no v6 addresses" % (str(neighIpaddr), upIfname))
                    #self.set_ifinfo_refresh_pending(msg.ifindex, pending=True)
                    self.inc_ifinfo_pending_neigh_count(msg.ifindex)
                    self.set_neigh_info_fields(msg.ifindex, neighIpaddr,\
                                                [(self.NEIGH_INFO_PENDING_REFRESH, True)])
                else:
                    fieldIdx = self.NEIGH_INFO_IDX_REACH_RETRIES
                    retries = neighInfo[fieldIdx]
                    if retries < self.neighReachDampenTh:
                        self.set_neigh_info_fields(msg.ifindex, neighIpaddr,\
                                                   [(fieldIdx, retries+1)])
                        self.add_neigh_work(msg.ifindex, neighIpaddr,\
                                            self.NEIGH_WORKQ_IDX_STALE_REFRESH,\
                                            "neigh-update", upIfname)
                    else:
                        self.stats[self.STAT_IDX_NEIGH_MOD_DAMPENS] +=1
            self.neighInfoLock.release()

            isMod = False if neighOldmac == None else True
            if isMod:
                isExtMod = True if isExt != oldExt else False
            else:
                isExtMod = False
            self.stat_update_neigh_add(neighIpaddr.version,\
                                 upKind == 'macvlan', isExt, isMod, isExtMod)

            # If the update is for a remote neigh entry we need to announce it
            # to the local/access ports by sending a gratituous ARP request.
            # We do the announce for the following cases -
            # 1. new remote neigh
            # 2. mac address of a remote neigh changed
            # 3. neigh type changed from local to remote
            if isExt and (neighOldmac != neighNewmac or oldExt != isExt):
                neighMacPacked = nm_pkt.mactobinary(neighNewmac)
                if neighIpaddr.version == 4:
                    logger.info("sending ext arp announce for %s on %s" % (str(neighIpaddr), upIfname))
                    ArpSend(_ARP_REQUEST, msg.ifindex, neighMacPacked, neighIpaddr.packed, _ETH_BROADCAST, neighIpaddr.packed, True)
                elif neighIpaddr.version == 6:
                    logger.info("sending ext na announce for %s on %s" % (str(neighIpaddr), upIfname))
                    Icmp6GratNASend(msg.ifindex, neighIpaddr, neighMacPacked, isRouter)

            # Has the MAC address of the neighbor changed?
            if neighOldmac != neighNewmac:
                # Is the upper interface a macvlan, and do we have interface names
                # for both the upper and lower interfaces?
                if upKind == 'macvlan' and upIfname is not None and loIfname is not None:
                    # Do the names of the upper and lower interfaces match VRR naming?
                    (base, sep, idx) = upIfname.partition("-v")
                    if base == loIfname.replace(".", "-") and idx.isdigit():
                        # Is the MAC of the neighbor on the lower interface different than the
                        # MAC of the neighbor on the upper interface for the same IP?
                        if neighLomac != neighNewmac:
                            loMacPacked = nm_pkt.mactobinary(loMac)
                            # Send an ARP request (IPv4) or Neighbor Solicitation (IPv6)
                            if neighIpaddr.version == 4 and loMacPacked is not None:
                                logger.info("sending arp to refresh %s on %s" % (str(neighIpaddr), loIfname))
                                # Find matching network and use it as src ip
                                srcIp = self.get_probe_src_addr(upLower, None, neighIpaddr)
                                sip = _IPV4_UNSPEC_ADDR if srcIp is None else srcIp.packed
                                ArpSend(_ARP_REQUEST, upLower, loMacPacked, sip, _ETH_BROADCAST, neighIpaddr.packed, False)
                            elif neighIpaddr.version == 6 and loMacPacked is not None:
                                logger.info("sending ns to refresh %s on %s" % (str(neighIpaddr), loIfname))
                                # Find matching network and use it as src ip
                                srcIp = self.get_probe_src_addr(upLower, None, neighIpaddr)
                                sip = _IPV6_UNSPEC_ADDR if srcIp is None else srcIp.packed
                                # ipv6 neighbors cannot be refreshed without a matching ipv6 network on iface
                                if srcIp is None:
                                    logger.info("refresh %s on svi interface %s deferred;"\
                                             " no matching v6 addresses" % (str(neighIpaddr), loIfname))
                                    self.neighInfoLock.acquire()
                                    self.inc_ifinfo_pending_neigh_count(upLower)
                                    self.set_neigh_info_fields(upLower, neighIpaddr,\
                                                                [(self.NEIGH_INFO_PENDING_REFRESH, True)])
                                    self.neighInfoLock.release()
                                else:
                                    NsSend(upLower, loMacPacked, sip, neighIpaddr.packed)
        else:
            # if the neigh entry is no longer interesting to us del
            # it. The main reason for doing this is because the neigh entry
            # stays in the failed state for a long time. Now when FRR comes
            # along and refreshes it we are not catching that state change.
            logger.debug("rx rtm_newneigh %d: (%s %s ext:%s 0x%x)" %
                         (msg.ifindex, str(neighIpaddr),\
                          str(neighNewmac), isExt, msg.state))
            self.del_neigh(msg, badState=True)

    def add_neigh_invalid(self, ifindex, ipaddr, state):
        logger.debug("add invalid neigh %d: (%s 0x%x)" %
                     (ifindex, str(ipaddr), state))
        self.neigh_invalid_db[(ifindex, ipaddr)] = state

    def del_neigh_invalid(self, ifindex, ipaddr):
        try:
            del self.neigh_invalid_db[(ifindex, ipaddr)]
            logger.debug("del invalid neigh %d: %s" % (ifindex, str(ipaddr)))
        except KeyError:
            pass

    def del_neigh(self, msg, badState):
        '''
        Remove information about the neighbor.
        '''
        ipaddr = msg.get_attribute_value(msg.NDA_DST)
        logger.debug("del neigh %d: %s" % (msg.ifindex, str(ipaddr)))
        self.neighInfoLock.acquire()
        interestingState = (msg.state & (msg.NUD_INCOMPLETE|msg.NUD_FAILED))\
                            and not (msg.state & msg.NUD_PERMANENT)
        removed = False
        if badState and interestingState:
            self.add_neigh_invalid(msg.ifindex, ipaddr, msg.state)
        else:
            self.del_neigh_invalid(msg.ifindex, ipaddr)
        try:
            neighInfo = self.neigh_by_index_ip.pop((msg.ifindex, ipaddr))
            removed = self.del_neigh_work(msg.ifindex, ipaddr,\
                                    self.NEIGH_WORKQ_IDX_STALE_REFRESH)
            if removed and neighInfo[self.NEIGH_INFO_PENDING_REFRESH] is not None \
                    and neighInfo[self.NEIGH_INFO_PENDING_REFRESH]:
                self.dec_ifinfo_pending_neigh_count(msg.ifindex)
        except KeyError:
            neighInfo = None
        self.neighInfoLock.release()

        if neighInfo:
            ifInfo = self.get_ifinfo_cache_entry(msg.ifindex)
            ifKind = ifInfo[self.IFINFO_IDX_KIND]
            self.stat_update_neigh_del(ipaddr.version, ifKind == 'macvlan',\
                                       neighInfo[self.NEIGH_INFO_IDX_IS_EXT])

    def rx_rtm_delneigh(self, msg):
        ipaddr = msg.get_attribute_value(msg.NDA_DST)
        self.del_neigh(msg, badState=False)
        logger.debug("rx rtm_delneigh %d: %s" %
                     (msg.ifindex, str(ipaddr)))

    def apply_config(self):
        logCfg = GetConfigValue('main', 'logging', self.cmdargs.loglevel)
        logger.setLevel(logCfg.upper())

        # non-zero value will enable subnet checks in the snooper i.e.
        # neigh will only be added if the packet's SIP is one of the SVI's
        # subnets
        self.subnetChecks = int(GetConfigValue('snooper', 'subnet_checks', 0))
        # if interface has no ip, instead of using unspec ip as sip, use a valid one
        # this option is configurable
        srcIPv4 = GetConfigValue('main', 'setsrcipv4', None)
        try:
            if srcIPv4 is not None:
                if srcIPv4 == "None":
                    self.srcIPv4 = None
                else:
                    self.srcIPv4 = ipaddr.IPv4Address(srcIPv4)
        except ipaddr.AddressValueError:
            logger.error("setsrcipv4 read failed."
                    " Addr format issue - %s" % str(srcIPv4))

    def reload_config(self):
        '''
        Reload the config from neighmgr.conf
        '''
        try:
            confParser.readfp(open(_NEIGHMGR_CONF_FILE))
        except IOError:
            pass

        self.apply_config()

        self.stats[self.STAT_IDX_CONFIG_RELOADS] +=1
        logger.warning('reloaded neighmgr config')

    def resync(self):
        '''
        Flushes the local caches for kernel tables and restarts the
        nllistener thread. Returns True if a shutdown request is detected
        during resync.
        '''
        if self.resync_in_prog:
            return False

        # XXX: should probably add some backoff mechanism here to handle
        # repeated/back-to-back overflows
        self.resync_in_prog = True
        self.stats[self.STAT_IDX_RESYNCS] +=1
        logger.warning('resync with kernel started')

        # shutdown the listener
        self.listener.shutdown_event.set()
        self.listener.join()

        # stop snooping on any/all interfaces
        arpsnoop.del_all_intfs()

        # flush kernel caches
        self.neighInfoLock.acquire()
        self.neigh_by_index_ip.clear()
        self.neighWorkQDedup.clear()
        self.neighWorkQ.queue.clear()
        self.clear_stat(curr_cnt=True)
        self.neighInfoLock.release()

        self.ifInfoLock.acquire()
        self.ifinfo_by_index.clear()
        self.ifInfoLock.release()

        # flush the current workq - if shutdown is detected just bail
        while True:
            try:
                (event, options) = self.workq.get_nowait()
            except Queue.Empty:
                break
            if event == 'SHUTDOWN':
                return True

        # re-init the listener
        self.restart_listener()

        # redump
        nlm.workq.put(('GET_ALL_LINKS', None))
        nlm.workq.put(('GET_ALL_ADDRESSES', None))
        nlm.workq.put(('GET_ALL_NEIGHBORS', None))

        logger.warning('resync with kernel complete')
        self.resync_in_prog = False

        return False

    def service_overflow(self):
        logger.warning('netlink overflow')
        return self.resync()

    def service_netlinkq(self):
        '''
        Handle the message on the netlink queue.
        '''
        msg_count = {}
        processed = 0

        for (msgtype, length, flags, seq, pid, data) in self.netlinkq:
            processed += 1

            # If this is a reply to a TX message that debugs were enabled for then debug the reply
            if (seq, pid) in self.debug_seq_pid:
                debug = True
            else:
                debug = self.debug_this_packet(msgtype)

            if msgtype == nlmanager.nlpacket.RTM_NEWLINK or msgtype == nlmanager.nlpacket.RTM_DELLINK:
                msg = nlmanager.nlpacket.Link(msgtype, debug, logger, self.use_color)

            elif msgtype == nlmanager.nlpacket.RTM_NEWADDR or msgtype == nlmanager.nlpacket.RTM_DELADDR:
                msg = nlmanager.nlpacket.Address(msgtype, debug, logger, self.use_color)

            elif msgtype == nlmanager.nlpacket.RTM_NEWNEIGH or msgtype == nlmanager.nlpacket.RTM_DELNEIGH:
                msg = nlmanager.nlpacket.Neighbor(msgtype, debug, logger, self.use_color)

            elif msgtype == nlmanager.nlpacket.RTM_NEWROUTE or msgtype == nlmanager.nlpacket.RTM_DELROUTE:
                msg = nlmanager.nlpacket.Route(msgtype, debug, logger, self.use_color)

            else:
                logger.warning('rx unknown netlink message type %s' % msgtype)
                continue

            msg.decode_packet(length, flags, seq, pid, data)

            if not self.filter_permit(msg):
                continue

            if debug:
                msg.dump()

            # Only used for printing debugs about how many we RXed of each type
            if msg.msgtype not in msg_count:
                msg_count[msg.msgtype] = 0
            msg_count[msg.msgtype] += 1

            # Call the appropriate handler method based on the msgtype.  The handler
            # functions are defined in our child class.
            if msg.msgtype == nlmanager.nlpacket.RTM_NEWLINK:
                self.rx_rtm_newlink(msg)

            elif msg.msgtype == nlmanager.nlpacket.RTM_DELLINK:
                self.rx_rtm_dellink(msg)

            elif msg.msgtype == nlmanager.nlpacket.RTM_NEWADDR:
                self.rx_rtm_newaddr(msg)

            elif msg.msgtype == nlmanager.nlpacket.RTM_DELADDR:
                self.rx_rtm_deladdr(msg)

            elif msg.msgtype == nlmanager.nlpacket.RTM_NEWNEIGH:
                self.rx_rtm_newneigh(msg)

            elif msg.msgtype == nlmanager.nlpacket.RTM_DELNEIGH:
                self.rx_rtm_delneigh(msg)

            elif msg.msgtype == nlmanager.nlpacket.RTM_NEWROUTE:
                self.rx_rtm_newroute(msg)

            elif msg.msgtype == nlmanager.nlpacket.RTM_DELROUTE:
                self.rx_rtm_delroute(msg)

            else:
                logger.warning('rx unknown netlink message type %s' % msgtype)

        if processed:
            self.netlinkq = self.netlinkq[processed:]

#-------------------------------------------------------------------------------
#
#   Threads
#
#-------------------------------------------------------------------------------
def NeighborRefresh():
    """
    This function refreshes the Neighbor table entries by sending out an
    ARP request or Neighbor Solicitation packet to each neighbor
    """
    logger.debug("Beginning execution of the thread NeighborRefresh")
    stopEvent.wait(10)  # Delay slightly for neighbor table to be populated
    while not stopEvent.is_set():

        # Get the current list of neighbors in the refreshable states that are not mgmt interfaces
        neighs = []
        for (neighIfindex, neighIpaddr), neighInfo in nlm.get_neigh_cache().iteritems():
            neighIfname = nlm.get_ifinfo_cache_entry(neighIfindex)[1]
            if neighIfname is not None and neighIpaddr is not None and not neighIfname.startswith("eth") and not neighInfo[nlm.NEIGH_INFO_IDX_IS_EXT]:
                neighs.append((neighIfindex, neighIpaddr))

        if not len(neighs):
            pktDelay = GetIpv4BaseReachableMs() / (4.0 * 1000.0)
            logger.debug("NeighborRefresh: waiting %f seconds" % (pktDelay,))
            stopEvent.wait(pktDelay)
        else:
            for (neighIfindex, neighIpaddr) in neighs:
                ifInfo = nlm.get_ifinfo_for_arp_req(neighIfindex)
                ifName = ifInfo[nlm.IFINFO_IDX_IFNAME]
                if ifName is not None:
                    ifMac = ifInfo[nlm.IFINFO_IDX_MAC]
                    # Send an ARP request (IPv4) or Neighbor Solicitation (IPv6)
                    ifMacPacked = nm_pkt.mactobinary(ifMac)
                    if ifMacPacked is not None and ifName is not None:
                        if neighIpaddr.version == 4:
                            logger.info("sending arp to refresh %s on %s" % (str(neighIpaddr), ifName))
                            # Find matching network and use it as src ip
                            srcIp = nlm.get_probe_src_addr(0, ifInfo, neighIpaddr)
                            sip = _IPV4_UNSPEC_ADDR if srcIp is None else srcIp.packed
                            ArpSend(_ARP_REQUEST, neighIfindex, ifMacPacked, sip, _ETH_BROADCAST, neighIpaddr.packed, False)
                        elif neighIpaddr.version == 6:
                            logger.info("sending ns to refresh %s on interface %s" % (str(neighIpaddr), ifName))
                            # Find matching network and use it as src ip
                            srcIp = nlm.get_probe_src_addr(0, ifInfo, neighIpaddr)
                            sip = _IPV6_UNSPEC_ADDR if srcIp is None else srcIp.packed
                            NsSend(neighIfindex, ifMacPacked, sip, neighIpaddr.packed)


                # Figure out how long to wait until the next packet. Delay is 1/4 of
                # base_reachable divided by the number of neighbors
                pktDelay = GetIpv4BaseReachableMs() / (4.0 * 1000.0 * len(neighs))
                logger.debug("NeighborRefresh: waiting %f seconds" % (pktDelay,))
                if stopEvent.wait(pktDelay):
                    break

    logger.debug("Finished execution of the thread NeighborRefresh")

def VrrMacRefresh():
    """
    This function refreshes the VRR MAC addresses by sending out a gratituous
    ARP packet for each MAC address
    """
    logger.debug("Beginning execution of the thread VrrMacRefresh")
    stopEvent.wait(10)  # Delay slightly for interface table to be populated
    while not stopEvent.is_set():

        # Create a list of VRR interfaces
        vrrIf = []
        ifCache = nlm.get_ifinfo_cache()
        for ifIndex in ifCache:
            ifInfo = ifCache[ifIndex]
            (kind, ifname, lower, mac, ipv4, ipv6) = ifInfo[:6]
            (loKind, loName, loLower, loMac, loIpv4, loIpv6) = nlm.get_ifinfo_cache_entry(lower)[:6]
            if ifname is not None:
                (base, sep, idx) = ifname.partition("-v")
                if kind == 'macvlan' and loName is not None and base == loName.replace(".", "-") and idx.isdigit():
                    vrrIf.append((kind, ifname, lower, mac, ipv4, ipv6, ifIndex))

        if not len(vrrIf):
            logger.debug("VRR: waiting %f seconds" % (300.0/2.0))
            stopEvent.wait(300.0/2.0)
        else:
            for (kind, ifname, lower, mac, ipv4, ipv6, ifIndex) in vrrIf:
                macPacked = nm_pkt.mactobinary(mac)
                if macPacked is not None:
                    # Send the grat ARP or neighbor solicitation
                    if ipv4 is not None:
                        logger.info("sending arp to refresh %s on %s" % (str(ipv4), ifname))
                        ArpSend(_ARP_REQUEST, ifIndex, macPacked, ipv4.packed, _ETH_BROADCAST, ipv4.packed, False)
                    elif ipv6 is not None:
                        logger.info("sending ns to refresh %s on interface %s" % (str(ipv6), ifname))
                        NsSend(ifIndex, macPacked, ipv6.packed, ipv6.packed)

                # Wait to send the next one. We wait 50% of the default bridge aging time
                pktDelay = 300.0/(2.0*len(vrrIf))
                logger.debug("VRR: Waiting %f seconds" % (pktDelay,))
                if stopEvent.wait(pktDelay):
                    break

    logger.debug("Finished execution of the thread VrrMacRefresh")

def NeighWork():
    """
    This function processes the pending work in the neighWorkQ
    """
    logger.debug("Beginning execution of the thread NeighWork")
    dampenTics = 0
    while not stopEvent.is_set():
        # Do x amount of work and sleep for 1 second
        workCount = 0
        dampenTics += 1
        if dampenTics > nlm.neighReachDampenTout:
            dampenTics = 0
            nlm.undampen_neighs()
        while workCount < nlm.neighWorkRate:
            (ifIndex, ipAddr, workList, neighMac, isExt) = nlm.get_neigh_work()
            if not ifIndex:
                # nothing left in the workQ, sleep
                break
            for work in workList:
                workDone = False
                sendProbe = True
                if work == nlm.NEIGH_WORKQ_IDX_STALE_REFRESH and not isExt:
                    ifInfo = nlm.get_ifinfo_for_arp_req(ifIndex)
                    ifName = ifInfo[nlm.IFINFO_IDX_IFNAME]
                    if ifName is None:
                        continue
                    logger.debug("sending arp to refresh stale %s (%d): %s" %
                                (str(ifName), ifIndex, str(ipAddr)))

                    srcIp = nlm.get_probe_src_addr(0, ifInfo, ipAddr)
                    if not srcIp and ipAddr.version is 6:
                        sendProbe = False
                    if sendProbe:
                        # let this method decide the family and choose correct ip
                        ArpReqNsSend(ifName, ifIndex,
                                    ifInfo[nlm.IFINFO_IDX_MAC],\
                                    srcIp, srcIp, ipAddr)
                        if ipAddr.version is 6:
                            # this neigh is no longer pending refresh. Set flag to false
                            # and also decr pending neigh refresh count for the interface
                            nlm.neighInfoLock.acquire()
                            neighInfo = nlm.neigh_by_index_ip.get((ifIndex, ipAddr))
                            if neighInfo is not None \
                                    and neighInfo[nlm.NEIGH_INFO_PENDING_REFRESH]:
                                nlm.set_neigh_info_fields(ifIndex, ipAddr,\
                                                [(nlm.NEIGH_INFO_PENDING_REFRESH, False)])
                                # now thr is 1 lesser neigh pending refresh on this intf
                                nlm.dec_ifinfo_pending_neigh_count(ifIndex)
                            nlm.neighInfoLock.release()
                    workDone = True
                if workDone:
                    workCount += 1

        if workCount:
            nlm.stats[nlm.STAT_IDX_NEIGH_WORK_TOTAL] += workCount
            if workCount > nlm.stats[nlm.STAT_IDX_NEIGH_WORK_MAX]:
                nlm.stats[nlm.STAT_IDX_NEIGH_WORK_MAX] = workCount
            logger.debug("refreshed %d stale entries" % workCount)

        stopEvent.wait(1)
    logger.debug("Finished execution of the thread NeighWork")

#-------------------------------------------------------------------------------
#
#   Threading objects
#
#-------------------------------------------------------------------------------

def NeighborRefreshT():
    try:
        NeighborRefresh()
    except Exception as e:
        logger.exception(e)
        StopEventSet()
        if nlm:
            nlm.workq.put(('SHUTDOWN', None))
            nlm.listener.shutdown_event.set()
            nlm.listener.join()

def NeighWorkT():
    try:
        NeighWork()
    except Exception as e:
        logger.exception(e)
        StopEventSet()
        if nlm:
            nlm.workq.put(('SHUTDOWN', None))
            nlm.listener.shutdown_event.set()
            nlm.listener.join()

def VrrMacRefreshT():
    try:
        VrrMacRefresh()
    except Exception as e:
        logger.exception(e)
        StopEventSet()
        if nlm:
            nlm.workq.put(('SHUTDOWN', None))
            nlm.listener.shutdown_event.set()
            nlm.listener.join()

def NeighMgrCliT():
    arp_cli = neighmgr_cli.NeighMgrCliHandler(stopEvent, nlm, arpsnoop)
    try:
        arp_cli.uds_proc()
    except Exception as e:
        logger.exception(e)
        StopEventSet()
        if nlm:
            nlm.workq.put(('SHUTDOWN', None))
            nlm.listener.shutdown_event.set()
            nlm.listener.join()

Threads = {
    "NeighborRefresh"      : [NeighborRefreshT,      None],
    "VrrMacRefresh"        : [VrrMacRefreshT,        None],
    "NeighMgrCli"          : [NeighMgrCliT,          None],
    "NeighWork"            : [NeighWorkT,            None],
}


#-------------------------------------------------------------------------------
#
#   The Main Program
#
#-------------------------------------------------------------------------------

def main():
    # Parse the command line parameters and initialize logging.
    cmdargs = ParseCmdLine()

    # read config file
    try:
        confParser.readfp(open(_NEIGHMGR_CONF_FILE))
    except IOError:
        pass

    InitLogging(cmdargs)
    logger.info("Beginning execution of neighmgrd")

    groups = nlmanager.nlpacket.RTMGRP_LINK  | \
             nlmanager.nlpacket.RTMGRP_NEIGH | \
             nlmanager.nlpacket.RTMGRP_IPV4_IFADDR | \
             nlmanager.nlpacket.RTMGRP_IPV6_IFADDR

    global arpSock
    arpSock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW)

    global remoteArpSock
    remoteArpSock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW)
    remoteArpSock.setsockopt(socket.SOL_SOCKET, SO_MARK, TC_FWMARK)

    global nlm
    nlm = NeighborListener(groups, cmdargs, use_color=False)

    global arpsnoop
    localLearn = not cmdargs.disable_snooper
    arpsnoop = neighmgr_snoop.ArpSnoop(nlm, stopEvent, localLearn)

    # Create the threads
    global Threads
    for threadName in Threads:
        Threads[threadName][1] = threading.Thread(None, Threads[threadName][0], threadName)
        Threads[threadName][1].daemon = True

    # Start the threads
    for threadName in Threads:
        Threads[threadName][1].start()

    # Run the Netlink Listener
    try:
        status = 0
        # We don't care about FDB updates, or bridge links
        nlm.filter_by_address_family(True, 'blacklist', nlmanager.nlpacket.RTM_NEWNEIGH, socket.AF_BRIDGE)
        nlm.filter_by_address_family(True, 'blacklist', nlmanager.nlpacket.RTM_DELNEIGH, socket.AF_BRIDGE)

        #nlm.debug_address(True)

        # Prime the pump with the current list of all links, neighbors, and addresses
        nlm.workq.put(('GET_ALL_LINKS', None))
        nlm.workq.put(('GET_ALL_ADDRESSES', None))
        nlm.workq.put(('GET_ALL_NEIGHBORS', None))

        nlm.main() # This will block

    except RuntimeError, e:
        logger.error(str(e))
        status = 2
    except Exception as e:
        logger.exception(e)
        status = 3

    StopEventSet()
    if nlm:
        nlm.workq.put(('SHUTDOWN', None))
        nlm.listener.shutdown_event.set()
        nlm.listener.join()

    # Wait for all threads to stop
    for threadName in Threads:
        if Threads[threadName][1]:
            Threads[threadName][1].join()

    arpsnoop.join_threads()

    # Get out of here
    logger.info("Execution of neighmgrd is complete")
    sys.exit(status)


if __name__ == '__main__':
    signal.signal(signal.SIGTERM, SignalHandler)
    signal.signal(signal.SIGINT, SignalHandler)
    main()

