Source code for networkapi.plugins.SDN.ODL.flows.acl

# -*- coding: utf-8 -*-
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


from networkapi.plugins.SDN.ODL.utils.cookie_handler import CookieHandler
from networkapi.plugins.SDN.ODL.utils.tcp_control_bits import TCPControlBits
from networkapi.plugins.SDN.ODL.utils.odl_plugin_masks import ODLPluginMasks

import re
import logging
from json import dumps
from copy import deepcopy

to_str_id = ODLPluginMasks.to_str_id
to_str_id_both = ODLPluginMasks.to_str_id_both
to_str_description = ODLPluginMasks.to_str_description
to_str_description_both = ODLPluginMasks.to_str_description_both


[docs]class Tokens(object): """ Class that holds all key words from the source json that identifies a valid ACL to be translated to a OpenDayLight json format """ kind = "kind" rules = "rules" id_ = "id" action = "action" description = "description" source = "source" destination = "destination" protocol = "protocol" l4_options = "l4-options" src_port_op = "src-port-op" src_port = "src-port-start" src_port_end = "src-port-end" dst_port_op = "dest-port-op" dst_port = "dest-port-start" dst_port_end = "dest-port-end" icmp_options = "icmp-options" icmp_code = "icmp-code" icmp_type = "icmp-type" flags = "flags" priority = "priority" cookie = "cookie" sequence = "sequence" range = "range" eq = "eq"
[docs]class AclFlowBuilder(object): """ Class responsible for build json data for Access control list flow at OpenDayLight controller """ LOG_FORMAT = '%(levelname)s:%(message)s' MALFORMED_MESSAGE = "Error building ACL Json. Malformed input data: \n%s" PRIORITY_DEFAULT = 65000 TABLE = 0 ALLOWED_FLOWS_SIZE = 5 MAX_RANGE_LENGTH = 120 def __init__(self, data, environment=0, version="BERYLLIUM"): self.raw_data = data # Original data self.flows = {"flow": []} # Processed data if environment is None: environment = 0 self.environment = int(environment) self.version = version self.dumped_rule = None # Actual processing rule in json format self._reset_control_members() logging.basicConfig(format=self.LOG_FORMAT, level=logging.DEBUG) def _reset_control_members(self): # Used to build double ranges self.current_src_port = None self.current_dst_port = None # Used to build simple ranges self.current_src_or_dst_port = None # Used to control flows generation for one rule self.generated_all_flows_from_rule = False def _clear_flows(self): """ Clear flows variable to avoid huge object in memory """ self.flows["flow"] = []
[docs] def dump(self): """ Returns a json of built flows """ if not isinstance(self.flows, dict): raise TypeError("self.flows must be a dictionary") flows_set = self.build() for flows in flows_set: flows_ids = [flow["id"] for flow in flows["flow"]] yield flows_ids, dumps(flows)
[docs] def build(self): """ Verifies input data and build flows for OpenDayLight controller """ if Tokens.kind in self.raw_data and Tokens.rules in self.raw_data: logging.info("Building ACL Json: %s", self.raw_data["kind"]) for rule in self.raw_data[Tokens.rules]: for current_flows in self._build_rule(rule): if len(current_flows["flow"]) == self.ALLOWED_FLOWS_SIZE: yield current_flows self._clear_flows() if len(self.flows["flow"]) > 0: yield self.flows self._clear_flows() else: message = "Missing %s or %s fields." % (Tokens.kind, Tokens.rules) logging.error(self.MALFORMED_MESSAGE % message) raise ValueError(self.MALFORMED_MESSAGE % message)
def _build_rule(self, rule): """ Builds one or more flows based at one ACL rule """ self.dumped_rule = dumps(rule, sort_keys=True) self._reset_control_members() while not self.generated_all_flows_from_rule: # Assigns the id of the current ACL # We always insert in the head of the list to simplify the access # to the current index self.flows["flow"].insert(0, {Tokens.id_: rule[Tokens.id_]}) # Flow table and priority self.flows["flow"][0]["table_id"] = self.TABLE self._build_description(rule) self._build_match(rule) self._build_action(rule) self._build_cookie(rule) self._build_sequence(rule) self._build_protocol(rule) yield self.flows def _build_description(self, rule): """ Builds the flow name field using OpenDayLight json format """ if Tokens.description not in rule: rule[Tokens.description] = "" self.flows["flow"][0]["flow-name"] = rule[Tokens.description] def _build_match(self, rule): """ Builds the match field that identifies the ACL rule """ self.flows["flow"][0]["match"] = { "ethernet-match": { "ethernet-type": { "type": 2048 } } } if Tokens.destination in rule and Tokens.source in rule: self.flows["flow"][0]["match"]["ipv4-destination"] = \ rule[Tokens.destination] self.flows["flow"][0]["match"]["ipv4-source"] = rule[Tokens.source] else: logging.error(self.MALFORMED_MESSAGE % self.dumped_rule) raise ValueError(self.MALFORMED_MESSAGE % self.dumped_rule) def _build_action(self, rule): """ Builds the Openflow actions to a flow """ if Tokens.action in rule and rule[Tokens.action] == "permit": self.flows["flow"][0]["instructions"] = { "instruction": [{ "order": 0, "apply-actions": { "action": [{ "order": 0, "output-action": { "output-node-connector": "NORMAL" } }] } }] } def _build_cookie(self, rule): """ Builds optional 64-bits field named cookie """ id_rule = rule[Tokens.id_] cookie_handler = CookieHandler(id_rule, self.environment) self.flows["flow"][0][Tokens.cookie] = cookie_handler.cookie def _build_sequence(self, rule): """ Build sequence field to set flow priority """ # if Tokens.sequence in rule: # self.flows["flow"][0]["priority"] = rule[Tokens.sequence] # else: self.flows["flow"][0]["priority"] = self.PRIORITY_DEFAULT def _build_protocol(self, rule): """ Identifies the protocol of the ACL rule """ if Tokens.protocol not in rule: message = "Missing %s field:\n%s" % (Tokens.protocol, self.dumped_rule) logging.error(self.MALFORMED_MESSAGE % message) raise ValueError(self.MALFORMED_MESSAGE % message) else: if rule[Tokens.protocol] == "tcp": self._build_tcp(rule) elif rule[Tokens.protocol] == "udp": self._build_udp(rule) elif rule[Tokens.protocol] == "icmp": self._build_icmp(rule) elif rule[Tokens.protocol] == "ip": self.generated_all_flows_from_rule = True pass # It is not necessary to process a IP protocol else: message = "Unknown protocol '%s'" % rule[Tokens.protocol] logging.error(self.MALFORMED_MESSAGE % message) raise ValueError(self.MALFORMED_MESSAGE % message) def _build_tcp(self, rule): """ Builds a TCP flow based on OpenDayLight json format """ self._set_flow_ip_protocol(6) self._set_tcp_flags(rule) self._check_source_and_destination_ports(rule, "tcp") def _build_udp(self, rule): """ Builds a UDP flow based on OpenDayLight json format """ self._set_flow_ip_protocol(17) self._check_source_and_destination_ports(rule, "udp") def _build_icmp(self, rule): """ Builds ICMP protocol acl using OpenDayLight json format """ self._set_flow_ip_protocol(1) self.generated_all_flows_from_rule = True if Tokens.icmp_options in rule: if Tokens.icmp_code in rule[Tokens.icmp_options] and \ Tokens.icmp_type in rule[Tokens.icmp_options]: icmp_options = rule[Tokens.icmp_options] self.flows["flow"][0]["match"]["icmpv4-match"] = { "icmpv4-code": icmp_options[Tokens.icmp_code], "icmpv4-type": icmp_options[Tokens.icmp_type] } else: message = "Missing %s or %s icmp options:\n%s" % ( Tokens.icmp_code, Tokens.icmp_type, self.dumped_rule) logging.error(self.MALFORMED_MESSAGE % message) raise ValueError(self.MALFORMED_MESSAGE % message) else: message = "Missing %s for icmp protocol" % Tokens.icmp_options logging.error(self.MALFORMED_MESSAGE % message) raise ValueError(self.MALFORMED_MESSAGE % message) def _set_flow_ip_protocol(self, protocol_n): """ Sets the IP protocol number inside given flow """ self.flows["flow"][0]["match"]["ip-match"] = { "ip-protocol": protocol_n } def _check_source_and_destination_ports(self, rule, protocol): """ Checks source and destination options inside json """ l4_options = rule.get(Tokens.l4_options, {}) #this if is an temporary solution if self._calc_length_of_range(rule)>self.MAX_RANGE_LENGTH: logging.warning("Max range lenght reached. A more permissive flow will be used.") self.generated_all_flows_from_rule = True elif l4_options.get(Tokens.src_port_op) == Tokens.range and \ l4_options.get(Tokens.dst_port_op) == Tokens.range: self._build_double_range(rule, protocol) elif l4_options.get(Tokens.src_port_op) == Tokens.range: self._build_simple_range(rule, protocol, Tokens.src_port, Tokens.src_port_end) elif l4_options.get(Tokens.dst_port_op) == Tokens.range: self._build_simple_range(rule, protocol, Tokens.dst_port, Tokens.dst_port_end) else: self._build_transport_source_ports(rule, protocol) self._build_transport_destination_ports(rule, protocol) self.generated_all_flows_from_rule = True def _set_tcp_flags(self, rule): """ Sets the flags inside given flow """ l4_options = rule.get(Tokens.l4_options, {}) if Tokens.flags in l4_options: flags = l4_options[Tokens.flags] tcp_flags = TCPControlBits(flags).to_int() if self.version in ["BERYLLIUM"]: self.flows["flow"][0]["match"]["tcp-flag-match"] = { "tcp-flag": tcp_flags, } elif self.version in ["BORON", "CARBON", "NITROGEN"]: self.flows["flow"][0]["match"]["tcp-flags-match"] = { "tcp-flags": tcp_flags, } def _build_simple_range(self, rule, protocol, start, end): """ Builds a TCP|UDP flows when ACL has Src and Dst ranges.""" port_end = int(rule[Tokens.l4_options][end]) if self.current_src_or_dst_port is not None: # Assigns if the last iteration made flows array to reach # ALLOWED_FLOWS_SIZE before reach the last port in range port_start = self.current_src_or_dst_port else: # Assigns if it is the first time building range for rule port_start = int(rule[Tokens.l4_options][start]) for port in xrange(port_start, port_end + 1): # Do this to avoid change the first port of range in orig rule # We use it to update each flow description field rule_copy = deepcopy(rule) rule_copy[Tokens.l4_options][start] = str(port) self._build_transport_source_ports(rule_copy, protocol) self._build_transport_destination_ports(rule_copy, protocol) self._build_id_and_description_when_simple_range( rule, port, rule[Tokens.l4_options][start], port_end) if port == port_end: # If we finished to build all ports in range, # set variable below to True to avoid rebuild # the same rule self.generated_all_flows_from_rule = True return if len(self.flows["flow"]) == self.ALLOWED_FLOWS_SIZE: # If flows array reach ALLOWED_FLOWS_SIZE, we stop to # build and save actual state self.current_src_or_dst_port = port + 1 return self._insert_new_flow_when_single_range(port, port_end) def _build_double_range(self, rule, protocol): """ Builds a TCP|UDP flows when ACL has Src or Dst ranges.""" l4_options = rule[Tokens.l4_options] src_port_end = int(l4_options[Tokens.src_port_end]) dst_port_end = int(l4_options[Tokens.dst_port_end]) if self.current_src_port is not None \ and self.current_dst_port is not None: # Assigns if the last iteration made flows array to reach # ALLOWED_FLOWS_SIZE before reach the last port in range src_port_start = self.current_src_port dst_port_start = self.current_dst_port else: # Assigns if it is the first time building range for rule src_port_start = int(l4_options[Tokens.src_port]) dst_port_start = int(l4_options[Tokens.dst_port]) if dst_port_start > dst_port_end: src_port_start += 1 dst_port_start = int(l4_options[Tokens.dst_port]) for src_port in xrange(src_port_start, src_port_end + 1): for dst_port in xrange(dst_port_start, dst_port_end + 1): # Do this to avoid change the first port of range # in original rule. # We use it to update each flow description field rule_copy = deepcopy(rule) rule_copy[Tokens.l4_options][Tokens.src_port] = str(src_port) rule_copy[Tokens.l4_options][Tokens.dst_port] = str(dst_port) self._build_transport_source_ports(rule_copy, protocol) self._build_transport_destination_ports(rule_copy, protocol) self._build_id_and_description_when_double_range( rule, src_port, dst_port) if src_port == src_port_end and dst_port == dst_port_end: # If we finished to build all ports in range, # set variable below to True to avoid rebuild # the same rule self.generated_all_flows_from_rule = True return if dst_port == dst_port_end: # When state is save, make sure that in next iteration # of src ranges all related destination ports will be # built together dst_port_start = int(l4_options[Tokens.dst_port]) if len(self.flows["flow"]) == self.ALLOWED_FLOWS_SIZE: # If flows array reach ALLOWED_FLOWS_SIZE, we stop to # build and save actual state self.current_src_port = src_port self.current_dst_port = dst_port + 1 return self._insert_new_flow_when_double_range(src_port, src_port_end, dst_port, dst_port_end) def _insert_new_flow_when_single_range(self, port, port_end): """ Check inside single range if still exists ports to build allocating one flow more """ if port < port_end: self._insert_new_flow() def _insert_new_flow_when_double_range(self, src_port, src_port_end, dst_port, dst_port_end): """ Check inside double range if still exists ports to build allocating one flow more """ if src_port < src_port_end or dst_port < dst_port_end: self._insert_new_flow() def _insert_new_flow(self): """ Create deep copy of last flow built in the head of the list to simplify the access to the current index when building next port. """ self.flows["flow"].insert(0, deepcopy(self.flows["flow"][0])) def _build_id_and_description_when_simple_range( self, rule, port, port_start, port_end): """ Builds custom id and description for flows generated by ACL that have Src range or Dst range. """ self._build_id_when_only_src_or_dst_range(rule, port) self._build_id_when_src_eq_and_dst_range(rule, port) self._build_id_when_src_range_and_dst_eq(rule, port) self._build_description_when_only_src_or_dst_range( rule, port_start, port_end) self._build_description_when_src_range_and_dst_eq( rule, port_start, port_end) self._build_description_when_src_eq_and_dst_range( rule, port_start, port_end) def _build_id_and_description_when_double_range( self, rule, src_port, dst_port): """ Builds custom id and description for flows generated by ACL that have Src range and Dst range. """ self._build_id_when_src_range_and_dst_range(rule, src_port, dst_port) self._build_description_when_src_range_and_dst_range(rule) def _build_description_when_src_eq_and_dst_range( self, rule, port_start, port_end): l4_options = rule[Tokens.l4_options] if l4_options.get(Tokens.src_port_op) == Tokens.eq and \ l4_options.get(Tokens.dst_port_op) == Tokens.range: self.flows["flow"][0]["flow-name"] = to_str_description_both( rule[Tokens.description], rule[Tokens.l4_options].get(Tokens.src_port), rule[Tokens.l4_options].get(Tokens.src_port), port_start, port_end) def _build_description_when_src_range_and_dst_eq( self, rule, port_start, port_end): l4_options = rule[Tokens.l4_options] if l4_options.get(Tokens.src_port_op) == Tokens.range and \ l4_options.get(Tokens.dst_port_op) == Tokens.eq: self.flows["flow"][0]["flow-name"] = to_str_description_both( rule[Tokens.description], port_start, port_end, rule[Tokens.l4_options].get(Tokens.dst_port), rule[Tokens.l4_options].get(Tokens.dst_port)) def _build_description_when_only_src_or_dst_range( self, rule, port_start, port_end): self.flows["flow"][0]["flow-name"] = to_str_description( rule[Tokens.description], port_start, port_end) def _build_description_when_src_range_and_dst_range(self, rule): self.flows["flow"][0]["flow-name"] = to_str_description_both( rule[Tokens.description], rule[Tokens.l4_options][Tokens.src_port], rule[Tokens.l4_options][Tokens.src_port_end], rule[Tokens.l4_options][Tokens.dst_port], rule[Tokens.l4_options][Tokens.dst_port_end]) def _build_id_when_src_eq_and_dst_range(self, rule, port): l4_options = rule[Tokens.l4_options] if l4_options.get(Tokens.src_port_op) == Tokens.eq and \ l4_options.get(Tokens.dst_port_op) == Tokens.range: self.flows["flow"][0]["id"] = to_str_id_both( rule[Tokens.id_], rule[Tokens.l4_options][Tokens.src_port], port) def _build_id_when_src_range_and_dst_eq(self, rule, port): l4_options = rule[Tokens.l4_options] if l4_options.get(Tokens.src_port_op) == Tokens.range and \ l4_options.get(Tokens.dst_port_op) == Tokens.eq: self.flows["flow"][0]["id"] = to_str_id_both( rule[Tokens.id_], port, rule[Tokens.l4_options][Tokens.dst_port]) def _build_id_when_only_src_or_dst_range(self, rule, port): self.flows["flow"][0]["id"] = to_str_id( rule[Tokens.id_], port) def _build_id_when_src_range_and_dst_range(self, rule, src_port, dst_port): self.flows["flow"][0]["id"] = to_str_id_both( rule[Tokens.id_], src_port, dst_port) def _build_transport_source_ports(self, rule, protocol): """ Builds source ports for transport protocols TCP or UDP """ l4_options = rule.get(Tokens.l4_options, {}) if Tokens.src_port_op in l4_options: prefix = protocol + "-source-port" self._build_transport_ports(rule, prefix, Tokens.src_port_op, Tokens.src_port, Tokens.src_port_end) def _build_transport_destination_ports(self, rule, protocol): """ Builds destination ports for transport protocols TCP or UDP """ l4_options = rule.get(Tokens.l4_options, {}) if Tokens.dst_port_op in l4_options: prefix = protocol + "-destination-port" self._build_transport_ports(rule, prefix, Tokens.dst_port_op, Tokens.dst_port, Tokens.dst_port_end) def _build_transport_ports(self, rule, prefix, operation, start, end): """ Builds transport (TCP | UDP) protocols json data """ self.flows["flow"][0]["match"][prefix] = \ rule[Tokens.l4_options][start] def _calc_length_of_range(self, rule): l4_options = rule.get(Tokens.l4_options, {}) src_range=1 dst_range=1 if l4_options.get(Tokens.src_port_op) == Tokens.range: src_port_start = int(l4_options[Tokens.src_port]) src_port_end = int(l4_options[Tokens.src_port_end]) src_range=src_port_end-src_port_start+1 if l4_options.get(Tokens.dst_port_op) == Tokens.range: dst_port_start = int(l4_options[Tokens.dst_port]) dst_port_end = int(l4_options[Tokens.dst_port_end]) dst_range=dst_port_end-dst_port_start+1 return src_range * dst_range