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,43 @@
1
+ helpers do
2
+ def handle_file(params)
3
+ filepath = "./#{params[:filename]}"
4
+ File.open(filepath, 'w') { |f| f.write(params[:tempfile].read) }
5
+ filepath
6
+ end
7
+
8
+ def run_network(dot_filepath, hvx_filepath, opts = {})
9
+ mln_filename = "./#{File.basename(hvx_filepath, '.hvx')}.mln"
10
+ eval File.read(hvx_filepath)
11
+ mln_blocks = Havox::Network.transcompile(opts)
12
+ print_blocks(mln_blocks, opts)
13
+ return nil if mln_blocks.empty?
14
+ File.open(mln_filename, 'w') { |f| f.write(mln_blocks.join("\n")) }
15
+ mln_filename
16
+ end
17
+
18
+ def print_blocks(merlin_blocks, opts)
19
+ print_opts(opts)
20
+ if merlin_blocks.empty?
21
+ puts 'No Merlin code was generated'.bold.red
22
+ else
23
+ puts 'Merlin code generated:'.bold
24
+ puts merlin_blocks.join("\n").green
25
+ end
26
+ end
27
+
28
+ def print_policy(policy, opts)
29
+ print_opts(opts)
30
+ puts "Generated #{policy.rules.size} rule(s)".bold
31
+ end
32
+
33
+ def print_opts(opts)
34
+ puts 'Havox will:'.bold
35
+ puts "- generate Merlin code with QoS '#{opts[:qos]}'".blue unless opts[:qos].nil?
36
+ puts "- generate Merlin code with preference for exit switches: #{opts[:preferred].join(', ')}".blue unless opts[:preferred].nil?
37
+ puts "- generate Merlin code with arbitrary exit switches: #{opts[:arbitrary].join(', ')}".blue unless opts[:arbitrary].nil?
38
+ puts '- force attribute redefinition for rules with an attribute defined twice'.blue if opts[:force]
39
+ puts '- automatically append policies for ARP and ICMP protocols'.blue if opts[:basic]
40
+ puts '- expand generated rules from VLAN-based to their full predicates'.blue if opts[:expand]
41
+ puts '- switch all occurrences of Enqueue action to Output action'.blue if opts[:output]
42
+ end
43
+ end
@@ -0,0 +1,11 @@
1
+ module Havox
2
+ class Edge
3
+ attr_reader :from, :to, :attributes
4
+
5
+ def initialize(from, to, attributes)
6
+ @from = from
7
+ @to = to
8
+ @attributes = attributes
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,44 @@
1
+ module Havox
2
+ class ModifiedPolicy
3
+ attr_reader :path
4
+
5
+ def initialize(topology_file_path, policy_file_path)
6
+ @topology_file_path = topology_file_path
7
+ @original_policy_file_path = policy_file_path
8
+ @hosts = parsed_hosts
9
+ @path = nil
10
+ append_basic_policies
11
+ end
12
+
13
+ private
14
+
15
+ def arp_policy
16
+ Havox::DSL::Directive.new(nil, nil, ethernet_type: 2054).
17
+ to_block(@hosts, @hosts, 'min(100 Mbps)')
18
+ end
19
+
20
+ def icmp_policy
21
+ Havox::DSL::Directive.new(nil, nil, ethernet_type: 2048, ip_protocol: 1).
22
+ to_block(@hosts, @hosts, 'min(100 Mbps)')
23
+ end
24
+
25
+ def policies
26
+ [icmp_policy, arp_policy]
27
+ end
28
+
29
+ def append_basic_policies
30
+ policy_file_string = File.read(@original_policy_file_path)
31
+ basename = File.basename(@original_policy_file_path, '.mln')
32
+ Tempfile.open([basename, '.mln']) do |tmp|
33
+ tmp.puts "#{policy_file_string}\n"
34
+ tmp.puts policies.join("\n")
35
+ @path = tmp.path
36
+ end
37
+ end
38
+
39
+ def parsed_hosts
40
+ topology = Havox::Topology.new(@topology_file_path)
41
+ topology.host_names
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,19 @@
1
+ module Havox
2
+ class Node
3
+ attr_reader :name, :type, :attributes
4
+
5
+ def initialize(name, attributes)
6
+ @name = name
7
+ @type = attributes[:type]&.to_sym
8
+ @attributes = attributes
9
+ end
10
+
11
+ def host?
12
+ @type.eql?(:host)
13
+ end
14
+
15
+ def switch?
16
+ @type.eql?(:switch)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,68 @@
1
+ module Havox
2
+ class Policy
3
+ attr_reader :rules
4
+
5
+ def initialize(opts = {})
6
+ @opts = opts
7
+ @rules = nil
8
+ generate_rules
9
+ check_ip_netmasks if Havox::Network.reachable.any?
10
+ end
11
+
12
+ def to_json
13
+ @rules.map(&:to_h).to_json
14
+ end
15
+
16
+ private
17
+
18
+ MERLIN_IP_SRC = 'ipSrc'
19
+ MERLIN_IP_DST = 'ipDst'
20
+ MERLIN_ETHERTYPE = 'ethTyp'
21
+ ETHERTYPE_IP = 2048
22
+
23
+ def generate_rules
24
+ @rules = Havox::Merlin.compile!(
25
+ @opts[:merlin_topology],
26
+ @opts[:merlin_policy],
27
+ @opts
28
+ )
29
+ end
30
+
31
+ def check_ip_netmasks
32
+ @rules.each do |r|
33
+ r.matches[src_ip] = netmasked_or_nil(r.matches[src_ip]) if r.matches.key?(src_ip)
34
+ r.matches[dst_ip] = netmasked_or_nil(r.matches[dst_ip]) if r.matches.key?(dst_ip)
35
+ delete_ip_match(r.matches, src_ip) if has_key_but_nil?(r.matches, src_ip)
36
+ delete_ip_match(r.matches, dst_ip) if has_key_but_nil?(r.matches, dst_ip)
37
+ end
38
+ end
39
+
40
+ def src_ip
41
+ Havox::Translator.instance.fields_to(@opts[:syntax])[MERLIN_IP_SRC]
42
+ end
43
+
44
+ def dst_ip
45
+ Havox::Translator.instance.fields_to(@opts[:syntax])[MERLIN_IP_DST]
46
+ end
47
+
48
+ def ethertype
49
+ Havox::Translator.instance.fields_to(@opts[:syntax])[MERLIN_ETHERTYPE]
50
+ end
51
+
52
+ def delete_ip_match(matches, target_ip_key)
53
+ matches.delete(target_ip_key)
54
+ matches[ethertype] = ETHERTYPE_IP unless matches.key?(ethertype)
55
+ end
56
+
57
+ def netmasked_or_nil(ip)
58
+ Havox::Network.reachable.each do |network|
59
+ return network if IPAddr.new(network).include?(ip)
60
+ end
61
+ nil
62
+ end
63
+
64
+ def has_key_but_nil?(matches, target_key)
65
+ matches.key?(target_key) && matches[target_key].nil?
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,30 @@
1
+ module Havox
2
+ class RIB
3
+ attr_reader :routes
4
+
5
+ def initialize(opts = {})
6
+ @opts = opts
7
+ @routes = Havox::RouteFlow.ribs(vm_names, @opts)
8
+ end
9
+
10
+ def routes_to(ip, protocol = :bgp)
11
+ @routes.select do |r|
12
+ r.protocol.eql?(protocol) && IPAddr.new(r.network).include?(ip)
13
+ end
14
+ end
15
+
16
+ def network_list(protocol = :bgp)
17
+ @routes.select { |r| r.protocol.eql?(protocol) }.map(&:network).uniq
18
+ end
19
+
20
+ private
21
+
22
+ def vm_names
23
+ case @opts[:vm_names]
24
+ when Array then @opts[:vm_names]
25
+ when String then @opts[:vm_names].split(',').map(&:strip)
26
+ else Havox.configuration.rf_lxc_names
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,82 @@
1
+ module Havox
2
+ class Route
3
+ attr_reader :network, :via, :interface, :protocol, :best, :fib, :raw,
4
+ :timestamp, :router, :recursive_via
5
+
6
+ IP_REGEX = /([0-9]{1,3}\.){3}[0-9]{1,3}/
7
+ TYPE_CHAR_REGEX = /^[\w\s]/
8
+
9
+ ROUTE_REGEX = %r(^
10
+ (?<flags>[A-Z>\*\s]{3})\s*
11
+ (?<network>#{IP_REGEX}\/[0-9]{1,2})?\s
12
+ (\[\d*\/\d*\])?\s?
13
+ (via\s(?<via>([0-9]{1,3}\.){3}[0-9]{1,3})|is\sdirectly\sconnected)
14
+ (,\s(?<interface>\w+)|\s\(recursive\svia\s(?<recursive_via>#{IP_REGEX})\))
15
+ (,\s(?<timestamp>\d\d:\d\d:\d\d))?
16
+ )x
17
+
18
+ TYPE_HASH = {
19
+ 'O' => :ospf, 'B' => :bgp, 'C' => :connected, 'S' => :static,
20
+ 'R' => :rip, 'I' => :isis, 'A' => :babel, 'K' => :kernel
21
+ }
22
+
23
+ def initialize(raw, router, opts = {})
24
+ @router = router
25
+ @opts = opts
26
+ @raw = raw
27
+ parse_raw_entry
28
+ end
29
+
30
+ def to_s
31
+ connection = @via.nil? ? 'direct' : "via #{@via}"
32
+ "#{@protocol.upcase}: to #{@network} #{connection} in " \
33
+ "#{@interface || @recursive_via}#{', BEST' if @best}" \
34
+ "#{', FIB route' if @fib}"
35
+ end
36
+
37
+ def inspect
38
+ "Route #{object_id.to_s(16)}, router #{@router}, #{to_s}"
39
+ end
40
+
41
+ def to_h
42
+ { router: @router, protocol: @protocol, network: @network, via: @via,
43
+ recursive_via: @recursive_via, interface: @interface,
44
+ timestamp: @timestamp, best: @best, fib: @fib }
45
+ end
46
+
47
+ def direct?
48
+ @via.nil?
49
+ end
50
+
51
+ def bgp?
52
+ @protocol.eql?(:bgp)
53
+ end
54
+
55
+ def ospf?
56
+ @protocol.eql?(:ospf)
57
+ end
58
+
59
+ private
60
+
61
+ def parse_raw_entry
62
+ parsed_entry = @raw.match(ROUTE_REGEX)
63
+ evaluate_protocol(parsed_entry[:flags])
64
+ evaluate_route_attributes(parsed_entry)
65
+ end
66
+
67
+ def evaluate_protocol(flags_str)
68
+ type_char = flags_str.scan(TYPE_CHAR_REGEX).first
69
+ @protocol = TYPE_HASH[type_char] || :unknown
70
+ end
71
+
72
+ def evaluate_route_attributes(parsed_entry)
73
+ @network = parsed_entry[:network]&.to_s
74
+ @via = parsed_entry[:via]&.to_s
75
+ @interface = parsed_entry[:interface]&.to_s
76
+ @timestamp = parsed_entry[:timestamp]&.to_s
77
+ @best = parsed_entry[:flags].include?('>')
78
+ @fib = parsed_entry[:flags].include?('*')
79
+ @recursive_via = parsed_entry[:recursive_via]&.to_s
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,43 @@
1
+ module Havox
2
+ class RouteFiller
3
+ attr_reader :filled_routes
4
+
5
+ ROUTE_REGEX = %r(^
6
+ (?<protocol_char>[A-Z\s]{1})[>\s]{1}[\*\s]{1}\s
7
+ (?<network>([0-9]{1,3}\.){3}[0-9]{1,3}\/[0-9]{1,2})?.+$
8
+ )x
9
+
10
+ def initialize(raw_routes)
11
+ @raw_routes = raw_routes
12
+ @filled_routes = []
13
+ fill_routes
14
+ end
15
+
16
+ private
17
+
18
+ def fill_routes
19
+ pivot = nil # Sets null as initial value for the pivot, since the first route is always OK.
20
+ @raw_routes.each do |raw_route|
21
+ match = raw_route.match(ROUTE_REGEX) # Tries to match the current route to the regex.
22
+ if match[:network].nil? # If the network match is null, the current route must be treated:
23
+ @filled_routes << filled_route(raw_route, pivot) # -- Treats the current route and adds it to the returning array.
24
+ else # Otherwise, the current route is OK:
25
+ pivot = raw_route # -- Sets the current route as the new pivot.
26
+ @filled_routes << raw_route # -- Adds the current route to the returning array.
27
+ end
28
+ end
29
+ end
30
+
31
+ def filled_route(raw_route, pivot)
32
+ match = pivot.match(ROUTE_REGEX) # Extracts protocol char and network from the pivot.
33
+ network_str = "#{match[:protocol_char]} * #{match[:network]} [110/10] " # Builds the substitution string containing the char and the network.
34
+ treated_route = raw_route.gsub(/\s{2}\*\s+(?=via|is)/, network_str) # Fits the substitution string to the correct place.
35
+ raise_error(raw_route) if treated_route.eql?(raw_route) # Raises an error if the treated route is equal to its initial state.
36
+ treated_route
37
+ end
38
+
39
+ def raise_error(raw_route)
40
+ raise Havox::OperationError, "Failed to fill route '#{raw_route}'"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,74 @@
1
+ module Havox
2
+ class Rule
3
+ attr_reader :matches, :actions, :dp_id, :raw
4
+
5
+ def initialize(raw, opts = {})
6
+ @opts = opts
7
+ @syntax = @opts[:syntax] || :trema
8
+ @matches = parsed_matches(raw)
9
+ @actions = parsed_actions(raw)
10
+ @dp_id = @matches[:dp_id].to_i
11
+ @matches.delete(:dp_id)
12
+ @raw = raw.strip
13
+ end
14
+
15
+ def to_s # Stringifies the rule.
16
+ sep = ' AND '
17
+ matches_str = @matches.map { |k, v| "#{k.to_s} = #{v.to_s}" }.join(sep)
18
+ actions_str = @actions.map do |o|
19
+ %Q(#{o[:action]}(#{o[:arg_a]}#{", #{o[:arg_b]}" unless o[:arg_b].nil?}))
20
+ end
21
+ "#{matches_str} --> #{actions_str.join(sep)}"
22
+ end
23
+
24
+ def inspect
25
+ "Rule #{object_id.to_s(16)}, dp_id = #{@dp_id}: #{to_s}" # Prints the rule when debugging or array listing.
26
+ end
27
+
28
+ def to_h
29
+ { dp_id: @dp_id, matches: @matches, actions: @actions }
30
+ end
31
+
32
+ private
33
+
34
+ def parsed_matches(raw_rule)
35
+ ok_matches = {}
36
+ raw_matches = raw_rule.split('->').first.split('and') # Parses each match field in the 1st part.
37
+ raw_matches = raw_matches.map { |str| str.tr('()*', '').strip } # Removes unwanted characters.
38
+ raw_matches = raw_matches.reject(&:empty?) # Rejects resulting empty match fields.
39
+ raw_matches.each do |raw_match| # Parses and adds each match based on the dictionary.
40
+ stmt = raw_match.split(/\s?=\s?/) # Splits the statement by '='.
41
+ field = translate.fields_to(@syntax)[stmt.first] # Gets the right field by the raw field name.
42
+ ok_matches[field] = stmt.last unless already_set?(ok_matches, stmt)
43
+ end
44
+ translate.matches_to(@syntax, ok_matches)
45
+ end
46
+
47
+ def parsed_actions(raw_rule)
48
+ ok_actions = []
49
+ raw_actions = raw_rule.split('->').last.strip # Parses the actions in the 2nd part.
50
+ raw_actions = raw_actions.split(/(?<=\))\s+(?=\w)/) # Splits the string into single raw actions.
51
+ raw_actions.each do |raw_action|
52
+ regex = /(?<action>\w+)\((?<arg_a>[\w<>]+)[,\s]*(?<arg_b>[\w<>]*)\)/ # Matches raw actions in the format 'Action(x, y)'.
53
+ ok_actions << hashed(raw_action.match(regex)) # Adds the structured action to the returning array.
54
+ end
55
+ translate.actions_to(@syntax, ok_actions, @opts)
56
+ end
57
+
58
+ def hashed(match_data)
59
+ Hash[match_data.names.map(&:to_sym).zip(match_data.captures)] # Converts the match data to a hash object.
60
+ end
61
+
62
+ def translate
63
+ Havox::Translator.instance
64
+ end
65
+
66
+ def already_set?(matches_hash, stmt)
67
+ field = translate.fields_to(@syntax)[stmt.first]
68
+ unless matches_hash[field].nil? || matches_hash[field].eql?(stmt.last)
69
+ return !@opts[:force]
70
+ end
71
+ false
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,47 @@
1
+ module Havox
2
+ class RuleExpander
3
+ attr_reader :expanded_rules
4
+
5
+ SET_VLAN_REGEX = /SetField\(vlan,\s?(<none>|\d+)\)/
6
+ SET_VLAN_ID_REGEX = /SetField\(vlan,\s?(?<vlan_id>\d+)\)/
7
+ VLAN_MATCH_REGEX = /vlanId\s?=\s?(?<vlan_id>\d+)/
8
+ PREDICATE_REGEX = /\(switch\s?=\s?\d+\s?and(?<pred>.+)\)/
9
+
10
+ def initialize(raw_rules)
11
+ @raw_rules = raw_rules
12
+ @vlan_predicates = {}
13
+ @expanded_rules = []
14
+ scan_vlan_predicates
15
+ expand
16
+ end
17
+
18
+ private
19
+
20
+ def expand
21
+ @raw_rules.each do |raw_rule|
22
+ rule_str = raw_rule.gsub(SET_VLAN_REGEX, '') # Removes any 'SetField(vlan, x)' pattern, x being a number or '<none>'.
23
+ @expanded_rules << sub_vlan_predicates(rule_str) # Substitutes 'vlanId = x' pattern for the full predicate.
24
+ end
25
+ end
26
+
27
+ def sub_vlan_predicates(raw_rule)
28
+ match_data = raw_rule.match(VLAN_MATCH_REGEX) # Matches 'vlanId = x' pattern.
29
+ unless match_data.nil?
30
+ vlan_id = match_data[:vlan_id]
31
+ return raw_rule.gsub(VLAN_MATCH_REGEX, @vlan_predicates[vlan_id]) # Returns the substitution of the pattern for the full predicate.
32
+ end
33
+ raw_rule # Returns the raw rule unmodified if there are no match pattern.
34
+ end
35
+
36
+ def scan_vlan_predicates
37
+ @raw_rules.each do |raw_rule|
38
+ match_data = raw_rule.match(SET_VLAN_ID_REGEX) # Matches 'SetField(vlan, x)' pattern, x being a number only.
39
+ unless match_data.nil?
40
+ vlan_id = match_data[:vlan_id]
41
+ predicate = raw_rule.split('->').first.match(PREDICATE_REGEX)[:pred] # Takes the predicate of the rule without the 'switch = x' substring.
42
+ @vlan_predicates[vlan_id] = predicate # Associates the predicate to its VLAN id.
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end