├── .document ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── Makefile ├── README.md ├── Rakefile ├── classbench ├── lib ├── classbench.rb └── classbench │ ├── .gitignore │ ├── analyser.rb │ ├── generator.rb │ ├── port_class.rb │ ├── rule.rb │ ├── trie.rb │ └── trie_node.rb ├── patches └── ipv6.patch └── vendor └── Makefile /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # rcov generated 2 | coverage 3 | coverage.data 4 | 5 | # rdoc generated 6 | rdoc 7 | 8 | # yard generated 9 | doc 10 | .yardoc 11 | 12 | # bundler 13 | .bundle 14 | 15 | # jeweler generated 16 | pkg 17 | 18 | 19 | 20 | other 21 | resources 22 | NOTES* 23 | .idea 24 | .DS_Store 25 | vendor/db_generator 26 | vendor/db_generator-* 27 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Add dependencies to develop your gem here. 4 | # Include everything needed to run rake, tests, features, etc. 5 | group :development do 6 | gem "bundler" #, "~> 1.0" 7 | end 8 | 9 | gem "docopt" 10 | gem "ruby-ip" 11 | gem "open4" 12 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | docopt (0.5.0) 5 | open4 (1.3.4) 6 | ruby-ip (0.9.3) 7 | 8 | PLATFORMS 9 | ruby 10 | 11 | DEPENDENCIES 12 | bundler 13 | docopt 14 | open4 15 | ruby-ip 16 | 17 | BUNDLED WITH 18 | 1.10.6 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Recurse into vendor and run Makefile 3 | # (downloads, patches and compiles ClassBench) 4 | all: 5 | make -C vendor all 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Classbench 2 | 3 | Utility for generation of firewall/OpenFlow rules based on original (no longer maintained) [Classbench](http://www.arl.wustl.edu/classbench/). 4 | 5 | ## Requirements 6 | - Ruby 1.9.3+ 7 | - RubyGems 8 | 9 | ``` 10 | sudo gem install open4 ruby-ip docopt ipaddress 11 | ``` 12 | ## Installation 13 | ``` 14 | git clone https://github.com/lucansky/classbench-ng.git 15 | make # Downloads, patches and compiles db_generator in ./vendor/db_generator/db_generator 16 | ``` 17 | 18 | ### Patching classbench 19 | Due to statically initialized arrays in ClassBench, patching is required which increases the limit. 20 | Patch is automatically applied by make in process of downloading ClassBench. 21 | (see vendor/Makefile) 22 | 23 | ## Usage 24 | ``` 25 | ./classbench analyse FILE 26 | ``` 27 | Analyses file, expecting FILE to be ovs-ofctl dump. 28 | Fields extracted from dump are: 29 | - dl_dst, dl_src, dl_type, dl_vlan, dl_vlan_pcp, 30 | - eth_type, in_port, 31 | - nw_dst, nw_proto, nw_src, nw_tos, 32 | - tp_dst, tp_src 33 | 34 | Output's original Classbench seed with openflow YAML structure as last section. 35 | 36 | ``` 37 | ./classbench generate v4 SEED [--count=100] [--db-generator=] 38 | ``` 39 | Generates --count of OpenFlow rules. 40 | If seed without OpenFlow section is provided, regular 5-tuples are generated. 41 | Output format is "attribute=value", joined by ", ". 42 | 43 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rubygems' 4 | require 'bundler' 5 | begin 6 | Bundler.setup(:default, :development) 7 | rescue Bundler::BundlerError => e 8 | $stderr.puts e.message 9 | $stderr.puts "Run `bundle install` to install missing gems" 10 | exit e.status_code 11 | end 12 | require 'rake' 13 | 14 | -------------------------------------------------------------------------------- /classbench: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "lib/classbench" 4 | 5 | require "pp" 6 | require "docopt" 7 | doc = < [--count=] [--db-generator=] 14 | #{__FILE__} generate v6 [--count=] 15 | #{__FILE__} -h | --help 16 | #{__FILE__} version 17 | 18 | Options: 19 | --db-generator= Path to binary of original db_generator [default: ./vendor/db_generator/db_generator] 20 | --count= Count of rules to generate [default: 100] 21 | -h --help Show this screen. 22 | 23 | Analyser accept's as input ovs-ofctl dump. 24 | Fields extracted from dump are: 25 | - dl_dst, dl_src, dl_type, (dl_vlan, dl_vlan_pcp,) 26 | - eth_type, in_port, 27 | - nw_dst, nw_proto, nw_src, nw_tos, 28 | - tp_dst, tp_src 29 | Output is original Classbench seed 30 | with openflow YAML structure as last section. 31 | 32 | Generator accept's Classbench seed with openflow section. 33 | Output's one rule per line in format "attribute=value", joined by ", ". 34 | 35 | DOCOPT 36 | 37 | begin 38 | opts = Docopt::docopt(doc) 39 | if opts["analyse"] 40 | Classbench::analyse(opts["FILE"]) 41 | elsif opts["generate"] 42 | #pp opts 43 | Classbench::generate(opts[""], (opts["--count"].to_i), opts["--db-generator"]) 44 | elsif opts["version"] 45 | puts "Version: #{Classbench::VERSION}" 46 | end 47 | # TODO: --version 48 | 49 | rescue Docopt::Exit => e 50 | STDERR.puts e.message 51 | end 52 | -------------------------------------------------------------------------------- /lib/classbench.rb: -------------------------------------------------------------------------------- 1 | require_relative 'classbench/analyser' 2 | require_relative 'classbench/generator' 3 | require_relative 'classbench/port_class' 4 | require_relative 'classbench/rule' 5 | require_relative 'classbench/trie' 6 | require_relative 'classbench/trie_node' 7 | 8 | require 'ip' # ruby-ip gem 9 | require 'pp' 10 | 11 | module Classbench 12 | VERSION = '0.1.2' 13 | 14 | def self.generate_prefix 15 | len = rand(33) 16 | 17 | (0...len).map { [0,1][rand(2)]}.join 18 | end 19 | 20 | def self.ip_to_binary_string(ip) 21 | ip = IP.new(ip) 22 | ip.to_b.to_s[0,ip.pfxlen] 23 | end 24 | 25 | def self.load_prefixes_from_file(filename) 26 | t = Trie.new 27 | 28 | prefixes = File.readlines(filename).map(&:chomp) 29 | prefixes.each do |pfx| 30 | t.insert ip_to_binary_string(pfx) 31 | end 32 | t 33 | end 34 | 35 | def self.analyse(filename) 36 | analyser = Analyser.new 37 | analyser.parse_openflow(File.read(filename)) 38 | 39 | analyser.calculate_stats 40 | 41 | puts analyser.generate_seed 42 | 43 | end 44 | 45 | def self.generate(filename, count, db_generator_path) 46 | generator = Generator.new(filename, db_generator_path) 47 | has_openflow = generator.parse_seed 48 | 49 | #puts YAML.dump(generator.openflow_section) 50 | rules = generator.generate_rules(count) 51 | if has_openflow 52 | rules.map!(&:to_vswitch_format) 53 | end 54 | 55 | puts rules 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /lib/classbench/.gitignore: -------------------------------------------------------------------------------- 1 | db_generator 2 | -------------------------------------------------------------------------------- /lib/classbench/analyser.rb: -------------------------------------------------------------------------------- 1 | require "base64" 2 | require "yaml" 3 | 4 | module Classbench 5 | class Analyser 6 | INTERESTING_ATTRIBUTES = %w(dl_dst dl_src dl_type dl_vlan dl_pcp eth_type in_port nw_dst nw_proto nw_src nw_tos tp_dst tp_src) 7 | 8 | attr_accessor :rules 9 | attr_accessor :protocol_port_class_stats 10 | attr_accessor :port_class_prefix_lengths 11 | attr_accessor :omitted_rules_count 12 | 13 | def initialize 14 | self.rules ||= [] 15 | self.omitted_rules_count = 0 16 | end 17 | 18 | def parse_openflow(lines) 19 | lines.split("\n").each do |line| 20 | # Array of arrays ... [["dl_dst", "fa:16:3e:91:c3:01", ","], ["nw_src", "255.255.255.255", ","], 21 | attributes = line.scan(/([a-z_\-]+)=([A-Za-z0-9\-_:\.\/]+)(,|\w)?/). 22 | keep_if {|a| INTERESTING_ATTRIBUTES.include?(a.first) } 23 | 24 | if attributes.empty? 25 | self.omitted_rules_count += 1 26 | next 27 | end 28 | 29 | rule = {} 30 | attributes.each do |attr| 31 | if INTERESTING_ATTRIBUTES.include? attr.first 32 | rule[attr.first] = attr[1] 33 | end 34 | end 35 | self.rules << Rule.new(rule) 36 | end 37 | end 38 | 39 | def rules_per_port_class(class_name) 40 | self.port_class_prefix_lengths[class_name].values.map(&:values).flatten.inject(&:+) 41 | end 42 | 43 | def generate_seed 44 | calculate_stats 45 | 46 | seed = "" 47 | seed += "-scale\n#{rules.size}\n#\n" 48 | 49 | seed += "-prots\n" 50 | 51 | #puts rules.count 52 | protocol_port_class_stats.each do |protocol_number, port_classes| 53 | protocol_probability = port_classes.values.inject(&:+) / rules.count.to_f 54 | #p port_classes 55 | seed += "#{protocol_number}\t#{protocol_probability}" 56 | 57 | protocol_rule_count = port_classes.values.inject(&:+) 58 | 59 | 0.upto(24).each do |i| 60 | class_name = PortClass::CLASS_NAMES[i] 61 | probability_of_port_class = (port_classes[class_name]||0)/protocol_rule_count.to_f 62 | seed += "\t#{probability_of_port_class}" 63 | end 64 | seed += "\n" 65 | end 66 | seed += "#\n" 67 | 68 | seed += "-flags\n" 69 | protocol_port_class_stats.each do |protocol_number, port_classes| 70 | seed += "#{protocol_number}\t0x0000/0x0000,1.00000000\t\n" 71 | end 72 | seed += "#\n" 73 | 74 | seed += "-extra\n0\n#\n" 75 | 76 | seed += generate_range_probability("src","ar") 77 | seed += generate_range_probability("src","em") 78 | seed += generate_range_probability("dst","ar") 79 | seed += generate_range_probability("dst","em") 80 | 81 | PortClass::CLASS_NAMES.each do |class_name| 82 | seed += "-#{class_name.downcase.gsub('/', '_')}" 83 | 84 | rules_of_port_class = rules_per_port_class(class_name) 85 | 86 | # Foreach distinct total length 87 | self.port_class_prefix_lengths[class_name].each do |total_length, partial_lengths| 88 | count_of_rules_in_total_length = partial_lengths.values.inject(&:+) 89 | seed += "\n#{total_length},#{count_of_rules_in_total_length/rules_of_port_class.to_f}\t" 90 | 91 | # Foreach source length 92 | partial_lengths.each do |length, count| 93 | seed += "\t#{length},#{count/count_of_rules_in_total_length.to_f}" 94 | end 95 | end 96 | seed += "\n#\n" 97 | end 98 | 99 | nw_src_trie = Trie.new 100 | rules.map {|r| r.attributes["nw_src"]}.compact.each do |ip| 101 | nw_src_trie.insert Classbench::ip_to_binary_string(ip) 102 | end 103 | 104 | nw_src_stats = nw_src_trie.get_stats 105 | 106 | seed += "-snest\n#{nw_src_stats.classbench.prefix_nesting}\n#\n" 107 | 108 | seed += "-sskew\n" 109 | seed += nw_src_trie.get_stats.classbench_stats 110 | seed += "#\n" 111 | 112 | nw_dst_trie = Trie.new 113 | rules.map {|r| r.attributes["nw_dst"]}.compact.each do |ip| 114 | nw_dst_trie.insert Classbench::ip_to_binary_string(ip) 115 | end 116 | 117 | nw_dst_stats = nw_dst_trie.get_stats 118 | 119 | seed += "-dnest\n#{nw_dst_stats.classbench.prefix_nesting}\n#\n" 120 | 121 | seed += "-dskew\n" 122 | seed += nw_dst_trie.get_stats.classbench_stats 123 | seed += "#\n" 124 | 125 | #pp nw_dst_trie.get_stats 126 | seed += "-pcorr\n" 127 | 1.upto(32).each do |i| 128 | seed += "#{i}\t0.0\n" 129 | end 130 | 131 | seed += "#\n" 132 | 133 | # Openflow 134 | seed += "-openflow\n" 135 | 136 | seed += YAML.dump(openflow_stats) 137 | seed += "#\n" 138 | 139 | seed 140 | end 141 | 142 | # Direction: "src" or "dst" 143 | # port_class: "em" or "ar" 144 | def generate_range_probability(direction, port_class) 145 | seed = "" 146 | if direction == "src" 147 | seed += "-sp#{port_class}\n" 148 | else 149 | seed += "-dp#{port_class}\n" 150 | end 151 | 152 | accumulator = {} 153 | rules.each do |r| 154 | rule_port_class = (direction == "src") ? r.src_port_range_group : r.dst_port_range_group 155 | 156 | if rule_port_class == port_class.to_sym 157 | range = r.attributes["tp_#{direction}"] 158 | accumulator[range] ||= 0 159 | accumulator[range] += 1 160 | end 161 | end 162 | 163 | total = accumulator.values.inject(&:+) 164 | accumulator.each do |range, count| 165 | seed += "#{count/total.to_f}\t#{range.first}:#{range.last}\n" 166 | #p [count, range] 167 | end 168 | seed += "#\n" 169 | end 170 | 171 | def calculate_stats 172 | 173 | # Port class statistics 174 | self.protocol_port_class_stats = {} 175 | rules.each do |r| 176 | protocol_port_class_stats[ r.protocol ] ||= {} 177 | protocol_port_class_stats[ r.protocol ][r.port_class_name] ||= 0 178 | protocol_port_class_stats[ r.protocol ][r.port_class_name] += 1 179 | end 180 | 181 | # Prefix lengths of dst/src address based on port class 182 | self.port_class_prefix_lengths = {} 183 | 184 | # Prefill port_class_prefix_lengths with empty hashes 185 | PortClass::CLASS_NAMES.each {|cn| self.port_class_prefix_lengths[cn] = {}} 186 | 187 | rules.each do |r| 188 | lengths_of_class = port_class_prefix_lengths[r.port_class_name] 189 | 190 | specific_length = lengths_of_class[r.src_length + r.dst_length] ||= {} 191 | specific_length[r.dst_length] = (specific_length[r.dst_length] || 0) + 1 192 | end 193 | end 194 | 195 | def openflow_stats 196 | {"in_port" => in_ports, 197 | "eth_type" => eth_types, 198 | "dl_src" => vendors("src"), 199 | "dl_dst" => vendors("dst"), 200 | "unique_vlan_ids_count" => unique_vlans_count, 201 | "empty_rules_count" => omitted_rules_count, 202 | "rule_distribution" => occurences_of_rule_types} 203 | end 204 | 205 | # Returns hash with keys MAC address (only vendor part) and values as count 206 | def vendors(type) 207 | l2_vendors = rules.map {|r| r.l2_vendor(type)}.compact 208 | 209 | l2_vendors.each_with_object(Hash.new(0)) { |v,counts| counts[v] += 1 } 210 | end 211 | 212 | def in_ports 213 | ports = rules.map {|r| r.attributes["in_port"]}.compact 214 | ports.each_with_object(Hash.new(0)) { |port,counts| counts[port] += 1 } 215 | end 216 | 217 | def eth_types 218 | eth_types = rules.map {|r| r.attributes["eth_type"]}.compact 219 | eth_types.each_with_object(Hash.new(0)) { |eth_type,counts| counts[eth_type] += 1 } 220 | end 221 | 222 | def unique_vlans_count 223 | rules.map {|r| r.attributes["dl_vlan"]}.compact.uniq.count 224 | end 225 | 226 | def occurences_of_rule_types 227 | rules.each_with_object(Hash.new(0)) { |rule,counts| counts[rule.fields.sort] += 1 }.to_a.map {|a| {"attributes" => a[0], "count" => a[1]}} 228 | end 229 | 230 | def generate_rule 231 | random_rule = rules[rand(rules.size)] 232 | random_rule.fields 233 | 234 | #1. determine its OF type 235 | #2. remove 5-tuple header fields that ARE NOT defined by the OF type 236 | #3. add OF-specific header fields that ARE defined by the OF type 237 | # 1. if the field is ingress port, Ethernet type or IP protocol, 238 | # use specified dependencies (see shared Google document) to constrain a set 239 | # of possible values for this field 240 | # 2. generate value for the OF-specific header field in a 241 | # specified way (see shared Google document) 242 | #4. Label all the header fields by the corresponding OF name 243 | end 244 | end 245 | end 246 | -------------------------------------------------------------------------------- /lib/classbench/generator.rb: -------------------------------------------------------------------------------- 1 | require 'tempfile' 2 | require 'pp' 3 | require 'open4' 4 | 5 | module Classbench 6 | class Generator 7 | attr_accessor :openflow_section 8 | 9 | # Rules generated by classbench 10 | attr_accessor :classbench_rules 11 | 12 | # Full path to seed, needs to be stored for later generation 13 | attr_accessor :seed_path 14 | 15 | attr_accessor :raw_rules 16 | 17 | # Array of arrays containing footprint for OpenFlow rule, 18 | # as if they were from original set, but without values 19 | # Ex: [["tp_dst"], ["nw_src", "nw_dst"] 20 | attr_accessor :pregenerated_rule_types 21 | 22 | attr_accessor :pregenerated_dl_srcs 23 | attr_accessor :pregenerated_dl_dsts 24 | attr_accessor :pregenerated_in_ports 25 | attr_accessor :pregenerated_eth_types 26 | 27 | attr_accessor :db_generator_path 28 | 29 | # All possible values in field nw_tos from DSCP Pool 1 30 | # https://www.iana.org/assignments/dscp-registry/dscp-registry.xhtml 31 | attr_accessor :nw_tos_values 32 | 33 | # Pregenerated possible VLAN values. First are [10,20,30..4090], followed by all other vlan IDs. 34 | # If seed contains 4 unique vlan IDs, then only IDs 10, 20, 30 or 40 will be used in generated rules. 35 | attr_accessor :vlan_pool 36 | 37 | def initialize(filename, db_generator_path) 38 | self.seed_path = filename 39 | self.classbench_rules = [] 40 | self.db_generator_path = db_generator_path 41 | 42 | self.nw_tos_values = [0, 8, 16, 24, 32, 40, 48, 56, 10, 12, 14, 18, 20, 22, 26, 28, 30, 34, 36, 38, 46, 46] 43 | self.vlan_pool = ( (1..4094).select {|x,y| x % 10 == 0 } + (1..4094).to_a ).uniq 44 | end 45 | 46 | def parse_seed 47 | begin 48 | openflow_raw = File.read(self.seed_path).match(/openflow\n(.*)#/mi).captures.first 49 | self.openflow_section = YAML.load(openflow_raw) 50 | #pp self.openflow_section 51 | 52 | pregenerate_rule_types 53 | pregenerate_dl 54 | pregenerate_in_ports 55 | pregenerate_eth_types 56 | 57 | #p self.openflow_section 58 | 59 | rescue NoMethodError 60 | STDERR.puts "No openflow section found in seed." 61 | return false 62 | end 63 | 64 | true 65 | end 66 | 67 | ########################## 68 | 69 | def pregenerate_rule_types 70 | self.pregenerated_rule_types = [] 71 | 72 | self.openflow_section["rule_distribution"].each do |rule_count| 73 | rule_count["count"].times do 74 | self.pregenerated_rule_types << rule_count["attributes"] 75 | end 76 | end 77 | end 78 | 79 | # TODO: Refactor, nasty repetition. Probably will make it unclear. 80 | 81 | def pregenerate_dl 82 | self.pregenerated_dl_srcs = [] 83 | self.openflow_section["dl_src"].each do |vendor, count| 84 | count.to_i.times do 85 | self.pregenerated_dl_srcs << vendor 86 | end 87 | end 88 | 89 | self.pregenerated_dl_dsts = [] 90 | self.openflow_section["dl_dst"].each do |vendor, count| 91 | count.to_i.times do 92 | self.pregenerated_dl_dsts << vendor 93 | end 94 | end 95 | end 96 | 97 | def pregenerate_in_ports 98 | self.pregenerated_in_ports = [] 99 | self.openflow_section["in_port"].each do |port, count| 100 | count.to_i.times do 101 | self.pregenerated_in_ports << port 102 | end 103 | end 104 | end 105 | 106 | def pregenerate_eth_types 107 | self.pregenerated_eth_types = [] 108 | self.openflow_section["eth_type"].each do |eth_type, count| 109 | count.to_i.times do 110 | self.pregenerated_eth_types << eth_type 111 | end 112 | end 113 | end 114 | 115 | ########################## 116 | def generate_classbench_rules(count) 117 | current_dir = File.dirname(__FILE__) 118 | tmp_filters = Tempfile.new('filters') 119 | 120 | # db_generator -c filename #{count} 0 0 0 tmp/#{rand} 121 | # Call classbench 122 | #system(current_dir+"/db_generator", "-c", self.seed_path, count.to_s, "0", "0", "0", tmp_filters.path, " > /dev/null") 123 | pid, stdin, stdout, stderr = Open4::popen4(self.db_generator_path, "-c", self.seed_path, count.to_s, "0", "0", "0", tmp_filters.path) 124 | ignored, status = Process::waitpid2 pid 125 | 126 | #STDERR.puts "done" 127 | #puts status 128 | #puts $? 129 | 130 | self.raw_rules = File.readlines(tmp_filters.path) 131 | self.raw_rules.each do |classbench_line| 132 | self.classbench_rules << Rule.from_classbench_format(classbench_line) 133 | end 134 | 135 | return raw_rules 136 | end 137 | 138 | def generate_rules(count) 139 | generate_classbench_rules(count) 140 | 141 | if not self.openflow_section 142 | return self.raw_rules 143 | end 144 | 145 | return classbench_rules.map do |rule| 146 | random_openflow_type = pregenerated_rule_types.sample 147 | rule.remove_missing_attributes(random_openflow_type) 148 | 149 | #p random_openflow_type 150 | random_openflow_type.each do |attribute| 151 | if not rule.attributes.include?(attribute) 152 | #puts "Fill #{attribute}" 153 | 154 | if attribute == "in_port" 155 | rule.attributes["in_port"] = pregenerated_in_ports.sample 156 | end 157 | 158 | if attribute == "eth_type" 159 | rule.attributes["eth_type"] = pregenerated_eth_types.sample 160 | end 161 | 162 | random_device_mac = (1..3).collect { "%02x" % [rand(255)] }.join(":") 163 | if attribute == "dl_dst" 164 | random_vendor = pregenerated_dl_dsts.sample 165 | rule.attributes["dl_dst"] = random_vendor + ":" + random_device_mac 166 | end 167 | 168 | if attribute == "dl_src" 169 | random_vendor = pregenerated_dl_srcs.sample 170 | rule.attributes["dl_src"] = random_vendor + ":" + random_device_mac 171 | end 172 | 173 | if attribute == "dl_vlan" 174 | random_vlan_id_position = rand(self.openflow_section["unique_vlan_ids_count"].to_i) 175 | rule.attributes["dl_vlan"] = self.vlan_pool[random_vlan_id_position] 176 | end 177 | 178 | if attribute == "nw_tos" 179 | rule.attributes["nw_tos"] = nw_tos_values.sample 180 | end 181 | 182 | if attribute == "dl_pcp" 183 | rule.attributes["dl_pcp"] = rand(8) 184 | end 185 | 186 | if not ["in_port", "eth_type", "dl_dst", "dl_src", "dl_vlan", "nw_tos", "dl_pcp"].include?(attribute) 187 | STDERR.puts "Error: attribute #{attribute} not covered in generation process" 188 | exit 1 189 | end 190 | 191 | end 192 | end 193 | 194 | rule 195 | end 196 | end 197 | end 198 | end 199 | -------------------------------------------------------------------------------- /lib/classbench/port_class.rb: -------------------------------------------------------------------------------- 1 | class PortClass 2 | # Incorrect 3 | # CLASS_NAMES = [ "WC/WC", "WC/LO", "WC/HI", "WC/AR", "WC/EM", 4 | # "LO/WC", "LO/LO", "LO/HI", "LO/AR", "LO/EM", 5 | # "HI/WC", "HI/LO", "HI/HI", "HI/AR", "HI/EM", 6 | # "AR/WC", "AR/LO", "AR/HI", "AR/AR", "AR/EM", 7 | # "EM/WC", "EM/LO", "EM/HI", "EM/AR", "EM/EM"] 8 | 9 | CLASS_NAMES = [ "WC/WC", "WC/HI", "HI/WC", "HI/HI", "WC/LO", 10 | "LO/WC", "HI/LO", "LO/HI", "LO/LO", "WC/AR", 11 | "AR/WC", "HI/AR", "AR/HI", "WC/EM", "EM/WC", 12 | "HI/EM", "EM/HI", "LO/AR", "AR/LO", "LO/EM", 13 | "EM/LO", "AR/AR", "AR/EM", "EM/AR", "EM/EM"] 14 | 15 | def self.name_to_index(name) 16 | CLASS_NAMES.find_index(name) 17 | end 18 | 19 | def self.port_range_group(range) 20 | return :wc if not range 21 | 22 | if range.first == range.last 23 | return :em 24 | elsif range == (0..1023) 25 | return :lo 26 | elsif range == (1024..65535) 27 | return :hi 28 | elsif range == (0..65535) 29 | return :wc 30 | end 31 | 32 | return :ar 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/classbench/rule.rb: -------------------------------------------------------------------------------- 1 | require 'ipaddress' 2 | 3 | module Classbench 4 | class Rule 5 | CLASSBENCH_FORMAT = /^@(?.*)\/(?\d+)\t(?.*)\/(?\d+)\t(?\d+) : (?\d+)\t(?\d+) : (?\d+)\t(?0x[0-9a-f]+?)\/(.*)\t?$/ 6 | 7 | attr_accessor :attributes 8 | 9 | def initialize(attrs) 10 | self.attributes = attrs 11 | convert_ports 12 | 13 | #p port_class_name 14 | end 15 | 16 | def self.from_classbench_format(line) 17 | match = line.match(CLASSBENCH_FORMAT) 18 | src_ip = match[:src_ip]+"/"+match[:src_ip_prefix] 19 | dst_ip = match[:dst_ip]+"/"+match[:dst_ip_prefix] 20 | src_port_range = (match[:src_port_from].to_i..match[:src_port_to].to_i) 21 | dst_port_range = (match[:dst_port_from].to_i..match[:dst_port_to].to_i) 22 | protocol = match[:proto].to_i(16) 23 | 24 | #p [src_ip, dst_ip, protocol, src_port_range, dst_port_range] 25 | r = Rule.new({"nw_proto" => protocol, 26 | "nw_src" => src_ip, 27 | "nw_dst" => dst_ip}) 28 | r.attributes["tp_src"] = src_port_range 29 | r.attributes["tp_dst"] = dst_port_range 30 | 31 | return r 32 | end 33 | 34 | def src_length 35 | IPAddress.parse(attributes["nw_src"] || '0.0.0.0').prefix.to_i 36 | end 37 | 38 | def dst_length 39 | IPAddress.parse(attributes["nw_dst"] || '0.0.0.0').prefix.to_i 40 | end 41 | 42 | def remove_missing_attributes(attrs) 43 | attributes.keep_if {|a| attrs.include?(a)} 44 | end 45 | 46 | def convert_ports 47 | # TODO: Accepting only exact match 48 | if attributes["tp_src"] 49 | attributes["tp_src"] = attributes["tp_src"].to_i(attributes["tp_src"] =~ /^0x/ ? 16 : 10) 50 | # Convert to range 51 | attributes["tp_src"] = (attributes["tp_src"]..attributes["tp_src"]) 52 | end 53 | 54 | if attributes["tp_dst"] 55 | attributes["tp_dst"] = attributes["tp_dst"].to_i(attributes["tp_dst"] =~ /^0x/ ? 16 : 10) 56 | # Convert to range 57 | attributes["tp_dst"] = (attributes["tp_dst"]..attributes["tp_dst"]) 58 | end 59 | end 60 | 61 | def protocol 62 | if attributes["nw_proto"] 63 | attributes["nw_proto"].to_i 64 | else 65 | 0 66 | end 67 | end 68 | 69 | def fields 70 | attributes.keys 71 | end 72 | 73 | # Returns string representation of port class 74 | def port_class_name 75 | "#{src_port_range_group.upcase}/#{dst_port_range_group.upcase}" 76 | end 77 | 78 | def port_class 79 | PortClass.name_to_index(port_class_name) 80 | end 81 | 82 | def src_port_range_group 83 | PortClass.port_range_group(attributes["tp_src"] || (0..65535)) 84 | end 85 | 86 | def dst_port_range_group 87 | PortClass.port_range_group(attributes["tp_dst"] || (0..65535)) 88 | end 89 | 90 | def of_format 91 | 92 | end 93 | 94 | def l2_vendor(type) 95 | begin 96 | attributes["dl_#{type}"][0,8] 97 | rescue NoMethodError 98 | nil 99 | end 100 | end 101 | 102 | def to_vswitch_format 103 | attributes.to_a. 104 | reject {|k,v| v == (0..65535)}. # tp_src and tp_dst is removed when wildcard 105 | map {|k,v| (v.is_a?(Range) and v.first == v.last) ? [k, v.first] : [k,v] }. # if port range is [x..x] => x 106 | map {|k,v| "#{k}=#{v}" }.join(", ") # Openflow format 107 | end 108 | 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/classbench/trie.rb: -------------------------------------------------------------------------------- 1 | module Classbench 2 | DEFAULT_DEPTH = 32 3 | 4 | # Structure representing trie statistics proposed in the ClassBench tool. 5 | # 6 | # Array members store statistics defined separately for each level of the 7 | # trie. Prefix nesting is defined for the whole trie. 8 | class ClassbenchStats 9 | # (float[]) number of prefixes (not prefix nodes) with given length 10 | attr_accessor :prefix_lengths 11 | 12 | # (float[]) probability of node with only one child (from all non-leaf nodes), 13 | attr_accessor :branching_one_child 14 | 15 | # (float[]) probability of node with two children (from all non-leaf nodes) 16 | attr_accessor :branching_two_children 17 | 18 | # (float[]) average relative weight ratio of lighter vs heavier subtree (nodes with two children only) 19 | attr_accessor :skew 20 | 21 | # (int) maximum number of prefix nodes on an arbitrary path in the trie 22 | attr_accessor :prefix_nesting 23 | 24 | def initialize 25 | preinitialized_hash = Hash[*(0..DEFAULT_DEPTH).flat_map { |k, v| [k , 0.0] }] 26 | 27 | self.prefix_lengths = preinitialized_hash.dup 28 | self.branching_one_child = preinitialized_hash.dup 29 | self.branching_two_children = preinitialized_hash.dup 30 | self.skew = preinitialized_hash.dup 31 | self.prefix_nesting = 0 32 | end 33 | end 34 | 35 | 36 | # Structure representing statistics related to trie nodes. 37 | # 38 | # All the statistics are stored separately for each level of the trie. 39 | class NodeStats 40 | attr_accessor :leaf # (int[]) number of leaf nodes 41 | attr_accessor :one_child # (int[]) number of nodes with one child only 42 | attr_accessor :two_children # (int[]) number of nodes with both children 43 | attr_accessor :prefix # (int[]) number of prefix nodes (not prefixes) 44 | attr_accessor :non_prefix # (int[]) number of non-prefix nodes 45 | 46 | def initialize 47 | preinitialized_hash = Hash[*(0..DEFAULT_DEPTH).flat_map { |k, v| [k , 0] }] 48 | 49 | self.leaf = preinitialized_hash.dup; 50 | self.one_child = preinitialized_hash.dup; 51 | self.two_children = preinitialized_hash.dup; 52 | self.prefix = preinitialized_hash.dup; 53 | self.non_prefix = preinitialized_hash.dup; 54 | end 55 | end 56 | 57 | # Class representing statistics related to the trie. 58 | # 59 | # Statistics are divided into two groups: 60 | # 1) statistics proposed in ClassBench tool and 61 | # 2) statistics related to trie nodes. 62 | # 63 | class TrieStats 64 | attr_accessor :classbench 65 | attr_accessor :nodes 66 | 67 | def initialize 68 | self.classbench = ClassbenchStats.new 69 | self.nodes = NodeStats.new 70 | end 71 | 72 | def classbench_stats 73 | text = "" 74 | classbench.branching_one_child.keys.each do |level| 75 | branching_1_child = classbench.branching_one_child[level] 76 | branching_2_children = classbench.branching_two_children[level] 77 | skew = classbench.skew[level] 78 | 79 | text += "#{level}\t#{branching_1_child}\t#{branching_2_children}\t#{skew}\n" 80 | end 81 | text 82 | end 83 | end 84 | 85 | # Class for representation of a n-ary prefix tree - trie. 86 | class Trie 87 | attr_accessor :root 88 | 89 | def initialize 90 | end 91 | 92 | def insert(prefix) 93 | self.root = TrieNode.new(0) if not root 94 | 95 | # Empty prefix 96 | if prefix.size == 0 97 | root.increment_prefixes 98 | return 99 | end 100 | 101 | current_node = self.root 102 | next_node = nil 103 | 104 | # For each char 105 | prefix.split('').each_with_index do |ch, i| 106 | next_node = current_node.subtree[ch] 107 | 108 | if next_node.nil? 109 | next_node = TrieNode.new(i+1) 110 | current_node.subtree[ch] = next_node 111 | end 112 | 113 | current_node = next_node 114 | end 115 | 116 | current_node.increment_prefixes 117 | end 118 | 119 | def self.get_prefix_nesting(node) 120 | if node # non-empty subtree 121 | # get prefix nesting from successor nodes 122 | zero_nesting = get_prefix_nesting(node.subtree["0"]) 123 | one_nesting = get_prefix_nesting(node.subtree["1"]) 124 | # will this node increase prefix nesting? 125 | if node.prefixes_count > 0 # this is a prefix node 126 | is_prefix = 1 127 | else 128 | is_prefix = 0 129 | end 130 | 131 | # return maximum of successors' nesting, possibly incremented 132 | if zero_nesting > one_nesting 133 | return zero_nesting + is_prefix 134 | else 135 | return one_nesting + is_prefix 136 | end 137 | else # empty subtree 138 | return 0 139 | end 140 | end 141 | 142 | # Erase not implemented/neccessary 143 | 144 | def get_stats 145 | stats = TrieStats.new 146 | 147 | return stats if not root 148 | root.compute_weights 149 | 150 | level = root.level 151 | 152 | que = [root] 153 | # BFS, append from right, take from left 154 | while not que.empty? 155 | node = que.shift # take one from left 156 | 157 | node.subtree.each do |char, subnode| 158 | que << subnode 159 | end 160 | 161 | # level change - finish statistics computation for the previous level 162 | if node.level != level 163 | # auxiliary variables 164 | one_child = stats.nodes.one_child[level]; 165 | two_children = stats.nodes.two_children[level]; 166 | sum = one_child + two_children; 167 | # branching_one_child and branching_two_children 168 | if sum != 0 169 | stats.classbench.branching_one_child[level] = 170 | one_child.to_f / sum; 171 | stats.classbench.branching_two_children[level] = 172 | two_children.to_f / sum; 173 | end 174 | # skew 175 | if two_children != 0 176 | stats.classbench.skew[level] /= two_children.to_f; 177 | end 178 | # increment the level counter 179 | level += 1; 180 | end 181 | 182 | # trie node visit - classbench statistics 183 | stats.classbench.prefix_lengths[level] += node.prefixes_count; 184 | if node.subtree["0"] and node.subtree["1"] # skew is defined 185 | if node.zero_weight > node.one_weight # lighter 1-subtree 186 | skew = 1 - (node.one_weight.to_f / node.zero_weight); 187 | else # lighter 0-subtree 188 | skew = 1 - (node.zero_weight.to_f / node.one_weight); 189 | end 190 | stats.classbench.skew[level] += skew; 191 | end 192 | 193 | # trie node visit - nodes statistics 194 | if node.subtree["0"].nil? 195 | if node.subtree["1"].nil? # leaf node 196 | stats.nodes.leaf[level] += 1 197 | else # one child node 198 | stats.nodes.one_child[level] += 1 199 | end 200 | else # node->zero != NULL 201 | if node.subtree["1"] # two child node 202 | stats.nodes.two_children[level] += 1 203 | else # one child node 204 | stats.nodes.one_child[level] += 1 205 | end 206 | end 207 | 208 | if node.prefixes_count > 0 # prefix node 209 | stats.nodes.prefix[level] += 1 210 | else # non-prefix node 211 | stats.nodes.non_prefix[level] += 1 212 | end 213 | 214 | end # end of while BFS 215 | 216 | # finish statistics computation for the last level 217 | # auxiliary variables 218 | one_child = stats.nodes.one_child[level] 219 | two_children = stats.nodes.two_children[level] 220 | sum = one_child + two_children 221 | # branching_one_child and branching_two_children 222 | if sum != 0 223 | stats.classbench.branching_one_child[level] = 224 | one_child.to_f / sum 225 | stats.classbench.branching_two_children[level] = 226 | two_children.to_f / sum 227 | end 228 | # skew 229 | if two_children != 0 230 | stats.classbench.skew[level] /= two_children.to_f 231 | end 232 | # compute prefix nesting 233 | stats.classbench.prefix_nesting = Trie.get_prefix_nesting(root) 234 | 235 | return stats 236 | end # end of get_stats 237 | end 238 | end 239 | -------------------------------------------------------------------------------- /lib/classbench/trie_node.rb: -------------------------------------------------------------------------------- 1 | class TrieNode 2 | attr_accessor :level # depth of node 3 | attr_accessor :prefixes_count # number of occurences of the prefix 4 | 5 | attr_accessor :subtree # Hash mapping character -> TrieNode 6 | attr_accessor :subtree_weights 7 | 8 | def initialize(level) 9 | self.prefixes_count = 0 10 | 11 | self.subtree = {} 12 | self.subtree_weights = {} 13 | 14 | self.level = level 15 | end 16 | 17 | def compute_weights 18 | weight = 0 19 | 20 | subtree.each do |char, st| 21 | self.subtree_weights[char] = st.compute_weights 22 | weight += self.subtree_weights[char] 23 | end 24 | 25 | weight += self.prefixes_count 26 | end 27 | 28 | def increment_prefixes 29 | self.prefixes_count += 1 30 | end 31 | 32 | def zero_weight 33 | subtree_weights["0"] 34 | end 35 | 36 | def one_weight 37 | subtree_weights["1"] 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /patches/ipv6.patch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucansky/classbench-ng/7c1eaa378a1d0e820df201d05532f42db855b73e/patches/ipv6.patch -------------------------------------------------------------------------------- /vendor/Makefile: -------------------------------------------------------------------------------- 1 | # Compilation of classbench 2 | 3 | all: compile 4 | 5 | download: 6 | # Make backup of db_generator if exists 7 | -[ -d db_generator ] && mv db_generator db_generator-$(shell date --rfc-3339=seconds | tr ' ' '_') 8 | 9 | # Download and unpack ClassBench 10 | wget -O- -q http://www.arl.wustl.edu/classbench/db_generator.tar.gz | tar -xvz 11 | 12 | patch: download 13 | # Apply patches from ../patches 14 | git apply --directory=vendor/db_generator ../patches/ipv6.patch 15 | 16 | compile: patch 17 | # Patching PortList (extension of preallocated array is necessary) 18 | # Raise limit of L5 rules from 200 to 20000. 19 | sed -i 's/200/20000/' db_generator/PortList.h 20 | 21 | # Recurse to ClassBench compilation 22 | make -C db_generator -B db_generator 23 | --------------------------------------------------------------------------------