diff --git a/docs/module/routing-acl.txt b/docs/module/routing-acl.txt index c8b8107371..6bbf33d1e8 100644 --- a/docs/module/routing-acl.txt +++ b/docs/module/routing-acl.txt @@ -33,13 +33,13 @@ Each ACL entry can have these attributes: * **sequence**: Statement sequence number (default: if unspecified, Netlab will generate a sequence starting with 10, with an increment of 10) * **protocol**: IP protocol to match — one of **ahp**, **esp**, **icmp**, **ip**, **ipv6**, **tcp**, **udp**, or a numeric protocol number. **Required**. * **established**: Match established TCP sessions (boolean) -* **src**: Dictionary for source address/port match — see [](routing-acl-matching). Default value: `prefix: any` -* **dst**: Dictionary for destination address/port match — see [](routing-acl-matching). Default value: `prefix: any` +* **src**: Dictionary for source address/port match — see [](routing-acl-matching-addr) and [](routing-acl-matching-port). Default value: `prefix: any` +* **dst**: Dictionary for destination address/port match. Default value: `prefix: any` * **log**: Log matched packets (boolean). * **description**: Free-text description of the ACL entry -(routing-acl-matching)= -### Matching Addresses +(routing-acl-matching-addr)= +### Matching Source and Destination Addresses Both **src** and **dst** dictionaries use the following attributes to describe what an ACL entry matches on the source and destination side of a packet. @@ -54,21 +54,25 @@ You can use either a single value or a list of values in all of the above parame You can also mix IPv4 and IPv6 addresses in an access list. _netlab_ always generates address-family-specific access lists for all address families used on the device. -Port matching (TCP/UDP only) is specified within the same **src**/**dst** dictionary: +(routing-acl-matching-port)= +### Matching TCP/UDP Ports -* **port_op**: Comparison operator — **eq** (default), **gt**, **lt**, **neq**, **in**, **not_in** -* **port**: A single port number to match with **port_op**. Valid operators for port are: **eq**, **gt**, **lt**, **neq** -* **port_range**: A dictionary with **min** and **max** specifying a range. Valid operators for **port_range** are **in** and **not_in** +Port matching parameters (TCP/UDP only) are specified within the **src.port**/**dst.port** dictionaries. These dictionaries can have one or more matching parameters: -Port-matching operations cannot be specified in both the source and destination dictionaries simultaneously. **not_in** operator is not supported in hardware. Netlab will generate synthetic ACL entries. Example from a Cisco IOL-XE router: +* **eq** (int or list): List of allowed ports (or a single allowed port) +* **neq** (int): A port that is not allowed[^BUEQ] +* **lt** (int): Ports lower than the specified value +* **gt** (int): Ports greater than the specified value +* **in** (list with two values): Ports within the specified range +* **not_in** (list with two values): Ports outside of the specified range -```text - 10 remark Allow dogs in - 11 permit tcp 10.10.10.0 0.0.0.255 1.5.6.0 0.0.0.255 lt 5 - 12 permit tcp 10.10.10.0 0.0.0.255 1.5.6.0 0.0.0.255 gt 100 -``` +[^BUEQ]: It might be better to use a separate entry with opposite **permit** action and **port.eq** parameter if you want to match ports not being equal to a list of ports. -In the above example, **not_in** operator has been split into two entries, one guarding the lower range, one guarding the upper range. The lower-range match retained the original sequence number, while the upper-range entry got the next one. +```{note} +* The **eq** parameter can be combined with any other parameter. You can also combine **lt** and **gt** parameters, others (**neq**, **in**, **not_in**) cannot be combined. +* Multiple port parameters, or multiple ports specified in the **‌eq** parameter, are expanded into multiple ACL entries. +* Most platforms do not support the *‌not in range* operation. The **‌not_in** parameter is thus rewritten as a combination of **‌lt** and **‌gt** parameters. +``` ## Applying ACLs to Interfaces diff --git a/netsim/modules/routing.yml b/netsim/modules/routing.yml index f8279cfa49..e98b6c93ee 100644 --- a/netsim/modules/routing.yml +++ b/netsim/modules/routing.yml @@ -295,27 +295,27 @@ _top: # Modification of global defaults _subtype: str _valid_with: [node] _invalid_with: [interface] - port_op: - type: str - valid_values: [eq, gt, lt, neq, in, not_in] - _default: eq port: - type: int - min_value: 0 - max_value: 65535 - _invalid_with: port_range - port_range: - min: - type: int - min_value: 0 - max_value: 65535 - _required: True - max: - type: int - min_value: 0 - max_value: 65535 - _required: True - _invalid_with: { port_op: [eq, gt, lt, neq] } + eq: { type: list, _subtype: acl_port } + neq: acl_port + lt: + type: acl_port + _invalid_with: [ neq, in, not_in ] # Full list of restrictions + gt: + type: acl_port + _invalid_with: [ neq, in, not_in ] # Full list of restrictions + in: + type: list + _subtype: acl_port + min_length: 2 + max_length: 2 + _invalid_with: [ neq, not_in ] # Minimized to what's not already restricted + not_in: + type: list + _subtype: acl_port + min_length: 2 + max_length: 2 + _invalid_with: [ neq ] # Minimized to what's not already restricted acl_entry: action: @@ -349,6 +349,11 @@ _top: # Modification of global defaults description: type: str + acl_port: + type: int + min_value: 1 + max_value: 65535 + features: policy: set: Route map SET attributes diff --git a/netsim/modules/routing/acl.py b/netsim/modules/routing/acl.py index 24c06fce02..ceaa5a9fe6 100644 --- a/netsim/modules/routing/acl.py +++ b/netsim/modules/routing/acl.py @@ -33,7 +33,9 @@ def add_acl_prefixes(p_entry: Box, data: Box) -> None: for node_name in p_entry.get('node',[]): node_data = topology.nodes[node_name] - intf_list = node_data.get("interfaces", []) + node_data.get("loopback",[]) + intf_list = node_data.get("interfaces", []) + if 'loopback' in node_data: + intf_list += [ node_data.loopback ] if "interface" in p_entry: ifname = p_entry.interface @@ -95,7 +97,6 @@ def validate_acl_address_entry(p_entry: Box, ctx: validation_context) -> None: UDP = 17 TCP = 6 - port_keys = ("port", "port_range") if ctx.protocol < 0 or ctx.protocol > 255: log.error( @@ -116,65 +117,20 @@ def validate_acl_address_entry(p_entry: Box, ctx: validation_context) -> None: for direction in ("src", "dst"): entry = p_entry[direction] - if any(k in entry for k in port_keys) and ctx.protocol not in (TCP, UDP): + if 'port' in entry and ctx.protocol not in (TCP, UDP): log.error( f"ACL {ctx.p_name} entry {ctx.idx} cannot use a port or " f"port range in {direction} address with this protocol. Use UDP/TCP", category=log.IncorrectAttr, ) - if "port_range" in entry and entry.port_range.min >= entry.port_range.max: - log.error( - f"ACL {ctx.p_name} entry {ctx.idx} has an invalid {direction} port range: min greater or equal to max", - category=log.IncorrectAttr, - ) - - # get rid of port_op if we do not need it - if not any(k in entry for k in port_keys): - entry.pop("port_op", None) - - if any(k in p_entry.src for k in port_keys) and any(k in p_entry.dst for k in port_keys): - log.error( - f"ACL {ctx.p_name} entry {ctx.idx} cannot specify a port or port range in both source and destination address", - category=log.IncorrectAttr, - ) - - -def expand_acl_description(entry: Box, expansion: list) -> None: - if "description" not in entry: - return - - description_seq = entry.get("sequence") - entry.sequence = description_seq + 1 # the real entry moves one past it - description_entry = get_box({"sequence": description_seq, "description": entry.pop("description")}) - expansion.append(description_entry) - - -def expand_acl_portop(entry: Box) -> list: - expansion = [] - - for addr_key in ("src", "dst"): - addr_entry = entry.get(addr_key) - if not addr_entry: continue - port_range = addr_entry.get("port_range") - if not port_range or addr_entry.get("port_op") != "not_in": - continue - - port_min = port_range.min - port_max = port_range.max - - upper_entry = get_box(entry.to_dict()) - upper_entry.sequence = entry.sequence + 1 - entry[addr_key].port = port_min - entry[addr_key].port_op = "lt" - entry[addr_key].pop("port_range", None) - upper_entry[addr_key].port = port_max - upper_entry[addr_key].port_op = "gt" - upper_entry[addr_key].pop("port_range", None) - expansion.append(upper_entry) - - return expansion + # Convert "not in" into a combination of "gt"/"lt" + if 'port.not_in' in entry: + port_list = sorted(entry.port.not_in) + entry.port.lt = port_list[0] + entry.port.gt = port_list[1] + entry.port.pop('not_in') def expand_acl(p_name: str, o_name: str, node: Box, topology: Box) -> typing.Optional[list]: @@ -201,15 +157,47 @@ def expand_af_acl(acl_list: list,acl_af: str, acl_name: str, node_name: str) -> """ if not acl_list: # Nothing to do return acl_list + acl_sequence = 100 acl_result: list = [] + + def get_port_op(direction: str, port_op: str, port_value: typing.Union[int,list]) -> dict: + if port_op == 'none': + return {} + + port_data: dict = { + 'port_op' : port_op + } + if isinstance(port_value,list): + port_value = sorted(port_value) + port_data['port_range'] = { 'min': port_value[0], 'max': port_value[1] } + else: + port_data['port'] = port_value + return { direction: port_data } + + def generate_acl_items(acl_data: Box, src_port: Box, dst_port: Box) -> None: + """ + Add port information to ACL entry with SRC/DST addresses. Iterate over all + port qualifiers, and over all port values (but only for "eq" condition) + """ + nonlocal acl_sequence, acl_result + + for s_port_op, s_port in src_port.items(): + for s_port_n in s_port if s_port_op == 'eq' else [ s_port ]: + acl_sp_item = acl_data + get_port_op('src',s_port_op,s_port_n) + for d_port_op, d_port in dst_port.items(): + for d_port_n in d_port if d_port_op == 'eq' else [ d_port ]: + acl_final = acl_sp_item + get_port_op('dst',d_port_op,d_port_n) + acl_final.sequence = acl_sequence + acl_result.append(acl_final) + acl_sequence += 10 + for acl_idx,acl_entry in enumerate(acl_list,1): # Iterate over all ACL entries src_list = acl_entry.src.get(acl_af,[]) # Get source/destination AF-specific entries dst_list = acl_entry.dst.get(acl_af,[]) - acl_rest = { k:v for k,v in acl_entry.items() if k not in ['src','dst'] } - src_data = { k:v for k,v in acl_entry.src.items() if k not in log.AF_LIST } - dst_data = { k:v for k,v in acl_entry.dst.items() if k not in log.AF_LIST } - acl_data = get_box(acl_rest) + { 'src': src_data } + {'dst': dst_data } + acl_data = get_box({ k:v for k,v in acl_entry.items() if k not in ['src','dst'] }) + src_port = acl_entry.src.get('port',{'none': 0 }) + dst_port = acl_entry.dst.get('port',{'none': 0 }) if not src_list and not dst_list: # No usable AF entries? Move on... continue @@ -233,13 +221,7 @@ def expand_af_acl(acl_list: list,acl_af: str, acl_name: str, node_name: str) -> if acl_item[kw][acl_af].endswith('/0'): acl_item[kw].any = True - acl_item.sequence = acl_sequence - acl_result.append(acl_item) - extra_items = expand_acl_portop(acl_item) - if extra_items: - acl_result.extend(extra_items) - - acl_sequence += 10 + generate_acl_items(acl_item,src_port,dst_port) return acl_result diff --git a/tests/integration/routing/30-acl-ipv4.yml b/tests/integration/routing/30-acl-ipv4.yml index b38d655ac2..62d86e184b 100644 --- a/tests/integration/routing/30-acl-ipv4.yml +++ b/tests/integration/routing/30-acl-ipv4.yml @@ -29,23 +29,19 @@ routing.acl: protocol: tcp src.ipv4: 172.16.0.1 dst.ipv4: 192.168.0.2 - dst.port: 100 + dst.port.eq: 100 - action: deny protocol: tcp src.node: h1 src.role: probe dst.pool: lan_h2 - dst.port_range.min: 50 - dst.port_range.max: 150 - dst.port_op: in + dst.port.in: [ 50, 150 ] - action: permit protocol: tcp src.node: h1 src.interface: eth1 dst.prefix: s_pfx - dst.port_range.min: 50 - dst.port_range.max: 150 - dst.port_op: not_in + dst.port.not_in: [ 50, 150 ] h1_est: - protocol: tcp established: true diff --git a/tests/integration/routing/31-acl-ipv6.yml b/tests/integration/routing/31-acl-ipv6.yml index 43e32aedbc..0fe3fc0203 100644 --- a/tests/integration/routing/31-acl-ipv6.yml +++ b/tests/integration/routing/31-acl-ipv6.yml @@ -34,23 +34,19 @@ routing.acl: protocol: tcp src.ipv6: 2001:db8:2::1 dst.ipv6: 2001:db8:5::2 - dst.port: 100 + dst.port.eq: 100 - action: deny protocol: tcp src.node: h1 src.role: probe dst.pool: lan_h2 - dst.port_range.min: 50 - dst.port_range.max: 150 - dst.port_op: in + dst.port.in: [ 50, 150 ] - action: permit protocol: tcp src.node: h1 src.interface: eth1 dst.prefix: s_pfx - dst.port_range.min: 50 - dst.port_range.max: 150 - dst.port_op: not_in + dst.port.not_in: [ 50, 150 ] h1_est: - protocol: tcp established: true diff --git a/tests/topology/expected/acl-simple.yml b/tests/topology/expected/acl-simple.yml index d4100bd4a5..9a61af6ff7 100644 --- a/tests/topology/expected/acl-simple.yml +++ b/tests/topology/expected/acl-simple.yml @@ -3,25 +3,123 @@ input: - package:topology-defaults.yml links: - _linkname: links[1] - bridge: input_1 interfaces: - ifindex: 1 ifname: eth1 - ipv4: 172.16.0.1/24 + ipv4: 192.168.0.2/24 node: n1 routing: acl: in: h1_filter + - ifindex: 1 + ifname: eth1 + ipv4: 192.168.0.1/24 + node: h1 linkindex: 1 - node_count: 1 + node_count: 2 + prefix: + _name: s_pfx + ipv4: 192.168.0.0/24 + type: p2p +- _linkname: links[2] + interfaces: + - ifindex: 2 + ifname: eth2 + ipv4: 192.168.0.1/24 + node: n1 + - ifindex: 1 + ifname: eth1 + ipv4: 192.168.0.3/24 + node: h2 + linkindex: 2 + node_count: 2 + pool: lan_h2 prefix: - ipv4: 172.16.0.0/24 - role: stub - type: stub + allocation: id_based + ipv4: 192.168.0.0/24 + type: p2p module: - routing name: input nodes: + h1: + af: + ipv4: true + box: none + device: none + id: 2 + interfaces: + - ifindex: 1 + ifname: eth1 + ipv4: 192.168.0.1/24 + linkindex: 1 + name: h1 -> n1 + neighbors: + - ifname: eth1 + ipv4: 192.168.0.2/24 + node: n1 + routing: + acl: + in: h1_filter + type: p2p + - ifindex: 0 + ifname: Loopback0 + ipv4: 10.0.0.2/32 + neighbors: [] + type: loopback + virtual_interface: true + loopback: + ifindex: 0 + ifname: Loopback0 + ipv4: 10.0.0.2/32 + neighbors: [] + type: loopback + virtual_interface: true + mgmt: + ifname: eth0 + ipv4: 192.168.121.102 + mac: ca:fe:00:02:00:00 + module: + - routing + name: h1 + h2: + af: + ipv4: true + box: none + device: none + id: 3 + interfaces: + - ifindex: 1 + ifname: eth1 + ipv4: 192.168.0.3/24 + linkindex: 2 + name: h2 -> n1 + neighbors: + - ifname: eth2 + ipv4: 192.168.0.1/24 + node: n1 + pool: lan_h2 + type: p2p + - ifindex: 0 + ifname: Loopback0 + ipv4: 10.0.0.3/32 + neighbors: [] + type: loopback + virtual_interface: true + loopback: + ifindex: 0 + ifname: Loopback0 + ipv4: 10.0.0.3/32 + neighbors: [] + type: loopback + virtual_interface: true + mgmt: + ifname: eth0 + ipv4: 192.168.121.103 + mac: ca:fe:00:03:00:00 + module: + - routing + name: h2 n1: af: ipv4: true @@ -29,19 +127,31 @@ nodes: device: none id: 1 interfaces: - - bridge: input_1 - ifindex: 1 + - ifindex: 1 ifname: eth1 - ipv4: 172.16.0.1/24 + ipv4: 192.168.0.2/24 linkindex: 1 - name: n1 -> stub - neighbors: [] - role: stub + name: n1 -> h1 + neighbors: + - ifname: eth1 + ipv4: 192.168.0.1/24 + node: h1 routing: acl: in: ipv4: h1_filter - type: stub + type: p2p + - ifindex: 2 + ifname: eth2 + ipv4: 192.168.0.1/24 + linkindex: 2 + name: n1 -> h2 + neighbors: + - ifname: eth1 + ipv4: 192.168.0.3/24 + node: h2 + pool: lan_h2 + type: p2p loopback: ifindex: 0 ifname: Loopback0 @@ -71,36 +181,160 @@ nodes: src: any: true ipv4: 0.0.0.0/0 - - description: Named prefix -> host (not in port range) + - description: Allowed server ports sequence: 120 + - action: permit + dst: + ipv4: 192.168.0.3/24 + port: 25 + port_op: eq + protocol: tcp + sequence: 130 + src: + ipv4: 192.168.0.1/24 + - action: permit + dst: + ipv4: 192.168.0.3/24 + port: 80 + port_op: eq + protocol: tcp + sequence: 140 + src: + ipv4: 192.168.0.1/24 + - action: permit + dst: + ipv4: 192.168.0.3/24 + port: 443 + port_op: eq + protocol: tcp + sequence: 150 + src: + ipv4: 192.168.0.1/24 + - action: permit + dst: + ipv4: 10.0.0.3/32 + port: 25 + port_op: eq + protocol: tcp + sequence: 160 + src: + ipv4: 192.168.0.1/24 + - action: permit + dst: + ipv4: 10.0.0.3/32 + port: 80 + port_op: eq + protocol: tcp + sequence: 170 + src: + ipv4: 192.168.0.1/24 + - action: permit + dst: + ipv4: 10.0.0.3/32 + port: 443 + port_op: eq + protocol: tcp + sequence: 180 + src: + ipv4: 192.168.0.1/24 + - action: permit + dst: + ipv4: 192.168.0.3/24 + port: 25 + port_op: eq + protocol: tcp + sequence: 190 + src: + ipv4: 10.0.0.2/32 + - action: permit + dst: + ipv4: 192.168.0.3/24 + port: 80 + port_op: eq + protocol: tcp + sequence: 200 + src: + ipv4: 10.0.0.2/32 + - action: permit + dst: + ipv4: 192.168.0.3/24 + port: 443 + port_op: eq + protocol: tcp + sequence: 210 + src: + ipv4: 10.0.0.2/32 + - action: permit + dst: + ipv4: 10.0.0.3/32 + port: 25 + port_op: eq + protocol: tcp + sequence: 220 + src: + ipv4: 10.0.0.2/32 + - action: permit + dst: + ipv4: 10.0.0.3/32 + port: 80 + port_op: eq + protocol: tcp + sequence: 230 + src: + ipv4: 10.0.0.2/32 + - action: permit + dst: + ipv4: 10.0.0.3/32 + port: 443 + port_op: eq + protocol: tcp + sequence: 240 + src: + ipv4: 10.0.0.2/32 + - description: Named prefix -> host (not in port range) + sequence: 250 - action: permit dst: host: true ipv4: 10.0.0.1 - port: 80 + port: 1 port_op: lt protocol: tcp - sequence: 130 + sequence: 260 src: ipv4: 192.168.0.0/24 - action: permit dst: host: true ipv4: 10.0.0.1 - port: 443 + port: 1024 port_op: gt protocol: tcp - sequence: 131 + sequence: 270 + src: + ipv4: 192.168.0.0/24 + - description: Port.in test + sequence: 280 + - action: deny + dst: + host: true + ipv4: 10.0.0.42 + port_op: in + port_range: + max: 64 + min: 1 + protocol: tcp + sequence: 290 src: ipv4: 192.168.0.0/24 - description: Permit all UDP - sequence: 140 + sequence: 300 - action: permit dst: any: true ipv4: 0.0.0.0/0 protocol: udp - sequence: 150 + sequence: 310 src: any: true ipv4: 0.0.0.0/0 @@ -120,17 +354,47 @@ nodes: - 0.0.0.0/0 ipv6: - ::/0 + - action: permit + description: Allowed server ports + dst: + ipv4: + - 192.168.0.3/24 + - 10.0.0.3/32 + port: + eq: + - 25 + - 80 + - 443 + protocol: tcp + sequence: 20 + src: + ipv4: + - 192.168.0.1/24 + - 10.0.0.2/32 - action: permit description: Named prefix -> host (not in port range) dst: ipv4: - 10.0.0.1 - port_op: not_in - port_range: - max: 443 - min: 80 + port: + gt: 1024 + lt: 1 protocol: tcp - sequence: 20 + sequence: 30 + src: + ipv4: + - 192.168.0.0/24 + - action: deny + description: Port.in test + dst: + ipv4: + - 10.0.0.42 + port: + in: + - 1 + - 64 + protocol: tcp + sequence: 40 src: ipv4: - 192.168.0.0/24 @@ -142,7 +406,7 @@ nodes: ipv6: - ::/0 protocol: udp - sequence: 30 + sequence: 50 src: ipv4: - 0.0.0.0/0 @@ -153,6 +417,7 @@ prefix: ipv4: 0.0.0.0/0 ipv6: ::/0 s_pfx: + _name: s_pfx ipv4: 192.168.0.0/24 provider: libvirt routing: @@ -172,17 +437,47 @@ routing: - 0.0.0.0/0 ipv6: - ::/0 + - action: permit + description: Allowed server ports + dst: + ipv4: + - 192.168.0.3/24 + - 10.0.0.3/32 + port: + eq: + - 25 + - 80 + - 443 + protocol: tcp + sequence: 20 + src: + ipv4: + - 192.168.0.1/24 + - 10.0.0.2/32 - action: permit description: Named prefix -> host (not in port range) dst: ipv4: - 10.0.0.1 - port_op: not_in - port_range: - max: 443 - min: 80 + port: + gt: 1024 + lt: 1 protocol: tcp - sequence: 20 + sequence: 30 + src: + ipv4: + - 192.168.0.0/24 + - action: deny + description: Port.in test + dst: + ipv4: + - 10.0.0.42 + port: + in: + - 1 + - 64 + protocol: tcp + sequence: 40 src: ipv4: - 192.168.0.0/24 @@ -194,7 +489,7 @@ routing: ipv6: - ::/0 protocol: udp - sequence: 30 + sequence: 50 src: ipv4: - 0.0.0.0/0 diff --git a/tests/topology/input/acl-simple.yml b/tests/topology/input/acl-simple.yml index dea1b2f5a1..fe2395c5cc 100644 --- a/tests/topology/input/acl-simple.yml +++ b/tests/topology/input/acl-simple.yml @@ -20,20 +20,39 @@ routing.acl: src.prefix: any dst.prefix: any description: test ACL + - action: permit + protocol: tcp + src.node: h1 + dst.node: h2 + dst.port.eq: [ 25, 80, 443 ] + description: Allowed server ports - action: permit protocol: tcp src.prefix: s_pfx dst: ipv4: 10.0.0.1 - port_op: not_in - port_range: { min: 80, max: 443 } + port.not_in: [ 1, 1024 ] description: Named prefix -> host (not in port range) + - action: deny + protocol: tcp + src.prefix: s_pfx + dst: + ipv4: 10.0.0.42 + port.in: [ 1, 64 ] + description: Port.in test - action: permit protocol: udp description: Permit all UDP nodes: n1: + h1: + h2: links: - n1.routing.acl.in: h1_filter + h1: + prefix: s_pfx +- n1: + h2: + pool: lan_h2