ffi-pcap 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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