logstash-codec-ipfix 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,7 @@
1
+ module Logstash
2
+ module Codec
3
+ module IPFIX
4
+ VERSION = '0.9.0'
5
+ end
6
+ end
7
+ end
@@ -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
+