|
| 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 | + |
0 commit comments