fluent-plugin-netflow 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1374b820e1a4d0cb22669c1a4bd2b671959cb032
4
- data.tar.gz: 2539e059a991ae7b3fbb6af818b86a6e376646c8
3
+ metadata.gz: d5a011a605cc39a2aff47556d09b98973bd84eb9
4
+ data.tar.gz: 779a28e68e6cd2bcd480d74ddcaa69605111839b
5
5
  SHA512:
6
- metadata.gz: 524a0902155144ee32939939617e5051f7d6618f87c4c1316476a0738839f46b6b39d317246cba239fd67102f7e061486f4dea53eed7b2c875b5581a76753b43
7
- data.tar.gz: c27c449af0a538fed071427472905aececdd9daeacfa6405123137f578cc4d75be22ed260e549874aa5f6220ddefba6d0ccbbec9b4903ba1e7951a1123540bf9
6
+ metadata.gz: 0c9396c4d0b6f9b8f1d6fda6ca0a53bdb4c0772fb12780e5a52930e840c886f73b4b6221cf6225773053038cf532319aab0c3439797b244239a20c3816462ae1
7
+ data.tar.gz: 6e6e44c7685bff5996bd068b068308934245f32928ab0e334623dfadd0359e7dc14658d96a7c85dded55dd58a7c9c155f0f3e033626cf3923295870f4dcc3977
data/.gitignore ADDED
@@ -0,0 +1,24 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ # For TextMate, emacs, vim
19
+ *.tmproj
20
+ tmtags
21
+ *~
22
+ \#*
23
+ .\#*
24
+ *.swp
data/.travis.yml CHANGED
@@ -1,19 +1,18 @@
1
1
  language: ruby
2
2
 
3
3
  rvm:
4
- - 1.9.3
5
4
  - 2.0.0
6
5
  - 2.1
6
+ - 2.2
7
+ - 2.3.0
7
8
  - ruby-head
8
9
  - rbx
9
10
 
10
- branches:
11
- only:
12
- - master
13
-
14
11
  matrix:
15
12
  allow_failures:
16
13
  - rvm: ruby-head
17
14
  - rvm: rbx
18
15
 
16
+ before_install: gem update bundler
17
+
19
18
  script: bundle exec rake test
data/Rakefile CHANGED
@@ -6,7 +6,7 @@ require 'rake/testtask'
6
6
 
7
7
  Rake::TestTask.new(:test) do |test|
8
8
  test.libs << 'lib' << 'test'
9
- test.test_files = FileList['test/*.rb']
9
+ test.test_files = FileList['test/**/test_*.rb']
10
10
  test.verbose = true
11
11
  end
12
12
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.2
1
+ 0.2.0
@@ -0,0 +1,9 @@
1
+ <source>
2
+ @type netflow
3
+ bind 127.0.0.1
4
+ tag example.netflow
5
+ </source>
6
+
7
+ <match example.netflow>
8
+ @type stdout
9
+ </match>
@@ -20,4 +20,5 @@ Gem::Specification.new do |gem|
20
20
  gem.add_dependency "fluentd", [">= 0.10.17", "< 2"]
21
21
  gem.add_dependency "bindata", "~> 2.1"
22
22
  gem.add_development_dependency "rake", ">= 0.9.2"
23
+ gem.add_development_dependency "test-unit", "~> 3.0"
23
24
  end
@@ -15,21 +15,18 @@
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
  #
18
+ require 'cool.io'
19
+ require 'fluent/plugin/socket_util'
20
+ require 'fluent/plugin/parser_netflow'
21
+
18
22
  module Fluent
19
23
  class NetflowInput < Input
20
24
  Plugin.register_input('netflow', self)
21
25
 
22
- def initialize
23
- super
24
- require 'cool.io'
25
- require 'fluent/plugin/socket_util'
26
- require 'fluent/plugin/parser_netflow'
27
- end
28
-
29
- config_param :port, :integer, :default => 5140
30
- config_param :bind, :string, :default => '0.0.0.0'
26
+ config_param :port, :integer, default: 5140
27
+ config_param :bind, :string, default: '0.0.0.0'
31
28
  config_param :tag, :string
32
- config_param :protocol_type, :default => :udp do |val|
29
+ config_param :protocol_type, default: :udp do |val|
33
30
  case val.downcase
34
31
  when 'udp'
35
32
  :udp
@@ -62,8 +59,8 @@ module Fluent
62
59
 
63
60
  def run
64
61
  @loop.run
65
- rescue
66
- log.error "unexpected error", :error=>$!.to_s
62
+ rescue => e
63
+ log.error "unexpected error", error_class: e.class, error: e.message
67
64
  log.error_backtrace
68
65
  end
69
66
 
@@ -82,7 +79,7 @@ module Fluent
82
79
  router.emit(@tag, time, record)
83
80
  }
84
81
  rescue => e
