From 359b80e012367179b315c1313f2edcc8086274e5 Mon Sep 17 00:00:00 2001
From: qiwang4 <97264329+qiwang4@users.noreply.github.com>
Date: Sat, 27 May 2023 07:32:05 +0800
Subject: [PATCH] [master]staticroutebfd process implementation (#13789)

* [BFD] staticroutebfd implementation
* To enable the BFD for static route

HLD: sonic-net/SONiC#1216
---
 .../frr/supervisord/critical_processes.j2     |   1 +
 .../frr/supervisord/supervisord.conf.j2       |  14 +
 .../bgpcfgd/managers_static_rt.py             |  61 +-
 src/sonic-bgpcfgd/setup.py                    |   1 +
 src/sonic-bgpcfgd/staticroutebfd/__init__.py  |   0
 src/sonic-bgpcfgd/staticroutebfd/main.py      | 764 ++++++++++++++++++
 src/sonic-bgpcfgd/staticroutebfd/vars.py      |   5 +
 src/sonic-bgpcfgd/tests/test_static_rt.py     | 110 ++-
 src/sonic-bgpcfgd/tests/test_static_rt_bfd.py | 516 ++++++++++++
 9 files changed, 1465 insertions(+), 7 deletions(-)
 create mode 100644 src/sonic-bgpcfgd/staticroutebfd/__init__.py
 create mode 100644 src/sonic-bgpcfgd/staticroutebfd/main.py
 create mode 100644 src/sonic-bgpcfgd/staticroutebfd/vars.py
 create mode 100644 src/sonic-bgpcfgd/tests/test_static_rt_bfd.py

diff --git a/dockers/docker-fpm-frr/frr/supervisord/critical_processes.j2 b/dockers/docker-fpm-frr/frr/supervisord/critical_processes.j2
index 69f4e8e6931e..dad38888e883 100644
--- a/dockers/docker-fpm-frr/frr/supervisord/critical_processes.j2
+++ b/dockers/docker-fpm-frr/frr/supervisord/critical_processes.j2
@@ -9,4 +9,5 @@ program:pimd
 program:frrcfgd
 {%- else %}
 program:bgpcfgd
+program:staticroutebfd
 {%- endif %}
diff --git a/dockers/docker-fpm-frr/frr/supervisord/supervisord.conf.j2 b/dockers/docker-fpm-frr/frr/supervisord/supervisord.conf.j2
index a19847962280..584e7fdc2e51 100644
--- a/dockers/docker-fpm-frr/frr/supervisord/supervisord.conf.j2
+++ b/dockers/docker-fpm-frr/frr/supervisord/supervisord.conf.j2
@@ -141,6 +141,20 @@ stderr_logfile=syslog
 dependent_startup=true
 dependent_startup_wait_for=bgpd:running
 
+{% if DEVICE_METADATA.localhost.frr_mgmt_framework_config is defined and DEVICE_METADATA.localhost.frr_mgmt_framework_config == "true" %}
+{% else %}
+[program:staticroutebfd]
+command=/usr/local/bin/staticroutebfd
+priority=6
+autostart=false
+autorestart=false
+startsecs=0
+stdout_logfile=syslog
+stderr_logfile=syslog
+dependent_startup=true
+dependent_startup_wait_for=bgpd:running
+{% endif %}
+
 [program:bgpmon]
 command=/usr/local/bin/bgpmon
 priority=6
diff --git a/src/sonic-bgpcfgd/bgpcfgd/managers_static_rt.py b/src/sonic-bgpcfgd/bgpcfgd/managers_static_rt.py
index 9593913b7ec7..c5c55406d6f0 100644
--- a/src/sonic-bgpcfgd/bgpcfgd/managers_static_rt.py
+++ b/src/sonic-bgpcfgd/bgpcfgd/managers_static_rt.py
@@ -25,6 +25,7 @@ def __init__(self, common_objs, db, table):
         self.directory.subscribe([("CONFIG_DB", swsscommon.CFG_DEVICE_METADATA_TABLE_NAME, "localhost/bgp_asn"),], self.on_bgp_asn_change)
         self.static_routes = {}
         self.vrf_pending_redistribution = set()
+        self.config_db = None
 
     OP_DELETE = 'DELETE'
     OP_ADD = 'ADD'
@@ -41,7 +42,17 @@ def set_handler(self, key, data):
         intf_list   = arg_list(data['ifname']) if 'ifname' in data else None
         dist_list   = arg_list(data['distance']) if 'distance' in data else None
         nh_vrf_list = arg_list(data['nexthop-vrf']) if 'nexthop-vrf' in data else None
-        route_tag   = self.ROUTE_ADVERTISE_DISABLE_TAG if 'advertise' in data and data['advertise'] == "false" else self.ROUTE_ADVERTISE_ENABLE_TAG
+        bfd_enable  = arg_list(data['bfd']) if 'bfd' in data else None
+        route_tag   = self.ROUTE_ADVERTISE_DISABLE_TAG if 'advertise' in data and data['advertise'] == "false" else self.ROUTE_ADVERTISE_ENABLE_TAG 
+
+        # bfd enabled route would be handled in staticroutebfd, skip here
+        if bfd_enable and bfd_enable[0].lower() == "true":
+            log_debug("{} static route {} bfd flag is true".format(self.db_name, key))
+            tmp_nh_set, tmp_route_tag = self.static_routes.get(vrf, {}).get(ip_prefix, (IpNextHopSet(is_ipv6), route_tag))
+            if tmp_nh_set: #clear nexthop set if it is not empty
+                log_debug("{} static route {} bfd flag is true, cur_nh is not empty, clear it".format(self.db_name, key))
+                self.static_routes.setdefault(vrf, {}).pop(ip_prefix, None)
+            return True
 
         try:
             ip_nh_set = IpNextHopSet(is_ipv6, bkh_list, nh_list, intf_list, dist_list, nh_vrf_list)
@@ -60,19 +71,59 @@ def set_handler(self, key, data):
 
         if cmd_list:
             self.cfg_mgr.push_list(cmd_list)
-            log_debug("Static route {} is scheduled for updates".format(key))
+            log_debug("{} Static route {} is scheduled for updates. {}".format(self.db_name, key, str(cmd_list)))
         else:
-            log_debug("Nothing to update for static route {}".format(key))
+            log_debug("{} Nothing to update for static route {}".format(self.db_name, key))
 
         self.static_routes.setdefault(vrf, {})[ip_prefix] = (ip_nh_set, route_tag)
 
         return True
 
 
+    def skip_appl_del(self, vrf, ip_prefix):
+        """
+        If a static route is bfd enabled, the processed static route is written into application DB by staticroutebfd.
+        When we disable bfd for that route at runtime, that static route entry will be removed from APPL_DB STATIC_ROUTE_TABLE.
+        In the case, the StaticRouteMgr(appl_db) cannot uninstall the static route from FRR if the static route is still in CONFIG_DB,
+        so need this checking (skip appl_db deletion) to avoid race condition between StaticRouteMgr(appl_db) and StaticRouteMgr(config_db)
+        For more detailed information:
+        https://github.com/sonic-net/SONiC/blob/master/doc/static-route/SONiC_static_route_bfd_hld.md#bfd-field-changes-from-true-to-false
+
+        :param vrf: vrf from the split_key(key) return
+        :param ip_prefix: ip_prefix from the split_key(key) return
+        :return: True if the deletion comes from APPL_DB and the vrf|ip_prefix exists in CONFIG_DB, otherwise return False
+        """
+        if self.db_name == "CONFIG_DB":
+            return False
+
+        if self.config_db is None:
+            self.config_db = swsscommon.SonicV2Connector()
+            self.config_db.connect(self.config_db.CONFIG_DB)
+
+        #just pop local cache if the route exist in config_db
+        cfg_key = "STATIC_ROUTE|" + vrf + "|" + ip_prefix
+        nexthop = self.config_db.get(self.config_db.CONFIG_DB, cfg_key, "nexthop")
+        if nexthop and len(nexthop)>0:
+            self.static_routes.setdefault(vrf, {}).pop(ip_prefix, None)
+            return True
+
+        if vrf == "default":
+            cfg_key = "STATIC_ROUTE|" + ip_prefix
+        nexthop = self.config_db.get(self.config_db.CONFIG_DB, cfg_key, "nexthop")
+        if nexthop and len(nexthop)>0:
+            self.static_routes.setdefault(vrf, {}).pop(ip_prefix, None)
+            return True
+
+        return False
+
     def del_handler(self, key):
         vrf, ip_prefix = self.split_key(key)
         is_ipv6 = TemplateFabric.is_ipv6(ip_prefix)
 
+        if self.skip_appl_del(vrf, ip_prefix):
+            log_debug("{} ignore appl_db static route deletion because of key {} exist in config_db".format(self.db_name, key))
+            return
+
         ip_nh_set = IpNextHopSet(is_ipv6)
         cur_nh_set, route_tag = self.static_routes.get(vrf, {}).get(ip_prefix, (IpNextHopSet(is_ipv6), self.ROUTE_ADVERTISE_DISABLE_TAG))
         cmd_list = self.static_route_commands(ip_nh_set, cur_nh_set, ip_prefix, vrf, route_tag, route_tag)
@@ -85,9 +136,9 @@ def del_handler(self, key):
 
         if cmd_list:
             self.cfg_mgr.push_list(cmd_list)
-            log_debug("Static route {} is scheduled for updates".format(key))
+            log_debug("{} Static route {} is scheduled for updates. {}".format(self.db_name, key, str(cmd_list)))
         else:
-            log_debug("Nothing to update for static route {}".format(key))
+            log_debug("{} Nothing to update for static route {}".format(self.db_name, key))
 
         self.static_routes.setdefault(vrf, {}).pop(ip_prefix, None)
 
diff --git a/src/sonic-bgpcfgd/setup.py b/src/sonic-bgpcfgd/setup.py
index b451accb9792..30298fea989a 100755
--- a/src/sonic-bgpcfgd/setup.py
+++ b/src/sonic-bgpcfgd/setup.py
@@ -11,6 +11,7 @@
     entry_points = {
         'console_scripts': [
             'bgpcfgd = bgpcfgd.main:main',
+            'staticroutebfd = staticroutebfd.main:main',
             'bgpmon = bgpmon.bgpmon:main',
         ]
     },
diff --git a/src/sonic-bgpcfgd/staticroutebfd/__init__.py b/src/sonic-bgpcfgd/staticroutebfd/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/src/sonic-bgpcfgd/staticroutebfd/main.py b/src/sonic-bgpcfgd/staticroutebfd/main.py
new file mode 100644
index 000000000000..4483d0589cca
--- /dev/null
+++ b/src/sonic-bgpcfgd/staticroutebfd/main.py
@@ -0,0 +1,764 @@
+import os
+import signal
+import sys
+import syslog
+import threading
+import traceback
+from collections import defaultdict
+from ipaddress import IPv4Address, IPv6Address
+from copy import deepcopy
+
+from swsscommon import swsscommon
+
+from .vars import g_debug, bfd_multihop, bfd_rx_interval, bfd_tx_interval, bfd_multiplier
+
+g_run = True
+
+CONFIG_DB_NAME = "CONFIG_DB"
+APPL_DB_NAME = "APPL_DB"
+STATE_DB_NAME = "STATE_DB"
+
+INTERFACE_TABLE_NAME = "INTERFACE"
+PORTCHANNEL_INTERFACE_TABLE_NAME = "PORTCHANNEL_INTERFACE"
+STATIC_ROUTE_TABLE_NAME = "STATIC_ROUTE"
+BFD_SESSION_TABLE_NAME = "BFD_SESSION_TABLE"
+
+LOCAL_CONFIG_TABLE = "config"
+LOCAL_NEXTHOP_TABLE = "nexthop"
+LOCAL_SRT_TABLE = "srt"
+LOCAL_BFD_TABLE = "bfd"
+LOCAL_BFD_PENDING_TABLE = "bfd_pending"
+LOCAL_INTERFACE_TABLE = "interface"
+
+def log_debug(msg):
+    """ Send a message msg to the syslog as DEBUG """
+    if g_debug:
+        syslog.syslog(syslog.LOG_DEBUG, msg)
+
+def log_notice(msg):
+    """ Send a message msg to the syslog as NOTICE """
+    syslog.syslog(syslog.LOG_NOTICE, msg)
+
+def log_info(msg):
+    """ Send a message msg to the syslog as INFO """
+    syslog.syslog(syslog.LOG_INFO, msg)
+
+def log_warn(msg):
+    """ Send a message msg to the syslog as WARNING """
+    syslog.syslog(syslog.LOG_WARNING, msg)
+
+def log_err(msg):
+    """ Send a message msg to the syslog as ERR """
+    syslog.syslog(syslog.LOG_ERR, msg)
+
+def log_crit(msg):
+    """ Send a message msg to the syslog as CRIT """
+    syslog.syslog(syslog.LOG_CRIT, msg)
+
+def signal_handler(_, __):  # signal_handler(signum, frame)
+    """ signal handler """
+    global g_run
+    g_run = False
+
+def static_route_split_key(key):
+    """
+    Split key into vrf name and prefix.
+    :param key: key to split
+    :return: valid, vrf name extracted from the key, ip prefix extracted from the key
+    """
+    l = tuple(key.split('|'))
+
+    if len(l) == 1:
+        return True, 'default', l[0]
+
+    return True, l[0], l[1]
+
+def check_ip(ip):
+    if len(ip) == 0:
+        return False, False, ""
+
+    value = ip.split('/',1)
+    v = value[0]
+    try:
+        IPv4Address(v)
+        valid = True
+        is_ipv4 = True
+    except:
+        is_ipv4 = False
+        try:
+            IPv6Address(v)
+            valid = True
+        except:
+            valid = False
+    return valid, is_ipv4, v
+
+class StaticRouteBfd(object):
+
+    SELECT_TIMEOUT = 1000
+    BFD_DEFAULT_CFG = {"multihop": "false", "rx_interval": "50", "tx_interval": "50"}
+
+    def __init__(self):
+        self.local_db = defaultdict(dict)
+        self.local_db[LOCAL_CONFIG_TABLE] = defaultdict(dict)
+        self.local_db[LOCAL_NEXTHOP_TABLE] = defaultdict(set)
+        self.local_db[LOCAL_SRT_TABLE] = defaultdict(set)
+        self.local_db[LOCAL_BFD_TABLE] = defaultdict(dict)
+        self.local_db[LOCAL_BFD_PENDING_TABLE] = defaultdict(dict)
+        #interface, portchannel_interface and loopback_interface share same table, assume name is unique
+        #assume only one ipv4  and/or one ipv6 for each interface
+        self.local_db[LOCAL_INTERFACE_TABLE] = defaultdict(dict)
+
+        self.config_db  = swsscommon.DBConnector(CONFIG_DB_NAME, 0, True)
+        self.appl_db = swsscommon.DBConnector(APPL_DB_NAME, 0, True)
+        self.state_db = swsscommon.DBConnector(STATE_DB_NAME, 0, True)
+
+        self.bfd_appl_tbl = swsscommon.ProducerStateTable(self.appl_db, BFD_SESSION_TABLE_NAME)
+
+        self.static_route_appl_tbl = swsscommon.Table(self.appl_db, STATIC_ROUTE_TABLE_NAME)
+
+        self.selector = swsscommon.Select()
+        self.callbacks = defaultdict(lambda: defaultdict(list))  # db -> table -> handlers[]
+        self.subscribers = set()
+        self.first_time = True
+
+    def get_ip_from_key(self, key):
+        """
+        Get ip address from key for LOOPBACK/INTERFACE/PORTCHANNEL_INTERFACE table
+        :param key: key in the tables
+        :return: valid, is_ipv4, ip address
+        """
+        if '|' not in key:
+            return False, False, "", ""
+        else:
+            if_ip = key.split('|')
+            if len(if_ip) < 2:
+                return False, False, "", ""
+            if_name = if_ip[0]
+            value = if_ip[1]
+            valid, is_ipv4, ip = check_ip(value)
+            log_debug("get ip from intf key: valid %s is_ipv4 %s, if_name %s ip %s"%(str(valid), str(is_ipv4), if_name, ip))
+            return valid, is_ipv4, if_name, ip
+
+    def set_local_db(self, table, key, data):
+        try:
+            self.local_db[table][key] = data
+            return
+        except:
+            log_err("set_local_db error, table %s key %s"%(table, key))
+            pass
+
+    def get_local_db(self, table, key):
+        try:
+            v = self.local_db[table][key]
+            return v
+        except:
+            return {}
+
+    def remove_from_local_db(self, table, key):
+        if table in self.local_db:
+            if key in self.local_db[table]:
+                del self.local_db[table][key]
+
+    def append_to_nh_table_entry(self, nh_key, ip_prefix):
+        entry = self.get_local_db(LOCAL_NEXTHOP_TABLE, nh_key)
+        entry.add(ip_prefix)
+
+    def remove_from_nh_table_entry(self, nh_key, ip_prefix):
+        entry = self.get_local_db(LOCAL_NEXTHOP_TABLE, nh_key)
+        if ip_prefix in entry:
+            entry.remove(ip_prefix)
+            if len(entry) == 0:
+                self.remove_from_local_db(LOCAL_NEXTHOP_TABLE, nh_key)
+
+    def set_bfd_session_into_appl_db(self, key, data):
+        fvs = swsscommon.FieldValuePairs(list(data.items()))
+        self.bfd_appl_tbl.set(key, fvs)
+        log_debug("set bfd session to appl_db, key %s, data %s"%(key, str(data)))
+
+    def del_bfd_session_from_appl_db(self, key):
+        self.bfd_appl_tbl.delete(key)
+
+    def interface_set_handler(self, key, data):
+        valid, is_ipv4, if_name, ip = self.get_ip_from_key(key)
+        if not valid:
+            return True
+        value = self.get_local_db(LOCAL_INTERFACE_TABLE, if_name)
+        if len(value) == 0:
+            value = {is_ipv4: ip}
+        else:
+            value[is_ipv4] = ip
+        self.set_local_db(LOCAL_INTERFACE_TABLE, if_name, value)
+        self.update_bfd_pending(if_name)
+        return True
+
+    def interface_del_handler(self, key):
+        valid, is_ipv4, if_name, ip = self.get_ip_from_key(key)
+        if not valid:
+            return True
+        value = self.get_local_db(LOCAL_INTERFACE_TABLE, if_name)
+        if len(value) == 0:
+            return
+        else:
+            value[is_ipv4] = ""  #remove the IP address for the interface
+        self.set_local_db(LOCAL_INTERFACE_TABLE, if_name, value)
+
+    def find_interface_ip(self, if_name, ip_example):
+        valid, is_ipv4, nh = check_ip(ip_example)
+        if not valid:
+            return False, ""
+
+        value = self.get_local_db(LOCAL_INTERFACE_TABLE, if_name)
+        ip = value.get(is_ipv4, "")
+        if len(ip)>0: #ip should be verified when add to local_db
+            return True, ip
+
+        return False, ""
+
+    def update_bfd_pending(self, if_name):
+        del_list=[]
+        for k, v in self.local_db[LOCAL_BFD_PENDING_TABLE].items():
+            if len(v) == 3 and v[0] == if_name:
+                intf, nh_ip, bfd_key = v[0], v[1], v[2]
+                valid, local_addr = self.find_interface_ip(intf, nh_ip)
+                if not valid: #IP address might not be available for this type of nh_ip (IPv4 or IPv6) yet
+                    continue
+                log_notice("bfd_pending: get ip for interface: %s, create bfd session for %s"%(intf, bfd_key))
+                bfd_entry_cfg = self.BFD_DEFAULT_CFG.copy()
+                if all([bfd_rx_interval, bfd_tx_interval, bfd_multiplier, bfd_multihop]):
+                    bfd_entry_cfg["multihop"] = bfd_multihop
+                    bfd_entry_cfg["rx_interval"] = bfd_rx_interval
+                    bfd_entry_cfg["tx_interval"] = bfd_tx_interval
+                    bfd_entry_cfg["multiplier"] = bfd_multiplier
+
+                bfd_entry_cfg["local_addr"] = local_addr
+                self.set_bfd_session_into_appl_db(bfd_key, bfd_entry_cfg)
+                bfd_entry_cfg["static_route"] = "true"
+                self.set_local_db(LOCAL_BFD_TABLE, bfd_key, bfd_entry_cfg)
+                del_list.append(k)
+
+        for k in del_list:
+            self.local_db[LOCAL_BFD_PENDING_TABLE].pop(k)
+
+    def strip_table_name(self, key, splitter):
+        return key.split(splitter, 1)[1]
+
+    def reconciliation(self):
+        #to use SonicV2Connector get_all method, DBConnector doesn't have get_all
+        db = swsscommon.SonicV2Connector()
+        db.connect(db.CONFIG_DB)
+        db.connect(db.APPL_DB)
+        db.connect(db.STATE_DB)
+
+        #MUST keep the restore sequene
+        #restore interface(loopback/interface/portchannel_interface) tables
+
+        #restore interface tables
+        log_info("restore interface table -->")
+        keys = db.keys(db.CONFIG_DB, "LOOPBACK_INTERFACE|*")
+        for key in keys:
+            key_new = self.strip_table_name(key, "|")
+            self.interface_set_handler(key_new, "")
+        keys = db.keys(db.CONFIG_DB, "INTERFACE|*")
+        for key in keys:
+            key_new = self.strip_table_name(key, "|")
+            self.interface_set_handler(key_new, "")
+        keys = db.keys(db.CONFIG_DB, "PORTCHANNEL_INTERFACE|*")
+        for key in keys:
+            key_new = self.strip_table_name(key, "|")
+            self.interface_set_handler(key_new, "")
+
+        #restore bfd session table, static route won't create bfd session if it is already in appl_db
+        log_info("restore bfd session table -->")
+        keys = db.keys(db.APPL_DB, "BFD_SESSION_TABLE:*")
+        for key in keys:
+            data = db.get_all(db.APPL_DB, key)
+            key_new = self.strip_table_name(key, ":")
+            self.set_local_db(LOCAL_BFD_TABLE, key_new, data)
+
+        #restore static route table
+        log_info("restore static route table -->")
+        keys = db.keys(db.CONFIG_DB, "STATIC_ROUTE|*")
+        for key in keys:
+            data = db.get_all(db.CONFIG_DB, key)
+            key_new = self.strip_table_name(key, "|")
+            log_debug("SRT_BFD: restore static route from config_db, key %s, data %s"%(key, str(data)))
+            self.static_route_set_handler(key_new, data)
+
+        #clean up local bfd table, remove non static route bfd session
+        log_info("cleanup bfd session table -->")
+        self.cleanup_local_bfd_table()
+
+        #restore bfd state table
+        log_info("restore bfd state table -->")
+        keys = db.keys(db.STATE_DB, "BFD_SESSION_TABLE|*")
+        for key in keys:
+            data = db.get_all(db.STATE_DB, key)
+            key_new = self.strip_table_name(key, "|")
+            self.bfd_state_set_handler(key_new, data)
+
+    def cleanup_local_bfd_table(self):
+        kl=[]
+        for key in self.local_db[LOCAL_BFD_TABLE]:
+            kl.append(key)
+        for key in kl:
+            bfd_session = self.local_db[LOCAL_BFD_TABLE][key]
+            if "static_route" not in bfd_session or bfd_session["static_route"] != "true":
+                self.local_db[LOCAL_BFD_TABLE].pop(key)
+
+    def isFieldTrue(self, bfd_field):
+        if isinstance(bfd_field, list):
+            if len(bfd_field) == 1:
+                if isinstance(bfd_field[0], str):
+                    if bfd_field[0].lower() == "true":
+                        return True
+        return False
+
+    def refresh_active_nh(self, route_cfg_key):
+        data = self.get_local_db(LOCAL_CONFIG_TABLE, route_cfg_key)
+
+        arg_list    = lambda v: [x.strip() for x in v.split(',')] if len(v.strip()) != 0 else None
+        nh_list     = arg_list(data['nexthop']) if 'nexthop' in data else None
+        nh_vrf_list = arg_list(data['nexthop-vrf']) if 'nexthop-vrf' in data else None
+        nh_cnt      = 0
+
+        for index in range(len(nh_list)):
+            nh_ip = nh_list[index]
+            nh_vrf = nh_vrf_list[index]
+            nh_key = nh_vrf + "|" + nh_ip
+            bfd_key = nh_vrf + ":default:" + nh_ip
+
+            bfd_session = self.get_local_db(LOCAL_BFD_TABLE, bfd_key)
+            if len(bfd_session) == 0:
+                continue
+            if "state" in bfd_session and bfd_session["state"].upper() == "UP":
+                self.append_to_srt_table_entry(route_cfg_key, (nh_vrf, nh_ip))
+                nh_cnt += 1
+
+        #do not write to appl_db is no nexthop reachable
+        if nh_cnt == 0:
+            return
+
+        #if there is any bfd session state UP, we don't need to hold the static route update.
+        data['bfd_nh_hold'] = "false"
+        new_config = self.reconstruct_static_route_config(data, self.get_local_db(LOCAL_SRT_TABLE, route_cfg_key))
+        self.set_static_route_into_appl_db(route_cfg_key.replace("|", ":"), new_config)
+
+    def handle_bfd_change(self, cfg_key, data, to_bfd_enable):
+        valid, vrf, ip_prefix = static_route_split_key(cfg_key)
+        key = vrf + ":" + ip_prefix
+        log_debug("SRT_BFD: handle_bfd_change. key %s, data %s, to_bfd_enable %s"%(key, str(data), str(to_bfd_enable)))
+        if to_bfd_enable:
+            #write route with full_nh_list to appl_db, let StaticRouteMgr(appl_db) install this route to update its cache
+            data['bfd'] = "false"
+            data['expiry'] = "false"
+            self.set_static_route_into_appl_db(key, data)
+            log_debug("SRT_BFD: bfd toggle to true. write the route to appl_db, update StaticRouteMgr(appl_db), key %s"%(key))
+        else:
+            self.del_static_route_from_appl_db(key)
+            log_debug("SRT_BFD: bfd toggle to false. delete static route from appl_db, key %s"%(key))
+
+            #treat it as static route deletion, but do not delete it from LOCAL_CONFIG_TABLE
+            self.static_route_del_handler(cfg_key, False)
+
+    def static_route_set_handler(self, key, data):
+
+        #sanity checking
+        if len(data) == 0:
+            return True
+
+        valid, vrf, ip_prefix = static_route_split_key(key)
+        route_cfg_key = vrf + "|" + ip_prefix
+        if not valid:
+            return True
+
+        valid, is_ipv4, ip = check_ip(ip_prefix)
+        if not valid:
+            log_err("invalid ip prefix for static route: ", key)
+            return True
+
+        arg_list  = lambda v: [x.strip() for x in v.split(',')] if len(v.strip()) != 0 else None
+        bfd_field = arg_list(data['bfd']) if 'bfd' in data else ["false"]
+
+        cur_data = self.get_local_db(LOCAL_CONFIG_TABLE, route_cfg_key)
+        cur_bfd_enabled = False
+        if cur_data:
+            cur_bfd_field = arg_list(cur_data['bfd']) if 'bfd' in cur_data else ["false"]
+            cur_bfd_enabled = self.isFieldTrue(cur_bfd_field)
+
+        # this process, staticroutebfd, only handle the bfd enabled case, other cases would be handled in bgpcfgd/StaticRouteMgr
+        bfd_enabled = self.isFieldTrue(bfd_field)
+
+        #when bfd changed from "false" to "true", before bfd session created and state becomes up,
+        #the installed static route need to be kept in the system system, so put this route in "hold" state until at least one 
+        #bfd session becomes UP.
+        data_copy = data.copy()
+        data['bfd_nh_hold'] = "false"
+        if cur_data:
+            if cur_bfd_enabled and not bfd_enabled: # dynamic bfd flag change from TRUE to FALSE
+                self.handle_bfd_change(key, data_copy, False)
+            if not cur_bfd_enabled and bfd_enabled: # dynamic bfd flag change from FALSE to TRUE
+                self.handle_bfd_change(key, data_copy, True)
+                data['bfd_nh_hold'] = "true"
+
+        if not bfd_enabled: 
+            #skip if bfd is not enabled, but store it to local_db to detect bfd field dynamic change
+            data['bfd'] = "false"
+            self.set_local_db(LOCAL_CONFIG_TABLE, route_cfg_key, data)
+            return True
+
+        bkh_list    = arg_list(data['blackhole']) if 'blackhole' in data else None
+        nh_list     = arg_list(data['nexthop']) if 'nexthop' in data else None
+        intf_list   = arg_list(data['ifname']) if 'ifname' in data else None
+        dist_list   = arg_list(data['distance']) if 'distance' in data else None
+        nh_vrf_list = arg_list(data['nexthop-vrf']) if 'nexthop-vrf' in data else None
+        if nh_vrf_list is None:
+            nh_vrf_list = [vrf] * len(nh_list) if len(nh_list) > 0 else None
+            data['nexthop-vrf'] = ','.join(nh_vrf_list) if nh_vrf_list else ''
+        if intf_list is None or nh_list is None or nh_vrf_list is None or \
+                len(intf_list) != len(nh_list) or len(intf_list) != len(nh_vrf_list):
+            log_err("Static route bfd set Failed, nexthop, interface and vrf lists do not match.")
+            return True
+
+
+        if cur_data and cur_bfd_enabled:
+            # route with the prefix already exist, remove the deleted nexthops
+            nh_list_exist = arg_list(cur_data['nexthop']) if 'nexthop' in cur_data else None
+            nh_vrf_list_exist = arg_list(cur_data['nexthop-vrf']) if 'nexthop-vrf' in cur_data else None
+            if nh_vrf_list_exist is None:
+                nh_vrf_list_exist = []
+                for nh in nh_list:
+                    nh_vrf_list_exist.append(vrf)
+
+            intf_list_exist   = arg_list(cur_data['ifname']) if 'ifname' in cur_data else None
+            nh_key_list_exist = list(zip(nh_vrf_list_exist, intf_list_exist, nh_list_exist))
+            nh_key_list_new = list(zip(nh_vrf_list, intf_list, nh_list))
+            for nh in nh_key_list_exist:
+                if nh not in nh_key_list_new:
+                    nh_vrf = nh[0]
+                    nh_ip = nh[2]
+                    nh_key = nh_vrf + "|" + nh_ip
+                    self.remove_from_srt_table_entry(route_cfg_key, (nh_vrf, nh_ip))
+                    self.remove_from_nh_table_entry(nh_key, route_cfg_key)
+                    if len(self.get_local_db(LOCAL_NEXTHOP_TABLE, nh_key)) == 0:
+                        bfd_key = nh_vrf + ":default:" + nh_ip
+                        self.remove_from_local_db(LOCAL_BFD_TABLE, bfd_key)
+                        self.del_bfd_session_from_appl_db(bfd_key)
+
+        self.set_local_db(LOCAL_CONFIG_TABLE, route_cfg_key, data)
+        for index in range(len(nh_list)):
+            nh_ip = nh_list[index]
+            intf = intf_list[index]
+            nh_vrf = nh_vrf_list[index]
+            nh_key = nh_vrf + "|" + nh_ip
+
+            #check if the bfd session is already created
+            bfd_key = nh_vrf  + ":default:" + nh_ip
+            bfd_session = self.get_local_db(LOCAL_BFD_TABLE, bfd_key)
+            if len(bfd_session)>0:
+                self.local_db[LOCAL_BFD_TABLE][bfd_key]["static_route"] = "true"
+
+            if len(self.get_local_db(LOCAL_NEXTHOP_TABLE, nh_key)) == 0 and len(bfd_session) == 0:
+                valid, local_addr = self.find_interface_ip(intf, nh_ip)
+                if not valid:
+                    #interface IP is not available yet, put this request to cache
+                    self.set_local_db(LOCAL_BFD_PENDING_TABLE, intf+"_"+bfd_key, [intf, nh_ip, bfd_key])
+                    self.append_to_nh_table_entry(nh_key, vrf + "|" + ip_prefix)
+                    log_warn("bfd_pending: cannot find ip for interface: %s, postpone bfd session creation" %intf)
+                    continue
+
+                bfd_entry_cfg = self.BFD_DEFAULT_CFG.copy()
+                if all([bfd_rx_interval, bfd_tx_interval, bfd_multiplier, bfd_multihop]):
+                    bfd_entry_cfg["multihop"] = bfd_multihop
+                    bfd_entry_cfg["rx_interval"] = bfd_rx_interval
+                    bfd_entry_cfg["tx_interval"] = bfd_tx_interval
+                    bfd_entry_cfg["multiplier"] = bfd_multiplier
+
+                bfd_entry_cfg["local_addr"] = local_addr
+                self.set_bfd_session_into_appl_db(bfd_key, bfd_entry_cfg)
+                bfd_entry_cfg["static_route"] = "true"
+                self.set_local_db(LOCAL_BFD_TABLE, bfd_key, bfd_entry_cfg)
+
+            self.append_to_nh_table_entry(nh_key, vrf + "|" + ip_prefix)
+
+        self.refresh_active_nh(route_cfg_key)
+
+        return True
+
+    def static_route_del_handler(self, key, redis_del):
+        valid, vrf, ip_prefix = static_route_split_key(key)
+        if not valid:
+            return True
+        route_cfg_key = vrf + "|" + ip_prefix
+
+        valid, is_ipv4, ip = check_ip(ip_prefix)
+        if not valid:
+            return True
+
+        data = self.get_local_db(LOCAL_CONFIG_TABLE, route_cfg_key)
+        if len(data) == 0:
+            # this route is not handled by StaticRouteBfd, skip
+            return True
+
+        arg_list    = lambda v: [x.strip() for x in v.split(',')] if len(v.strip()) != 0 else None
+        nh_list     = arg_list(data['nexthop']) if 'nexthop' in data else None
+        nh_vrf_list = arg_list(data['nexthop-vrf']) if 'nexthop-vrf' in data else None
+        for index in range(len(nh_list)):
+            nh_ip = nh_list[index]
+            nh_vrf = nh_vrf_list[index]
+            nh_key = nh_vrf + "|" + nh_ip
+            self.remove_from_nh_table_entry(nh_key, route_cfg_key)
+
+            if len(self.get_local_db(LOCAL_NEXTHOP_TABLE, nh_key)) == 0:
+                bfd_key = nh_vrf + ":default:" + nh_ip
+                self.remove_from_local_db(LOCAL_BFD_TABLE, bfd_key)
+                self.del_bfd_session_from_appl_db(bfd_key)
+
+        self.del_static_route_from_appl_db(route_cfg_key.replace("|", ":"))
+        self.remove_from_local_db(LOCAL_SRT_TABLE, route_cfg_key)
+
+        if redis_del:
+            self.remove_from_local_db(LOCAL_CONFIG_TABLE, route_cfg_key)
+
+        return True
+
+    def interface_callback(self, key, op, data):
+        if op == swsscommon.SET_COMMAND:
+            self.interface_set_handler(key, data)
+        elif op == swsscommon.DEL_COMMAND:
+            self.interface_del_handler(key)
+        else:
+            log_err("Invalid operation '%s' for key '%s'" % (op, key))
+
+    def static_route_callback(self, key, op, data):
+        if op == swsscommon.SET_COMMAND:
+            self.static_route_set_handler(key, data)
+        elif op == swsscommon.DEL_COMMAND:
+            self.static_route_del_handler(key, True)
+        else:
+            log_err("Invalid operation '%s' for key '%s'" % (op, key))
+
+    def bfd_state_split_key(self, key):
+        """
+        Split key into table name, vrf name, interface name and peer ip.
+        :param key: key to split
+        :return: table name, vrf name, interface name and peer ip extracted from the key
+        """
+        if key.count("|") < 2:
+            return 'default', 'default', key
+        else:
+            return tuple(key.split('|'))
+
+    def append_to_srt_table_entry(self, srt_key, nh_info):
+        entry = self.get_local_db(LOCAL_SRT_TABLE, srt_key)
+        entry.add(nh_info)
+
+    def remove_from_srt_table_entry(self, srt_key, nh_info):
+        entry = self.get_local_db(LOCAL_SRT_TABLE, srt_key)
+        if nh_info in entry:
+            entry.remove(nh_info)
+            if len(entry) == 0:
+                self.remove_from_local_db(LOCAL_SRT_TABLE, srt_key)
+
+    def set_static_route_into_appl_db(self, key, data):
+        fvs = swsscommon.FieldValuePairs(list(data.items()))
+        self.static_route_appl_tbl.set(key, fvs)
+        log_debug("SRT_BFD: set static route to appl_db, key %s, data %s"%(key, str(data)))
+
+    def del_static_route_from_appl_db(self, key):
+        self.static_route_appl_tbl.delete(key)
+
+    def reconstruct_static_route_config(self, original_config, reachable_nexthops):
+        arg_list    = lambda v: [x.strip() for x in v.split(',')] if len(v.strip()) != 0 else None
+        bkh_list    = arg_list(original_config['blackhole']) if 'blackhole' in original_config else None
+        nh_list     = arg_list(original_config['nexthop']) if 'nexthop' in original_config else None
+        intf_list   = arg_list(original_config['ifname']) if 'ifname' in original_config else None
+        dist_list   = arg_list(original_config['distance']) if 'distance' in original_config else None
+        nh_vrf_list = arg_list(original_config['nexthop-vrf']) if 'nexthop-vrf' in original_config else None
+
+        bkh_candidate = ""
+        nh_candidate = ""
+        intf_candidate = ""
+        dist_candidate = ""
+        nh_vrf_candidate = ""
+
+
+        for i in range(len(nh_list)):
+            if (nh_vrf_list[i], nh_list[i]) in reachable_nexthops:
+                bkh_candidate += "," + (bkh_list[i] if bkh_list else "")
+                nh_candidate += "," + (nh_list[i] if nh_list else "")
+                intf_candidate += "," + (intf_list[i] if intf_list else "")
+                dist_candidate += "," + (dist_list[i] if dist_list else "")
+                nh_vrf_candidate += "," + (nh_vrf_list[i] if nh_vrf_list else "")
+
+        new_config = dict()
+        for key in original_config:
+            if key == "bfd":
+                continue
+            if key == "bfd_nh_hold":
+                continue
+            if key == "blackhole":
+                new_config[key] = bkh_candidate[1:]
+            elif key == "nexthop":
+                new_config[key] = nh_candidate[1:]
+            elif key == "ifname":
+                new_config[key] = intf_candidate[1:]
+            elif key == "distance":
+                new_config[key] = dist_candidate[1:]
+            elif key == "nexthop-vrf":
+                new_config[key] = nh_vrf_candidate[1:]
+            else:
+                new_config[key] = original_config[key]
+        new_config["expiry"] = "false"
+
+        return new_config
+
+
+    def bfd_state_set_handler(self, key, data):
+        #key are diff in state db and appl_db,
+        #intf is always default for multihop bfd
+        vrf, intf, peer_ip = self.bfd_state_split_key(key)
+        bfd_key = vrf + ":" + intf + ":" + peer_ip
+
+        #check if the BFD session is in local table
+        bfd_session = self.get_local_db(LOCAL_BFD_TABLE, bfd_key)
+        if len(bfd_session) == 0:
+            return True
+
+        nh_key = vrf + "|" + peer_ip
+        state = data['state'] if 'state' in data else "DOWN"
+        log_info("bfd seesion %s state %s" %(bfd_key, state))
+
+        self.local_db[LOCAL_BFD_TABLE][bfd_key]["state"] = state
+
+        if state.upper() == "UP":
+            for prefix in self.get_local_db(LOCAL_NEXTHOP_TABLE, nh_key):
+                srt_key =  prefix
+                config_key =  prefix
+                #skip if the (vrf, peer_ip) is already in the nexthop list
+                if (vrf, peer_ip) in self.get_local_db(LOCAL_SRT_TABLE, srt_key):
+                    continue
+                self.append_to_srt_table_entry(srt_key, (vrf, peer_ip))
+                config_data = self.get_local_db(LOCAL_CONFIG_TABLE, config_key)
+                #exit "hold" state when any BFD session becomes UP
+                config_data['bfd_nh_hold'] = "false"
+                new_config = self.reconstruct_static_route_config(config_data, self.get_local_db(LOCAL_SRT_TABLE, srt_key))
+                self.set_static_route_into_appl_db(srt_key.replace("|", ":"), new_config)
+
+        elif state.upper() == "DOWN":
+            for prefix in self.get_local_db(LOCAL_NEXTHOP_TABLE, nh_key):
+                srt_key =  prefix
+                config_key =  prefix
+                config_data = self.get_local_db(LOCAL_CONFIG_TABLE, config_key)
+                #skip if the static route is in "hold" state
+                if config_data['bfd_nh_hold'] == "true":
+                    continue
+                self.remove_from_srt_table_entry(srt_key, (vrf, peer_ip))
+                if len(self.get_local_db(LOCAL_SRT_TABLE, srt_key)) == 0:
+                    log_debug("SRT_BFD: bfd_state DOWN. nh_list is empty, delete static route from appl_db, key %s"%(srt_key.replace("|", ":")))
+                    self.del_static_route_from_appl_db(srt_key.replace("|", ":"))
+                else:
+                    config_data = self.get_local_db(LOCAL_CONFIG_TABLE, config_key)
+                    new_config = self.reconstruct_static_route_config(config_data, self.get_local_db(LOCAL_SRT_TABLE, srt_key))
+                    self.set_static_route_into_appl_db(srt_key.replace("|", ":"), new_config)
+
+
+    def bfd_state_del_handler(self, key):
+        vrf, intf, peer_ip = self.bfd_state_split_key(key)
+        bfd_key = vrf + ":" + intf + ":" + peer_ip
+
+        nh_key = vrf + "|" + peer_ip
+
+        for prefix in self.get_local_db(LOCAL_NEXTHOP_TABLE, nh_key):
+            srt_key =  prefix
+            config_key = prefix
+            self.remove_from_srt_table_entry(srt_key, (vrf, peer_ip))
+            if len(self.get_local_db(LOCAL_SRT_TABLE, srt_key)) == 0:
+                log_debug("SRT_BFD: bfd_state deletion. nh_list is empty, delete static route from appl_db, key %s"%(srt_key.replace("|", ":")))
+                self.del_static_route_from_appl_db(srt_key.replace("|", ":"))
+            else:
+                config_data = self.get_local_db(LOCAL_CONFIG_TABLE, config_key)
+                new_config = self.reconstruct_static_route_config(config_data, self.get_local_db(LOCAL_SRT_TABLE, srt_key))
+                self.set_static_route_into_appl_db(srt_key.replace("|", ":"), new_config)
+
+    def bfd_state_callback(self, key, op, data):
+        if op == swsscommon.SET_COMMAND:
+            self.bfd_state_set_handler(key, data)
+        elif op == swsscommon.DEL_COMMAND:
+            self.bfd_state_del_handler(key)
+        else:
+            log_err("Invalid operation '%s' for key '%s'" % (op, key))
+
+
+    def prepare_selector(self):
+        interface_subscriber = swsscommon.SubscriberStateTable(self.config_db, INTERFACE_TABLE_NAME)
+        portchannel_interface_subscriber = swsscommon.SubscriberStateTable(self.config_db, PORTCHANNEL_INTERFACE_TABLE_NAME)
+        static_route_subscriber = swsscommon.SubscriberStateTable(self.config_db, STATIC_ROUTE_TABLE_NAME)
+        bfd_state_subscriber = swsscommon.SubscriberStateTable(self.state_db, swsscommon.STATE_BFD_SESSION_TABLE_NAME)
+
+        self.selector.addSelectable(interface_subscriber)
+        self.selector.addSelectable(portchannel_interface_subscriber)
+        self.selector.addSelectable(static_route_subscriber)
+        self.selector.addSelectable(bfd_state_subscriber)
+
+        self.subscribers.add(interface_subscriber)
+        self.subscribers.add(portchannel_interface_subscriber)
+        self.subscribers.add(static_route_subscriber)
+        self.subscribers.add(bfd_state_subscriber)
+
+        self.callbacks[self.config_db.getDbId()][INTERFACE_TABLE_NAME].append(self.interface_callback)
+        self.callbacks[self.config_db.getDbId()][PORTCHANNEL_INTERFACE_TABLE_NAME].append(self.interface_callback)
+        self.callbacks[self.config_db.getDbId()][STATIC_ROUTE_TABLE_NAME].append(self.static_route_callback)
+        self.callbacks[self.state_db.getDbId()][swsscommon.STATE_BFD_SESSION_TABLE_NAME].append(self.bfd_state_callback)
+
+    def run(self):
+        self.prepare_selector()
+        while g_run:
+            state, _ = self.selector.select(self.SELECT_TIMEOUT)
+            if state == self.selector.TIMEOUT:
+                continue
+            elif state == self.selector.ERROR:
+                raise Exception("Received error from select")
+
+            if self.first_time:
+                self.first_time = False
+                self.reconciliation()
+
+            for sub in self.subscribers:
+                while True:
+                    key, op, fvs = sub.pop()
+                    if len(key) == 0:
+                        break
+                    log_debug("Received message : '%s'" % str((key, op, fvs)))
+                    for callback in self.callbacks[sub.getDbConnector().getDbId()][sub.getTableName()]:
+                        callback(key, op, dict(fvs))
+
+def do_work():
+    sr_bfd = StaticRouteBfd()
+    sr_bfd.run()
+
+def main():
+    rc = 0
+    try:
+        syslog.openlog('staticroutebfd')
+        signal.signal(signal.SIGTERM, signal_handler)
+        signal.signal(signal.SIGINT, signal_handler)
+        do_work()
+    except KeyboardInterrupt:
+        syslog.syslog(syslog.LOG_NOTICE, "Keyboard interrupt")
+        pass
+    except RuntimeError as exc:
+        syslog.syslog(syslog.LOG_CRIT, str(exc))
+        rc = -2
+        if g_debug:
+            raise
+    except Exception as exc:
+        syslog.syslog(syslog.LOG_CRIT, "Got an exception %s: Traceback: %s" % (str(exc), traceback.format_exc()))
+        rc = -1
+        if g_debug:
+            raise
+    finally:
+        syslog.closelog()
+    try:
+        sys.exit(rc)
+    except SystemExit:
+        os._exit(rc)
diff --git a/src/sonic-bgpcfgd/staticroutebfd/vars.py b/src/sonic-bgpcfgd/staticroutebfd/vars.py
new file mode 100644
index 000000000000..e880844687f6
--- /dev/null
+++ b/src/sonic-bgpcfgd/staticroutebfd/vars.py
@@ -0,0 +1,5 @@
+g_debug = True
+bfd_multihop = "true" 
+bfd_rx_interval = "50"
+bfd_tx_interval = "50"
+bfd_multiplier = "3"
diff --git a/src/sonic-bgpcfgd/tests/test_static_rt.py b/src/sonic-bgpcfgd/tests/test_static_rt.py
index 881bba1563b7..3d947a47ac73 100644
--- a/src/sonic-bgpcfgd/tests/test_static_rt.py
+++ b/src/sonic-bgpcfgd/tests/test_static_rt.py
@@ -604,7 +604,7 @@ def test_set_no_action(mocked_log_debug):
         True,
         []
     )
