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