85
- log.warn data.dump, :error => e.to_s
82
+ log.warn "unexpected error on parsing", data: data.dump, error_class: e.class, error: e.message
86
83
  log.warn_backtrace
87
84
  end
88
85
 
@@ -109,8 +106,9 @@ module Fluent
109
106
  def on_readable
110
107
  msg, addr = @io.recvfrom_nonblock(4096)
111
108
  @callback.call(addr[3], msg)
112
- rescue
113
- # TODO log?
109
+ rescue => e
110
+ log.error "unexpected error on reading from socket", error_class: e.class, error: e.message
111
+ log.error_backtrace
114
112
  end
115
113
  end
116
114
  end
@@ -0,0 +1,160 @@
1
+ require "bindata"
2
+
3
+ module Fluent
4
+ class TextParser
5
+ class NetflowParser < Parser
6
+ class IP4Addr < BinData::Primitive
7
+ endian :big
8
+ uint32 :storage
9
+
10
+ def set(val)
11
+ ip = IPAddr.new(val)
12
+ if ! ip.ipv4?
13
+ raise ArgumentError, "invalid IPv4 address '#{val}'"
14
+ end
15
+ self.storage = ip.to_i
16
+ end
17
+
18
+ def get
19
+ IPAddr.new_ntoh([self.storage].pack('N')).to_s
20
+ end
21
+ end
22
+
23
+ class IP6Addr < BinData::Primitive
24
+ endian :big
25
+ uint128 :storage
26
+
27
+ def set(val)
28
+ ip = IPAddr.new(val)
29
+ if ! ip.ipv6?
30
+ raise ArgumentError, "invalid IPv6 address `#{val}'"
31
+ end
32
+ self.storage = ip.to_i
33
+ end
34
+
35
+ def get
36
+ IPAddr.new_ntoh((0..7).map { |i|
37
+ (self.storage >> (112 - 16 * i)) & 0xffff
38
+ }.pack('n8')).to_s
39
+ end
40
+ end
41
+
42
+ class MacAddr < BinData::Primitive
43
+ array :bytes, type: :uint8, initial_length: 6
44
+
45
+ def set(val)
46
+ ints = val.split(/:/).collect { |int| int.to_i(16) }
47
+ self.bytes = ints
48
+ end
49
+
50
+ def get
51
+ self.bytes.collect { |byte| byte.value.to_s(16).rjust(2,'0') }.join(":")
52
+ end
53
+ end
54
+
55
+ class MplsLabel < BinData::Primitive
56
+ bit20 :label
57
+ bit3 :exp
58
+ bit1 :bottom
59
+ def set(val)
60
+ self.label = val >> 4
61
+ self.exp = (val & 0b1111) >> 1
62
+ self.bottom = val & 0b1
63
+ end
64
+ def get
65
+ self.label
66
+ end
67
+ end
68
+
69
+ class Header < BinData::Record
70
+ endian :big
71
+ uint16 :version
72
+ end
73
+
74
+ class Netflow5PDU < BinData::Record
75
+ endian :big
76
+ uint16 :version
77
+ uint16 :flow_records
78
+ uint32 :uptime
79
+ uint32 :unix_sec
80
+ uint32 :unix_nsec
81
+ uint32 :flow_seq_num
82
+ uint8 :engine_type
83
+ uint8 :engine_id
84
+ bit2 :sampling_algorithm
85
+ bit14 :sampling_interval
86
+ array :records, initial_length: :flow_records do
87
+ ip4_addr :ipv4_src_addr
88
+ ip4_addr :ipv4_dst_addr
89
+ ip4_addr :ipv4_next_hop
90
+ uint16 :input_snmp
91
+ uint16 :output_snmp
92
+ uint32 :in_pkts
93
+ uint32 :in_bytes
94
+ uint32 :first_switched
95
+ uint32 :last_switched
96
+ uint16 :l4_src_port
97
+ uint16 :l4_dst_port
98
+ skip length: 1
99
+ uint8 :tcp_flags # Split up the TCP flags maybe?
100
+ uint8 :protocol
101
+ uint8 :src_tos
102
+ uint16 :src_as
103
+ uint16 :dst_as
104
+ uint8 :src_mask
105
+ uint8 :dst_mask
106
+ skip length: 2
107
+ end
108
+ end
109
+
110
+ class TemplateFlowset < BinData::Record
111
+ endian :big
112
+ array :templates, read_until: lambda { array.num_bytes == flowset_length - 4 } do
113
+ uint16 :template_id
114
+ uint16 :field_count
115
+ array :fields, initial_length: :field_count do
116
+ uint16 :field_type
117
+ uint16 :field_length
118
+ end
119
+ end
120
+ end
121
+
122
+ class OptionFlowset < BinData::Record
123
+ endian :big
124
+ array :templates, read_until: lambda { flowset_length - 4 - array.num_bytes <= 2 } do
125
+ uint16 :template_id
126
+ uint16 :scope_length
127
+ uint16 :option_length
128
+ array :scope_fields, initial_length: lambda { scope_length / 4 } do
129
+ uint16 :field_type
130
+ uint16 :field_length
131
+ end
132
+ array :option_fields, initial_length: lambda { option_length / 4 } do
133
+ uint16 :field_type
134
+ uint16 :field_length
135
+ end
136
+ end
137
+ skip length: lambda { templates.length.odd? ? 2 : 0 }
138
+ end
139
+
140
+ class Netflow9PDU < BinData::Record
141
+ endian :big
142
+ uint16 :version
143
+ uint16 :flow_records
144
+ uint32 :uptime
145
+ uint32 :unix_sec
146
+ uint32 :flow_seq_num
147
+ uint32 :source_id
148
+ array :records, read_until: :eof do
149
+ uint16 :flowset_id
150
+ uint16 :flowset_length
151
+ choice :flowset_data, selection: :flowset_id do
152
+ template_flowset 0
153
+ option_flowset 1
154
+ string :default, read_length: lambda { flowset_length - 4 }
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -1,18 +1,26 @@
1
- require "bindata"
2
1
  require "ipaddr"
