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