Skip to content

Commit 394e2fb

Browse files
authored
Implement script null_route_helper (sonic-net#1737)
Signed-off-by: bingwang <[email protected]>
1 parent dd01b56 commit 394e2fb

File tree

5 files changed

+567
-29
lines changed

5 files changed

+567
-29
lines changed

scripts/null_route_helper

+267
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
Utility for blocking and unblocking traffic from given source ip address on ACL tables.
5+
6+
The block operation will insert a DENY rule at the top of the table. The unblock operation
7+
will remove an existing DENY rule that has been created by the block operation (i.e. it does
8+
NOT insert an ALLOW rule, only removes DENY rules).
9+
10+
Since SONiC supports multi ACL rules share the same priority, all ACL rules created by null_route_helper will
11+
use the highest priority(9999).
12+
13+
Example:
14+
15+
Block traffic from 10.2.3.4:
16+
./null_route_helper block acl_table_name 10.2.3.4
17+
18+
Unblock all traffic from 10.2.3.4:
19+
./null_route_helper unblock acl_table_name 10.2.3.4
20+
21+
List all acl rules added by this script
22+
./null_route_helper list acl_table_name
23+
"""
24+
25+
26+
from __future__ import print_function
27+
28+
import syslog
29+
import sys
30+
import click
31+
import ipaddress
32+
import tabulate
33+
34+
from swsscommon.swsscommon import ConfigDBConnector
35+
36+
37+
CONFIG_DB_ACL_TABLE_TABLE = "ACL_TABLE"
38+
CONFIG_DB_ACL_RULE_TABLE = "ACL_RULE"
39+
CONFIG_DB_VLAN_TABLE = "VLAN"
40+
41+
ACTION_ALLOW = "FORWARD"
42+
ACTION_DENY = "DROP"
43+
ACTION_LIST = "LIST"
44+
45+
# Since SONiC supports multi ACL rules share the same priority, we use 9999 (the highest) for all rules
46+
ACL_RULE_PRIORITY = 9999
47+
# The key of rule will be overridden with BLOCK_RULE_ + ip
48+
ACL_RULE_PREFIX = 'BLOCK_RULE_'
49+
50+
# Internet Protocol version 4 EtherType
51+
ETHER_TYPE_IPV4 = 0x0800
52+
53+
def notice(msg):
54+
"""
55+
Log a NOTICE message to the console and syslog
56+
"""
57+
syslog.syslog(syslog.LOG_NOTICE, msg)
58+
print(msg)
59+
60+
61+
def error(msg):
62+
"""
63+
Log an ERR message to the console and syslog, and exit the program with an error code
64+
"""
65+
syslog.syslog(syslog.LOG_ERR, msg)
66+
print(msg, file=sys.stderr)
67+
sys.exit(1)
68+
69+
70+
def ip_ver(ip_prefix):
71+
return ipaddress.ip_network(ip_prefix, False).version
72+
73+
74+
def confirm_required_table_existence(configdb, sub_table_name):
75+
"""
76+
Check the existence of required ACL table, and exit if absent
77+
"""
78+
target_table = configdb.get_entry(CONFIG_DB_ACL_TABLE_TABLE, sub_table_name)
79+
80+
if not target_table:
81+
error("Table {} not found, exiting...".format(sub_table_name))
82+
83+
return True
84+
85+
86+
def get_acl_rule_key(ip_prefix):
87+
"""
88+
Get the key that will be used to refer to the ACL rule used to block traffic from a source ip.
89+
Since the rules are all given the same priority in SONiC, we can't identify a rule based on the priority.
90+
So, we use the destination IP being blocked to give each rule a unique name in the system.
91+
"""
92+
return ACL_RULE_PREFIX + str(ip_prefix)
93+
94+
95+
def get_all_acl_rules(configdb, table_name):
96+
"""
97+
Return a dict of existed acl rules
98+
{(u'NULL_ROUTE_TABLE', u'BLOCK_RULE_1.1.1.1/32'): {'PRIORITY': '9999', 'PACKET_ACTION': 'FORWARD', 'SRC_IP': '1.1.1.1/32'},...}
99+
"""
100+
key = CONFIG_DB_ACL_RULE_TABLE + '|' + table_name
101+
all_rules = configdb.get_table(key)
102+
block_rules = {}
103+
for k, v in all_rules.items():
104+
if k[1].startswith(ACL_RULE_PREFIX):
105+
block_rules[k] = v
106+
107+
return block_rules
108+
109+
110+
def validate_input(ip_address):
111+
"""
112+
Validate the format of input
113+
"""
114+
try:
115+
ip_n = ipaddress.ip_network(ip_address, False)
116+
ver = ip_n.version
117+
prefix_len = ip_n.prefixlen
118+
# Prefix len must be 32 for IPV4 and 128 for IPV6
119+
if ver == 4 and prefix_len == 32 or ver == 6 and prefix_len == 128:
120+
return ip_n.with_prefixlen
121+
122+
error("Prefix length must be 32 (IPv4) or 128 (IPv6)")
123+
except ValueError as e:
124+
error("Could not parse {} as a valid IP address; exception={}".format(ip_address, e))
125+
126+
127+
def build_acl_rule(priority, src_ip):
128+
"""
129+
Bild DROP rule for given src_ip and priority
130+
"""
131+
rule = {
132+
"PRIORITY": str(priority),
133+
"PACKET_ACTION": "DROP"
134+
}
135+
if ip_ver(src_ip) == 4:
136+
rule['ETHER_TYPE'] = str(ETHER_TYPE_IPV4)
137+
rule['SRC_IP'] = src_ip
138+
else:
139+
rule['IP_TYPE'] = 'IPV6ANY'
140+
rule['SRC_IPV6'] = src_ip
141+
142+
return rule
143+
144+
145+
def get_rule(configdb, table_name, ip_prefix):
146+
"""
147+
Get Acl rule for given ip_prefix
148+
"""
149+
key_name = 'SRC_IP' if ip_ver(ip_prefix) == 4 else 'SRC_IPV6'
150+
all_rules = get_all_acl_rules(configdb, table_name)
151+
for key, rule in all_rules.items():
152+
if ip_prefix == rule.get(key_name, None):
153+
if ip_prefix:
154+
return {key: rule}
155+
156+
return None
157+
158+
159+
def update_acl_table(configdb, acl_table_name, ip_prefix, action):
160+
"""
161+
Update ACL table to apply new rules for given ip_prefix. 'action' is supposed to be in ['DENY', 'ALLOW']
162+
For 'DENY', an 'DROP' rule for given ip_prefix will be added if not existed
163+
For 'ALLOW', we will try to remove the existing 'DENY' rule, and nothing is changed if not existed
164+
"""
165+
confirm_required_table_existence(configdb, acl_table_name)
166+
rule = get_rule(configdb, acl_table_name, ip_prefix)
167+
rule_key = list(rule.keys())[0] if rule else None
168+
rule_value = list(rule.values())[0] if rule else None
169+
if action == ACTION_ALLOW:
170+
if not rule:
171+
return
172+
# Delete existing BLOCK rule for given ip_prefix
173+
# Pass None as data will delete the entry
174+
configdb.mod_entry(CONFIG_DB_ACL_RULE_TABLE, rule_key, None)
175+
else:
176+
if rule:
177+
if rule_value['PACKET_ACTION'] == 'DROP':
178+
return
179+
else:
180+
# If there is 'FORWARDED' ACL rule, then change it to 'DROP'
181+
rule_value['PACKET_ACTION'] = 'DROP'
182+
configdb.mod_entry(CONFIG_DB_ACL_RULE_TABLE, rule_key, rule_value)
183+
else:
184+
priority = ACL_RULE_PRIORITY
185+
new_rule_key = (acl_table_name, get_acl_rule_key(ip_prefix))
186+
new_rule_value = build_acl_rule(priority, ip_prefix)
187+
configdb.set_entry(CONFIG_DB_ACL_RULE_TABLE, new_rule_key, new_rule_value)
188+
189+
190+
def list_all_null_route_rules(configdb, table_name):
191+
"""
192+
List all rules added by this script
193+
"""
194+
195+
confirm_required_table_existence(configdb, table_name)
196+
header = ("Table", "Rule", "Priority", "Action", "Match")
197+
all_rules = get_all_acl_rules(configdb, table_name)
198+
199+
match_keys = ["SRC_IP", "SRC_IPV6"]
200+
data = []
201+
for (_, rule_id), rule in all_rules.items():
202+
priority = rule.get("PRIORITY", "N/A")
203+
action = rule.get("PACKET_ACTION", "N/A")
204+
match = "N/A"
205+
for k in match_keys:
206+
if k in rule:
207+
match = rule[k]
208+
break
209+
210+
data.append([table_name, rule_id, priority, action, match])
211+
212+
print(tabulate.tabulate(data, headers=header, tablefmt="simple", missingval=""))
213+
214+
215+
def null_route_helper(table_name, action, ip_prefix=None):
216+
"""
217+
Helper function called by 'click'.
218+
"""
219+
configdb = ConfigDBConnector()
220+
configdb.connect()
221+
if action == ACTION_LIST:
222+
list_all_null_route_rules(configdb, table_name)
223+
else:
224+
ip_prefix = validate_input(ip_prefix)
225+
update_acl_table(configdb, table_name, ip_prefix, action)
226+
227+
228+
@click.group()
229+
def cli():
230+
pass
231+
232+
233+
# ./null_route_helper block table_name 1.2.3.4
234+
@cli.command('block')
235+
@click.argument("table_name", type=click.STRING, required=True)
236+
@click.argument("ip_prefix", type=click.STRING, required=True)
237+
def block(table_name, ip_prefix):
238+
"""
239+
Block traffic from given src ip prefix
240+
"""
241+
null_route_helper(table_name, ACTION_DENY, ip_prefix)
242+
243+
244+
# ./null_route_helper unblock table_name 1.2.3.4
245+
@cli.command('unblock')
246+
@click.argument("table_name", type=click.STRING, required=True)
247+
@click.argument("ip_prefix", type=click.STRING, required=True)
248+
def unblock(table_name, ip_prefix):
249+
"""
250+
Unblock traffic from given src ip prefix
251+
"""
252+
null_route_helper(table_name, ACTION_ALLOW, ip_prefix)
253+
254+
255+
# ./null_route_helper list table_name
256+
@cli.command('list')
257+
@click.argument("table_name", type=click.STRING, required=True)
258+
def list_rules(table_name):
259+
"""
260+
List all rules *added by this script*
261+
"""
262+
null_route_helper(table_name, ACTION_LIST)
263+
264+
265+
if __name__ == "__main__":
266+
cli()
267+

setup.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@
130130
'scripts/watermarkstat',
131131
'scripts/watermarkcfg',
132132
'scripts/sonic-kdump-config',
133-
'scripts/centralize_database'
133+
'scripts/centralize_database',
134+
'scripts/null_route_helper'
134135
],
135136
entry_points={
136137
'console_scripts': [

tests/aclshow_test.py

+44-28
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,27 @@
3535

3636
# Expected output for aclshow -a
3737
all_output = """\
38-
RULE NAME TABLE NAME PRIO PACKETS COUNT BYTES COUNT
39-
------------ ------------ ------ --------------- -------------
40-
RULE_1 DATAACL 9999 101 100
41-
RULE_2 DATAACL 9998 201 200
42-
RULE_3 DATAACL 9997 301 300
43-
RULE_4 DATAACL 9996 401 400
44-
RULE_05 DATAACL 9995 0 0
45-
RULE_7 DATAACL 9993 701 700
46-
RULE_9 DATAACL 9991 901 900
47-
RULE_10 DATAACL 9989 1001 1000
48-
DEFAULT_RULE DATAACL 1 2 1
49-
RULE_6 EVERFLOW 9994 601 600
50-
RULE_08 EVERFLOW 9992 0 0
38+
RULE NAME TABLE NAME PRIO PACKETS COUNT BYTES COUNT
39+
------------------------------------- ------------- ------ --------------- -------------
40+
RULE_1 DATAACL 9999 101 100
41+
RULE_2 DATAACL 9998 201 200
42+
RULE_3 DATAACL 9997 301 300
43+
RULE_4 DATAACL 9996 401 400
44+
RULE_05 DATAACL 9995 0 0
45+
RULE_7 DATAACL 9993 701 700
46+
RULE_9 DATAACL 9991 901 900
47+
RULE_10 DATAACL 9989 1001 1000
48+
DEFAULT_RULE DATAACL 1 2 1
49+
RULE_6 EVERFLOW 9994 601 600
50+
RULE_08 EVERFLOW 9992 0 0
51+
RULE_1 NULL_ROUTE_V4 9999 N/A N/A
52+
BLOCK_RULE_10.0.0.2/32 NULL_ROUTE_V4 9999 N/A N/A
53+
BLOCK_RULE_10.0.0.3/32 NULL_ROUTE_V4 9999 N/A N/A
54+
DEFAULT_RULE NULL_ROUTE_V4 1 N/A N/A
55+
RULE_1 NULL_ROUTE_V6 9999 N/A N/A
56+
BLOCK_RULE_1000:1000:1000:1000::2/128 NULL_ROUTE_V6 9999 N/A N/A
57+
BLOCK_RULE_1000:1000:1000:1000::3/128 NULL_ROUTE_V6 9999 N/A N/A
58+
DEFAULT_RULE NULL_ROUTE_V6 1 N/A N/A
5159
"""
5260

5361
# Expected output for aclshow -r RULE_1 -t DATAACL
@@ -80,8 +88,8 @@
8088
# Expected output for aclshow -r RULE_4,RULE_6 -vv
8189
rule4_rule6_verbose_output = """\
8290
Reading ACL info...
83-
Total number of ACL Tables: 8
84-
Total number of ACL Rules: 11
91+
Total number of ACL Tables: 10
92+
Total number of ACL Rules: 19
8593
8694
RULE NAME TABLE NAME PRIO PACKETS COUNT BYTES COUNT
8795
----------- ------------ ------ --------------- -------------
@@ -116,19 +124,27 @@
116124
# Expected output for
117125
# aclshow -a -c ; aclshow -a
118126
all_after_clear_output = """\
119-
RULE NAME TABLE NAME PRIO PACKETS COUNT BYTES COUNT
120-
------------ ------------ ------ --------------- -------------
121-
RULE_1 DATAACL 9999 0 0
122-
RULE_2 DATAACL 9998 0 0
123-
RULE_3 DATAACL 9997 0 0
124-
RULE_4 DATAACL 9996 0 0
125-
RULE_05 DATAACL 9995 0 0
126-
RULE_7 DATAACL 9993 0 0
127-
RULE_9 DATAACL 9991 0 0
128-
RULE_10 DATAACL 9989 0 0
129-
DEFAULT_RULE DATAACL 1 0 0
130-
RULE_6 EVERFLOW 9994 0 0
131-
RULE_08 EVERFLOW 9992 0 0
127+
RULE NAME TABLE NAME PRIO PACKETS COUNT BYTES COUNT
128+
------------------------------------- ------------- ------ --------------- -------------
129+
RULE_1 DATAACL 9999 0 0
130+
RULE_2 DATAACL 9998 0 0
131+
RULE_3 DATAACL 9997 0 0
132+
RULE_4 DATAACL 9996 0 0
133+
RULE_05 DATAACL 9995 0 0
134+
RULE_7 DATAACL 9993 0 0
135+
RULE_9 DATAACL 9991 0 0
136+
RULE_10 DATAACL 9989 0 0
137+
DEFAULT_RULE DATAACL 1 0 0
138+
RULE_6 EVERFLOW 9994 0 0
139+
RULE_08 EVERFLOW 9992 0 0
140+
RULE_1 NULL_ROUTE_V4 9999 N/A N/A
141+
BLOCK_RULE_10.0.0.2/32 NULL_ROUTE_V4 9999 N/A N/A
142+
BLOCK_RULE_10.0.0.3/32 NULL_ROUTE_V4 9999 N/A N/A
143+
DEFAULT_RULE NULL_ROUTE_V4 1 N/A N/A
144+
RULE_1 NULL_ROUTE_V6 9999 N/A N/A
145+
BLOCK_RULE_1000:1000:1000:1000::2/128 NULL_ROUTE_V6 9999 N/A N/A
146+
BLOCK_RULE_1000:1000:1000:1000::3/128 NULL_ROUTE_V6 9999 N/A N/A
147+
DEFAULT_RULE NULL_ROUTE_V6 1 N/A N/A
132148
"""
133149

134150

0 commit comments

Comments
 (0)