havox 0.11.1

Sign up to get free protection for your applications and to get access to all the features.
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