packetgen 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,39 @@
1
+ require 'stringio'
2
+
3
+ module PacketGen
4
+
5
+ # Module to handle PCAP-NG file format.
6
+ # See http://xml2rfc.tools.ietf.org/cgi-bin/xml2rfc.cgi?url=https://raw.githubusercontent.com/pcapng/pcapng/master/draft-tuexen-opsawg-pcapng.xml&modeAsFormat=html/ascii&type=ascii
7
+ module PcapNG
8
+
9
+ # Section Header Block type number
10
+ SHB_TYPE = StructFu::Int32.new(0x0A0D0D0A, :little)
11
+ # Interface Description Block type number
12
+ IDB_TYPE = StructFu::Int32.new(1, :little)
13
+ # Simple Packet Block type number
14
+ SPB_TYPE = StructFu::Int32.new(3, :little)
15
+ # Enhanced Packet Block type number
16
+ EPB_TYPE = StructFu::Int32.new(6, :little)
17
+
18
+ # Various LINKTYPE values from http://www.tcpdump.org/linktypes.html
19
+ # FIXME: only ETHERNET type is defined as this is the only link layer
20
+ # type supported by PacketGen
21
+ LINKTYPE_ETHERNET = 1
22
+
23
+ # Base error class for PcapNG
24
+ class Error < PacketGen::Error; end
25
+ # Invalid PcapNG file error
26
+ class InvalidFileError < Error; end
27
+
28
+ end
29
+
30
+ end
31
+
32
+
33
+ require_relative 'pcapng/block.rb'
34
+ require_relative 'pcapng/unknown_block.rb'
35
+ require_relative 'pcapng/shb.rb'
36
+ require_relative 'pcapng/idb.rb'
37
+ require_relative 'pcapng/epb.rb'
38
+ require_relative 'pcapng/spb.rb'
39
+ require_relative 'pcapng/file.rb'
@@ -0,0 +1,32 @@
1
+ module PacketGen
2
+ module PcapNG
3
+
4
+ # Mixin module to declare some common methods for block classes.
5
+ module Block
6
+
7
+ # Has this block option?
8
+ # @return [Boolean]
9
+ def has_options?
10
+ self[:options].size > 0
11
+ end
12
+
13
+ # Calculate block length and update :block_len and block_len2 fields
14
+ # @return [void]
15
+ def recalc_block_len
16
+ len = to_a.map(&:to_s).join.size
17
+ self[:block_len].value = self[:block_len2].value = len
18
+ end
19
+
20
+ # Pad given field to 32 bit boundary, if needed
21
+ # @param [Array<Symbol>] fields block fields to pad
22
+ # @return [void]
23
+ def pad_field(*fields)
24
+ fields.each do |field|
25
+ unless self[field].size % 4 == 0
26
+ self[field] << "\x00" * (4 - (self[field].size % 4))
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,131 @@
1
+ module PacketGen
2
+ module PcapNG
3
+
4
+ # {EPB} represents a Enhanced Packet Block (EPB) of a pcapng file.
5
+ #
6
+ # == EPB Definition
7
+ # Int32 :type Default: 0x00000006
8
+ # Int32 :block_len
9
+ # Int32 :interface_id
10
+ # Int32 :tsh (timestamp high)
11
+ # Int32 :tsl (timestamp low)
12
+ # Int32 :cap_len
13
+ # Int32 :orig_len
14
+ # String :data
15
+ # String :options
16
+ # Int32 :block_len2
17
+ class EPB < Struct.new(:type, :block_len, :interface_id, :tsh, :tsl,
18
+ :cap_len, :orig_len, :data, :options, :block_len2)
19
+ include StructFu
20
+ include Block
21
+
22
+ # @return [:little, :big]
23
+ attr_accessor :endian
24
+ # @return [IPB]
25
+ attr_accessor :interface
26
+
27
+ # Minimum EPB size
28
+ MIN_SIZE = 8*4
29
+
30
+ # @param [Hash] options
31
+ # @option options [:little, :big] :endian set block endianness
32
+ # @option options [Integer] :type
33
+ # @option options [Integer] :block_len block total length
34
+ # @option options [Integer] :interface_id specifies the interface this packet
35
+ # comes from
36
+ # @option options [Integer] :tsh timestamp (high nibbles)
37
+ # @option options [Integer] :tsl timestamp (low nibbles)
38
+ # @option options [Integer] :cap_len number of octets captured from the packet
39
+ # @option options [Integer] :orig_len actual length of the packet when it was
40
+ # transmitted on the network
41
+ # @option options [::String] :data
42
+ # @option options [::String] :options
43
+ # @option options [Integer] :block_len2 block total length
44
+ def initialize(options={})
45
+ @endian = set_endianness(options[:endian] || :little)
46
+ init_fields(options)
47
+ super(options[:type], options[:block_len], options[:interface_id], options[:tsh],
48
+ options[:tsl], options[:cap_len], options[:orig_len], options[:data],
49
+ options[:options], options[:block_len2])
50
+ end
51
+
52
+ # Used by {#initialize} to set the initial fields
53
+ # @param [Hash] options
54
+ # @see #initialize possible options
55
+ # @return [Hash] return +options+
56
+ def init_fields(options={})
57
+ options[:type] = @int32.new(options[:type] || PcapNG::EPB_TYPE.to_i)
58
+ options[:block_len] = @int32.new(options[:block_len] || MIN_SIZE)
59
+ options[:interface_id] = @int32.new(options[:interface_id] || 0)
60
+ options[:tsh] = @int32.new(options[:tsh] || 0)
61
+ options[:tsl] = @int32.new(options[:tsl] || 0)
62
+ options[:cap_len] = @int32.new(options[:cap_len] || 0)
63
+ options[:orig_len] = @int32.new(options[:orig_len] || 0)
64
+ options[:data] = StructFu::String.new(options[:data] || '')
65
+ options[:options] = StructFu::String.new(options[:options] || '')
66
+ options[:block_len2] = @int32.new(options[:block_len2] || MIN_SIZE)
67
+ options
68
+ end
69
+
70
+ # Reads a String or a IO to populate the object
71
+ # @param [::String,IO] str_or_io
72
+ # @return [self]
73
+ def read(str_or_io)
74
+ if str_or_io.respond_to? :read
75
+ io = str_or_io
76
+ else
77
+ io = StringIO.new(force_binary(str_or_io.to_s))
78
+ end
79
+ return self if io.eof?
80
+
81
+ self[:type].read io.read(4)
82
+ self[:block_len].read io.read(4)
83
+ self[:interface_id].read io.read(4)
84
+ self[:tsh].read io.read(4)
85
+ self[:tsl].read io.read(4)
86
+ self[:cap_len].read io.read(4)
87
+ self[:orig_len].read io.read(4)
88
+ self[:data].read io.read(self[:cap_len].to_i)
89
+ data_pad_len = (4 - (self[:cap_len].to_i % 4)) % 4
90
+ io.read data_pad_len
91
+ options_len = self[:block_len].to_i - self[:cap_len].to_i - data_pad_len
92
+ options_len -= MIN_SIZE
93
+ self[:options].read io.read(options_len)
94
+ self[:block_len2].read io.read(4)
95
+
96
+ unless self[:block_len].to_i == self[:block_len2].to_i
97
+ raise InvalidFileError, 'Incoherency in Extended Packet Block'
98
+ end
99
+
100
+ self
101
+ end
102
+
103
+ # Return timestamp as a Time object
104
+ # @return [Time]
105
+ def timestamp
106
+ Time.at((self[:tsh].to_i << 32 | self[:tsl].to_i) * ts_resol)
107
+ end
108
+
109
+ # Return the object as a String
110
+ # @return [String]
111
+ def to_s
112
+ pad_field :data, :options
113
+ recalc_block_len
114
+ to_a.map(&:to_s).join
115
+ end
116
+
117
+
118
+ private
119
+
120
+ def ts_resol
121
+ if @interface.nil?
122
+ 1E-6
123
+ else
124
+ @interface.ts_resol
125
+ end
126
+ end
127
+
128
+ end
129
+
130
+ end
131
+ end
@@ -0,0 +1,345 @@
1
+ module PacketGen
2
+ module PcapNG
3
+
4
+ # PcapNG::File is a complete Pcap-NG file handler.
5
+ class File
6
+ # Get file sections
7
+ # @return [Array]
8
+ attr_accessor :sections
9
+
10
+ def initialize
11
+ @sections = []
12
+ end
13
+
14
+ # Read a string to populate the object. Note that this appends new blocks to
15
+ # the Pcapng::File object.
16
+ # @param [String] str
17
+ # @return [self]
18
+ def read(str)
19
+ PacketGen.force_binary(str)
20
+ io = StringIO.new(str)
21
+ parse_section(io)
22
+ self
23
+ end
24
+
25
+ # Clear the contents of the Pcapng::File prior to reading in a new string.
26
+ # This string should contain a Section Header Block and an Interface Description
27
+ # Block to create a conform pcapng file.
28
+ # @param [String] str
29
+ # @return [self]
30
+ def read!(str)
31
+ clear
32
+ read(str)
33
+ end
34
+
35
+ # Read a given file and analyze it.
36
+ # If given a block, it will yield PcapNG::EPB or PcapNG::SPB objects.
37
+ # This is the only way to get packet timestamps.
38
+ # @param [String] fname pcapng file name
39
+ # @yieldparam [EPB,SPB] block
40
+ # @return [Integer] return number of yielded blocks (only if a block is given)
41
+ # @raise [ArgumentError] cannot read +fname+
42
+ def readfile(fname, &blk)
43
+ unless ::File.readable?(fname)
44
+ raise ArgumentError, "cannot read file #{fname}"
45
+ end
46
+
47
+ ::File.open(fname, 'rb') do |f|
48
+ while !f.eof? do
49
+ parse_section(f)
50
+ end
51
+ end
52
+
53
+ if blk
54
+ count = 0
55
+ @sections.each do |section|
56
+ section.interfaces.each do |intf|
57
+ intf.packets.each { |pkt| count += 1; yield pkt }
58
+ end
59
+ end
60
+ count
61
+ end
62
+ end
63
+
64
+ # Give an array of parsed packets (raw data from packets).
65
+ # If a block is given, yield raw packet data from the given file.
66
+ # @overload read_packet_bytes(fname)
67
+ # @param [String] fname pcapng file name
68
+ # @return [Array] array of packet raw data
69
+ # @overload read_packet_bytes(fname)
70
+ # @param [String] fname pcapng file name
71
+ # @yieldparam [String] raw packet raw data
72
+ # @return [Integer] number of packets
73
+ # @raise [ArgumentError] cannot read +fname+
74
+ def read_packet_bytes(fname, &blk)
75
+ count = 0
76
+ packets = [] unless blk
77
+
78
+ readfile(fname) do |packet|
79
+ if blk
80
+ count += 1
81
+ yield packet.data.to_s
82
+ else
83
+ packets << packet.data.to_s
84
+ end
85
+ end
86
+
87
+ blk ? count : packets
88
+ end
89
+
90
+ # Return an array of parsed packets.
91
+ # If a block is given, yield parsed packets from the given file.
92
+ # @overload read_packets(fname)
93
+ # @param [String] fname pcapng file name
94
+ # @return [Array<Packet>]
95
+ # @overload read_packets(fname)
96
+ # @param [String] fname pcapng file name
97
+ # @yieldparam [Packet] packet
98
+ # @return [Integer] number of packets
99
+ # @raise [ArgumentError] cannot read +fname+
100
+ def read_packets(fname, &blk)
101
+ count = 0
102
+ packets = [] unless blk
103
+
104
+ read_packet_bytes(fname) do |packet|
105
+ if blk
106
+ count += 1
107
+ yield Packet.parse(packet)
108
+ else
109
+ packets << Packet.parse(packet)
110
+ end
111
+ end
112
+
113
+ blk ? count : packets
114
+ end
115
+
116
+ # Return the object as a String
117
+ # @return [String]
118
+ def to_s
119
+ @sections.map { |section| section.to_s }.join
120
+ end
121
+
122
+ # Clear the contents of the Pcapng::File.
123
+ # @return [void]
124
+ def clear
125
+ @sections.clear
126
+ end
127
+
128
+ # Translates a {File} into an array of packets.
129
+ # Note that this strips out timestamps -- if you'd like to retain
130
+ # timestamps and other pcapng file information, you will want to
131
+ # use {#read} instead.
132
+ # @param [Hash] options
133
+ # @option options [String] :filename if given, object is cleared and filename
134
+ # is analyzed before generating array. Else, array is generated from +self+
135
+ # @option options [String] :file same as +:filename+
136
+ # @option options [Boolean] :keep_timestamps if +true+ (default value: +false+),
137
+ # generates an array of hashes, each one with timestamp as key and packet
138
+ # as value. There is one hash per packet.
139
+ # @option options [Boolean] :keep_ts same as +:keep_timestamp+
140
+ # @return [Array<Packet>,Array<Hash>]
141
+ def file_to_array(options={})
142
+ filename = options[:filename] || options[:file]
143
+ if filename
144
+ clear
145
+ readfile filename
146
+ end
147
+
148
+ ary = []
149
+ @sections.each do |section|
150
+ section.interfaces.each do |itf|
151
+ if options[:keep_timestamps] || options[:keep_ts]
152
+ ary.concat itf.packets.map { |pkt| { pkt.timestamp => pkt.data.to_s } }
153
+ else
154
+ ary.concat itf.packets.map { |pkt| pkt.data.to_s}
155
+ end
156
+ end
157
+ end
158
+ ary
159
+ end
160
+
161
+ # Writes the {File} to a file.
162
+ # @param [Hash] options
163
+ # @option options [Boolean] :append (default: +false+) if set to +true+,
164
+ # the packets are appended to the file, rather than overwriting it
165
+ # @return [Array] array of 2 elements: filename and size written
166
+ def to_file(filename, options={})
167
+ mode = ''
168
+ if options[:append] and ::File.exists? filename
169
+ mode = 'ab'
170
+ else
171
+ mode = 'wb'
172
+ end
173
+ ::File.open(filename, mode) {|f| f.write(self.to_s)}
174
+ [filename, self.to_s.size]
175
+ end
176
+ alias_method :to_f, :to_file
177
+
178
+ # Shorthand method for writing to a file.
179
+ # @param [#to_s] filename
180
+ # @return [Array] see return value from {#to_file}
181
+ def write(filename='out.pcapng')
182
+ self.to_file(filename.to_s, :append => false)
183
+ end
184
+
185
+ # Shorthand method for appending to a file.
186
+ # @param [#to_s] filename
187
+ # @return [Array] see return value from {#to_file}
188
+ def append(filename='out.pcapng')
189
+ self.to_file(filename.to_s, :append => true)
190
+ end
191
+
192
+ # @overload array_to_file(ary)
193
+ # Update {File} object with packets.
194
+ # @param [Array] ary as generated by {#file_to_array} or Array of Packet objects.
195
+ # Update {File} object without writing file on disk
196
+ # @return [self]
197
+ # @overload array_to_file(options={})
198
+ # Update {File} and/or write it to a file
199
+ # @param [Hash] options
200
+ # @option options [String] :filename file written on disk only if given
201
+ # @option options [Array] :array can either be an array of packet data,
202
+ # or a hash-value pair of timestamp => data.
203
+ # @option options [Time] :timestamp set an initial timestamp
204
+ # @option options [Integer] :ts_inc set the increment between timestamps.
205
+ # Defaults to 1
206
+ # @option options [Boolean] :append if +true+, append packets to the end of
207
+ # the file
208
+ # @return [Array] see return value from {#to_file}
209
+ def array_to_file(options={})
210
+ case options
211
+ when Hash
212
+ filename = options[:filename] || options[:file]
213
+ ary = options[:array] || options[:arr]
214
+ unless ary.kind_of? Array
215
+ raise ArgumentError, ':array parameter needs to be an array'
216
+ end
217
+ ts = options[:timestamp] || options[:ts] || Time.now
218
+ ts_inc = options[:ts_inc] || 1
219
+ append = !!options[:append]
220
+ when Array
221
+ ary = options
222
+ ts = Time.now
223
+ ts_inc = 1
224
+ filename = nil
225
+ append = false
226
+ else
227
+ raise ArgumentError, 'unknown argument. Need either a Hash or Array'
228
+ end
229
+
230
+ section = SHB.new
231
+ @sections << section
232
+ itf = IDB.new(:endian => section.endian)
233
+ classify_block section, itf
234
+
235
+ ary.each_with_index do |pkt, i|
236
+ case pkt
237
+ when Hash
238
+ this_ts = pkt.keys.first.to_i
239
+ this_cap_len = pkt.values.first.to_s.size
240
+ this_data = pkt.values.first.to_s
241
+ else
242
+ this_ts = (ts + ts_inc * i).to_i
243
+ this_cap_len = pkt.to_s.size
244
+ this_data = pkt.to_s
245
+ end
246
+ this_ts = (this_ts / itf.ts_resol).to_i
247
+ this_tsh = this_ts >> 32
248
+ this_tsl = this_ts & 0xffffffff
249
+ this_pkt = EPB.new(:endian => section.endian,
250
+ :interface_id => 0,
251
+ :tsh => this_tsh,
252
+ :tsl => this_tsl,
253
+ :cap_len => this_cap_len,
254
+ :orig_len => this_cap_len,
255
+ :data => this_data)
256
+ classify_block section, this_pkt
257
+ end
258
+
259
+ if filename
260
+ self.to_f(filename, :append => append)
261
+ else
262
+ self
263
+ end
264
+ end
265
+
266
+
267
+ private
268
+
269
+ # Parse a section. A section is made of at least a SHB. It than may contain
270
+ # others blocks, such as IDB, SPB or EPB.
271
+ # @param [IO] io
272
+ # @return [void]
273
+ def parse_section(io)
274
+ shb = SHB.new
275
+ type = StructFu::Int32.new(0, shb.endian).read(io.read(4))
276
+ io.seek(-4, IO::SEEK_CUR)
277
+ shb = parse(type, io, shb)
278
+ raise InvalidFileError, 'no Section header found' unless shb.is_a?(SHB)
279
+
280
+ if shb.section_len.to_i != 0xffffffffffffffff
281
+ # Section length is defined
282
+ section = StringIO.new(io.read(shb.section_len.to_i))
283
+ while !section.eof? do
284
+ shb = @sections.last
285
+ type = StructFu::Int32.new(0, shb.endian).read(section.read(4))
286
+ section.seek(-4, IO::SEEK_CUR)
287
+ block = parse(type, section, shb)
288
+ end
289
+ else
290
+ # section length is undefined
291
+ while !io.eof?
292
+ shb = @sections.last
293
+ type = StructFu::Int32.new(0, shb.endian).read(io.read(4))
294
+ io.seek(-4, IO::SEEK_CUR)
295
+ block = parse(type, io, shb)
296
+ end
297
+ end
298
+ end
299
+
300
+ # Parse a block from its type
301
+ # @param [StructFu::Int32] type
302
+ # @param [IO] io stream from which parse block
303
+ # @param [SHB] shb header of current section
304
+ # @return [void]
305
+ def parse(type, io, shb)
306
+ types = PcapNG.constants(false).select { |c| c.to_s =~ /_TYPE/ }.
307
+ map { |c| [PcapNG.const_get(c).to_i, c] }
308
+ types = Hash[types]
309
+
310
+ if types.has_key?(type.to_i)
311
+ klass = PcapNG.const_get(types[type.to_i].to_s.gsub(/_TYPE/, '').to_sym)
312
+ block = klass.new(endian: shb.endian)
313
+ else
314
+ block = UnknownBlock.new(endian: shb.endian)
315
+ end
316
+
317
+ classify_block shb, block
318
+ block.read(io)
319
+ end
320
+
321
+ # Classify block from its type
322
+ # @param [SHB] shb header of current section
323
+ # @param [Block] block block to classify
324
+ # @return [void]
325
+ def classify_block(shb, block)
326
+ case block
327
+ when SHB
328
+ @sections << block
329
+ when IDB
330
+ shb << block
331
+ block.section = shb
332
+ when EPB
333
+ shb.interfaces[block.interface_id.to_i] << block
334
+ block.interface = shb.interfaces[block.interface_id.to_i]
335
+ when SPB
336
+ shb.interfaces[0] << block
337
+ block.interface = shb.interfaces[0]
338
+ else
339
+ shb.unknown_blocks << block
340
+ block.section = shb
341
+ end
342
+ end
343
+ end
344
+ end
345
+ end