3
2
  require 'yaml'
4
3
 
5
4
  require 'fluent/parser'
6
5
 
6
+ require_relative 'netflow_records'
7
+ require_relative 'vash'
8
+
7
9
  module Fluent
8
10
  class TextParser
9
11
  # port from logstash's netflow parser
10
12
  class NetflowParser < Parser
11
13
  Plugin.register_parser('netflow', self)
12
14
 
13
- config_param :cache_ttl, :integer, :default => 4000
14
- config_param :versions, :array, :default => [5, 9]
15
- config_param :definitions, :string, :default => nil
15
+ config_param :switched_times_from_uptime, :bool, default: false
16
+ config_param :cache_ttl, :integer, default: 4000
17
+ config_param :versions, :array, default: [5, 9]
18
+ config_param :definitions, :string, default: nil
19
+
20
+ # Cisco NetFlow Export Datagram Format
21
+ # http://www.cisco.com/c/en/us/td/docs/net_mgmt/netflow_collection_engine/3-6/user/guide/format.html
22
+ # Cisco NetFlow Version 9 Flow-Record Format
23
+ # http://www.cisco.com/en/US/technologies/tk648/tk362/technologies_white_paper09186a00800a3db9.html
16
24
 
17
25
  def configure(conf)
18
26
  super
@@ -23,8 +31,8 @@ module Fluent
23
31
 
24
32
  begin
25
33
  @fields = YAML.load_file(filename)
26
- rescue Exception => e
27
- raise "Bad syntax in definitions file #{filename}"
34
+ rescue => e
35
+ raise "Bad syntax in definitions file #{filename}", error_class: e.class, error: e.message
28
36
  end
29
37
 
30
38
  # Allow the user to augment/override/rename the supported Netflow fields
@@ -32,8 +40,8 @@ module Fluent
32
40
  raise "definitions file #{@definitions} does not exists" unless File.exist?(@definitions)
33
41
  begin
34
42
  @fields.merge!(YAML.load_file(@definitions))
35
- rescue Exception => e
36
- raise "Bad syntax in definitions file #{@definitions}"
43
+ rescue => e
44
+ raise "Bad syntax in definitions file #{@definitions}", error_class: e.class, error: e.message
37
45
  end
38
46
  end
39
47
  # Path to default Netflow v9 scope field definitions
@@ -41,427 +49,291 @@ module Fluent
41
49
 
42
50
  begin
43
51
  @scope_fields = YAML.load_file(filename)
44
- rescue Exception => e
45
- raise "Bad syntax in scope definitions file #{filename}"
52
+ rescue => e
53
+ raise "Bad syntax in scope definitions file #{filename}", error_class: e.class, error: e.message
46
54
  end
47
55
  end
48
56
 
49
- def call(payload)
50
- header = Header.read(payload)
51
- unless @versions.include?(header.version)
52
- $log.warn "Ignoring Netflow version v#{header.version}"
53
- return
54
- end
55
-
56
- if header.version == 5
57
- flowset = Netflow5PDU.read(payload)
58
- elsif header.version == 9
57
+ def call(payload, &block)
58
+ version,_ = payload[0,2].unpack('n')
59
+ case version
60
+ when 5
61
+ forV5(payload, block)
62
+ when 9
63
+ # TODO: implement forV9
59
64
  flowset = Netflow9PDU.read(payload)
65
+ handle_v9(flowset, block)
60
66
  else
61
- $log.warn "Unsupported Netflow version v#{header.version}"
62
- return
67
+ $log.warn "Unsupported Netflow version v#{version}: #{version.class}"
63
68
  end
69
+ end
64
70
 
