raioquic 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.containerignore +4 -0
- data/.rubocop.yml +93 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Containerfile +6 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +113 -0
- data/LICENSE +28 -0
- data/README.md +48 -0
- data/Rakefile +16 -0
- data/Steepfile +8 -0
- data/example/curlcatcher.rb +18 -0
- data/example/interoperability/README.md +9 -0
- data/example/interoperability/aioquic/aioquic_client.py +47 -0
- data/example/interoperability/aioquic/aioquic_server.py +34 -0
- data/example/interoperability/key.pem +28 -0
- data/example/interoperability/localhost-unasuke-dev.crt +21 -0
- data/example/interoperability/quic-go/sample_server.go +61 -0
- data/example/interoperability/raioquic_client.rb +42 -0
- data/example/interoperability/raioquic_server.rb +43 -0
- data/example/parse_curl_example.rb +108 -0
- data/lib/raioquic/buffer.rb +202 -0
- data/lib/raioquic/core_ext.rb +54 -0
- data/lib/raioquic/crypto/README.md +5 -0
- data/lib/raioquic/crypto/aesgcm.rb +52 -0
- data/lib/raioquic/crypto/backend/aead.rb +52 -0
- data/lib/raioquic/crypto/backend.rb +12 -0
- data/lib/raioquic/crypto.rb +10 -0
- data/lib/raioquic/quic/configuration.rb +81 -0
- data/lib/raioquic/quic/connection.rb +2776 -0
- data/lib/raioquic/quic/crypto.rb +317 -0
- data/lib/raioquic/quic/event.rb +69 -0
- data/lib/raioquic/quic/logger.rb +272 -0
- data/lib/raioquic/quic/packet.rb +471 -0
- data/lib/raioquic/quic/packet_builder.rb +301 -0
- data/lib/raioquic/quic/rangeset.rb +113 -0
- data/lib/raioquic/quic/recovery.rb +528 -0
- data/lib/raioquic/quic/stream.rb +343 -0
- data/lib/raioquic/quic.rb +20 -0
- data/lib/raioquic/tls.rb +1659 -0
- data/lib/raioquic/version.rb +5 -0
- data/lib/raioquic.rb +12 -0
- data/misc/export_x25519.py +43 -0
- data/misc/gen_rfc8448_keypair.rb +90 -0
- data/raioquic.gemspec +37 -0
- data/sig/raioquic/buffer.rbs +37 -0
- data/sig/raioquic/core_ext.rbs +7 -0
- data/sig/raioquic/crypto/aesgcm.rbs +20 -0
- data/sig/raioquic/crypto/backend/aead.rbs +11 -0
- data/sig/raioquic/quic/configuration.rbs +34 -0
- data/sig/raioquic/quic/connection.rbs +277 -0
- data/sig/raioquic/quic/crypto.rbs +88 -0
- data/sig/raioquic/quic/event.rbs +51 -0
- data/sig/raioquic/quic/logger.rbs +57 -0
- data/sig/raioquic/quic/packet.rbs +157 -0
- data/sig/raioquic/quic/packet_builder.rbs +76 -0
- data/sig/raioquic/quic/rangeset.rbs +17 -0
- data/sig/raioquic/quic/recovery.rbs +142 -0
- data/sig/raioquic/quic/stream.rbs +87 -0
- data/sig/raioquic/tls.rbs +444 -0
- data/sig/raioquic.rbs +9 -0
- metadata +121 -0
@@ -0,0 +1,317 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../crypto"
|
4
|
+
require_relative "packet"
|
5
|
+
require "tttls1.3"
|
6
|
+
require "openssl"
|
7
|
+
|
8
|
+
module Raioquic
|
9
|
+
module Quic
|
10
|
+
# Raioquic::Quic::Crypto
|
11
|
+
# Migrated from these files
|
12
|
+
# - aioquic/src/aioquic/quic/crypto.py
|
13
|
+
# - aioquic/src/aioquic/_crypto.c
|
14
|
+
module Crypto
|
15
|
+
INITIAL_CIPHER_SUITE = nil
|
16
|
+
AEAD_KEY_LENGTH_MAX = 32
|
17
|
+
AEAD_NONCE_LENGTH = 12
|
18
|
+
AEAD_TAG_LENGTH = 16
|
19
|
+
PACKET_LENGTH_MAX = 1500
|
20
|
+
PACKET_NUMBER_LENGTH_MAX = 4
|
21
|
+
SAMPLE_LENGTH = 16
|
22
|
+
INITIAL_SALT_VERSION_1 = ["38762cf7f55934b34d179ae6a4c80cadccbb7f0a"].pack("H*")
|
23
|
+
|
24
|
+
# class CryptoError < ::StandardError; end
|
25
|
+
CryptoError = Class.new(StandardError)
|
26
|
+
|
27
|
+
def derive_key_iv_hp(_cipher_suite, secret)
|
28
|
+
# TODO: implement on TLS module
|
29
|
+
[
|
30
|
+
TTTLS13::KeySchedule.hkdf_expand_label(secret, "quic key", "", 16, "SHA256"),
|
31
|
+
TTTLS13::KeySchedule.hkdf_expand_label(secret, "quic iv", "", 12, "SHA256"),
|
32
|
+
TTTLS13::KeySchedule.hkdf_expand_label(secret, "quic hp", "", 16, "SHA256"),
|
33
|
+
]
|
34
|
+
end
|
35
|
+
module_function :derive_key_iv_hp
|
36
|
+
|
37
|
+
def xor_str(a, b) # rubocop:disable Naming/MethodParameterName
|
38
|
+
a.unpack("C*").zip(b.unpack("C*")).map { |x, y| x ^ y }.pack("C*")
|
39
|
+
end
|
40
|
+
module_function :xor_str
|
41
|
+
|
42
|
+
class NoCallback
|
43
|
+
def call(_); end
|
44
|
+
end
|
45
|
+
|
46
|
+
# CryptoContext
|
47
|
+
# represent sender or receiver crypto context
|
48
|
+
class CryptoContext
|
49
|
+
attr_reader :aead
|
50
|
+
attr_reader :key_phase
|
51
|
+
attr_reader :secret
|
52
|
+
|
53
|
+
def initialize(key_phase: 0, setup_cb: NoCallback.new, teardown_cb: NoCallback.new)
|
54
|
+
@aead = nil
|
55
|
+
@cipher_suite = nil
|
56
|
+
@hp = nil
|
57
|
+
@key_phase = key_phase
|
58
|
+
@secret = nil
|
59
|
+
@version = nil
|
60
|
+
@setup_cb = setup_cb
|
61
|
+
@teardown_cb = teardown_cb
|
62
|
+
end
|
63
|
+
|
64
|
+
def decrypt_packet(packet:, encrypted_offset:, expected_packet_number:)
|
65
|
+
raise ArgumentError unless @aead # TODO: KeyUnavailableError
|
66
|
+
|
67
|
+
# header protection
|
68
|
+
plain_header, packet_number, = @hp.remove(packet: packet, encrypted_offset: encrypted_offset)
|
69
|
+
first_byte = plain_header[0].unpack1("C*")
|
70
|
+
|
71
|
+
# packet number
|
72
|
+
pn_length = (first_byte & 0x03) + 1
|
73
|
+
packet_number = Packet.decode_packet_number(truncated: packet_number, num_bits: pn_length * 8, expected: expected_packet_number)
|
74
|
+
|
75
|
+
# detect key phase change
|
76
|
+
crypto = self
|
77
|
+
unless Packet.is_long_header(first_byte)
|
78
|
+
key_phase = (first_byte & 4) >> 2
|
79
|
+
crypto = next_key_phase if key_phase != @key_phase
|
80
|
+
end
|
81
|
+
payload = crypto.aead.decrypt(data: packet[plain_header.length..], associated_data: plain_header, packet_number: packet_number)
|
82
|
+
|
83
|
+
return [plain_header, payload, packet_number, crypto != self]
|
84
|
+
end
|
85
|
+
|
86
|
+
def encrypt_packet(plain_header:, plain_payload:, packet_number:)
|
87
|
+
raise RuntimeError unless is_valid
|
88
|
+
|
89
|
+
protected_payload = @aead.encrypt(data: plain_payload, associated_data: plain_header, packet_number: packet_number)
|
90
|
+
return @hp.apply(plain_header: plain_header, protected_payload: protected_payload)
|
91
|
+
end
|
92
|
+
|
93
|
+
def is_valid
|
94
|
+
!!@aead
|
95
|
+
end
|
96
|
+
|
97
|
+
def setup(cipher_suite:, secret:, version:)
|
98
|
+
hp_cipher_name = "aes-128-ecb" # TODO: hardcode
|
99
|
+
_aead_cipher_name = OpenSSL::Digest.new("SHA256") # TODO: hardcode
|
100
|
+
|
101
|
+
key, iv, hp = ::Raioquic::Quic::Crypto.derive_key_iv_hp(cipher_suite, secret)
|
102
|
+
# binding.b
|
103
|
+
@aead = AEAD.new(cipher_name: "aes-128-gcm", key: key, iv: iv) # TODO: hardcode
|
104
|
+
@cipher_suite = cipher_suite
|
105
|
+
@hp = HeaderProtection.new(cipher_name: hp_cipher_name, key: hp)
|
106
|
+
@secret = secret
|
107
|
+
@version = version
|
108
|
+
|
109
|
+
# trigger callback
|
110
|
+
@setup_cb&.call("tls")
|
111
|
+
end
|
112
|
+
|
113
|
+
def teardown
|
114
|
+
@aead = nil
|
115
|
+
@cipher_suite = nil
|
116
|
+
@hp = nil
|
117
|
+
|
118
|
+
# trigger callback
|
119
|
+
@teardown_cb&.call("tls")
|
120
|
+
end
|
121
|
+
|
122
|
+
def apply_key_phase(crypto, _trigger)
|
123
|
+
@aead = crypto.aead
|
124
|
+
@key_phase = crypto.key_phase
|
125
|
+
@secret = crypto.secret
|
126
|
+
|
127
|
+
# trigger callback
|
128
|
+
@setup_cb&.call("tls")
|
129
|
+
end
|
130
|
+
|
131
|
+
def next_key_phase
|
132
|
+
crypto = self.class.new(key_phase: (@key_phase.zero? ? 1 : 0))
|
133
|
+
crypto.setup(
|
134
|
+
cipher_suite: "aes-128-gcm", # TODO: hardcode
|
135
|
+
secret: TTTLS13::KeySchedule.hkdf_expand_label(@secret, "quic ku", "", 32, "SHA256"),
|
136
|
+
version: Packet::QuicProtocolVersion::VERSION_1,
|
137
|
+
)
|
138
|
+
return crypto
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# CryptoPair
|
143
|
+
# store sender and receiver crypto context object
|
144
|
+
class CryptoPair
|
145
|
+
attr_reader :recv
|
146
|
+
attr_reader :send
|
147
|
+
attr_reader :aead_tag_size
|
148
|
+
|
149
|
+
def initialize(recv_setup_cb: NoCallback.new, recv_teardown_cb: NoCallback.new, send_setup_cb: NoCallback.new, send_teardown_cb: NoCallback.new) # rubocop:disable Layout/LineLength
|
150
|
+
@aead_tag_size = 16
|
151
|
+
@recv = CryptoContext.new(setup_cb: recv_setup_cb, teardown_cb: recv_teardown_cb)
|
152
|
+
@send = CryptoContext.new(setup_cb: send_setup_cb, teardown_cb: send_teardown_cb)
|
153
|
+
@update_key_requested = false
|
154
|
+
end
|
155
|
+
|
156
|
+
def decrypt_packet(packet:, encrypted_offset:, expected_packet_number:)
|
157
|
+
plain_header, payload, packe_number, need_update_key = @recv.decrypt_packet(
|
158
|
+
packet: packet, encrypted_offset: encrypted_offset, expected_packet_number: expected_packet_number,
|
159
|
+
)
|
160
|
+
_update_key("remote_update") if need_update_key
|
161
|
+
|
162
|
+
return [plain_header, payload, packe_number]
|
163
|
+
end
|
164
|
+
|
165
|
+
def encrypt_packet(plain_header:, plain_payload:, packet_number:)
|
166
|
+
_update_key("local_update") if @update_key_requested
|
167
|
+
|
168
|
+
return @send.encrypt_packet(plain_header: plain_header, plain_payload: plain_payload, packet_number: packet_number)
|
169
|
+
end
|
170
|
+
|
171
|
+
def setup_initial(cid:, is_client:, version:)
|
172
|
+
if is_client
|
173
|
+
recv_label = "server in"
|
174
|
+
send_label = "client in"
|
175
|
+
else
|
176
|
+
recv_label = "client in"
|
177
|
+
send_label = "server in"
|
178
|
+
|
179
|
+
end
|
180
|
+
initial_salt = INITIAL_SALT_VERSION_1
|
181
|
+
# algorithm = TLS.cipher_suite_hash(INITIAL_CIPHER_SUITE) TODO: impleement on tls module
|
182
|
+
algorithm = OpenSSL::Digest.new("SHA256")
|
183
|
+
# initial_secret = hkdf_extract(algorithm, initial_salt, cid) TODO: implement on tls module
|
184
|
+
initial_secret = OpenSSL::HMAC.digest(algorithm, initial_salt, cid)
|
185
|
+
@recv.setup(
|
186
|
+
cipher_suite: INITIAL_CIPHER_SUITE,
|
187
|
+
secret: TTTLS13::KeySchedule.hkdf_expand_label(initial_secret, recv_label, "", 32, "SHA256"),
|
188
|
+
version: version,
|
189
|
+
)
|
190
|
+
@send.setup(
|
191
|
+
cipher_suite: INITIAL_CIPHER_SUITE,
|
192
|
+
secret: TTTLS13::KeySchedule.hkdf_expand_label(initial_secret, send_label, "", 32, "SHA256"),
|
193
|
+
version: version,
|
194
|
+
)
|
195
|
+
end
|
196
|
+
|
197
|
+
def teardown
|
198
|
+
@recv.teardown
|
199
|
+
@send.teardown
|
200
|
+
end
|
201
|
+
|
202
|
+
def key_phase
|
203
|
+
if @update_key_requested
|
204
|
+
@recv.key_phase.zero? ? 1 : 0
|
205
|
+
else
|
206
|
+
@recv.key_phase
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def update_key
|
211
|
+
@update_key_requested = true
|
212
|
+
end
|
213
|
+
|
214
|
+
def _update_key(trigger)
|
215
|
+
# binding.irb
|
216
|
+
@recv.apply_key_phase(@recv.next_key_phase, trigger)
|
217
|
+
@send.apply_key_phase(@send.next_key_phase, trigger)
|
218
|
+
@update_key_requested = false
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# HeaderProtection
|
223
|
+
# remove/apply QUIC header protection
|
224
|
+
class HeaderProtection
|
225
|
+
def initialize(cipher_name:, key:)
|
226
|
+
@cipher = OpenSSL::Cipher.new(cipher_name)
|
227
|
+
@cipher.encrypt
|
228
|
+
@cipher.key = key
|
229
|
+
@key = key
|
230
|
+
@mask = "\x00" * 31
|
231
|
+
@zero = "\x00" * 5
|
232
|
+
end
|
233
|
+
|
234
|
+
def apply(plain_header:, protected_payload:)
|
235
|
+
pn_length = (plain_header[0].unpack1("C*") & 0x03) + 1
|
236
|
+
pn_offset = plain_header.length - pn_length
|
237
|
+
mask(protected_payload.slice((PACKET_NUMBER_LENGTH_MAX - pn_length)..-1)[0, SAMPLE_LENGTH])
|
238
|
+
buffer = plain_header + protected_payload
|
239
|
+
if buffer[0].unpack1("C*") & 0x80 != 0 # rubocop:disable Style/NegatedIfElseCondition, Style/ConditionalAssignment
|
240
|
+
buffer[0] = Crypto.xor_str(buffer[0], [@mask[0].unpack1("C*") & 0x0f].pack("C*"))
|
241
|
+
else
|
242
|
+
buffer[0] = Crypto.xor_str(buffer[0], [@mask[0].unpack1("C*") & 0x1f].pack("C*"))
|
243
|
+
end
|
244
|
+
pn_length.times do |i|
|
245
|
+
buffer[pn_offset + i] = Crypto.xor_str(buffer[pn_offset + i], @mask[1 + i])
|
246
|
+
end
|
247
|
+
return buffer
|
248
|
+
end
|
249
|
+
|
250
|
+
def remove(packet:, encrypted_offset:)
|
251
|
+
mask(packet.slice(encrypted_offset + PACKET_NUMBER_LENGTH_MAX, SAMPLE_LENGTH))
|
252
|
+
buffer = packet.dup.slice(0, encrypted_offset + PACKET_NUMBER_LENGTH_MAX)
|
253
|
+
if buffer[0].unpack1("C*") & 0x80 != 0 # rubocop:disable Style/NegatedIfElseCondition, Style/ConditionalAssignment
|
254
|
+
buffer[0] = Crypto.xor_str(buffer[0], [@mask[0].unpack1("C*") & 0x0f].pack("C*"))
|
255
|
+
else
|
256
|
+
buffer[0] = Crypto.xor_str(buffer[0], [@mask[0].unpack1("C*") & 0x1f].pack("C*"))
|
257
|
+
end
|
258
|
+
pn_length = (buffer[0].unpack1("C*") & 0x03) + 1
|
259
|
+
pn_truncated = 0
|
260
|
+
pn_length.times do |i|
|
261
|
+
buffer[encrypted_offset + i] = Crypto.xor_str(buffer[encrypted_offset + i], @mask[1 + i])
|
262
|
+
pn_truncated = buffer[encrypted_offset + i].unpack1("C*") | (pn_truncated << 8)
|
263
|
+
end
|
264
|
+
[buffer.slice(0, encrypted_offset + pn_length), pn_truncated]
|
265
|
+
end
|
266
|
+
|
267
|
+
private def mask(sample)
|
268
|
+
# if chacha20 TODO: chacha20
|
269
|
+
@mask = @cipher.update(sample) + @cipher.final
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
# AEAD
|
274
|
+
# encrypt/dectypt AEAD
|
275
|
+
class AEAD
|
276
|
+
def initialize(cipher_name:, key:, iv:) # rubocop:disable Naming/MethodParameterName
|
277
|
+
@cipher = OpenSSL::Cipher.new(cipher_name)
|
278
|
+
@cipher_name = cipher_name
|
279
|
+
@key = key
|
280
|
+
@iv = iv
|
281
|
+
end
|
282
|
+
|
283
|
+
def decrypt(data:, associated_data:, packet_number:)
|
284
|
+
raise CryptoError, "Invalid payload length" if data.length < AEAD_TAG_LENGTH || data.length > PACKET_LENGTH_MAX
|
285
|
+
|
286
|
+
nonce = @iv.dup
|
287
|
+
8.times do |i|
|
288
|
+
nonce[AEAD_NONCE_LENGTH - 1 - i] = Crypto.xor_str(nonce[AEAD_NONCE_LENGTH - 1 - i], [packet_number >> (8 * i)].pack("C*"))
|
289
|
+
end
|
290
|
+
@cipher.decrypt
|
291
|
+
@cipher.key = @key
|
292
|
+
@cipher.iv = nonce
|
293
|
+
@cipher.auth_tag = data.slice(data.length - AEAD_TAG_LENGTH, AEAD_TAG_LENGTH)
|
294
|
+
@cipher.auth_data = associated_data
|
295
|
+
decrypted = @cipher.update(data.slice(0...(data.length - AEAD_TAG_LENGTH))) + @cipher.final
|
296
|
+
return decrypted
|
297
|
+
end
|
298
|
+
|
299
|
+
def encrypt(data:, associated_data:, packet_number:)
|
300
|
+
raise CryptoError, "Invalid payload length" if data.length > PACKET_LENGTH_MAX
|
301
|
+
|
302
|
+
nonce = @iv.dup
|
303
|
+
8.times do |i|
|
304
|
+
nonce[AEAD_NONCE_LENGTH - 1 - i] = Crypto.xor_str(nonce[AEAD_NONCE_LENGTH - 1 - i], [packet_number >> (8 * i)].pack("C*"))
|
305
|
+
end
|
306
|
+
|
307
|
+
@cipher.encrypt
|
308
|
+
@cipher.key = @key
|
309
|
+
@cipher.iv = nonce
|
310
|
+
@cipher.auth_data = associated_data
|
311
|
+
encrypted = @cipher.update(data) + @cipher.final
|
312
|
+
return encrypted + @cipher.auth_tag
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raioquic
|
4
|
+
module Quic
|
5
|
+
module Event
|
6
|
+
# Base class for QUIC events.
|
7
|
+
# Should be implement by Struct?
|
8
|
+
class QuicEvent
|
9
|
+
def ==(other)
|
10
|
+
return false if self.class != other.class
|
11
|
+
|
12
|
+
return false if instance_variables != other.instance_variables
|
13
|
+
|
14
|
+
return instance_variables.all? { |iver| instance_variable_get(iver) == other.instance_variable_get(iver) }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class ConnectionIdIssued < QuicEvent
|
19
|
+
attr_accessor :connection_id
|
20
|
+
end
|
21
|
+
|
22
|
+
class ConnectionIdRetired < QuicEvent
|
23
|
+
attr_accessor :connection_id
|
24
|
+
end
|
25
|
+
|
26
|
+
# The ConnectionTerminated event is fired when the QUIC connection is terminated.
|
27
|
+
class ConnectionTerminated < QuicEvent
|
28
|
+
attr_accessor :error_code # The error code which was specified when closing the connection.
|
29
|
+
attr_accessor :frame_type # The frame type which caused the connection to be closed, or `None`.
|
30
|
+
attr_accessor :reason_phrase # The human-readable reason for which the connection was closed.
|
31
|
+
end
|
32
|
+
|
33
|
+
# The DatagramFrameReceived event is fired when a DATAGRAM frame is received.
|
34
|
+
class DatagramFrameReceived < QuicEvent
|
35
|
+
attr_accessor :data # The data which was received.
|
36
|
+
end
|
37
|
+
|
38
|
+
# The HandshakeCompleted event is fired when the TLS handshake completes.
|
39
|
+
class HandshakeCompleted < QuicEvent
|
40
|
+
attr_accessor :alpn_protocol # The protocol which was negotiated using ALPN, or `nil`.
|
41
|
+
attr_accessor :early_data_accepted # WHether early (0-RTT) data was accepted by the remote peer.
|
42
|
+
attr_accessor :session_resumed # Whether a TLS session was resumed.
|
43
|
+
end
|
44
|
+
|
45
|
+
# The PingAcknowledged event is fired when a PING frame is acknowledged.
|
46
|
+
class PingAcknowledged < QuicEvent
|
47
|
+
attr_accessor :uid # The unique ID of the PING.
|
48
|
+
end
|
49
|
+
|
50
|
+
# The ProtocolNegotiated event is fired when when ALPN negotiation completes.
|
51
|
+
class ProtocolNegotiated < QuicEvent
|
52
|
+
attr_accessor :alpn_protocol # The protocol which was negotiated using ALPN, or `nil`.
|
53
|
+
end
|
54
|
+
|
55
|
+
# The StreamDataReceived event is fired whenever data is received on a stream.
|
56
|
+
class StreamDataReceived < QuicEvent
|
57
|
+
attr_accessor :data # The data which was received.
|
58
|
+
attr_accessor :end_stream # Whether the STREAM frame has the FIN bit set.
|
59
|
+
attr_accessor :stream_id # The ID of the stream the data was received for.
|
60
|
+
end
|
61
|
+
|
62
|
+
# The StreamReset event is fired when the remote peer resets a stream.
|
63
|
+
class StreamReset < QuicEvent
|
64
|
+
attr_accessor :error_code # The error code that triggered the reset.
|
65
|
+
attr_accessor :stream_id # The ID of the stream that was reset.
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,272 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Raioquic
|
6
|
+
module Quic
|
7
|
+
module Logger
|
8
|
+
QLOG_VERSION = "0.3"
|
9
|
+
|
10
|
+
# A QUIC event trace.
|
11
|
+
#
|
12
|
+
# Events are logged in the format defined by qlog.
|
13
|
+
#
|
14
|
+
# See:
|
15
|
+
# - https://datatracker.ietf.org/doc/html/draft-ietf-quic-qlog-main-schema-02
|
16
|
+
# - https://datatracker.ietf.org/doc/html/draft-marx-quic-qlog-quic-events
|
17
|
+
# - https://datatracker.ietf.org/doc/html/draft-marx-quic-qlog-h3-events
|
18
|
+
class QuicLoggerTrace
|
19
|
+
PACKET_TYPE_NAMES = {
|
20
|
+
Quic::Packet::PACKET_TYPE_INITIAL => "initial",
|
21
|
+
Quic::Packet::PACKET_TYPE_HANDSHAKE => "handshake",
|
22
|
+
Quic::Packet::PACKET_TYPE_ZERO_RTT => "0RTT",
|
23
|
+
Quic::Packet::PACKET_TYPE_ONE_RTT => "1RTT",
|
24
|
+
Quic::Packet::PACKET_TYPE_RETRY => "retry",
|
25
|
+
}
|
26
|
+
|
27
|
+
attr_reader :is_client
|
28
|
+
|
29
|
+
def initialize(is_client:, odcid:)
|
30
|
+
@odcid = odcid
|
31
|
+
@events = []
|
32
|
+
@is_client = is_client
|
33
|
+
@vantage_point = {
|
34
|
+
name: "raioquic(aioquic porting)",
|
35
|
+
type: (is_client ? "client" : "server"),
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def encode_ack_frame(ranges:, delay:)
|
40
|
+
{
|
41
|
+
ack_delay: encode_time(delay),
|
42
|
+
acked_ranges: ranges.list.map { |x| [x.first, x.last - 1] },
|
43
|
+
frame_type: "ack",
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def encode_connection_close_frame(error_code:, frame_type: nil, reason_phrase:)
|
48
|
+
attrs = {
|
49
|
+
error_code: error_code,
|
50
|
+
error_space: (frame_type ? "transport" : "application"),
|
51
|
+
frame_type: "connection_close",
|
52
|
+
raw_error_code: error_code,
|
53
|
+
reason: reason_phrase,
|
54
|
+
}
|
55
|
+
attrs[:trigger_frame_type] = frame_type if frame_type
|
56
|
+
attrs
|
57
|
+
end
|
58
|
+
|
59
|
+
def encode_connection_limit_frame(frame_type:, maximum:)
|
60
|
+
if frame_type == Quic::Packet::QuicFrameType::MAX_DATA
|
61
|
+
{ frame_type: "max_data", maximum: maximum }
|
62
|
+
else
|
63
|
+
{
|
64
|
+
frame_type: "max_streams",
|
65
|
+
maximum: maximum,
|
66
|
+
stream_type: (frame_type == Quic::Packet::QuicFrameType::MAX_STREAMS_UNI ? "unidirectional" : "bidirectional"),
|
67
|
+
}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def encode_crypto_frame(frame:)
|
72
|
+
{
|
73
|
+
frame_type: "crypto",
|
74
|
+
length: frame.data.bytesize,
|
75
|
+
offset: frame.offset,
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
def encode_data_blocked_frame(limit:)
|
80
|
+
{ frame_type: "data_blocked", limit: limit }
|
81
|
+
end
|
82
|
+
|
83
|
+
def encode_datagram_frame(length:)
|
84
|
+
{ frame_type: "datagram", length: length }
|
85
|
+
end
|
86
|
+
|
87
|
+
def encode_handshake_done_frame
|
88
|
+
{ frame_type: "handshake_done" }
|
89
|
+
end
|
90
|
+
|
91
|
+
def encode_max_stream_data_frame(maximum:, stream_id:)
|
92
|
+
{
|
93
|
+
frame_type: "max_stream_data",
|
94
|
+
maximum: maximum,
|
95
|
+
stream_id: stream_id,
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
def encode_new_connection_id_frame(connection_id:, retire_prior_to:, sequence_number:, stateless_reset_token:)
|
100
|
+
{
|
101
|
+
connection_id: connection_id.unpack1("H*"),
|
102
|
+
frame_type: "new_connection_id",
|
103
|
+
length: connection_id.bytesize,
|
104
|
+
reset_token: stateless_reset_token.unpack1("H*"),
|
105
|
+
retire_prior_to: retire_prior_to,
|
106
|
+
sequence_number: sequence_number,
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
def encode_new_token_frame(token:)
|
111
|
+
{
|
112
|
+
frame_type: "new_token",
|
113
|
+
length: token.bytesize,
|
114
|
+
token: token.unpack1("H*"),
|
115
|
+
}
|
116
|
+
end
|
117
|
+
|
118
|
+
def encode_padding_frame
|
119
|
+
{ frame_type: "padding" }
|
120
|
+
end
|
121
|
+
|
122
|
+
def encode_path_challenge_frame(data:)
|
123
|
+
{ frame_type: "path_challenge", data: data.unpack1("H*") }
|
124
|
+
end
|
125
|
+
|
126
|
+
def encode_path_response_frame(data:)
|
127
|
+
{ frame_type: "path_response", data: data.unpack1("H*") }
|
128
|
+
end
|
129
|
+
|
130
|
+
def encode_ping_frame
|
131
|
+
{ frame_type: "ping" }
|
132
|
+
end
|
133
|
+
|
134
|
+
def encode_reset_stream_frame(error_code:, final_size:, stream_id:)
|
135
|
+
{
|
136
|
+
error_code: error_code,
|
137
|
+
final_size: final_size,
|
138
|
+
frame_type: "reset_stream",
|
139
|
+
stream_id: stream_id,
|
140
|
+
}
|
141
|
+
end
|
142
|
+
|
143
|
+
def encode_retire_connection_id_frame(sequence_number:)
|
144
|
+
{
|
145
|
+
frame_type: "retire_connection_id",
|
146
|
+
sequence_number: sequence_number,
|
147
|
+
}
|
148
|
+
end
|
149
|
+
|
150
|
+
def encode_stream_data_blocked_frame(limit:, stream_id:)
|
151
|
+
{
|
152
|
+
frame_type: "stream_data_blocked",
|
153
|
+
limit: limit,
|
154
|
+
stream_id: stream_id,
|
155
|
+
}
|
156
|
+
end
|
157
|
+
|
158
|
+
def encode_stop_sending_frame(error_code:, stream_id:)
|
159
|
+
{
|
160
|
+
frame_type: "stop_sending",
|
161
|
+
error_code: error_code,
|
162
|
+
stream_id: stream_id,
|
163
|
+
}
|
164
|
+
end
|
165
|
+
|
166
|
+
def encode_stream_frame(frame:, stream_id:)
|
167
|
+
{
|
168
|
+
fin: frame.fin,
|
169
|
+
frame_type: "stream",
|
170
|
+
length: frame.data.bytesize,
|
171
|
+
offset: frame.offset,
|
172
|
+
stream_id: stream_id,
|
173
|
+
}
|
174
|
+
end
|
175
|
+
|
176
|
+
def encode_streams_blocked_frame(is_unidirectional:, limit:)
|
177
|
+
{
|
178
|
+
frame_type: "streams_blocked",
|
179
|
+
limit: limit,
|
180
|
+
stream_type: (is_unidirectional ? "unidirectional" : "bidirectional"),
|
181
|
+
}
|
182
|
+
end
|
183
|
+
|
184
|
+
# Convert a time to milliseconds.
|
185
|
+
def encode_time(seconds)
|
186
|
+
seconds * 1000
|
187
|
+
end
|
188
|
+
|
189
|
+
def encode_transport_parameters(owner:, parameters:)
|
190
|
+
data = { owner: owner }
|
191
|
+
parameters.each_pair do |key, value|
|
192
|
+
if value.is_a?(TrueClass) || value.is_a?(FalseClass) || value.is_a?(Integer)
|
193
|
+
data[key] = value
|
194
|
+
elsif value.is_a?(String)
|
195
|
+
data[key] = value.unpack1("H*")
|
196
|
+
end
|
197
|
+
end
|
198
|
+
data
|
199
|
+
end
|
200
|
+
|
201
|
+
def packet_type(packet_type)
|
202
|
+
PACKET_TYPE_NAMES[packet_type & Quic::Packet::PACKET_TYPE_MASK] || "1RTT"
|
203
|
+
end
|
204
|
+
|
205
|
+
def log_event(category:, event:, data:)
|
206
|
+
@events << {
|
207
|
+
data: data,
|
208
|
+
name: "#{category}:#{event}",
|
209
|
+
time: encode_time(Time.now.to_f),
|
210
|
+
}
|
211
|
+
end
|
212
|
+
|
213
|
+
# Return the trace as a dictionary which can be written as JSON.
|
214
|
+
def to_dict
|
215
|
+
{
|
216
|
+
common_fields: {
|
217
|
+
ODCID: @odcid.unpack1("H*"),
|
218
|
+
},
|
219
|
+
events: @events,
|
220
|
+
vantage_point: @vantage_point,
|
221
|
+
}
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
class QuicLogger
|
226
|
+
def initialize
|
227
|
+
@traces = []
|
228
|
+
end
|
229
|
+
|
230
|
+
def start_trace(is_client:, odcid:)
|
231
|
+
@trace = QuicLoggerTrace.new(is_client: is_client, odcid: odcid)
|
232
|
+
@traces << @trace
|
233
|
+
@trace
|
234
|
+
end
|
235
|
+
|
236
|
+
def to_dict
|
237
|
+
{
|
238
|
+
qlog_format: "JSON",
|
239
|
+
qlog_version: QLOG_VERSION,
|
240
|
+
traces: @traces.map(&:to_dict),
|
241
|
+
}
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# A QUIC event logger which writes one trace per file.
|
246
|
+
class QuicFileLogger < QuicLogger
|
247
|
+
def initialize(path:)
|
248
|
+
raise ValueError, "QUIC log output directory #{path} does not exist" unless File.directory?(path)
|
249
|
+
|
250
|
+
@path = path # TODO: path check
|
251
|
+
super()
|
252
|
+
end
|
253
|
+
|
254
|
+
def end_trace(trace)
|
255
|
+
return unless trace
|
256
|
+
trace_dict = trace.to_dict
|
257
|
+
trace_type = trace.is_client ? "client" : "server"
|
258
|
+
trace_path = File.join(@path, trace_dict[:common_fields][:ODCID] + "_#{trace_type}.qlog")
|
259
|
+
File.write(
|
260
|
+
trace_path,
|
261
|
+
JSON.generate({
|
262
|
+
qlog_format: "JSON",
|
263
|
+
qlog_version: QLOG_VERSION,
|
264
|
+
traces: [trace_dict],
|
265
|
+
}))
|
266
|
+
idx = @traces.find_index(trace)
|
267
|
+
@traces.delete_at(idx) if idx
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|