packetgen 0.1.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,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