65
- flowset.records.each do |record|
66
- if flowset.version == 5
67
- event = {}
68
-
69
- # FIXME Probably not doing this right WRT JRuby?
70
- #
71
- # The flowset header gives us the UTC epoch seconds along with
72
- # residual nanoseconds so we can set @timestamp to that easily
73
- time = flowset.unix_sec
74
-
75
- # Copy some of the pertinent fields in the header to the event
76
- ['version', 'flow_seq_num', 'engine_type', 'engine_id', 'sampling_algorithm', 'sampling_interval', 'flow_records'].each do |f|
77
- event[f] = flowset[f]
78
- end
79
-
80
- # Create fields in the event from each field in the flow record
81
- record.each_pair do |k,v|
82
- case k.to_s
83
- when /_switched$/
84
- # The flow record sets the first and last times to the device
85
- # uptime in milliseconds. Given the actual uptime is provided
86
- # in the flowset header along with the epoch seconds we can
87
- # convert these into absolute times
88
- millis = flowset.uptime - v
89
- seconds = flowset.unix_sec - (millis / 1000)
90
- micros = (flowset.unix_nsec / 1000) - (millis % 1000)
91
- if micros < 0
92
- seconds--
93
- micros += 1000000
94
- end
95
-
96
- # FIXME Again, probably doing this wrong WRT JRuby?
97
- event[k.to_s] = Time.at(seconds, micros).utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
98
- else
99
- event[k.to_s] = v
100
- end
101
- end
102
-
103
- yield time, event
104
- elsif flowset.version == 9
105
- case record.flowset_id
106
- when 0
107
- # Template flowset
108
- record.flowset_data.templates.each do |template|
109
- catch (:field) do
110
- fields = []
111
- template.fields.each do |field|
112
- entry = netflow_field_for(field.field_type, field.field_length, @fields)
113
- if !entry
114
- throw :field
115
- end
116
- fields += entry
117
- end
118
- # We get this far, we have a list of fields
119
- #key = "#{flowset.source_id}|#{event["source"]}|#{template.template_id}"
120
- key = "#{flowset.source_id}|#{template.template_id}"
121
- @templates[key, @cache_ttl] = BinData::Struct.new(:endian => :big, :fields => fields)
122
- # Purge any expired templates
123
- @templates.cleanup!
124
- end
125
- end
126
- when 1
127
- # Options template flowset
128
- record.flowset_data.templates.each do |template|
129
- catch (:field) do
130
- fields = []
131
- template.scope_fields.each do |field|
132
- entry = netflow_field_for(field.field_type, field.field_length, @scope_fields)
133
- if ! entry
134
- throw :field
135
- end
136
- fields += entry
137
- end
138
- template.option_fields.each do |field|
139
- entry = netflow_field_for(field.field_type, field.field_length, @fields)
140
- if ! entry
141
- throw :field
142
- end
143
- fields += entry
144
- end
145
- # We get this far, we have a list of fields
146
- #key = "#{flowset.source_id}|#{event["source"]}|#{template.template_id}"
147
- key = "#{flowset.source_id}|#{template.template_id}"
148
- @templates[key, @cache_ttl] = BinData::Struct.new(:endian => :big, :fields => fields)
149
- # Purge any expired templates
150
- @templates.cleanup!
151
- end
152
- end
153
- when 256..65535
154
- # Data flowset
155
- #key = "#{flowset.source_id}|#{event["source"]}|#{record.flowset_id}"
156
- key = "#{flowset.source_id}|#{record.flowset_id}"
157
- template = @templates[key]
158
- if ! template
159
- #$log.warn("No matching template for flow id #{record.flowset_id} from #{event["source"]}")
160
- $log.warn("No matching template for flow id #{record.flowset_id}")
161
- next
162
- end
163
-
164
- length = record.flowset_length - 4
71
+ private
165
72
 
166
- # Template shouldn't be longer than the record and there should
167
- # be at most 3 padding bytes
168
- if template.num_bytes > length or ! (length % template.num_bytes).between?(0, 3)
169
- $log.warn("Template length doesn't fit cleanly into flowset", :template_id => record.flowset_id, :template_length => template.num_bytes, :record_length => length)
170
- next
171
- end
73
+ def ipv4_addr_to_string(uint32)
74
+ "#{(uint32 & 0xff000000) >> 24}.#{(uint32 & 0x00ff0000) >> 16}.#{(uint32 & 0x0000ff00) >> 8}.#{uint32 & 0x000000ff}"
75
+ end
172
76
 
