fluent-plugin-netflow 0.1.2 → 0.2.0

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.
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