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 +4 -4
- data/.gitignore +24 -0
- data/.travis.yml +4 -5
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/example/fluentd.conf +9 -0
- data/fluent-plugin-netflow.gemspec +1 -0
- data/lib/fluent/plugin/in_netflow.rb +13 -15
- data/lib/fluent/plugin/netflow_records.rb +160 -0
- data/lib/fluent/plugin/parser_netflow.rb +250 -378
- data/lib/fluent/plugin/vash.rb +75 -0
- data/test/helper.rb +26 -0
- data/test/netflow.v5.dump +0 -0
- data/test/test_in_netflow.rb +32 -0
- data/test/test_parser_netflow.rb +379 -0
- metadata +29 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d5a011a605cc39a2aff47556d09b98973bd84eb9
|
4
|
+
data.tar.gz: 779a28e68e6cd2bcd480d74ddcaa69605111839b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
@@ -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
|
-
|
23
|
-
|
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, :
|
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
|
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
|
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
|
-
|
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 :
|
14
|
-
config_param :
|
15
|
-
config_param :
|
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
|
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
|
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
|
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
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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#{
|
62
|
-
return
|
67
|
+
$log.warn "Unsupported Netflow version v#{version}: #{version.class}"
|
63
68
|
end
|
69
|
+
end
|
64
70
|
|
65
|
-
|
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
|
-
|
167
|
-
|
168
|
-
|
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
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
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
|
-
|
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
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
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
|
-
|
222
|
-
|
223
|
-
|
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
|
-
|
226
|
-
|
227
|
-
|
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
|
-
|
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
|
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
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
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
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
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
|
-
|
282
|
-
array :bytes, :type => :uint8, :initial_length => 6
|
255
|
+
FIELDS_FOR_COPY_V9 = ['version', 'flow_seq_num']
|
283
256
|
|
284
|
-
|
285
|
-
|
286
|
-
|
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
|
-
|
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
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
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
|
-
|
309
|
-
endian :big
|
310
|
-
uint16 :version
|
311
|
-
end
|
275
|
+
array = BinData::Array.new(type: template, initial_length: length / template.num_bytes)
|
312
276
|
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
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
|
-
|
350
|
-
|
351
|
-
|
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
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
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
|
-
|
380
|
-
|
381
|
-
|
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
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
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
|
-
|
411
|
-
|
316
|
+
if field[0].is_a?(Integer)
|
317
|
+
field[0] = uint_field(length, field[0])
|
318
|
+
end
|
412
319
|
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
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
|
-
|
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
|
-
|
426
|
-
|
331
|
+
$log.warn "Definition should be an array", field: field
|
332
|
+
nil
|
427
333
|
end
|
428
|
-
|
429
|
-
|
430
|
-
|
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
|