Skip to content

Commit 6eed082

Browse files
[bgp] Add 'allow list' manager feature (#5309)
implements a new feature: "BGP Allow list." This feature allows us to control which IP prefixes are going to be advertised via ebgp from the routes received from EBGP neighbors.
1 parent 4006ce7 commit 6eed082

File tree

21 files changed

+1515
-22
lines changed

21 files changed

+1515
-22
lines changed

dockers/docker-fpm-frr/frr/bgpd/templates/general/peer-group.conf.j2

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
{% if CONFIG_DB__DEVICE_METADATA['localhost']['type'] == 'ToRRouter' %}
2525
neighbor PEER_V6 allowas-in 1
2626
neighbor PEER_V6_INT allowas-in 1
27-
{% endif %}
27+
{% endif %}
2828
{% if CONFIG_DB__DEVICE_METADATA['localhost']['sub_role'] == 'BackEnd' %}
2929
neighbor PEER_V6_INT route-reflector-client
3030
{% endif %}

dockers/docker-fpm-frr/frr/bgpd/templates/general/policies.conf.j2

+27
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,33 @@
33
!
44
!
55
!
6+
{% if constants.bgp.allow_list is defined and constants.bgp.allow_list.enabled is defined and constants.bgp.allow_list.enabled %}
7+
{% if constants.bgp.allow_list.default_action is defined and constants.bgp.allow_list.default_action.strip() == 'deny' %}
8+
route-map ALLOW_LIST_DEPLOYMENT_ID_0_V4 permit 65535
9+
set community no-export additive
10+
!
11+
route-map ALLOW_LIST_DEPLOYMENT_ID_0_V6 permit 65535
12+
set community no-export additive
13+
{% else %}
14+
route-map ALLOW_LIST_DEPLOYMENT_ID_0_V4 permit 65535
15+
set community {{ constants.bgp.allow_list.drop_community }} additive
16+
!
17+
route-map ALLOW_LIST_DEPLOYMENT_ID_0_V6 permit 65535
18+
set community {{ constants.bgp.allow_list.drop_community }} additive
19+
{% endif %}
20+
!
21+
route-map FROM_BGP_PEER_V4 permit 2
22+
call ALLOW_LIST_DEPLOYMENT_ID_0_V4
23+
on-match next
24+
!
25+
route-map FROM_BGP_PEER_V6 permit 2
26+
call ALLOW_LIST_DEPLOYMENT_ID_0_V6
27+
on-match next
28+
!
29+
{% endif %}
30+
!
31+
!
32+
!
633
route-map FROM_BGP_PEER_V4 permit 100
734
!
835
route-map TO_BGP_PEER_V4 permit 100

files/image_config/constants/constants.yml

+12
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ constants:
1818
enabled: true
1919
ipv4: 64
2020
ipv6: 64
21+
allow_list:
22+
enabled: true
23+
default_action: "permit" # or "deny"
24+
drop_community: 5060:12345 # value of the community to identify a prefix to drop. Make sense only with allow_list_default_action equal to 'permit'
25+
default_pl_rules:
26+
v4:
27+
- "deny 0.0.0.0/0 le 17"
28+
- "permit 127.0.0.1/32"
29+
v6:
30+
- "deny 0::/0 le 59"
31+
- "deny 0::/0 ge 65"
32+
- "permit fe80::/64"
2133
peers:
2234
general: # peer_type
2335
db_table: "BGP_NEIGHBOR"

rules/sonic_bgpcfgd.mk

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ $(SONIC_BGPCFGD)_SRC_PATH = $(SRC_PATH)/sonic-bgpcfgd
66
# of sonic-config-engine and bgpcfgd explicitly calls sonic-cfggen
77
# as part of its unit tests.
88
# TODO: Refactor unit tests so that these dependencies are not needed
9-
$(SONIC_BGPCFGD)_DEPENDS += $(SWSSSDK_PY2) $(SONIC_PY_COMMON_PY2)
9+
$(SONIC_BGPCFGD)_DEPENDS += $(SONIC_PY_COMMON_PY2)
10+
$(SONIC_BGPCFGD)_DEBS_DEPENDS += $(LIBSWSSCOMMON) $(PYTHON_SWSSCOMMON)
1011
$(SONIC_BGPCFGD)_PYTHON_VERSION = 2
1112
SONIC_PYTHON_WHEELS += $(SONIC_BGPCFGD)

src/sonic-bgpcfgd/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ app/*.pyc
66
tests/*.pyc
77
tests/__pycache__/
88
.idea
9+
.coverage

src/sonic-bgpcfgd/app/allow_list.py

+632
Large diffs are not rendered by default.

src/sonic-bgpcfgd/app/config.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,33 @@ class ConfigMgr(object):
1010
""" The class represents frr configuration """
1111
def __init__(self):
1212
self.current_config = None
13+
self.current_config_raw = None
1314

1415
def reset(self):
1516
""" Reset stored config """
1617
self.current_config = None
18+
self.current_config_raw = None
1719

1820
def update(self):
1921
""" Read current config from FRR """
2022
self.current_config = None
23+
self.current_config_raw = None
2124
ret_code, out, err = run_command(["vtysh", "-c", "show running-config"])
2225
if ret_code != 0:
26+
# FIXME: should we throw exception here?
2327
log_crit("can't update running config: rc=%d out='%s' err='%s'" % (ret_code, out, err))
2428
return
25-
self.current_config = self.to_canonical(out)
29+
text = []
30+
for line in out.split('\n'):
31+
if line.lstrip().startswith('!'):
32+
continue
33+
text.append(line)
34+
text += [" "] # Add empty line to have something to work on, if there is no text
35+
self.current_config_raw = text
36+
self.current_config = self.to_canonical(out) # FIXME: use test as an input
37+
38+
def push_list(self, cmdlist):
39+
return self.push("\n".join(cmdlist))
2640

2741
def push(self, cmd):
2842
"""
@@ -51,8 +65,12 @@ def write(self, cmd):
5165
log_err("ConfigMgr::push(): can't push configuration '%s', rc='%d', stdout='%s', stderr='%s'" % err_tuple)
5266
if ret_code == 0:
5367
self.current_config = None # invalidate config
68+
self.current_config_raw = None
5469
return ret_code == 0
5570

71+
def get_text(self):
72+
return self.current_config_raw
73+
5674
@staticmethod
5775
def to_canonical(raw_config):
5876
"""

src/sonic-bgpcfgd/app/directory.py

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
from collections import defaultdict
2+
3+
from app.log import log_err
4+
5+
6+
class Directory(object):
7+
""" This class stores values and notifies callbacks which were registered to be executed as soon
8+
as some value is changed. This class works as DB cache mostly """
9+
def __init__(self):
10+
self.data = defaultdict(dict) # storage. A key is a slot name, a value is a dictionary with data
11+
self.notify = defaultdict(lambda: defaultdict(list)) # registered callbacks: slot -> path -> handlers[]
12+
13+
@staticmethod
14+
def get_slot_name(db, table):
15+
""" Convert db, table pair into a slot name """
16+
return db + "__" + table
17+
18+
def path_traverse(self, slot, path):
19+
"""
20+
Traverse a path in the storage.
21+
If the path is an empty string, it returns a value as it is.
22+
If the path is not an empty string, the method will traverse through the dictionary value.
23+
Example:
24+
self.data["key_1"] = { "abc": { "cde": { "fgh": "val_1", "ijk": "val_2" } } }
25+
self.path_traverse("key_1", "abc/cde") will return True, { "fgh": "val_1", "ijk": "val_2" }
26+
:param slot: storage key
27+
:param path: storage path as a string where each internal key is separated by '/'
28+
:return: a pair: True if the path was found, object if it was found
29+
"""
30+
if slot not in self.data:
31+
return False, None
32+
elif path == '':
33+
return True, self.data[slot]
34+
d = self.data[slot]
35+
for p in path.split("/"):
36+
if p not in d:
37+
return False, None
38+
d = d[p]
39+
return True, d
40+
41+
def path_exist(self, db, table, path):
42+
"""
43+
Check if the path exists in the storage
44+
:param db: db name
45+
:param table: table name
46+
:param path: requested path
47+
:return: True if the path is available, False otherwise
48+
"""
49+
slot = self.get_slot_name(db, table)
50+
return self.path_traverse(slot, path)[0]
51+
52+
def get_path(self, db, table, path):
53+
"""
54+
Return the requested path from the storage
55+
:param db: db name
56+
:param table: table name
57+
:param path: requested path
58+
:return: object if the path was found, None otherwise
59+
"""
60+
slot = self.get_slot_name(db, table)
61+
return self.path_traverse(slot, path)[1]
62+
63+
def put(self, db, table, key, value):
64+
"""
65+
Put information into the storage. Notify handlers which are dependant to the information
66+
:param db: db name
67+
:param table: table name
68+
:param key: key to change
69+
:param value: value to put
70+
:return:
71+
"""
72+
slot = self.get_slot_name(db, table)
73+
self.data[slot][key] = value
74+
if slot in self.notify:
75+
for path in self.notify[slot].keys():
76+
if self.path_exist(db, table, path):
77+
for handler in self.notify[slot][path]:
78+
handler()
79+
80+
def get(self, db, table, key):
81+
"""
82+
Get a value from the storage
83+
:param db: db name
84+
:param table: table name
85+
:param key: ket to get
86+
:return: value for the key
87+
"""
88+
slot = self.get_slot_name(db, table)
89+
return self.data[slot][key]
90+
91+
def get_slot(self, db, table):
92+
"""
93+
Get an object from the storage
94+
:param db: db name
95+
:param table: table name
96+
:return: object for the slot
97+
"""
98+
slot = self.get_slot_name(db, table)
99+
return self.data[slot]
100+
101+
def remove(self, db, table, key):
102+
"""
103+
Remove a value from the storage
104+
:param db: db name
105+
:param table: table name
106+
:param key: key to remove
107+
"""
108+
slot = self.get_slot_name(db, table)
109+
if slot in self.data:
110+
if key in self.data[slot]:
111+
del self.data[slot][key]
112+
else:
113+
log_err("Directory: Can't remove key '%s' from slot '%s'. The key doesn't exist" % (key, slot))
114+
else:
115+
log_err("Directory: Can't remove key '%s' from slot '%s'. The slot doesn't exist" % (key, slot))
116+
117+
def remove_slot(self, db, table):
118+
"""
119+
Remove an object from the storage
120+
:param db: db name
121+
:param table: table name
122+
"""
123+
slot = self.get_slot_name(db, table)
124+
if slot in self.data:
125+
del self.data[slot]
126+
else:
127+
log_err("Directory: Can't remove slot '%s'. The slot doesn't exist" % slot)
128+
129+
def available(self, db, table):
130+
"""
131+
Check if the table is available
132+
:param db: db name
133+
:param table: table name
134+
:return: True if the slot is available, False if not
135+
"""
136+
slot = self.get_slot_name(db, table)
137+
return slot in self.data
138+
139+
def available_deps(self, deps):
140+
"""
141+
Check if all items from the deps list is available in the storage
142+
:param deps: list of dependencies
143+
:return: True if all dependencies are presented, False otherwise
144+
"""
145+
res = True
146+
for db, table, path in deps:
147+
res = res and self.path_exist(db, table, path)
148+
return res
149+
150+
def subscribe(self, deps, handler):
151+
"""
152+
Subscribe the handler to be run as soon as all dependencies are presented
153+
:param deps:
154+
:param handler:
155+
:return:
156+
"""
157+
for db, table, path in deps:
158+
slot = self.get_slot_name(db, table)
159+
self.notify[slot][path].append(handler)

src/sonic-bgpcfgd/app/manager.py

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from swsscommon import swsscommon
2+
3+
from app.log import log_debug, log_err
4+
5+
6+
class Manager(object):
7+
""" This class represents a SONiC DB table """
8+
def __init__(self, common_objs, deps, database, table_name):
9+
"""
10+
Initialize class
11+
:param common_objs: common object dictionary
12+
:param deps: dependencies list
13+
:param database: database name
14+
:param table_name: table name
15+
"""
16+
self.directory = common_objs['directory']
17+
self.cfg_mgr = common_objs['cfg_mgr']
18+
self.constants = common_objs['constants']
19+
self.deps = deps
20+
self.db_name = database
21+
self.table_name = table_name
22+
self.set_queue = []
23+
self.directory.subscribe(deps, self.on_deps_change) # subscribe this class method on directory changes
24+
25+
def get_database(self):
26+
""" Return associated database """
27+
return self.db_name
28+
29+
def get_table_name(self):
30+
""" Return associated table name"""
31+
return self.table_name
32+
33+
def handler(self, key, op, data):
34+
"""
35+
This method is executed on each add/remove event on the table.
36+
:param key: key of the table entry
37+
:param op: operation on the table entry. Could be either 'SET' or 'DEL'
38+
:param data: associated data of the event. Empty for 'DEL' operation.
39+
"""
40+
if op == swsscommon.SET_COMMAND:
41+
if self.directory.available_deps(self.deps): # all required dependencies are set in the Directory?
42+
res = self.set_handler(key, data)
43+
if not res: # set handler returned False, which means it is not ready to process is. Save it for later.
44+
log_debug("'SET' handler returned NOT_READY for the Manager: %s" % self.__class__)
45+
self.set_queue.append((key, data))
46+
else:
47+
log_debug("Not all dependencies are met for the Manager: %s" % self.__class__)
48+
self.set_queue.append((key, data))
49+
elif op == swsscommon.DEL_COMMAND:
50+
self.del_handler(key)
51+
else:
52+
log_err("Invalid operation '%s' for key '%s'" % (op, key))
53+
54+
def on_deps_change(self):
55+
""" This method is being executed on every dependency change """
56+
if not self.directory.available_deps(self.deps):
57+
return
58+
new_queue = []
59+
for key, data in self.set_queue:
60+
res = self.set_handler(key, data)
61+
if not res:
62+
new_queue.append((key, data))
63+
self.set_queue = new_queue
64+
65+
def set_handler(self, key, data):
66+
""" Placeholder for 'SET' command """
67+
log_err("set_handler() wasn't implemented for %s" % self.__class__.__name__)
68+
69+
def del_handler(self, key):
70+
""" Placeholder for 'DEL' command """
71+
log_err("del_handler wasn't implemented for %s" % self.__class__.__name__)

src/sonic-bgpcfgd/app/vars.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
g_debug = False
1+
g_debug = True # FIXME: read from env variable, or from constants

src/sonic-bgpcfgd/bgpcfgd

+6-1
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@ import jinja2
1515
import netaddr
1616
from swsscommon import swsscommon
1717

18+
from app.directory import Directory
19+
from app.manager import Manager
1820
from app.vars import g_debug
1921
from app.log import log_debug, log_notice, log_info, log_warn, log_err, log_crit
2022
from app.template import TemplateFabric
2123
from app.config import ConfigMgr
24+
from app.allow_list import BGPAllowListMgr
2225
from app.util import run_command
2326

2427
g_run = True
@@ -846,7 +849,7 @@ def wait_for_daemons(daemons, seconds):
846849
def read_constants():
847850
""" Read file with constants values from /etc/sonic/constants.yml """
848851
with open('/etc/sonic/constants.yml') as fp:
849-
content = yaml.load(fp)
852+
content = yaml.load(fp) # FIXME: , Loader=yaml.FullLoader)
850853
if "constants" not in content:
851854
log_crit("/etc/sonic/constants.yml doesn't have 'constants' key")
852855
raise Exception("/etc/sonic/constants.yml doesn't have 'constants' key")
@@ -878,6 +881,8 @@ def main():
878881
BGPPeerMgrBase(common_objs, "CONFIG_DB", swsscommon.CFG_BGP_NEIGHBOR_TABLE_NAME, "general", True),
879882
BGPPeerMgrBase(common_objs, "CONFIG_DB", "BGP_MONITORS", "monitors", True),
880883
BGPPeerMgrBase(common_objs, "CONFIG_DB", "BGP_PEER_RANGE", "dynamic", False),
884+
# AllowList Managers
885+
BGPAllowListMgr(common_objs, "CONFIG_DB", "BGP_ALLOWED_PREFIXES"),
881886
]
882887
runner = Runner()
883888
for mgr in managers:

src/sonic-bgpcfgd/pytest.ini

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[pytest]
2+
addopts = --cov=app --cov-report term

0 commit comments

Comments
 (0)