diff --git a/nca/CoreDS/ConnectivityProperties.py b/nca/CoreDS/ConnectivityProperties.py index 330a042b2..9384a418b 100644 --- a/nca/CoreDS/ConnectivityProperties.py +++ b/nca/CoreDS/ConnectivityProperties.py @@ -21,29 +21,19 @@ class ConnectivityProperties(CanonicalHyperCubeSet): for TCP, it may be any of the dimensions from dimensions_list, except for icmp_type and icmp_code, for icmp data the actual used dimensions are only [src_peers, dst_peers, icmp_type, icmp_code]. - The usage of this class in the original solution: - In the original solution ConnectivityProperties do not hold src_peers, dst_peers and protocols dimensions. - First, ConnectivityProperties are built at parse time. Since peers are not a part of ConnectivityProperties, - the named ports cannot be resolved at parse time, and so are kept in named_ports and excluded_named_ports, - as explained below. - Second, at the query time, ConnectivityProperties is calculated for every pair of peers, and the named ports - are resolved. The pairs of peers and the protocols are kept in ConnectionSet class, together with - the resulting ConnectivityProperties. - - The usage of this class in the optimized solution: - In the optimized solution ConnectivityProperties potentially hold all the dimensions, including sets - of source peers and destination peers. The connectivity properties are built at the parse time for every policy. - The named ports are resolved during the construction, therefore in the optimized solution named_ports and - excluded_named_ports fields are not used. - - The src_peers and dst_peers dimensions are special dimensions, they do not have constant domain. Their domain - depends on the current set of peers in the system (as appears in BasePeerSet singleton). This set grows when - adding more configurations. Thus, there is no unique 'all values' representation. In particular, those - dimensions are never reduced to inactive. - This might be a problem in comparison and inclusion operators of ConnectivityProperties. The possible solution - may be to keep 'reference full domain value' for these dimensions (as another member in the BasePeerSet), - and to set it to relevant values per query, and to make a special treatment of these dimensions - in the above operators. + ConnectivityProperties potentially hold all the dimensions, including sets of source peers and destination peers. + The connectivity properties are built at the parse time for every policy. + The named ports are resolved during the construction, therefore in the optimized solution named_ports and + excluded_named_ports fields are not used. + + The src_peers and dst_peers dimensions are special dimensions, they do not have constant domain. Their domain + depends on the current set of peers in the system (as appears in BasePeerSet singleton). This set grows when + adding more configurations. Thus, there is no unique 'all values' representation. In particular, those + dimensions are never reduced to inactive. + This might be a problem in comparison and inclusion operators of ConnectivityProperties. The possible solution + may be to keep 'reference full domain value' for these dimensions (as another member in the BasePeerSet), + and to set it to relevant values per query, and to make a special treatment of these dimensions + in the above operators. Also, including support for (included and excluded) named ports (relevant for dest ports only). @@ -366,7 +356,7 @@ def project_on_one_dimension(self, dim_name): return res @staticmethod - def _resolve_named_ports(named_ports, peer, protocols): + def _resolve_named_ports(named_ports, peer, protocols, used_named_ports): peer_named_ports = peer.get_named_ports() real_ports = PortSet() for named_port in named_ports: @@ -379,6 +369,7 @@ def _resolve_named_ports(named_ports, peer, protocols): f'of the pod {peer}. Ignoring the pod') continue real_ports.add_port(real_port[0]) + used_named_ports.add(named_port) return real_ports @staticmethod @@ -389,11 +380,8 @@ def make_conn_props(conn_cube): If possible (i.e., in the optimized solution, when dst_peers are supported in the given cube), the named ports will be resolved. - In the optimized solution, the resulting ConnectivityProperties should not contain named ports: + The resulting ConnectivityProperties should not contain named ports: they are substituted with corresponding port numbers, per peer - In the original solution, the resulting ConnectivityProperties may contain named ports; - they cannot yet be resolved, since dst peers are not provided at this stage the original solution; - they will be resolved by convert_named_ports call during query runs. :param ConnectivityCube conn_cube: the input connectivity cube including all dimension values, whereas missing dimensions are represented by their default values (representing all possible values). @@ -402,11 +390,12 @@ def make_conn_props(conn_cube): src_ports = conn_cube["src_ports"] dst_ports = conn_cube["dst_ports"] assert not src_ports.named_ports and not src_ports.excluded_named_ports - if (not dst_ports.named_ports and not dst_ports.excluded_named_ports) or \ - not conn_cube.is_active_dim("dst_peers"): - # Should not resolve named ports + if not dst_ports.named_ports and not dst_ports.excluded_named_ports: + # No named ports return ConnectivityProperties._make_conn_props_no_named_ports_resolution(conn_cube) + # Should resolve named ports + assert conn_cube.is_active_dim("dst_peers") # Initialize conn_properties if dst_ports.port_set: dst_ports_no_named_ports = PortSet() @@ -419,15 +408,21 @@ def make_conn_props(conn_cube): # Resolving dst named ports protocols = conn_cube["protocols"] dst_peers = conn_cube["dst_peers"] + used_named_ports = set() for peer in dst_peers: - real_ports = ConnectivityProperties._resolve_named_ports(dst_ports.named_ports, peer, protocols) + real_ports = ConnectivityProperties._resolve_named_ports(dst_ports.named_ports, peer, protocols, + used_named_ports) if real_ports: conn_cube.update({"dst_ports": real_ports, "dst_peers": PeerSet({peer})}) conn_properties |= ConnectivityProperties._make_conn_props_no_named_ports_resolution(conn_cube) - excluded_real_ports = ConnectivityProperties._resolve_named_ports(dst_ports.excluded_named_ports, peer, protocols) + excluded_real_ports = ConnectivityProperties._resolve_named_ports(dst_ports.excluded_named_ports, peer, + protocols, used_named_ports) if excluded_real_ports: conn_cube.update({"dst_ports": excluded_real_ports, "dst_peers": PeerSet({peer})}) conn_properties -= ConnectivityProperties._make_conn_props_no_named_ports_resolution(conn_cube) + unresolved_named_ports = (dst_ports.named_ports.union(dst_ports.excluded_named_ports)).difference(used_named_ports) + if unresolved_named_ports: + print(f'Warning: Named ports {unresolved_named_ports} are not defined in any pod') return conn_properties @staticmethod diff --git a/nca/CoreDS/DimensionsManager.py b/nca/CoreDS/DimensionsManager.py index 20213e12e..26529a192 100644 --- a/nca/CoreDS/DimensionsManager.py +++ b/nca/CoreDS/DimensionsManager.py @@ -13,7 +13,7 @@ class DimensionsManager: """ A singleton class to manage dimensions names and their association to type and domain. - The dimensions are related to certain protocol's properties in ConnectionSet / ConnectivityProperties. + The dimensions are related to certain protocol's properties in ConnectivityProperties. They are used for allowed connection representation, as protocols properties, within CanonicalHyperCubeSet objects. The src_peers and dst_peers are special dimensions, they do not have constant domain. diff --git a/nca/CoreDS/Peer.py b/nca/CoreDS/Peer.py index 40b1efce3..cc68d1af5 100644 --- a/nca/CoreDS/Peer.py +++ b/nca/CoreDS/Peer.py @@ -2,7 +2,6 @@ # Copyright 2020- IBM Inc. All rights reserved # SPDX-License-Identifier: Apache2.0 # -import copy import ipaddress import re from ipaddress import ip_network @@ -425,39 +424,6 @@ def _add_interval_to_list(interval, non_overlapping_interval_list): non_overlapping_interval_list += interval.split() non_overlapping_interval_list += to_add - @staticmethod - def disjoint_ip_blocks(ip_blocks1, ip_blocks2, exclude_ipv6=False): - """ - Takes all (atomic) ip-ranges in both ip-blocks and returns a new set of ip-ranges where - each ip-range is: - 1. a subset of an ip-range in either ip-blocks AND - 2. cannot be partially intersected by an ip-range in either ip-blocks AND - 3. is maximal (extending the range to either side will violate either 1 or 2) - :param ip_blocks1: A set of ip blocks - :param ip_blocks2: A set of ip blocks - :param bool exclude_ipv6: indicates if to exclude the IPv6 addresses in case the result is all_ips_block - :return: A set of ip ranges as specified above - :rtype: PeerSet - """ - # deepcopy is required since add_interval_to_list() changes the 'interval' argument - ip_blocks_set = copy.deepcopy(ip_blocks1) - ip_blocks_set |= copy.deepcopy(ip_blocks2) - ip_blocks = sorted(ip_blocks_set, key=IpBlock.ip_count) - - # making sure the resulting list does not contain overlapping ipBlocks - blocks_with_no_overlap = [] - for interval in ip_blocks: - IpBlock._add_interval_to_list(interval, blocks_with_no_overlap) - - res = PeerSet() - for ip_block in blocks_with_no_overlap: - res.add(ip_block) - - if not res: - res.add(IpBlock.get_all_ips_block(exclude_ipv6)) - - return res - def is_ipv4_block(self): """ checks whether self IpBlock includes only IPv4 addresses diff --git a/nca/CoreDS/ProtocolSet.py b/nca/CoreDS/ProtocolSet.py index 99006018b..d4ff4dbbe 100644 --- a/nca/CoreDS/ProtocolSet.py +++ b/nca/CoreDS/ProtocolSet.py @@ -13,6 +13,8 @@ class ProtocolSet(CanonicalIntervalSet): """ min_protocol_num = 0 max_protocol_num = 255 + port_supporting_protocols = {6, 17, 132} + icmp_protocols = {1, 58} def __init__(self, all_protocols=False): """ @@ -148,3 +150,27 @@ def copy(self): for interval in self.interval_set: new_copy.interval_set.append(interval.copy()) return new_copy + + @staticmethod + def protocol_supports_ports(protocol): + """ + :param protocol: Protocol number or name + :return: Whether the given protocol has ports + :rtype: bool + """ + prot = protocol + if isinstance(protocol, str): + prot = ProtocolNameResolver.get_protocol_number(protocol) + return prot in ProtocolSet.port_supporting_protocols + + @staticmethod + def protocol_is_icmp(protocol): + """ + :param protocol: Protocol number or name + :return: Whether the protocol is icmp or icmpv6 + :rtype: bool + """ + prot = protocol + if isinstance(protocol, str): + prot = ProtocolNameResolver.get_protocol_number(protocol) + return prot in ProtocolSet.icmp_protocols diff --git a/nca/FWRules/MinimizeBasic.py b/nca/FWRules/MinimizeBasic.py index 81362ee24..64d5b862b 100644 --- a/nca/FWRules/MinimizeBasic.py +++ b/nca/FWRules/MinimizeBasic.py @@ -130,7 +130,7 @@ def get_connection_set_and_peers_from_cube(the_cube, peer_container, if has_active_dim: conns.add_connections(protocol, props) else: - if ConnectionSet.protocol_supports_ports(protocol) or ConnectionSet.protocol_is_icmp(protocol): + if ProtocolSet.protocol_supports_ports(protocol) or ProtocolSet.protocol_is_icmp(protocol): conns.add_connections(protocol, props) else: conns.add_connections(protocol, True) diff --git a/nca/NetworkConfig/NetworkConfig.py b/nca/NetworkConfig/NetworkConfig.py index 534223ca7..84227741b 100644 --- a/nca/NetworkConfig/NetworkConfig.py +++ b/nca/NetworkConfig/NetworkConfig.py @@ -5,9 +5,8 @@ from dataclasses import dataclass, field, replace from nca.CoreDS import Peer -from nca.CoreDS.ConnectionSet import ConnectionSet from nca.CoreDS.ConnectivityProperties import ConnectivityProperties -from nca.Resources.PolicyResources.NetworkPolicy import NetworkPolicy, OptimizedPolicyConnections, PolicyConnectionsFilter +from nca.Resources.PolicyResources.NetworkPolicy import NetworkPolicy, PolicyConnections, PolicyConnectionsFilter from .NetworkLayer import NetworkLayersContainer, NetworkLayerName from nca.Utils.ExplTracker import ExplTracker @@ -59,7 +58,6 @@ def __init__(self, name, peer_container, policies_container, optimized_run='fals self.policies_container = policies_container self.optimized_run = optimized_run self.allowed_labels = None - self.referenced_ip_blocks = None def __eq__(self, other): if not isinstance(other, NetworkConfig): @@ -193,22 +191,6 @@ def check_for_excluding_ipv6_addresses(self, exclude_ipv6): return False return True # getting here means all policies didn't reference ipv6, it is safe to exclude ipv6 addresses - def get_referenced_ip_blocks(self, exclude_non_ref_ipv6=False): - """ - :param bool exclude_non_ref_ipv6: indicates if to exclude non-referenced ipv_6 addresses from the result - :return: All ip ranges, referenced in any of the policies' rules - :rtype: Peer.PeerSet - """ - if self.referenced_ip_blocks is not None: - return self.referenced_ip_blocks - - exclude_non_ref_ipv6_from_policies = self.check_for_excluding_ipv6_addresses(exclude_non_ref_ipv6) - self.referenced_ip_blocks = Peer.PeerSet() - for policy in self.policies_container.policies.values(): - self.referenced_ip_blocks |= policy.referenced_ip_blocks(exclude_non_ref_ipv6_from_policies) - - return self.referenced_ip_blocks - def get_allowed_labels(self): if self.allowed_labels is not None: return self.allowed_labels @@ -217,76 +199,24 @@ def get_allowed_labels(self): self.allowed_labels |= policy.referenced_labels return self.allowed_labels - # return the allowed connections considering all layers in the config - def allowed_connections(self, from_peer, to_peer, layer_name=None): - """ - This is the core of the whole application - computes the set of allowed connections from one peer to another. - In our connectivity model, this function computes the labels for the edges in our directed graph. - :param Peer.Peer from_peer: The source peer - :param Peer.Peer to_peer: The target peer - :param NetworkLayerName layer_name: The name of the layer to use, if requested to use a specific layer only - :return: a 4-tuple with: - - allowed_conns: all allowed connections (captured/non-captured) - - captured_flag: flag to indicate if any of the policies captured one of the peers (src/dst) - - allowed_captured_conns: allowed captured connections (can be used only if the captured flag is True) - - denied_conns: connections denied by the policies (captured) - :rtype: ConnectionSet, bool, ConnectionSet, ConnectionSet - """ - if layer_name is not None: - if layer_name not in self.policies_container.layers: - return self.policies_container.layers.empty_layer_allowed_connections(layer_name, from_peer, to_peer) - return self.policies_container.layers[layer_name].allowed_connections(from_peer, to_peer) - - # connectivity of hostEndpoints is only determined by calico layer - if isinstance(from_peer, Peer.HostEP) or isinstance(to_peer, Peer.HostEP): - # maintain K8s_Calico layer as active if peer container has hostEndpoint - if NetworkLayerName.K8s_Calico not in self.policies_container.layers: - return self.policies_container.layers.empty_layer_allowed_connections(NetworkLayerName.K8s_Calico, - from_peer, to_peer) - return self.policies_container.layers[NetworkLayerName.K8s_Calico].allowed_connections(from_peer, to_peer) - - allowed_conns_res = ConnectionSet(True) - allowed_captured_conns_res = ConnectionSet() - captured_flag_res = False - denied_conns_res = ConnectionSet() - - for layer, layer_obj in self.policies_container.layers.items(): - allowed_conns_per_layer, captured_flag_per_layer, allowed_captured_conns_per_layer, \ - denied_conns_per_layer = layer_obj.allowed_connections(from_peer, to_peer) - - # all allowed connections: intersection of all allowed connections from all layers - allowed_conns_res &= allowed_conns_per_layer - - # all allowed captured connections: should be captured by at least one layer - allowed_captured_conns_res |= allowed_captured_conns_per_layer - captured_flag_res |= captured_flag_per_layer - - # denied conns: should be denied by at least one layer - denied_conns_res |= denied_conns_per_layer - - # an allowed captured conn (by at least one layer) has to be allowed by all layers (either implicitly or explicitly) - allowed_captured_conns_res &= allowed_conns_res - - return allowed_conns_res, captured_flag_res, allowed_captured_conns_res, denied_conns_res - - def allowed_connections_optimized(self, layer_name=None, res_conns_filter=PolicyConnectionsFilter()): + def allowed_connections(self, layer_name=None, res_conns_filter=PolicyConnectionsFilter()): """ Computes the set of allowed connections between any relevant peers. :param NetworkLayerName layer_name: The name of the layer to use, if requested to use a specific layer only :param PolicyConnectionsFilter res_conns_filter: filter of the required resulting connections (connections with False value will not be calculated) :return: allowed_conns: all allowed connections for relevant peers. - :rtype: OptimizedPolicyConnections + :rtype: PolicyConnections """ if ExplTracker().is_active(): ExplTracker().set_peers(self.peer_container.peer_set) if layer_name is not None: if layer_name not in self.policies_container.layers: - return self.policies_container.layers.empty_layer_allowed_connections_optimized(self.peer_container, - layer_name, - res_conns_filter) - return self.policies_container.layers[layer_name].allowed_connections_optimized(self.peer_container, - res_conns_filter) + return self.policies_container.layers.empty_layer_allowed_connections(self.peer_container, + layer_name, + res_conns_filter) + return self.policies_container.layers[layer_name].allowed_connections(self.peer_container, + res_conns_filter) all_peers = self.peer_container.get_all_peers_group() host_eps = Peer.PeerSet(set([peer for peer in all_peers if isinstance(peer, Peer.HostEP)])) @@ -296,16 +226,16 @@ def allowed_connections_optimized(self, layer_name=None, res_conns_filter=Policy if host_eps and NetworkLayerName.K8s_Calico not in self.policies_container.layers: # maintain K8s_Calico layer as active if peer container has hostEndpoint conns_res = \ - self.policies_container.layers.empty_layer_allowed_connections_optimized(self.peer_container, - NetworkLayerName.K8s_Calico, - res_conns_filter) + self.policies_container.layers.empty_layer_allowed_connections(self.peer_container, + NetworkLayerName.K8s_Calico, + res_conns_filter) conns_res.and_by_filter(conn_hep, replace(res_conns_filter, calc_all_allowed=False)) else: - conns_res = OptimizedPolicyConnections() + conns_res = PolicyConnections() if res_conns_filter.calc_all_allowed: conns_res.all_allowed_conns = ConnectivityProperties.get_all_conns_props_per_config_peers(self.peer_container) for layer, layer_obj in self.policies_container.layers.items(): - conns_per_layer = layer_obj.allowed_connections_optimized(self.peer_container, res_conns_filter) + conns_per_layer = layer_obj.allowed_connections(self.peer_container, res_conns_filter) # only K8s_Calico layer handles host_eps if layer != NetworkLayerName.K8s_Calico: # connectivity of hostEndpoints is only determined by calico layer @@ -339,7 +269,6 @@ def filter_conns_by_peer_types(self, conns): Filter the given connections by removing several connection kinds that are never allowed (such as IpBlock to IpBlock connections, connections from DNSEntries, and more). :param ConnectivityProperties conns: the given connections. - :param PeerSet all_peers: all peers in the system. :return The resulting connections. :rtype ConnectivityProperties """ diff --git a/nca/NetworkConfig/NetworkConfigQuery.py b/nca/NetworkConfig/NetworkConfigQuery.py index c6e6903f0..1ce944c4c 100644 --- a/nca/NetworkConfig/NetworkConfigQuery.py +++ b/nca/NetworkConfig/NetworkConfigQuery.py @@ -2,11 +2,8 @@ # Copyright 2020- IBM Inc. All rights reserved # SPDX-License-Identifier: Apache2.0 # -import itertools import os -import time from abc import abstractmethod -from collections import defaultdict from enum import Enum from dataclasses import dataclass @@ -94,14 +91,14 @@ def execute_and_compute_output_in_required_format(self, cmd_line_flag=False): # according to their updated domains (above) for config in self.get_configs(): for policy in config.policies_container.policies.values(): - policy.reorganize_opt_props_by_new_domains() + policy.reorganize_props_by_new_domains() # run the query query_answer = self.execute(cmd_line_flag) - # restore peers domains and optimized connectivity properties original values + # restore peers domains and connectivity properties original values DimensionsManager.reset() for config in self.get_configs(): for policy in config.policies_container.policies.values(): - policy.restore_opt_props() + policy.restore_props() return query_answer.numerical_result, self._handle_output(query_answer), query_answer.query_not_executed def _handle_output(self, query_answer): @@ -152,18 +149,6 @@ def determine_whether_to_compute_allowed_conns_for_peer_types(peer1, peer2): return False # connectivity between external peers is not relevant either return True - @staticmethod - def compare_fw_rules(fw_rules1, fw_rules2, peer_container, rules_descr=""): - text_prefix = "Original and optimized fw-rules" - if rules_descr: - text_prefix += " for " + rules_descr - if fw_rules1.fw_rules_map == fw_rules2.fw_rules_map: - print(f"{text_prefix} are semantically equivalent") - return - conn_props1 = MinimizeBasic.fw_rules_to_conn_props(fw_rules1, peer_container) - conn_props2 = MinimizeBasic.fw_rules_to_conn_props(fw_rules2, peer_container) - BaseNetworkQuery.compare_conn_props(conn_props1, conn_props2, text_prefix) - @staticmethod def compare_conn_props(props1, props2, text_prefix): if props1 == props2: @@ -506,38 +491,15 @@ def other_policy_containing_deny(self, self_policy, config_with_self_policy, lay if not other_policy.has_deny_rules(): continue config_with_other_policy = self.config.clone_with_just_one_policy(other_policy.full_name()) - if self.config.optimized_run == 'false': - res = self.check_deny_containment_original(config_with_self_policy, config_with_other_policy, layer_name) - else: - res = self.check_deny_containment_optimized(config_with_self_policy, config_with_other_policy, layer_name) - if res: + if self.check_deny_containment(config_with_self_policy, config_with_other_policy, layer_name): return other_policy return None - def check_deny_containment_original(self, config_with_self_policy, config_with_other_policy, layer_name): - # calling get_all_peers_group does not require getting dnsEntry peers, since they are not relevant when computing - # deny connections - pods_to_compare = self.config.peer_container.get_all_peers_group() - pods_to_compare |= TwoNetworkConfigsQuery(self.config, config_with_other_policy).disjoint_referenced_ip_blocks() - for pod1 in pods_to_compare: - for pod2 in pods_to_compare: - if isinstance(pod1, IpBlock) and isinstance(pod2, IpBlock): - continue - if pod1 == pod2: - continue # no way to prevent a pod from communicating with itself - _, _, _, self_deny_conns = config_with_self_policy.allowed_connections(pod1, pod2, layer_name) - _, _, _, other_deny_conns = config_with_other_policy.allowed_connections(pod1, pod2, layer_name) - if not self_deny_conns: - continue - if not self_deny_conns.contained_in(other_deny_conns): - return False - return True - @staticmethod - def check_deny_containment_optimized(config_with_self_policy, config_with_other_policy, layer_name): + def check_deny_containment(config_with_self_policy, config_with_other_policy, layer_name): res_conns_filter = PolicyConnectionsFilter.only_denied_connections() - self_props = config_with_self_policy.allowed_connections_optimized(layer_name, res_conns_filter) - other_props = config_with_other_policy.allowed_connections_optimized(layer_name, res_conns_filter) + self_props = config_with_self_policy.allowed_connections(layer_name, res_conns_filter) + other_props = config_with_other_policy.allowed_connections(layer_name, res_conns_filter) return self_props.denied_conns.contained_in(other_props.denied_conns) def other_rule_containing(self, self_policy, self_rule_index, is_ingress, layer_name): @@ -781,135 +743,57 @@ def are_labels_all_included(target_labels, pool_labels): return False return True - def compute_connectivity_output_original(self): - """ - Compute connectivity output with original implementation (running for every pair of peers). - :return: a tuple of output result (in a required format), FwRules, tcp FWRules and non-tcp FWRules. - :rtype ([Union[str, dict], MinimizeFWRules, MinimizeFWRules], MinimizeFWRules) - """ - fw_rules = None - fw_rules_tcp = None - fw_rules_non_tcp = None - exclude_ipv6 = self.config.check_for_excluding_ipv6_addresses(self.output_config.excludeIPv6Range) - connections = defaultdict(list) - # if dns entry peers exist but no istio policies are configured, - # then actually istio layer exists implicitly, connections to these peers will be considered with the - # default Istio outbound traffic mode - allow any - peers_to_compare = self.config.peer_container.get_all_peers_group(include_dns_entries=True) - ref_ip_blocks = IpBlock.disjoint_ip_blocks(self.config.get_referenced_ip_blocks(exclude_ipv6), - IpBlock.get_all_ips_block_peer_set(exclude_ipv6), exclude_ipv6) - peers_to_compare |= ref_ip_blocks - peers = PeerSet() - for peer1 in peers_to_compare: - for peer2 in peers_to_compare: - if self.is_in_subset(peer1): - peers.add(peer1) - elif not self.is_in_subset(peer2): - continue # skipping pairs if none of them are in the given subset - if not self.determine_whether_to_compute_allowed_conns_for_peer_types(peer1, peer2): - continue - if peer1 == peer2: - # cannot restrict pod's connection to itself - connections[ConnectionSet(True)].append((peer1, peer2)) - else: - conns, _, _, _ = self.config.allowed_connections(peer1, peer2) - if conns: - connections[conns].append((peer1, peer2)) - # collect both peers, even if one of them is not in the subset - peers.add(peer1) - peers.add(peer2) - # if Istio is a layer in the network config - produce 2 maps, for TCP and for non-TCP - # because Istio policies can only capture TCP connectivity - if self.config.policies_container.layers.does_contain_istio_layers(): - output_res, fw_rules_tcp, fw_rules_non_tcp = \ - self.get_connectivity_output_split_by_tcp(connections, peers, peers_to_compare) - else: - output_res, fw_rules = self.get_connectivity_output_full(connections, peers, peers_to_compare) - return output_res, fw_rules, fw_rules_tcp, fw_rules_non_tcp - - def compute_connectivity_output_optimized(self): + def compute_connectivity_output(self): """ Compute connectivity output with optimized implementation. - :return: a tuple of output result (in a required format), FwRules, tcp FWRules and non-tcp FWRules. - :rtype: ([Union[str, dict], MinimizeFWRules, MinimizeFWRules, MinimizeFWRules) + :return: output result in a required format + :rtype: Union[str, dict] """ - opt_fw_rules = None - opt_fw_rules_tcp = None - opt_fw_rules_non_tcp = None exclude_ipv6 = self.config.check_for_excluding_ipv6_addresses(self.output_config.excludeIPv6Range) res_conns_filter = PolicyConnectionsFilter.only_all_allowed_connections() - opt_conns = self.config.allowed_connections_optimized(res_conns_filter=res_conns_filter) - all_conns_opt = opt_conns.all_allowed_conns - opt_peers_to_compare = self.config.peer_container.get_all_peers_group(include_dns_entries=True) + conns = self.config.allowed_connections(res_conns_filter=res_conns_filter) + all_conns = conns.all_allowed_conns + peers_to_compare = self.config.peer_container.get_all_peers_group(include_dns_entries=True) # add all relevant IpBlocks, used in connections - opt_peers_to_compare |= all_conns_opt.get_all_peers() + peers_to_compare |= all_conns.get_all_peers() if exclude_ipv6: # remove connections where any of src_peers or dst_peers contain automatically-added IPv6 blocks, # while keeping connections with IPv6 blocks directly referenced in policies - opt_peers_to_compare.filter_ip_blocks_by_mask(IpBlock.get_all_ips_block(exclude_ipv6=True)) - all_conns_opt &= ConnectivityProperties.make_conn_props_from_dict({"src_peers": opt_peers_to_compare, - "dst_peers": opt_peers_to_compare}) - base_peers_num = len(opt_peers_to_compare) - subset_peers = self.compute_subset(opt_peers_to_compare) + peers_to_compare.filter_ip_blocks_by_mask(IpBlock.get_all_ips_block(exclude_ipv6=True)) + all_conns &= ConnectivityProperties.make_conn_props_from_dict({"src_peers": peers_to_compare, + "dst_peers": peers_to_compare}) + base_peers_num = len(peers_to_compare) + subset_peers = self.compute_subset(peers_to_compare) all_peers = subset_peers if len(subset_peers) != base_peers_num: # remove connections where both of src_peers and dst_peers are out of the subset subset_conns = ConnectivityProperties.make_conn_props_from_dict({"src_peers": subset_peers}) | \ ConnectivityProperties.make_conn_props_from_dict({"dst_peers": subset_peers}) - all_conns_opt &= subset_conns - src_peers, dst_peers = ExplTracker().extract_peers(all_conns_opt) + all_conns &= subset_conns + src_peers, dst_peers = ExplTracker().extract_peers(all_conns) all_peers = src_peers | dst_peers - all_conns_opt = self.config.filter_conns_by_peer_types(all_conns_opt) - expl_conns = all_conns_opt + all_conns = self.config.filter_conns_by_peer_types(all_conns) + expl_conns = all_conns if self.config.policies_container.layers.does_contain_istio_layers(): - output_res, opt_fw_rules_tcp, opt_fw_rules_non_tcp = \ - self.get_props_output_split_by_tcp(all_conns_opt, opt_peers_to_compare) - expl_conns, _ = self.convert_props_to_split_by_tcp(all_conns_opt) + output_res = self.get_props_output_split_by_tcp(all_conns, peers_to_compare) + expl_conns, _ = self.convert_props_to_split_by_tcp(all_conns) else: - output_res, opt_fw_rules = self.get_props_output_full(all_conns_opt, opt_peers_to_compare) + output_res = self.get_props_output_full(all_conns, peers_to_compare) if ExplTracker().is_active(): ExplTracker().set_connections_and_peers(expl_conns, all_peers) - return output_res, opt_fw_rules, opt_fw_rules_tcp, opt_fw_rules_non_tcp + return output_res def exec(self): self.output_config.fullExplanation = True # assign true for this query - it is always ok to compare its results self.output_config.configName = os.path.basename(self.config.name) if self.config.name.startswith('./') else \ self.config.name res = QueryAnswer(True) - fw_rules = None - fw_rules_tcp = None - fw_rules_non_tcp = None - if self.config.optimized_run != 'true': - orig_start = time.time() - output_res, fw_rules, fw_rules_tcp, fw_rules_non_tcp = self.compute_connectivity_output_original() - orig_end = time.time() - print(f'Original loop: time: {(orig_end - orig_start):6.2f} seconds') - if self.output_config.outputFormat in ['json', 'yaml']: - res.output_explanation = [ComputedExplanation(dict_explanation=output_res)] - else: - res.output_explanation = [ComputedExplanation(str_explanation=output_res)] - - if self.config.optimized_run != 'false': - opt_start = time.time() - output_res, opt_fw_rules, opt_fw_rules_tcp, opt_fw_rules_non_tcp = \ - self.compute_connectivity_output_optimized() - opt_end = time.time() - print(f'Opt time: {(opt_end - opt_start):6.2f} seconds') - # the same result for opt == 'true'/'debug' - if self.output_config.outputFormat in ['json', 'yaml']: - res.output_explanation = [ComputedExplanation(dict_explanation=output_res)] - else: - res.output_explanation = [ComputedExplanation(str_explanation=output_res)] - if self.config.optimized_run == 'debug': - if fw_rules and opt_fw_rules: - self.compare_fw_rules(fw_rules, opt_fw_rules, self.config.peer_container, - f"connectivity of {self.config.name}") - if fw_rules_tcp and opt_fw_rules_tcp: - self.compare_fw_rules(fw_rules_tcp, opt_fw_rules_tcp, self.config.peer_container, - f"connectivity - tcp only of {self.config.name}") - if fw_rules_non_tcp and opt_fw_rules_non_tcp: - self.compare_fw_rules(fw_rules_non_tcp, opt_fw_rules_non_tcp, self.config.peer_container, - f"connectivity - non-tcp only of {self.config.name}") + + output_res = self.compute_connectivity_output() + if self.output_config.outputFormat in ['json', 'yaml']: + res.output_explanation = [ComputedExplanation(dict_explanation=output_res)] + else: + res.output_explanation = [ComputedExplanation(str_explanation=output_res)] return res def get_connectivity_output_full(self, connections, peers, peers_to_compare): @@ -936,62 +820,18 @@ def get_props_output_full(self, props, all_peers): :param ConnectivityProperties props: properties describing allowed connections :param PeerSet all_peers: the peers to consider for dot/fw-rules output whereas all other values should be filtered out in the output - :rtype ([Union[str, dict], MinimizeFWRules]) + :rtype Union[str, dict] """ peers_to_compare = props.get_all_peers() if self.output_config.outputFormat in ['dot', 'jpg', 'html']: dot_full = self.dot_format_from_props(props, peers_to_compare) - return dot_full, None + return dot_full if self.output_config.outputFormat == 'txt_no_fw_rules': conns_wo_fw_rules = self.txt_no_fw_rules_format_from_props(props, peers_to_compare) - return conns_wo_fw_rules, None + return conns_wo_fw_rules # handle other formats - formatted_rules, fw_rules = self.fw_rules_from_props(props, all_peers) - return formatted_rules, fw_rules - - def get_connectivity_output_split_by_tcp(self, connections, peers, peers_to_compare): - """ - get the connectivity map output as two parts: TCP and non-TCP - :param dict connections: the connections' dict (map from connection-set to peer pairs) - :param PeerSet peers: the peers to consider for dot output - :param PeerSet peers_to_compare: the peers to consider for fw-rules output - :rtype (Union[str, dict], MinimizeFWRules, MinimizeFWRules) - """ - connectivity_tcp_str = 'TCP' - connectivity_non_tcp_str = 'non-TCP' - connections_tcp, connections_non_tcp = self.convert_connections_to_split_by_tcp(connections) - if self.output_config.outputFormat in ['dot', 'jpg', 'html']: - dot_tcp = self.dot_format_from_connections_dict(connections_tcp, peers, connectivity_tcp_str) - dot_non_tcp = self.dot_format_from_connections_dict(connections_non_tcp, peers, connectivity_non_tcp_str) - # concatenate the two graphs into one dot file - res_str = dot_tcp + dot_non_tcp - return res_str, None, None - - if self.output_config.outputFormat == 'txt_no_fw_rules': - conns_msg_suffix = ' Connections:' - tcp_conns_wo_fw_rules = \ - self._txt_no_fw_rules_format_from_connections_dict(connections_tcp, peers, - connectivity_tcp_str + conns_msg_suffix) - non_tcp_conns_wo_fw_rules = \ - self._txt_no_fw_rules_format_from_connections_dict(connections_non_tcp, peers, - connectivity_non_tcp_str + conns_msg_suffix) - return tcp_conns_wo_fw_rules + '\n\n' + non_tcp_conns_wo_fw_rules, None, None - # handle formats other than dot and txt_no_fw_rules - formatted_rules_tcp, fw_rules_tcp = \ - self.fw_rules_from_connections_dict(connections_tcp, peers_to_compare, connectivity_tcp_str) - formatted_rules_non_tcp, fw_rules_non_tcp = \ - self.fw_rules_from_connections_dict(connections_non_tcp, peers_to_compare, connectivity_non_tcp_str) - if self.output_config.outputFormat in ['json', 'yaml']: - # get a dict object containing the two maps on different keys (TCP_rules and non-TCP_rules) - rules = formatted_rules_tcp - rules.update(formatted_rules_non_tcp) - return rules, fw_rules_tcp, fw_rules_non_tcp - # remaining formats: txt / csv / md : concatenate the two strings of the conn-maps - if self.output_config.outputFormat == 'txt': - res_str = f'{formatted_rules_tcp}\n{formatted_rules_non_tcp}' - else: - res_str = formatted_rules_tcp + formatted_rules_non_tcp - return res_str, fw_rules_tcp, fw_rules_non_tcp + formatted_rules = self.fw_rules_from_props(props, all_peers) + return formatted_rules def get_props_output_split_by_tcp(self, props, all_peers): """ @@ -999,7 +839,7 @@ def get_props_output_split_by_tcp(self, props, all_peers): :param ConnectivityProperties props: properties describing allowed connections :param PeerSet all_peers: the peers to consider for dot/fw-rules output whereas all other values should be filtered out in the output - :rtype (Union[str, dict], MinimizeFWRules, MinimizeFWRules) + :rtype Union[str, dict] """ peers_to_compare = props.get_all_peers() connectivity_tcp_str = 'TCP' @@ -1010,28 +850,27 @@ def get_props_output_split_by_tcp(self, props, all_peers): dot_non_tcp = self.dot_format_from_props(props_non_tcp, peers_to_compare, connectivity_non_tcp_str) # concatenate the two graphs into one dot file res_str = dot_tcp + dot_non_tcp - return res_str, None, None + return res_str if self.output_config.outputFormat in ['txt_no_fw_rules']: txt_no_fw_rules_tcp = self.txt_no_fw_rules_format_from_props(props_tcp, peers_to_compare, connectivity_tcp_str) txt_no_fw_rules_non_tcp = self.txt_no_fw_rules_format_from_props(props_non_tcp, peers_to_compare, connectivity_non_tcp_str) res_str = txt_no_fw_rules_tcp + '\n\n' + txt_no_fw_rules_non_tcp - return res_str, None, None + return res_str # handle formats other than dot and txt_no_fw_rules - formatted_rules_tcp, fw_rules_tcp = self.fw_rules_from_props(props_tcp, all_peers, connectivity_tcp_str) - formatted_rules_non_tcp, fw_rules_non_tcp = self.fw_rules_from_props(props_non_tcp, all_peers, - connectivity_non_tcp_str) + formatted_rules_tcp = self.fw_rules_from_props(props_tcp, all_peers, connectivity_tcp_str) + formatted_rules_non_tcp = self.fw_rules_from_props(props_non_tcp, all_peers, connectivity_non_tcp_str) if self.output_config.outputFormat in ['json', 'yaml']: # get a dict object containing the two maps on different keys (TCP_rules and non-TCP_rules) rules = formatted_rules_tcp rules.update(formatted_rules_non_tcp) - return rules, fw_rules_tcp, fw_rules_non_tcp + return rules # remaining formats: txt / csv / md : concatenate the two strings of the conn-maps if self.output_config.outputFormat == 'txt': res_str = f'{formatted_rules_tcp}\n{formatted_rules_non_tcp}' else: res_str = formatted_rules_tcp + formatted_rules_non_tcp - return res_str, fw_rules_tcp, fw_rules_non_tcp + return res_str def _get_conn_graph(self, connections, peers): """ @@ -1118,7 +957,7 @@ def fw_rules_from_props(self, props, peers_to_compare, connectivity_restriction= :param Union[str,None] connectivity_restriction: specify if connectivity is restricted to TCP / non-TCP , or not :return the connectivity map in fw-rules, considering connectivity_restriction if required - :rtype: (Union[str, dict], MinimizeFWRules) + :rtype: Union[str, dict] """ if self.output_config.fwRulesOverrideAllowedLabels: allowed_labels = set(label for label in self.output_config.fwRulesOverrideAllowedLabels.split(',')) @@ -1133,41 +972,7 @@ def fw_rules_from_props(self, props, peers_to_compare, connectivity_restriction= self.compare_fw_rules_to_conn_props(fw_rules, props, self.config.peer_container, connectivity_restriction=connectivity_restriction) formatted_rules = fw_rules.get_fw_rules_in_required_format(connectivity_restriction=connectivity_restriction) - return formatted_rules, fw_rules - - def convert_connections_to_split_by_tcp(self, connections): - """ - given the connections' dict , convert it to two connection maps, one for TCP only, and the other - for non-TCP only. - :param dict connections: the connections' dict (map from connection-set to peer pairs) - :return: a tuple of the two connection maps : first for TCP, second for non-TCP - :rtype: tuple(dict, dict) - """ - connections_tcp = defaultdict(list) - connections_non_tcp = defaultdict(list) - for conn, peers_list in connections.items(): - tcp_conns, non_tcp_conns = self.split_to_tcp_and_non_tcp_conns(conn) - connections_tcp[tcp_conns] += peers_list - connections_non_tcp[non_tcp_conns] += peers_list - - return connections_tcp, connections_non_tcp - - @staticmethod - def split_to_tcp_and_non_tcp_conns(conns): - """ - split a ConnectionSet object to two objects: one within TCP only, the other within non-TCP protocols - :param ConnectionSet conns: a ConnectionSet object - :return: a tuple of the two ConnectionSet objects: first for TCP, second for non-TCP - :rtype: tuple(ConnectionSet, ConnectionSet) - """ - tcp_conns = conns - ConnectionSet.get_non_tcp_connections() - non_tcp_conns = conns - tcp_conns - if non_tcp_conns == ConnectionSet.get_non_tcp_connections(): - non_tcp_conns = ConnectionSet(True) # all connections in terms of non-TCP - if tcp_conns == ConnectionSet.get_all_tcp_connections(): - tcp_conns = ConnectionSet(True) # all connections in terms of TCP - - return tcp_conns, non_tcp_conns + return formatted_rules @staticmethod def convert_props_to_split_by_tcp(props): @@ -1227,19 +1032,6 @@ def is_identical_topologies(self, check_same_policies=False): 'topology and the same set of policies.') return QueryAnswer(True) - def disjoint_referenced_ip_blocks(self): - """ - Returns disjoint ip-blocks in the policies of both configs - :return: A set of disjoint ip-blocks - :rtype: PeerSet - """ - exclude_ipv6 = self.config1.check_for_excluding_ipv6_addresses(self.output_config.excludeIPv6Range) and \ - self.config2.check_for_excluding_ipv6_addresses(self.output_config.excludeIPv6Range) - # TODO - consider including also non referenced IPBlocks, as in ConnectivityMapQuery - # (see issue https://github.com/IBM/network-config-analyzer/issues/522) - return IpBlock.disjoint_ip_blocks(self.config1.get_referenced_ip_blocks(exclude_ipv6), - self.config2.get_referenced_ip_blocks(exclude_ipv6), exclude_ipv6) - def filter_conns_by_input_or_internal_constraints(self, conns1, conns2): """ Given two allowed connections (in config1 and in config2 respectively), filter those connections @@ -1329,40 +1121,12 @@ def exec(self, cmd_line_flag=False, layer_name=None): if query_answer.output_result: query_answer.numerical_result = not query_answer.bool_result return query_answer - if self.config1.optimized_run == 'false': - return self.check_equivalence_original(layer_name) - else: - return self.check_equivalence_optimized(layer_name) - - def check_equivalence_original(self, layer_name=None): - peers_to_compare = \ - self.config1.peer_container.get_all_peers_group(include_dns_entries=True) - peers_to_compare |= self.disjoint_referenced_ip_blocks() - captured_pods = self.config1.get_captured_pods(layer_name) | self.config2.get_captured_pods(layer_name) - different_conns_list = [] - for peer1 in peers_to_compare: - for peer2 in peers_to_compare if peer1 in captured_pods else captured_pods: - if peer1 == peer2: - continue - if not self.determine_whether_to_compute_allowed_conns_for_peer_types(peer1, peer2): - continue - conns1, _, _, _ = self.config1.allowed_connections(peer1, peer2, layer_name) - conns2, _, _, _ = self.config2.allowed_connections(peer1, peer2, layer_name) - if conns1 != conns2: - different_conns_list.append(PeersAndConnections(str(peer1), str(peer2), conns1, conns2)) - if not self.output_config.fullExplanation: - return self._query_answer_with_relevant_explanation(different_conns_list) - - if different_conns_list: - return self._query_answer_with_relevant_explanation(sorted(different_conns_list)) + return self.check_equivalence(layer_name) - return QueryAnswer(True, self.name1 + ' and ' + self.name2 + ' are semantically equivalent.', - numerical_result=0) - - def check_equivalence_optimized(self, layer_name=None): + def check_equivalence(self, layer_name=None): res_conns_filter = PolicyConnectionsFilter.only_all_allowed_connections() - conn_props1 = self.config1.allowed_connections_optimized(layer_name, res_conns_filter) - conn_props2 = self.config2.allowed_connections_optimized(layer_name, res_conns_filter) + conn_props1 = self.config1.allowed_connections(layer_name, res_conns_filter) + conn_props2 = self.config2.allowed_connections(layer_name, res_conns_filter) all_conns1, all_conns2 = self.filter_conns_by_input_or_internal_constraints(conn_props1.all_allowed_conns, conn_props2.all_allowed_conns) if all_conns1 == all_conns2: @@ -1430,76 +1194,7 @@ def get_explanation_from_conn_graph(conn_graph, is_first_connectivity_result): fw_rules_output = fw_rules.get_fw_rules_in_required_format(False, is_first_connectivity_result) return fw_rules_output, fw_rules - def compute_explanation_for_key(self, key, is_added, conn_graph, is_first_connectivity_result): - """ - computes the explanation for given key and conn_graph with description and fw-rules results - prepares the description and explanation - description text is written for txt, yaml and json formats - other formats description already included in the conn_graph data - :param str key: the key describing the changes - :param bool is_added: a bool flag indicating if connections are added or removed - :param ConnectivityGraph conn_graph: a ConnectivityGraph with added/removed connections - :param bool is_first_connectivity_result: flag indicating if this is the first connectivity fw-rules computation - for the current semantic-diff query - :return the computedExplanation of the current key and conn_graph considering the outputFormat, - and fw_rules from which the explanation was computed - :rtype: ComputedExplanation, Union[None, MinimizeFWRules] - """ - updated_key = self._get_updated_key(key, is_added) - topology_config_name = self.name2 if is_added else self.name1 - connectivity_changes_header = f'{updated_key} (based on topology from config: {topology_config_name}) :' - fw_rules = None - if self.output_config.outputFormat == 'txt_no_fw_rules': - conn_graph_explanation = conn_graph.get_connections_without_fw_rules_txt_format( - connectivity_changes_header, exclude_self_loop_conns=False) + '\n' - else: - conn_graph_explanation, fw_rules = self.get_explanation_from_conn_graph(conn_graph, is_first_connectivity_result) - - if self.output_config.outputFormat in ['json', 'yaml']: - explanation_dict = {'description': updated_key} - explanation_dict.update(conn_graph_explanation) - key_explanation = ComputedExplanation(dict_explanation=explanation_dict) - else: - str_explanation = f'\n{connectivity_changes_header}\n' if self.output_config.outputFormat == 'txt' else '' - str_explanation += conn_graph_explanation - key_explanation = ComputedExplanation(str_explanation=str_explanation) - - return key_explanation, fw_rules - - def get_results_for_computed_fw_rules(self, keys_list, conn_graph_removed_per_key, conn_graph_added_per_key): - """ - Compute accumulated explanation and res for all keys of changed connections categories - :param keys_list: the list of keys - :param conn_graph_removed_per_key: map from key to ConnectivityGraph of removed connections - :param conn_graph_added_per_key: map from key to ConnectivityGraph of added connections - :return: - res (int): number of categories with diffs - explanation (list): list of ComputedExplanation, the diffs' explanations, one for each category - :rtype: int, list[ComputedExplanation] - """ - explanation = [] - add_explanation = self.output_config.outputFormat in SemanticDiffQuery.get_supported_output_formats() - res = 0 - for key in keys_list: - conn_graph_added_conns = conn_graph_added_per_key[key] - conn_graph_removed_conns = conn_graph_removed_per_key[key] - is_added = conn_graph_added_conns is not None and conn_graph_added_conns.conn_graph_has_fw_rules() - is_removed = conn_graph_removed_conns is not None and conn_graph_removed_conns.conn_graph_has_fw_rules() - if is_added: - if add_explanation: - key_explanation, _ = self.compute_explanation_for_key(key, True, conn_graph_added_conns, res == 0) - explanation.append(key_explanation) - res += 1 - - if is_removed: - if add_explanation: - key_explanation, _ = self.compute_explanation_for_key(key, False, conn_graph_removed_conns, res == 0) - explanation.append(key_explanation) - res += 1 - - return res, explanation - - def compute_explanation_for_key_opt(self, key, is_added, props_data, is_first_connectivity_result): + def compute_explanation_for_key(self, key, is_added, props_data, is_first_connectivity_result): """ computes the explanation for given key and conn_graph with description and fw-rules results prepares the description and explanation @@ -1543,7 +1238,7 @@ def compute_explanation_for_key_opt(self, key, is_added, props_data, is_first_co return key_explanation, fw_rules - def get_results_for_computed_fw_rules_opt(self, keys_list, removed_props_per_key, added_props_per_key): + def get_results_for_computed_fw_rules(self, keys_list, removed_props_per_key, added_props_per_key): """ Compute accumulated explanation and res for all keys of changed connections categories :param keys_list: the list of keys @@ -1564,86 +1259,18 @@ def get_results_for_computed_fw_rules_opt(self, keys_list, removed_props_per_key is_removed = removed_props is not None and removed_props.props if is_added: if add_explanation: - key_explanation, _ = self.compute_explanation_for_key_opt(key, True, added_props, res == 0) + key_explanation, _ = self.compute_explanation_for_key(key, True, added_props, res == 0) explanation.append(key_explanation) res += 1 if is_removed: if add_explanation: - key_explanation, _ = self.compute_explanation_for_key_opt(key, False, removed_props, res == 0) + key_explanation, _ = self.compute_explanation_for_key(key, False, removed_props, res == 0) explanation.append(key_explanation) res += 1 return res, explanation - def get_results_for_computed_fw_rules_and_compare_orig_to_opt(self, keys_list, orig_conn_graph_removed_per_key, - orig_conn_graph_added_per_key, - removed_props_per_key, added_props_per_key): - """ - Compute accumulated explanation and res for all keys of changed connections categories. - Also, compare original and optimized results. - :param keys_list: the list of keys - :param orig_conn_graph_removed_per_key: map from key to ConnectivityGraph of original removed connections - :param orig_conn_graph_added_per_key: map from key to ConnectivityGraph of original added connections - :param removed_props_per_key: map from key to PropsAndExplanationData of optimized removed connections - :param added_props_per_key: map from key to PropsAndExplanationData of optimized added connections - :return: - res (int): number of categories with diffs - explanation (list): list of ComputedExplanation, the diffs' explanations, one for each category - :rtype: int, list[ComputedExplanation] - """ - explanation = [] - add_explanation = self.output_config.outputFormat in SemanticDiffQuery.get_supported_output_formats() - res = 0 - for key in keys_list: - orig_conn_graph_added_conns = orig_conn_graph_added_per_key[key] - orig_conn_graph_removed_conns = orig_conn_graph_removed_per_key[key] - is_added = orig_conn_graph_added_conns is not None and orig_conn_graph_added_conns.conn_graph_has_fw_rules() - is_removed = orig_conn_graph_removed_conns is not None and orig_conn_graph_removed_conns.conn_graph_has_fw_rules() - if is_added: - if add_explanation: - key_explanation, orig_fw_rules = self.compute_explanation_for_key( - key, True, orig_conn_graph_added_conns, res == 0) - if not orig_fw_rules: - orig_fw_rules = orig_conn_graph_added_conns.get_minimized_firewall_rules() - added_props = added_props_per_key[key] - assert added_props - opt_key_explanation, opt_fw_rules = self.compute_explanation_for_key_opt( - key, True, added_props, res == 0) - if not opt_fw_rules: - opt_fw_rules = MinimizeFWRules.get_minimized_firewall_rules_from_props( - added_props.props, added_props.cluster_info, added_props.output_config, - added_props.peer_container, None) - if self.config1.optimized_run == 'debug': - self.compare_fw_rules(orig_fw_rules, opt_fw_rules, self.config2.peer_container, - self._get_updated_key(key, True) + - f'between {self.config1.name} and {self.config2.name}') - explanation.append(opt_key_explanation) - res += 1 - - if is_removed: - if add_explanation: - key_explanation, orig_fw_rules = self.compute_explanation_for_key( - key, False, orig_conn_graph_removed_conns, res == 0) - if not orig_fw_rules: - orig_fw_rules = orig_conn_graph_removed_conns.get_minimized_firewall_rules() - removed_props = removed_props_per_key[key] - assert removed_props - opt_key_explanation, opt_fw_rules = self.compute_explanation_for_key_opt( - key, False, removed_props, res == 0) - if not opt_fw_rules: - opt_fw_rules = MinimizeFWRules.get_minimized_firewall_rules_from_props( - removed_props.props, removed_props.cluster_info, removed_props.output_config, - removed_props.peer_container, None) - if self.config1.optimized_run == 'debug': - self.compare_fw_rules(orig_fw_rules, opt_fw_rules, self.config1.peer_container, - self._get_updated_key(key, False) + - f'between {self.config1.name} and {self.config2.name}') - explanation.append(opt_key_explanation) - res += 1 - - return res, explanation - def get_conn_graph_changed_conns(self, key, ip_blocks, is_added): """ create a ConnectivityGraph for changed (added/removed) connections per given key @@ -1666,176 +1293,6 @@ def get_conn_graph_changed_conns(self, key, ip_blocks, is_added): output_config = OutputConfiguration(self.output_config, query_name) return ConnectivityGraph(topology_peers, allowed_labels, output_config) - def compute_diff_original(self): # noqa: C901 - """ - Compute changed connections as following: - - 1.1. lost connections between removed peers - 1.2. lost connections between removed peers and ipBlocks - - 2.1. lost connections between removed peers and intersected peers - - 3.1. lost/new connections between intersected peers due to changes in policies and labels of pods/namespaces - 3.2. lost/new connections between intersected peers and ipBlocks due to changes in policies and labels - - 4.1. new connections between intersected peers and added peers - - 5.1. new connections between added peers - 5.2. new connections between added peers and ipBlocks - - Some sections might be empty and can be dropped. - - :return: - keys_list (list[str]): list of names of connection categories, - being the keys in conn_graph_removed_per_key/conn_graph_added_per_key - conn_graph_removed_per_key (dict): a dictionary of removed connections connectivity graphs per category - conn_graph_added_per_key (dict): a dictionary of added connections connectivity graphs per category - :rtype: list[str], dict, dict - """ - old_peers = self.config1.peer_container.get_all_peers_group(include_dns_entries=True) - new_peers = self.config2.peer_container.get_all_peers_group(include_dns_entries=True) - intersected_peers = old_peers & new_peers - removed_peers = old_peers - intersected_peers - added_peers = new_peers - intersected_peers - captured_pods = (self.config1.get_captured_pods() | self.config2.get_captured_pods()) & intersected_peers - exclude_ipv6 = self.config1.check_for_excluding_ipv6_addresses(self.output_config.excludeIPv6Range) and \ - self.config2.check_for_excluding_ipv6_addresses(self.output_config.excludeIPv6Range) - old_ip_blocks = IpBlock.disjoint_ip_blocks(self.config1.get_referenced_ip_blocks(exclude_ipv6), - IpBlock.get_all_ips_block_peer_set(exclude_ipv6), - exclude_ipv6) - new_ip_blocks = IpBlock.disjoint_ip_blocks(self.config2.get_referenced_ip_blocks(exclude_ipv6), - IpBlock.get_all_ips_block_peer_set(exclude_ipv6), - exclude_ipv6) - - conn_graph_removed_per_key = dict() - conn_graph_added_per_key = dict() - keys_list = [] - - # 1.1. lost connections between removed peers - key = 'Lost connections between removed peers' - keys_list.append(key) - conn_graph_removed_per_key[key] = self.get_conn_graph_changed_conns(key, PeerSet(), False) - conn_graph_added_per_key[key] = None - for pair in itertools.permutations(removed_peers, 2): - if not self.determine_whether_to_compute_allowed_conns_for_peer_types(pair[0], pair[1]): - continue - lost_conns, _, _, _ = self.config1.allowed_connections(pair[0], pair[1]) - if lost_conns: - conn_graph_removed_per_key[key].add_edge(pair[0], pair[1], lost_conns) - - # 1.2. lost connections between removed peers and ipBlocks - key = 'Lost connections between removed peers and ipBlocks' - keys_list.append(key) - conn_graph_removed_per_key[key] = self.get_conn_graph_changed_conns(key, old_ip_blocks, False) - conn_graph_added_per_key[key] = None - for pair in itertools.product(removed_peers, old_ip_blocks): - if self.determine_whether_to_compute_allowed_conns_for_peer_types(pair[0], pair[1]): - lost_conns, _, _, _ = self.config1.allowed_connections(pair[0], pair[1]) - if lost_conns: - conn_graph_removed_per_key[key].add_edge(pair[0], pair[1], lost_conns) - - if self.determine_whether_to_compute_allowed_conns_for_peer_types(pair[1], pair[0]): - lost_conns, _, _, _ = self.config1.allowed_connections(pair[1], pair[0]) - if lost_conns: - conn_graph_removed_per_key[key].add_edge(pair[1], pair[0], lost_conns) - - # 2.1. lost connections between removed peers and intersected peers - key = 'Lost connections between removed peers and persistent peers' - keys_list.append(key) - conn_graph_removed_per_key[key] = self.get_conn_graph_changed_conns(key, PeerSet(), False) - conn_graph_added_per_key[key] = None - for pair in itertools.product(removed_peers, intersected_peers): - if self.determine_whether_to_compute_allowed_conns_for_peer_types(pair[0], pair[1]): - lost_conns, _, _, _ = self.config1.allowed_connections(pair[0], pair[1]) - if lost_conns: - conn_graph_removed_per_key[key].add_edge(pair[0], pair[1], lost_conns) - - if self.determine_whether_to_compute_allowed_conns_for_peer_types(pair[1], pair[0]): - lost_conns, _, _, _ = self.config1.allowed_connections(pair[1], pair[0]) - if lost_conns: - conn_graph_removed_per_key[key].add_edge(pair[1], pair[0], lost_conns) - - # 3.1. lost/new connections between intersected peers due to changes in policies and labels of pods/namespaces - key = 'Changed connections between persistent peers' - keys_list.append(key) - conn_graph_removed_per_key[key] = self.get_conn_graph_changed_conns(key, PeerSet(), False) - conn_graph_added_per_key[key] = self.get_conn_graph_changed_conns(key, PeerSet(), True) - for peer1 in intersected_peers: - for peer2 in intersected_peers if peer1 in captured_pods else captured_pods: - if peer1 == peer2: - continue - if not self.determine_whether_to_compute_allowed_conns_for_peer_types(peer1, peer2): - continue - old_conns, _, _, _ = self.config1.allowed_connections(peer1, peer2) - new_conns, _, _, _ = self.config2.allowed_connections(peer1, peer2) - if new_conns != old_conns: - conn_graph_removed_per_key[key].add_edge(peer1, peer2, old_conns - new_conns) - conn_graph_added_per_key[key].add_edge(peer1, peer2, new_conns - old_conns) - - # 3.2. lost/new connections between intersected peers and ipBlocks due to changes in policies and labels - key = 'Changed connections between persistent peers and ipBlocks' - disjoint_ip_blocks = IpBlock.disjoint_ip_blocks(old_ip_blocks, new_ip_blocks, exclude_ipv6) - peers = captured_pods | disjoint_ip_blocks - keys_list.append(key) - conn_graph_removed_per_key[key] = self.get_conn_graph_changed_conns(key, disjoint_ip_blocks, False) - conn_graph_added_per_key[key] = self.get_conn_graph_changed_conns(key, disjoint_ip_blocks, True) - for peer1 in peers: - for peer2 in disjoint_ip_blocks if peer1 in captured_pods else captured_pods: - if not self.determine_whether_to_compute_allowed_conns_for_peer_types(peer1, peer2): - continue - old_conns, _, _, _ = self.config1.allowed_connections(peer1, peer2) - new_conns, _, _, _ = self.config2.allowed_connections(peer1, peer2) - if new_conns != old_conns: - conn_graph_removed_per_key[key].add_edge(peer1, peer2, old_conns - new_conns) - conn_graph_added_per_key[key].add_edge(peer1, peer2, new_conns - old_conns) - - # 4.1. new connections between intersected peers and added peers - key = 'New connections between persistent peers and added peers' - keys_list.append(key) - conn_graph_removed_per_key[key] = None - conn_graph_added_per_key[key] = self.get_conn_graph_changed_conns(key, PeerSet(), True) - for pair in itertools.product(intersected_peers, added_peers): - if self.determine_whether_to_compute_allowed_conns_for_peer_types(pair[0], pair[1]): - new_conns, _, _, _ = self.config2.allowed_connections(pair[0], pair[1]) - if new_conns: - conn_graph_added_per_key[key].add_edge(pair[0], pair[1], new_conns) - - if self.determine_whether_to_compute_allowed_conns_for_peer_types(pair[1], pair[0]): - new_conns, _, _, _ = self.config2.allowed_connections(pair[1], pair[0]) - if new_conns: - conn_graph_added_per_key[key].add_edge(pair[1], pair[0], new_conns) - - # 5.1. new connections between added peers - key = 'New connections between added peers' - keys_list.append(key) - conn_graph_removed_per_key[key] = None - conn_graph_added_per_key[key] = self.get_conn_graph_changed_conns(key, PeerSet(), True) - for pair in itertools.permutations(added_peers, 2): - if not self.determine_whether_to_compute_allowed_conns_for_peer_types(pair[0], pair[1]): - continue - new_conns, _, _, _ = self.config2.allowed_connections(pair[0], pair[1]) - if new_conns: - conn_graph_added_per_key[key].add_edge(pair[0], pair[1], new_conns) - - # 5.2. new connections between added peers and ipBlocks - key = 'New connections between added peers and ipBlocks' - keys_list.append(key) - conn_graph_removed_per_key[key] = None - conn_graph_added_per_key[key] = self.get_conn_graph_changed_conns(key, new_ip_blocks, True) - - for pair in itertools.product(added_peers, new_ip_blocks): - if self.determine_whether_to_compute_allowed_conns_for_peer_types(pair[0], pair[1]): - new_conns, _, _, _ = self.config2.allowed_connections(pair[0], pair[1]) - if new_conns: - conn_graph_added_per_key[key].add_edge(pair[0], pair[1], new_conns) - - if self.determine_whether_to_compute_allowed_conns_for_peer_types(pair[1], pair[0]): - new_conns, _, _, _ = self.config2.allowed_connections(pair[1], pair[0]) - if new_conns: - conn_graph_added_per_key[key].add_edge(pair[1], pair[0], new_conns) - - return keys_list, conn_graph_removed_per_key, conn_graph_added_per_key - def get_changed_props_expl_data(self, key, ip_blocks, is_added, props, peer_container): """ create a ConnectivityGraph for changed (added/removed) connections per given key @@ -1861,7 +1318,7 @@ def get_changed_props_expl_data(self, key, ip_blocks, is_added, props, peer_cont return SemanticDiffQuery.PropsAndExplanationData(props, ClusterInfo(topology_peers, allowed_labels), output_config, peer_container) - def compute_diff_optimized(self): # noqa: C901 + def compute_diff(self): # noqa: C901 """ Compute changed connections (by optimized implementation) as following: @@ -1902,8 +1359,8 @@ def compute_diff_optimized(self): # noqa: C901 added_props_per_key = dict() keys_list = [] res_conns_filter = PolicyConnectionsFilter.only_all_allowed_connections() - old_conns = self.config1.allowed_connections_optimized(res_conns_filter=res_conns_filter) - new_conns = self.config2.allowed_connections_optimized(res_conns_filter=res_conns_filter) + old_conns = self.config1.allowed_connections(res_conns_filter=res_conns_filter) + new_conns = self.config2.allowed_connections(res_conns_filter=res_conns_filter) old_props, new_props = self.filter_conns_by_input_or_internal_constraints(old_conns.all_allowed_conns, new_conns.all_allowed_conns) @@ -2016,25 +1473,9 @@ def exec(self, cmd_line_flag): query_answer = self.is_identical_topologies(True) if query_answer.bool_result and query_answer.output_result: return query_answer - orig_conn_graph_removed_per_key = dict() - orig_conn_graph_added_per_key = dict() - res = 0 - explanation = "" - if self.config1.optimized_run != 'true': - keys_list, orig_conn_graph_removed_per_key, orig_conn_graph_added_per_key = self.compute_diff_original() - if self.config1.optimized_run == 'false': - res, explanation = self.get_results_for_computed_fw_rules(keys_list, orig_conn_graph_removed_per_key, - orig_conn_graph_added_per_key) - if self.config1.optimized_run != 'false': - keys_list, removed_props_per_key, added_props_per_key = self.compute_diff_optimized() - if self.config1.optimized_run == 'true': - res, explanation = self.get_results_for_computed_fw_rules_opt(keys_list, removed_props_per_key, - added_props_per_key) - else: - res, explanation = self.get_results_for_computed_fw_rules_and_compare_orig_to_opt( - keys_list, orig_conn_graph_removed_per_key, orig_conn_graph_added_per_key, - removed_props_per_key, added_props_per_key) - + keys_list, removed_props_per_key, added_props_per_key = self.compute_diff() + res, explanation = self.get_results_for_computed_fw_rules(keys_list, removed_props_per_key, + added_props_per_key) if res > 0: return QueryAnswer(bool_result=False, output_result=f'{self.name1} and {self.name2} are not semantically equivalent.', @@ -2107,44 +1548,16 @@ def exec(self, cmd_line_flag=False, only_captured=False): return QueryAnswer(False, f'{self.name1} is not contained in {self.name2} ', output_explanation=[final_explanation], numerical_result=0 if not cmd_line_flag else 1) - if self.config1.optimized_run == 'false': - return self.check_containment_original(cmd_line_flag, only_captured) - else: - return self.check_containment_optimized(cmd_line_flag, only_captured) - - def check_containment_original(self, cmd_line_flag=False, only_captured=False): - config1_peers = self.config1.peer_container.get_all_peers_group(include_dns_entries=True) - peers_to_compare = config1_peers | self.disjoint_referenced_ip_blocks() - captured_pods = self.config1.get_captured_pods() | self.config2.get_captured_pods() - not_contained_list = [] - for peer1 in peers_to_compare: - for peer2 in peers_to_compare if peer1 in captured_pods else captured_pods: - if peer1 == peer2: - continue - if not self.determine_whether_to_compute_allowed_conns_for_peer_types(peer1, peer2): - continue - conns1_all, captured1_flag, conns1_captured, _ = self.config1.allowed_connections(peer1, peer2) - if only_captured and not captured1_flag: - continue - conns1 = conns1_captured if only_captured else conns1_all - conns2, _, _, _ = self.config2.allowed_connections(peer1, peer2) - if not conns1.contained_in(conns2): - not_contained_list.append(PeersAndConnections(str(peer1), str(peer2), conns1)) - if not self.output_config.fullExplanation: - return self._query_answer_with_relevant_explanation(not_contained_list, cmd_line_flag) - if not_contained_list: - return self._query_answer_with_relevant_explanation(sorted(not_contained_list), cmd_line_flag) - return QueryAnswer(True, self.name1 + ' is contained in ' + self.name2, - numerical_result=1 if not cmd_line_flag else 0) + return self.check_containment(cmd_line_flag, only_captured) - def check_containment_optimized(self, cmd_line_flag=False, only_captured=False): + def check_containment(self, cmd_line_flag=False, only_captured=False): if only_captured: res_conns_filter1 = PolicyConnectionsFilter.only_allowed_connections() else: res_conns_filter1 = PolicyConnectionsFilter.only_all_allowed_connections() res_conns_filter2 = PolicyConnectionsFilter.only_all_allowed_connections() - conn_props1 = self.config1.allowed_connections_optimized(res_conns_filter=res_conns_filter1) - conn_props2 = self.config2.allowed_connections_optimized(res_conns_filter=res_conns_filter2) + conn_props1 = self.config1.allowed_connections(res_conns_filter=res_conns_filter1) + conn_props2 = self.config2.allowed_connections(res_conns_filter=res_conns_filter2) conns1, conns2 = self.filter_conns_by_input_or_internal_constraints( conn_props1.allowed_conns if only_captured else conn_props1.all_allowed_conns, conn_props2.all_allowed_conns) @@ -2258,42 +1671,13 @@ def exec(self, cmd_line_flag): else not query_answer.bool_result return query_answer - if self.config1.optimized_run == 'false': - return self.check_interferes_original(cmd_line_flag) - else: - return self.check_interferes_optimized(cmd_line_flag) + return self.check_interferes(cmd_line_flag) - def check_interferes_original(self, cmd_line_flag): - peers_to_compare = \ - self.config2.peer_container.get_all_peers_group(include_dns_entries=True) - peers_to_compare |= self.disjoint_referenced_ip_blocks() - captured_pods = self.config2.get_captured_pods() | self.config1.get_captured_pods() - extended_conns_list = [] - for peer1 in peers_to_compare: - for peer2 in peers_to_compare if peer1 in captured_pods else captured_pods: - if peer1 == peer2: - continue - if not self.determine_whether_to_compute_allowed_conns_for_peer_types(peer1, peer2): - continue - _, captured2_flag, conns2_captured, _ = self.config2.allowed_connections(peer1, peer2) - if not captured2_flag: - continue - _, captured1_flag, conns1_captured, _ = self.config1.allowed_connections(peer1, peer2) - if captured1_flag and not conns1_captured.contained_in(conns2_captured): - extended_conns_list.append(PeersAndConnections(str(peer1), str(peer2), conns1_captured, - conns2_captured)) - if not self.output_config.fullExplanation: - return self._query_answer_with_relevant_explanation(extended_conns_list, cmd_line_flag) - if extended_conns_list: - return self._query_answer_with_relevant_explanation(sorted(extended_conns_list), cmd_line_flag) - return QueryAnswer(False, self.name1 + ' does not interfere with ' + self.name2, - numerical_result=0 if not cmd_line_flag else 1) - - def check_interferes_optimized(self, cmd_line_flag=False): + def check_interferes(self, cmd_line_flag=False): res_conns_filter = PolicyConnectionsFilter.only_allowed_connections() - conn_props1 = self.config1.allowed_connections_optimized(res_conns_filter=res_conns_filter) - conn_props2 = self.config2.allowed_connections_optimized(res_conns_filter=res_conns_filter) + conn_props1 = self.config1.allowed_connections(res_conns_filter=res_conns_filter) + conn_props2 = self.config2.allowed_connections(res_conns_filter=res_conns_filter) conns1, conns2 = self.filter_conns_by_input_or_internal_constraints(conn_props1.allowed_conns, conn_props2.allowed_conns) if conns1.contained_in(conns2): @@ -2341,48 +1725,16 @@ def exec(self, cmd_line_flag=False, only_captured=True): if query_answer.output_result: return query_answer - if self.config1.optimized_run == 'false': - return self.check_intersects_original() - else: - return self.check_intersects_optimized() - - def check_intersects_original(self, only_captured=True): - peers_to_compare = \ - self.config1.peer_container.get_all_peers_group(include_dns_entries=True) - peers_to_compare |= self.disjoint_referenced_ip_blocks() - captured_pods = self.config1.get_captured_pods() | self.config2.get_captured_pods() - intersect_connections_list = [] - for peer1 in peers_to_compare: - for peer2 in peers_to_compare if peer1 in captured_pods else captured_pods: - if peer1 == peer2: - continue - if not self.determine_whether_to_compute_allowed_conns_for_peer_types(peer1, peer2): - continue - conns1_all, captured1_flag, conns1_captured, _ = self.config1.allowed_connections(peer1, peer2) - if only_captured and not captured1_flag: - continue - conns1 = conns1_captured if only_captured else conns1_all - conns2, _, _, _ = self.config2.allowed_connections(peer1, peer2) - conns_in_both = conns2 & conns1 - if bool(conns_in_both): - intersect_connections_list.append(PeersAndConnections(str(peer1), str(peer2), conns_in_both)) - if not self.output_config.fullExplanation: - return self._query_answer_with_relevant_explanation(intersect_connections_list) - - if intersect_connections_list: - return self._query_answer_with_relevant_explanation(sorted(intersect_connections_list)) - - return QueryAnswer(False, f'The connections allowed by {self.name1}' - f' do not intersect the connections allowed by {self.name2}', numerical_result=1) + return self.check_intersects() - def check_intersects_optimized(self, only_captured=True): + def check_intersects(self, only_captured=True): if only_captured: res_conns_filter1 = PolicyConnectionsFilter.only_allowed_connections() else: res_conns_filter1 = PolicyConnectionsFilter.only_all_allowed_connections() res_conns_filter2 = PolicyConnectionsFilter.only_all_allowed_connections() - conn_props1 = self.config1.allowed_connections_optimized(res_conns_filter=res_conns_filter1) - conn_props2 = self.config2.allowed_connections_optimized(res_conns_filter=res_conns_filter2) + conn_props1 = self.config1.allowed_connections(res_conns_filter=res_conns_filter1) + conn_props2 = self.config2.allowed_connections(res_conns_filter=res_conns_filter2) conns1, conns2 = self.filter_conns_by_input_or_internal_constraints( conn_props1.allowed_conns if only_captured else conn_props1.all_allowed_conns, conn_props2.all_allowed_conns) diff --git a/nca/NetworkConfig/NetworkLayer.py b/nca/NetworkConfig/NetworkLayer.py index affb7d817..7e2c08212 100644 --- a/nca/NetworkConfig/NetworkLayer.py +++ b/nca/NetworkConfig/NetworkLayer.py @@ -12,7 +12,7 @@ from nca.CoreDS.ProtocolSet import ProtocolSet from nca.Resources.PolicyResources.IstioNetworkPolicy import IstioNetworkPolicy from nca.Resources.PolicyResources.GatewayPolicy import GatewayPolicy -from nca.Resources.PolicyResources.NetworkPolicy import PolicyConnections, OptimizedPolicyConnections, NetworkPolicy, \ +from nca.Resources.PolicyResources.NetworkPolicy import PolicyConnections, NetworkPolicy, \ PolicyConnectionsFilter from nca.Utils.ExplTracker import ExplTracker @@ -99,30 +99,17 @@ def does_contain_istio_layers(self): return bool({NetworkLayerName.Istio, NetworkLayerName.IstioGateway} & set(self.keys())) @staticmethod - def empty_layer_allowed_connections(layer_name, from_peer, to_peer): - """ - Get allowed connections between two peers for an empty layer (no policies). - :param NetworkLayerName layer_name: The empty layer name - :param Peer.Peer from_peer: the source peer - :param Peer.Peer to_peer: the target peer - :rtype: ConnectionSet, bool, ConnectionSet, ConnectionSet - """ - empty_layer_obj = layer_name.create_network_layer([]) - return empty_layer_obj.allowed_connections(from_peer, to_peer) - - @staticmethod - def empty_layer_allowed_connections_optimized(peer_container, layer_name, - res_conns_filter=PolicyConnectionsFilter()): + def empty_layer_allowed_connections(peer_container, layer_name, res_conns_filter=PolicyConnectionsFilter()): """ Get allowed connections between for all relevant peers for an empty layer (no policies). :param PeerContainer peer_container: holds all the peers :param NetworkLayerName layer_name: The empty layer name :param PolicyConnectionsFilter res_conns_filter: filter of the required resulting connections (connections with None value will not be calculated) - :rtype: OptimizedPolicyConnections + :rtype: PolicyConnections """ empty_layer_obj = layer_name.create_network_layer([]) - return empty_layer_obj.allowed_connections_optimized(peer_container, res_conns_filter) + return empty_layer_obj.allowed_connections(peer_container, res_conns_filter) class NetworkLayer: @@ -144,39 +131,7 @@ def add_policy(self, policy): """ insort(self.policies_list, policy) - def allowed_connections(self, from_peer, to_peer): - """ - Compute per network layer the allowed connections between from_peer and to_peer, considering - all layer's policies (and defaults) - :param Peer.Peer from_peer: The source peer - :param Peer.Peer to_peer: The target peer - :return: a 4-tuple with: - - allowed_conns: all allowed connections (captured/non-captured) - - captured_flag: flag to indicate if any of the policies captured one of the peers (src/dst) - - allowed_captured_conns: allowed captured connections (can be used only if the captured flag is True) - - denied_conns: connections denied by the policies (captured) - :rtype: ConnectionSet, bool, ConnectionSet, ConnectionSet - """ - if isinstance(to_peer, IpBlock): - ingress_conns = PolicyConnections(captured=False, all_allowed_conns=ConnectionSet(True)) - else: - ingress_conns = self._allowed_xgress_conns(from_peer, to_peer, True) - - if isinstance(from_peer, IpBlock): - egress_conns = PolicyConnections(captured=False, all_allowed_conns=ConnectionSet(True)) - else: - egress_conns = self._allowed_xgress_conns(from_peer, to_peer, False) - - captured_flag = ingress_conns.captured or egress_conns.captured - denied_conns = ingress_conns.denied_conns | egress_conns.denied_conns - allowed_conns = ingress_conns.all_allowed_conns & egress_conns.all_allowed_conns - # captured connections are where at least one of ingress / egress is captured - allowed_captured_conns = (ingress_conns.allowed_conns & egress_conns.all_allowed_conns) | \ - (egress_conns.allowed_conns & ingress_conns.all_allowed_conns) - - return allowed_conns, captured_flag, allowed_captured_conns, denied_conns - - def allowed_connections_optimized(self, peer_container, res_conns_filter=PolicyConnectionsFilter()): + def allowed_connections(self, peer_container, res_conns_filter=PolicyConnectionsFilter()): """ Compute per network layer the allowed connections between any relevant peers, considering all layer's policies (and defaults) @@ -184,11 +139,11 @@ def allowed_connections_optimized(self, peer_container, res_conns_filter=PolicyC :param PolicyConnectionsFilter res_conns_filter: filter of the required resulting connections (connections with None value will not be calculated) :return: all allowed, denied and captured connections - :rtype: OptimizedPolicyConnections + :rtype: PolicyConnections """ - res_conns = OptimizedPolicyConnections() - ingress_conns = self._allowed_xgress_conns_optimized(True, peer_container, res_conns_filter) - egress_conns = self._allowed_xgress_conns_optimized(False, peer_container, res_conns_filter) + res_conns = PolicyConnections() + ingress_conns = self._allowed_xgress_conns(True, peer_container, res_conns_filter) + egress_conns = self._allowed_xgress_conns(False, peer_container, res_conns_filter) all_pods_peer_set = peer_container.get_all_peers_group() all_ips_peer_set = IpBlock.get_all_ips_block_peer_set() if res_conns_filter.calc_all_allowed: @@ -209,51 +164,13 @@ def allowed_connections_optimized(self, peer_container, res_conns_filter=PolicyC (egress_conns.allowed_conns & ingress_conns.all_allowed_conns) return res_conns - def _allowed_xgress_conns(self, from_peer, to_peer, is_ingress): - """ - Implemented by derived classes to get allowed and denied ingress/egress connections between from_peer and to_pee - """ - return NotImplemented - - def _allowed_xgress_conns_optimized(self, is_ingress, peer_container, res_conns_filter=PolicyConnectionsFilter()): + def _allowed_xgress_conns(self, is_ingress, peer_container, res_conns_filter=PolicyConnectionsFilter()): """ Implemented by derived classes to get ingress/egress connections between any relevant peers - :rtype: OptimizedPolicyConnections + :rtype: PolicyConnections """ return NotImplemented - def collect_policies_conns(self, from_peer, to_peer, is_ingress, - captured_func=lambda policy: True): - """ - Collect allowed/denied/pass connections between two peers, considering all layer's policies that capture the - relevant peers. - :param Peer.Peer from_peer: the source peer - :param Peer.Peer to_peer: the dest peer - :param bool is_ingress: indicates whether to return ingress connections or egress connections - :param captured_func: callable that returns True if the policy satisfies additional conditions required for - considering the target pod as captured and not applying the default connections to it. - :return: (allowed_conns, denied_conns, pass_conns, captured_res) - :rtype: (ConnectionSet, ConnectionSet, ConnectionSet, bool) - """ - allowed_conns = ConnectionSet() - denied_conns = ConnectionSet() - pass_conns = ConnectionSet() - captured_res = False - for policy in self.policies_list: - policy_conns = policy.allowed_connections(from_peer, to_peer, is_ingress) - if policy_conns.captured: - captured_res |= captured_func(policy) - policy_conns.denied_conns -= allowed_conns - policy_conns.denied_conns -= pass_conns - policy_conns.allowed_conns -= denied_conns - policy_conns.allowed_conns -= pass_conns - policy_conns.pass_conns -= denied_conns - policy_conns.pass_conns -= allowed_conns - denied_conns |= policy_conns.denied_conns - allowed_conns |= policy_conns.allowed_conns - pass_conns |= policy_conns.pass_conns - return allowed_conns, denied_conns, pass_conns, captured_res - def collect_policies_conns_optimized(self, is_ingress, captured_func=lambda policy: True): """ Collect all connections (between all relevant peers), considering all layer's policies that capture the @@ -262,11 +179,11 @@ def collect_policies_conns_optimized(self, is_ingress, captured_func=lambda poli :param captured_func: callable that returns True if the policy satisfies additional conditions required for considering captured pods instead of applying the default connections. :return: allowed_conns, denied_conns and set of peers to be added to captured peers - :rtype: OptimizedPolicyConnections + :rtype: PolicyConnections """ - res_conns = OptimizedPolicyConnections() + res_conns = PolicyConnections() for policy in self.policies_list: - policy_conns = policy.allowed_connections_optimized(is_ingress) + policy_conns = policy.allowed_connections(is_ingress) if policy_conns.captured: # not empty if captured_func(policy): res_conns.captured |= policy_conns.captured @@ -285,25 +202,7 @@ def collect_policies_conns_optimized(self, is_ingress, captured_func=lambda poli class K8sCalicoNetworkLayer(NetworkLayer): - def _allowed_xgress_conns(self, from_peer, to_peer, is_ingress): - allowed_conns, denied_conns, pass_conns, captured_res = self.collect_policies_conns(from_peer, to_peer, - is_ingress) - - allowed_non_captured_conns = ConnectionSet() - captured_peer_is_host_endpoint = (is_ingress and isinstance(to_peer, HostEP)) or \ - (not is_ingress and isinstance(from_peer, HostEP)) - if not captured_res and not captured_peer_is_host_endpoint: - # default Allow-all in k8s / calico - # (assuming only calico's default profiles for pods with connectivity rules exist) - # assuming host endpoints have no profiles - allowed_non_captured_conns = ConnectionSet(True) - elif pass_conns and not captured_peer_is_host_endpoint: - # assuming only default profiles generated by calico exist, which allow all for pods - allowed_conns |= pass_conns - return PolicyConnections(captured_res, allowed_conns, denied_conns, - all_allowed_conns=allowed_conns | allowed_non_captured_conns) - - def _allowed_xgress_conns_optimized(self, is_ingress, peer_container, res_conns_filter=PolicyConnectionsFilter()): + def _allowed_xgress_conns(self, is_ingress, peer_container, res_conns_filter=PolicyConnectionsFilter()): res_conns = self.collect_policies_conns_optimized(is_ingress) # Note: The below computation of non-captured conns cannot be done during the parse stage, # since before computing non-captured conns we should collect all policies conns @@ -356,23 +255,7 @@ def captured_cond_func(policy): return policy.action == GatewayPolicy.ActionType.Allow return True # only for Istio AuthorizationPolicy the captured condition is more refined with 'Allow' policies - def _allowed_xgress_conns(self, from_peer, to_peer, is_ingress): - # in istio applying default-allow if there is no capturing policy with action allow - - allowed_conns, denied_conns, _, captured_res = self.collect_policies_conns(from_peer, to_peer, is_ingress, - IstioNetworkLayer.captured_cond_func) - # for istio initialize non-captured conns with non-TCP connections - allowed_non_captured_conns = ConnectionSet.get_non_tcp_connections() - if not captured_res: # no allow policies for target - # add connections allowed by default that are not captured - allowed_non_captured_conns |= (ConnectionSet(True) - denied_conns) - # exception: update allowed non-captured conns to DNSEntry dst with TCP only - if isinstance(to_peer, DNSEntry): - allowed_non_captured_conns = ConnectionSet.get_all_tcp_connections() - denied_conns - return PolicyConnections(captured_res, allowed_conns, denied_conns, - all_allowed_conns=allowed_conns | allowed_non_captured_conns) - - def _allowed_xgress_conns_optimized(self, is_ingress, peer_container, res_conns_filter=PolicyConnectionsFilter()): + def _allowed_xgress_conns(self, is_ingress, peer_container, res_conns_filter=PolicyConnectionsFilter()): res_conns = self.collect_policies_conns_optimized(is_ingress, IstioNetworkLayer.captured_cond_func) if not res_conns_filter.calc_all_allowed: return res_conns diff --git a/nca/Parsers/CalicoPolicyYamlParser.py b/nca/Parsers/CalicoPolicyYamlParser.py index 672db2e47..1eb497b4d 100644 --- a/nca/Parsers/CalicoPolicyYamlParser.py +++ b/nca/Parsers/CalicoPolicyYamlParser.py @@ -11,7 +11,6 @@ from nca.CoreDS.ConnectivityProperties import ConnectivityProperties from nca.CoreDS.ProtocolSet import ProtocolSet from nca.CoreDS.DimensionsManager import DimensionsManager -from nca.CoreDS.ConnectionSet import ConnectionSet from nca.Resources.PolicyResources.NetworkPolicy import NetworkPolicy from nca.Resources.PolicyResources.CalicoNetworkPolicy import CalicoNetworkPolicy, CalicoPolicyRule from .GenericYamlParser import GenericYamlParser @@ -380,9 +379,7 @@ def _parse_icmp(self, icmp_data, not_icmp_data, protocol, src_pods, dst_pods): :param: str protocol: the ICMP-like protocol :param PeerSet src_pods: the source pods :param PeerSet dst_pods: the destination pods - :return: a tuple (ConnectivityProperties, ConnectivityProperties), - where the first ConnectivityProperties is an original-format ICMP connections, - and the second ConnectivityProperties is an optimized-format ICMP connections, including src and dst pods. + :return: a ConnectivityProperties, representing ICMP properties, including src and dst pods. :rtype: tuple (ConnectivityProperties, ConnectivityProperties) """ icmp_type = icmp_data.get('type') if icmp_data is not None else None @@ -413,41 +410,25 @@ def _parse_icmp(self, icmp_data, not_icmp_data, protocol, src_pods, dst_pods): not_conn_cube["icmp_type"] = not_icmp_type if not_icmp_code: not_conn_cube["icmp_code"] = not_icmp_code - opt_conn_cube = conn_cube.copy() - opt_not_conn_cube = not_conn_cube.copy() - if self.optimized_run != 'false': - opt_conn_cube.update({"src_peers": src_pods, "dst_peers": dst_pods, "protocols": protocols}) - opt_not_conn_cube.update({"src_peers": src_pods, "dst_peers": dst_pods, "protocols": protocols}) + conn_cube.update({"src_peers": src_pods, "dst_peers": dst_pods, "protocols": protocols}) + not_conn_cube.update({"src_peers": src_pods, "dst_peers": dst_pods, "protocols": protocols}) - opt_props = ConnectivityProperties.make_empty_props() if icmp_data is not None: - res = ConnectivityProperties.make_conn_props(conn_cube) - if self.optimized_run != 'false': - opt_props = ConnectivityProperties.make_conn_props(opt_conn_cube) + res_props = ConnectivityProperties.make_conn_props(conn_cube) if not_icmp_data is not None: if icmp_type == not_icmp_type and icmp_code == not_icmp_code: - res = ConnectivityProperties.make_empty_props() self.warning('icmp and notICMP are conflicting - no traffic will be matched', not_icmp_data) elif icmp_type == not_icmp_type and icmp_code is None: # this is the only case where it makes sense to combine icmp and notICMP - tmp = ConnectivityProperties.make_conn_props(not_conn_cube) - res -= tmp - if self.optimized_run != 'false': - tmp_opt_props = ConnectivityProperties.make_conn_props(opt_not_conn_cube) - opt_props -= tmp_opt_props + res_props -= ConnectivityProperties.make_conn_props(not_conn_cube) else: self.warning('notICMP has no effect', not_icmp_data) elif not_icmp_data is not None: - res = ConnectivityProperties.make_conn_props(conn_cube) - \ - ConnectivityProperties.make_conn_props(not_conn_cube) - if self.optimized_run != 'false': - opt_props = ConnectivityProperties.make_conn_props(opt_conn_cube) - \ - ConnectivityProperties.make_conn_props(opt_not_conn_cube) + res_props = ConnectivityProperties.make_conn_props(conn_cube) - \ + ConnectivityProperties.make_conn_props(not_conn_cube) else: # no icmp_data or no_icmp_data; only protocol - res = ConnectivityProperties.make_conn_props(conn_cube) - if self.optimized_run != 'false': - opt_props = ConnectivityProperties.make_conn_props(opt_conn_cube) - return res, opt_props + res_props = ConnectivityProperties.make_conn_props(conn_cube) + return res_props def _parse_protocol(self, protocol, rule): """ @@ -475,8 +456,8 @@ def _parse_xgress_rule(self, rule, is_ingress, policy_selected_eps, is_profile): :param bool is_ingress: Whether this is an ingress rule :param PeerSet policy_selected_eps: The endpoints the policy captured :param bool is_profile: Whether the parsed policy is a Profile object - :return: A tuple (CalicoPolicyRule, ConnectivityProperties) with the proper PeerSets, ConnectionSets and Action, - where ConnectivityProperties is an optimized rule format with protocols, src_peers and dst_peers in a HyperCubeSet + :return: A tuple (CalicoPolicyRule, ConnectivityProperties) with the proper PeerSets, connectivity properties + and Action :rtype: tuple(CalicoPolicyRule, ConnectivityProperties) """ allowed_keys = {'action': 1, 'protocol': 0, 'notProtocol': 0, 'icmp': 0, 'notICMP': 0, 'ipVersion': 0, @@ -490,7 +471,7 @@ def _parse_xgress_rule(self, rule, is_ingress, policy_selected_eps, is_profile): self.warning('Pass actions in Profile rules will be ignored', rule) protocol = self._parse_protocol(rule.get('protocol'), rule) - protocol_supports_ports = ConnectionSet.protocol_supports_ports(protocol) + protocol_supports_ports = ProtocolSet.protocol_supports_ports(protocol) not_protocol = self._parse_protocol(rule.get('notProtocol'), rule) src_entity_rule = rule.get('source') if src_entity_rule: @@ -511,7 +492,6 @@ def _parse_xgress_rule(self, rule, is_ingress, policy_selected_eps, is_profile): else: src_res_pods &= policy_selected_eps - connections = ConnectionSet() conn_props = ConnectivityProperties.make_empty_props() if protocol is not None: protocols = ProtocolSet.get_protocol_set_with_single_protocol(protocol) @@ -522,68 +502,33 @@ def _parse_xgress_rule(self, rule, is_ingress, policy_selected_eps, is_profile): self.warning('notProtocol field has no effect', rule) else: if protocol_supports_ports: - conn_cube = ConnectivityCube.make_from_dict({"src_ports": src_res_ports, "dst_ports": dst_res_ports}) - connections.add_connections(protocol, ConnectivityProperties.make_conn_props(conn_cube)) - if self.optimized_run != 'false': - conn_cube.update({"protocols": protocols, "src_peers": src_res_pods, "dst_peers": dst_res_pods}) - conn_props = ConnectivityProperties.make_conn_props(conn_cube) - elif ConnectionSet.protocol_is_icmp(protocol): - icmp_props, conn_props = self._parse_icmp(rule.get('icmp'), rule.get('notICMP'), - protocol, src_res_pods, dst_res_pods) - connections.add_connections(protocol, icmp_props) + conn_props = ConnectivityProperties.make_conn_props_from_dict( + {"src_ports": src_res_ports, "dst_ports": dst_res_ports, "protocols": protocols, + "src_peers": src_res_pods, "dst_peers": dst_res_pods}) + elif ProtocolSet.protocol_is_icmp(protocol): + conn_props = self._parse_icmp(rule.get('icmp'), rule.get('notICMP'), protocol, + src_res_pods, dst_res_pods) else: - connections.add_connections(protocol, True) - if self.optimized_run != 'false': - conn_props = ConnectivityProperties.make_conn_props_from_dict({"protocols": protocols, - "src_peers": src_res_pods, - "dst_peers": dst_res_pods}) + conn_props = ConnectivityProperties.make_conn_props_from_dict({"protocols": protocols, + "src_peers": src_res_pods, + "dst_peers": dst_res_pods}) elif not_protocol is not None: - connections.add_all_connections() - connections.remove_protocol(not_protocol) - if self.optimized_run != 'false' and src_res_pods and dst_res_pods: + if src_res_pods and dst_res_pods: protocols = ProtocolSet(True) protocols.remove_protocol(not_protocol) conn_props = ConnectivityProperties.make_conn_props_from_dict({"protocols": protocols, "src_peers": src_res_pods, "dst_peers": dst_res_pods}) else: - connections.allow_all = True - if self.optimized_run != 'false': - conn_props = ConnectivityProperties.make_conn_props_from_dict({"src_peers": src_res_pods, - "dst_peers": dst_res_pods}) - self._verify_named_ports(rule, dst_res_pods, connections) + conn_props = ConnectivityProperties.make_conn_props_from_dict({"src_peers": src_res_pods, + "dst_peers": dst_res_pods}) if not src_res_pods and policy_selected_eps and (is_ingress or not is_profile): self.warning('Rule selects no source endpoints', rule) if not dst_res_pods and policy_selected_eps and (not is_ingress or not is_profile): self.warning('Rule selects no destination endpoints', rule) - return CalicoPolicyRule(src_res_pods, dst_res_pods, connections, action, conn_props) - - def _verify_named_ports(self, rule, rule_eps, rule_conns): - """ - Check the validity of named ports in a given rule: whether a relevant ep refers to the named port and whether - the protocol defined in the policy matches the protocol defined by the ep. Issue warnings as required. - :param dict rule: The unparsed rule (for reference in warnings) - :param Peer.PeerSet rule_eps: The set of eps in which the named ports should be defined - :param ConnectionSet rule_conns: The rule-specified connections, possibly containing named ports - :return: None - """ - if not rule_conns.has_named_ports(): - return - named_ports = rule_conns.get_named_ports() - for protocol, rule_ports in named_ports: - for port in rule_ports: - port_used = False - for pod in rule_eps: - pod_named_port = pod.get_named_ports().get(port) - if pod_named_port: - port_used = True - if ProtocolNameResolver.get_protocol_number(pod_named_port[1]) != protocol: - self.warning(f'Protocol mismatch for named port {port} (vs. Pod {pod.full_name()})', rule) - - if not port_used: - self.warning(f'Named port {port} is not defined in any selected pod', rule) + return CalicoPolicyRule(src_res_pods, dst_res_pods, action, conn_props) def _apply_extra_labels(self, policy_spec, is_profile, profile_name): """ @@ -667,7 +612,7 @@ def _get_selected_peers(self, policy_spec, is_profile, policy_name): def parse_policy(self): """ Parses the input object to create a CalicoNetworkPolicy object - :return: a CalicoNetworkPolicy object with proper PeerSets, ConnectionSets and Actions + :return: a CalicoNetworkPolicy object with proper PeerSets, connectivity properties and Actions :rtype: CalicoNetworkPolicy """ policy_name, policy_ns = \ diff --git a/nca/Parsers/GenericGatewayYamlParser.py b/nca/Parsers/GenericGatewayYamlParser.py index 33b354071..85c1c2dd9 100644 --- a/nca/Parsers/GenericGatewayYamlParser.py +++ b/nca/Parsers/GenericGatewayYamlParser.py @@ -10,7 +10,6 @@ from nca.CoreDS.PortSet import PortSet from nca.CoreDS.ProtocolSet import ProtocolSet from nca.CoreDS.ConnectivityProperties import ConnectivityProperties -from nca.CoreDS.ConnectionSet import ConnectionSet from nca.Resources.PolicyResources.GatewayPolicy import GatewayPolicyRule from .GenericYamlParser import GenericYamlParser @@ -78,8 +77,6 @@ def _make_allow_rules(conn_props, src_peers): :param PeerSet src_peers: the source peers to add to optimized props :return: the list of IngressPolicyRules """ - assert not conn_props.named_ports - assert not conn_props.excluded_named_ports res = [] assert not conn_props.is_active_dimension("src_peers") # extract dst_peers dimension from cubes @@ -91,10 +88,7 @@ def _make_allow_rules(conn_props, src_peers): rule_opt_props = ConnectivityProperties.make_conn_props(conn_cube) dst_peer_set = new_conn_cube["dst_peers"] new_conn_cube.unset_dim("dst_peers") - new_props = ConnectivityProperties.make_conn_props(new_conn_cube) - new_conns = ConnectionSet() - new_conns.add_connections('TCP', new_props) - res.append(GatewayPolicyRule(dst_peer_set, new_conns, rule_opt_props)) + res.append(GatewayPolicyRule(dst_peer_set, rule_opt_props)) return res @staticmethod diff --git a/nca/Parsers/IstioGatewayPolicyGenerator.py b/nca/Parsers/IstioGatewayPolicyGenerator.py index 5579b67db..c9cdd9619 100644 --- a/nca/Parsers/IstioGatewayPolicyGenerator.py +++ b/nca/Parsers/IstioGatewayPolicyGenerator.py @@ -7,7 +7,6 @@ from nca.CoreDS.MinDFA import MinDFA from nca.CoreDS.ConnectivityCube import ConnectivityCube from nca.CoreDS.ConnectivityProperties import ConnectivityProperties -from nca.CoreDS.ConnectionSet import ConnectionSet from nca.CoreDS.ProtocolSet import ProtocolSet from nca.Resources.PolicyResources.GatewayPolicy import GatewayPolicy, GatewayPolicyRule from nca.Resources.PolicyResources.NetworkPolicy import NetworkPolicy @@ -212,14 +211,12 @@ def create_allow_rule(self, source_peers, dest, this_route_conn_cube, is_ingress """ conn_cube = this_route_conn_cube.copy() conn_cube["dst_ports"] = dest.ports - conns = ConnectionSet() - conns.add_connections(self.protocol_name, ConnectivityProperties.make_conn_props(conn_cube)) conn_cube.update({"src_peers": source_peers, "dst_peers": dest.pods, "protocols": self.protocols}) opt_props = ConnectivityProperties.make_conn_props(conn_cube) if is_ingress: - return GatewayPolicyRule(source_peers, conns, opt_props) + return GatewayPolicyRule(source_peers, opt_props) else: - return GatewayPolicyRule(dest.pods, conns, opt_props) + return GatewayPolicyRule(dest.pods, opt_props) @staticmethod def create_deny_rule(source_peers, dst_peers): @@ -229,7 +226,7 @@ def create_deny_rule(source_peers, dst_peers): """ opt_props = ConnectivityProperties.make_conn_props_from_dict({"src_peers": source_peers, "dst_peers": dst_peers}) - return GatewayPolicyRule(dst_peers, ConnectionSet(True), opt_props) + return GatewayPolicyRule(dst_peers, opt_props) def create_gtw_to_mesh_and_deny_policies(self, vs, route, route_cnt, gtw_to_hosts, used_gateways): """ diff --git a/nca/Parsers/IstioPolicyYamlParser.py b/nca/Parsers/IstioPolicyYamlParser.py index 9beeb507f..e70302767 100644 --- a/nca/Parsers/IstioPolicyYamlParser.py +++ b/nca/Parsers/IstioPolicyYamlParser.py @@ -7,7 +7,6 @@ from nca.CoreDS.MinDFA import MinDFA from nca.CoreDS.DimensionsManager import DimensionsManager from nca.CoreDS.Peer import IpBlock, PeerSet -from nca.CoreDS.ConnectionSet import ConnectionSet from nca.CoreDS.PortSet import PortSet from nca.CoreDS.ProtocolSet import ProtocolSet from nca.CoreDS.MethodSet import MethodSet @@ -465,8 +464,7 @@ def parse_ingress_rule(self, rule, selected_peers): Parse a single ingress rule, producing a IstioPolicyRule. :param dict rule: The dict with the rule fields :param PeerSet selected_peers: The selected peers of the policy - :return: A tuple (IstioPolicyRule, ConnectivityProperties) with the proper PeerSet and ConnectionSet, - where ConnectivityProperties is an optimized rule format in a HyperCubeSet format + :return: A tuple (IstioPolicyRule, ConnectivityProperties) with the proper PeerSet and connectivity properties :rtype: tuple(IstioPolicyRule, ConnectivityProperties) """ if rule is None: @@ -495,11 +493,8 @@ def parse_ingress_rule(self, rule, selected_peers): if to_array is not None: for operation_dict in to_array: conn_props |= self.parse_operation(operation_dict) - connections = ConnectionSet() - connections.add_connections('TCP', conn_props) conn_props &= tcp_props else: # no 'to' in the rule => all connections allowed - connections = ConnectionSet(True) conn_props = ConnectivityProperties.get_all_conns_props_per_config_peers(self.peer_container) # condition possible result value: @@ -507,7 +502,6 @@ def parse_ingress_rule(self, rule, selected_peers): # should update either res_pods or condition_props according to the condition condition_array = rule.get('when') # this array can be empty (unlike 'to' and 'from') # the combined condition ("AND" of all conditions) should be applied - condition_conns = ConnectionSet(True) condition_props = ConnectivityProperties.make_all_props() if condition_array is not None: for condition in condition_array: @@ -516,8 +510,6 @@ def parse_ingress_rule(self, rule, selected_peers): res_peers &= condition_res elif isinstance(condition_res, ConnectivityProperties): condition_props &= condition_res - condition_conns = ConnectionSet() - condition_conns.add_connections('TCP', condition_props) condition_props &= tcp_props if not res_peers: self.warning('Rule selects no pods', rule) @@ -526,9 +518,8 @@ def parse_ingress_rule(self, rule, selected_peers): else: condition_props &= ConnectivityProperties.make_conn_props_from_dict({"src_peers": res_peers, "dst_peers": selected_peers}) - connections &= condition_conns conn_props &= condition_props - return IstioPolicyRule(res_peers, connections, conn_props) + return IstioPolicyRule(res_peers, conn_props) @staticmethod def parse_policy_action(action): @@ -545,7 +536,7 @@ def parse_policy_action(action): def parse_policy(self): """ Parses the input object to create a IstioNetworkPolicy object - :return: a IstioNetworkPolicy object with proper PeerSets and ConnectionSets + :return: a IstioNetworkPolicy object with proper PeerSets and connectivity properties :rtype: IstioNetworkPolicy """ policy_name, policy_ns = self.parse_generic_yaml_objects_fields(self.policy, ['AuthorizationPolicy'], diff --git a/nca/Parsers/K8sPolicyYamlParser.py b/nca/Parsers/K8sPolicyYamlParser.py index 8b4e5737f..563006860 100644 --- a/nca/Parsers/K8sPolicyYamlParser.py +++ b/nca/Parsers/K8sPolicyYamlParser.py @@ -5,7 +5,6 @@ import re from nca.CoreDS import Peer -from nca.CoreDS.ConnectionSet import ConnectionSet from nca.CoreDS.PortSet import PortSet from nca.CoreDS.ConnectivityCube import ConnectivityCube from nca.CoreDS.ConnectivityProperties import ConnectivityProperties @@ -330,57 +329,27 @@ def parse_ingress_egress_rule(self, rule, peer_array_key, policy_selected_pods): src_pods = policy_selected_pods dst_pods = res_pods - res_opt_props = ConnectivityProperties.make_empty_props() + res_props = ConnectivityProperties.make_empty_props() ports_array = rule.get('ports', []) if ports_array: - res_conns = ConnectionSet() for port in ports_array: protocol, dest_port_set = self.parse_port(port) if isinstance(protocol, str): protocol = ProtocolNameResolver.get_protocol_number(protocol) - conn_cube = ConnectivityCube.make_from_dict({"dst_ports": dest_port_set}) # K8s doesn't reason about src ports - res_conns.add_connections(protocol, ConnectivityProperties.make_conn_props(conn_cube)) - if self.optimized_run != 'false' and src_pods and dst_pods: + if src_pods and dst_pods: protocols = ProtocolSet.get_protocol_set_with_single_protocol(protocol) - conn_cube.update({"protocols": protocols, "src_peers": src_pods, "dst_peers": dst_pods}) - conn_props = ConnectivityProperties.make_conn_props(conn_cube) - res_opt_props |= conn_props + conn_props = ConnectivityProperties.make_conn_props_from_dict( + {"dst_ports": dest_port_set, "protocols": protocols, "src_peers": src_pods, + "dst_peers": dst_pods}) + res_props |= conn_props else: - res_conns = ConnectionSet(True) - if self.optimized_run != 'false': - res_opt_props = ConnectivityProperties.make_conn_props_from_dict({"src_peers": src_pods, - "dst_peers": dst_pods}) + res_props = ConnectivityProperties.make_conn_props_from_dict({"src_peers": src_pods, + "dst_peers": dst_pods}) if not res_pods: self.warning('Rule selects no pods', rule) - return K8sPolicyRule(res_pods, res_conns, res_opt_props) - - def verify_named_ports(self, rule, rule_pods, rule_conns): - """ - Check the validity of named ports in a given rule: whether a relevant pod refers to the named port and whether - the protocol defined in the policy matches the protocol defined by the Pod. Issue warnings as required. - :param dict rule: The unparsed rule (for reference in warnings) - :param Peer.PeerSet rule_pods: The set of Pods in which the named ports should be defined - :param ConnectionSet rule_conns: The rule-specified connections, possibly containing named ports - :return: None - """ - if not rule_conns.has_named_ports(): - return - named_ports = rule_conns.get_named_ports() - for protocol, rule_ports in named_ports: - for port in rule_ports: - port_used = False - for pod in rule_pods: - pod_named_port = pod.named_ports.get(port) - if pod_named_port: - port_used = True - if ProtocolNameResolver.get_protocol_number(pod_named_port[1]) != protocol: - self.warning(f'Protocol mismatch for named port {port} (vs. Pod {pod.full_name()})', - rule['ports']) - - if not port_used: - self.warning(f'Named port {port} is not defined in any selected pod', rule['ports']) + return K8sPolicyRule(res_pods, res_props) def parse_ingress_rule(self, rule, policy_selected_pods): """ @@ -393,7 +362,6 @@ def parse_ingress_rule(self, rule, policy_selected_pods): :rtype: tuple(K8sPolicyRule, ConnectivityProperties) """ res_rule = self.parse_ingress_egress_rule(rule, 'from', policy_selected_pods) - self.verify_named_ports(rule, policy_selected_pods, res_rule.port_set) return res_rule def parse_egress_rule(self, rule, policy_selected_pods): @@ -407,13 +375,12 @@ def parse_egress_rule(self, rule, policy_selected_pods): :rtype: tuple(K8sPolicyRule, ConnectivityProperties) """ res_rule = self.parse_ingress_egress_rule(rule, 'to', policy_selected_pods) - self.verify_named_ports(rule, res_rule.peer_set, res_rule.port_set) return res_rule def parse_policy(self): """ Parses the input object to create a K8sNetworkPolicy object - :return: a K8sNetworkPolicy object with proper PeerSets and ConnectionSets + :return: a K8sNetworkPolicy object with proper PeerSets and connectivity properties :rtype: K8sNetworkPolicy """ policy_name, policy_ns = self.parse_generic_yaml_objects_fields(self.policy, ['NetworkPolicy'], diff --git a/nca/Resources/PolicyResources/CalicoNetworkPolicy.py b/nca/Resources/PolicyResources/CalicoNetworkPolicy.py index 38570d534..e8ec4ff00 100644 --- a/nca/Resources/PolicyResources/CalicoNetworkPolicy.py +++ b/nca/Resources/PolicyResources/CalicoNetworkPolicy.py @@ -4,10 +4,9 @@ # from enum import Enum -from nca.CoreDS.ConnectionSet import ConnectionSet from nca.CoreDS.ConnectivityProperties import ConnectivityProperties from nca.CoreDS import Peer -from .NetworkPolicy import PolicyConnections, OptimizedPolicyConnections, NetworkPolicy +from .NetworkPolicy import PolicyConnections, NetworkPolicy class CalicoPolicyRule: @@ -23,24 +22,23 @@ class ActionType(Enum): Log = 2 Pass = 3 - def __init__(self, src_peers, dst_peers, connections, action, opt_props): + def __init__(self, src_peers, dst_peers, action, props): """ :param Peer.PeerSet src_peers: The source peers this rule refers to :param Peer.PeerSet dst_peers: The destination peers this rule refers to - :param ConnectionSet connections: The connections allowed/denied/passed by this rule :param ActionType action: The rule action + :param ConnectivityProperties props: the connectivity properties represented by this rule """ self.src_peers = src_peers self.dst_peers = dst_peers - self.connections = connections self.action = action - self.optimized_props = opt_props + self.props = props # copy of optimized props (used by src_peers/dst_peers domain-updating mechanism) - self.optimized_props_copy = ConnectivityProperties() + self.props_copy = ConnectivityProperties() def __eq__(self, other): return self.src_peers == other.src_peers and self.dst_peers == other.dst_peers and \ - self.connections == other.connections and self.action == other.action + self.props == other.props and self.action == other.action def contained_in(self, other): """ @@ -49,7 +47,7 @@ def contained_in(self, other): :rtype: bool """ return self.src_peers.issubset(other.src_peers) and self.dst_peers.issubset(other.dst_peers) and \ - self.connections.contained_in(other.connections) + self.props.contained_in(other.props) @staticmethod def action_str_to_action_type(action_str): @@ -85,82 +83,42 @@ def __eq__(self, other): def _update_opt_props_by_order(self, is_ingress): # handle the order of rules for rule in self.ingress_rules if is_ingress else self.egress_rules: - props = rule.optimized_props.copy() + props = rule.props.copy() if rule.action == CalicoPolicyRule.ActionType.Allow: - props -= self._optimized_deny_ingress_props if is_ingress else self._optimized_deny_egress_props - props -= self._optimized_pass_ingress_props if is_ingress else self._optimized_pass_egress_props + props -= self._deny_ingress_props if is_ingress else self._deny_egress_props + props -= self._pass_ingress_props if is_ingress else self._pass_egress_props if is_ingress: - self._optimized_allow_ingress_props |= props + self._allow_ingress_props |= props else: - self._optimized_allow_egress_props |= props + self._allow_egress_props |= props elif rule.action == CalicoPolicyRule.ActionType.Deny: - props -= self._optimized_allow_ingress_props if is_ingress else self._optimized_allow_egress_props - props -= self._optimized_pass_ingress_props if is_ingress else self._optimized_pass_egress_props + props -= self._allow_ingress_props if is_ingress else self._allow_egress_props + props -= self._pass_ingress_props if is_ingress else self._pass_egress_props if is_ingress: - self._optimized_deny_ingress_props |= props + self._deny_ingress_props |= props else: - self._optimized_deny_egress_props |= props + self._deny_egress_props |= props elif rule.action == CalicoPolicyRule.ActionType.Pass: - props -= self._optimized_allow_ingress_props if is_ingress else self._optimized_allow_egress_props - props -= self._optimized_deny_ingress_props if is_ingress else self._optimized_deny_egress_props + props -= self._allow_ingress_props if is_ingress else self._allow_egress_props + props -= self._deny_ingress_props if is_ingress else self._deny_egress_props if is_ingress: - self._optimized_pass_ingress_props |= props + self._pass_ingress_props |= props else: - self._optimized_pass_egress_props |= props + self._pass_egress_props |= props - def sync_opt_props(self): + def sync_props(self): """ - If optimized props of the policy are not synchronized (self.optimized_props_in_sync is False), + If optimized props of the policy are not synchronized (self.props_in_sync is False), compute optimized props of the policy according to the optimized props of its rules """ - if self.optimized_props_in_sync: + if self.props_in_sync: return - self._init_opt_props() + self._init_props() self._update_opt_props_by_order(True) self._update_opt_props_by_order(False) - self.optimized_props_in_sync = True + self.props_in_sync = True - def allowed_connections(self, from_peer, to_peer, is_ingress): - """ - Evaluate the set of connections this policy allows/denies/passes between two peers - :param Peer.Peer from_peer: The source peer - :param Peer.Peer to_peer: The target peer - :param bool is_ingress: whether we evaluate ingress rules only or egress rules only - :return: A PolicyConnections object containing sets of allowed/denied/pass connections - :rtype: PolicyConnections - """ - captured = is_ingress and self.affects_ingress and to_peer in self.selected_peers or \ - not is_ingress and self.affects_egress and from_peer in self.selected_peers - if not captured: - return PolicyConnections(False) - - allowed_conns = ConnectionSet() - denied_conns = ConnectionSet() - pass_conns = ConnectionSet() - rules = self.ingress_rules if is_ingress else self.egress_rules - for rule in rules: - if from_peer in rule.src_peers and to_peer in rule.dst_peers: - rule_conns = rule.connections.copy() # we need a copy because convert_named_ports is destructive - rule_conns.convert_named_ports(to_peer.get_named_ports()) - - if rule.action == CalicoPolicyRule.ActionType.Allow: - rule_conns -= denied_conns - rule_conns -= pass_conns - allowed_conns |= rule_conns - elif rule.action == CalicoPolicyRule.ActionType.Deny: - rule_conns -= allowed_conns - rule_conns -= pass_conns - denied_conns |= rule_conns - elif rule.action == CalicoPolicyRule.ActionType.Pass: - rule_conns -= allowed_conns - rule_conns -= denied_conns - pass_conns |= rule_conns - else: - pass # Nothing to do for Log action - does not affect connectivity - - return PolicyConnections(True, allowed_conns, denied_conns, pass_conns) - - def allowed_connections_optimized(self, is_ingress): + def allowed_connections(self, is_ingress): """ Evaluate the set of connections this policy allows/denies/passes between any two peers :param bool is_ingress: whether we evaluate ingress rules only or egress rules only @@ -169,16 +127,16 @@ def allowed_connections_optimized(self, is_ingress): and the peer set of captured peers by this policy. :rtype: tuple (ConnectivityProperties, ConnectivityProperties, PeerSet) """ - res_conns = OptimizedPolicyConnections() + res_conns = PolicyConnections() if is_ingress: - res_conns.allowed_conns = self.optimized_allow_ingress_props().copy() - res_conns.denied_conns = self.optimized_deny_ingress_props().copy() - res_conns.pass_conns = self.optimized_pass_ingress_props().copy() + res_conns.allowed_conns = self.allow_ingress_props().copy() + res_conns.denied_conns = self.deny_ingress_props().copy() + res_conns.pass_conns = self.pass_ingress_props().copy() res_conns.captured = self.selected_peers if self.affects_ingress else Peer.PeerSet() else: - res_conns.allowed_conns = self.optimized_allow_egress_props().copy() - res_conns.denied_conns = self.optimized_deny_egress_props().copy() - res_conns.pass_conns = self.optimized_pass_egress_props().copy() + res_conns.allowed_conns = self.allow_egress_props().copy() + res_conns.denied_conns = self.deny_egress_props().copy() + res_conns.pass_conns = self.pass_egress_props().copy() res_conns.captured = self.selected_peers if self.affects_egress else Peer.PeerSet() return res_conns @@ -204,25 +162,6 @@ def clone_without_rule(self, rule_to_exclude, ingress_rule): res.add_ingress_rule(rule) return res - def referenced_ip_blocks(self, exclude_ipv6=False): - """ - :param bool exclude_ipv6: indicates if to exclude the automatically added IPv6 addresses in the referenced ip_blocks. - IPv6 addresses that are referenced in the policy by the user will always be included - :return: A set of all ipblocks referenced in one of the policy rules (one Peer object per one ip range) - :rtype: Peer.PeerSet - """ - res = Peer.PeerSet() - for rule in self.egress_rules: - for peer in rule.dst_peers: - if isinstance(peer, Peer.IpBlock) and self._include_ip_block(peer, exclude_ipv6): - res |= peer.split() - for rule in self.ingress_rules: - for peer in rule.src_peers: - if isinstance(peer, Peer.IpBlock) and self._include_ip_block(peer, exclude_ipv6): - res |= peer.split() - - return res - def has_empty_rules(self, config_name=''): """ Checks whether the policy contains empty rules (rules that do not select any peers) diff --git a/nca/Resources/PolicyResources/GatewayPolicy.py b/nca/Resources/PolicyResources/GatewayPolicy.py index f48815e29..e09fdd3be 100644 --- a/nca/Resources/PolicyResources/GatewayPolicy.py +++ b/nca/Resources/PolicyResources/GatewayPolicy.py @@ -4,30 +4,27 @@ # from enum import Enum -from nca.CoreDS.ConnectionSet import ConnectionSet from nca.CoreDS.ConnectivityProperties import ConnectivityProperties from nca.CoreDS.Peer import PeerSet -from nca.Resources.PolicyResources.NetworkPolicy import PolicyConnections, OptimizedPolicyConnections, NetworkPolicy +from nca.Resources.PolicyResources.NetworkPolicy import PolicyConnections, NetworkPolicy class GatewayPolicyRule: """ A class representing a single rule in a GatewayPolicy object """ - def __init__(self, peer_set, connections, opt_props): + def __init__(self, peer_set, props): """ :param Peer.PeerSet peer_set: The set of peers this rule allows connection to - :param ConnectionSet connections: The set of connections allowed by this rule - :param ConnectivityProperties opt_props: the optimized connections + :param ConnectivityProperties props: the connections """ self.peer_set = peer_set - self.connections = connections - self.optimized_props = opt_props - # copy of optimized props (used by src_peers/dst_peers domain-updating mechanism) - self.optimized_props_copy = ConnectivityProperties() + self.props = props + # copy of props (used by src_peers/dst_peers domain-updating mechanism) + self.props_copy = ConnectivityProperties() def __eq__(self, other): - return self.peer_set == other.peer_set and self.connections == other.connections + return self.peer_set == other.peer_set and self.props == other.props def contained_in(self, other): """ @@ -35,7 +32,7 @@ def contained_in(self, other): :return: whether the self rule is contained in the other rule (self doesn't allow anything that other does not) :type: bool """ - return self.peer_set.issubset(other.peer_set) and self.connections.contained_in(other.connections) + return self.peer_set.issubset(other.peer_set) and self.props.contained_in(other.props) class GatewayPolicy(NetworkPolicy): @@ -94,70 +91,42 @@ def add_egress_rules(self, rules): """ self.egress_rules.extend(rules) - def sync_opt_props(self): + def sync_props(self): """ - If optimized props of the policy are not synchronized (self.optimized_props_in_sync is False), - compute optimized props of the policy according to the optimized props of its rules + If props of the policy are not synchronized (self.props_in_sync is False), + compute props of the policy according to the props of its rules """ - if self.optimized_props_in_sync: + if self.props_in_sync: return - self._init_opt_props() + self._init_props() for rule in self.ingress_rules: if self.action == GatewayPolicy.ActionType.Allow: - self._optimized_allow_ingress_props |= rule.optimized_props + self._allow_ingress_props |= rule.props elif self.action == GatewayPolicy.ActionType.Deny: - self._optimized_deny_ingress_props |= rule.optimized_props + self._deny_ingress_props |= rule.props for rule in self.egress_rules: if self.action == GatewayPolicy.ActionType.Allow: - self._optimized_allow_egress_props |= rule.optimized_props + self._allow_egress_props |= rule.props elif self.action == GatewayPolicy.ActionType.Deny: - self._optimized_deny_egress_props |= rule.optimized_props - self.optimized_props_in_sync = True + self._deny_egress_props |= rule.props + self.props_in_sync = True - def allowed_connections(self, from_peer, to_peer, is_ingress): - """ - Evaluate the set of connections this gateway policy allows between two peers - :param Peer.Peer from_peer: The source peer - :param Peer.Peer to_peer: The target peer - :param bool is_ingress: whether we evaluate ingress rules only or egress rules only. - :return: A PolicyConnections object containing sets of allowed/denied connections - :rtype: PolicyConnections - """ - - captured = is_ingress and self.affects_ingress and to_peer in self.selected_peers or \ - not is_ingress and self.affects_egress and from_peer in self.selected_peers - if not captured: - return PolicyConnections(False) - - conns = ConnectionSet() - rules = self.ingress_rules if is_ingress else self.egress_rules - other_peer = from_peer if is_ingress else to_peer - for rule in rules: - if other_peer in rule.peer_set: - assert not rule.connections.has_named_ports() - conns |= rule.connections - - if self.action == self.ActionType.Allow: - return PolicyConnections(True, allowed_conns=conns) - else: # Deny - return PolicyConnections(True, denied_conns=conns) - - def allowed_connections_optimized(self, is_ingress): + def allowed_connections(self, is_ingress): """ Evaluate the set of connections this ingress resource allows between any two peers :param bool is_ingress: whether we evaluate ingress rules only or egress rules only. :return: A OptimizedPolicyConnections object containing all allowed/denied connections for any peers and the peer set of captured peers by this policy. - :rtype: OptimizedPolicyConnections + :rtype: PolicyConnections """ - res_conns = OptimizedPolicyConnections() + res_conns = PolicyConnections() if is_ingress: - res_conns.allowed_conns = self.optimized_allow_ingress_props().copy() - res_conns.denied_conns = self.optimized_deny_ingress_props().copy() + res_conns.allowed_conns = self.allow_ingress_props().copy() + res_conns.denied_conns = self.deny_ingress_props().copy() res_conns.captured = self.selected_peers if self.affects_ingress else PeerSet() else: - res_conns.allowed_conns = self.optimized_allow_egress_props().copy() - res_conns.denied_conns = self.optimized_deny_egress_props().copy() + res_conns.allowed_conns = self.allow_egress_props().copy() + res_conns.denied_conns = self.deny_egress_props().copy() res_conns.captured = self.selected_peers if self.affects_egress else PeerSet() return res_conns diff --git a/nca/Resources/PolicyResources/IstioNetworkPolicy.py b/nca/Resources/PolicyResources/IstioNetworkPolicy.py index 54b29a487..2ca99c273 100644 --- a/nca/Resources/PolicyResources/IstioNetworkPolicy.py +++ b/nca/Resources/PolicyResources/IstioNetworkPolicy.py @@ -4,10 +4,9 @@ # from enum import Enum -from nca.CoreDS.ConnectionSet import ConnectionSet from nca.CoreDS.ConnectivityProperties import ConnectivityProperties -from nca.CoreDS.Peer import PeerSet, IpBlock -from .NetworkPolicy import PolicyConnections, OptimizedPolicyConnections, NetworkPolicy +from nca.CoreDS.Peer import PeerSet +from .NetworkPolicy import PolicyConnections, NetworkPolicy class IstioPolicyRule: @@ -15,20 +14,18 @@ class IstioPolicyRule: A class representing a single ingress rule in a Istio AuthorizationPolicy object """ - def __init__(self, peer_set, connections, opt_props): + def __init__(self, peer_set, props): """ :param Peer.PeerSet peer_set: The set of peers this rule allows connection from - :param ConnectionSet connections: The set of connections allowed/denied by this rule (the action resides in the policy) + :param ConnectivityProperties props: the connections """ - # TODO: extend connections (ConnectionSet) to represent HTTP/grpc requests attributes self.peer_set = peer_set - self.connections = connections - self.optimized_props = opt_props - # copy of optimized props (used by src_peers/dst_peers domain-updating mechanism) - self.optimized_props_copy = ConnectivityProperties() + self.props = props + # copy of props (used by src_peers/dst_peers domain-updating mechanism) + self.props_copy = ConnectivityProperties() def __eq__(self, other): - return self.peer_set == other.peer_set and self.connections == other.connections + return self.peer_set == other.peer_set and self.props == other.props def contained_in(self, other): """ @@ -36,7 +33,7 @@ def contained_in(self, other): :return: whether the self rule is contained in the other rule (self doesn't allow anything that other does not) :type: bool """ - return self.peer_set.issubset(other.peer_set) and self.connections.contained_in(other.connections) + return self.peer_set.issubset(other.peer_set) and self.props.contained_in(other.props) class IstioNetworkPolicy(NetworkPolicy): @@ -68,52 +65,24 @@ def __lt__(self, other): # required so we can evaluate the policies according t return self.action == IstioNetworkPolicy.ActionType.Deny return False - def sync_opt_props(self): + def sync_props(self): """ - If optimized props of the policy are not synchronized (self.optimized_props_in_sync is False), - compute optimized props of the policy according to the optimized props of its rules + If props of the policy are not synchronized (self.props_in_sync is False), + compute props of the policy according to the optimized props of its rules """ - if self.optimized_props_in_sync: + if self.props_in_sync: return - self._init_opt_props() + self._init_props() for rule in self.ingress_rules: if self.action == IstioNetworkPolicy.ActionType.Allow: - self._optimized_allow_ingress_props |= rule.optimized_props + self._allow_ingress_props |= rule.props elif self.action == IstioNetworkPolicy.ActionType.Deny: - self._optimized_deny_ingress_props |= rule.optimized_props + self._deny_ingress_props |= rule.props self._optimized_allow_egress_props = ConnectivityProperties.get_all_conns_props_per_domain_peers() - self.optimized_props_in_sync = True + self.props_in_sync = True - def allowed_connections(self, from_peer, to_peer, is_ingress): - """ - Evaluate the set of connections this policy allows/denies/passes between two peers - :param Peer.Peer from_peer: The source peer - :param Peer.Peer to_peer: The target peer - :param bool is_ingress: whether we evaluate ingress rules only or egress rules only - :return: A PolicyConnections object containing sets of allowed/denied/pass connections - :rtype: PolicyConnections - """ - - # TODO: currently not handling egress, istio authorization policies have no egress rules - if not is_ingress: - return PolicyConnections(False, ConnectionSet(True)) - - captured = to_peer in self.selected_peers - if not captured: - return PolicyConnections(False) - - allowed_conns = ConnectionSet() - denied_conns = ConnectionSet() - - collected_conns = allowed_conns if self.action == IstioNetworkPolicy.ActionType.Allow else denied_conns - for rule in self.ingress_rules: - if from_peer in rule.peer_set: - collected_conns |= rule.connections - - return PolicyConnections(True, allowed_conns, denied_conns) - - def allowed_connections_optimized(self, is_ingress): + def allowed_connections(self, is_ingress): """ Evaluate the set of connections this policy allows/denied/passed between any two peers :param bool is_ingress: whether we evaluate ingress rules only or egress rules only @@ -122,31 +91,17 @@ def allowed_connections_optimized(self, is_ingress): and the peer set of captured peers by this policy. :rtype: tuple (ConnectivityProperties, ConnectivityProperties, PeerSet) """ - res_conns = OptimizedPolicyConnections() + res_conns = PolicyConnections() if is_ingress: - res_conns.allowed_conns = self.optimized_allow_ingress_props().copy() - res_conns.denied_conns = self.optimized_deny_ingress_props().copy() + res_conns.allowed_conns = self.allow_ingress_props().copy() + res_conns.denied_conns = self.deny_ingress_props().copy() res_conns.captured = self.selected_peers else: - res_conns.allowed_conns = self.optimized_allow_egress_props().copy() - res_conns.denied_conns = self.optimized_deny_egress_props().copy() + res_conns.allowed_conns = self.allow_egress_props().copy() + res_conns.denied_conns = self.deny_egress_props().copy() res_conns.captured = PeerSet() return res_conns - def referenced_ip_blocks(self, exclude_ipv6=False): - """ - :param bool exclude_ipv6: indicates if to exclude the automatically added IPv6 addresses in the referenced ip_blocks. - IPv6 addresses that are referenced in the policy by the user will always be included - :return: A set of all ipblocks referenced in one of the policy rules (one Peer object per one ip range) - :rtype: Peer.PeerSet - """ - res = PeerSet() - for rule in self.ingress_rules: - for peer in rule.peer_set: - if isinstance(peer, IpBlock) and self._include_ip_block(peer, exclude_ipv6): - res |= peer.split() - return res - def has_empty_rules(self, config_name=''): """ Checks whether the policy contains empty rules (rules that do not select any peers) diff --git a/nca/Resources/PolicyResources/IstioSidecar.py b/nca/Resources/PolicyResources/IstioSidecar.py index 9555c49c6..37f1d6068 100644 --- a/nca/Resources/PolicyResources/IstioSidecar.py +++ b/nca/Resources/PolicyResources/IstioSidecar.py @@ -5,11 +5,10 @@ from dataclasses import dataclass from enum import Enum -from nca.CoreDS.ConnectionSet import ConnectionSet -from nca.CoreDS.Peer import IpBlock, PeerSet, DNSEntry +from nca.CoreDS.Peer import IpBlock, PeerSet from nca.CoreDS.ProtocolSet import ProtocolSet from nca.CoreDS.ConnectivityProperties import ConnectivityProperties -from nca.Resources.PolicyResources.NetworkPolicy import PolicyConnections, OptimizedPolicyConnections, NetworkPolicy +from nca.Resources.PolicyResources.NetworkPolicy import PolicyConnections, NetworkPolicy @dataclass @@ -29,9 +28,9 @@ def __init__(self, peer_set, peers_for_ns_compare): self.special_egress_peer_set = peers_for_ns_compare # set of peers captured by a global sidecar with hosts of # './' form - then peers in this set will be in allowed connections only if are in the same namespace of the # source peer captured by the sidecar - self.optimized_props = ConnectivityProperties() - # copy of optimized props (used by src_peers/dst_peers domain-updating mechanism) - self.optimized_props_copy = ConnectivityProperties() + self.props = ConnectivityProperties() + # copy of props (used by src_peers/dst_peers domain-updating mechanism) + self.props_copy = ConnectivityProperties() class IstioSidecar(NetworkPolicy): @@ -51,63 +50,28 @@ def __init__(self, name, namespace): def __eq__(self, other): return super().__eq__(other) and self.default_sidecar == other.default_sidecar - def sync_opt_props(self): + def sync_props(self): """ - If optimized props of the policy are not synchronized (self.optimized_props_in_sync is False), - compute optimized props of the policy according to the optimized props of its rules + If props of the policy are not synchronized (self.props_in_sync is False), + compute props of the policy according to the props of its rules """ - if self.optimized_props_in_sync: + if self.props_in_sync: return - self._init_opt_props() - self._optimized_allow_ingress_props = ConnectivityProperties.get_all_conns_props_per_domain_peers() + self._init_props() + self._allow_ingress_props = ConnectivityProperties.get_all_conns_props_per_domain_peers() for rule in self.egress_rules: - self._optimized_allow_egress_props |= rule.optimized_props - self.optimized_props_in_sync = True + self._allow_egress_props |= rule.props + self.props_in_sync = True - def allowed_connections(self, from_peer, to_peer, is_ingress): - """ - Evaluate the set of connections this policy allows/denies/passes between two peers - :param Peer.Peer from_peer: The source peer - :param Peer.Peer to_peer: The target peer - :param bool is_ingress: whether we evaluate ingress rules only or egress rules only - :return: A PolicyConnections object containing sets of allowed/denied/pass connections - :rtype: PolicyConnections - """ - # currently not handling ingress - if is_ingress: - return PolicyConnections(False, ConnectionSet(True)) - - captured = from_peer in self.selected_peers - # if not captured, or captured but the sidecar is not in from_peer top priority, don't consider connections - if not captured: - return PolicyConnections(False) - - # connections to IP-block is enabled only if the outbound mode is allow-any (disabled for registry only) - if isinstance(to_peer, IpBlock) and self.outbound_mode == IstioSidecar.OutboundMode.ALLOW_ANY: - return PolicyConnections(True, allowed_conns=ConnectionSet(True)) - - # since sidecar rules include only peer sets for now, if a to_peer appears in any rule then connections allowed - for rule in self.egress_rules: - if isinstance(to_peer, DNSEntry) and \ - (to_peer in rule.egress_peer_set or to_peer in rule.special_egress_peer_set): - return PolicyConnections(True, allowed_conns=ConnectionSet.get_all_tcp_connections()) - if to_peer in rule.egress_peer_set or \ - (to_peer in rule.special_egress_peer_set and from_peer.namespace == to_peer.namespace): - return PolicyConnections(True, allowed_conns=ConnectionSet(True)) - - # egress from from_peer to to_peer is not allowed : if to_peer not been captured in the rules' egress_peer_set, - # or if the sidecar is global and to_peer is not in same namespace of from_peer while rule host's ns is '.' - return PolicyConnections(True, allowed_conns=ConnectionSet()) - - def allowed_connections_optimized(self, is_ingress): - res_conns = OptimizedPolicyConnections() + def allowed_connections(self, is_ingress): + res_conns = PolicyConnections() if is_ingress: - res_conns.allowed_conns = self.optimized_allow_ingress_props().copy() - res_conns.denied_conns = self.optimized_deny_ingress_props().copy() + res_conns.allowed_conns = self.allow_ingress_props().copy() + res_conns.denied_conns = self.deny_ingress_props().copy() res_conns.captured = PeerSet() else: - res_conns.allowed_conns = self.optimized_allow_egress_props().copy() - res_conns.denied_conns = self.optimized_deny_egress_props().copy() + res_conns.allowed_conns = self.allow_egress_props().copy() + res_conns.denied_conns = self.deny_egress_props().copy() res_conns.captured = self.selected_peers if self.affects_egress else PeerSet() return res_conns @@ -169,7 +133,7 @@ def create_opt_egress_props(self, peer_container): # connections to IP-block is enabled only if the outbound mode is allow-any (disabled for registry only) if self.outbound_mode == IstioSidecar.OutboundMode.ALLOW_ANY: ip_blocks = IpBlock.get_all_ips_block_peer_set() - rule.optimized_props |= \ + rule.props |= \ ConnectivityProperties.make_conn_props_from_dict({"src_peers": self.selected_peers, "dst_peers": ip_blocks}) @@ -177,19 +141,19 @@ def create_opt_egress_props(self, peer_container): dst_dns_entries = dns_entries & (rule.egress_peer_set | rule.special_egress_peer_set) if self.selected_peers and dst_dns_entries: protocols = ProtocolSet.get_protocol_set_with_single_protocol('TCP') - rule.optimized_props |= \ + rule.props |= \ ConnectivityProperties.make_conn_props_from_dict({"src_peers": self.selected_peers, "dst_peers": dst_dns_entries, "protocols": protocols}) if self.selected_peers and rule.egress_peer_set: - rule.optimized_props |= \ + rule.props |= \ ConnectivityProperties.make_conn_props_from_dict({"src_peers": self.selected_peers, "dst_peers": rule.egress_peer_set}) peers_sets_by_ns = self.combine_peer_sets_by_ns(self.selected_peers, rule.special_egress_peer_set, peer_container) for (from_peers, to_peers) in peers_sets_by_ns: if from_peers and to_peers: - rule.optimized_props |= \ + rule.props |= \ ConnectivityProperties.make_conn_props_from_dict({"src_peers": from_peers, "dst_peers": to_peers}) diff --git a/nca/Resources/PolicyResources/K8sNetworkPolicy.py b/nca/Resources/PolicyResources/K8sNetworkPolicy.py index bee0f242b..887487d56 100644 --- a/nca/Resources/PolicyResources/K8sNetworkPolicy.py +++ b/nca/Resources/PolicyResources/K8sNetworkPolicy.py @@ -2,29 +2,27 @@ # Copyright 2020- IBM Inc. All rights reserved # SPDX-License-Identifier: Apache2.0 # -from nca.CoreDS.ConnectionSet import ConnectionSet from nca.CoreDS.ConnectivityProperties import ConnectivityProperties from nca.CoreDS import Peer -from .NetworkPolicy import PolicyConnections, OptimizedPolicyConnections, NetworkPolicy +from .NetworkPolicy import PolicyConnections, NetworkPolicy class K8sPolicyRule: """ A class representing a single ingress/egress rule in a K8s NetworkPolicy object """ - def __init__(self, peer_set, port_set, opt_props): + def __init__(self, peer_set, props): """ :param Peer.PeerSet peer_set: The set of peers this rule allows connection to/from - :param ConnectionSet port_set: The set of connections allowed by this rule + :param ConnectivityProperties props: the connections """ self.peer_set = peer_set - self.port_set = port_set - self.optimized_props = opt_props - # copy of optimized props (used by src_peers/dst_peers domain-updating mechanism) - self.optimized_props_copy = ConnectivityProperties() + self.props = props + # copy of props (used by src_peers/dst_peers domain-updating mechanism) + self.props_copy = ConnectivityProperties() def __eq__(self, other): - return self.peer_set == other.peer_set and self.port_set == other.port_set + return self.peer_set == other.peer_set and self.props == other.props def contained_in(self, other): """ @@ -32,54 +30,28 @@ def contained_in(self, other): :return: whether the self rule is contained in the other rule (self doesn't allow anything that other does not) :type: bool """ - return self.peer_set.issubset(other.peer_set) and self.port_set.contained_in(other.port_set) + return self.peer_set.issubset(other.peer_set) and self.props.contained_in(other.props) class K8sNetworkPolicy(NetworkPolicy): """ This class implements K8s-specific logic for NetworkPolicies """ - def sync_opt_props(self): + def sync_props(self): """ - If optimized props of the policy are not synchronized (self.optimized_props_in_sync is False), - compute optimized props of the policy according to the optimized props of its rules + If props of the policy are not synchronized (self.props_in_sync is False), + compute props of the policy according to the props of its rules """ - if self.optimized_props_in_sync: + if self.props_in_sync: return - self._init_opt_props() + self._init_props() for rule in self.ingress_rules: - self._optimized_allow_ingress_props |= rule.optimized_props + self._allow_ingress_props |= rule.props for rule in self.egress_rules: - self._optimized_allow_egress_props |= rule.optimized_props - self.optimized_props_in_sync = True + self._allow_egress_props |= rule.props + self.props_in_sync = True - def allowed_connections(self, from_peer, to_peer, is_ingress): - """ - Evaluate the set of connections this policy allows between two peers - (either the allowed ingress into to_peer or the allowed egress from from_peer). - :param Peer.Peer from_peer: The source peer - :param Peer.Peer to_peer: The target peer - :param bool is_ingress: whether we evaluate ingress rules only or egress rules only - :return: A PolicyConnections object containing sets of allowed connections - :rtype: PolicyConnections - """ - captured = is_ingress and self.affects_ingress and to_peer in self.selected_peers or \ - not is_ingress and self.affects_egress and from_peer in self.selected_peers - if not captured: - return PolicyConnections(False) - - allowed_conns = ConnectionSet() - rules = self.ingress_rules if is_ingress else self.egress_rules - other_peer = from_peer if is_ingress else to_peer - for rule in rules: - if other_peer in rule.peer_set: - rule_conns = rule.port_set.copy() # we need a copy because convert_named_ports is destructive - rule_conns.convert_named_ports(to_peer.get_named_ports()) - allowed_conns |= rule_conns - - return PolicyConnections(True, allowed_conns) - - def allowed_connections_optimized(self, is_ingress): + def allowed_connections(self, is_ingress): """ Return the set of connections this policy allows between any two peers (either ingress or egress). @@ -89,12 +61,12 @@ def allowed_connections_optimized(self, is_ingress): and the peer set of captured peers by this policy. :rtype: tuple (ConnectivityProperties, ConnectivityProperties, PeerSet) """ - res_conns = OptimizedPolicyConnections() + res_conns = PolicyConnections() if is_ingress: - res_conns.allowed_conns = self.optimized_allow_ingress_props().copy() + res_conns.allowed_conns = self.allow_ingress_props().copy() res_conns.captured = self.selected_peers if self.affects_ingress else Peer.PeerSet() else: - res_conns.allowed_conns = self.optimized_allow_egress_props().copy() + res_conns.allowed_conns = self.allow_egress_props().copy() res_conns.captured = self.selected_peers if self.affects_egress else Peer.PeerSet() return res_conns @@ -119,25 +91,6 @@ def clone_without_rule(self, rule_to_exclude, ingress_rule): res.add_ingress_rule(rule) return res - def referenced_ip_blocks(self, exclude_ipv6=False): - """ - :param bool exclude_ipv6: indicates if to exclude the automatically added IPv6 addresses in the referenced ip_blocks. - IPv6 addresses that are referenced in the policy by the user will always be included - :return: A set of all ipblocks referenced in one of the policy rules (one Peer object per one ip range) - :rtype: Peer.PeerSet - """ - res = Peer.PeerSet() - for rule in self.egress_rules: - for peer in rule.peer_set: - if isinstance(peer, Peer.IpBlock) and self._include_ip_block(peer, exclude_ipv6): - res |= peer.split() - for rule in self.ingress_rules: - for peer in rule.peer_set: - if isinstance(peer, Peer.IpBlock) and self._include_ip_block(peer, exclude_ipv6): - res |= peer.split() - - return res - def has_empty_rules(self, config_name=''): """ Checks whether the policy contains empty rules (rules that do not select any peers) diff --git a/nca/Resources/PolicyResources/NetworkPolicy.py b/nca/Resources/PolicyResources/NetworkPolicy.py index 1d3a45959..1e64abbb9 100644 --- a/nca/Resources/PolicyResources/NetworkPolicy.py +++ b/nca/Resources/PolicyResources/NetworkPolicy.py @@ -5,7 +5,6 @@ from enum import Enum from dataclasses import dataclass -from nca.CoreDS.ConnectionSet import ConnectionSet from nca.CoreDS.Peer import PeerSet from nca.CoreDS.ConnectivityProperties import ConnectivityProperties @@ -55,12 +54,12 @@ def __init__(self, name, namespace): self.ingress_rules = [] self.egress_rules = [] - # The flag below is used for lazy calculation of optimized policy connections (as a union of rules connections) + # The flag below is used for lazy calculation of policy connections (as a union of rules connections) # The flag is set to False for new policies (including in redundancy query, when removing a rule from policy by # creating a new policy with a subset of rules), or after changing peers domains (per query). - # When this flag is False, the sync_opt_props function will (re)calculate optimized policy connections. - self.optimized_props_in_sync = False - self._init_opt_props() + # When this flag is False, the sync_props function will (re)calculate policy connections. + self.props_in_sync = False + self._init_props() self.affects_ingress = False # whether the policy affects the ingress of the selected peers self.affects_egress = False # whether the policy affects the egress of the selected peers @@ -70,46 +69,46 @@ def __init__(self, name, namespace): self.has_ipv6_addresses = False # whether the policy referenced ip addresses (by user) # if this flag is False, excluding ipv6 addresses from the query results will be enabled - def _init_opt_props(self): + def _init_props(self): """ The members below are used for lazy evaluation of policy connectivity properties. NOTE: THEY CANNOT BE ACCESSED DIRECTLY, ONLY BY 'GETTER' METHODS BELOW! """ - self._optimized_allow_ingress_props = ConnectivityProperties.make_empty_props() - self._optimized_deny_ingress_props = ConnectivityProperties.make_empty_props() - self._optimized_pass_ingress_props = ConnectivityProperties.make_empty_props() - self._optimized_allow_egress_props = ConnectivityProperties.make_empty_props() - self._optimized_deny_egress_props = ConnectivityProperties.make_empty_props() - self._optimized_pass_egress_props = ConnectivityProperties.make_empty_props() + self._allow_ingress_props = ConnectivityProperties.make_empty_props() + self._deny_ingress_props = ConnectivityProperties.make_empty_props() + self._pass_ingress_props = ConnectivityProperties.make_empty_props() + self._allow_egress_props = ConnectivityProperties.make_empty_props() + self._deny_egress_props = ConnectivityProperties.make_empty_props() + self._pass_egress_props = ConnectivityProperties.make_empty_props() - def optimized_allow_ingress_props(self): - self.sync_opt_props() - return self._optimized_allow_ingress_props + def allow_ingress_props(self): + self.sync_props() + return self._allow_ingress_props - def optimized_deny_ingress_props(self): - self.sync_opt_props() - return self._optimized_deny_ingress_props + def deny_ingress_props(self): + self.sync_props() + return self._deny_ingress_props - def optimized_pass_ingress_props(self): - self.sync_opt_props() - return self._optimized_pass_ingress_props + def pass_ingress_props(self): + self.sync_props() + return self._pass_ingress_props - def optimized_allow_egress_props(self): - self.sync_opt_props() - return self._optimized_allow_egress_props + def allow_egress_props(self): + self.sync_props() + return self._allow_egress_props - def optimized_deny_egress_props(self): - self.sync_opt_props() - return self._optimized_deny_egress_props + def deny_egress_props(self): + self.sync_props() + return self._deny_egress_props - def optimized_pass_egress_props(self): - self.sync_opt_props() - return self._optimized_pass_egress_props + def pass_egress_props(self): + self.sync_props() + return self._pass_egress_props - def sync_opt_props(self): + def sync_props(self): """ - Implemented by derived policies to compute optimized props of the policy according to the optimized props - of its rules, in case optimized props are not currently synchronized. + Implemented by derived policies to compute props of the policy according to the props + of its rules, in case props are not currently synchronized. """ return NotImplemented @@ -118,8 +117,8 @@ def __str__(self): def __eq__(self, other): if isinstance(self, type(other)): - self.sync_opt_props() - other.sync_opt_props() + self.sync_props() + other.sync_props() return \ self.name == other.name and \ self.namespace == other.namespace and \ @@ -128,12 +127,12 @@ def __eq__(self, other): self.selected_peers == other.selected_peers and \ self.ingress_rules == other.ingress_rules and \ self.egress_rules == other.egress_rules and \ - self._optimized_allow_ingress_props == other._optimized_allow_ingress_props and \ - self._optimized_deny_ingress_props == other._optimized_deny_ingress_props and \ - self._optimized_pass_ingress_props == other._optimized_pass_ingress_props and \ - self._optimized_allow_egress_props == other._optimized_allow_egress_props and \ - self._optimized_deny_egress_props == other._optimized_deny_egress_props and \ - self._optimized_pass_egress_props == other._optimized_pass_egress_props + self._allow_ingress_props == other._allow_ingress_props and \ + self._deny_ingress_props == other._deny_ingress_props and \ + self._pass_ingress_props == other._pass_ingress_props and \ + self._allow_egress_props == other._allow_egress_props and \ + self._deny_egress_props == other._deny_egress_props and \ + self._pass_egress_props == other._pass_egress_props return False def __lt__(self, other): # required so we can evaluate the policies according to their order @@ -187,37 +186,37 @@ def add_egress_rule(self, rule): """ self.egress_rules.append(rule) - def reorganize_opt_props_by_new_domains(self): + def reorganize_props_by_new_domains(self): """ This method is called to allow reduction of src_peers/dst_peers to inactive dimensions - in optimized properties of every rule. It is called when running in a context of a certain query + in properties of every rule. It is called when running in a context of a certain query and after updating the domain accordingly in DimensionsManager. - It also saves a copy of the optimized connectivity properties before reduction, to allow restoring to + It also saves a copy of the connectivity properties before reduction, to allow restoring to these values after the query's run. Note: there is an assumption that rules of all derived policies have - optimized_props and optimized_props_copy members + props and props_copy members """ for rule in self.ingress_rules + self.egress_rules: - if not rule.optimized_props_copy: + if not rule.props_copy: # to avoid calling with the same rule multiple times - rule.optimized_props_copy = rule.optimized_props.copy() - rule.optimized_props.reduce_active_dimensions() - self.optimized_props_in_sync = False + rule.props_copy = rule.props.copy() + rule.props.reduce_active_dimensions() + self.props_in_sync = False - def restore_opt_props(self): + def restore_props(self): """ - This method is called to restore optimized connectivity properties of every rule to their original values, + This method is called to restore connectivity properties of every rule to their original values, before the reduction of src_peers/dst_peers dimensions, s.t. the values of those dimensions will be with respect to the "full" default domain of these dimensions. Note: there is an assumption that rules of all derived policies have - optimized_props and optimized_props_copy members + props and props_copy members """ for rule in self.ingress_rules + self.egress_rules: - if rule.optimized_props_copy: + if rule.props_copy: # to avoid calling with the same rule multiple times - rule.optimized_props = rule.optimized_props_copy - rule.optimized_props_copy = ConnectivityProperties() - self.optimized_props_in_sync = False + rule.props = rule.props_copy + rule.props_copy = ConnectivityProperties() + self.props_in_sync = False @staticmethod def get_policy_type_from_dict(policy): # noqa: C901 @@ -316,28 +315,6 @@ def egress_rule_containing(self, other_policy, other_egress_rule_index): return self.rule_containing(other_policy, other_policy.egress_rules[other_egress_rule_index - 1], other_egress_rule_index, self.egress_rules) - def referenced_ip_blocks(self, exclude_ipv6=False): - """ - Returns ip blocks referenced by this policy, or empty PeerSet - :param bool exclude_ipv6: indicates if to exclude the automatically added IPv6 addresses in the referenced ip_blocks. - IPv6 addresses that are referenced in the policy by the user will always be included - :return: PeerSet of the referenced ip blocks - """ - return PeerSet() # default value, can be overridden in derived classes - - @staticmethod - def _include_ip_block(ip_block, exclude_ipv6): - """ - returns whether to include or not the ipblock in the policy's referenced_ip_blocks - :param IpBlock ip_block: the ip_block to check - :param bool exclude_ipv6 : indicates if to exclude ipv6 addresses - excluding the ip_block will be enabled only if the policy didn't reference any ipv6 addresses. - if policy referenced only ipv4 addresses ,then the parser didn't add auto ip_blocks, all will be included. - otherwise, if the policy didn't reference any ips, this mean automatic ip_block with all ips was added, - this is the ip_block to be excluded - so query results will not consider the ipv6 full range - """ - return ip_block.is_ipv4_block() or not exclude_ipv6 - def get_order(self): """ :return: the order of the policy @@ -351,12 +328,6 @@ def clone_without_rule(self, rule_to_exclude, ingress_rule): """ return NotImplemented - def allowed_connections(self, from_peer, to_peer, is_ingress): - """ - Implemented by derived classes to evaluate the set of connections this policy allows between two peers - """ - return NotImplemented - def policy_type_str(self): if self.policy_kind == NetworkPolicy.PolicyType.Ingress: return "Ingress resource" @@ -366,21 +337,9 @@ def policy_type_str(self): return "NetworkPolicy" -@dataclass -class PolicyConnections: - """ - A class to contain the effect of applying policies to a pair of peers - """ - captured: bool # Whether policy(ies) selectors captured relevant peers (can have empty allowed-conns with captured==True) - allowed_conns: ConnectionSet = ConnectionSet() # Connections allowed (and captured) by the policy(ies) - denied_conns: ConnectionSet = ConnectionSet() # Connections denied by the policy(ies) - pass_conns: ConnectionSet = ConnectionSet() # Connections specified as PASS by the policy(ies) - all_allowed_conns: ConnectionSet = ConnectionSet() # all (captured+ non-captured) Connections allowed by the policy(ies) - - -# TODO - making OptimizedPolicyConnections a dataclass does not work +# TODO - making PolicyConnections a dataclass does not work # (probably because PeerSet and ConnectivityProperties are mutable) -class OptimizedPolicyConnections: +class PolicyConnections: """ A class to contain the effect of applying policies to all src and dst peers It also serves as a filter for lazy evaluations of connections: diff --git a/nca/Utils/ExplTracker.py b/nca/Utils/ExplTracker.py index 24e9762bb..32cdeada9 100644 --- a/nca/Utils/ExplTracker.py +++ b/nca/Utils/ExplTracker.py @@ -239,8 +239,8 @@ def are_peers_connected(self, src, dst): def add_policy_to_peers(self, policy): for peer in policy.selected_peers: - src_peers, _ = self.extract_peers(policy.optimized_allow_ingress_props()) - _, dst_peers = self.extract_peers(policy.optimized_allow_egress_props()) + src_peers, _ = self.extract_peers(policy.allow_ingress_props()) + _, dst_peers = self.extract_peers(policy.allow_egress_props()) peer_name = peer.full_name() self.add_peer_policy(peer_name, policy.name, dst_peers, src_peers) diff --git a/tests/calico_testcases/example_policies/testcase15-ports/testcase15-scheme.yaml b/tests/calico_testcases/example_policies/testcase15-ports/testcase15-scheme.yaml index 3f605d7be..103b3ee19 100644 --- a/tests/calico_testcases/example_policies/testcase15-ports/testcase15-scheme.yaml +++ b/tests/calico_testcases/example_policies/testcase15-ports/testcase15-scheme.yaml @@ -15,7 +15,7 @@ networkConfigList: - name: named-ports networkPolicyList: - testcase15-named-ports.yaml - expectedWarnings: 12 + expectedWarnings: 0 - name: equiv-games1 networkPolicyList: diff --git a/tests/calico_testcases/example_policies/testcase15-ports/testcase15-with-ingress-scheme.yaml b/tests/calico_testcases/example_policies/testcase15-ports/testcase15-with-ingress-scheme.yaml index 70f516ee2..00a28ad8b 100644 --- a/tests/calico_testcases/example_policies/testcase15-ports/testcase15-with-ingress-scheme.yaml +++ b/tests/calico_testcases/example_policies/testcase15-ports/testcase15-with-ingress-scheme.yaml @@ -32,7 +32,7 @@ networkConfigList: - name: named-ports networkPolicyList: - testcase15-named-ports.yaml - expectedWarnings: 12 + expectedWarnings: 0 - name: equiv-games1 networkPolicyList: diff --git a/tests/fw_rules_tests/policies/semantic_diff_namedPorts-scheme.yaml b/tests/fw_rules_tests/policies/semantic_diff_namedPorts-scheme.yaml index df7ab8b85..99428a3b6 100644 --- a/tests/fw_rules_tests/policies/semantic_diff_namedPorts-scheme.yaml +++ b/tests/fw_rules_tests/policies/semantic_diff_namedPorts-scheme.yaml @@ -20,7 +20,7 @@ networkConfigList: - name: np5_named_ports networkPolicyList: - namedPorts-policy5.yaml - expectedWarnings: 5 + expectedWarnings: 0 queries: - name: semantic_diff_named_ports_np1_and_np2_by_deployments semanticDiff: diff --git a/tests/k8s_testcases/example_policies/namedPorts/namedPorts-scheme.yaml b/tests/k8s_testcases/example_policies/namedPorts/namedPorts-scheme.yaml index 237e34bce..49d48f819 100644 --- a/tests/k8s_testcases/example_policies/namedPorts/namedPorts-scheme.yaml +++ b/tests/k8s_testcases/example_policies/namedPorts/namedPorts-scheme.yaml @@ -25,7 +25,7 @@ networkConfigList: - name: np5 # just for warnings networkPolicyList: - namedPorts-policy5.yaml - expectedWarnings: 5 + expectedWarnings: 0 queries: - name: compare_np1_and_np2 diff --git a/tests/k8s_testcases/example_policies/tests-different-topologies/namedPorts-scheme.yaml b/tests/k8s_testcases/example_policies/tests-different-topologies/namedPorts-scheme.yaml index df3f2ee77..0f1b77ba8 100644 --- a/tests/k8s_testcases/example_policies/tests-different-topologies/namedPorts-scheme.yaml +++ b/tests/k8s_testcases/example_policies/tests-different-topologies/namedPorts-scheme.yaml @@ -25,7 +25,7 @@ networkConfigList: - name: np5 # just for warnings networkPolicyList: - namedPorts-policy5.yaml - expectedWarnings: 5 + expectedWarnings: 0 queries: - name: semantic_diff_named_ports_np1_and_np2