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,303 @@
1
+ require 'ffi/pcap/capture_wrapper'
2
+
3
+ module FFI
4
+ module PCap
5
+ begin
6
+ attach_function :pcap_setdirection, [:pcap_t, :pcap_direction_t], :int
7
+ rescue FFI::NotFoundError
8
+ end
9
+
10
+ begin
11
+ attach_function :pcap_sendpacket, [:pcap_t, :pointer, :int], :int
12
+ rescue FFI::NotFoundError
13
+ end
14
+
15
+ begin
16
+ attach_function :pcap_inject, [:pcap_t, :pointer, :int], :int
17
+ rescue FFI::NotFoundError
18
+ end
19
+
20
+
21
+ # Creates a pcap interface for capturing from the network.
22
+ #
23
+ # @param [Hash] opts
24
+ # Options are ignored and passed to the super-class except those below.
25
+ #
26
+ # @option opts [String, nil] :device, :dev
27
+ # The device to open. On some platforms, this can be "any". If nil or
28
+ # unspecified FFI::PCap.lookupdev() is called to obtain a default device.
29
+ #
30
+ # @option opts [Integer] :snaplen
31
+ # The snapshot length for the pcap object. Defaults to DEFAULT_SNAPLEN
32
+ #
33
+ # @option opts [Boolean] :promisc
34
+ # Specifies if the interface is to be put into promiscuous mode. Defaults
35
+ # to false.
36
+ #
37
+ # @option opts [Integer] :timeout
38
+ # Specifies the read timeout in milliseconds. Defaults to DEFAULT_TO_MS
39
+ #
40
+ # @return [Live]
41
+ # A FFI::PCap::Live wrapper.
42
+ #
43
+ # @raise [LibError]
44
+ # On failure, an exception is raised with the relevant error
45
+ # message from libpcap.
46
+ #
47
+ # @raise [ArgumentError]
48
+ # May raise an exception if a :device cannot be autodetected using
49
+ # FFI::PCap.lookupdev() for any reason. This should never happen on most platforms.
50
+ #
51
+ class Live < CaptureWrapper
52
+ DEFAULT_TO_MS = 1000 # Default timeout for pcap_open_live()
53
+
54
+ attr_reader :device, :promisc, :timeout, :direction
55
+
56
+ def initialize(opts=nil)
57
+ opts ||= {}
58
+ @device = opts[:device] || opts[:dev] || FFI::PCap.lookupdev()
59
+ unless @device
60
+ raise(ArgumentError, "Couldn't detect a device. One must be specified.")
61
+ end
62
+
63
+ @snaplen = opts[:snaplen] || DEFAULT_SNAPLEN
64
+ @promisc = opts[:promisc] ? 1 : 0
65
+ @timeout = opts[:timeout] || DEFAULT_TO_MS
66
+ @direction = (opts[:direction] || opts[:dir])
67
+
68
+ @errbuf = ErrorBuffer.create()
69
+ @pcap = FFI::PCap.pcap_open_live(@device, @snaplen, @promisc, @timeout, @errbuf)
70
+ raise(LibError, "pcap_open_live(): #{@errbuf.to_s}") if @pcap.null?
71
+
72
+ # call super to get all our ducks in a row
73
+ super(@pcap, opts)
74
+
75
+ set_direction(@direction) if @direction
76
+
77
+ # Cache network and netmask from pcap_lookupdev.
78
+ # These pointers may be used internally (and should get autoreleased)
79
+ @netp, @maskp = nil
80
+ begin
81
+ FFI::PCap.lookupnet(@device) do |netp, maskp|
82
+ @netp = netp
83
+ @maskp = maskp
84
+ end
85
+ rescue LibError
86
+ warn "Warning: #{$!}"
87
+ end
88
+
89
+ yield self if block_given?
90
+ end
91
+
92
+ # Returns the dotted notation string for the IPv4 network address for
93
+ # the device used by this pcap interface.
94
+ def network
95
+ return nil unless @netp
96
+ @network ||= @netp.get_array_of_uchar(0,4).join('.')
97
+ end
98
+
99
+ # Returns the dotted notation string for the IPv4 netmask for the device
100
+ # used by this pcap interface.
101
+ def netmask
102
+ return nil unless @maskp
103
+ @netmask ||= @maskp.get_array_of_uchar(0,4).join('.')
104
+ end
105
+
106
+ # Returns the 32-bit numeric representation of the IPv4 network address
107
+ # for this device.
108
+ def network_n32
109
+ return nil unless @netp
110
+ ::FFI::DRY::NetEndian.ntohl(@netp.get_uint32(0))
111
+ end
112
+
113
+ # Returns the 32-bit numeric representation of the IPv4 network address
114
+ # for this device.
115
+ def netmask_n32
116
+ return nil unless @maskp
117
+ ::FFI::DRY::NetEndian.ntohl(@maskp.get_uint32(0))
118
+ end
119
+
120
+ @@have_setdirection = FFI::PCap.respond_to?(:pcap_setdirection)
121
+
122
+ # Sets the direction for which packets will be captured.
123
+ #
124
+ # (Not supported on all platforms)
125
+ def set_direction(dir)
126
+ unless @@have_setdirection
127
+ raise(NotImplementedError,
128
+ "pcap_setdirection() is not avaiable from your pcap library")
129
+ end
130
+
131
+ dirs = FFI::PCap.enum_type(:pcap_direction_t)
132
+ if FFI::PCap.pcap_setdirection(_pcap, dirs[:"pcap_d_#{dir}"]) == 0
133
+ return true
134
+ else
135
+ raise(LibError, "pcap_setdirection(): #{geterr()}", caller)
136
+ end
137
+ end
138
+
139
+ alias direction= set_direction
140
+
141
+ # set the state of non-blocking mode on a capture device
142
+ #
143
+ # @param [Boolean] mode
144
+ #
145
+ # @raise [LibError]
146
+ # On failure, an exception is raised with the relevant error message
147
+ # from libpcap.
148
+ #
149
+ def set_non_blocking(mode)
150
+ mode = mode ? 1 : 0
151
+ if FFI::PCap.pcap_setnonblock(_pcap, mode, @errbuf) == 0
152
+ return mode == 1
153
+ else
154
+ raise(LibError, "pcap_setnonblock(): #{@errbuf.to_s}", caller)
155
+ end
156
+ end
157
+
158
+ alias non_blocking= set_non_blocking
159
+
160
+ # get the state of non-blocking mode on a capture device
161
+ #
162
+ # @return [Boolean]
163
+ # non-blocking mode
164
+ #
165
+ # @raise [LibError]
166
+ # On failure, an exception is raised with the relevant error message
167
+ # from libpcap.
168
+ #
169
+ def non_blocking
170
+ if (mode=FFI::PCap.pcap_getnonblock(_pcap, @errbuf)) == -1
171
+ raise(LibError, "pcap_getnonblock(): #{@errbuf.to_s}", caller)
172
+ else
173
+ return mode == 1
174
+ end
175
+ end
176
+
177
+ alias non_blocking? non_blocking
178
+
179
+ # Get capture statistics
180
+ #
181
+ # @return [Stats]
182
+ #
183
+ # @raise [LibError]
184
+ # On failure, an exception is raised with the relevant error message
185
+ # from libpcap.
186
+ #
187
+ def stats
188
+ stats = Stat.new
189
+ unless FFI::PCap.pcap_stats(_pcap, stats) == 0
190
+ raise(LibError, "pcap_stats(): #{geterr()}")
191
+ end
192
+ return stats
193
+ end
194
+
195
+
196
+ @@have_inject = FFI::PCap.respond_to?(:pcap_inject)
197
+
198
+ # Transmit a packet using pcap_inject()
199
+ #
200
+ # (not available on all platforms)
201
+ #
202
+ # @param [Packet, String] obj
203
+ # The packet to send. This can be a Packet or String object.
204
+ #
205
+ # @return [Integer]
206
+ # The number of bytes sent.
207
+ #
208
+ # @raise [ArgumentError]
209
+ # An exception is raised if the pkt object type is incorrect or
210
+ # if it is a Packet and the body pointer is null.
211
+ #
212
+ # @raise [LibError]
213
+ # On failure, an exception is raised with the relevant libpcap
214
+ # error message.
215
+ #
216
+ # @raise [NotImplementedError]
217
+ # If the pcap_inject() function is not available from your libpcap
218
+ # library pcap_sendpacket will be tried, if both are missing, this
219
+ # exception will be raised.
220
+ #
221
+ def inject(pkt)
222
+ if @@have_inject
223
+ if pkt.kind_of? Packet
224
+ len = pkt.caplen
225
+ bufp = pkt.body_ptr
226
+ raise(ArgumentError, "packet data null pointer") if bufp.null?
227
+ elsif pkt.kind_of? String
228
+ len = pkt.size
229
+ bufp = FFI::MemoryPointer.from_string(pkt)
230
+ else
231
+ raise(ArgumentError, "Don't know how to inject #{pkt.class}")
232
+ end
233
+
234
+ if (sent=FFI::PCap.pcap_inject(_pcap, bufp, len)) < 0
235
+ raise(LibError, "pcap_inject(): #{geterr()}")
236
+ end
237
+ return sent
238
+ else
239
+ # fake it with sendpacket on windows
240
+ if sendpacket(pkt)
241
+ return (Packet === pkt)? pkt.caplen : pkt.size
242
+ end
243
+ end
244
+ end
245
+
246
+ @@have_sendpacket = FFI::PCap.respond_to?(:pcap_sendpacket)
247
+
248
+ # Transmit a packet using pcap_sendpacket()
249
+ #
250
+ # (not available on all platforms)
251
+ #
252
+ # @param [Packet, String] obj
253
+ # The packet to send. This can be a Packet or String object.
254
+ #
255
+ # @return [True]
256
+ # True is returned on success. Otherwise an exception is raised.
257
+ #
258
+ # @raise [ArgumentError]
259
+ # An exception is raised if the pkt object type is incorrect or
260
+ # if it is a Packet and the body pointer is null.
261
+ #
262
+ # @raise [LibError]
263
+ # On failure, an exception is raised with the relevant libpcap
264
+ # error message.
265
+ #
266
+ # @raise [NotImplementedError]
267
+ # If the pcap_sendpacket() function is not available from your libpcap
268
+ # library this exception will be raised.
269
+ #
270
+ def sendpacket(pkt)
271
+ unless @@have_sendpacket
272
+ raise(NotImplementedError,
273
+ "packet injectors are not avaiable from your pcap library")
274
+ end
275
+
276
+ if pkt.kind_of? Packet
277
+ len = pkt.caplen
278
+ bufp = pkt.body_ptr
279
+ raise(ArgumentError, "packet data null pointer") if bufp.null?
280
+ elsif pkt.kind_of? String
281
+ len = pkt.size
282
+ bufp = FFI::MemoryPointer.from_string(pkt)
283
+ else
284
+ raise(ArgumentError, "Don't know how to send #{pkt.class}")
285
+ end
286
+
287
+ if FFI::PCap.pcap_sendpacket(_pcap, bufp, len) != 0
288
+ raise(LibError, "pcap_sendpacket(): #{geterr()}")
289
+ end
290
+ return true
291
+ end
292
+
293
+ alias send_packet sendpacket
294
+
295
+ end
296
+
297
+ attach_function :pcap_open_live, [:string, :int, :int, :int, :pointer], :pcap_t
298
+ attach_function :pcap_getnonblock, [:pcap_t, :pointer], :int
299
+ attach_function :pcap_setnonblock, [:pcap_t, :int, :pointer], :int
300
+ attach_function :pcap_stats, [:pcap_t, Stat], :int
301
+
302
+ end
303
+ end
@@ -0,0 +1,53 @@
1
+ require 'ffi/pcap/capture_wrapper'
2
+
3
+ module FFI
4
+ module PCap
5
+ # A wrapper class for pcap devices opened with open_offline()
6
+ #
7
+ class Offline < CaptureWrapper
8
+ attr_accessor :path
9
+
10
+ # Creates a pcap interface for reading saved capture files.
11
+ #
12
+ # @param [String] path
13
+ # The path to the file to open.
14
+ #
15
+ # @param [Hash] opts
16
+ # Options are ignored and passed to the super-class except for those
17
+ # below.
18
+ #
19
+ # @option opts [ignored] :path
20
+ # The :path option will be overridden with the value of the path
21
+ # argument. If specified in opts, its value will be ignored.
22
+ #
23
+ # @return [Offline]
24
+ # A FFI::PCap::Offline wrapper.
25
+ #
26
+ # @raise [LibError]
27
+ # On failure, an exception is raised with the relevant error
28
+ # message from libpcap.
29
+ #
30
+ def initialize(path, opts={}, &block)
31
+ @path = path
32
+ @errbuf = ErrorBuffer.create()
33
+ @pcap = FFI::PCap.pcap_open_offline(File.expand_path(@path), @errbuf)
34
+ raise(LibError, "pcap_open_offline(): #{@errbuf.to_s}") if @pcap.null?
35
+ super(@pcap, opts, &block)
36
+ end
37
+
38
+ def swapped?
39
+ FFI::PCap.pcap_is_swapped(_pcap) == 1 ? true : false
40
+ end
41
+
42
+ def file_version
43
+ "#{FFI::PCap.pcap_major_version(_pcap)}.#{FFI::PCap.pcap_minor_version(_pcap)}"
44
+ end
45
+ end
46
+
47
+ attach_function :pcap_open_offline, [:string, :pointer], :pcap_t
48
+ attach_function :pcap_is_swapped, [:pcap_t], :int
49
+ attach_function :pcap_major_version, [:pcap_t], :int
50
+ attach_function :pcap_minor_version, [:pcap_t], :int
51
+
52
+ end
53
+ end
@@ -0,0 +1,164 @@
1
+ module FFI
2
+ module PCap
3
+ class Packet
4
+ attr_reader :body_ptr, :header
5
+
6
+ # Creates a Packet from a Ruby string object.
7
+ #
8
+ # see new() for more information about the arguments.
9
+ def self.from_string(body, opts={})
10
+ new(nil, body, opts)
11
+ end
12
+
13
+ # Allocates a Packet using new memory. Used primarily for pcap_loop
14
+ # and pcap_dispatch to retain packets after new ones have been received
15
+ # or a pcap device is closed.
16
+ def self.allocate(phdr, buf)
17
+ new(phdr, buf).copy()
18
+ end
19
+
20
+ # @param [PacketHeader, nil] hdr
21
+ # The pcap pkthdr struct for this packet or nil. hdr may only be
22
+ # nil if a string is supplied for the body. A header will be
23
+ # created automatically and set_body called with opts.
24
+ #
25
+ # @param [FFI::Pointer, String] body
26
+ # A string or pointer for the body of the packet. A String may
27
+ # only be specified if hdr is set to nil.
28
+ #
29
+ # @param [optional, Hash] opts
30
+ # Specifies additional options at creation time. Only those
31
+ # below are applicable for all initiatialization styles.
32
+ # All other options are sent to set_body(), but only if the header
33
+ # is nil and body is a String. See set_body() for more info.
34
+ #
35
+ # @option opts [optional, Time] :time, :timestamp
36
+ # Sets the timestamp in the header.
37
+ #
38
+ # @raise [ArgumentError, TypeError]
39
+ # An exception is raised if any of the parameter rules described
40
+ # are not followed.
41
+ #
42
+ def initialize(hdr, body, opts={})
43
+ o = opts.dup
44
+ ts = o.delete(:time) || o.delete(:timestamp)
45
+ case hdr
46
+ when PacketHeader
47
+ raise(ArgumentError, "NULL header pointer") if hdr.to_ptr.null?
48
+ @header = hdr
49
+ when ::FFI::Pointer
50
+ raise(ArgumentError, "NULL header pointer") if hdr.null?
51
+ @header = PacketHeader.new(hdr)
52
+ when nil
53
+ if body.is_a? String
54
+ set_body(body, o)
55
+ else
56
+ raise(TypeError, "invalid body with nil header: #{body.class}")
57
+ end
58
+ else
59
+ raise(TypeError, "invalid header: #{hdr.class}")
60
+ end
61
+
62
+ @header.ts.time = ts if ts
63
+
64
+ unless @body_ptr
65
+ if body.is_a?(FFI::Pointer) and not body.null?
66
+ @body_ptr = body
67
+ else
68
+ raise(TypeError, "invalid body for header: #{body.class}")
69
+ end
70
+ end
71
+ end
72
+
73
+ # Sets the body from a string. A pointer is automatically derived from
74
+ #
75
+ # @param [String] data
76
+ # The body to set
77
+ #
78
+ # @param [Hash] opts
79
+ # Body length options.
80
+ #
81
+ # @option opts [optional, Integer] :caplen, :captured
82
+ # The captured length (or snaplen) for this packet.
83
+ # Length of data portion present. Defaults to body.size(). If
84
+ # caplen is larger than the body, then it is overridden with body.size.
85
+ #
86
+ # @option opts [optional, Integer] :len, :length
87
+ # The total length of the packet (off wire). Defaults to caplen. If
88
+ # If :length is less than the :caplen, it is overridden as :caplen.
89
+ #
90
+ #
91
+ # @return [String]
92
+ # Returns the data as supplied per attr_writer convention.
93
+ #
94
+ def set_body(data, opts={})
95
+ cl = opts[:caplen] || opts[:captured] || data.size
96
+ l = opts[:length] || opts[:len] || cl
97
+ clen = (cl < data.size) ? cl : data.size
98
+ len = (l < clen) ? clen : l
99
+
100
+ @header ||= PacketHeader.new
101
+ @header.caplen = len || @header.caplen
102
+ @header.len = len || @header.caplen
103
+ @body_ptr = FFI::MemoryPointer.from_string(data)
104
+ return self
105
+ end
106
+
107
+ alias body= set_body
108
+
109
+ # @return [String]
110
+ # A String representation of the packet data.
111
+ # The reference to the string is not kept by the object and changes
112
+ # won't affect the data in this packet.
113
+ def body
114
+ @body_ptr.read_string(@header.caplen)
115
+ end
116
+
117
+ # @return [Time]
118
+ # Returns the pcap timestamp as a Time object
119
+ def time
120
+ @header.ts.time
121
+ end
122
+
123
+ alias timestamp time
124
+
125
+ # Sets the pcap timestamp.
126
+ def time=(t)
127
+ @header.ts.time=(t)
128
+ end
129
+
130
+ def caplen
131
+ @header.caplen
132
+ end
133
+
134
+ alias captured caplen
135
+
136
+ def len
137
+ @header.len
138
+ end
139
+
140
+ alias length len
141
+
142
+ # An optimized copy which allocates new memory for a PacketHeader and
143
+ # body.
144
+ #
145
+ # DANGEROUS: This method uses direct FFI bindings for the copy and
146
+ # may crash Ruby if the packet header or body is incorrect.
147
+ #
148
+ # @raise [StandardError]
149
+ # An exception is raised if the header or body is a NULL pointer.
150
+ #
151
+ def copy
152
+ raise(StandardError, "header is a NULL pointer") if @header.to_ptr.null?
153
+ raise(StandardError, "body is a NULL pointer") if body_ptr.null?
154
+ cpy_hdr = PacketHeader.new
155
+ cpy_buf = FFI::MemoryPointer.new(@header.caplen)
156
+ CRT.memcpy(cpy_hdr, @header, PacketHeader.size)
157
+ CRT.memcpy(cpy_buf, @body_ptr, @header.caplen)
158
+ self.class.new( cpy_hdr, cpy_buf )
159
+ end
160
+
161
+ end
162
+
163
+ end
164
+ end