Ejemplo n.º 1
0
    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
Ejemplo n.º 5
0
 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
Ejemplo n.º 7
0
 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
Ejemplo n.º 9
0
    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
Ejemplo n.º 12
0
 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
Ejemplo n.º 14
0
 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
Ejemplo n.º 16
0
 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)