caper 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.
- data/.gitignore +9 -0
- data/History.rdoc +31 -0
- data/LICENSE.ffi-pcap +23 -0
- data/README.rdoc +55 -0
- data/Rakefile +26 -0
- data/VERSION +1 -0
- data/examples/ipfw_divert.rb +45 -0
- data/examples/print_bytes.rb +17 -0
- data/examples/test_loop.rb +48 -0
- data/lib/caper.rb +40 -0
- data/lib/caper/addr.rb +19 -0
- data/lib/caper/bpf.rb +104 -0
- data/lib/caper/bsd.rb +96 -0
- data/lib/caper/capture_wrapper.rb +287 -0
- data/lib/caper/common_wrapper.rb +173 -0
- data/lib/caper/copy_handler.rb +36 -0
- data/lib/caper/crt.rb +13 -0
- data/lib/caper/data_link.rb +171 -0
- data/lib/caper/dead.rb +35 -0
- data/lib/caper/dumper.rb +53 -0
- data/lib/caper/error_buffer.rb +42 -0
- data/lib/caper/exceptions.rb +19 -0
- data/lib/caper/file_header.rb +24 -0
- data/lib/caper/in_addr.rb +7 -0
- data/lib/caper/interface.rb +27 -0
- data/lib/caper/live.rb +301 -0
- data/lib/caper/offline.rb +51 -0
- data/lib/caper/packet.rb +163 -0
- data/lib/caper/packet_header.rb +23 -0
- data/lib/caper/pcap.rb +250 -0
- data/lib/caper/stat.rb +56 -0
- data/lib/caper/time_val.rb +46 -0
- data/lib/caper/typedefs.rb +25 -0
- data/lib/caper/version.rb +4 -0
- data/spec/data_link_spec.rb +65 -0
- data/spec/dead_spec.rb +34 -0
- data/spec/dumps/http.pcap +0 -0
- data/spec/dumps/simple_tcp.pcap +0 -0
- data/spec/error_buffer_spec.rb +17 -0
- data/spec/file_header_spec.rb +28 -0
- data/spec/live_spec.rb +87 -0
- data/spec/offline_spec.rb +61 -0
- data/spec/packet_behaviors.rb +68 -0
- data/spec/packet_injection_spec.rb +38 -0
- data/spec/packet_spec.rb +111 -0
- data/spec/pcap_spec.rb +149 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/wrapper_behaviors.rb +110 -0
- data/tasks/rcov.rb +6 -0
- data/tasks/rdoc.rb +17 -0
- data/tasks/spec.rb +9 -0
- data/tasks/yard.rb +16 -0
- metadata +140 -0
@@ -0,0 +1,287 @@
|
|
1
|
+
require 'caper/common_wrapper'
|
2
|
+
require 'caper/copy_handler'
|
3
|
+
|
4
|
+
module Caper
|
5
|
+
# A superclass for both offline and live interfaces, but not dead interfaces
|
6
|
+
# This class provides all the features necessary for receiving packets
|
7
|
+
# through libpcap.
|
8
|
+
#
|
9
|
+
#
|
10
|
+
# The loop and dispatch methods default to using a CopyHandler object
|
11
|
+
# when preparing values to the callback block. This is done to safely
|
12
|
+
# provide references to packets outside of the callback blocks.
|
13
|
+
# See CopyHandler for more information.
|
14
|
+
#
|
15
|
+
# Note that for performance reasons, you may not need or want to incur
|
16
|
+
# the extra overhead of creating a copy for every Packet. You can supply
|
17
|
+
# a nil value for the loop handler which will simply pass volatile
|
18
|
+
# references to packets directly to your block. You can also write custom
|
19
|
+
# handlers which implement the 'receive_pcap' method and implement custom
|
20
|
+
# defined behaviors.
|
21
|
+
class CaptureWrapper < CommonWrapper
|
22
|
+
include Enumerable
|
23
|
+
|
24
|
+
attr_accessor :handler
|
25
|
+
|
26
|
+
# Adds an extra parameter :handler for specifying a capture handler
|
27
|
+
# when using loop or dispatch. The handler defaults to CopyHandler,
|
28
|
+
# which always yields a copy of each packet to a block. Setting :handler
|
29
|
+
# to nil will pass packets directly to a block without copying them,
|
30
|
+
# which may be desirable if the packets are only ever processed within
|
31
|
+
# the block, and code does not need to retain a reference to them
|
32
|
+
# elsewhere.
|
33
|
+
def initialize(pcap, opts={}, &block)
|
34
|
+
if opts.has_key?(opts[:handler])
|
35
|
+
@handler = opts[:handler]
|
36
|
+
else
|
37
|
+
@handler = CopyHandler
|
38
|
+
end
|
39
|
+
|
40
|
+
trap('INT') {stop(); close(); raise(SignalException, 'INT')}
|
41
|
+
trap('TERM') {stop(); close(); raise(SignalException, 'TERM')}
|
42
|
+
|
43
|
+
super(pcap, opts, &block)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def _wrap_callback(h, blk)
|
48
|
+
h ||= @handler
|
49
|
+
if h
|
50
|
+
h = h.new() if h.kind_of?(Class)
|
51
|
+
if ! h.respond_to?(:receive_pcap)
|
52
|
+
raise(NoMethodError,
|
53
|
+
"The handler #{h.class} has no receive_pcap method")
|
54
|
+
end
|
55
|
+
return lambda do |usr,phdr,body|
|
56
|
+
yld = h.receive_pcap(self, Packet.new(phdr,body))
|
57
|
+
blk.call(*yld) if blk and yld
|
58
|
+
end
|
59
|
+
elsif blk.kind_of?(Proc) or blk.kind_of?(Method)
|
60
|
+
return lambda do |usr,phdr,body|
|
61
|
+
blk.call(pcap, Packet.new(phdr,body))
|
62
|
+
end
|
63
|
+
else
|
64
|
+
raise(ArgumentError, "Neither a handler nor block were provided")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
public
|
68
|
+
|
69
|
+
|
70
|
+
# Processes packets from a live capture or savefile until cnt packets
|
71
|
+
# are processed, the end of the savefile is reached (when reading from a
|
72
|
+
# savefile), pcap_breakloop() is called, or an error occurs.
|
73
|
+
#
|
74
|
+
# It does not return when live read timeouts occur. A value of -1 or 0
|
75
|
+
# for cnt is equivalent to infinity, so that packets are processed until
|
76
|
+
# another ending condition occurs.
|
77
|
+
#
|
78
|
+
# (In older versions of libpcap, the behavior when cnt was 0 was
|
79
|
+
# undefined; different platforms and devices behaved differently, so
|
80
|
+
# code that must work with older versions of libpcap should use -1, nor
|
81
|
+
# 0, as the value of cnt.)
|
82
|
+
#
|
83
|
+
# @param [Hash] opts
|
84
|
+
# Receive options.
|
85
|
+
#
|
86
|
+
# @option [optional, Integer] :count
|
87
|
+
# Limit to :count number of packets. Default is infinite.
|
88
|
+
#
|
89
|
+
# @yield [self, pkt]
|
90
|
+
#
|
91
|
+
# @yieldparam [CaptureWrapper] self
|
92
|
+
# A reference to self is passed to the block.
|
93
|
+
#
|
94
|
+
# @yieldparam [Packet] pkt
|
95
|
+
# A packet object is yielded which references the header and bytes.
|
96
|
+
#
|
97
|
+
#
|
98
|
+
# @return [Integer, nil]
|
99
|
+
# returns 0 if cnt is exhausted, or nil if the loop terminated due to
|
100
|
+
# a call to pcap_breakloop() before any packets were processed. It
|
101
|
+
# does not return when live read timeouts occur; instead, it attempts
|
102
|
+
# to read more packets.
|
103
|
+
#
|
104
|
+
# @raise [ReadError]
|
105
|
+
# An exception is raised if an error occurs or if libpcap returns
|
106
|
+
# an unexpected value.
|
107
|
+
#
|
108
|
+
def loop(opts={}, &block)
|
109
|
+
cnt = opts[:count] || -1 # default to infinite loop
|
110
|
+
h = opts[:handler]
|
111
|
+
|
112
|
+
ret = Caper.pcap_loop(_pcap, cnt, _wrap_callback(h, block), nil)
|
113
|
+
if ret == -1
|
114
|
+
raise(ReadError, "pcap_loop(): #{geterr()}")
|
115
|
+
elsif ret -2
|
116
|
+
return nil
|
117
|
+
elsif ret > -1
|
118
|
+
return ret
|
119
|
+
else
|
120
|
+
raise(ReadError, "unexpected return from pcap_loop(): #{ret}")
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
alias each loop
|
125
|
+
|
126
|
+
|
127
|
+
# Processes packets from a live capture or savefile until cnt packets
|
128
|
+
# are processed, the end of the current bufferful of packets is reached
|
129
|
+
# when doing a live capture, the end of the savefile is reached (when
|
130
|
+
# reading from a savefile), pcap_breakloop() is called, or an error
|
131
|
+
# occurs.
|
132
|
+
#
|
133
|
+
# Thus, when doing a live capture, cnt is the maximum number of packets
|
134
|
+
# to process before returning, but is not a minimum number; when reading
|
135
|
+
# a live capture, only one bufferful of packets is read at a time, so
|
136
|
+
# fewer than cnt packets may be processed. A value of -1 or 0 for cnt
|
137
|
+
# causes all the packets received in one buffer to be processed when
|
138
|
+
# reading a live capture, and causes all the packets in the file to be
|
139
|
+
# processed when reading a savefile.
|
140
|
+
#
|
141
|
+
# Note: In older versions of libpcap, the behavior when cnt was 0 was
|
142
|
+
# undefined; different platforms and devices behaved differently, so
|
143
|
+
# code that must work with older versions of libpcap should use -1, nor
|
144
|
+
# 0, as the value of cnt.
|
145
|
+
#
|
146
|
+
# @yield [self, pkt]
|
147
|
+
#
|
148
|
+
# @yieldparam [CaptureWrapper] self
|
149
|
+
# A reference to self is passed to the block.
|
150
|
+
#
|
151
|
+
# @yieldparam [Packet] pkt
|
152
|
+
# A packet object is yielded which references the header and bytes.
|
153
|
+
#
|
154
|
+
# @return [Integer, nil]
|
155
|
+
# Returns the number of packets processed on success; this can be 0 if
|
156
|
+
# no packets were read from a live capture or if no more packets are
|
157
|
+
# available in a savefile. It returns nil if the loop terminated due
|
158
|
+
# to a call to CommonWrapper.stop() before any packets were processed.
|
159
|
+
#
|
160
|
+
# @raise [ReadError]
|
161
|
+
# An exception is raised if an error occurs or if libpcap returns
|
162
|
+
# an unexpected value.
|
163
|
+
#
|
164
|
+
def dispatch(opts={}, &block)
|
165
|
+
cnt = opts[:count] || -1 # default to infinite loop
|
166
|
+
h = opts[:handler]
|
167
|
+
|
168
|
+
ret = Caper.pcap_loop(_pcap, cnt, _wrap_callback(h, block),nil)
|
169
|
+
if ret == -1
|
170
|
+
raise(ReadError, "pcap_dispatch(): #{geterr()}")
|
171
|
+
elsif ret -2
|
172
|
+
return nil
|
173
|
+
elsif ret > -1
|
174
|
+
return ret
|
175
|
+
else
|
176
|
+
raise(ReadError, "unexpected return from pcap_dispatch() -> #{ret}")
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
|
181
|
+
# This method uses the older pcap_next() function which has been
|
182
|
+
# deprecated in favor of pcap_next_ex(). It is included only for
|
183
|
+
# backward compatability purposes.
|
184
|
+
#
|
185
|
+
# Important Note. According to libpcap documentation:
|
186
|
+
#
|
187
|
+
# Unfortunately, there is no way to determine whether an error
|
188
|
+
# occured or not when using pcap_next().
|
189
|
+
#
|
190
|
+
def old_next
|
191
|
+
header = PacketHeader.new
|
192
|
+
bytes = Caper.pcap_next(_pcap, header)
|
193
|
+
if bytes.null?
|
194
|
+
return nil # or raise an exception?
|
195
|
+
else
|
196
|
+
return Packet.new(header, bytes)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
|
201
|
+
# Reads the next packet from a pcap device and returns a success/failure
|
202
|
+
# indication.
|
203
|
+
#
|
204
|
+
# @return [Packet, nil]
|
205
|
+
# A packet is returned on success or a nil if the timeout expired or
|
206
|
+
# all packets in a dump file have been exhausted when reading from
|
207
|
+
# a savefile.
|
208
|
+
#
|
209
|
+
# @raise [ReadError]
|
210
|
+
# This exception is raised if there was an error calling
|
211
|
+
# pcap_next_ex().
|
212
|
+
#
|
213
|
+
# @raise [TimeoutError]
|
214
|
+
# This exception is raised if the timeout expires
|
215
|
+
#
|
216
|
+
def next
|
217
|
+
hdr_p = MemoryPointer.new(:pointer)
|
218
|
+
buf_p = MemoryPointer.new(:pointer)
|
219
|
+
|
220
|
+
case Caper.pcap_next_ex(_pcap, hdr_p, buf_p)
|
221
|
+
when -1 # error
|
222
|
+
raise(ReadError, "pcap_next_ex(): #{geterr()}")
|
223
|
+
when 0 # live capture read timeout expired
|
224
|
+
return nil
|
225
|
+
when -2 # savefile packets exhausted
|
226
|
+
return nil
|
227
|
+
when 1
|
228
|
+
hdr = PacketHeader.new( hdr_p.get_pointer(0) )
|
229
|
+
return Packet.new(hdr, buf_p.get_pointer(0))
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
alias next_extra next
|
234
|
+
alias next_ex next
|
235
|
+
|
236
|
+
|
237
|
+
# Sets a flag that will force dispatch() or loop() to return rather
|
238
|
+
# than looping; they will return the number of packets that have been
|
239
|
+
# processed so far, or nil if no packets have been processed so far.
|
240
|
+
#
|
241
|
+
# breakloop does not guarantee that no further packets will be
|
242
|
+
# processed by dispatch() or loop() after it is called. At most
|
243
|
+
# one more packet may be processed.
|
244
|
+
#
|
245
|
+
def breakloop
|
246
|
+
Caper.pcap_breakloop(_pcap)
|
247
|
+
end
|
248
|
+
|
249
|
+
alias stop breakloop
|
250
|
+
|
251
|
+
|
252
|
+
# Used to specify a pcap filter for the pcap interface. This method
|
253
|
+
# compiles a filter expression and applies it on the wrapped pcap
|
254
|
+
# interface.
|
255
|
+
#
|
256
|
+
# @param [String] expression
|
257
|
+
# A pcap filter expression. See pcap-filter(7) manpage for syntax.
|
258
|
+
#
|
259
|
+
# @param [Hash] opts
|
260
|
+
# Compile options. See compile()
|
261
|
+
#
|
262
|
+
# @raise [LibError]
|
263
|
+
# On failure, an exception is raised with the relevant error message
|
264
|
+
# from libpcap.
|
265
|
+
#
|
266
|
+
def set_filter(expression, opts={})
|
267
|
+
code = compile(expression, opts)
|
268
|
+
ret = Caper.pcap_setfilter(_pcap, code)
|
269
|
+
code.free! # done with this, we can free it
|
270
|
+
raise(LibError, "pcap_setfilter(): #{geterr()}") if ret < 0
|
271
|
+
return expression
|
272
|
+
end
|
273
|
+
|
274
|
+
alias setfilter set_filter
|
275
|
+
alias filter= set_filter
|
276
|
+
|
277
|
+
end
|
278
|
+
|
279
|
+
callback :pcap_handler, [:pointer, PacketHeader, :pointer], :void
|
280
|
+
attach_function :pcap_loop, [:pcap_t, :int, :pcap_handler, :pointer], :int
|
281
|
+
attach_function :pcap_dispatch, [:pcap_t, :int, :pcap_handler, :pointer], :int
|
282
|
+
attach_function :pcap_next, [:pcap_t, PacketHeader], :pointer
|
283
|
+
attach_function :pcap_next_ex, [:pcap_t, :pointer, :pointer], :int
|
284
|
+
attach_function :pcap_breakloop, [:pcap_t], :void
|
285
|
+
attach_function :pcap_setfilter, [:pcap_t, BPFProgram], :int
|
286
|
+
|
287
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
module Caper
|
2
|
+
|
3
|
+
# An abstract base wrapper class with features common to all pcap
|
4
|
+
# wrapper types. Do not use this directly. Instead refer to Live,
|
5
|
+
# Dead, or Offline class for open_live, open_dead, or open_file
|
6
|
+
# respectively.
|
7
|
+
class CommonWrapper
|
8
|
+
attr_accessor :pcap
|
9
|
+
|
10
|
+
def initialize(pcap, opts={})
|
11
|
+
@pcap = pcap
|
12
|
+
@closed = false
|
13
|
+
@errbuf ||= ErrorBuffer.create
|
14
|
+
|
15
|
+
yield(self) if block_given?
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
# Returns the DataLink for the pcap device.
|
20
|
+
def datalink
|
21
|
+
@datalink ||= DataLink.new(Caper.pcap_datalink(_pcap))
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
# Returns an array of supported DataLinks for the pcap device.
|
26
|
+
def supported_datalinks
|
27
|
+
dlt_lst = FFI::MemoryPointer.new(:pointer)
|
28
|
+
if (cnt=Caper.pcap_list_datalinks(_pcap, dlt_lst)) < 0
|
29
|
+
raise(LibError, "pcap_list_datalinks(): #{geterr()}")
|
30
|
+
end
|
31
|
+
# extract datalink values
|
32
|
+
p = dlt_lst.get_pointer(0)
|
33
|
+
ret = p.get_array_of_int(0, cnt).map {|dlt| DataLink.new(dlt) }
|
34
|
+
CRT.free(p)
|
35
|
+
return ret
|
36
|
+
end
|
37
|
+
|
38
|
+
# Indicates whether the pcap interface is already closed.
|
39
|
+
def closed?
|
40
|
+
@closed == true
|
41
|
+
end
|
42
|
+
|
43
|
+
def ready?
|
44
|
+
@closed == false and not @pcap.nil? and not @pcap.null?
|
45
|
+
end
|
46
|
+
|
47
|
+
# Closes the pcap interface using libpcap.
|
48
|
+
def close
|
49
|
+
unless @closed
|
50
|
+
Caper.pcap_close(_pcap)
|
51
|
+
@closed = true
|
52
|
+
@pcap = nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns the pcap interface pointer.
|
57
|
+
#
|
58
|
+
# @return [FFI::Pointer]
|
59
|
+
# Internal pointer to a pcap_t handle.
|
60
|
+
#
|
61
|
+
def to_ptr
|
62
|
+
_check_pcap()
|
63
|
+
end
|
64
|
+
|
65
|
+
# Gets the snapshot length.
|
66
|
+
#
|
67
|
+
# @return [Integer]
|
68
|
+
# Snapshot length for the pcap interface.
|
69
|
+
def snaplen
|
70
|
+
Caper.pcap_snapshot(_pcap)
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
# Compiles a pcap filter but does not apply it to the pcap interface.
|
75
|
+
#
|
76
|
+
# @param [String] expression
|
77
|
+
# A pcap filter expression. See pcap-filter(7) manpage for syntax.
|
78
|
+
#
|
79
|
+
# @param [Hash] opts
|
80
|
+
# Additional options for compile
|
81
|
+
#
|
82
|
+
# @option opts [optional, Integer] :optimize
|
83
|
+
# Optimization flag. 0 means don't optimize. Defaults to 1.
|
84
|
+
#
|
85
|
+
# @option opts [optional, Integer] :netmask
|
86
|
+
# A 32-bit number representing the IPv4 netmask of the network on which
|
87
|
+
# packets are being captured. It is only used when checking for IPv4
|
88
|
+
# broadcast addresses in the filter program. Default: 0 (unspecified
|
89
|
+
# netmask)
|
90
|
+
#
|
91
|
+
# @return [BPFProgram]
|
92
|
+
# A Caper::BPFProgram structure for the compiled filter.
|
93
|
+
#
|
94
|
+
# @raise [LibError]
|
95
|
+
# On failure, an exception is raised with the relevant error message
|
96
|
+
# from libpcap.
|
97
|
+
#
|
98
|
+
def compile(expression, opts={})
|
99
|
+
optimize = opts[:optimize] || 1
|
100
|
+
netmask = opts[:netmask] || 0
|
101
|
+
code = BPFProgram.new
|
102
|
+
if Caper.pcap_compile(_pcap, code, expression, optimize, netmask) != 0
|
103
|
+
raise(LibError, "pcap_compile(): #{geterr()}")
|
104
|
+
end
|
105
|
+
return code
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
# @return [Dumper]
|
110
|
+
#
|
111
|
+
# @raise [LibError]
|
112
|
+
# On failure, an exception is raised with the relevant error
|
113
|
+
# message from libpcap.
|
114
|
+
#
|
115
|
+
def open_dump(path)
|
116
|
+
dp = Caper.pcap_dump_open(_pcap, File.expand_path(path))
|
117
|
+
raise(LibError, "pcap_dump_open(): #{geterr()}") if dp.null?
|
118
|
+
return Dumper.new(dp)
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
# @return [String]
|
123
|
+
# The error text pertaining to the last pcap library error.
|
124
|
+
#
|
125
|
+
def geterr
|
126
|
+
Caper.pcap_geterr(_pcap)
|
127
|
+
end
|
128
|
+
|
129
|
+
alias error geterr
|
130
|
+
|
131
|
+
|
132
|
+
private
|
133
|
+
# Raises an exception if @pcap is not set.
|
134
|
+
#
|
135
|
+
# Internal sanity check to confirm the pcap instance
|
136
|
+
# variable has been set. Otherwise very bad things can
|
137
|
+
# ensue by passing a null pointer to various libpcap
|
138
|
+
# functions.
|
139
|
+
def _check_pcap
|
140
|
+
if @pcap.nil?
|
141
|
+
raise(StandardError, "nil pcap device")
|
142
|
+
else
|
143
|
+
return @pcap
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Raises an exception if @pcap is not set or is a null pointer.
|
148
|
+
#
|
149
|
+
# Internal sanity check to confirm the pcap pointer
|
150
|
+
# variable has been set and is not a null pointer.
|
151
|
+
# Otherwise very bad things can ensue by passing a null
|
152
|
+
# pointer to various libpcap functions.
|
153
|
+
def _pcap
|
154
|
+
if (p = _check_pcap()).null?
|
155
|
+
raise(StandardError, "null pointer to pcap device")
|
156
|
+
end
|
157
|
+
p
|
158
|
+
end
|
159
|
+
|
160
|
+
|
161
|
+
end
|
162
|
+
|
163
|
+
|
164
|
+
attach_function :pcap_close, [:pcap_t], :void
|
165
|
+
attach_function :pcap_geterr, [:pcap_t], :string
|
166
|
+
attach_function :pcap_compile, [:pcap_t, BPFProgram, :string, :int, :bpf_uint32], :int
|
167
|
+
attach_function :pcap_datalink, [:pcap_t], :int
|
168
|
+
attach_function :pcap_list_datalinks, [:pcap_t, :pointer], :int
|
169
|
+
attach_function :pcap_set_datalink, [:pcap_t, :int], :int
|
170
|
+
attach_function :pcap_snapshot, [:pcap_t], :int
|
171
|
+
attach_function :pcap_dump_open, [:pcap_t, :string], :pcap_dumper_t
|
172
|
+
|
173
|
+
end
|