def test_calico_flow_1(self): """ dest ports containing only positive named ports """ src_res_ports = PortSet() dst_res_ports = PortSet() src_res_ports.add_port_range(1, 100) dst_res_ports.add_port("x") dst_res_ports.add_port("y") dst_res_ports.add_port("z") dst_res_ports.add_port("w") tcp_properties = TcpLikeProperties(src_res_ports, dst_res_ports) tcp_properties_2 = tcp_properties.copy() self.assertTrue(tcp_properties.has_named_ports()) self.assertEqual(tcp_properties.get_named_ports(), {"x", "y", "z", "w"}) named_ports_dict = {"x": (15, 6), "z": (20, 6), "y": (200, 17)} tcp_properties.convert_named_ports(named_ports_dict, 6) #print(tcp_properties) expected_res_cubes = {(CanonicalIntervalSet.get_interval_set(1, 100), CanonicalIntervalSet.get_interval_set(15, 15) | CanonicalIntervalSet.get_interval_set(20, 20)) } self.assertEqual(expected_res_cubes, tcp_properties._get_cubes_set()) self.assertTrue(tcp_properties_2.has_named_ports()) self.assertEqual(tcp_properties_2.get_named_ports(), {"x", "y", "z", "w"}) tcp_properties_2.convert_named_ports(named_ports_dict, 17) #print(tcp_properties_2) expected_res_cubes = {(CanonicalIntervalSet.get_interval_set(1, 100), CanonicalIntervalSet.get_interval_set(200, 200))} self.assertEqual(expected_res_cubes, tcp_properties_2._get_cubes_set())
def _get_rule_ports(self, entity_rule, protocol_supports_ports): """ Parse the port-related parts of the source/destination parts of a rule :param dict entity_rule: The object to parse :param bool protocol_supports_ports: Whether ports are allowed for the rule's protocol :return: The ports that are specified by ports/notPorts :rtype: PortSet """ ports_array = entity_rule.get('ports') if ports_array is not None: if not protocol_supports_ports: self.syntax_error( 'A rule specifying ports must specify a protocol supporting ports', ports_array) rule_ports = PortSet() for port in ports_array: rule_ports |= self._parse_port(port, ports_array) else: rule_ports = PortSet(True) not_ports_array = entity_rule.get('notPorts') if not_ports_array is not None: if not protocol_supports_ports: self.syntax_error( 'A rule specifying notPorts must specify a protocol supporting ports', ports_array) for port in not_ports_array: rule_ports -= self._parse_port(port, not_ports_array) return rule_ports
def _parse_port(self, port, array): """ Parse a single port in the array defined in the ports/notPorts part of an EntityRule :param port: The port to parse (might be an int, a range of ints or a named port) :param list array: The object containing the port (for reporting errors) :return: The set of ports defined by port :rtype: PortSet """ res_port_set = PortSet() if isinstance(port, int): self.validate_value_in_domain(port, 'dst_ports', array, 'Port number') res_port_set.add_port(port) elif isinstance(port, str): if port.count(':') == 1: port_range = port.split(':') try: left_port = int(port_range[0]) right_port = int(port_range[1]) self.validate_value_in_domain(left_port, 'dst_ports', array, 'Port number') self.validate_value_in_domain(right_port, 'dst_ports', array, 'Port number') if right_port < left_port: self.syntax_error('Invalid port range: ' + port, array) res_port_set.add_port_range(left_port, right_port) except ValueError: res_port_set.add_port(port) else: res_port_set.add_port(port) return res_port_set
def __init__(self, source_ports=PortSet(), dest_ports=PortSet(), methods=MethodSet(True), paths=None, hosts=None): """ This will create all cubes made of the input arguments ranges/regex values. :param PortSet source_ports: The set of source ports (as a set of intervals/ranges) :param PortSet dest_ports: The set of target ports (as a set of intervals/ranges) :param MethodSet methods: the set of http request methods :param MinDFA paths: The dfa of http request paths :param MinDFA hosts: The dfa of http request hosts """ super().__init__(TcpLikeProperties.dimensions_list) self.named_ports = { } # a mapping from dst named port (String) to src ports interval set self.excluded_named_ports = { } # a mapping from dst named port (String) to src ports interval set # create the cube from input arguments cube = [] active_dims = [] if not source_ports.is_all(): cube.append(source_ports.port_set) active_dims.append("src_ports") if not dest_ports.is_all(): cube.append(dest_ports.port_set) active_dims.append("dst_ports") if not methods.is_whole_range(): cube.append(methods) active_dims.append("methods") if paths is not None: cube.append(paths) active_dims.append("paths") if hosts is not None: cube.append(hosts) active_dims.append("hosts") if not active_dims: self.set_all() else: has_empty_dim_value = False for dim_val in cube: if not dim_val: has_empty_dim_value = True break if not has_empty_dim_value: self.add_cube(cube, active_dims) # assuming named ports are only in dest, not src all_ports = PortSet.all_ports_interval.copy() for port_name in dest_ports.named_ports: self.named_ports[port_name] = source_ports.port_set for port_name in dest_ports.excluded_named_ports: # self.excluded_named_ports[port_name] = all_ports - source_ports.port_set self.excluded_named_ports[port_name] = all_ports
def _add_all_connections_of_protocol(self, protocol): """ Add all possible connections to the connection set for a given protocol :param protocol: the given protocol number :return: None """ if self.protocol_supports_ports(protocol): self.allowed_protocols[protocol] = TcpLikeProperties( PortSet(True), PortSet(True)) elif self.protocol_is_icmp(protocol): self.allowed_protocols[protocol] = ICMPDataSet(add_all=True) else: self.allowed_protocols[protocol] = True
def _parse_port(self, port, array): """ Parse a single port in the array defined in the ports/notPorts part :param Union[int,str] port: The port to parse :param list array: The object containing the port (for reporting errors) :return: The set of ports defined by port :rtype: PortSet """ res_port_set = PortSet() try: port_num = port if isinstance(port, int) else int(port) except ValueError: self.syntax_error('error parsing port ', port) return None self.validate_value_in_domain(port_num, 'dst_ports', array, 'Port number') res_port_set.add_port(port_num) return res_port_set
def add_all_connections(self, excluded_protocols=None): """ Add all possible connections to the connection set :param list[int] excluded_protocols: (optional) list of protocol numbers to exclude :return: None """ for protocol in range(ConnectionSet._min_protocol_num, ConnectionSet._max_protocol_num + 1): if excluded_protocols and protocol in excluded_protocols: continue if self.protocol_supports_ports(protocol): self.allowed_protocols[protocol] = TcpLikeProperties( PortSet(True), PortSet(True)) elif self.protocol_is_icmp(protocol): self.allowed_protocols[protocol] = ICMPDataSet(add_all=True) else: self.allowed_protocols[protocol] = True
def _make_rules_from_conns(self, tcp_conns): """ Make IngressPolicyRules from the given connections :param TcpLikeProperties tcp_conns: the given connections :return: the list of IngressPolicyRules """ peers_to_conns = dict() res = [] # extract peers dimension from cubes for cube in tcp_conns: ports = None paths = None hosts = None peer_set = None for i, dim in enumerate(tcp_conns.active_dimensions): if dim == "dst_ports": ports = cube[i] elif dim == "paths": paths = cube[i] elif dim == "hosts": hosts = cube[i] elif dim == "peers": peer_set = PeerSet( set( tcp_conns.base_peer_set.get_peer_list_by_indices( cube[i]))) else: assert False if not peer_set: peer_set = self.peer_container.peer_set.copy() port_set = PortSet() port_set.port_set = ports port_set.named_ports = tcp_conns.named_ports port_set.excluded_named_ports = tcp_conns.excluded_named_ports new_conns = self._get_connection_set_from_properties( port_set, paths_dfa=paths, hosts_dfa=hosts) if peers_to_conns.get(peer_set): peers_to_conns[ peer_set] |= new_conns # optimize conns for the same peers else: peers_to_conns[peer_set] = new_conns for peer_set, conns in peers_to_conns.items(): res.append(IngressPolicyRule(peer_set, conns)) return res
def get_rule_ports(self, ports_array, not_ports_array): """ Parse the port-related parts :param list ports_array: a positive ports list :param list not_ports_array: a negative ports list :return: The ports that are specified by ports/notPorts :rtype: PortSet """ if ports_array is not None: rule_ports = PortSet() for port in ports_array: rule_ports |= self._parse_port(port, ports_array) else: rule_ports = PortSet(True) if not_ports_array is not None: for port in not_ports_array: rule_ports -= self._parse_port(port, not_ports_array) return rule_ports
def __init__(self, policy, peer_container, ingress_file_name=''): """ :param dict policy: The ingress policy object as provided by the yaml parser :param PeerContainer peer_container: The ingress policy will be evaluated against this set of peers :param str ingress_file_name: The name of the ingress resource file """ GenericYamlParser.__init__(self, ingress_file_name) self.policy = policy self.peer_container = peer_container self.namespace = None self.default_backend_peers = PeerSet() self.default_backend_ports = PortSet()
def parse_backend(self, backend, is_default=False): """ Parses ingress backend and returns the set of pods and ports referenced by it. :param dict backend: the backend resource :param bool is_default: whether this is the default backend :return: a tuple PeerSet and PortSet: the sets of pods and ports referenced by the backend, or None and all ports when the default backend is None, or None and None when the non-default backend is None. """ if backend is None: return (None, PortSet(True)) if is_default else (None, None) allowed_elements = {'resource': [0, dict], 'service': [0, dict]} self.check_fields_validity(backend, 'backend', allowed_elements) resource = backend.get('resource') service = backend.get('service') if resource and service: self.syntax_error( f'Resource and service are not mutually exclusive' f'in the ingress {"default" if is_default else ""} backend', backend) if resource: self.warning( 'Resource is not yet supported in an ingress backend. Ignoring', backend) return (None, PortSet(True)) if is_default else (None, None) allowed_service_elements = {'name': [1, str], 'port': [1, dict]} self.check_fields_validity(service, 'backend service', allowed_service_elements) service_name = service.get('name') service_port = service.get('port') allowed_port_elements = {'name': [0, str], 'number': [0, int]} self.check_fields_validity(service_port, 'backend service port', allowed_port_elements) port_name = service_port.get('name') port_number = service_port.get('number') if port_name and port_number: self.syntax_error( f'Port name and port number are mutually exclusive' f'in the ingress {"default" if is_default else ""} backend', service) if port_number: self.validate_value_in_domain(port_number, 'dst_ports', backend, 'Port number') srv = self.peer_container.get_service_by_name_and_ns( service_name, self.namespace) if not srv: self.syntax_error( f'Missing service referenced by the ingress {"default" if is_default else ""} backend', service) service_port = srv.get_port_by_name( port_name) if port_name else srv.get_port_by_number(port_number) if not service_port: self.syntax_error( f'Missing port {port_name if port_name else port_number} in the service', service) rule_ports = PortSet() rule_ports.add_port( service_port.target_port) # may be either a number or a named port return srv.target_pods, rule_ports
def test_k8s_flow(self): """ dest ports with named ports, and 'or' between Tcp properties with named ports """ src_res_ports = PortSet(True) dst_res_ports = PortSet() dst_res_ports.add_port("x") tcp_properties1 = TcpLikeProperties(src_res_ports, dst_res_ports) dst_res_ports2 = PortSet() dst_res_ports2.add_port("y") tcp_properties2 = TcpLikeProperties(src_res_ports, dst_res_ports2) tcp_properties_res = tcp_properties1 | tcp_properties2 named_ports_dict = {"x": (15, 6), "z": (20, 6), "y": (16, 6)} tcp_properties_res.convert_named_ports(named_ports_dict, 6) #print(tcp_properties_res) cubes_list = tcp_properties_res._get_cubes_list_from_layers() expected_res_cubes = [[CanonicalIntervalSet.get_interval_set(15, 16)]] self.assertEqual(expected_res_cubes, cubes_list)
def _make_deny_rules(self, allowed_conns): """ Make deny rules from the given connections :param TcpLikeProperties allowed_conns: the given allowed connections :return: the list of deny IngressPolicyRules """ all_peers_and_ip_blocks = self.peer_container.peer_set.copy() all_peers_and_ip_blocks.add( IpBlock.get_all_ips_block()) # add IpBlock of all IPs all_conns = self._make_tcp_like_properties(PortSet(True), all_peers_and_ip_blocks) denied_conns = all_conns - allowed_conns res = self._make_rules_from_conns(denied_conns) # Add deny rule for all protocols but TCP , relevant for all peers and ip blocks non_tcp_conns = ConnectionSet.get_non_tcp_connections() res.append(IngressPolicyRule(all_peers_and_ip_blocks, non_tcp_conns)) return res
def _get_connection_set_from_properties(dest_ports, method_set=MethodSet(True), paths_dfa=None, hosts_dfa=None): """ get ConnectionSet with TCP allowed connections, corresponding to input properties cube :param PortSet dest_ports: ports set for dset_ports dimension :param MethodSet method_set: methods set for methods dimension :param MinDFA paths_dfa: MinDFA obj for paths dimension :param MinDFA hosts_dfa: MinDFA obj for hosts dimension :return: ConnectionSet with TCP allowed connections , corresponding to input properties cube """ tcp_properties = TcpLikeProperties(source_ports=PortSet(True), dest_ports=dest_ports, methods=method_set, paths=paths_dfa, hosts=hosts_dfa) res = ConnectionSet() res.add_connections('TCP', tcp_properties) return res
def parse_port(self, port): """ Parse an element of the "ports" phrase of a policy rule :param dict port: The element to parse :return: A ConnectionSet representing the allowed connections by this element (protocols X port numbers) :rtype: ConnectionSet """ self.check_fields_validity(port, 'NetworkPolicyPort', { 'port': 0, 'protocol': [0, str], 'endPort': [0, int] }, {'protocol': ['TCP', 'UDP', 'SCTP']}) port_id = port.get('port') protocol = port.get('protocol') end_port_num = port.get('endPort') if not protocol: protocol = 'TCP' res = ConnectionSet() dest_port_set = PortSet(port_id is None) if port_id is not None and end_port_num is not None: if isinstance(port_id, str): self.syntax_error( 'endPort cannot be defined if the port field is defined ' 'as a named (string) port', port) if not isinstance(port_id, int): self.syntax_error( 'type of port is not numerical in NetworkPolicyPort', port) self.validate_value_in_domain(port_id, 'dst_ports', port, 'Port number') self.validate_value_in_domain(end_port_num, 'dst_ports', port, 'endPort number') if port_id > end_port_num: self.syntax_error('endPort must be equal or greater than port', port) dest_port_set.add_port_range(port_id, end_port_num) elif port_id is not None: if not isinstance(port_id, str) and not isinstance(port_id, int): self.syntax_error( 'type of port is not numerical or named (string) in NetworkPolicyPort', port) if isinstance(port_id, int): self.validate_value_in_domain(port_id, 'dst_ports', port, 'Port number') if isinstance(port_id, str): if len(port_id) > 15: self.syntax_error( 'port name must be no more than 15 characters', port) if re.fullmatch(r"[a-z\d]([-a-z\d]*[a-z\d])?", port_id) is None: self.syntax_error( 'port name should contain only lowercase alphanumeric characters or "-", ' 'and start and end with alphanumeric characters', port) dest_port_set.add_port(port_id) elif end_port_num: self.syntax_error( 'endPort cannot be defined if the port field is not defined ', port) res.add_connections( protocol, TcpLikeProperties( PortSet(True), dest_port_set)) # K8s doesn't reason about src ports return res
def get_all_tcp_connections(): tcp_conns = ConnectionSet() tcp_conns.add_connections( 'TCP', TcpLikeProperties(PortSet(True), PortSet(True))) return tcp_conns
def _make_tcp_like_properties(self, dest_ports, peers, paths_dfa=None, hosts_dfa=None): """ get TcpLikeProperties with TCP allowed connections, corresponding to input properties cube. TcpLikeProperties should not contain named ports: substitute them with corresponding port numbers, per peer :param PortSet dest_ports: ports set for dest_ports dimension (possibly containing named ports) :param PeerSet peers: the set of (target) peers :param MinDFA paths_dfa: MinDFA obj for paths dimension :param MinDFA hosts_dfa: MinDFA obj for hosts dimension :return: TcpLikeProperties with TCP allowed connections, corresponding to input properties cube """ assert peers base_peer_set = self.peer_container.peer_set.copy() base_peer_set.add(IpBlock.get_all_ips_block()) if not dest_ports.named_ports: peers_interval = base_peer_set.get_peer_interval_of(peers) return TcpLikeProperties(source_ports=PortSet(True), dest_ports=dest_ports, methods=MethodSet(True), paths=paths_dfa, hosts=hosts_dfa, peers=peers_interval, base_peer_set=base_peer_set) assert not dest_ports.port_set assert len(dest_ports.named_ports) == 1 port = list(dest_ports.named_ports)[0] tcp_properties = None for peer in peers: named_ports = peer.get_named_ports() real_port = named_ports.get(port) if not real_port: self.warning( f'Missing named port {port} in the pod {peer}. Ignoring the pod' ) continue if real_port[1] != 'TCP': self.warning( f'Illegal protocol {real_port[1]} in the named port {port} ingress target pod {peer}.' f'Ignoring the pod') continue peer_in_set = PeerSet() peer_in_set.add(peer) ports = PortSet() ports.add_port(real_port[0]) props = TcpLikeProperties( source_ports=PortSet(True), dest_ports=ports, methods=MethodSet(True), paths=paths_dfa, hosts=hosts_dfa, peers=base_peer_set.get_peer_interval_of(peer_in_set), base_peer_set=base_peer_set) if tcp_properties: tcp_properties |= props else: tcp_properties = props return tcp_properties
def _parse_xgress_rule(self, rule, is_ingress, policy_selected_eps, is_profile): """ Parse a single ingres/egress rule, producing a CalicoPolicyRule :param dict rule: The rule element to parse :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 CalicoPolicyRule with the proper PeerSets, ConnectionSets and Action :rtype: CalicoPolicyRule """ allowed_keys = { 'action': 1, 'protocol': 0, 'notProtocol': 0, 'icmp': 0, 'notICMP': 0, 'ipVersion': 0, 'source': 0, 'destination': 0, 'http': 2 } self.check_fields_validity(rule, 'ingress/egress rule', allowed_keys) action = CalicoPolicyRule.action_str_to_action_type(rule['action']) if action is None: self.syntax_error('Invalid rule action: ' + rule['action'], rule) if is_profile and action == CalicoPolicyRule.ActionType.Pass: 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) not_protocol = self._parse_protocol(rule.get('notProtocol'), rule) src_entity_rule = rule.get('source') if src_entity_rule: src_res_pods, src_res_ports = self._parse_entity_rule( src_entity_rule, protocol_supports_ports) else: src_res_pods = self.peer_container.get_all_peers_group(True) src_res_ports = PortSet(True) dst_entity_rule = rule.get('destination') if dst_entity_rule: dst_res_pods, dst_res_ports = self._parse_entity_rule( dst_entity_rule, protocol_supports_ports) else: dst_res_pods = self.peer_container.get_all_peers_group(True) dst_res_ports = PortSet(True) if is_ingress: # FIXME: We do not handle well the case where dst_res_pods or src_res_pods contain ipBlocks dst_res_pods &= policy_selected_eps else: src_res_pods &= policy_selected_eps connections = ConnectionSet() if protocol is not None: if not_protocol is not None: if protocol == not_protocol: self.warning( 'Protocol and notProtocol are conflicting, no traffic will be matched', rule) else: self.warning('notProtocol field has no effect', rule) else: if protocol_supports_ports: connections.add_connections( protocol, TcpLikeProperties(src_res_ports, dst_res_ports)) elif ConnectionSet.protocol_is_icmp(protocol): connections.add_connections( protocol, self._parse_icmp(rule.get('icmp'), rule.get('notICMP'))) else: connections.add_connections(protocol, True) elif not_protocol is not None: connections.add_all_connections() connections.remove_protocol(not_protocol) else: connections.allow_all = True self._verify_named_ports(rule, dst_res_pods, connections) 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)