diff --git a/scripts/null_route_helper b/scripts/null_route_helper new file mode 100755 index 000000000000..2813ff5971b3 --- /dev/null +++ b/scripts/null_route_helper @@ -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 + + +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): + """ + 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 + # 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) + 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() + diff --git a/setup.py b/setup.py index 6c8a349c69c8..5847b6e6eedc 100644 --- a/setup.py +++ b/setup.py @@ -130,7 +130,8 @@ 'scripts/watermarkstat', 'scripts/watermarkcfg', 'scripts/sonic-kdump-config', - 'scripts/centralize_database' + 'scripts/centralize_database', + 'scripts/null_route_helper' ], entry_points={ 'console_scripts': [ diff --git a/tests/aclshow_test.py b/tests/aclshow_test.py index e41d56b9eb0c..9529be36891f 100644 --- a/tests/aclshow_test.py +++ b/tests/aclshow_test.py @@ -35,19 +35,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 @@ -80,8 +88,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 ----------- ------------ ------ --------------- ------------- @@ -116,19 +124,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 """ diff --git a/tests/mock_tables/config_db.json b/tests/mock_tables/config_db.json index 430fe3b16285..4c60cf992a2e 100644 --- a/tests/mock_tables/config_db.json +++ b/tests/mock_tables/config_db.json @@ -373,6 +373,44 @@ "VLAN_SUB_INTERFACE|Ethernet0.10": { "admin_status": "up" }, + "ACL_RULE|NULL_ROUTE_V4|DEFAULT_RULE": { + "PACKET_ACTION": "DROP", + "PRIORITY": "1" + }, + "ACL_RULE|NULL_ROUTE_V4|RULE_1": { + "PACKET_ACTION": "DROP", + "PRIORITY": "9999", + "SRC_IP": "10.0.0.1/32" + }, + "ACL_RULE|NULL_ROUTE_V4|BLOCK_RULE_10.0.0.2/32": { + "PACKET_ACTION": "DROP", + "PRIORITY": "9999", + "SRC_IP": "10.0.0.2/32" + }, + "ACL_RULE|NULL_ROUTE_V4|BLOCK_RULE_10.0.0.3/32": { + "PACKET_ACTION": "FORWARD", + "PRIORITY": "9999", + "SRC_IP": "10.0.0.3/32" + }, + "ACL_RULE|NULL_ROUTE_V6|DEFAULT_RULE": { + "PACKET_ACTION": "DROP", + "PRIORITY": "1" + }, + "ACL_RULE|NULL_ROUTE_V6|RULE_1": { + "PACKET_ACTION": "DROP", + "PRIORITY": "9999", + "SRC_IPV6": "1000:1000:1000:1000::1/128" + }, + "ACL_RULE|NULL_ROUTE_V6|BLOCK_RULE_1000:1000:1000:1000::2/128": { + "PACKET_ACTION": "DROP", + "PRIORITY": "9999", + "SRC_IPV6":"1000:1000:1000:1000::2/128" + }, + "ACL_RULE|NULL_ROUTE_V6|BLOCK_RULE_1000:1000:1000:1000::3/128": { + "PACKET_ACTION": "FORWARD", + "PRIORITY": "9999", + "SRC_IPV6":"1000:1000:1000:1000::3/128" + }, "ACL_RULE|DATAACL|DEFAULT_RULE": { "PACKET_ACTION": "DROP", "PRIORITY": "1" @@ -427,6 +465,16 @@ "priority": "9989", "SRC_IP": "10.0.0.3/32" }, + "ACL_TABLE|NULL_ROUTE_V4": { + "policy_desc": "DATAACL", + "ports@": "PortChannel0002,PortChannel0005,PortChannel0008,PortChannel0011,PortChannel0014,PortChannel0017,PortChannel0020,PortChannel0023", + "type": "L3" + }, + "ACL_TABLE|NULL_ROUTE_V6": { + "policy_desc": "DATAACL", + "ports@": "PortChannel0002,PortChannel0005,PortChannel0008,PortChannel0011,PortChannel0014,PortChannel0017,PortChannel0020,PortChannel0023", + "type": "L3V6" + }, "ACL_TABLE|DATAACL": { "policy_desc": "DATAACL", "ports@": "PortChannel0002,PortChannel0005,PortChannel0008,PortChannel0011,PortChannel0014,PortChannel0017,PortChannel0020,PortChannel0023,Ethernet64,Ethernet68,Ethernet72,Ethernet76,Ethernet80,Ethernet84,Ethernet88,Ethernet92,Ethernet96,Ethernet100,Ethernet104,Ethernet108,Ethernet112,Ethernet116,Ethernet120,Ethernet124", diff --git a/tests/null_route_helper_test.py b/tests/null_route_helper_test.py new file mode 100644 index 000000000000..f07a981aa356 --- /dev/null +++ b/tests/null_route_helper_test.py @@ -0,0 +1,206 @@ +import pytest +import os +import imp + +from click.testing import CliRunner +from swsssdk import ConfigDBConnector + +null_route_helper = imp.load_source('null_route_helper', os.path.join(os.path.dirname(__file__), '..', 'scripts','null_route_helper')) +null_route_helper.ConfigDBConnector = ConfigDBConnector + +expected_stdout_v4 = "" + \ +"""Table Rule Priority Action Match +------------- ---------------------- ---------- -------- ----------- +NULL_ROUTE_V4 BLOCK_RULE_10.0.0.2/32 9999 DROP 10.0.0.2/32 +NULL_ROUTE_V4 BLOCK_RULE_10.0.0.3/32 9999 FORWARD 10.0.0.3/32 +""" + +expected_stdout_v6 = "" + \ +"""Table Rule Priority Action Match +------------- ------------------------------------- ---------- -------- -------------------------- +NULL_ROUTE_V6 BLOCK_RULE_1000:1000:1000:1000::2/128 9999 DROP 1000:1000:1000:1000::2/128 +NULL_ROUTE_V6 BLOCK_RULE_1000:1000:1000:1000::3/128 9999 FORWARD 1000:1000:1000:1000::3/128 +""" + +def test_ip_validation(): + # Verify prefix len will be appended if not set + assert(null_route_helper.validate_input("1.2.3.4") == "1.2.3.4/32") + assert(null_route_helper.validate_input("::1") == "::1/128") + + assert(null_route_helper.validate_input("1.2.3.4/32") == "1.2.3.4/32") + + assert(null_route_helper.validate_input("1000:1000:1000:1000::1/128") == "1000:1000:1000:1000::1/128") + + with pytest.raises(SystemExit) as e: + null_route_helper.validate_input("a.b.c.d") + assert(e.value.code != 0) + + with pytest.raises(SystemExit) as e: + null_route_helper.validate_input("1.2.3.4/21/32") + assert(e.value.code != 0) + + # Verify only 32 prefix len is accepted for IPv4 + with pytest.raises(SystemExit) as e: + null_route_helper.validate_input("1.2.3.4/21") + assert(e.value.code != 0) + + # Verify only 128 prefix len is accepted for IPv6 + with pytest.raises(SystemExit) as e: + null_route_helper.validate_input("1000:1000:1000:1000::1/120") + assert(e.value.code != 0) + + +def test_confirm_required_table_existence(): + configdb = ConfigDBConnector() + configdb.connect() + + assert(null_route_helper.confirm_required_table_existence(configdb, "NULL_ROUTE_V4")) + assert(null_route_helper.confirm_required_table_existence(configdb, "NULL_ROUTE_V6")) + + with pytest.raises(SystemExit) as e: + null_route_helper.confirm_required_table_existence(configdb, "NULL_ROUTE_FAKE") + assert(e.value.code != 0) + + +def test_build_rule(): + expected_rule_v4 = { + "PRIORITY": "9999", + "PACKET_ACTION": "DROP", + "ETHER_TYPE": "2048", + "SRC_IP": "1.2.3.4/32" + } + expected_rule_v6 = { + "PRIORITY": "9999", + "PACKET_ACTION": "DROP", + "IP_TYPE": "IPV6ANY", + "SRC_IPV6": "1000:1000:1000:1000::1/128" + } + + assert(null_route_helper.build_acl_rule(9999, "1.2.3.4/32") == expected_rule_v4) + assert(null_route_helper.build_acl_rule(9999, "1000:1000:1000:1000::1/128") == expected_rule_v6) + + +def test_get_rule(): + configdb = ConfigDBConnector() + configdb.connect() + + assert(null_route_helper.get_rule(configdb, "NULL_ROUTE_ABSENT", "10.0.0.1/32") == None) + + assert(null_route_helper.get_rule(configdb, "NULL_ROUTE_V4", "10.0.0.1/32") == None) + assert(null_route_helper.get_rule(configdb, "NULL_ROUTE_V4", "10.0.0.2/32")) + + assert(null_route_helper.get_rule(configdb, "NULL_ROUTE_V6", "1000:1000:1000:1000::1/128") == None) + assert(null_route_helper.get_rule(configdb, "NULL_ROUTE_V6", "1000:1000:1000:1000::2/128")) + + +def test_run_when_table_absent(): + runner = CliRunner() + + result = runner.invoke(null_route_helper.cli.commands['block'], ['TABLE_ABSENT', '1.2.3.4']) + assert(result.exit_code != 0) + assert("not found" in result.output) + + result = runner.invoke(null_route_helper.cli.commands['unblock'], ['TABLE_ABSENT', '1.2.3.4']) + assert(result.exit_code != 0) + assert("not found" in result.output) + + +def test_run_with_invalid_ip(): + runner = CliRunner() + + result = runner.invoke(null_route_helper.cli.commands['block'], ['NULL_ROUTE_V4', 'a.b.c.d']) + assert(result.exit_code != 0) + assert("as a valid IP address" in result.output) + + result = runner.invoke(null_route_helper.cli.commands['block'], ['NULL_ROUTE_V6', 'xx:xx:xx:xx']) + assert(result.exit_code != 0) + assert("as a valid IP address" in result.output) + + result = runner.invoke(null_route_helper.cli.commands['unblock'], ['NULL_ROUTE_V4', 'a.b.c.d']) + assert(result.exit_code != 0) + assert("as a valid IP address" in result.output) + + result = runner.invoke(null_route_helper.cli.commands['unblock'], ['NULL_ROUTE_V6', 'xx:xx:xx:xx']) + assert(result.exit_code != 0) + assert("as a valid IP address" in result.output) + + result = runner.invoke(null_route_helper.cli.commands['block'], ['NULL_ROUTE_V4', '1.2.3.4/21']) + assert(result.exit_code != 0) + assert("Prefix length must be" in result.output) + + result = runner.invoke(null_route_helper.cli.commands['block'], ['NULL_ROUTE_V6', '::1/120']) + assert(result.exit_code != 0) + assert("Prefix length must be" in result.output) + + +def test_block(): + runner = CliRunner() + + # Verify block ip that is already blocked + result = runner.invoke(null_route_helper.cli.commands['block'], ['NULL_ROUTE_V4', '10.0.0.2/32']) + assert(result.exit_code == 0) + + # Verify block ip that is marked as forward + result = runner.invoke(null_route_helper.cli.commands['block'], ['NULL_ROUTE_V4', '10.0.0.3/32']) + assert(result.exit_code == 0) + + # Verify unblock ip that is not present in any rule + result = runner.invoke(null_route_helper.cli.commands['block'], ['NULL_ROUTE_V4', '10.0.0.4/32']) + assert(result.exit_code == 0) + + # Verify block ipv6 that is already blocked + result = runner.invoke(null_route_helper.cli.commands['block'], ['NULL_ROUTE_V6', '1000:1000:1000:1000::2/128']) + assert(result.exit_code == 0) + + # Verify block ipv6 that is marked as forward + result = runner.invoke(null_route_helper.cli.commands['block'], ['NULL_ROUTE_V6', '1000:1000:1000:1000::3/128']) + assert(result.exit_code == 0) + + # Verify block ipv6 that is not present in any rule + result = runner.invoke(null_route_helper.cli.commands['block'], ['NULL_ROUTE_V6', '1000:1000:1000:1000::4/128']) + assert(result.exit_code == 0) + + +def test_unblock(): + runner = CliRunner() + + # Verify unblock ip that is blocked + result = runner.invoke(null_route_helper.cli.commands['unblock'], ['NULL_ROUTE_V4', '10.0.0.2/32']) + assert(result.exit_code == 0) + + # Verify unblock ip that is not blocked + result = runner.invoke(null_route_helper.cli.commands['unblock'], ['NULL_ROUTE_V4', '10.0.0.3/32']) + assert(result.exit_code == 0) + + # Verify unblock ip that is not present in any rule + result = runner.invoke(null_route_helper.cli.commands['unblock'], ['NULL_ROUTE_V4', '10.0.0.4/32']) + assert(result.exit_code == 0) + + # Verify unblock ipv6 that is blocked + result = runner.invoke(null_route_helper.cli.commands['unblock'], ['NULL_ROUTE_V6', '1000:1000:1000:1000::2/128']) + assert(result.exit_code == 0) + + # Verify unblock ipv6 that is marked as forward + result = runner.invoke(null_route_helper.cli.commands['unblock'], ['NULL_ROUTE_V6', '1000:1000:1000:1000::3/128']) + assert(result.exit_code == 0) + + # Verify unblock ipv6 that is not present in any rule + result = runner.invoke(null_route_helper.cli.commands['unblock'], ['NULL_ROUTE_V6', '1000:1000:1000:1000::4/128']) + assert(result.exit_code == 0) + + +def test_list(): + runner = CliRunner() + + # Verify list rules in non-existing table + result = runner.invoke(null_route_helper.cli.commands['list'], ['FAKE_NULL_ROUTE_V4']) + assert(result.exit_code != 0) + + # Verify show IPv4 rules + result = runner.invoke(null_route_helper.cli.commands['list'], ['NULL_ROUTE_V4']) + assert(result.stdout == expected_stdout_v4) + + # Verify show IPv6 rules + result = runner.invoke(null_route_helper.cli.commands['list'], ['NULL_ROUTE_V6']) + assert(result.stdout == expected_stdout_v6) +