Skip to content

Commit

Permalink
cases to exclude ipv6 addresses from connectivity reports (#412)
Browse files Browse the repository at this point in the history
  • Loading branch information
shireenf-ibm authored Jan 30, 2023
1 parent bd87e4a commit 32ad6fd
Show file tree
Hide file tree
Showing 237 changed files with 253 additions and 2,531 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ The arguments to `--resource_list` and to `--base_resource_list` should be one o
- `--output_endpoints`\
Choose endpoints type in output (pods/deployments).\
*default:* deployments
- `--print_ipv6`\
include IPv6 range in the query results even when the policies of the config do not contain any IPv6 addresses.


For more information on command-line switches combinations, see [Common Query Patterns](docs/CommonQueryPatterns.md#cmdline-queries)

Expand Down
16 changes: 9 additions & 7 deletions docs/SchemeFileFormat.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,15 @@ For example: `my_set/prod_ns/deny_all_policy`. If there are multiple policies na
#### <a name="outputconfig"></a>Output Configuration object
The supported entries in the outputConfiguration object are as follows:

| Field | Description | Value |
|-----------------|--------------------------------------------------------------------------------|----------------------------------------|
| outputFormat | Output format specification. | string [ txt / yaml / csv / md / dot ] |
| outputPath | A file path to redirect output into. | string |
| outputEndpoints | Choose endpoints type in output. | string [ pods / deployments ] |
| subset | A dict object with the defined subset elements to display in the output | [subset](#subset) object |
| fullExplanation | Choose if to print all counterexamples causing the query result in the output | bool |
| Field | Description | Value |
|------------------|-----------------------------------------------------------------------------------------------------------------|----------------------------------------|
| outputFormat | Output format specification. | string [ txt / yaml / csv / md / dot ] |
| outputPath | A file path to redirect output into. | string |
| outputEndpoints | Choose endpoints type in output. | string [ pods / deployments ] |
| subset | A dict object with the defined subset elements to display in the output | [subset](#subset) object |
| fullExplanation | Choose if to print all counterexamples causing the query result in the output | bool |
| excludeIPv6Range | If the policies of the config do not contain any IPv6 addresses, do not include IPv6 range in the query results | bool [default: True] |


#### <a name="subset"></a>Subset object
The supported entries in the subset object are as follows:
Expand Down
34 changes: 26 additions & 8 deletions nca/CoreDS/Peer.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,23 +327,26 @@ def get_ip_range_or_cidr_str(self):
return self.get_cidr_list_str()

@staticmethod
def get_all_ips_block():
def get_all_ips_block(exclude_ipv6=False):
"""
:return: The full range of ipv4 and ipv6 addresses
:return: The full range of ipv4 and ipv6 addresses if exclude_ipv6 is False
:param bool exclude_ipv6: indicates if to exclude the IPv6 addresses
:rtype: IpBlock
"""
res = IpBlock('0.0.0.0/0')
res.add_cidr('::/0')
if not exclude_ipv6:
res.add_cidr('::/0')
return res

@staticmethod
def get_all_ips_block_peer_set():
def get_all_ips_block_peer_set(exclude_ipv6=False):
"""
:return: The full range of ipv4 and ipv6 addresses
:return: The full range of ipv4 and ipv6 addresses (ipv6 if exclude_ipv6 is False)
:param bool exclude_ipv6: indicates if to exclude the IPv6 addresses
:rtype: PeerSet
"""
res = PeerSet()
res.add(IpBlock.get_all_ips_block())
res.add(IpBlock.get_all_ips_block(exclude_ipv6))
return res

def split(self):
Expand Down Expand Up @@ -408,7 +411,7 @@ def _add_interval_to_list(interval, non_overlapping_interval_list):
non_overlapping_interval_list += to_add

@staticmethod
def disjoint_ip_blocks(ip_blocks1, ip_blocks2):
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:
Expand All @@ -417,6 +420,7 @@ def disjoint_ip_blocks(ip_blocks1, ip_blocks2):
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
"""
Expand All @@ -435,10 +439,24 @@ def disjoint_ip_blocks(ip_blocks1, ip_blocks2):
res.add(ip_block)

if not res:
res.add(IpBlock.get_all_ips_block())
res.add(IpBlock.get_all_ips_block(exclude_ipv6))

return res

def is_ipv4_block(self):
"""
checks whether self IpBlock includes only IPv4 addresses
:return: true if self includes only IPv4 addresses
:rtype: bool
"""
cnt = 0
for interval in self.interval_set:
ip_address = interval.start
if isinstance(ip_address, IPNetworkAddress) and isinstance(ip_address.address, ipaddress.IPv4Address) or \
isinstance(ip_address, ipaddress.IPv4Address):
cnt += 1
return cnt == len(self.interval_set)


class PeerSet(set):
"""
Expand Down
23 changes: 21 additions & 2 deletions nca/NetworkConfig/NetworkConfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,17 +172,36 @@ def get_affected_pods(self, is_ingress, layer_name):

return affected_pods

def get_referenced_ip_blocks(self):
def _check_for_excluding_ipv6_addresses(self, exclude_ipv6):
"""
checks and returns if to exclude non-referenced IPv6 addresses from the config
Excluding the IPv6 addresses will be enabled if the exclude_ipv6 param is True and
IPv6 addresses in all the policies of the config (if existed) were added automatically by the parser
and not referenced by user
:param bool exclude_ipv6: indicates if to exclude ipv_6 non-referenced addresses
:rtype bool
"""
if not exclude_ipv6:
return False

for policy in self.policies_container.policies.values():
if policy.has_ipv6_addresses: # if at least one policy has referenced ipv6 addresses, ipv6 will be included
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()
self.referenced_ip_blocks |= policy.referenced_ip_blocks(exclude_non_ref_ipv6_from_policies)

return self.referenced_ip_blocks

Expand Down
24 changes: 15 additions & 9 deletions nca/NetworkConfig/NetworkConfigQuery.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,8 +676,10 @@ def exec(self):
self.config.name
peers_to_compare = self.config.peer_container.get_all_peers_group()

ref_ip_blocks = IpBlock.disjoint_ip_blocks(self.config.get_referenced_ip_blocks(),
IpBlock.get_all_ips_block_peer_set())
exclude_ipv6 = self.output_config.excludeIPv6Range
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)
connections = defaultdict(list)
peers = PeerSet()
peers_to_compare |= ref_ip_blocks
Expand Down Expand Up @@ -876,8 +878,9 @@ def disjoint_referenced_ip_blocks(self):
:return: A set of disjoint ip-blocks
:rtype: PeerSet
"""
return IpBlock.disjoint_ip_blocks(self.config1.get_referenced_ip_blocks(),
self.config2.get_referenced_ip_blocks())
exclude_ipv6 = self.output_config.excludeIPv6Range
return IpBlock.disjoint_ip_blocks(self.config1.get_referenced_ip_blocks(exclude_ipv6),
self.config2.get_referenced_ip_blocks(exclude_ipv6), exclude_ipv6)

@staticmethod
def clone_without_ingress(config):
Expand Down Expand Up @@ -1100,10 +1103,13 @@ def compute_diff(self): # noqa: C901
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
old_ip_blocks = IpBlock.disjoint_ip_blocks(self.config1.get_referenced_ip_blocks(),
IpBlock.get_all_ips_block_peer_set())
new_ip_blocks = IpBlock.disjoint_ip_blocks(self.config2.get_referenced_ip_blocks(),
IpBlock.get_all_ips_block_peer_set())
exclude_ipv6 = 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()
Expand Down Expand Up @@ -1164,7 +1170,7 @@ def compute_diff(self): # noqa: C901

# 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)
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)
Expand Down
4 changes: 4 additions & 0 deletions nca/Parsers/CalicoPolicyYamlParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,9 @@ def _get_rule_peers(self, entity_rule):
elif nets or not_nets:
rule_peers = PeerSet()
rule_peers.add(rule_ips)
if not self.has_ipv6_addresses: # if already true, means a previous rule already had ipv6
# and then policy has ipv6 no need for more checks
self.check_and_update_has_ipv6_addresses(rule_peers)
else:
rule_peers = self.peer_container.get_all_peers_group(True)

Expand Down Expand Up @@ -641,4 +644,5 @@ def parse_policy(self):
self._apply_extra_labels(policy_spec, is_profile, res_policy.name)
res_policy.findings = self.warning_msgs
res_policy.referenced_labels = self.referenced_labels
res_policy.has_ipv6_addresses = self.has_ipv6_addresses
return res_policy
14 changes: 14 additions & 0 deletions nca/Parsers/GenericYamlParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from nca.CoreDS.ConnectionSet import ConnectionSet
from nca.CoreDS.PortSet import PortSet
from nca.Utils.NcaLogger import NcaLogger
from nca.CoreDS.Peer import IpBlock


class GenericYamlParser:
Expand All @@ -35,6 +36,7 @@ def __init__(self, yaml_file_name=''):
"""
self.yaml_file_name = yaml_file_name
self.warning_msgs = [] # Collect all warning messages during parsing here
self.has_ipv6_addresses = False

def set_file_name(self, yaml_file_name):
"""
Expand Down Expand Up @@ -239,3 +241,15 @@ def _get_connection_set_from_properties(dest_ports, method_set=MethodSet(True),
res = ConnectionSet()
res.add_connections('TCP', tcp_properties)
return res

def check_and_update_has_ipv6_addresses(self, peers):
"""
checks if the peer list has ipv6 addresses
updates self.has_ipv6_addresses=true if at least on peer is an IPblock with IPv6 addresses
:param PeerSet peers: list of peers
"""
for peer in peers:
if isinstance(peer, IpBlock):
if not peer.is_ipv4_block():
self.has_ipv6_addresses = True
return # if at least one peer is ipv6 block , this policy has_ipv6, no need to continue
13 changes: 8 additions & 5 deletions nca/Parsers/IstioPolicyYamlParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,23 +157,26 @@ def parse_namespaces(self, ns_list, not_ns_list):
res -= self._parse_ns_str(ns)
return res

@staticmethod
def parse_ip_block(ips_list, not_ips_list):
def parse_ip_block(self, ips_list, not_ips_list):
"""
parse ipBlocks elements (within a source component of a rule)
:param list[str] ips_list: list of ip-block addresses (either ip address or ip-block cidr)
:param list[str] not_ips_list: negative list of ip-block addresses (either ip address or ip-block cidr)
:return: A PeerSet containing the relevant IpBlocks
:rtype: Peer.PeerSet
"""
ips_list = ['0.0.0.0/0', '::/0'] if ips_list is None else ips_list # If not set, any IP is allowed
ips_list = IpBlock.get_all_ips_block() if ips_list is None else ips_list # If not set, any IP is allowed
not_ips_list = [] if not_ips_list is None else not_ips_list
res_ip_block = IpBlock()
for cidr in ips_list:
res_ip_block |= IpBlock(cidr)
for cidr in not_ips_list:
res_ip_block -= IpBlock(cidr)
return res_ip_block.split()
res_peer_set = res_ip_block.split()
if not self.has_ipv6_addresses: # if already true, means a previous rule already had ipv6
# and then policy has ipv6 no need for more checks
self.check_and_update_has_ipv6_addresses(res_peer_set)
return res_peer_set

def parse_key_values(self, key, values, not_values):
"""
Expand Down Expand Up @@ -558,5 +561,5 @@ def parse_policy(self):

res_policy.findings = self.warning_msgs
res_policy.referenced_labels = self.referenced_labels

res_policy.has_ipv6_addresses = self.has_ipv6_addresses
return res_policy
5 changes: 5 additions & 0 deletions nca/Parsers/K8sPolicyYamlParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,10 @@ def parse_ip_block(self, block):
self.syntax_error(str(e.args), block)
except TypeError as e:
self.syntax_error(str(e.args), block)

if not self.has_ipv6_addresses: # if already true, means a previous peer already had ipv6
# and then policy has ipv6 no need for more checks
self.check_and_update_has_ipv6_addresses(res)
return res

def parse_peer(self, peer):
Expand Down Expand Up @@ -437,4 +441,5 @@ def parse_policy(self):

res_policy.findings = self.warning_msgs
res_policy.referenced_labels = self.referenced_labels
res_policy.has_ipv6_addresses = self.has_ipv6_addresses
return res_policy
8 changes: 5 additions & 3 deletions nca/Resources/CalicoNetworkPolicy.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,19 +140,21 @@ def clone_without_rule(self, rule_to_exclude, ingress_rule):
res.add_ingress_rule(rule)
return res

def referenced_ip_blocks(self):
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):
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):
if isinstance(peer, Peer.IpBlock) and self._include_ip_block(peer, exclude_ipv6):
res |= peer.split()

return res
Expand Down
6 changes: 4 additions & 2 deletions nca/Resources/IstioNetworkPolicy.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,17 @@ def allowed_connections(self, from_peer, to_peer, is_ingress):

return PolicyConnections(True, allowed_conns, denied_conns)

def referenced_ip_blocks(self):
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):
if isinstance(peer, IpBlock) and self._include_ip_block(peer, exclude_ipv6):
res |= peer.split()
return res

Expand Down
16 changes: 9 additions & 7 deletions nca/Resources/K8sNetworkPolicy.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,20 +83,22 @@ def clone_without_rule(self, rule_to_exclude, ingress_rule):
res.add_ingress_rule(rule)
return res

def referenced_ip_blocks(self):
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 pod in rule.peer_set:
if isinstance(pod, Peer.IpBlock):
res |= pod.split()
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 pod in rule.peer_set:
if isinstance(pod, Peer.IpBlock):
res |= pod.split()
for peer in rule.peer_set:
if isinstance(peer, Peer.IpBlock) and self._include_ip_block(peer, exclude_ipv6):
res |= peer.split()

return res

Expand Down
Loading

0 comments on commit 32ad6fd

Please sign in to comment.