-    mocked_log_debug.assert_called_with("Nothing to update for static route default|10.1.1.0/24")
+    mocked_log_debug.assert_called_with("CONFIG_DB Nothing to update for static route default|10.1.1.0/24")
 
 @patch('bgpcfgd.managers_static_rt.log_debug')
 def test_del_no_action(mocked_log_debug):
@@ -616,7 +616,7 @@ def test_del_no_action(mocked_log_debug):
         True,
         []
     )
-    mocked_log_debug.assert_called_with("Nothing to update for static route default|10.1.1.0/24")
+    mocked_log_debug.assert_called_with("CONFIG_DB Nothing to update for static route default|10.1.1.0/24")
 
 def test_set_invalid_arg():
     mgr = constructor()
@@ -821,3 +821,109 @@ def test_set_tag_change():
             "ip route 10.1.0.0/24 10.0.0.57 tag 2",
         ]
     )
+
+def test_set_bfd_false():
+    mgr = constructor()
+    set_del_test(
+        mgr,
+        "SET",
+        ("10.1.0.0/24", {
+            "bfd": "false",
+            "nexthop": "PortChannel0001",
+        }),
+        True,
+        [
+            "ip route 10.1.0.0/24 PortChannel0001 tag 1",
+            "route-map STATIC_ROUTE_FILTER permit 10",
+            " match tag 1",
+            "router bgp 65100",
+            " address-family ipv4",
+            "  redistribute static route-map STATIC_ROUTE_FILTER",
+            " address-family ipv6",
+            "  redistribute static route-map STATIC_ROUTE_FILTER"
+        ]
+    )
+
+    set_del_test(
+        mgr,
+        "DEL",
+        ("10.1.0.0/24",),
+        True,
+        [
+            "no ip route 10.1.0.0/24 PortChannel0001 tag 1",
+            "router bgp 65100",
+            " address-family ipv4",
+            "  no redistribute static route-map STATIC_ROUTE_FILTER",
+            " address-family ipv6",
+            "  no redistribute static route-map STATIC_ROUTE_FILTER",
+            "no route-map STATIC_ROUTE_FILTER"
+        ]
+    )
+
+def test_set_bfd_true():
+    mgr = constructor()
+    set_del_test(
+        mgr,
+        "SET",
+        ("10.1.0.0/24", {
+            "bfd": "false",
+            "nexthop": "PortChannel0001",
+        }),
+        True,
+        [
+            "ip route 10.1.0.0/24 PortChannel0001 tag 1",
+            "route-map STATIC_ROUTE_FILTER permit 10",
+            " match tag 1",
+            "router bgp 65100",
+            " address-family ipv4",
+            "  redistribute static route-map STATIC_ROUTE_FILTER",
+            " address-family ipv6",
+            "  redistribute static route-map STATIC_ROUTE_FILTER"
+        ]
+    )
+    #do nothing for adding smae route second time
+    set_del_test(
+        mgr,
+        "SET",
+        ("10.1.0.0/24", {
+            "bfd": "false",
+            "nexthop": "PortChannel0001",
+        }),
+        True,
+        [
+        ]
+    )
+    #clear internal cache if bfd flag is true
+    set_del_test(
+        mgr,
+        "SET",
+        ("10.1.0.0/24", {
+            "bfd": "true",
+            "nexthop": "PortChannel0001",
+        }),
+        True,
+        [
+        ]
+    )
+
+    #install the route becasue that cache was cleared above
+    set_del_test(
+        mgr,
+        "SET",
+        ("10.1.0.0/24", {
+            "bfd": "false",
+            "nexthop": "PortChannel0001",
+        }),
+        True,
+        [
+            "ip route 10.1.0.0/24 PortChannel0001 tag 1",
+            "route-map STATIC_ROUTE_FILTER permit 10",
+            " match tag 1",
+            "router bgp 65100",
+            " address-family ipv4",
+            "  redistribute static route-map STATIC_ROUTE_FILTER",
+            " address-family ipv6",
+            "  redistribute static route-map STATIC_ROUTE_FILTER"
+        ]
+    )
+
diff --git a/src/sonic-bgpcfgd/tests/test_static_rt_bfd.py b/src/sonic-bgpcfgd/tests/test_static_rt_bfd.py
new file mode 100644
index 000000000000..e2eb32cb85d5
--- /dev/null
+++ b/src/sonic-bgpcfgd/tests/test_static_rt_bfd.py
@@ -0,0 +1,516 @@
+from unittest.mock import patch
+#from unittest.mock import MagicMock, patch
+
+from staticroutebfd.main import *
+from swsscommon import swsscommon
+
+@patch('swsscommon.swsscommon.DBConnector.__init__')
+@patch('swsscommon.swsscommon.ProducerStateTable.__init__')
+@patch('swsscommon.swsscommon.Table.__init__')
+def constructor(mock_db, mock_producer, mock_tbl):
+    mock_db.return_value = None
+    mock_producer.return_value = None
+    mock_tbl.return_value = None
+
+    srt_bfd = StaticRouteBfd()
+    return srt_bfd
+
+def set_del_test(dut, hdlr, op, args, e_bfd_dict, e_srt_dict):
+    set_del_test.bfd_dict = {}
+    set_del_test.srt_dict = {}
+
+    def bfd_app_set(key, data):
+        set_del_test.bfd_dict["set_"+key] = data.copy()
+    def bfd_app_del(key):
+        set_del_test.bfd_dict["del_"+key] = {}
+    def srt_app_set(key, data):
+        set_del_test.srt_dict["set_"+key] = data.copy()
+    def srt_app_del(key):
+        set_del_test.srt_dict["del_"+key] = {}
+
+    def compare_dict(r, e):
+        if len(r) == 0 and len(e) == 0:
+            return True
+        if len(r) != len(e):
+            return False
+        for k in e:
+            if k not in r:
+                return False
+            if type(e[k]) is str:
+                r_sort = "".join(sorted([x.strip() for x in r[k].split(',')]))
+                e_sort = "".join(sorted([x.strip() for x in e[k].split(',')]))
+                if r_sort != e_sort:
+                    return False
+            if type(e[k]) is dict:
+                ret = compare_dict(r[k], e[k])
+                if not ret:
+                    return False
+        return True
+
+    dut.set_bfd_session_into_appl_db = bfd_app_set
+    dut.del_bfd_session_from_appl_db = bfd_app_del
+    dut.set_static_route_into_appl_db = srt_app_set
+    dut.del_static_route_from_appl_db = srt_app_del
+
+    if op == "SET":
+        if hdlr == "bfd":
+            dut.bfd_state_set_handler(*args)
+        if hdlr == "srt":
+            dut.static_route_set_handler(*args)
+        if hdlr == "intf":
+            dut.interface_set_handler(*args)
+    elif op == "DEL":
+        if hdlr == "bfd":
+            dut.bfd_state_del_handler(*args)
+        if hdlr == "srt":
+            dut.static_route_del_handler(*args)
+        if hdlr == "intf":
+            dut.interface_del_handler(*args)
+    else:
+        assert False, "Wrong operation"
+
+    assert compare_dict(set_del_test.bfd_dict, e_bfd_dict)
+    assert compare_dict(set_del_test.srt_dict, e_srt_dict)
+
+def intf_setup(dut):
+    set_del_test(dut, "intf",
+        "SET",
+        ("if1|192.168.1.1/24", {}
+        ),
+        {},
+        {}
+    )
+    set_del_test(dut, "intf",
+        "SET",
+        ("if2|192.168.2.1/24", {}
+        ),
+        {},
+        {}
+    )
+    set_del_test(dut, "intf",
+        "SET",
+        ("if3|192.168.3.1/24", {}
+        ),
+        {},
+        {}
+    )
+
+def test_set_del():
+    dut = constructor()
+    intf_setup(dut)
+
+    #test #1
+    set_del_test(dut, "srt",
+        "SET",
+        ("2.2.2.0/24", {
+            "bfd": "true",
+            "nexthop": "192.168.1.2 , 192.168.2.2, 192.168.3.2",
+            "ifname": "if1, if2, if3",
+        }),
+        { 
+            "set_default:default:192.168.1.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.1.1'},
+            "set_default:default:192.168.2.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.2.1'},
+            "set_default:default:192.168.3.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.3.1'}
+        },
+        {}
+    )
+
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.1.2", {
+            "state": "Up"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.1.2', 'ifname': 'if1', 'nexthop-vrf': 'default', 'expiry': 'false'}}
+    )
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.2.2", {
+            "state": "Up"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2 ', 'ifname': 'if2,if1', 'nexthop-vrf': 'default,default', 'expiry': 'false'}}
+    )
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.3.2", {
+            "state": "Up"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2,192.168.3.2 ', 'ifname': 'if2,if1,if3', 'nexthop-vrf': 'default,default,default', 'expiry': 'false'}}
+    )
+
+    #test #2
+    set_del_test(dut, "srt",
+        "SET",
+        ("2.2.2.0/24", {
+            "bfd": "true",
+            "nexthop": "192.168.1.2 , 192.168.2.2",
+            "ifname": "if1, if2",
+        }),
+        { 
+            "del_default:default:192.168.3.2" : {}
+        },
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2 ', 'ifname': 'if2,if1', 'nexthop-vrf': 'default,default', 'expiry': 'false'}}
+    )
+
+    #test #3
+    set_del_test(dut, "srt",
+        "DEL",
+        ("2.2.2.0/24", {
+            "bfd": "true",
+            "nexthop": "192.168.1.2 , 192.168.2.2",
+            "ifname": "if1, if2",
+        }),
+        { 
+            "del_default:default:192.168.1.2" : {},
+            "del_default:default:192.168.2.2" : {}
+        },
+        {'del_default:2.2.2.0/24': {}}
+    )
+
+def test_bfd_del():
+    dut = constructor()
+    intf_setup(dut)
+
+    set_del_test(dut, "srt",
+        "SET",
+        ("2.2.2.0/24", {
+            "bfd": "true",
+            "nexthop": "192.168.1.2 , 192.168.2.2, 192.168.3.2",
+            "ifname": "if1, if2, if3",
+        }),
+        { 
+            "set_default:default:192.168.1.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.1.1'},
+            "set_default:default:192.168.2.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.2.1'},
+            "set_default:default:192.168.3.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.3.1'}
+        },
+        {}
+    )
+
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.1.2", {
+            "state": "Up"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.1.2', 'ifname': 'if1', 'nexthop-vrf': 'default', 'expiry': 'false'}}
+    )
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.2.2", {
+            "state": "Up"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2 ', 'ifname': 'if2,if1', 'nexthop-vrf': 'default,default', 'expiry': 'false'}}
+    )
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.3.2", {
+            "state": "Up"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2,192.168.3.2 ', 'ifname': 'if2,if1,if3', 'nexthop-vrf': 'default,default,default', 'expiry': 'false'}}
+    )
+
+    #test bfd state del
+    set_del_test(dut, "bfd",
+        "DEL",
+        ({"192.168.2.2"}),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.1.2,192.168.3.2 ', 'ifname': 'if1,if3', 'nexthop-vrf': 'default,default', 'expiry': 'false'}}
+    )
+
+def test_set_2routes():
+    dut = constructor()
+    intf_setup(dut)
+
+    #test #4
+    set_del_test(dut, "srt",
+        "SET",
+        ("2.2.2.0/24", {
+            "bfd": "true",
+            "nexthop": "192.168.1.2 , 192.168.2.2, 192.168.3.2",
+            "ifname": "if1, if2, if3",
+        }),
+        { 
+            "set_default:default:192.168.1.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.1.1'},
+            "set_default:default:192.168.2.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.2.1'},
+            "set_default:default:192.168.3.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.3.1'}
+        },
+        {}
+    )
+
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.1.2", {
+            "state": "Up"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.1.2', 'ifname': 'if1', 'nexthop-vrf': 'default', 'expiry': 'false'}}
+    )
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.2.2", {
+            "state": "Up"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2 ', 'ifname': 'if2,if1', 'nexthop-vrf': 'default,default', 'expiry': 'false'}}
+    )
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.3.2", {
+            "state": "Up"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2,192.168.3.2 ', 'ifname': 'if2,if1,if3', 'nexthop-vrf': 'default,default,default', 'expiry': 'false'}}
+    )
+
+    set_del_test(dut, "srt",
+        "SET",
+        ("3.3.3.0/24", {
+            "bfd": "true",
+            "nexthop": "192.168.2.2",
+            "ifname": "if2",
+        }),
+        {},
+        {'set_default:3.3.3.0/24': {'nexthop': '192.168.2.2', 'ifname': 'if2', 'nexthop-vrf': 'default', 'expiry': 'false'}}
+    )
+
+    #test #5
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.2.2", {
+            "state": "Down"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.3.2,192.168.1.2 ', 'ifname': 'if3,if1', 'nexthop-vrf': 'default,default', 'expiry': 'false'}, 'del_default:3.3.3.0/24': {}}
+    )
+
+    #test #6
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.2.2", {
+            "state": "Up"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2,192.168.3.2 ', 'ifname': 'if2,if1,if3', 'nexthop-vrf': 'default,default,default', 'expiry': 'false'},
+         'set_default:3.3.3.0/24': {'nexthop': '192.168.2.2', 'ifname': 'if2', 'nexthop-vrf': 'default', 'expiry': 'false'}}
+    )
+
+def test_set_bfd_change_hold():
+    dut = constructor()
+    intf_setup(dut)
+
+    #test #9 bfd: true -> false
+    set_del_test(dut, "srt",
+        "SET",
+        ("2.2.2.0/24", {
+            "bfd": "true",
+            "nexthop": "192.168.1.2 , 192.168.2.2, 192.168.3.2",
+            "ifname": "if1, if2, if3",
+        }),
+        { 
+            "set_default:default:192.168.1.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.1.1'},
+            "set_default:default:192.168.2.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.2.1'},
+            "set_default:default:192.168.3.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.3.1'}
+        },
+        {}
+    )
+
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.1.2", {
+            "state": "Up"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.1.2', 'ifname': 'if1', 'nexthop-vrf': 'default', 'expiry': 'false'}}
+    )
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.2.2", {
+            "state": "Up"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2 ', 'ifname': 'if2,if1', 'nexthop-vrf': 'default,default', 'expiry': 'false'}}
+    )
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.3.2", {
+            "state": "Up"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2,192.168.3.2 ', 'ifname': 'if2,if1,if3', 'nexthop-vrf': 'default,default,default', 'expiry': 'false'}}
+    )
+
+    set_del_test(dut, "srt",
+        "SET",
+        ("2.2.2.0/24", {
+            "bfd": "false",
+            "nexthop": "192.168.1.2 , 192.168.2.2, 192.168.3.2",
+            "ifname": "if1, if2, if3",
+        }),
+        { 
+            "del_default:default:192.168.1.2" : {},
+            "del_default:default:192.168.2.2" : {},
+            "del_default:default:192.168.3.2" : {}
+        },
+        {
+         'del_default:2.2.2.0/24': {}
+        }
+    )
+    return
+
+    #test #10 'bfd': false --> true, write original rout first
+    set_del_test(dut, "srt",
+        "SET",
+        ("2.2.2.0/24", {
+            "bfd": "true",
+            "nexthop": "192.168.1.2 , 192.168.2.2, 192.168.3.2",
+            "ifname": "if1, if2, if3",
+        }),
+        { 
+            "set_default:default:192.168.1.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.1.1'},
+            "set_default:default:192.168.2.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.2.1'},
+            "set_default:default:192.168.3.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.3.1'}
+        },
+        {'set_default:2.2.2.0/24': {'bfd':'false', 'nexthop': '192.168.2.2,192.168.1.2,192.168.3.2 ', 'ifname': 'if2,if1,if3', 'expiry': 'false'}}
+    )
+
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.1.2", {
+            "state": "Up"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.1.2', 'ifname': 'if1', 'nexthop-vrf': 'default', 'expiry': 'false'}}
+    )
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.2.2", {
+            "state": "Up"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2 ', 'ifname': 'if2,if1', 'nexthop-vrf': 'default,default', 'expiry': 'false'}}
+    )
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.3.2", {
+            "state": "Up"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2,192.168.3.2 ', 'ifname': 'if2,if1,if3', 'nexthop-vrf': 'default,default,default', 'expiry': 'false'}}
+    )
+
+
+def test_set_bfd_change_no_hold():
+    dut = constructor()
+    intf_setup(dut)
+
+    #setup runtime "bfd"="false" condition``
+    set_del_test(dut, "srt",
+        "SET",
+        ("2.2.2.0/24", {
+            "bfd": "true",
+            "nexthop": "192.168.1.2 , 192.168.2.2, 192.168.3.2",
+            "ifname": "if1, if2, if3",
+        }),
+        { 
+            "set_default:default:192.168.1.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.1.1'},
+            "set_default:default:192.168.2.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.2.1'},
+            "set_default:default:192.168.3.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.3.1'}
+        },
+        {}
+    )
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.1.2", {
+            "state": "Up"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.1.2', 'ifname': 'if1', 'nexthop-vrf': 'default', 'expiry': 'false'}}
+    )
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.2.2", {
+            "state": "Up"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2 ', 'ifname': 'if2,if1', 'nexthop-vrf': 'default,default', 'expiry': 'false'}}
+    )
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.3.2", {
+            "state": "Up"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2,192.168.3.2 ', 'ifname': 'if2,if1,if3', 'nexthop-vrf': 'default,default,default', 'expiry': 'false'}}
+    )
+    set_del_test(dut, "srt",
+        "SET",
+        ("3.3.3.0/24", {
+            "bfd": "true",
+            "nexthop": "192.168.2.2",
+            "ifname": "if2",
+        }),
+        {},
+        {'set_default:3.3.3.0/24': {'nexthop': '192.168.2.2', 'ifname': 'if2', 'nexthop-vrf': 'default', 'expiry': 'false'}}
+    )
+
+    set_del_test(dut, "srt",
+        "SET",
+        ("2.2.2.0/24", {
+            "bfd": "false",
+            "nexthop": "192.168.1.2 , 192.168.2.2, 192.168.3.2",
+            "ifname": "if1, if2, if3",
+        }),
+        { 
+            "del_default:default:192.168.1.2" : {},
+            "del_default:default:192.168.3.2" : {}
+        },
+        {
+         'del_default:2.2.2.0/24': {}
+        }
+    )
+
+    #test #10 change 'bfd': false to true, because the bfd session "default:default:192.168.2.2" is up, so add that nexthop right after "bfd" change to "true" 
+    set_del_test(dut, "srt",
+        "SET",
+        ("2.2.2.0/24", {
+            "bfd": "true",
+            "nexthop": "192.168.1.2 , 192.168.2.2, 192.168.3.2",
+            "ifname": "if1, if2, if3",
+        }),
+        { 
+            "set_default:default:192.168.1.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.1.1'},
+            "set_default:default:192.168.3.2" : {'multihop': 'true', 'rx_interval': '50', 'tx_interval': '50', 'multiplier': '3', 'local_addr': '192.168.3.1'}
+        },
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2', 'ifname': 'if2', 'nexthop-vrf': 'default', 'expiry': 'false'}}
+    )
+
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.1.2", {
+            "state": "Up"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2 ', 'ifname': 'if2,if1', 'nexthop-vrf': 'default,default', 'expiry': 'false'}}
+    )
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.2.2", {
+            "state": "Up"
+        }),
+        {},
+        {}
+    )
+    set_del_test(dut, "bfd",
+        "SET",
+        ("192.168.3.2", {
+            "state": "Up"
+        }),
+        {},
+        {'set_default:2.2.2.0/24': {'nexthop': '192.168.2.2,192.168.1.2,192.168.3.2 ', 'ifname': 'if2,if1,if3', 'nexthop-vrf': 'default,default,default', 'expiry': 'false'}}
+    )
+
+