havox 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +674 -0
  7. data/README.md +82 -0
  8. data/Rakefile +6 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/config.rb +4 -0
  12. data/examples/example_3x3.dot +81 -0
  13. data/examples/example_3x3.hvx +5 -0
  14. data/exe/havox +107 -0
  15. data/havox.gemspec +33 -0
  16. data/lib/havox.rb +51 -0
  17. data/lib/havox/app/api.rb +57 -0
  18. data/lib/havox/app/helpers/methods.rb +43 -0
  19. data/lib/havox/classes/edge.rb +11 -0
  20. data/lib/havox/classes/modified_policy.rb +44 -0
  21. data/lib/havox/classes/node.rb +19 -0
  22. data/lib/havox/classes/policy.rb +68 -0
  23. data/lib/havox/classes/rib.rb +30 -0
  24. data/lib/havox/classes/route.rb +82 -0
  25. data/lib/havox/classes/route_filler.rb +43 -0
  26. data/lib/havox/classes/rule.rb +74 -0
  27. data/lib/havox/classes/rule_expander.rb +47 -0
  28. data/lib/havox/classes/rule_sanitizer.rb +42 -0
  29. data/lib/havox/classes/topology.rb +75 -0
  30. data/lib/havox/classes/translator.rb +37 -0
  31. data/lib/havox/configuration.rb +20 -0
  32. data/lib/havox/dsl/directive.rb +70 -0
  33. data/lib/havox/dsl/directive_proxy.rb +36 -0
  34. data/lib/havox/dsl/examples/routeflow.hvx +12 -0
  35. data/lib/havox/dsl/network.rb +64 -0
  36. data/lib/havox/exceptions.rb +21 -0
  37. data/lib/havox/modules/command.rb +31 -0
  38. data/lib/havox/modules/field_parser.rb +21 -0
  39. data/lib/havox/modules/merlin.rb +85 -0
  40. data/lib/havox/modules/openflow10/ovs/actions.rb +41 -0
  41. data/lib/havox/modules/openflow10/ovs/matches.rb +35 -0
  42. data/lib/havox/modules/openflow10/routeflow/actions.rb +45 -0
  43. data/lib/havox/modules/openflow10/routeflow/matches.rb +44 -0
  44. data/lib/havox/modules/openflow10/trema/actions.rb +45 -0
  45. data/lib/havox/modules/openflow10/trema/matches.rb +43 -0
  46. data/lib/havox/modules/routeflow.rb +50 -0
  47. data/lib/havox/version.rb +3 -0
  48. data/lib/merlin/policies/common.mln +9 -0
  49. data/lib/merlin/policies/defense.mln +5 -0
  50. data/lib/merlin/policies/max.mln +2 -0
  51. data/lib/merlin/policies/min.mln +2 -0
  52. data/lib/merlin/policies/routeflow.mln +8 -0
  53. data/lib/merlin/policies/test.mln +2 -0
  54. data/lib/merlin/policies/tetrahedron.mln +8 -0
  55. data/lib/merlin/policies/tetrahedron.sample.mln +15 -0
  56. data/lib/merlin/policies/web_balanced.mln +7 -0
  57. data/lib/merlin/policies/web_unbalanced.mln +2 -0
  58. data/lib/merlin/topologies/common.dot +63 -0
  59. data/lib/merlin/topologies/defense.dot +20 -0
  60. data/lib/merlin/topologies/max.dot +17 -0
  61. data/lib/merlin/topologies/min.dot +11 -0
  62. data/lib/merlin/topologies/routeflow.dot +37 -0
  63. data/lib/merlin/topologies/tetrahedron.dot +39 -0
  64. data/lib/trema/controllers/main_controller.rb +113 -0
  65. data/lib/trema/topologies/common_topology.rb +36 -0
  66. data/lib/trema/topologies/routeflow_topology.rb +21 -0
  67. data/lib/trema/topologies/tetrahedron_topology.rb +22 -0
  68. metadata +253 -0
