-
Notifications
You must be signed in to change notification settings - Fork 664
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add script null_route_helper #1718
Changes from all commits
19baad0
45896c3
047a5b4
4df2bce
574be48
330fac7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,267 @@ | ||
#!/usr/bin/env python3 | ||
|
||
""" | ||
Utility for blocking and unblocking traffic from given source ip address on ACL tables. | ||
|
||
The block operation will insert a DENY rule at the top of the table. The unblock operation | ||
will remove an existing DENY rule that has been created by the block operation (i.e. it does | ||
NOT insert an ALLOW rule, only removes DENY rules). | ||
|
||
Since SONiC supports multi ACL rules share the same priority, all ACL rules created by null_route_helper will | ||
use the highest priority(9999). | ||
|
||
Example: | ||
|
||
Block traffic from 10.2.3.4: | ||
./null_route_helper block acl_table_name 10.2.3.4 | ||
|
||
Unblock all traffic from 10.2.3.4: | ||
./null_route_helper unblock acl_table_name 10.2.3.4 | ||
|
||
List all acl rules added by this script | ||
./null_route_helper list acl_table_name | ||
""" | ||
|
||
|
||
from __future__ import print_function | ||
|
||
import syslog | ||
import sys | ||
import click | ||
import ipaddress | ||
import tabulate | ||
|
||
from swsscommon.swsscommon import ConfigDBConnector | ||
|
||
|
||
CONFIG_DB_ACL_TABLE_TABLE = "ACL_TABLE" | ||
CONFIG_DB_ACL_RULE_TABLE = "ACL_RULE" | ||
CONFIG_DB_VLAN_TABLE = "VLAN" | ||
|
||
ACTION_ALLOW = "FORWARD" | ||
ACTION_DENY = "DROP" | ||
ACTION_LIST = "LIST" | ||
|
||
# Since SONiC supports multi ACL rules share the same priority, we use 9999 (the highest) for all rules | ||
ACL_RULE_PRIORITY = 9999 | ||
# The key of rule will be overridden with BLOCK_RULE_ + ip | ||
ACL_RULE_PREFIX = 'BLOCK_RULE_' | ||
|
||
# Internet Protocol version 4 EtherType | ||
ETHER_TYPE_IPV4 = 0x0800 | ||
|
||
def notice(msg): | ||
""" | ||
Log a NOTICE message to the console and syslog | ||
""" | ||
syslog.syslog(syslog.LOG_NOTICE, msg) | ||
print(msg) | ||
|
||
|
||
def error(msg): | ||
""" | ||
Log an ERR message to the console and syslog, and exit the program with an error code | ||
""" | ||
syslog.syslog(syslog.LOG_ERR, msg) | ||
print(msg, file=sys.stderr) | ||
sys.exit(1) | ||
|
||
|
||
def ip_ver(ip_prefix): | ||
return ipaddress.ip_network(ip_prefix, False).version | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't like to add a global variable to save the ip version. Any better ideas? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't like to add a global variable to save the return value. Any better ideas? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't like global var either. To clarify, I mean the object of |
||
|
||
|
||
def confirm_required_table_existence(configdb, sub_table_name): | ||
""" | ||
Check the existence of required ACL table, and exit if absent | ||
""" | ||
target_table = configdb.get_entry(CONFIG_DB_ACL_TABLE_TABLE, sub_table_name) | ||
|
||
if not target_table: | ||
error("Table {} not found, exiting...".format(sub_table_name)) | ||
|
||
return True | ||
|
||
|
||
def get_acl_rule_key(ip_prefix): | ||
""" | ||
Get the key that will be used to refer to the ACL rule used to block traffic from a source ip. | ||
Since the rules are all given the same priority in SONiC, we can't identify a rule based on the priority. | ||
So, we use the destination IP being blocked to give each rule a unique name in the system. | ||
""" | ||
return ACL_RULE_PREFIX + str(ip_prefix) | ||
|
||
|
||
def get_all_acl_rules(configdb, table_name): | ||
""" | ||
Return a dict of existed acl rules | ||
{(u'NULL_ROUTE_TABLE', u'BLOCK_RULE_1.1.1.1/32'): {'PRIORITY': '9999', 'PACKET_ACTION': 'FORWARD', 'SRC_IP': '1.1.1.1/32'},...} | ||
""" | ||
key = CONFIG_DB_ACL_RULE_TABLE + '|' + table_name | ||
all_rules = configdb.get_table(key) | ||
block_rules = {} | ||
for k, v in all_rules.items(): | ||
if k[1].startswith(ACL_RULE_PREFIX): | ||
block_rules[k] = v | ||
|
||
return block_rules | ||
|
||
|
||
def validate_input(ip_address): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you reuse anything from utils? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
""" | ||
Validate the format of input | ||
""" | ||
try: | ||
ip_n = ipaddress.ip_network(ip_address, False) | ||
ver = ip_n.version | ||
prefix_len = ip_n.prefixlen | ||
# Prefix len must be 32 for IPV4 and 128 for IPV6 | ||
if ver == 4 and prefix_len == 32 or ver == 6 and prefix_len == 128: | ||
return ip_n.with_prefixlen | ||
|
||
error("Prefix length must be 32 (IPv4) or 128 (IPv6)") | ||
except ValueError as e: | ||
error("Could not parse {} as a valid IP address; exception={}".format(ip_address, e)) | ||
|
||
|
||
def build_acl_rule(priority, src_ip): | ||
""" | ||
Bild DROP rule for given src_ip and priority | ||
""" | ||
rule = { | ||
"PRIORITY": str(priority), | ||
"PACKET_ACTION": "DROP" | ||
} | ||
if ip_ver(src_ip) == 4: | ||
rule['ETHER_TYPE'] = str(ETHER_TYPE_IPV4) | ||
rule['SRC_IP'] = src_ip | ||
else: | ||
rule['IP_TYPE'] = 'IPV6ANY' | ||
rule['SRC_IPV6'] = src_ip | ||
|
||
return rule | ||
|
||
|
||
def get_rule(configdb, table_name, ip_prefix): | ||
""" | ||
Get Acl rule for given ip_prefix | ||
""" | ||
key_name = 'SRC_IP' if ip_ver(ip_prefix) == 4 else 'SRC_IPV6' | ||
all_rules = get_all_acl_rules(configdb, table_name) | ||
for key, rule in all_rules.items(): | ||
if ip_prefix == rule.get(key_name, None): | ||
if ip_prefix: | ||
return {key: rule} | ||
|
||
return None | ||
|
||
|
||
def update_acl_table(configdb, acl_table_name, ip_prefix, action): | ||
""" | ||
Update ACL table to apply new rules for given ip_prefix. 'action' is supposed to be in ['DENY', 'ALLOW'] | ||
For 'DENY', an 'DROP' rule for given ip_prefix will be added if not existed | ||
For 'ALLOW', we will try to remove the existing 'DENY' rule, and nothing is changed if not existed | ||
""" | ||
confirm_required_table_existence(configdb, acl_table_name) | ||
rule = get_rule(configdb, acl_table_name, ip_prefix) | ||
rule_key = list(rule.keys())[0] if rule else None | ||
rule_value = list(rule.values())[0] if rule else None | ||
if action == ACTION_ALLOW: | ||
if not rule: | ||
return | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think here we should be adding a rule even if it doesn't exist, may be for managing priorities. Why is it skipped here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because it will be allowed anyway? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So no need to add a rule. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I skip here since there will be a default ALLOW rule in the pre-created ACL table. Is it necessary to add an ALLOW rule for a certain prefix? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. my concern is, lets say user specifies "unblock 1.2.3.4/32" and if there is some rule in between that says 1.2.3.0/24 drop. In this case if we don't explicitly add the "allow" rule, it will get dropped. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good catch. We discuss about it offline, and confirm that all prefix len are 32 (For IPv4) or 128 (IPv6). Based on this acknowledge, it shouldn't be a issue. I will update the |
||
# Delete existing BLOCK rule for given ip_prefix | ||
# Pass None as data will delete the entry | ||
configdb.mod_entry(CONFIG_DB_ACL_RULE_TABLE, rule_key, None) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. I checked the code and confirm the entry will be deleted. Also verified in test. |
||
else: | ||
if rule: | ||
if rule_value['PACKET_ACTION'] == 'DROP': | ||
return | ||
else: | ||
# If there is 'FORWARDED' ACL rule, then change it to 'DROP' | ||
rule_value['PACKET_ACTION'] = 'DROP' | ||
configdb.mod_entry(CONFIG_DB_ACL_RULE_TABLE, rule_key, rule_value) | ||
else: | ||
priority = ACL_RULE_PRIORITY | ||
new_rule_key = (acl_table_name, get_acl_rule_key(ip_prefix)) | ||
new_rule_value = build_acl_rule(priority, ip_prefix) | ||
configdb.set_entry(CONFIG_DB_ACL_RULE_TABLE, new_rule_key, new_rule_value) | ||
|
||
|
||
def list_all_null_route_rules(configdb, table_name): | ||
""" | ||
List all rules added by this script | ||
""" | ||
|
||
confirm_required_table_existence(configdb, table_name) | ||
header = ("Table", "Rule", "Priority", "Action", "Match") | ||
all_rules = get_all_acl_rules(configdb, table_name) | ||
|
||
match_keys = ["SRC_IP", "SRC_IPV6"] | ||
data = [] | ||
for (_, rule_id), rule in all_rules.items(): | ||
priority = rule.get("PRIORITY", "N/A") | ||
action = rule.get("PACKET_ACTION", "N/A") | ||
match = "N/A" | ||
for k in match_keys: | ||
if k in rule: | ||
match = rule[k] | ||
break | ||
|
||
data.append([table_name, rule_id, priority, action, match]) | ||
|
||
print(tabulate.tabulate(data, headers=header, tablefmt="simple", missingval="")) | ||
|
||
|
||
def null_route_helper(table_name, action, ip_prefix=None): | ||
""" | ||
Helper function called by 'click'. | ||
""" | ||
configdb = ConfigDBConnector() | ||
configdb.connect() | ||
if action == ACTION_LIST: | ||
list_all_null_route_rules(configdb, table_name) | ||
else: | ||
ip_prefix = validate_input(ip_prefix) | ||
update_acl_table(configdb, table_name, ip_prefix, action) | ||
|
||
|
||
@click.group() | ||
def cli(): | ||
pass | ||
|
||
|
||
# ./null_route_helper block table_name 1.2.3.4 | ||
@cli.command('block') | ||
@click.argument("table_name", type=click.STRING, required=True) | ||
@click.argument("ip_prefix", type=click.STRING, required=True) | ||
def block(table_name, ip_prefix): | ||
""" | ||
Block traffic from given src ip prefix | ||
""" | ||
null_route_helper(table_name, ACTION_DENY, ip_prefix) | ||
|
||
|
||
# ./null_route_helper unblock table_name 1.2.3.4 | ||
@cli.command('unblock') | ||
@click.argument("table_name", type=click.STRING, required=True) | ||
@click.argument("ip_prefix", type=click.STRING, required=True) | ||
def unblock(table_name, ip_prefix): | ||
""" | ||
Unblock traffic from given src ip prefix | ||
""" | ||
null_route_helper(table_name, ACTION_ALLOW, ip_prefix) | ||
|
||
|
||
# ./null_route_helper list table_name | ||
@cli.command('list') | ||
@click.argument("table_name", type=click.STRING, required=True) | ||
def list_rules(table_name): | ||
""" | ||
List all rules *added by this script* | ||
""" | ||
null_route_helper(table_name, ACTION_LIST) | ||
|
||
|
||
if __name__ == "__main__": | ||
cli() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need this feature on 201911 branch, so please make sure it could run on python2. Seems we could not cherry-pick directly. #WontFix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you tested the same script with both python2/3, you can change it to
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#!/usr/bin/env python
doesn't work on202012
image since it will point to python2, and several packages are missing. I think the only way to solve the issue is to create another PR for201911
or earlier image. It's not a clean cherry-pick.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sonic-utilities can no longer be cherry-picked to 201911. We faced it few times and suggest to create a new PR to 201911