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