tracepath 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE +21 -0
- data/README.md +221 -0
- data/docs/platforms.md +94 -0
- data/docs/releasing.md +16 -0
- data/docs/verification.md +116 -0
- data/exe/tracepath +8 -0
- data/lib/tracepath/cli/elevated.rb +27 -0
- data/lib/tracepath/cli/privilege_check.rb +65 -0
- data/lib/tracepath/cli/privileges.rb +76 -0
- data/lib/tracepath/cli/text_output.rb +122 -0
- data/lib/tracepath/cli.rb +254 -0
- data/lib/tracepath/errors.rb +33 -0
- data/lib/tracepath/models.rb +138 -0
- data/lib/tracepath/options.rb +89 -0
- data/lib/tracepath/packet.rb +433 -0
- data/lib/tracepath/pcap.rb +219 -0
- data/lib/tracepath/resolver.rb +82 -0
- data/lib/tracepath/runner.rb +248 -0
- data/lib/tracepath/transport.rb +705 -0
- data/lib/tracepath/version.rb +5 -0
- data/lib/tracepath.rb +110 -0
- metadata +126 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
|
|
5
|
+
module Tracepath
|
|
6
|
+
class Options
|
|
7
|
+
DEFAULTS = {
|
|
8
|
+
protocol: :udp, family: :auto, first_ttl: 1, max_hops: 30, probes: 3,
|
|
9
|
+
max_inflight: 3, timeout: 3.0, max_duration: 120.0, dns_timeout: 3.0,
|
|
10
|
+
reverse_dns: false, ptr_timeout: 1.0, send_interval: 0.05, port: nil,
|
|
11
|
+
source: nil, interface: nil, traffic_class: 0, packet_size: nil, dont_fragment: false
|
|
12
|
+
}.freeze
|
|
13
|
+
attr_reader(*DEFAULTS.keys)
|
|
14
|
+
|
|
15
|
+
def initialize(**options)
|
|
16
|
+
unknown = options.keys - DEFAULTS.keys
|
|
17
|
+
raise ArgumentError, "unknown options: #{unknown.join(', ')}" unless unknown.empty?
|
|
18
|
+
values = DEFAULTS.merge(options)
|
|
19
|
+
DEFAULTS.each_key { |key| instance_variable_set("@#{key}", Values.copy(values[key])) }
|
|
20
|
+
validate!
|
|
21
|
+
@port ||= protocol == :tcp ? 443 : (protocol == :udp ? 33_434 : nil)
|
|
22
|
+
if protocol == :udp && @port + (max_hops - first_ttl + 1) * probes - 1 > 65_535
|
|
23
|
+
raise ArgumentError, "UDP probe ports would exceed 65535"
|
|
24
|
+
end
|
|
25
|
+
freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_h = DEFAULTS.keys.to_h { |key| [key, public_send(key)] }
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def validate!
|
|
33
|
+
raise ArgumentError, "protocol must be udp, icmp, or tcp" unless %i[udp icmp tcp].include?(protocol)
|
|
34
|
+
raise ArgumentError, "family must be auto, ipv4, or ipv6" unless %i[auto ipv4 ipv6].include?(family)
|
|
35
|
+
{ first_ttl: 1..255, max_hops: 1..255, probes: 1..255, max_inflight: 1..255,
|
|
36
|
+
traffic_class: 0..255 }.each do |key, range|
|
|
37
|
+
value = public_send(key)
|
|
38
|
+
raise ArgumentError, "#{key} must be an integer in #{range}" unless value.is_a?(Integer) && range.cover?(value)
|
|
39
|
+
end
|
|
40
|
+
raise ArgumentError, "first_ttl must not exceed max_hops" if first_ttl > max_hops
|
|
41
|
+
%i[timeout max_duration dns_timeout ptr_timeout send_interval].each do |key|
|
|
42
|
+
value = public_send(key)
|
|
43
|
+
valid = value.is_a?(Numeric) && value.real? && value.finite? && (key == :send_interval ? value >= 0 : value > 0)
|
|
44
|
+
raise ArgumentError, "#{key} must be a finite #{key == :send_interval ? 'nonnegative' : 'positive'} number" unless valid
|
|
45
|
+
end
|
|
46
|
+
%i[reverse_dns dont_fragment].each do |key|
|
|
47
|
+
raise ArgumentError, "#{key} must be true or false" unless [true, false].include?(public_send(key))
|
|
48
|
+
end
|
|
49
|
+
if !port.nil? && (!port.is_a?(Integer) || !(1..65_535).cover?(port))
|
|
50
|
+
raise ArgumentError, "port must be an integer in 1..65535"
|
|
51
|
+
end
|
|
52
|
+
raise ArgumentError, "port does not apply to ICMP" if protocol == :icmp && port
|
|
53
|
+
if !packet_size.nil? && (!packet_size.is_a?(Integer) || !(28..65_535).cover?(packet_size))
|
|
54
|
+
raise ArgumentError, "packet_size must be an integer in 28..65535"
|
|
55
|
+
end
|
|
56
|
+
minimum_size = (family == :ipv6 ? 40 : 20) + (protocol == :tcp ? 20 : 8)
|
|
57
|
+
if packet_size && packet_size < minimum_size
|
|
58
|
+
raise ArgumentError, "packet_size must be at least #{minimum_size} for the selected protocol and family"
|
|
59
|
+
end
|
|
60
|
+
raise ArgumentError, "dont_fragment only applies to IPv4" if family == :ipv6 && dont_fragment
|
|
61
|
+
if !interface.nil? && (!interface.is_a?(String) || interface.empty? || interface.include?("\0"))
|
|
62
|
+
raise ArgumentError, "interface must be a nonempty interface name"
|
|
63
|
+
end
|
|
64
|
+
return if source.nil?
|
|
65
|
+
raise ArgumentError, "source must be a numeric local address" unless source.is_a?(String)
|
|
66
|
+
address = IPAddr.new(source.split("%", 2).first)
|
|
67
|
+
if (family == :ipv4 && !address.ipv4?) || (family == :ipv6 && !address.ipv6?)
|
|
68
|
+
raise ArgumentError, "source and family disagree"
|
|
69
|
+
end
|
|
70
|
+
rescue IPAddr::InvalidAddressError
|
|
71
|
+
raise ArgumentError, "source must be a numeric local address"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class Request
|
|
76
|
+
attr_reader :target, :options
|
|
77
|
+
|
|
78
|
+
def initialize(target, **options)
|
|
79
|
+
unless target.is_a?(String) && !target.empty? && !target.match?(/[\s\x00]/)
|
|
80
|
+
raise ArgumentError, "target must be a nonempty hostname or numeric address without whitespace"
|
|
81
|
+
end
|
|
82
|
+
unknown = options.keys - Options::DEFAULTS.keys
|
|
83
|
+
raise ArgumentError, "unknown options: #{unknown.join(', ')}" unless unknown.empty?
|
|
84
|
+
@target = target.dup.freeze
|
|
85
|
+
@options = Values.copy(options)
|
|
86
|
+
freeze
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ipaddr'
|
|
4
|
+
|
|
5
|
+
module Tracepath
|
|
6
|
+
# Wire codecs and response correlation. All decoders treat received bytes as
|
|
7
|
+
# untrusted input and return nil for unsupported or incomplete packets.
|
|
8
|
+
module Packet
|
|
9
|
+
MAX_PACKET_SIZE = 65_575
|
|
10
|
+
MAX_EXTENSION_HEADERS = 16
|
|
11
|
+
VLAN_TYPES = [0x8100, 0x88a8, 0x9100].freeze
|
|
12
|
+
ICMP_ERROR_KINDS = %i[time_exceeded unreachable packet_too_big parameter_problem].freeze
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
# ICMPv6 raw sockets calculate the checksum using the selected source IP.
|
|
16
|
+
def icmp_echo(family:, identifier:, sequence:, payload: '')
|
|
17
|
+
validate_family!(family)
|
|
18
|
+
validate_uint!(identifier, 16, 'identifier')
|
|
19
|
+
validate_uint!(sequence, 16, 'sequence')
|
|
20
|
+
validate_payload!(payload, family == :ipv4 ? 65_507 : 65_527)
|
|
21
|
+
|
|
22
|
+
packet = [family == :ipv4 ? 8 : 128, 0, 0, identifier, sequence].pack('CCnnn') + payload.b
|
|
23
|
+
packet[2, 2] = [checksum(packet)].pack('n') if family == :ipv4
|
|
24
|
+
packet
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Returns a TCP segment; the socket supplies the IP header.
|
|
28
|
+
def tcp_syn(family:, source:, destination:, source_port:, destination_port:, sequence:, payload: '')
|
|
29
|
+
validate_family!(family)
|
|
30
|
+
validate_uint!(source_port, 16, 'source_port')
|
|
31
|
+
validate_uint!(destination_port, 16, 'destination_port')
|
|
32
|
+
validate_uint!(sequence, 32, 'sequence')
|
|
33
|
+
validate_payload!(payload, family == :ipv4 ? 65_495 : 65_515)
|
|
34
|
+
|
|
35
|
+
packet = [source_port, destination_port, sequence, 0, 0x50, 0x02, 65_535, 0, 0].pack('nnNNCCnnn') + payload.b
|
|
36
|
+
header = pseudoheader(family, source, destination, 6, packet.bytesize)
|
|
37
|
+
packet[16, 2] = [checksum(header + packet)].pack('n')
|
|
38
|
+
packet
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# raw IPv6 sockets omit the IP header. In that case the caller supplies
|
|
42
|
+
# the recvmsg peer and local destination, and the socket's protocol.
|
|
43
|
+
# checksum_offloaded is exclusively for trusted receive metadata (for
|
|
44
|
+
# example Linux PACKET_AUXDATA), never inferred from packet contents.
|
|
45
|
+
# local_capture is a transport-verified loopback capture taken before
|
|
46
|
+
# checksum finalization; it cannot establish checksum validity.
|
|
47
|
+
def decode(data, family:, source: nil, destination: nil, ip_header: true, protocol: :icmp, checksum_offloaded: false, local_capture: false)
|
|
48
|
+
return unless data.is_a?(String) && data.bytesize <= MAX_PACKET_SIZE
|
|
49
|
+
return unless %i[ipv4 ipv6].include?(family)
|
|
50
|
+
|
|
51
|
+
data = data.b
|
|
52
|
+
if ip_header
|
|
53
|
+
envelope = decode_ip(data, family: family, local_capture: local_capture)
|
|
54
|
+
return unless envelope
|
|
55
|
+
|
|
56
|
+
source = envelope[:source]
|
|
57
|
+
destination = envelope[:destination]
|
|
58
|
+
number = envelope[:number]
|
|
59
|
+
payload = envelope[:payload]
|
|
60
|
+
else
|
|
61
|
+
return unless address_family?(source, family) && address_family?(destination, family)
|
|
62
|
+
|
|
63
|
+
source = address(source).to_s
|
|
64
|
+
destination = address(destination).to_s
|
|
65
|
+
number = protocol_number(protocol, family)
|
|
66
|
+
payload = data
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
reply = { family: family, source: source, destination: destination }
|
|
70
|
+
case number
|
|
71
|
+
when family == :ipv4 ? 1 : 58
|
|
72
|
+
decode_icmp(payload, reply)
|
|
73
|
+
when 6
|
|
74
|
+
decode_tcp(payload, reply, checksum_offloaded: checksum_offloaded, local_capture: local_capture)
|
|
75
|
+
end
|
|
76
|
+
rescue ArgumentError, TypeError
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# DLT values come from pcap_datalink, not the host's socket AF constants.
|
|
81
|
+
def decode_link(data, datalink:, local_capture: false)
|
|
82
|
+
return unless data.is_a?(String) && data.bytesize <= MAX_PACKET_SIZE + 64
|
|
83
|
+
|
|
84
|
+
case datalink
|
|
85
|
+
when 1 # DLT_EN10MB, including stacked 802.1Q / 802.1ad tags
|
|
86
|
+
return if data.bytesize < 14
|
|
87
|
+
|
|
88
|
+
offset = 14
|
|
89
|
+
ether_type = uint16(data, 12)
|
|
90
|
+
tags = 0
|
|
91
|
+
while VLAN_TYPES.include?(ether_type)
|
|
92
|
+
return if tags >= 4 || data.bytesize < offset + 4
|
|
93
|
+
|
|
94
|
+
ether_type = uint16(data, offset + 2)
|
|
95
|
+
offset += 4
|
|
96
|
+
tags += 1
|
|
97
|
+
end
|
|
98
|
+
family = { 0x0800 => :ipv4, 0x86dd => :ipv6 }[ether_type]
|
|
99
|
+
when 0, 108 # DLT_NULL (host endian), DLT_LOOP (network endian)
|
|
100
|
+
return if data.bytesize < 4
|
|
101
|
+
|
|
102
|
+
values = datalink == 108 ? [data.unpack1('N')] : [data.unpack1('V'), data.unpack1('N')]
|
|
103
|
+
af = values.find { |value| [2, 10, 24, 28, 30].include?(value) }
|
|
104
|
+
family = af == 2 ? :ipv4 : :ipv6 if af
|
|
105
|
+
offset = 4
|
|
106
|
+
when 12, 101 # DLT_RAW on different libpcap platforms
|
|
107
|
+
family = { 4 => :ipv4, 6 => :ipv6 }[data.getbyte(0).to_i >> 4]
|
|
108
|
+
offset = 0
|
|
109
|
+
when 228, 229 # DLT_IPV4, DLT_IPV6
|
|
110
|
+
family = datalink == 228 ? :ipv4 : :ipv6
|
|
111
|
+
offset = 0
|
|
112
|
+
else
|
|
113
|
+
return
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
decode(data.byteslice(offset..), family: family, local_capture: local_capture) if family
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def match?(reply, probe)
|
|
120
|
+
return false unless reply.is_a?(Hash) && reply[:family] == probe.family
|
|
121
|
+
return false unless same_address?(reply[:destination], probe.source_address)
|
|
122
|
+
|
|
123
|
+
case reply[:kind]
|
|
124
|
+
when :echo_reply
|
|
125
|
+
probe.protocol == :icmp && same_address?(reply[:source], probe.destination_address) &&
|
|
126
|
+
icmp_identity_match?(reply, probe)
|
|
127
|
+
when :tcp_reply
|
|
128
|
+
tcp_match?(reply, probe)
|
|
129
|
+
when *ICMP_ERROR_KINDS
|
|
130
|
+
quote_match?(reply[:quoted], probe)
|
|
131
|
+
else
|
|
132
|
+
false
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def destination_reached?(reply, probe)
|
|
137
|
+
return false unless match?(reply, probe)
|
|
138
|
+
return false unless same_address?(reply[:source], probe.destination_address)
|
|
139
|
+
|
|
140
|
+
case probe.protocol
|
|
141
|
+
when :icmp
|
|
142
|
+
reply[:kind] == :echo_reply
|
|
143
|
+
when :tcp
|
|
144
|
+
reply[:kind] == :tcp_reply
|
|
145
|
+
when :udp
|
|
146
|
+
reply[:kind] == :unreachable &&
|
|
147
|
+
[reply[:icmp_type], reply[:icmp_code]] == (probe.family == :ipv4 ? [3, 3] : [1, 4])
|
|
148
|
+
else
|
|
149
|
+
false
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
private
|
|
154
|
+
|
|
155
|
+
def decode_ip(data, family:, quoted: false, local_capture: false)
|
|
156
|
+
family == :ipv4 ? decode_ipv4(data, quoted: quoted, local_capture: local_capture) : decode_ipv6(data, quoted: quoted)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def decode_ipv4(data, quoted:, local_capture: false)
|
|
160
|
+
return if data.bytesize < 20 || data.getbyte(0) >> 4 != 4
|
|
161
|
+
|
|
162
|
+
header_length = (data.getbyte(0) & 0x0f) * 4
|
|
163
|
+
total_length = uint16(data, 2)
|
|
164
|
+
return if header_length < 20 || total_length < header_length || data.bytesize < header_length
|
|
165
|
+
return if !quoted && total_length > data.bytesize
|
|
166
|
+
if !quoted && checksum(data.byteslice(0, header_length)) != 0
|
|
167
|
+
return unless local_capture == true && data.getbyte(9) == 6 && uint16(data, 10).zero?
|
|
168
|
+
end
|
|
169
|
+
# An outer response must be complete. A quoted first fragment can still
|
|
170
|
+
# identify a kernel-fragmented probe from its first eight payload bytes.
|
|
171
|
+
fragment = uint16(data, 6)
|
|
172
|
+
return unless (fragment & 0x1fff).zero?
|
|
173
|
+
return if !quoted && (fragment & 0x2000).positive?
|
|
174
|
+
|
|
175
|
+
limit = [total_length, data.bytesize].min
|
|
176
|
+
{
|
|
177
|
+
family: :ipv4,
|
|
178
|
+
source: IPAddr.new_ntoh(data.byteslice(12, 4)).to_s,
|
|
179
|
+
destination: IPAddr.new_ntoh(data.byteslice(16, 4)).to_s,
|
|
180
|
+
number: data.getbyte(9),
|
|
181
|
+
payload: data.byteslice(header_length, limit - header_length)
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def decode_ipv6(data, quoted:)
|
|
186
|
+
return if data.bytesize < 40 || data.getbyte(0) >> 4 != 6
|
|
187
|
+
|
|
188
|
+
payload_length = uint16(data, 4)
|
|
189
|
+
return if payload_length.zero? # Jumbo Payload options are unsupported.
|
|
190
|
+
|
|
191
|
+
total_length = 40 + payload_length
|
|
192
|
+
return if !quoted && total_length > data.bytesize
|
|
193
|
+
|
|
194
|
+
limit = [total_length, data.bytesize].min
|
|
195
|
+
number = data.getbyte(6)
|
|
196
|
+
offset = 40
|
|
197
|
+
extension_count = 0
|
|
198
|
+
while [0, 43, 44, 51, 60].include?(number)
|
|
199
|
+
return if extension_count >= MAX_EXTENSION_HEADERS || offset + 2 > limit
|
|
200
|
+
|
|
201
|
+
length = case number
|
|
202
|
+
when 44
|
|
203
|
+
return if offset + 8 > limit
|
|
204
|
+
fragment = uint16(data, offset + 2)
|
|
205
|
+
return unless (fragment & 0xfff8).zero?
|
|
206
|
+
return if !quoted && (fragment & 1).positive?
|
|
207
|
+
|
|
208
|
+
8
|
|
209
|
+
when 51
|
|
210
|
+
return if data.getbyte(offset + 1).zero?
|
|
211
|
+
|
|
212
|
+
(data.getbyte(offset + 1) + 2) * 4
|
|
213
|
+
else
|
|
214
|
+
(data.getbyte(offset + 1) + 1) * 8
|
|
215
|
+
end
|
|
216
|
+
return if offset + length > limit
|
|
217
|
+
|
|
218
|
+
number = data.getbyte(offset)
|
|
219
|
+
offset += length
|
|
220
|
+
extension_count += 1
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
{
|
|
224
|
+
family: :ipv6,
|
|
225
|
+
source: IPAddr.new_ntoh(data.byteslice(8, 16)).to_s,
|
|
226
|
+
destination: IPAddr.new_ntoh(data.byteslice(24, 16)).to_s,
|
|
227
|
+
number: number,
|
|
228
|
+
payload: data.byteslice(offset, limit - offset)
|
|
229
|
+
}
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def decode_icmp(payload, reply)
|
|
233
|
+
return if payload.bytesize < 8
|
|
234
|
+
|
|
235
|
+
family = reply[:family]
|
|
236
|
+
check = family == :ipv4 ? payload : pseudoheader(family, reply[:source], reply[:destination], 58, payload.bytesize) + payload
|
|
237
|
+
return unless checksum(check).zero?
|
|
238
|
+
|
|
239
|
+
type = payload.getbyte(0)
|
|
240
|
+
code = payload.getbyte(1)
|
|
241
|
+
kind = icmp_kind(family, type, code)
|
|
242
|
+
return unless kind
|
|
243
|
+
|
|
244
|
+
reply = reply.merge(kind: kind, icmp_type: type, icmp_code: code)
|
|
245
|
+
if kind == :echo_reply
|
|
246
|
+
reply.merge(identifier: uint16(payload, 4), sequence: uint16(payload, 6))
|
|
247
|
+
else
|
|
248
|
+
quoted = decode_quote(payload.byteslice(8..), family)
|
|
249
|
+
return unless quoted
|
|
250
|
+
|
|
251
|
+
mtu = family == :ipv4 ? uint16(payload, 6) : uint32(payload, 4) if kind == :packet_too_big
|
|
252
|
+
reply.merge(quoted: quoted, mtu: mtu)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def icmp_kind(family, type, code)
|
|
257
|
+
if family == :ipv4
|
|
258
|
+
case type
|
|
259
|
+
when 0 then :echo_reply if code.zero?
|
|
260
|
+
when 3 then code == 4 ? :packet_too_big : :unreachable
|
|
261
|
+
when 11 then :time_exceeded if code <= 1
|
|
262
|
+
when 12 then :parameter_problem
|
|
263
|
+
end
|
|
264
|
+
else
|
|
265
|
+
case type
|
|
266
|
+
when 129 then :echo_reply if code.zero?
|
|
267
|
+
when 1 then :unreachable
|
|
268
|
+
when 2 then :packet_too_big if code.zero?
|
|
269
|
+
when 3 then :time_exceeded if code <= 1
|
|
270
|
+
when 4 then :parameter_problem
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def decode_tcp(payload, reply, checksum_offloaded: false, local_capture: false)
|
|
276
|
+
return if payload.bytesize < 20
|
|
277
|
+
|
|
278
|
+
header_length = (payload.getbyte(12) >> 4) * 4
|
|
279
|
+
return if header_length < 20 || header_length > payload.bytesize
|
|
280
|
+
|
|
281
|
+
unless checksum_offloaded == true || local_capture == true
|
|
282
|
+
header = pseudoheader(reply[:family], reply[:source], reply[:destination], 6, payload.bytesize)
|
|
283
|
+
return unless checksum(header + payload).zero?
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
decoded = reply.merge(
|
|
287
|
+
kind: :tcp_reply,
|
|
288
|
+
source_port: uint16(payload, 0), destination_port: uint16(payload, 2),
|
|
289
|
+
tcp_sequence: uint32(payload, 4), tcp_ack: uint32(payload, 8), tcp_flags: payload.getbyte(13)
|
|
290
|
+
)
|
|
291
|
+
if local_capture == true
|
|
292
|
+
decoded[:checksum_status] = :unverified_local_capture
|
|
293
|
+
elsif checksum_offloaded == true
|
|
294
|
+
decoded[:checksum_status] = :kernel_offload
|
|
295
|
+
end
|
|
296
|
+
decoded
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def decode_quote(data, family)
|
|
300
|
+
ip = decode_ip(data, family: family, quoted: true)
|
|
301
|
+
return unless ip && ip[:payload].bytesize >= 8
|
|
302
|
+
|
|
303
|
+
quote = ip.slice(:family, :source, :destination)
|
|
304
|
+
payload = ip[:payload]
|
|
305
|
+
case ip[:number]
|
|
306
|
+
when 17
|
|
307
|
+
return if uint16(payload, 4) < 8
|
|
308
|
+
|
|
309
|
+
quote.merge(protocol: :udp, source_port: uint16(payload, 0), destination_port: uint16(payload, 2))
|
|
310
|
+
when 6
|
|
311
|
+
quote.merge(protocol: :tcp, source_port: uint16(payload, 0), destination_port: uint16(payload, 2), tcp_sequence: uint32(payload, 4))
|
|
312
|
+
when family == :ipv4 ? 1 : 58
|
|
313
|
+
return unless payload.getbyte(0) == (family == :ipv4 ? 8 : 128) && payload.getbyte(1).zero?
|
|
314
|
+
|
|
315
|
+
quote.merge(protocol: :icmp, identifier: uint16(payload, 4), sequence: uint16(payload, 6))
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def quote_match?(quote, probe)
|
|
320
|
+
return false unless quote.is_a?(Hash) && quote[:family] == probe.family && quote[:protocol] == probe.protocol
|
|
321
|
+
return false unless same_address?(quote[:source], probe.source_address) && same_address?(quote[:destination], probe.destination_address)
|
|
322
|
+
|
|
323
|
+
case probe.protocol
|
|
324
|
+
when :icmp
|
|
325
|
+
icmp_identity_match?(quote, probe)
|
|
326
|
+
when :udp
|
|
327
|
+
ports_match?(quote, probe.source_port, probe.destination_port)
|
|
328
|
+
when :tcp
|
|
329
|
+
ports_match?(quote, probe.source_port, probe.destination_port) &&
|
|
330
|
+
valid_uint?(quote[:tcp_sequence], 32) && quote[:tcp_sequence] == probe.tcp_sequence
|
|
331
|
+
else
|
|
332
|
+
false
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def tcp_match?(reply, probe)
|
|
337
|
+
return false unless probe.protocol == :tcp && same_address?(reply[:source], probe.destination_address)
|
|
338
|
+
return false unless ports_match?(reply, probe.destination_port, probe.source_port)
|
|
339
|
+
|
|
340
|
+
flags = reply[:tcp_flags]
|
|
341
|
+
ack = reply[:tcp_ack]
|
|
342
|
+
return false unless valid_uint?(flags, 8) && valid_uint?(ack, 32) && valid_uint?(probe.tcp_sequence, 32)
|
|
343
|
+
return false unless (flags & 0x10).positive? && [0x02, 0x04].include?(flags & 0x06) && (flags & 0x01).zero?
|
|
344
|
+
|
|
345
|
+
payload_size = probe.respond_to?(:payload_size) ? probe.payload_size : 0
|
|
346
|
+
payload_size ||= 0
|
|
347
|
+
return false unless valid_uint?(payload_size, 16)
|
|
348
|
+
|
|
349
|
+
delta = (ack - probe.tcp_sequence) & 0xffff_ffff
|
|
350
|
+
delta.between?(1, 1 + payload_size)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def icmp_identity_match?(reply, probe)
|
|
354
|
+
valid_uint?(reply[:identifier], 16) && valid_uint?(reply[:sequence], 16) &&
|
|
355
|
+
reply[:identifier] == probe.identifier && reply[:sequence] == probe.sequence
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def ports_match?(reply, source_port, destination_port)
|
|
359
|
+
valid_uint?(reply[:source_port], 16) && valid_uint?(reply[:destination_port], 16) &&
|
|
360
|
+
reply[:source_port] == source_port && reply[:destination_port] == destination_port
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def protocol_number(protocol, family)
|
|
364
|
+
{ icmp: family == :ipv4 ? 1 : 58, tcp: 6, udp: 17 }[protocol]
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def checksum(data)
|
|
368
|
+
total = 0
|
|
369
|
+
offset = 0
|
|
370
|
+
while offset + 1 < data.bytesize
|
|
371
|
+
total += (data.getbyte(offset) << 8) | data.getbyte(offset + 1)
|
|
372
|
+
offset += 2
|
|
373
|
+
end
|
|
374
|
+
total += data.getbyte(offset) << 8 if offset < data.bytesize
|
|
375
|
+
total = (total & 0xffff) + (total >> 16) while total > 0xffff
|
|
376
|
+
(~total) & 0xffff
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def pseudoheader(family, source, destination, protocol, length)
|
|
380
|
+
unless address_family?(source, family) && address_family?(destination, family)
|
|
381
|
+
raise ArgumentError, 'source and destination must match the address family'
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
addresses = address(source).hton + address(destination).hton
|
|
385
|
+
addresses + (family == :ipv4 ? [0, protocol, length].pack('CCn') : [length, protocol].pack('NN'))
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def address(value)
|
|
389
|
+
IPAddr.new(value.to_s.split('%', 2).first)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def address_family?(value, family)
|
|
393
|
+
return false unless value
|
|
394
|
+
|
|
395
|
+
ip = address(value)
|
|
396
|
+
family == :ipv4 ? ip.ipv4? : ip.ipv6?
|
|
397
|
+
rescue ArgumentError
|
|
398
|
+
false
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def same_address?(left, right)
|
|
402
|
+
left && right && address(left) == address(right) || false
|
|
403
|
+
rescue ArgumentError
|
|
404
|
+
false
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def uint16(data, offset)
|
|
408
|
+
data.byteslice(offset, 2).unpack1('n')
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def uint32(data, offset)
|
|
412
|
+
data.byteslice(offset, 4).unpack1('N')
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def validate_family!(family)
|
|
416
|
+
raise ArgumentError, 'family must be :ipv4 or :ipv6' unless %i[ipv4 ipv6].include?(family)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def validate_uint!(value, bits, name)
|
|
420
|
+
raise ArgumentError, "#{name} must be an unsigned #{bits}-bit integer" unless valid_uint?(value, bits)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def valid_uint?(value, bits)
|
|
424
|
+
value.is_a?(Integer) && value.between?(0, (1 << bits) - 1)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def validate_payload!(payload, maximum)
|
|
428
|
+
raise ArgumentError, 'payload must be a String' unless payload.is_a?(String)
|
|
429
|
+
raise ArgumentError, "payload exceeds #{maximum} bytes" if payload.bytesize > maximum
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|