caper 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 +9 -0
  2. data/History.rdoc +31 -0
  3. data/LICENSE.ffi-pcap +23 -0
  4. data/README.rdoc +55 -0
  5. data/Rakefile +26 -0
  6. data/VERSION +1 -0
  7. data/examples/ipfw_divert.rb +45 -0
  8. data/examples/print_bytes.rb +17 -0
  9. data/examples/test_loop.rb +48 -0
  10. data/lib/caper.rb +40 -0
  11. data/lib/caper/addr.rb +19 -0
  12. data/lib/caper/bpf.rb +104 -0
  13. data/lib/caper/bsd.rb +96 -0
  14. data/lib/caper/capture_wrapper.rb +287 -0
  15. data/lib/caper/common_wrapper.rb +173 -0
  16. data/lib/caper/copy_handler.rb +36 -0
  17. data/lib/caper/crt.rb +13 -0
  18. data/lib/caper/data_link.rb +171 -0
  19. data/lib/caper/dead.rb +35 -0
  20. data/lib/caper/dumper.rb +53 -0
  21. data/lib/caper/error_buffer.rb +42 -0
  22. data/lib/caper/exceptions.rb +19 -0
  23. data/lib/caper/file_header.rb +24 -0
  24. data/lib/caper/in_addr.rb +7 -0
  25. data/lib/caper/interface.rb +27 -0
  26. data/lib/caper/live.rb +301 -0
  27. data/lib/caper/offline.rb +51 -0
  28. data/lib/caper/packet.rb +163 -0
  29. data/lib/caper/packet_header.rb +23 -0
  30. data/lib/caper/pcap.rb +250 -0
  31. data/lib/caper/stat.rb +56 -0
  32. data/lib/caper/time_val.rb +46 -0
  33. data/lib/caper/typedefs.rb +25 -0
  34. data/lib/caper/version.rb +4 -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 +110 -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 +16 -0
  53. metadata +140 -0
