DIY-pcap 0.2.5 → 0.2.6
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.
- data/bin/pcap +2 -62
- data/bin/rpcap +2 -63
- data/lib/diy/command.rb +80 -0
- data/lib/diy/device_finder.rb +1 -1
- data/lib/diy/dig.rb +3 -1
- data/lib/diy/live.rb +5 -0
- data/lib/diy/parser/mu/fixnum_ext.rb +7 -0
- data/lib/diy/parser/mu/pcap/ethernet.rb +148 -0
- data/lib/diy/parser/mu/pcap/header.rb +75 -0
- data/lib/diy/parser/mu/pcap/io_pair.rb +67 -0
- data/lib/diy/parser/mu/pcap/io_wrapper.rb +76 -0
- data/lib/diy/parser/mu/pcap/ip.rb +61 -0
- data/lib/diy/parser/mu/pcap/ipv4.rb +257 -0
- data/lib/diy/parser/mu/pcap/ipv6.rb +148 -0
- data/lib/diy/parser/mu/pcap/packet.rb +104 -0
- data/lib/diy/parser/mu/pcap/pkthdr.rb +155 -0
- data/lib/diy/parser/mu/pcap/reader.rb +61 -0
- data/lib/diy/parser/mu/pcap/reader/http_family.rb +170 -0
- data/lib/diy/parser/mu/pcap/sctp.rb +367 -0
- data/lib/diy/parser/mu/pcap/sctp/chunk.rb +123 -0
- data/lib/diy/parser/mu/pcap/sctp/chunk/data.rb +134 -0
- data/lib/diy/parser/mu/pcap/sctp/chunk/init.rb +100 -0
- data/lib/diy/parser/mu/pcap/sctp/chunk/init_ack.rb +68 -0
- data/lib/diy/parser/mu/pcap/sctp/parameter.rb +110 -0
- data/lib/diy/parser/mu/pcap/sctp/parameter/ip_address.rb +48 -0
- data/lib/diy/parser/mu/pcap/stream_packetizer.rb +72 -0
- data/lib/diy/parser/mu/pcap/tcp.rb +505 -0
- data/lib/diy/parser/mu/pcap/udp.rb +69 -0
- data/lib/diy/parser/mu/scenario/pcap.rb +172 -0
- data/lib/diy/parser/mu/scenario/pcap/fields.rb +50 -0
- data/lib/diy/parser/mu/scenario/pcap/rtp.rb +71 -0
- data/lib/diy/parser/pcap.rb +113 -0
- data/lib/diy/parser/readme.md +72 -0
- data/lib/diy/utils.rb +9 -1
- data/lib/diy/version.rb +1 -1
- data/lib/diy/worker.rb +3 -2
- data/lib/diy/worker_keeper.rb +6 -0
- data/spec/helper/tcp.dat +0 -0
- data/spec/live_spec.rb +9 -0
- data/spec/mu_parser_spec.rb +12 -0
- data/spec/utils_spec.rb +1 -1
- metadata +34 -3
@@ -0,0 +1,67 @@
|
|
1
|
+
# http://www.mudynamics.com
|
2
|
+
# http://labs.mudynamics.com
|
3
|
+
# http://www.pcapr.net
|
4
|
+
|
5
|
+
module Mu
|
6
|
+
class Pcap
|
7
|
+
|
8
|
+
# For emulating of a pair of connected sockets. Bytes written
|
9
|
+
# with #write to one side are returned by a subsequent #read on
|
10
|
+
# the other side.
|
11
|
+
#
|
12
|
+
# Use Pair.stream_pair to get a pair with stream semantics.
|
13
|
+
# Use Pair.packet_pair to get a pair with packet semantics.
|
14
|
+
class IOPair
|
15
|
+
attr_reader :read_queue
|
16
|
+
attr_accessor :other
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.stream_pair
|
23
|
+
io1 = Stream.new
|
24
|
+
io2 = Stream.new
|
25
|
+
io1.other = io2
|
26
|
+
io2.other = io1
|
27
|
+
return io1, io2
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.packet_pair
|
31
|
+
io1 = Packet.new
|
32
|
+
io2 = Packet.new
|
33
|
+
io1.other = io2
|
34
|
+
io2.other = io1
|
35
|
+
return io1, io2
|
36
|
+
end
|
37
|
+
|
38
|
+
def write bytes
|
39
|
+
@other.read_queue << bytes
|
40
|
+
bytes.size
|
41
|
+
end
|
42
|
+
|
43
|
+
class Stream < IOPair
|
44
|
+
def initialize
|
45
|
+
@read_queue = ""
|
46
|
+
end
|
47
|
+
|
48
|
+
def read n=nil
|
49
|
+
n ||= @read_queue.size
|
50
|
+
@read_queue.slice!(0,n)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Packet < IOPair
|
55
|
+
def initialize
|
56
|
+
@read_queue = []
|
57
|
+
end
|
58
|
+
|
59
|
+
def read
|
60
|
+
@read_queue.shift
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# http://www.mudynamics.com
|
2
|
+
# http://labs.mudynamics.com
|
3
|
+
# http://www.pcapr.net
|
4
|
+
|
5
|
+
require 'mu/pcap/io_pair'
|
6
|
+
|
7
|
+
module Mu
|
8
|
+
class Pcap
|
9
|
+
class IOWrapper
|
10
|
+
attr_reader :ios, :unread, :state
|
11
|
+
|
12
|
+
def initialize ios, reader
|
13
|
+
@ios = ios
|
14
|
+
@reader = reader
|
15
|
+
# parse state for reader
|
16
|
+
@state = {}
|
17
|
+
# read off of underlying io but not yet processed by @reader
|
18
|
+
@unread = ""
|
19
|
+
end
|
20
|
+
|
21
|
+
# Impose upper limit to protect against memory exhaustion.
|
22
|
+
MAX_RECEIVE_SIZE = 1048576 # 1MB
|
23
|
+
|
24
|
+
# Returns next higher level protocol message.
|
25
|
+
def read
|
26
|
+
until message = @reader.read_message!(@unread, @state)
|
27
|
+
bytes = @ios.read
|
28
|
+
if bytes and not bytes.empty?
|
29
|
+
@unread << bytes
|
30
|
+
else
|
31
|
+
return nil
|
32
|
+
end
|
33
|
+
if @unread.size > MAX_RECEIVE_SIZE
|
34
|
+
raise "Maximum message size (#{MAX_RECEIVE_SIZE}) exceeded"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
return message
|
39
|
+
end
|
40
|
+
|
41
|
+
# Parser may need to see requests to understand responses.
|
42
|
+
def record_write bytes
|
43
|
+
@reader.record_write bytes, @state
|
44
|
+
end
|
45
|
+
|
46
|
+
def write bytes, *args
|
47
|
+
w = @ios.write bytes, *args
|
48
|
+
record_write bytes
|
49
|
+
w
|
50
|
+
end
|
51
|
+
|
52
|
+
def write_to bytes, *args
|
53
|
+
w = @ios.write_to bytes, *args
|
54
|
+
record_write bytes
|
55
|
+
w
|
56
|
+
end
|
57
|
+
|
58
|
+
def open
|
59
|
+
if block_given?
|
60
|
+
@ios.open { yield }
|
61
|
+
else
|
62
|
+
@ios.open
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def open?
|
67
|
+
@ios.open?
|
68
|
+
end
|
69
|
+
|
70
|
+
def close
|
71
|
+
@ios.close
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# http://www.mudynamics.com
|
2
|
+
# http://labs.mudynamics.com
|
3
|
+
# http://www.pcapr.net
|
4
|
+
|
5
|
+
module Mu
|
6
|
+
class Pcap
|
7
|
+
|
8
|
+
class IP < Packet
|
9
|
+
IPPROTO_TCP = 6
|
10
|
+
IPPROTO_UDP = 17
|
11
|
+
IPPROTO_HOPOPTS = 0
|
12
|
+
IPPROTO_ROUTING = 43
|
13
|
+
IPPROTO_FRAGMENT = 44
|
14
|
+
IPPROTO_AH = 51
|
15
|
+
IPPROTO_NONE = 59
|
16
|
+
IPPROTO_DSTOPTS = 60
|
17
|
+
IPPROTO_SCTP = 132
|
18
|
+
|
19
|
+
attr_accessor :src, :dst
|
20
|
+
|
21
|
+
def initialize src=nil, dst=nil
|
22
|
+
super()
|
23
|
+
@src = src
|
24
|
+
@dst = dst
|
25
|
+
end
|
26
|
+
|
27
|
+
def v4?
|
28
|
+
return false
|
29
|
+
end
|
30
|
+
|
31
|
+
def v6?
|
32
|
+
return false
|
33
|
+
end
|
34
|
+
|
35
|
+
def proto
|
36
|
+
raise NotImplementedError
|
37
|
+
end
|
38
|
+
|
39
|
+
def pseudo_header payload_length
|
40
|
+
raise NotImplementedError
|
41
|
+
end
|
42
|
+
|
43
|
+
def == other
|
44
|
+
return super &&
|
45
|
+
self.src == other.src &&
|
46
|
+
self.dst == other.dst
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.checksum bytes
|
50
|
+
if bytes.size & 1 == 1
|
51
|
+
bytes = bytes + "\0"
|
52
|
+
end
|
53
|
+
sum = 0
|
54
|
+
bytes.unpack("n*").each {|n| sum += n }
|
55
|
+
sum = (sum & 0xffff) + (sum >> 16 & 0xffff)
|
56
|
+
~sum & 0xffff
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,257 @@
|
|
1
|
+
# http://www.mudynamics.com
|
2
|
+
# http://labs.mudynamics.com
|
3
|
+
# http://www.pcapr.net
|
4
|
+
|
5
|
+
require 'ipaddr'
|
6
|
+
|
7
|
+
module Mu
|
8
|
+
class Pcap
|
9
|
+
|
10
|
+
class IPv4 < IP
|
11
|
+
IP_RF = 0x8000 # Reserved
|
12
|
+
IP_DF = 0x4000 # Don't fragment
|
13
|
+
IP_MF = 0x2000 # More fragments
|
14
|
+
IP_OFFMASK = 0x1fff
|
15
|
+
|
16
|
+
FMT_HEADER = 'CCnnnCCna4a4'
|
17
|
+
|
18
|
+
attr_accessor :ip_id, :offset, :ttl, :proto, :src, :dst, :dscp
|
19
|
+
|
20
|
+
def initialize src=nil, dst=nil, ip_id=0, offset=0, ttl=64, proto=0, dscp=0
|
21
|
+
super()
|
22
|
+
@ip_id = ip_id
|
23
|
+
@offset = offset
|
24
|
+
@ttl = ttl
|
25
|
+
@proto = proto
|
26
|
+
@src = src
|
27
|
+
@dst = dst
|
28
|
+
@dscp = dscp
|
29
|
+
end
|
30
|
+
|
31
|
+
def v4?
|
32
|
+
return true
|
33
|
+
end
|
34
|
+
|
35
|
+
def flow_id
|
36
|
+
if not @payload or @payload.is_a? String
|
37
|
+
return [:ipv4, @proto, @src, @dst]
|
38
|
+
else
|
39
|
+
return [:ipv4, @src, @dst, @payload.flow_id]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
NTOP = {} # Network to human cache
|
44
|
+
HTON = {} # Human to network cache
|
45
|
+
|
46
|
+
def self.from_bytes bytes
|
47
|
+
bytes.length >= 20 or
|
48
|
+
raise ParseError, "Truncated IPv4 header: expected at least 20 bytes, got #{bytes.length} bytes"
|
49
|
+
|
50
|
+
vhl, tos, length, id, offset, ttl, proto, checksum, src, dst = bytes[0,20].unpack FMT_HEADER
|
51
|
+
version = vhl >> 4
|
52
|
+
hl = (vhl & 0b1111) * 4
|
53
|
+
|
54
|
+
version == 4 or
|
55
|
+
raise ParseError, "Wrong IPv4 version: got (#{version})"
|
56
|
+
hl >= 20 or
|
57
|
+
raise ParseError, "Bad IPv4 header length: expected at least 20 bytes raise ParseError, got #{hl} bytes"
|
58
|
+
bytes.length >= hl or
|
59
|
+
raise ParseError, "Truncated IPv4 header: expected #{hl} bytes raise ParseError, got #{bytes.length} bytes"
|
60
|
+
length >= 20 or
|
61
|
+
raise ParseError, "Bad IPv4 packet length: expected at least 20 bytes raise ParseError, got #{length} bytes"
|
62
|
+
bytes.length >= length or
|
63
|
+
raise ParseError, "Truncated IPv4 packet: expected #{length} bytes raise ParseError, got #{bytes.length} bytes"
|
64
|
+
|
65
|
+
if hl != 20
|
66
|
+
IPv4.check_options bytes[20, hl-20]
|
67
|
+
end
|
68
|
+
|
69
|
+
src = NTOP[src] ||= IPAddr.ntop(src)
|
70
|
+
dst = NTOP[dst] ||= IPAddr.ntop(dst)
|
71
|
+
dscp = tos >> 2
|
72
|
+
ipv4 = IPv4.new(src, dst, id, offset, ttl, proto, dscp)
|
73
|
+
ipv4.payload_raw = bytes[hl..-1]
|
74
|
+
|
75
|
+
payload = bytes[hl...length]
|
76
|
+
if offset & (IP_OFFMASK | IP_MF) == 0
|
77
|
+
begin
|
78
|
+
case proto
|
79
|
+
when IPPROTO_TCP
|
80
|
+
ipv4.payload = TCP.from_bytes payload
|
81
|
+
when IPPROTO_UDP
|
82
|
+
ipv4.payload = UDP.from_bytes payload
|
83
|
+
when IPPROTO_SCTP
|
84
|
+
ipv4.payload = SCTP.from_bytes payload
|
85
|
+
else
|
86
|
+
ipv4.payload = payload
|
87
|
+
end
|
88
|
+
rescue ParseError => e
|
89
|
+
Pcap.warning e
|
90
|
+
end
|
91
|
+
else
|
92
|
+
ipv4.payload = payload
|
93
|
+
end
|
94
|
+
return ipv4
|
95
|
+
end
|
96
|
+
|
97
|
+
def write io
|
98
|
+
if @payload.is_a? String
|
99
|
+
payload = @payload
|
100
|
+
else
|
101
|
+
string_io = StringIO.new
|
102
|
+
@payload.write string_io, self
|
103
|
+
payload = string_io.string
|
104
|
+
end
|
105
|
+
length = 20 + payload.length
|
106
|
+
if length > 65535
|
107
|
+
Pcap.warning "IPv4 payload is too large"
|
108
|
+
end
|
109
|
+
|
110
|
+
src = HTON[@src] ||= IPAddr.new(@src).hton
|
111
|
+
dst = HTON[@dst] ||= IPAddr.new(@dst).hton
|
112
|
+
fields = [0x45, @dscp << 2, length, @ip_id, @offset, @ttl, @proto, 0, src, dst]
|
113
|
+
header = fields.pack(FMT_HEADER)
|
114
|
+
fields[7] = IP.checksum(header)
|
115
|
+
header = fields.pack(FMT_HEADER)
|
116
|
+
io.write header
|
117
|
+
io.write payload
|
118
|
+
end
|
119
|
+
|
120
|
+
FMT_PSEUDO_HEADER = 'a4a4CCn'
|
121
|
+
def pseudo_header payload_length
|
122
|
+
src = HTON[@src] ||= IPAddr.new(@src).hton
|
123
|
+
dst = HTON[@dst] ||= IPAddr.new(@dst).hton
|
124
|
+
return [src, dst, 0, @proto, payload_length].pack(FMT_PSEUDO_HEADER)
|
125
|
+
end
|
126
|
+
|
127
|
+
def fragment?
|
128
|
+
return (@offset & (IP_OFFMASK | IP_MF) != 0)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Check that IP or TCP options are valid. Do nothing if they are valid.
|
132
|
+
# Both IP and TCP options are 8-bit TLVs with an inclusive length. Both
|
133
|
+
# have one byte options 0 and 1.
|
134
|
+
def self.check_options options, label='IPv4'
|
135
|
+
while not options.empty?
|
136
|
+
type = options.slice!(0, 1)[0].ord
|
137
|
+
if type == 0 or type == 1
|
138
|
+
next
|
139
|
+
end
|
140
|
+
Pcap.assert !options.empty?,
|
141
|
+
"#{label} option #{type} is missing the length field"
|
142
|
+
length = options.slice!(0, 1)[0].ord
|
143
|
+
Pcap.assert length >= 2,
|
144
|
+
"#{label} option #{type} has invalid length: #{length}"
|
145
|
+
Pcap.assert length - 2 <= options.length,
|
146
|
+
"#{label} option #{type} has truncated data"
|
147
|
+
options.slice! 0, length - 2
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
ReassembleState = ::Struct.new :packets, :bytes, :mf, :overlap
|
152
|
+
|
153
|
+
# Reassemble fragmented IPv4 packets
|
154
|
+
def self.reassemble packets
|
155
|
+
reassembled_packets = []
|
156
|
+
flow_id_to_state = {}
|
157
|
+
packets.each do |packet|
|
158
|
+
if not packet.is_a?(Ethernet) or not packet.payload.is_a?(IPv4)
|
159
|
+
# Ignore non-IPv4 packet
|
160
|
+
elsif not packet.payload.fragment?
|
161
|
+
# Ignore non-fragments
|
162
|
+
else
|
163
|
+
# Get reassembly state
|
164
|
+
ip = packet.payload
|
165
|
+
flow_id = [ip.ip_id, ip.proto, ip.src, ip.dst]
|
166
|
+
state = flow_id_to_state[flow_id]
|
167
|
+
if not state
|
168
|
+
state = ReassembleState.new [], [], true, false
|
169
|
+
flow_id_to_state[flow_id] = state
|
170
|
+
end
|
171
|
+
state.packets << packet
|
172
|
+
|
173
|
+
# Clear the more-fragments flag if no more fragments
|
174
|
+
if ip.offset & IP_MF == 0
|
175
|
+
state.mf = false
|
176
|
+
end
|
177
|
+
|
178
|
+
# Add the bytes
|
179
|
+
start = (ip.offset & IP_OFFMASK) * 8
|
180
|
+
finish = start + ip.payload.length
|
181
|
+
state.bytes.fill nil, start, finish - start
|
182
|
+
start.upto(finish-1) do |i|
|
183
|
+
if not state.bytes[i]
|
184
|
+
byte = ip.payload[i - start].chr
|
185
|
+
state.bytes[i] = byte
|
186
|
+
elsif not state.overlap
|
187
|
+
name = "%s:%s:%d" % [ip.src, ip.dst, ip.proto]
|
188
|
+
Pcap.warning \
|
189
|
+
"IPv4 flow #{name} contains overlapping fragements"
|
190
|
+
state.overlap = true
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# We're done if we've received a fragment without the
|
195
|
+
# more-fragments flag and all the bytes in the buffer have been
|
196
|
+
# set.
|
197
|
+
if not state.mf and state.bytes.all?
|
198
|
+
# Remove fragments from reassembled_packets
|
199
|
+
state.packets.each do |packet|
|
200
|
+
reassembled_packets.delete_if do |reassembled_packet|
|
201
|
+
packet.object_id == reassembled_packet.object_id
|
202
|
+
end
|
203
|
+
end
|
204
|
+
# Remove state
|
205
|
+
flow_id_to_state.delete flow_id
|
206
|
+
# Create new packet
|
207
|
+
packet = state.packets[0].deepdup
|
208
|
+
ipv4 = packet.payload
|
209
|
+
ipv4.offset = 0
|
210
|
+
ipv4.payload = state.bytes.join
|
211
|
+
# Decode
|
212
|
+
begin
|
213
|
+
case ipv4.proto
|
214
|
+
when IPPROTO_TCP
|
215
|
+
ipv4.payload = TCP.from_bytes ipv4.payload
|
216
|
+
when IPPROTO_UDP
|
217
|
+
ipv4.payload = UDP.from_bytes ipv4.payload
|
218
|
+
when IPPROTO_SCTP
|
219
|
+
ipv4.payload = SCTP.from_bytes ipv4.payload
|
220
|
+
end
|
221
|
+
rescue ParseError => e
|
222
|
+
Pcap.warning e
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
reassembled_packets << packet
|
227
|
+
end
|
228
|
+
if !flow_id_to_state.empty?
|
229
|
+
Pcap.warning \
|
230
|
+
"#{flow_id_to_state.length} flow(s) have IPv4 fragments " \
|
231
|
+
"that can't be reassembled"
|
232
|
+
end
|
233
|
+
|
234
|
+
return reassembled_packets
|
235
|
+
end
|
236
|
+
|
237
|
+
def to_s
|
238
|
+
if @payload.is_a? String
|
239
|
+
payload = @payload.inspect
|
240
|
+
else
|
241
|
+
payload = @payload.to_s
|
242
|
+
end
|
243
|
+
return "ipv4(%d, %s, %s, %s)" % [@proto, @src, @dst, payload]
|
244
|
+
end
|
245
|
+
|
246
|
+
def == other
|
247
|
+
return super &&
|
248
|
+
self.proto == other.proto &&
|
249
|
+
self.ip_id == other.ip_id &&
|
250
|
+
self.offset == other.offset &&
|
251
|
+
self.ttl == other.ttl &&
|
252
|
+
self.dscp == other.dscp
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
end
|
257
|
+
end
|