@@ -0,0 +1,42 @@
1
+ module Havox
2
+ class RuleSanitizer
3
+ attr_reader :sanitized_rules
4
+
5
+ SET_VLAN_ID_REGEX = /SetField\(vlan,\s?(?<vlan_id>\d+)\)/
6
+ VLAN_MATCH_REGEX = /vlanId\s?=\s?(?<vlan_id>\d+)/
7
+ SELF_VLAN_REGEX = /\s?and\svlanId\s?=\s?65535/
8
+
9
+ def initialize(raw_rules)
10
+ @raw_rules = raw_rules
11
+ @setted_vlan_ids = []
12
+ @sanitized_rules = []
13
+ scan_setted_vlan_ids
14
+ sanitize
15
+ end
16
+
17
+ private
18
+
19
+ # Main method which iterates over all the rules and cleans them.
20
+ def sanitize
21
+ @raw_rules.each do |raw_rule|
22
+ mod_raw_rule = raw_rule.gsub(SELF_VLAN_REGEX, '')
23
+ @sanitized_rules << mod_raw_rule if valid_vlan?(mod_raw_rule)
24
+ end
25
+ end
26
+
27
+ # Determines all VLAN IDs defined by the SetField action.
28
+ def scan_setted_vlan_ids
29
+ @raw_rules.each do |raw_rule|
30
+ match_data = raw_rule.match(SET_VLAN_ID_REGEX)
31
+ @setted_vlan_ids << match_data[:vlan_id] unless match_data.nil?
32
+ end
33
+ end
34
+
35
+ # The rule is valid if it does not have matches like 'vlanId = x' or, in
36
+ # case it has, if 'x' is set by the SetField action.
37
+ def valid_vlan?(raw_rule)
38
+ match_data = raw_rule.match(VLAN_MATCH_REGEX)
39
+ match_data.nil? || @setted_vlan_ids.include?(match_data[:vlan_id])
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,75 @@
1
+ module Havox
2
+ class Topology
3
+ attr_reader :nodes, :edges
4
+
5
+ NODE_REGEX = /^\s*(?<name>\w+)\s?\[(?<attributes>.*)\];$/i
6
+ EDGE_REGEX = /^\s*(?<from>\w+)\s?->\s?(?<to>\w+)\s?\[(?<attributes>.*)\];$/i
7
+
8
+ def initialize(file_path)
9
+ @file_path = file_path
10
+ @nodes = []
11
+ @edges = []
12
+ parse_dot_file
13
+ end
14
+
15
+ def host_names
16
+ @nodes.select(&:host?).map(&:name)
17
+ end
18
+
19
+ def switch_ips
20
+ switches = @nodes.select(&:switch?)
21
+ switch_ip_hash = {}
22
+ switches.each { |s| switch_ip_hash[s.name] = s.attributes[:ip] }
23
+ switch_ip_hash
24
+ end
25
+
26
+ def switch_hosts
27
+ exit_edges = @edges.select { |e| e.from.switch? && e.to.host? }
28
+ hash = {}
29
+ exit_edges.each do |e|
30
+ if hash[e.from.name].nil?
31
+ hash[e.from.name] = [e.to.name]
32
+ else
33
+ hash[e.from.name] << e.to.name
34
+ end
35
+ end
36
+ hash
37
+ end
38
+
39
+ private
40
+
41
+ def parse_dot_file
42
+ File.read(@file_path).each_line do |line|
43
+ parse_node(line)
44
+ parse_edge(line)
45
+ end
46
+ end
47
+
48
+ def parse_node(line)
49
+ match = line.match(NODE_REGEX)
50
+ unless match.nil?
51
+ attributes = hashed_attributes(match[:attributes].to_s)
52
+ @nodes << Havox::Node.new(match[:name].to_s, attributes)
53
+ end
54
+ end
55
+
56
+ def parse_edge(line)
57
+ match = line.match(EDGE_REGEX)
58
+ unless match.nil?
59
+ attributes = hashed_attributes(match[:attributes].to_s)
60
+ node_from = @nodes.find { |n| n.name.eql?(match[:from].to_s) }
61
+ node_to = @nodes.find { |n| n.name.eql?(match[:to].to_s) }
62
+ @edges << Havox::Edge.new(node_from, node_to, attributes)
63
+ end
64
+ end
65
+
66
+ def hashed_attributes(raw_attrs)
67
+ hash = {}
68
+ raw_attrs.gsub('"', '').split(',').each do |str|
69
+ field, value = str.strip.split(/\s*=\s*/)
70
+ hash[field.to_sym] = value
71
+ end
72
+ hash
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,37 @@
1
+ module Havox
2
+ class Translator
3
+ # OPTIMIZE: Refactor the translation schema to be scalable.
4
+ # Current way of selecting the syntax via multiple match and action files
5
+ # is not DRY. Refactor the translation schema to be scalable, maybe using
6
+ # YAML or some other better structure.
7
+
8
+ include Singleton
9
+
10
+ def fields_to(syntax)
11
+ translation_module(syntax)::Matches::FIELDS
12
+ end
13
+
14
+ def matches_to(syntax, matches_array)
15
+ translation_module(syntax)::Matches.treat(matches_array)
16
+ end
17
+
18
+ def actions_to(syntax, actions_array, opts = {})
19
+ translation_module(syntax)::Actions.treat(actions_array, opts)
20
+ end
21
+
22
+ private
23
+
24
+ def translation_module(syntax)
25
+ case syntax
26
+ when :ovs then Havox::OpenFlow10::OVS
27
+ when :routeflow then Havox::OpenFlow10::RouteFlow
28
+ when :trema then Havox::OpenFlow10::Trema
29
+ else raise_unknown_translator(syntax)
30
+ end
31
+ end
32
+
33
+ def raise_unknown_translator(syntax)
34
+ raise Havox::UnknownTranslator, "Unknown translator '#{syntax}'"
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,20 @@
1
+ module Havox
2
+ class Configuration
3
+ attr_accessor :rf_host, :rf_user, :rf_password, :rf_lxc_names, :merlin_host,
4
+ :merlin_user, :merlin_password, :merlin_path, :gurobi_path,
5
+ :protocol_daemons
6
+
7
+ def initialize
8
+ @rf_host = '192.168.56.101'
9
+ @rf_user = 'routeflow'
10
+ @rf_password = 'routeflow'
11
+ @rf_lxc_names = %w(rfvmA rfvmB rfvmC rfvmD)
12
+ @merlin_host = '192.168.56.102'
13
+ @merlin_user = 'frenetic'
14
+ @merlin_password = 'frenetic'
15
+ @merlin_path = '/home/frenetic/merlin'
16
+ @gurobi_path = '/opt/gurobi701/linux64'
17
+ @protocol_daemons = [:bgpd]
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,70 @@
1
+ module Havox
2
+ module DSL
3
+ class Directive
4
+ attr_reader :switch, :attributes
5
+
6
+ MERLIN_DIC = {
7
+ destination_ip: 'ipDst',
8
+ destination_mac: 'ethDst',
9
+ destination_port: 'tcpDstPort',
10
+ ethernet_type: 'ethTyp',
11
+ in_port: 'port',
12
+ ip_protocol: 'ipProto',
13
+ source_ip: 'ipSrc',
14
+ source_mac: 'ethSrc',
15
+ source_port: 'tcpSrcPort',
16
+ vlan_id: 'vlanId',
17
+ vlan_priority: 'vlanPcp'
18
+ }
19
+
20
+ def initialize(type, switch, attributes = {})
21
+ @type = type
22
+ @switch = switch
23
+ @attributes = attributes
24
+ end
25
+
26
+ def method_missing(name, *args, &block)
27
+ @attributes[name] = args.first
28
+ end
29
+
30
+ def to_block(src_hosts, dst_hosts, qos = nil)
31
+ "#{foreach_code(src_hosts, dst_hosts)}\n #{to_statement(qos)}\n"
32
+ end
33
+
34
+ private
35
+
36
+ def to_statement(qos)
37
+ fields = @attributes.map { |k, v| "#{MERLIN_DIC[k]} = #{treated(v, k)}" }
38
+ predicate = fields.join(' and ')
39
+ qos_str = qos.nil? ? '' : " at #{qos}"
40
+ "#{predicate} -> #{regex_path}#{qos_str};"
41
+ end
42
+
43
+ def format_hosts(host_names)
44
+ "{ #{host_names.join('; ')} }"
45
+ end
46
+
47
+ def foreach_code(src_hosts, dst_hosts)
48
+ src_hosts_str = format_hosts(src_hosts)
49
+ dst_hosts_str = format_hosts(dst_hosts)
50
+ "foreach (s, d): cross(#{src_hosts_str}, #{dst_hosts_str})"
51
+ end
52
+
53
+ def treated(value, field)
54
+ case field
55
+ when :source_ip then netmask_removed(value)
56
+ when :destination_ip then netmask_removed(value)
57
+ else value
58
+ end
59
+ end
60
+
61
+ def netmask_removed(ip)
62
+ IPAddr.new(ip).to_s
63
+ end
64
+
65
+ def regex_path
66
+ ".* #{@switch.to_s}".strip
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,36 @@
1
+ module Havox
2
+ module DSL
3
+ class DirectiveProxy
4
+ def exit(switch, &block)
5
+ eval_directive(:exit, switch, &block)
6
+ end
7
+
8
+ # def drop(&block)
9
+ # eval_directive(:drop, nil, &block)
10
+ # end
11
+
12
+ def topology(file_path)
13
+ raise_invalid_topology(file_path) unless File.exists?(file_path)
14
+ topo = Havox::Topology.new(file_path)
15
+ Havox::Network.topology = topo
16
+ end
17
+
18
+ def associate(router, switch)
19
+ Havox::Network.devices[router.to_s] = switch.to_s
20
+ end
21
+
22
+ private
23
+
24
+ def eval_directive(type, switch, &block)
25
+ directive = Havox::DSL::Directive.new(type, switch)
26
+ directive.instance_eval(&block)
27
+ Havox::Network.directives << directive
28
+ end
29
+
30
+ def raise_invalid_topology(file_path)
31
+ raise Havox::Network::InvalidTopology,
32
+ "invalid topology file '#{file_path}'"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,12 @@
1
+ Havox::Network.define do
2
+ topology 'lib/merlin/topologies/routeflow.dot'
3
+
4
+ exit(:s5) { destination_port 80 }
5
+ exit(:s6) { destination_ip '172.50.0.0/16' }
6
+ exit(:s7) { source_ip '172.70.0.0/16' }
7
+
8
+ exit :s8 do
9
+ destination_port 20
10
+ destination_ip '172.50.0.0/16'
11
+ end
12
+ end
@@ -0,0 +1,64 @@
1
+ module Havox
2
+ module Network
3
+ @directives = []
4
+ @devices = {}
5
+ @rib = nil
6
+ @topology = nil
7
+
8
+ def self.directives; @directives end
9
+ def self.rib; @rib end
10
+ def self.devices; @devices end
11
+ def self.topology; @topology end
12
+ def self.topology=(topo); @topology = topo end
13
+
14
+ def self.define(&block)
15
+ reset!
16
+ directive_proxy = Havox::DSL::DirectiveProxy.new
17
+ directive_proxy.instance_eval(&block)
18
+ @rib = Havox::RIB.new
19
+ infer_associations_by_ospf
20
+ end
21
+
22
+ def self.transcompile(opts = {})
23
+ @directives.map do |d|
24
+ src_hosts = @topology.host_names - @topology.switch_hosts[d.switch.to_s]
25
+ dst_hosts = @topology.switch_hosts[d.switch.to_s]
26
+ d.to_block(src_hosts, dst_hosts, opts[:qos])
27
+ end
28
+ end
29
+
30
+ def self.reachable(protocol = :bgp)
31
+ @rib.nil? ? [] : @rib.network_list(protocol)
32
+ end
33
+
34
+ def self.reset!
35
+ @directives = []
36
+ @devices = {}
37
+ @rib = nil
38
+ @topology = nil
39
+ end
40
+
41
+ class << self
42
+ private
43
+
44
+ def infer_associations_by_ospf
45
+ direct_ospf_routes = @rib.routes.select { |r| r.ospf? && r.direct? }
46
+ grouped_routes = direct_ospf_routes.group_by(&:network)
47
+ @topology.switch_ips.each do |switch_name, switch_ip|
48
+ associate_routers(grouped_routes, switch_name, switch_ip)
49
+ end
50
+ end
51
+
52
+ def associate_routers(grouped_routes, switch_name, switch_ip)
53
+ grouped_routes.each do |network_str, routes|
54
+ network = IPAddr.new(network_str)
55
+ router_name = routes.last.router
56
+ if network.include?(switch_ip) && @devices[router_name].nil?
57
+ @devices[router_name] = switch_name
58
+ break
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,21 @@
1
+ module Havox
2
+ module Merlin
3
+ class ParsingError < StandardError; end
4
+ end
5
+
6
+ module RouteFlow
7
+ class UnknownProtocol < StandardError; end
8
+ end
9
+
10
+ module Trema
11
+ class UnpredictedAction < StandardError; end
12
+ end
13
+
14
+ module Network
15
+ class InvalidTopology < StandardError; end
16
+ end
17
+
18
+ class UnknownTranslator < StandardError; end
19
+ class UnknownAction < StandardError; end
20
+ class OperationError < StandardError; end
21
+ end
@@ -0,0 +1,31 @@
1
+ module Havox
2
+ module Command
3
+ class << self
4
+ def show_ip_route(vm_name, protocol)
5
+ vtysh_run(vm_name, "show ip route #{protocol}")
6
+ end
7
+
8
+ def vtysh_run(vm_name, command)
9
+ rf_command(vm_name, "/usr/bin/vtysh -c '#{command}'")
10
+ end
11
+
12
+ def compile(topology_file, policy_file)
13
+ merlin_command("-topo #{topology_file} #{policy_file} -verbose")
14
+ end
15
+
16
+ private
17
+
18
+ def rf_command(vm_name, command)
19
+ "sudo lxc-attach -n #{vm_name} -- #{command}"
20
+ end
21
+
22
+ def merlin_command(args)
23
+ env_vars = "GUROBI_HOME=\"#{Havox.configuration.gurobi_path}\" " \
24
+ 'PATH="${PATH}:${GUROBI_HOME}/bin" ' \
25
+ 'LD_LIBRARY_PATH="${GUROBI_HOME}/lib" ' \
26
+ 'GRB_LICENSE_FILE="${GUROBI_HOME}/gurobi.lic"'
27
+ "#{env_vars} #{Havox.configuration.merlin_path}/Merlin.native #{args}"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ module Havox
2
+ module FieldParser
3
+ def parsed_ipv4(ip_integer)
4
+ ip_integer = ip_integer.to_i
5
+ value = ip_integer.positive? ? ip_integer : (2**32 - ip_integer.abs) # Handles two's complement integers.
6
+ bits = value.to_s(2).rjust(32, '0') # Transforms the string number into a 32-bit sequence.
7
+ octets = bits.scan(/\d{8}/).map { |octet_bits| octet_bits.to_i(2) } # Splits the sequence into decimal octets.
8
+ octets.join('.') # Returns the joined octets.
9
+ end
10
+
11
+ def basic_action(action, arg_a = nil, arg_b = nil)
12
+ { action: action, arg_a: arg_a, arg_b: arg_b }
13
+ end
14
+
15
+ def raise_unknown_action(obj)
16
+ raise Havox::UnknownAction,
17
+ "Unable to translate action #{obj[:action]} with arguments A:" \
18
+ " #{obj[:arg_a]} and B: #{obj[:arg_b]}, respectively"
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,85 @@
1
+ module Havox
2
+ module Merlin
3
+ class << self
4
+ private
5
+
6
+ ERROR_REGEX = /Uncaught\sexception:\s*(.+)Raised/
7
+ BASENAME_REGEX = /^.+\/(?<name>[^\/]+)$/
8
+ SEPARATOR_REGEX = /On\sswitch\s\d+/
9
+ RULES_BLOCK_REGEX = /(?<=OpenFlow\srules\s)\(\d+\):#{SEPARATOR_REGEX}(.+)(?=Queue\sConfigurations)/
10
+
11
+ def config
12
+ Havox.configuration
13
+ end
14
+
15
+ def cmd
16
+ Havox::Command
17
+ end
18
+
19
+ def ssh_connection
20
+ Net::SSH.start(config.merlin_host, config.merlin_user, password: config.merlin_password) do |ssh|
21
+ yield(ssh)
22
+ end
23
+ end
24
+
25
+ def parse(result)
26
+ result = result.tr("\n", '').tr("\t", ' ') # Removes line break and tab characters.
27
+ check_for_errors(result) # Raises an error if Merlin has errored.
28
+ result = result.scan(RULES_BLOCK_REGEX).flatten.first # Matches OpenFlow rules block.
29
+ return [] if result.nil? # Returns an empty array if no rules were created.
30
+ result = result.split(SEPARATOR_REGEX) # Splits the block into separated rules.
31
+ result.map(&:to_s) # Converts NetSSH special string to string.
32
+ end
33
+
34
+ def check_for_errors(result)
35
+ error_msg = result.scan(ERROR_REGEX).flatten.first
36
+ raise Havox::Merlin::ParsingError, error_msg unless error_msg.nil?
37
+ end
38
+
39
+ def basename(path)
40
+ path.match(BASENAME_REGEX)[:name]
41
+ end
42
+
43
+ def check_options(opts)
44
+ opts[:dst] ||= "#{config.merlin_path}/examples/" # Sets the upload path in Merlin VM.
45
+ opts[:force] ||= false # Forces Havox to ignore field conflicts.
46
+ opts[:basic] ||= false # Instructs to append basic policies.
47
+ opts[:expand] ||= false # Expands raw rules from VLAN-based to full predicates.
48
+ opts[:output] ||= false # Switches Enqueue action for Output action.
49
+ opts[:syntax] ||= :trema # Sets the output syntax for generated rules.
50
+ end
51
+ end
52
+
53
+ def self.run(command)
54
+ output = nil
55
+ ssh_connection { |ssh| output = ssh.exec!(command) }
56
+ output
57
+ end
58
+
59
+ def self.upload!(file, dst = nil)
60
+ dst ||= "#{config.merlin_path}/examples/"
61
+ ssh_connection { |ssh| ssh.scp.upload!(file, dst) }
62
+ true
63
+ end
64
+
65
+ def self.compile(topology_file, policy_file, opts = {})
66
+ rules = []
67
+ result = run(cmd.compile(topology_file, policy_file)) # Runs Merlin in the remote VM and retrieves its output.
68
+ result = parse(result) # Parses the output into raw rules.
69
+ result = Havox::RuleSanitizer.new(result).sanitized_rules # Removes unwanted rule snippets.
70
+ result = Havox::RuleExpander.new(result).expanded_rules if opts[:expand] # Expands each raw rule in the parsed result.
71
+ result.each { |raw_rule| rules << Havox::Rule.new(raw_rule, opts) } # Creates Rule instances for each raw rule.
72
+ rules
73
+ end
74
+
75
+ def self.compile!(topology_file, policy_file, opts = {})
76
+ check_options(opts)
77
+ policy_file = Havox::ModifiedPolicy.new(topology_file, policy_file).path if opts[:basic]
78
+ if upload!(topology_file, opts[:dst]) && upload!(policy_file, opts[:dst])
79
+ topology_file = "#{opts[:dst]}#{basename(topology_file)}"
80
+ policy_file = "#{opts[:dst]}#{basename(policy_file)}"
81
+ compile(topology_file, policy_file, opts)
82
+ end
83
+ end
84
+ end
85
+ end