Skip to content
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

Merged
merged 6 commits into from
Aug 4, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
267 changes: 267 additions & 0 deletions scripts/null_route_helper
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
#!/usr/bin/env python3
Copy link
Contributor

@qiluo-msft qiluo-msft Jul 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

python3

We need this feature on 201911 branch, so please make sure it could run on python2. Seems we could not cherry-pick directly. #WontFix

Copy link
Contributor

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

#!/usr/bin/env python

Copy link
Contributor Author

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 on 202012 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 for 201911 or earlier image. It's not a clean cherry-pick.

Copy link
Contributor

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


"""
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/32:
./null_route_helper block acl_table_name 10.2.3.4/32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/32

we can remove the prefix completely from CLI

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I will update.


Unblock all traffic from 10.2.3.4/32:
./null_route_helper unblock acl_table_name 10.2.3.4/32

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
Copy link
Contributor

@qiluo-msft qiluo-msft Jul 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ip_network

If you keep this variable, you can reuse it in future, such as check prefix length. #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The 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 ip_network is very useful, and you could deduce many attributes from it such as prefix length, not only the version.



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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you reuse anything from utils?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validate_input in this PR will add prefix len for IP addresses that don't explicitly set prefix. For example, 1.2.3.4 is changed to 1.2.3.4/32. The common utils may not do this.

"""
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
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it will be allowed anyway?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

Copy link
Contributor

@qiluo-msft qiluo-msft Jul 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So no need to add a rule.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?

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?

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 validate_input interface to ensure that. Thanks

# 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)
Copy link
Contributor

@qiluo-msft qiluo-msft Jul 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mod_entry

Do you actually delete the rule? add some comment to help reading. #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
https://github.com/Azure/sonic-swss-common/blob/bf8c832cf1c7a6e72d7b1c843888ffb4a27088c8/common/configdb.cpp#L96

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/32
@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/32
@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()

3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@
'scripts/watermarkstat',
'scripts/watermarkcfg',
'scripts/sonic-kdump-config',
'scripts/centralize_database'
'scripts/centralize_database',
'scripts/null_route_helper'
],
entry_points={
'console_scripts': [
Expand Down
72 changes: 44 additions & 28 deletions tests/aclshow_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,27 @@

# Expected output for aclshow -a
all_output = '' + \
"""RULE NAME TABLE NAME PRIO PACKETS COUNT BYTES COUNT
------------ ------------ ------ --------------- -------------
RULE_1 DATAACL 9999 101 100
RULE_2 DATAACL 9998 201 200
RULE_3 DATAACL 9997 301 300
RULE_4 DATAACL 9996 401 400
RULE_05 DATAACL 9995 0 0
RULE_7 DATAACL 9993 701 700
RULE_9 DATAACL 9991 901 900
RULE_10 DATAACL 9989 1001 1000
DEFAULT_RULE DATAACL 1 2 1
RULE_6 EVERFLOW 9994 601 600
RULE_08 EVERFLOW 9992 0 0
"""RULE NAME TABLE NAME PRIO PACKETS COUNT BYTES COUNT
------------------------------------- ------------- ------ --------------- -------------
RULE_1 DATAACL 9999 101 100
RULE_2 DATAACL 9998 201 200
RULE_3 DATAACL 9997 301 300
RULE_4 DATAACL 9996 401 400
RULE_05 DATAACL 9995 0 0
RULE_7 DATAACL 9993 701 700
RULE_9 DATAACL 9991 901 900
RULE_10 DATAACL 9989 1001 1000
DEFAULT_RULE DATAACL 1 2 1
RULE_6 EVERFLOW 9994 601 600
RULE_08 EVERFLOW 9992 0 0
RULE_1 NULL_ROUTE_V4 9999 N/A N/A
BLOCK_RULE_10.0.0.2/32 NULL_ROUTE_V4 9999 N/A N/A
BLOCK_RULE_10.0.0.3/32 NULL_ROUTE_V4 9999 N/A N/A
DEFAULT_RULE NULL_ROUTE_V4 1 N/A N/A
RULE_1 NULL_ROUTE_V6 9999 N/A N/A
BLOCK_RULE_1000:1000:1000:1000::2/128 NULL_ROUTE_V6 9999 N/A N/A
BLOCK_RULE_1000:1000:1000:1000::3/128 NULL_ROUTE_V6 9999 N/A N/A
DEFAULT_RULE NULL_ROUTE_V6 1 N/A N/A
"""

# Expected output for aclshow -r RULE_1 -t DATAACL
Expand Down Expand Up @@ -78,8 +86,8 @@
# Expected output for aclshow -r RULE_4,RULE_6 -vv
rule4_rule6_verbose_output = '' + \
"""Reading ACL info...
Total number of ACL Tables: 8
Total number of ACL Rules: 11
Total number of ACL Tables: 10
Total number of ACL Rules: 19

RULE NAME TABLE NAME PRIO PACKETS COUNT BYTES COUNT
----------- ------------ ------ --------------- -------------
Expand Down Expand Up @@ -114,19 +122,27 @@
# Expected output for
# aclshow -a -c ; aclshow -a
all_after_clear_output = '' + \
"""RULE NAME TABLE NAME PRIO PACKETS COUNT BYTES COUNT
------------ ------------ ------ --------------- -------------
RULE_1 DATAACL 9999 0 0
RULE_2 DATAACL 9998 0 0
RULE_3 DATAACL 9997 0 0
RULE_4 DATAACL 9996 0 0
RULE_05 DATAACL 9995 0 0
RULE_7 DATAACL 9993 0 0
RULE_9 DATAACL 9991 0 0
RULE_10 DATAACL 9989 0 0
DEFAULT_RULE DATAACL 1 0 0
RULE_6 EVERFLOW 9994 0 0
RULE_08 EVERFLOW 9992 0 0
"""RULE NAME TABLE NAME PRIO PACKETS COUNT BYTES COUNT
------------------------------------- ------------- ------ --------------- -------------
RULE_1 DATAACL 9999 0 0
RULE_2 DATAACL 9998 0 0
RULE_3 DATAACL 9997 0 0
RULE_4 DATAACL 9996 0 0
RULE_05 DATAACL 9995 0 0
RULE_7 DATAACL 9993 0 0
RULE_9 DATAACL 9991 0 0
RULE_10 DATAACL 9989 0 0
DEFAULT_RULE DATAACL 1 0 0
RULE_6 EVERFLOW 9994 0 0
RULE_08 EVERFLOW 9992 0 0
RULE_1 NULL_ROUTE_V4 9999 N/A N/A
BLOCK_RULE_10.0.0.2/32 NULL_ROUTE_V4 9999 N/A N/A
BLOCK_RULE_10.0.0.3/32 NULL_ROUTE_V4 9999 N/A N/A
DEFAULT_RULE NULL_ROUTE_V4 1 N/A N/A
RULE_1 NULL_ROUTE_V6 9999 N/A N/A
BLOCK_RULE_1000:1000:1000:1000::2/128 NULL_ROUTE_V6 9999 N/A N/A
BLOCK_RULE_1000:1000:1000:1000::3/128 NULL_ROUTE_V6 9999 N/A N/A
DEFAULT_RULE NULL_ROUTE_V6 1 N/A N/A
"""

class Aclshow():
Expand Down
Loading