173
- array = BinData::Array.new(:type => template, :initial_length => length / template.num_bytes)
174
-
175
- records = array.read(record.flowset_data)
176
- records.each do |r|
177
- time = flowset.unix_sec
178
- event = {}
179
-
180
- # Fewer fields in the v9 header
181
- ['version', 'flow_seq_num'].each do |f|
182
- event[f] = flowset[f]
183
- end
184
-
185
- event['flowset_id'] = record.flowset_id
186
-
187
- r.each_pair do |k,v|
188
- case k.to_s
189
- when /_switched$/
190
- millis = flowset.uptime - v
191
- seconds = flowset.unix_sec - (millis / 1000)
192
- # v9 did away with the nanosecs field
193
- micros = 1000000 - (millis % 1000)
194
- event[k.to_s] = Time.at(seconds, micros).utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
195
- else
196
- event[k.to_s] = v
197
- end
198
- end
199
-
200
- yield time, event
201
- end
202
- else
203
- $log.warn("Unsupported flowset id #{record.flowset_id}")
204
- end
205
- end
77
+ def msec_from_boot_to_time(msec, uptime, current_unix_time, current_nsec)
78
+ millis = uptime - msec
79
+ seconds = current_unix_time - (millis / 1000)
80
+ micros = (current_nsec / 1000) - ((millis % 1000) * 1000)
81
+ if micros < 0
82
+ seconds -= 1
83
+ micros += 1000000
206
84
  end
85
+ Time.at(seconds, micros)
207
86
  end
208
87
 
209
- private
210
-
211
- def uint_field(length, default)
212
- # If length is 4, return :uint32, etc. and use default if length is 0
213
- ("uint" + (((length > 0) ? length : default) * 8).to_s).to_sym
88
+ def format_for_switched(time)
89
+ time.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
214
90
  end
215
91
 
216
- def netflow_field_for(type, length, field_definitions)
217
- if field_definitions.include?(type)
218
- field = field_definitions[type]
219
- if field.is_a?(Array)
92
+ NETFLOW_V5_HEADER_FORMAT = 'nnNNNNnn'
93
+ NETFLOW_V5_HEADER_BYTES = 24
94
+ NETFLOW_V5_RECORD_FORMAT = 'NNNnnNNNNnnnnnnnxx'
95
+ NETFLOW_V5_RECORD_BYTES = 48
96
+
97
+ # V5 header
98
+ # uint16 :version # n
99
+ # uint16 :flow_records # n
100
+ # uint32 :uptime # N
101
+ # uint32 :unix_sec # N
102
+ # uint32 :unix_nsec # N
103
+ # uint32 :flow_seq_num # N
104
+ # uint8 :engine_type # n -> 0xff00
105
+ # uint8 :engine_id # -> 0x00ff
106
+ # bit2 :sampling_algorithm # n -> 0b1100000000000000
107
+ # bit14 :sampling_interval # -> 0b0011111111111111
108
+
109
+ # V5 records
110
+ # array :records, initial_length: :flow_records do
111
+ # ip4_addr :ipv4_src_addr # uint32 N
112
+ # ip4_addr :ipv4_dst_addr # uint32 N
113
+ # ip4_addr :ipv4_next_hop # uint32 N
114
+ # uint16 :input_snmp # n
115
+ # uint16 :output_snmp # n
116
+ # uint32 :in_pkts # N
117
+ # uint32 :in_bytes # N
118
+ # uint32 :first_switched # N
119
+ # uint32 :last_switched # N
120
+ # uint16 :l4_src_port # n
121
+ # uint16 :l4_dst_port # n
122
+ # skip length: 1 # n -> (ignored)
123
+ # uint8 :tcp_flags # -> 0x00ff
124
+ # uint8 :protocol # n -> 0xff00
125
+ # uint8 :src_tos # -> 0x00ff
126
+ # uint16 :src_as # n
127
+ # uint16 :dst_as # n
128
+ # uint8 :src_mask # n -> 0xff00
129
+ # uint8 :dst_mask # -> 0x00ff
130
+ # skip length: 2 # xx
131
+ # end
132
+ def forV5(payload, block)
133
+ version, flow_records, uptime, unix_sec, unix_nsec, flow_seq_num, engine, sampling = payload.unpack(NETFLOW_V5_HEADER_FORMAT)
134
+ engine_type = (engine & 0xff00) >> 8
135
+ engine_id = engine & 0x00ff
136
+ sampling_algorithm = (sampling & 0b1100000000000000) >> 14
137
+ sampling_interval = sampling & 0b0011111111111111
138
+
139
+ time = Time.at(unix_sec, unix_nsec / 1000).to_i # TODO: Fluent::EventTime
140
+
141
+ records_bytes = payload.bytesize - NETFLOW_V5_HEADER_BYTES
142
+
143
+ if records_bytes / NETFLOW_V5_RECORD_BYTES != flow_records
144
+ $log.warn "bytesize mismatch, records_bytes:#{records_bytes}, records:#{flow_records}"
145
+ return
146
+ end
220
147
 
