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.
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