@@ -0,0 +1,51 @@
1
+ require 'caper/capture_wrapper'
2
+
3
+ module Caper
4
+ # A wrapper class for pcap devices opened with open_offline()
5
+ #
6
+ class Offline < CaptureWrapper
7
+ attr_accessor :path
8
+
9
+ # Creates a pcap interface for reading saved capture files.
10
+ #
11
+ # @param [String] path
12
+ # The path to the file to open.
13
+ #
14
+ # @param [Hash] opts
15
+ # Options are ignored and passed to the super-class except for those
16
+ # below.
17
+ #
18
+ # @option opts [ignored] :path
19
+ # The :path option will be overridden with the value of the path
20
+ # argument. If specified in opts, its value will be ignored.
21
+ #
22
+ # @return [Offline]
23
+ # A Caper::Offline wrapper.
24
+ #
25
+ # @raise [LibError]
26
+ # On failure, an exception is raised with the relevant error
27
+ # message from libpcap.
28
+ #
29
+ def initialize(path, opts={}, &block)
30
+ @path = path
31
+ @errbuf = ErrorBuffer.create()
32
+ @pcap = Caper.pcap_open_offline(File.expand_path(@path), @errbuf)
33
+ raise(LibError, "pcap_open_offline(): #{@errbuf.to_s}") if @pcap.null?
34
+ super(@pcap, opts, &block)
35
+ end
36
+
37
+ def swapped?
38
+ Caper.pcap_is_swapped(_pcap) == 1 ? true : false
39
+ end
40
+
41
+ def file_version
42
+ "#{Caper.pcap_major_version(_pcap)}.#{Caper.pcap_minor_version(_pcap)}"
43
+ end
44
+ end
45
+
46
+ attach_function :pcap_open_offline, [:string, :pointer], :pcap_t
47
+ attach_function :pcap_is_swapped, [:pcap_t], :int
48
+ attach_function :pcap_major_version, [:pcap_t], :int
49
+ attach_function :pcap_minor_version, [:pcap_t], :int
50
+
51
+ end
@@ -0,0 +1,163 @@
1
+
2
+ module Caper
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
@@ -0,0 +1,23 @@
1
+ module Caper
2
+
3
+ # Generic per-packet information, as supplied by libpcap. This structure
4
+ # is used to track only the libpcap header and just contains the timestamp
5
+ # and length information used by libpcap.
6
+ #
7
+ # See pcap_pkthdr struct in pcap.h
8
+ class PacketHeader < FFI::Struct
9
+ include FFI::DRY::StructHelper
10
+
11
+ dsl_layout do
12
+ struct :ts, ::Caper::TimeVal, :desc => 'time stamp'
13
+ field :caplen, :bpf_uint32, :desc => 'length of portion present'
14
+ field :len, :bpf_uint32, :desc => 'length of packet (off wire)'
15
+ end
16
+
17
+ alias timestamp ts
18
+ alias captured caplen
19
+ alias length len
20
+
21
+ end
22
+
23
+ end
@@ -0,0 +1,250 @@
1
+ require 'enumerator'
2
+
3
+ module Caper
4
+ DEFAULT_SNAPLEN = 65535 # Default snapshot length for packets
5
+
6
+ attach_function :pcap_lookupdev, [:pointer], :string
7
+
8
+ # Find the default device on which to capture.
9
+ #
10
+ # @return [String]
11
+ # Name of default device
12
+ #
13
+ # @raise [LibError]
14
+ # On failure, an exception is raised with the relevant error
15
+ # message from libpcap.
16
+ #
17
+ def Caper.lookupdev
18
+ e = ErrorBuffer.create()
19
+ unless name = Caper.pcap_lookupdev(e)
20
+ raise(LibError, "pcap_lookupdev(): #{e.to_s}")
21
+ end
22
+ return name
23
+ end
24
+
25
+
26
+ attach_function :pcap_lookupnet, [:string, :pointer, :pointer, :pointer], :int
27
+
28
+ # Determine the IPv4 network number and mask relevant with a network
29
+ # device.
30
+ #
31
+ # @param [String] device
32
+ # The name of the device to look up.
33
+ #
34
+ # @yield [netp, maskp]
35
+ #
36
+ # @yieldparam [FFI::MemoryPointer] netp
37
+ # A pointer to the network return value.
38
+ #
39
+ # @yieldparam [FFI::MemoryPointer] maskp
40
+ # A pointer to the netmask return value.
41
+ #
42
+ # @return [nil, String]
43
+ # The IPv4 network number and mask presented as "n.n.n.n/m.m.m.m".
44
+ # nil is returned when a block is specified.
45
+ #
46
+ # @raise [LibError]
47
+ # On failure, an exception is raised with the relevant error message
48
+ # from libpcap.
49
+ #
50
+ def Caper.lookupnet(device)
51
+ netp = FFI::MemoryPointer.new(find_type(:bpf_uint32))
52
+ maskp = FFI::MemoryPointer.new(find_type(:bpf_uint32))
53
+ errbuf = ErrorBuffer.create()
54
+ unless Caper.pcap_lookupnet(device, netp, maskp, errbuf) == 0
55
+ raise(LibError, "pcap_lookupnet(): #{errbuf.to_s}")
56
+ end
57
+ if block_given?
58
+ yield(netp, maskp)
59
+ else
60
+ return( netp.get_array_of_uchar(0,4).join('.') << "/" <<
61
+ maskp.get_array_of_uchar(0,4).join('.') )
62
+ end
63
+ end
64
+
65
+
66
+ # Opens a new Live device for capturing from the network. See Live.new()
67
+ # for arguments.
68
+ #
69
+ # If passed a block, the block is passed to Live.new() and the Live
70
+ # object is closed after completion of the block
71
+ def Caper.open_live(opts={},&block)
72
+ ret = Live.new(opts, &block)
73
+ return block_given? ? ret.close : ret
74
+ end
75
+
76
+
77
+ # Opens a new Dead pcap interface for compiling filters or opening
78
+ # a capture for output. See Dead.new() for arguments.
79
+ def Caper.open_dead(opts={}, &block)
80
+ ret = Dead.new(opts, &block)
81
+ return block_given? ? ret.close : ret
82
+ end
83
+
84
+
85
+ # Opens a saved capture file for reading. See Offline.new for arguments.
86
+ def Caper.open_offline(path, opts={}, &block)
87
+ ret = Offline.new(path, opts={}, &block)
88
+ return block_given? ? ret.close : ret
89
+ end
90
+
91
+ # Same as open_offline
92
+ def Caper.open_file(path, opts={}, &block)
93
+ open_offline(path, opts, &block)
94
+ end
95
+
96
+ attach_function :pcap_findalldevs, [:pointer, :pointer], :int
97
+ attach_function :pcap_freealldevs, [Interface], :void
98
+
99
+ # List all capture devices and yield them each to a block
100
+ #
101
+ # @yield [dev]
102
+ #
103
+ # @yieldparam [Interface] dev
104
+ # An Interface structure for each device.
105
+ #
106
+ # @return [nil]
107
+ #
108
+ # @raise [LibError]
109
+ # On failure, an exception is raised with the relevant error
110
+ # message from libpcap.
111
+ #
112
+ def Caper.each_device
113
+ devices = ::FFI::MemoryPointer.new(:pointer)
114
+ errbuf = ErrorBuffer.create()
115
+
116
+ Caper.pcap_findalldevs(devices, errbuf)
117
+ node = devices.get_pointer(0)
118
+
119
+ if node.null?
120
+ raise(LibError, "pcap_findalldevs(): #{errbuf.to_s}")
121
+ end
122
+
123
+ device = Interface.new(node)
124
+
125
+ while device
126
+ yield(device)
127
+ device = device.next
128
+ end
129
+
130
+ Caper.pcap_freealldevs(node)
131
+ return nil
132
+ end
133
+
134
+
135
+ # Returns an array of device name and network/netmask pairs for
136
+ # each interface found on the system.
137
+ #
138
+ # If an interface does not have an address assigned, its network/netmask
139
+ # value is returned as a nil value.
140
+ def Caper.dump_devices
141
+ Caper.enum_for(:each_device).map do |dev|
142
+ net = begin; Caper.lookupnet(dev.name); rescue LibError; end
143
+ [dev.name, net]
144
+ end
145
+ end
146
+
147
+
148
+ # Returns an array of device names for each interface found on the system.
149
+ def Caper.device_names
150
+ Caper.enum_for(:each_device).map {|dev| dev.name }
151
+ end
152
+
153
+ attach_function :pcap_lib_version, [], :string
154
+
155
+
156
+ # Get the version information for libpcap.
157
+ #
158
+ # @return [String]
159
+ # Information about the version of the libpcap library being used;
160
+ # note that it contains more information than just a version number.
161
+ #
162
+ def Caper.lib_version
163
+ Caper.pcap_lib_version
164
+ end
165
+
166
+
167
+ # Extract just the version number from the lib_version string.
168
+ #
169
+ # @return [String]
170
+ # Version number.
171
+ #
172
+ def Caper.lib_version_number
173
+ if lib_version() =~ /libpcap version (\d+\.\d+.\d+)/
174
+ return $1
175
+ end
176
+ end
177
+
178
+
179
+ # Unix Only:
180
+ begin
181
+
182
+ attach_function :pcap_get_selectable_fd, [:pcap_t], :int
183
+
184
+
185
+ # Drops privileges back to the uid of the SUDO_USER environment
186
+ # variable.
187
+ #
188
+ # Only available on Unix.
189
+ #
190
+ # This is useful for the paranoid when sudo is used to run a
191
+ # ruby pcap program as root.
192
+ #
193
+ # This method can generally be called right after a call to
194
+ # open_live() has returned a pcap handle or another privileged
195
+ # call has completed. Note, however, that once privileges are
196
+ # dropped, pcap functions that a require higher privilege will
197
+ # no longer work.
198
+ #
199
+ # @raise [StandardError]
200
+ # An error is raised if privileges cannot be dropped for
201
+ # some reason. This may be because the SUDO_USER environment
202
+ # variable is not set, because we already have a lower
203
+ # privilige and the SUDO_USER id is not the current uid,
204
+ # or because the SUDO_USER environment variable is not
205
+ # a valid user.
206
+ #
207
+ def Caper.drop_sudo_privs
208
+ if ENV["SUDO_USER"] and pwent=Etc.getpwnam(ENV["SUDO_USER"])
209
+ Process::Sys.setgid(pwent.gid)
210
+ Process::Sys.setegid(pwent.gid)
211
+ Process::Sys.setuid(pwent.uid)
212
+ Process::Sys.seteuid(pwent.uid)
213
+ return true if (
214
+ Process::Sys.getuid == pwent.uid and
215
+ Process::Sys.geteuid == pwent.uid and
216
+ Process::Sys.getgid == pwent.gid and
217
+ Process::Sys.getegid == pwent.gid )
218
+ end
219
+ raise(StandardError, "Unable to drop privileges")
220
+ end
221
+
222
+ rescue FFI::NotFoundError
223
+ $pcap_not_unix=true
224
+ end
225
+
226
+ # Win32 only:
227
+ begin
228
+ attach_function :pcap_setbuff, [:pcap_t, :int], :int
229
+ attach_function :pcap_setmode, [:pcap_t, :pcap_w32_modes_enum], :int
230
+ attach_function :pcap_setmintocopy, [:pcap_t, :int], :int
231
+ rescue FFI::NotFoundError
232
+ $pcap_not_win32=true
233
+ end if $pcap_not_unix
234
+
235
+ # MSDOS only???:
236
+ begin
237
+ attach_function :pcap_stats_ex, [:pcap_t, StatEx], :int
238
+ attach_function :pcap_set_wait, [:pcap_t, :pointer, :int], :void
239
+ attach_function :pcap_mac_packets, [], :ulong
240
+ rescue FFI::NotFoundError
241
+ end if $pcap_not_win32
242
+
243
+
244
+ #### XXX not sure if we even want FILE io stuff yet (or ever).
245
+
246
+ #attach_function :pcap_fopen_offline, [:FILE, :pointer], :pcap_t
247
+ #attach_function :pcap_file, [:pcap_t], :FILE
248
+ #attach_function :pcap_dump_fopen, [:pcap_t, :FILE], :pcap_dumper_t
249
+ #attach_function :pcap_fileno, [:pcap_t], :int
250
+ end