221
- if field[0].is_a?(Integer)
222
- field[0] = uint_field(length, field[0])
223
- end
148
+ format_full = NETFLOW_V5_RECORD_FORMAT * flow_records
149
+ objects = payload[NETFLOW_V5_HEADER_BYTES, records_bytes].unpack(format_full)
150
+
151
+ while objects.size > 0
152
+ src_addr, dst_addr, next_hop, input_snmp, output_snmp,
153
+ in_pkts, in_bytes, first_switched, last_switched, l4_src_port, l4_dst_port,
154
+ tcp_flags_16, protocol_src_tos, src_as, dst_as, src_dst_mask = objects.shift(16)
155
+ record = {
156
+ "version" => version,
157
+ "uptime" => uptime,
158
+ "flow_records" => flow_records,
159
+ "flow_seq_num" => flow_seq_num,
160
+ "engine_type" => engine_type,
161
+ "engine_id" => engine_id,
162
+ "sampling_algorithm" => sampling_algorithm,
163
+ "sampling_interval" => sampling_interval,
164
+
165
+ "ipv4_src_addr" => ipv4_addr_to_string(src_addr),
166
+ "ipv4_dst_addr" => ipv4_addr_to_string(dst_addr),
167
+ "ipv4_next_hop" => ipv4_addr_to_string(next_hop),
168
+ "input_snmp" => input_snmp,
169
+ "output_snmp" => output_snmp,
170
+ "in_pkts" => in_pkts,
171
+ "in_bytes" => in_bytes,
172
+ "first_switched" => first_switched,
173
+ "last_switched" => last_switched,
174
+ "l4_src_port" => l4_src_port,
175
+ "l4_dst_port" => l4_dst_port,
176
+ "tcp_flags" => tcp_flags_16 & 0x00ff,
177
+ "protocol" => (protocol_src_tos & 0xff00) >> 8,
178
+ "src_tos" => (protocol_src_tos & 0x00ff),
179
+ "src_as" => src_as,
180
+ "dst_as" => dst_as,
181
+ "src_mask" => (src_dst_mask & 0xff00) >> 8,
182
+ "dst_mask" => (src_dst_mask & 0x00ff)
183
+ }
184
+ unless @switched_times_from_uptime
185
+ record["first_switched"] = format_for_switched(msec_from_boot_to_time(record["first_switched"], uptime, unix_sec, unix_nsec))
186
+ record["last_switched"] = format_for_switched(msec_from_boot_to_time(record["last_switched"] , uptime, unix_sec, unix_nsec))
187
+ end
224
188
 
225
- # Small bit of fixup for skip or string field types where the length
226
- # is dynamic
227
- case field[0]
228
- when :skip
229
- field += [nil, {:length => length}]
230
- when :string
231
- field += [{:length => length, :trim_padding => true}]
232
- end
189
+ block.call(time, record)
190
+ end
191
+ end
233
192
 
234
- [field]
193
+ def handle_v9(flowset, block)
194
+ flowset.records.each do |record|
195
+ case record.flowset_id
196
+ when 0
197
+ handle_v9_flowset_template(flowset, record)
198
+ when 1
199
+ handle_v9_flowset_options_template(flowset, record)
200
+ when 256..65535
201
+ handle_v9_flowset_data(flowset, record, block)
235
202
  else
236
- $log.warn("Definition should be an array", :field => field)
237
- nil
203
+ $log.warn "Unsupported flowset id #{record.flowset_id}"
238
204
  end
239
- else
240
- $log.warn("Unsupported field", :type => type, :length => length)
241
- nil
242
205
  end
243
206
  end
244
207
 
245
- class IP4Addr < BinData::Primitive
246
- endian :big
247
- uint32 :storage
248
-
249
- def set(val)
250
- ip = IPAddr.new(val)
251
- if ! ip.ipv4?
252
- raise ArgumentError, "invalid IPv4 address '#{val}'"
208
+ def handle_v9_flowset_template(flowset, record)
209
+ record.flowset_data.templates.each do |template|
210
+ catch (:field) do
211
+ fields = []
212
+ template.fields.each do |field|
213
+ entry = netflow_field_for(field.field_type, field.field_length, @fields)
214
+ if !entry
215
+ throw :field
216
+ end
217
+ fields += entry
218
+ end
219
+ # We get this far, we have a list of fields
220
+ key = "#{flowset.source_id}|#{template.template_id}"
221
+ @templates[key, @cache_ttl] = BinData::Struct.new(endian: :big, fields: fields)
222
+ # Purge any expired templates
223
+ @templates.cleanup!
253
224
  end
254
- self.storage = ip.to_i
255
- end
256
-
257
- def get
258
- IPAddr.new_ntoh([self.storage].pack('N')).to_s
259
225
  end
260
226
  end
261
227
 
