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