Skip to content

Commit 72689c2

Browse files
authored
improve connectivity output fot Istio: split by TCP and non-TCP conne… (#406)
* improve connectivity output for Istio: split by TCP and non-TCP connection maps + simplify connection-set output in dot format Signed-off-by: adisos <[email protected]>
1 parent a56ddd6 commit 72689c2

File tree

331 files changed

+1850
-650
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

331 files changed

+1850
-650
lines changed

nca/FWRules/ConnectivityGraph.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,20 @@ def _get_peer_name(self, peer):
6262
return peer.workload_name, False
6363
return str(peer), False
6464

65-
def get_connectivity_dot_format_str(self):
65+
def get_connectivity_dot_format_str(self, connectivity_restriction=None):
6666
"""
67+
:param Union[str,None] connectivity_restriction: specify if connectivity is restricted to
68+
TCP / non-TCP , or not
69+
:rtype str
6770
:return: a string with content of dot format for connectivity graph
6871
"""
69-
output_result = f'// The Connectivity Graph of {self.output_config.configName}\n'
72+
header_suffix = '' if connectivity_restriction is None else f', for {connectivity_restriction} connections'
73+
output_result = f'// The Connectivity Graph of {self.output_config.configName}{header_suffix}\n'
7074
output_result += 'digraph ' + '{\n'
7175
if self.output_config.queryName and self.output_config.configName:
72-
output_result += f'\tHEADER [shape="box" label=< <B>{self.output_config.queryName}/' \
73-
f'{self.output_config.configName}</B> > fontsize=30 color=webmaroon fontcolor=webmaroon];\n'
76+
header_label_str = f'{self.output_config.queryName}/{self.output_config.configName}{header_suffix}'
77+
output_result += f'\tHEADER [shape="box" label=< <B>{header_label_str}' \
78+
f'</B> > fontsize=30 color=webmaroon fontcolor=webmaroon];\n'
7479
peer_lines = set()
7580
for peer in self.cluster_info.all_peers:
7681
peer_name, is_ip_block = self._get_peer_name(peer)
@@ -87,7 +92,7 @@ def get_connectivity_dot_format_str(self):
8792
line += f'\"{src_peer_name}\"'
8893
line += ' -> '
8994
line += f'\"{dst_peer_name}\"'
90-
conn_str = str(connections).replace("Protocol:", "")
95+
conn_str = connections.get_simplified_connections_representation(True).replace("Protocol:", "")
9196
line += f' [label=\"{conn_str}\" color=\"gold2\" fontcolor=\"darkgreen\"]\n'
9297
edge_lines.add(line)
9398
output_result += ''.join(line for line in sorted(list(peer_lines))) + \

nca/FWRules/MinimizeFWRules.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -654,11 +654,14 @@ def __init__(self, fw_rules_map, cluster_info, output_config, results_map):
654654
self.output_config = output_config
655655
self.results_map = results_map
656656

657-
def get_fw_rules_in_required_format(self, add_txt_header=True, add_csv_header=True):
657+
def get_fw_rules_in_required_format(self, add_txt_header=True, add_csv_header=True, connectivity_restriction=None):
658658
"""
659659
:param add_txt_header: bool flag to indicate if header of fw-rules query should be added in txt format
660660
:param add_csv_header: bool flag to indicate if header csv should be added in csv format
661+
:param Union[str,None] connectivity_restriction: specify if connectivity is restricted to
662+
TCP / non-TCP , or not
661663
:return: a string or dict representing the computed minimized fw-rules (in a supported format txt/yaml/csv)
664+
:rtype: Union[str, dict]
662665
"""
663666
query_name = self.output_config.queryName
664667
if self.output_config.configName:
@@ -667,33 +670,39 @@ def get_fw_rules_in_required_format(self, add_txt_header=True, add_csv_header=Tr
667670
if output_format not in FWRule.supported_formats:
668671
print(f'error: unexpected outputFormat in output configuration value [should be txt/yaml/csv], '
669672
f'value is: {output_format}')
670-
return self.get_fw_rules_content(query_name, output_format, add_txt_header, add_csv_header)
673+
return self.get_fw_rules_content(query_name, output_format, add_txt_header, add_csv_header, connectivity_restriction)
671674

672-
def get_fw_rules_content(self, query_name, req_format, add_txt_header, add_csv_header):
675+
def get_fw_rules_content(self, query_name, req_format, add_txt_header, add_csv_header, connectivity_restriction):
673676
"""
674677
:param query_name: a string of the query name
675678
:param req_format: a string of the required format, should be in FWRule.supported_formats
676679
:param add_txt_header: bool flag to indicate if header of fw-rules query should be added in txt format
677680
:param add_csv_header: bool flag to indicate if header csv should be added in csv format
681+
:param Union[str,None] connectivity_restriction: specify if connectivity is restricted to
682+
TCP / non-TCP , or not
678683
:return: a dict of the fw-rules if the required format is json or yaml, else
679684
a string of the query name + fw-rules in the required format
680685
:rtype: Union[str, dict]
681686
"""
682687
rules_list = self._get_all_rules_list_in_req_format(req_format)
688+
key_prefix = '' if connectivity_restriction is None else f'{connectivity_restriction}_'
689+
header_prefix = ''
690+
if connectivity_restriction is not None:
691+
header_prefix = f'For connections of type {connectivity_restriction}, '
683692

684693
if req_format == 'txt':
685694
res = ''.join(line for line in sorted(rules_list))
686695
if add_txt_header:
687-
res = f'final fw rules for query: {query_name}:\n' + res
696+
res = f'{header_prefix}final fw rules for query: {query_name}:\n' + res
688697
return res
689698

690699
elif req_format in ['yaml', 'json']:
691-
return {'rules': rules_list}
700+
return {f'{key_prefix}rules': rules_list}
692701

693702
elif req_format in ['csv', 'md']:
694703
is_csv = req_format == 'csv'
695704
res = ''
696-
header_lines = [[query_name] + [''] * (len(FWRule.rule_csv_header) - 1)]
705+
header_lines = [[header_prefix + query_name] + [''] * (len(FWRule.rule_csv_header) - 1)]
697706
if add_csv_header:
698707
if is_csv:
699708
header_lines = [FWRule.rule_csv_header] + header_lines

nca/NetworkConfig/NetworkConfigQuery.py

Lines changed: 125 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -696,51 +696,135 @@ def exec(self):
696696
else:
697697
conns, _, _, _ = self.config.allowed_connections(peer1, peer2)
698698
if conns:
699-
# TODO: consider separate connectivity maps for config that involves istio -
700-
# one that handles non-TCP connections, and one for TCP
701-
# TODO: consider avoid "hiding" egress allowed connections, even though they are
702-
# not covered by authorization policies
703-
if self.config.policies_container.layers.does_contain_single_layer(NetworkLayerName.Istio) and \
704-
self.output_config.connectivityFilterIstioEdges:
705-
should_filter, modified_conns = self.filter_istio_edge(peer2, conns)
706-
if not should_filter:
707-
connections[modified_conns].append((peer1, peer2))
708-
# collect both peers, even if one of them is not in the subset
709-
peers.add(peer1)
710-
peers.add(peer2)
711-
else:
712-
connections[conns].append((peer1, peer2))
713-
# collect both peers, even if one of them is not in the subset
714-
peers.add(peer1)
715-
peers.add(peer2)
699+
connections[conns].append((peer1, peer2))
700+
# collect both peers, even if one of them is not in the subset
701+
peers.add(peer1)
702+
peers.add(peer2)
703+
704+
# if Istio is a layer in the network config - produce 2 maps, for TCP and for non-TCP
705+
# because Istio policies can only capture TCP connectivity
706+
if self.config.policies_container.layers.does_contain_layer(NetworkLayerName.Istio):
707+
output_res = self.get_connectivity_output_split_by_tcp(connections, peers, peers_to_compare)
708+
else:
709+
output_res = self.get_connectivity_output_full(connections, peers, peers_to_compare)
716710

717711
res = QueryAnswer(True)
718-
if self.output_config.outputFormat == 'dot':
719-
conn_graph = ConnectivityGraph(peers, self.config.get_allowed_labels(), self.output_config)
720-
conn_graph.add_edges(connections)
721-
res.output_explanation = [ComputedExplanation(str_explanation=conn_graph.get_connectivity_dot_format_str())]
712+
if self.output_config.outputFormat in ['json', 'yaml']:
713+
res.output_explanation = [ComputedExplanation(dict_explanation=output_res)]
722714
else:
723-
conn_graph = ConnectivityGraph(peers_to_compare, self.config.get_allowed_labels(), self.output_config)
724-
conn_graph.add_edges(connections)
725-
fw_rules = conn_graph.get_minimized_firewall_rules()
726-
formatted_rules = fw_rules.get_fw_rules_in_required_format()
727-
if self.output_config.outputFormat in ['json', 'yaml']:
728-
res.output_explanation = [ComputedExplanation(dict_explanation=formatted_rules)]
729-
else:
730-
res.output_explanation = [ComputedExplanation(str_explanation=formatted_rules)]
715+
res.output_explanation = [ComputedExplanation(str_explanation=output_res)]
731716
return res
732717

718+
def get_connectivity_output_full(self, connections, peers, peers_to_compare):
719+
"""
720+
get the connectivity map output considering all connections in the output
721+
:param dict connections: the connections' dict (map from connection-set to peer pairs)
722+
:param PeerSet peers: the peers to consider for dot output
723+
:param PeerSet peers_to_compare: the peers to consider for fw-rules output
724+
:rtype Union[str,dict]
725+
"""
726+
if self.output_config.outputFormat == 'dot':
727+
dot_full = self.dot_format_from_connections_dict(connections, peers)
728+
return dot_full
729+
# handle formats other than dot
730+
formatted_rules = self.fw_rules_from_connections_dict(connections, peers_to_compare)
731+
return formatted_rules
732+
733+
def get_connectivity_output_split_by_tcp(self, connections, peers, peers_to_compare):
734+
"""
735+
get the connectivity map output as two parts: TCP and non-TCP
736+
:param dict connections: the connections' dict (map from connection-set to peer pairs)
737+
:param PeerSet peers: the peers to consider for dot output
738+
:param PeerSet peers_to_compare: the peers to consider for fw-rules output
739+
:rtype Union[str,dict]
740+
"""
741+
connectivity_tcp_str = 'TCP'
742+
connectivity_non_tcp_str = 'non-TCP'
743+
connections_tcp, connections_non_tcp = self.convert_connections_to_split_by_tcp(connections)
744+
if self.output_config.outputFormat == 'dot':
745+
dot_tcp = self.dot_format_from_connections_dict(connections_tcp, peers, connectivity_tcp_str)
746+
dot_non_tcp = self.dot_format_from_connections_dict(connections_non_tcp, peers, connectivity_non_tcp_str)
747+
# concatenate the two graphs into one dot file
748+
res_str = dot_tcp + dot_non_tcp
749+
return res_str
750+
# handle formats other than dot
751+
formatted_rules_tcp = self.fw_rules_from_connections_dict(connections_tcp, peers_to_compare,
752+
connectivity_tcp_str)
753+
formatted_rules_non_tcp = self.fw_rules_from_connections_dict(connections_non_tcp, peers_to_compare,
754+
connectivity_non_tcp_str)
755+
if self.output_config.outputFormat in ['json', 'yaml']:
756+
# get a dict object containing the two maps on different keys (TCP_rules and non-TCP_rules)
757+
rules = formatted_rules_tcp
758+
rules.update(formatted_rules_non_tcp)
759+
return rules
760+
# remaining formats: txt / csv / md : concatenate the two strings of the conn-maps
761+
if self.output_config.outputFormat == 'txt':
762+
res_str = f'{formatted_rules_tcp}\n{formatted_rules_non_tcp}'
763+
else:
764+
res_str = formatted_rules_tcp + formatted_rules_non_tcp
765+
return res_str
766+
767+
def dot_format_from_connections_dict(self, connections, peers, connectivity_restriction=None):
768+
"""
769+
:param dict connections: the connections' dict (map from connection-set to peer pairs)
770+
:param PeerSet peers: the peers to consider for dot output
771+
:param Union[str,None] connectivity_restriction: specify if connectivity is restricted to
772+
TCP / non-TCP , or not
773+
:rtype str
774+
:return the connectivity map in dot-format, considering connectivity_restriction if required
775+
"""
776+
conn_graph = ConnectivityGraph(peers, self.config.get_allowed_labels(), self.output_config)
777+
conn_graph.add_edges(connections)
778+
return conn_graph.get_connectivity_dot_format_str(connectivity_restriction)
779+
780+
def fw_rules_from_connections_dict(self, connections, peers_to_compare, connectivity_restriction=None):
781+
"""
782+
:param dict connections: the connections' dict (map from connection-set to peer pairs)
783+
:param PeerSet peers_to_compare: the peers to consider for fw-rules output
784+
:param Union[str,None] connectivity_restriction: specify if connectivity is restricted to
785+
TCP / non-TCP , or not
786+
:return the connectivity map in fw-rules, considering connectivity_restriction if required
787+
:rtype: Union[str, dict]
788+
"""
789+
conn_graph = ConnectivityGraph(peers_to_compare, self.config.get_allowed_labels(), self.output_config)
790+
conn_graph.add_edges(connections)
791+
fw_rules = conn_graph.get_minimized_firewall_rules()
792+
formatted_rules = fw_rules.get_fw_rules_in_required_format(connectivity_restriction=connectivity_restriction)
793+
return formatted_rules
794+
795+
def convert_connections_to_split_by_tcp(self, connections):
796+
"""
797+
given the connections' dict , convert it to two connection maps, one for TCP only, and the other
798+
for non-TCP only.
799+
:param dict connections: the connections' dict (map from connection-set to peer pairs)
800+
:return: a tuple of the two connection maps : first for TCP, second for non-TCP
801+
:rtype: tuple(dict, dict)
802+
"""
803+
connections_tcp = defaultdict(list)
804+
connections_non_tcp = defaultdict(list)
805+
for conn, peers_list in connections.items():
806+
tcp_conns, non_tcp_conns = self.split_to_tcp_and_non_tcp_conns(conn)
807+
connections_tcp[tcp_conns] += peers_list
808+
connections_non_tcp[non_tcp_conns] += peers_list
809+
810+
return connections_tcp, connections_non_tcp
811+
733812
@staticmethod
734-
def filter_istio_edge(peer2, conns):
735-
# currently only supporting authorization policies, that do not capture egress rules
736-
if isinstance(peer2, IpBlock):
737-
return True, None
738-
# remove allowed connections for non TCP protocols
739-
# https://istio.io/latest/docs/ops/configuration/traffic-management/protocol-selection/
740-
# Non-TCP based protocols, such as UDP, are not proxied. These protocols will continue to function as normal,
741-
# without any interception by the Istio proxy
742-
conns_new = conns - ConnectionSet.get_non_tcp_connections()
743-
return False, conns_new
813+
def split_to_tcp_and_non_tcp_conns(conns):
814+
"""
815+
split a ConnectionSet object to two objects: one within TCP only, the other within non-TCP protocols
816+
:param ConnectionSet conns: a ConnectionSet object
817+
:return: a tuple of the two ConnectionSet objects: first for TCP, second for non-TCP
818+
:rtype: tuple(ConnectionSet, ConnectionSet)
819+
"""
820+
tcp_conns = conns - ConnectionSet.get_non_tcp_connections()
821+
non_tcp_conns = conns - tcp_conns
822+
if non_tcp_conns == ConnectionSet.get_non_tcp_connections():
823+
non_tcp_conns = ConnectionSet(True) # all connections in terms of non-TCP
824+
if tcp_conns == ConnectionSet.get_all_tcp_connections():
825+
tcp_conns = ConnectionSet(True) # all connections in terms of TCP
826+
827+
return tcp_conns, non_tcp_conns
744828

745829

746830
class TwoNetworkConfigsQuery(BaseNetworkQuery):
@@ -853,7 +937,8 @@ def exec(self, cmd_line_flag=False, layer_name=None):
853937
if different_conns_list:
854938
return self._query_answer_with_relevant_explanation(sorted(different_conns_list))
855939

856-
return QueryAnswer(True, self.name1 + ' and ' + self.name2 + ' are semantically equivalent.', numerical_result=0)
940+
return QueryAnswer(True, self.name1 + ' and ' + self.name2 + ' are semantically equivalent.',
941+
numerical_result=0)
857942

858943
def _query_answer_with_relevant_explanation(self, explanation_list):
859944
output_result = self.name1 + ' and ' + self.name2 + ' are not semantically equivalent.'

nca/NetworkConfig/NetworkLayer.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ def does_contain_single_layer(self, layer_name):
8181
"""
8282
return len(self) == 1 and list(self.keys())[0] == layer_name
8383

84+
def does_contain_layer(self, layer_name):
85+
"""
86+
Checks if the given layer is in the map.
87+
:param NetworkLayerName layer_name: the layer to check
88+
:return: True if the layer is in the map
89+
"""
90+
return layer_name in self
91+
8492
@staticmethod
8593
def empty_layer_allowed_connections(layer_name, from_peer, to_peer):
8694
"""

tests/calico_testcases/expected_output/disjointness-various-policies-full-explanation.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ There are policies capturing the same pods in np_various_policies
22
policies with overlapping captured pods:
33
NetworkPolicy_1: kube-system/testcase1-equiv-networkpolicy, NetworkPolicy_2: kube-system/testcase1-nonequiv-networkpolicy, pods: kube-system/calico-node-mgdlr, kube-system/file-plugin-7bfb8b69bf-p86gk, kube-system/keepalived-watcher-57ghx, kube-system/keepalived-watcher-gzdfm, kube-system/keepalived-watcher-wczq8, kube-system/kube-fluentd-h6rjg, kube-system/storage-watcher-8494b4b8bb-f8csd, kube-system/tiller-deploy-5c45c9966b-nqwz6, kube-system/vpn-858f6d9777-2bw5m
44
NetworkPolicy_1: kube-system/testcase1-equiv-networkpolicy, NetworkPolicy_2: testcase1-global-networkpolicy, pods: kube-system/calico-node-mgdlr, kube-system/file-plugin-7bfb8b69bf-p86gk, kube-system/keepalived-watcher-57ghx, kube-system/keepalived-watcher-gzdfm, kube-system/keepalived-watcher-wczq8, kube-system/kube-fluentd-h6rjg, kube-system/storage-watcher-8494b4b8bb-f8csd, kube-system/tiller-deploy-5c45c9966b-nqwz6, kube-system/vpn-858f6d9777-2bw5m
5-
NetworkPolicy_1: kube-system/testcase1-nonequiv-networkpolicy, NetworkPolicy_2: testcase1-global-networkpolicy, pods: kube-system/calico-node-mgdlr, kube-system/file-plugin-7bfb8b69bf-p86gk, kube-system/keepalived-watcher-57ghx, kube-system/keepalived-watcher-gzdfm, kube-system/keepalived-watcher-wczq8, kube-system/kube-fluentd-h6rjg, kube-system/storage-watcher-8494b4b8bb-f8csd, kube-system/tiller-deploy-5c45c9966b-nqwz6, kube-system/vpn-858f6d9777-2bw5m
5+
NetworkPolicy_1: kube-system/testcase1-nonequiv-networkpolicy, NetworkPolicy_2: testcase1-global-networkpolicy, pods: kube-system/calico-node-mgdlr, kube-system/file-plugin-7bfb8b69bf-p86gk, kube-system/keepalived-watcher-57ghx, kube-system/keepalived-watcher-gzdfm, kube-system/keepalived-watcher-wczq8, kube-system/kube-fluentd-h6rjg, kube-system/storage-watcher-8494b4b8bb-f8csd, kube-system/tiller-deploy-5c45c9966b-nqwz6, kube-system/vpn-858f6d9777-2bw5m

tests/calico_testcases/expected_output/disjointness-various-policies-full-explanation.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,4 @@
4444
- kube-system/kube-fluentd-h6rjg
4545
- kube-system/storage-watcher-8494b4b8bb-f8csd
4646
- kube-system/tiller-deploy-5c45c9966b-nqwz6
47-
- kube-system/vpn-858f6d9777-2bw5m
47+
- kube-system/vpn-858f6d9777-2bw5m

tests/calico_testcases/expected_output/equiv-all-range1.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,4 +188,4 @@
188188
"numerical_result": 0,
189189
"textual_result": "equiv-ranges-writing-games/kube-system/testcase16-nets-all-range-partitioned-4-net-notNets and equiv-ranges-writing-games/kube-system/testcase16-all-range-with-nets-notNets-single-ips are semantically equivalent."
190190
}
191-
]
191+
]

0 commit comments

Comments
 (0)