262
- class IP6Addr < BinData::Primitive
263
- endian :big
264
- uint128 :storage
265
-
266
- def set(val)
267
- ip = IPAddr.new(val)
268
- if ! ip.ipv6?
269
- raise ArgumentError, "invalid IPv6 address `#{val}'"
228
+ def handle_v9_flowset_options_template(flowset, record)
229
+ record.flowset_data.templates.each do |template|
230
+ catch (:field) do
231
+ fields = []
232
+ template.scope_fields.each do |field|
233
+ entry = netflow_field_for(field.field_type, field.field_length, @scope_fields)
234
+ if ! entry
235
+ throw :field
236
+ end
237
+ fields += entry
238
+ end
239
+ template.option_fields.each do |field|
240
+ entry = netflow_field_for(field.field_type, field.field_length, @fields)
241
+ if ! entry
242
+ throw :field
243
+ end
244
+ fields += entry
245
+ end
246
+ # We get this far, we have a list of fields
247
+ key = "#{flowset.source_id}|#{template.template_id}"
248
+ @templates[key, @cache_ttl] = BinData::Struct.new(endian: :big, fields: fields)
249
+ # Purge any expired templates
250
+ @templates.cleanup!
270
251
  end
271
- self.storage = ip.to_i
272
- end
273
-
274
- def get
275
- IPAddr.new_ntoh((0..7).map { |i|
276
- (self.storage >> (112 - 16 * i)) & 0xffff
277
- }.pack('n8')).to_s
278
252
  end
279
253
  end
280
254
 
281
- class MacAddr < BinData::Primitive
282
- array :bytes, :type => :uint8, :initial_length => 6
255
+ FIELDS_FOR_COPY_V9 = ['version', 'flow_seq_num']
283
256
 
284
- def set(val)
285
- ints = val.split(/:/).collect { |int| int.to_i(16) }
286
- self.bytes = ints
257
+ def handle_v9_flowset_data(flowset, record, block)
258
+ key = "#{flowset.source_id}|#{record.flowset_id}"
259
+ template = @templates[key]
260
+ if ! template
261
+ $log.warn("No matching template for flow id #{record.flowset_id}")
262
+ return
287
263
  end
288
264
 
289
- def get
290
- self.bytes.collect { |byte| byte.value.to_s(16).rjust(2,'0') }.join(":")
291
- end
292
- end
265
+ length = record.flowset_length - 4
293
266
 
294
- class MplsLabel < BinData::Primitive
295
- bit20 :label
296
- bit3 :exp
297
- bit1 :bottom
298
- def set(val)
299
- self.label = val >> 4
300
- self.exp = (val & 0b1111) >> 1
301
- self.bottom = val & 0b1
302
- end
303
- def get
304
- self.label
267
+ # Template shouldn't be longer than the record and there should
268
+ # be at most 3 padding bytes
269
+ if template.num_bytes > length or ! (length % template.num_bytes).between?(0, 3)
270
+ $log.warn "Template length doesn't fit cleanly into flowset",
271
+ template_id: record.flowset_id, template_length: template.num_bytes, record_length: length
272
+ return
305
273
  end
306
- end
307
274
 
308
- class Header < BinData::Record
309
- endian :big
310
- uint16 :version
311
- end
275
+ array = BinData::Array.new(type: template, initial_length: length / template.num_bytes)
312
276
 
313
- class Netflow5PDU < BinData::Record
314
- endian :big
315
- uint16 :version
316
- uint16 :flow_records
317
- uint32 :uptime
318
- uint32 :unix_sec
319
- uint32 :unix_nsec
320
- uint32 :flow_seq_num
321
- uint8 :engine_type
322
- uint8 :engine_id
323
- bit2 :sampling_algorithm
324
- bit14 :sampling_interval
325
- array :records, :initial_length => :flow_records do
326
- ip4_addr :ipv4_src_addr
327
- ip4_addr :ipv4_dst_addr
328
- ip4_addr :ipv4_next_hop
329
- uint16 :input_snmp
330
- uint16 :output_snmp
331
- uint32 :in_pkts
332
- uint32 :in_bytes
333
- uint32 :first_switched
334
- uint32 :last_switched
335
- uint16 :l4_src_port
336
- uint16 :l4_dst_port
337
- skip :length => 1
338
- uint8 :tcp_flags # Split up the TCP flags maybe?
339
- uint8 :protocol
340
- uint8 :src_tos
341
- uint16 :src_as
342
- uint16 :dst_as
343
- uint8 :src_mask
344
- uint8 :dst_mask
345
- skip :length => 2
346
- end
347
- end
277
+ records = array.read(record.flowset_data)
278
+ records.each do |r|
279
+ time = flowset.unix_sec
280
+ event = {}
348
281
 
349
- class TemplateFlowset < BinData::Record
350
- endian :big
351
- array :templates, :read_until => lambda { array.num_bytes == flowset_length - 4 } do
352
- uint16 :template_id
353
- uint16 :field_count
354
- array :fields, :initial_length => :field_count do
355
- uint16 :field_type
356
- uint16 :field_length
282
+ # Fewer fields in the v9 header
283
+ FIELDS_FOR_COPY_V9.each do |f|
284
+ event[f] = flowset[f]
357
285
  end
358
- end
359
- end
360
286
 
