logstash-codec-ipfix 0.9.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 +7 -0
- data/CHANGELOG.md +0 -0
- data/CONTRIBUTORS +0 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/NOTICE.TXT +0 -0
- data/README.md +40 -0
- data/lib/logstash/codecs/IPFIX/enterprise.yaml +14 -0
- data/lib/logstash/codecs/IPFIX/ipfix.yaml +1198 -0
- data/lib/logstash/codecs/IPFIX/util.rb +197 -0
- data/lib/logstash/codecs/IPFIX/version.rb +7 -0
- data/lib/logstash/codecs/ipfix.rb +259 -0
- data/logstash-codec-ipfix.gemspec +39 -0
- data/spec/logstash/codecs/IPFIX_spec.rb +11 -0
- data/spec/spec_helper.rb +2 -0
- metadata +164 -0
@@ -0,0 +1,197 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'bindata'
|
3
|
+
require 'ipaddr'
|
4
|
+
|
5
|
+
class IP4Addr < BinData::Primitive
|
6
|
+
endian :big
|
7
|
+
uint32 :storage
|
8
|
+
|
9
|
+
def set(val)
|
10
|
+
ip = IPAddr.new(val)
|
11
|
+
if ! ip.ipv4?
|
12
|
+
raise ArgumentError, "invalid IPv4 address '#{val}'"
|
13
|
+
end
|
14
|
+
self.storage = ip.to_i
|
15
|
+
end
|
16
|
+
|
17
|
+
def get
|
18
|
+
IPAddr.new_ntoh([self.storage].pack('N')).to_s
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class IP6Addr < BinData::Primitive
|
23
|
+
endian :big
|
24
|
+
uint128 :storage
|
25
|
+
|
26
|
+
def set(val)
|
27
|
+
ip = IPAddr.new(val)
|
28
|
+
if ! ip.ipv6?
|
29
|
+
raise ArgumentError, "invalid IPv6 address `#{val}'"
|
30
|
+
end
|
31
|
+
self.storage = ip.to_i
|
32
|
+
end
|
33
|
+
|
34
|
+
def get
|
35
|
+
IPAddr.new_ntoh((0..7).map { |i|
|
36
|
+
(self.storage >> (112 - 16 * i)) & 0xffff
|
37
|
+
}.pack('n8')).to_s
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class MacAddr < BinData::Primitive
|
42
|
+
array :bytes, :type => :uint8, :initial_length => 6
|
43
|
+
|
44
|
+
def set(val)
|
45
|
+
ints = val.split(/:/).collect { |int| int.to_i(16) }
|
46
|
+
self.bytes = ints
|
47
|
+
end
|
48
|
+
|
49
|
+
def get
|
50
|
+
self.bytes.collect { |byte| byte.to_s(16) }.join(":")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Header < BinData::Record
|
55
|
+
endian :big
|
56
|
+
uint16 :version_number
|
57
|
+
end
|
58
|
+
|
59
|
+
class IANAField < BinData::Record
|
60
|
+
endian :big
|
61
|
+
end
|
62
|
+
|
63
|
+
class EnterpriseField < BinData::Record
|
64
|
+
endian :big
|
65
|
+
uint32 :enterprise_number
|
66
|
+
end
|
67
|
+
|
68
|
+
class TemplateFlowset < BinData::Record
|
69
|
+
endian :big
|
70
|
+
array :templates, :read_until => lambda { array.num_bytes == set_length_in_bytes - 4 } do
|
71
|
+
uint16 :template_id
|
72
|
+
uint16 :field_count
|
73
|
+
array :fields, :initial_length => :field_count do
|
74
|
+
uint16 :field_type
|
75
|
+
uint16 :field_length
|
76
|
+
choice :information_element, :selection => lambda { (field_type & 0x8000) == 0x8000 } do
|
77
|
+
IANAField false
|
78
|
+
EnterpriseField true
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class OptionFlowset < BinData::Record
|
85
|
+
endian :big
|
86
|
+
array :templates, :read_until => lambda { set_length_in_bytes - 4 - array.num_bytes <= 2 } do
|
87
|
+
uint16 :template_id
|
88
|
+
uint16 :field_count
|
89
|
+
uint16 :scope_field_count
|
90
|
+
array :scope_fields, :initial_length => lambda { scope_field_count / 4 } do
|
91
|
+
uint16 :field_type
|
92
|
+
uint16 :field_length
|
93
|
+
choice :information_element, :selection => lambda { (field_type & 0x8000) == 0x8000 } do
|
94
|
+
IANAField false
|
95
|
+
EnterpriseField true
|
96
|
+
end
|
97
|
+
end
|
98
|
+
array :option_fields, :initial_length => lambda { (field_count - scope_field_count) / 4 } do
|
99
|
+
uint16 :field_type
|
100
|
+
uint16 :field_length
|
101
|
+
choice :information_element, :selection => lambda { (field_type & 0x8000) == 0x8000 } do
|
102
|
+
IANAField false
|
103
|
+
EnterpriseField true
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
skip :length => lambda { templates.length.odd? ? 2 : 0 }
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
class IPFIXSet < BinData::Record
|
112
|
+
endian :big
|
113
|
+
uint16 :version_number
|
114
|
+
uint16 :message_length_in_bytes
|
115
|
+
uint32 :export_time
|
116
|
+
uint32 :sequence_number
|
117
|
+
uint32 :observation_domain_id
|
118
|
+
array :records, :read_until => :eof do
|
119
|
+
uint16 :set_id
|
120
|
+
uint16 :set_length_in_bytes
|
121
|
+
choice :flowset_data, :selection => :set_id do
|
122
|
+
TemplateFlowset 2
|
123
|
+
OptionFlowset 3
|
124
|
+
string :default, :read_length => lambda { set_length_in_bytes - 4 }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# https://gist.github.com/joshaven/184837
|
130
|
+
class Vash < Hash
|
131
|
+
def initialize(constructor = {})
|
132
|
+
@register ||= {}
|
133
|
+
if constructor.is_a?(Hash)
|
134
|
+
super()
|
135
|
+
merge(constructor)
|
136
|
+
else
|
137
|
+
super(constructor)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
|
142
|
+
alias_method :regular_reader, :[] unless method_defined?(:regular_reader)
|
143
|
+
|
144
|
+
def [](key)
|
145
|
+
sterilize(key)
|
146
|
+
clear(key) if expired?(key)
|
147
|
+
regular_reader(key)
|
148
|
+
end
|
149
|
+
|
150
|
+
def []=(key, *args)
|
151
|
+
if args.length == 2
|
152
|
+
value, ttl = args[1], args[0]
|
153
|
+
elsif args.length == 1
|
154
|
+
value, ttl = args[0], 60
|
155
|
+
else
|
156
|
+
raise ArgumentError, "Wrong number of arguments, expected 2 or 3, received: #{args.length+1}\n"+
|
157
|
+
"Example Usage: volatile_hash[:key]=value OR volatile_hash[:key, ttl]=value"
|
158
|
+
end
|
159
|
+
sterilize(key)
|
160
|
+
ttl(key, ttl)
|
161
|
+
regular_writer(key, value)
|
162
|
+
end
|
163
|
+
|
164
|
+
def merge(hsh)
|
165
|
+
hsh.map {|key,value| self[sterile(key)] = hsh[key]}
|
166
|
+
self
|
167
|
+
end
|
168
|
+
|
169
|
+
def cleanup!
|
170
|
+
now = Time.now.to_i
|
171
|
+
@register.map {|k,v| clear(k) if v < now}
|
172
|
+
end
|
173
|
+
|
174
|
+
def clear(key)
|
175
|
+
sterilize(key)
|
176
|
+
@register.delete key
|
177
|
+
self.delete key
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
def expired?(key)
|
182
|
+
Time.now.to_i > @register[key].to_i
|
183
|
+
end
|
184
|
+
|
185
|
+
def ttl(key, secs=60)
|
186
|
+
@register[key] = Time.now.to_i + secs.to_i
|
187
|
+
end
|
188
|
+
|
189
|
+
def sterile(key)
|
190
|
+
String === key ? key.chomp('!').chomp('=') : key.to_s.chomp('!').chomp('=').to_sym
|
191
|
+
end
|
192
|
+
|
193
|
+
def sterilize(key)
|
194
|
+
key = sterile(key)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
@@ -0,0 +1,259 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'logstash/codecs/base'
|
3
|
+
require 'logstash/codecs/IPFIX/version'
|
4
|
+
require 'logstash/codecs/IPFIX/util'
|
5
|
+
require 'logstash/namespace'
|
6
|
+
|
7
|
+
class LogStash::Codecs::IPFIX < LogStash::Codecs::Base
|
8
|
+
config_name 'ipfix'
|
9
|
+
|
10
|
+
# template cache TTL (minutes)
|
11
|
+
config :cache_ttl, :validate => :number, :default => 4000
|
12
|
+
|
13
|
+
# Specify into what field you want the IPFIX data.
|
14
|
+
config :target, :validate => :string, :default => 'ipfix'
|
15
|
+
|
16
|
+
# Add enterprise field definitions
|
17
|
+
# See <https://tools.ietf.org/html/rfc7011#section-3.2> for Field Specifier Format
|
18
|
+
# See <https://www.iana.org/assignments/enterprise-numbers/enterprise-numbers> for Private Enterprise Numbers
|
19
|
+
#
|
20
|
+
# Enterprise fields are defined in the YAML file like so:
|
21
|
+
#
|
22
|
+
# ---
|
23
|
+
# 1246:
|
24
|
+
# 10:
|
25
|
+
# - :uint32
|
26
|
+
# - :application_id
|
27
|
+
# 17:
|
28
|
+
# - :uint32
|
29
|
+
# - :client_site
|
30
|
+
# 18:
|
31
|
+
# - :uint32
|
32
|
+
# - :server_site
|
33
|
+
# 30:
|
34
|
+
# - :uint8
|
35
|
+
# - :server_indicator
|
36
|
+
config :definitions, :validate => :path
|
37
|
+
|
38
|
+
IPFIX10_FIELDS = %w{ export_time sequence_number observation_domain_id }
|
39
|
+
|
40
|
+
public
|
41
|
+
def initialize(params = {})
|
42
|
+
super(params)
|
43
|
+
@threadsafe = false
|
44
|
+
end
|
45
|
+
|
46
|
+
public
|
47
|
+
def register
|
48
|
+
@templates = Vash.new
|
49
|
+
|
50
|
+
# Path to default field definitions
|
51
|
+
filename = ::File.expand_path('IPFIX/ipfix.yaml', ::File.dirname(__FILE__))
|
52
|
+
|
53
|
+
begin
|
54
|
+
@fields = YAML.load_file(filename)
|
55
|
+
rescue Exception => e
|
56
|
+
raise "#{self.class.name}: Bad syntax in definitions file #{filename}: " + e.message
|
57
|
+
end
|
58
|
+
|
59
|
+
# Allow the user to supply enterprise fields
|
60
|
+
if @definitions
|
61
|
+
raise "#{self.class.name}: definitions file #{@definitions} does not exist" unless File.exists?(@definitions)
|
62
|
+
begin
|
63
|
+
@enterprise_fields = YAML.load_file(@definitions)
|
64
|
+
@logger.debug? and @logger.debug('Enterprise fields: ', @enterprise_fields)
|
65
|
+
rescue Exception => e
|
66
|
+
raise "#{self.class.name}: Bad syntax in definitions file #{@definitions}: " + e.message
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end # def register
|
70
|
+
|
71
|
+
public
|
72
|
+
def decode(payload, &block)
|
73
|
+
message_header = Header.read(payload)
|
74
|
+
|
75
|
+
if message_header.version_number == 10
|
76
|
+
flowset = IPFIXSet.read(payload)
|
77
|
+
flowset.records.each do |record|
|
78
|
+
decode_ipfix10(flowset, record).each{|event| yield(event)}
|
79
|
+
end
|
80
|
+
else
|
81
|
+
@logger.warn("Unsupported IPFIX version v#{header.version}")
|
82
|
+
end
|
83
|
+
end # def decode
|
84
|
+
|
85
|
+
# enterprise_field_for(type, length, enterprise_number)
|
86
|
+
# if (type & 0x8000) == 0x8000
|
87
|
+
# if @enterprise_fields.include?(type & 0xFFF)
|
88
|
+
|
89
|
+
private
|
90
|
+
def decode_ipfix10(flowset, record)
|
91
|
+
events = []
|
92
|
+
|
93
|
+
case record.set_id
|
94
|
+
when 2
|
95
|
+
# Template set
|
96
|
+
record.flowset_data.templates.each do |template|
|
97
|
+
catch (:field) do
|
98
|
+
fields = []
|
99
|
+
template.fields.each do |field|
|
100
|
+
if (field.field_type & 0x8000) == 0x8000
|
101
|
+
entry = enterprise_field_for((field.field_type & 0xFFF), field.field_length, field.information_element.enterprise_number)
|
102
|
+
throw :field unless entry
|
103
|
+
fields += entry
|
104
|
+
else
|
105
|
+
entry = iana_field_for(field.field_type, field.field_length)
|
106
|
+
throw :field unless entry
|
107
|
+
fields += entry
|
108
|
+
end
|
109
|
+
end
|
110
|
+
# We get this far, we have a list of fields
|
111
|
+
key = "#{flowset.observation_domain_id}|#{template.template_id}"
|
112
|
+
@templates[key, @cache_ttl] = BinData::Struct.new(:endian => :big, :fields => fields)
|
113
|
+
# Purge any expired templates
|
114
|
+
@templates.cleanup!
|
115
|
+
end
|
116
|
+
end
|
117
|
+
when 3
|
118
|
+
# Options template set
|
119
|
+
record.flowset_data.templates.each do |template|
|
120
|
+
catch (:field) do
|
121
|
+
fields = []
|
122
|
+
template.option_fields.each do |field|
|
123
|
+
entry = iana_field_for(field.field_type, field.field_length)
|
124
|
+
throw :field unless entry
|
125
|
+
fields += entry
|
126
|
+
end
|
127
|
+
# We get this far, we have a list of fields
|
128
|
+
key = "#{flowset.observation_domain_id}|#{template.template_id}"
|
129
|
+
@templates[key, @cache_ttl] = BinData::Struct.new(:endian => :big, :fields => fields)
|
130
|
+
# Purge any expired templates
|
131
|
+
@templates.cleanup!
|
132
|
+
end
|
133
|
+
end
|
134
|
+
when 256..65535
|
135
|
+
# Data set
|
136
|
+
key = "#{flowset.observation_domain_id}|#{record.set_id}"
|
137
|
+
template = @templates[key]
|
138
|
+
|
139
|
+
unless template
|
140
|
+
@logger.warn("No matching template for set id #{record.set_id}")
|
141
|
+
return
|
142
|
+
end
|
143
|
+
|
144
|
+
length = record.set_length_in_bytes - 4
|
145
|
+
|
146
|
+
# Template shouldn't be longer than the record and there should
|
147
|
+
# be at most 3 padding bytes
|
148
|
+
if template.num_bytes > length or ! (length % template.num_bytes).between?(0, 3)
|
149
|
+
@logger.warn("Template length doesn't fit cleanly into flowset", :template_id => record.set_id, :template_length => template.num_bytes, :record_length => length)
|
150
|
+
return
|
151
|
+
end
|
152
|
+
|
153
|
+
array = BinData::Array.new(:type => template, :initial_length => length / template.num_bytes)
|
154
|
+
records = array.read(record.flowset_data)
|
155
|
+
|
156
|
+
records.each do |r|
|
157
|
+
event = {
|
158
|
+
LogStash::Event::TIMESTAMP => LogStash::Timestamp.at(Time.now.to_i),
|
159
|
+
@target => {}
|
160
|
+
}
|
161
|
+
|
162
|
+
IPFIX10_FIELDS.each do |f|
|
163
|
+
event[@target][f] = flowset[f].snapshot
|
164
|
+
end
|
165
|
+
|
166
|
+
event[@target]['set_id'] = record.set_id.snapshot
|
167
|
+
|
168
|
+
r.each_pair do |k, v|
|
169
|
+
event[@target][k.to_s] = v.snapshot
|
170
|
+
end
|
171
|
+
|
172
|
+
events << LogStash::Event.new(event)
|
173
|
+
end
|
174
|
+
else
|
175
|
+
@logger.warn("Unsupported set id #{record.set_id}")
|
176
|
+
end
|
177
|
+
|
178
|
+
events
|
179
|
+
end
|
180
|
+
|
181
|
+
def uint_field(length, default)
|
182
|
+
# If length is 4, return :uint32, etc. and use default if length is 0
|
183
|
+
('uint' + (((length > 0) ? length : default) * 8).to_s).to_sym
|
184
|
+
end # def uint_field
|
185
|
+
|
186
|
+
def iana_field_for(type, length)
|
187
|
+
if @fields.include?(type)
|
188
|
+
field = @fields[type]
|
189
|
+
if field.is_a?(Array)
|
190
|
+
|
191
|
+
field[0] = uint_field(length, field[0]) if field[0].is_a?(Integer)
|
192
|
+
|
193
|
+
# Small bit of fixup for skip or string field types where the length
|
194
|
+
# is dynamic
|
195
|
+
case field[0]
|
196
|
+
when :skip
|
197
|
+
field += [nil, {:length => length}]
|
198
|
+
when :string
|
199
|
+
field += [{:length => length, :trim_padding => true}]
|
200
|
+
end
|
201
|
+
|
202
|
+
@logger.debug? and @logger.debug('Definition complete', :field => field)
|
203
|
+
|
204
|
+
[field]
|
205
|
+
else
|
206
|
+
@logger.warn('Definition should be an array', :field => field)
|
207
|
+
field = []
|
208
|
+
field[0] = uint_field(length, 4)
|
209
|
+
field[1] = ('unknown_field_'+type.to_s).to_sym
|
210
|
+
|
211
|
+
@logger.debug? and @logger.debug('Definition complete', :field => field)
|
212
|
+
|
213
|
+
[field]
|
214
|
+
end
|
215
|
+
else
|
216
|
+
@logger.warn('Unknown field', :type => type, :length => length)
|
217
|
+
field = []
|
218
|
+
field[0] = uint_field(length, 4)
|
219
|
+
field[1] = ('unknown_field_'+type.to_s).to_sym
|
220
|
+
|
221
|
+
@logger.debug? and @logger.debug('Definition complete', :field => field)
|
222
|
+
|
223
|
+
[field]
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# def iana_field_for
|
228
|
+
|
229
|
+
def enterprise_field_for(type, length, enterprise_number)
|
230
|
+
if @enterprise_fields.include?(enterprise_number)
|
231
|
+
fields = @enterprise_fields[enterprise_number]
|
232
|
+
if fields.include?(type)
|
233
|
+
field = fields[type]
|
234
|
+
|
235
|
+
@logger.debug? and @logger.debug('Enterprise definition complete', :field => field)
|
236
|
+
|
237
|
+
[field]
|
238
|
+
else
|
239
|
+
field = []
|
240
|
+
field[0] = uint_field(length, 4)
|
241
|
+
field[1] = ('enterprise_field_'+(type & 0xFFF).to_s).to_sym
|
242
|
+
|
243
|
+
@logger.debug? and @logger.debug('Unknown enterprise field definition complete', :field => field)
|
244
|
+
|
245
|
+
[field]
|
246
|
+
end
|
247
|
+
else
|
248
|
+
@logger.warn('Unknown enterprise number', :type => type, :length => length, :enterprise_number => enterprise_number)
|
249
|
+
field = []
|
250
|
+
field[0] = uint_field(length, 4)
|
251
|
+
field[1] = ('enterprise_field_'+type.to_s).to_sym
|
252
|
+
|
253
|
+
@logger.debug? and @logger.debug('Unknown enterprise definition complete', :field => field)
|
254
|
+
|
255
|
+
[field]
|
256
|
+
end
|
257
|
+
end # def enterprise_field_for
|
258
|
+
end # class LogStash::Codecs::IPFIX
|
259
|
+
|