361
- class OptionFlowset < BinData::Record
362
- endian :big
363
- array :templates, :read_until => lambda { flowset_length - 4 - array.num_bytes <= 2 } do
364
- uint16 :template_id
365
- uint16 :scope_length
366
- uint16 :option_length
367
- array :scope_fields, :initial_length => lambda { scope_length / 4 } do
368
- uint16 :field_type
369
- uint16 :field_length
370
- end
371
- array :option_fields, :initial_length => lambda { option_length / 4 } do
372
- uint16 :field_type
373
- uint16 :field_length
287
+ event['flowset_id'] = record.flowset_id
288
+
289
+ r.each_pair do |k,v|
290
+ case k.to_s
291
+ when /_switched$/
292
+ millis = flowset.uptime - v
293
+ seconds = flowset.unix_sec - (millis / 1000)
294
+ # v9 did away with the nanosecs field
295
+ micros = 1000000 - (millis % 1000)
296
+ event[k.to_s] = Time.at(seconds, micros).utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
297
+ else
298
+ event[k.to_s] = v
299
+ end
374
300
  end
301
+
302
+ block.call(time, event)
375
303
  end
376
- skip :length => lambda { templates.length.odd? ? 2 : 0 }
377
304
  end
378
305
 
379
- class Netflow9PDU < BinData::Record
380
- endian :big
381
- uint16 :version
382
- uint16 :flow_records
383
- uint32 :uptime
384
- uint32 :unix_sec
385
- uint32 :flow_seq_num
386
- uint32 :source_id
387
- array :records, :read_until => :eof do
388
- uint16 :flowset_id
389
- uint16 :flowset_length
390
- choice :flowset_data, :selection => :flowset_id do
391
- template_flowset 0
392
- option_flowset 1
393
- string :default, :read_length => lambda { flowset_length - 4 }
394
- end
395
- end
306
+ def uint_field(length, default)
307
+ # If length is 4, return :uint32, etc. and use default if length is 0
308
+ ("uint" + (((length > 0) ? length : default) * 8).to_s).to_sym
396
309
  end
397
310
 
398
- # https://gist.github.com/joshaven/184837
399
- class Vash < Hash
400
- def initialize(constructor = {})
401
- @register ||= {}
402
- if constructor.is_a?(Hash)
403
- super()
404
- merge(constructor)
405
- else
406
- super(constructor)
407
- end
408
- end
311
+ def netflow_field_for(type, length, field_definitions)
312
+ if field_definitions.include?(type)
313
+ field = field_definitions[type]
314
+ if field.is_a?(Array)
409
315
 
410
- alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
411
- alias_method :regular_reader, :[] unless method_defined?(:regular_reader)
316
+ if field[0].is_a?(Integer)
317
+ field[0] = uint_field(length, field[0])
318
+ end
412
319
 
413
- def [](key)
414
- sterilize(key)
415
- clear(key) if expired?(key)
416
- regular_reader(key)
417
- end
320
+ # Small bit of fixup for skip or string field types where the length
321
+ # is dynamic
322
+ case field[0]
323
+ when :skip
324
+ field += [nil, {length: length}]
325
+ when :string
326
+ field += [{length: length, trim_padding: true}]
327
+ end
418
328
 
419
- def []=(key, *args)
420
- if args.length == 2
421
- value, ttl = args[1], args[0]
422
- elsif args.length == 1
423
- value, ttl = args[0], 60
329
+ [field]
424
330
  else
425
- raise ArgumentError, "Wrong number of arguments, expected 2 or 3, received: #{args.length+1}\n"+
426
- "Example Usage: volatile_hash[:key]=value OR volatile_hash[:key, ttl]=value"
331
+ $log.warn "Definition should be an array", field: field
332
+ nil
427
333
  end
428
- sterilize(key)
429
- ttl(key, ttl)
430
- regular_writer(key, value)
431
- end
432
-
433
- def merge(hsh)
434
- hsh.map {|key,value| self[sterile(key)] = hsh[key]}
435
- self
436
- end
437
-
438
- def cleanup!
439
- now = Time.now.to_i
440
- @register.map {|k,v| clear(k) if v < now}
441
- end
442
-
443
- def clear(key)
444
- sterilize(key)
445
- @register.delete key
446
- self.delete key
447
- end
448
-
449
- private
450
-
451
- def expired?(key)
452
- Time.now.to_i > @register[key].to_i
453
- end
454
-
455
- def ttl(key, secs=60)
456
- @register[key] = Time.now.to_i + secs.to_i
457
- end
458
-
459
- def sterile(key)
460
- String === key ? key.chomp('!').chomp('=') : key.to_s.chomp('!').chomp('=').to_sym
461
- end
462
-
463
- def sterilize(key)
464
- key = sterile(key)
334
+ else
335
+ $log.warn "Unsupported field", type: type, length: length
336
+ nil
465
337
  end
466
338
  end
467
339
  end