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,301 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../buffer"
|
4
|
+
require_relative "../tls"
|
5
|
+
require_relative "packet"
|
6
|
+
|
7
|
+
module Raioquic
|
8
|
+
module Quic
|
9
|
+
class PacketBuilder
|
10
|
+
PACKET_MAX_SIZE = 1280
|
11
|
+
PACKET_LENGTH_SEND_SIZE = 2
|
12
|
+
PACKET_NUMBER_SEND_SIZE = 2
|
13
|
+
|
14
|
+
class QuicDeliveryState
|
15
|
+
ACKED = 0
|
16
|
+
LOST = 1
|
17
|
+
EXPIRED = 2
|
18
|
+
end
|
19
|
+
|
20
|
+
QuicSentPacket = _ = Struct.new( # rubocop:disable Naming/ConstantName
|
21
|
+
:epoch,
|
22
|
+
:in_flight,
|
23
|
+
:is_ack_eliciting,
|
24
|
+
:is_crypto_packet,
|
25
|
+
:packet_number,
|
26
|
+
:packet_type,
|
27
|
+
:sent_time,
|
28
|
+
:sent_bytes,
|
29
|
+
:delivery_handlers,
|
30
|
+
:quic_logger_frames,
|
31
|
+
)
|
32
|
+
|
33
|
+
QuicPacketBuilderStop = Class.new(StandardError)
|
34
|
+
|
35
|
+
# Helper for building QUIC packets.
|
36
|
+
class QuicPacketBuilder
|
37
|
+
attr_reader :packet_number
|
38
|
+
attr_reader :quic_logger_frames
|
39
|
+
attr_accessor :max_flight_bytes
|
40
|
+
attr_accessor :max_total_bytes
|
41
|
+
|
42
|
+
# Helper for building QUIC packets.
|
43
|
+
def initialize(host_cid:, peer_cid:, version:, is_client:, packet_number: 0, peer_token: "", quic_logger: nil, spin_bit: false)
|
44
|
+
@max_flight_bytes = nil
|
45
|
+
@max_total_bytes = nil
|
46
|
+
@quic_logger_frames = nil
|
47
|
+
|
48
|
+
@host_cid = host_cid
|
49
|
+
@is_client = is_client
|
50
|
+
@peer_cid = peer_cid
|
51
|
+
@peer_token = peer_token
|
52
|
+
@quic_logger = quic_logger
|
53
|
+
@spin_bit = spin_bit
|
54
|
+
@version = version
|
55
|
+
|
56
|
+
@datagrams = []
|
57
|
+
@datagram_flight_bytes = 0
|
58
|
+
@datagram_init = true
|
59
|
+
@packets = []
|
60
|
+
@flight_bytes = 0
|
61
|
+
@total_bytes = 0
|
62
|
+
|
63
|
+
@header_size = 0
|
64
|
+
@packet = nil
|
65
|
+
@packet_crypto = nil
|
66
|
+
@packet_long_header = false
|
67
|
+
@packet_number = packet_number
|
68
|
+
@packet_start = 0
|
69
|
+
@packet_type = 0
|
70
|
+
|
71
|
+
@buffer = Buffer.new(capacity: PACKET_MAX_SIZE)
|
72
|
+
@buffer_capacity = PACKET_MAX_SIZE
|
73
|
+
@flight_capacity = PACKET_MAX_SIZE
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns true if the current packet is empty
|
77
|
+
def packet_is_empty
|
78
|
+
raise RuntimeError unless @packet
|
79
|
+
|
80
|
+
packet_size = @buffer.tell - @packet_start
|
81
|
+
# puts "*** Builder#packet_is_empty packet_size: #{packet_size}, header_size: #{@header_size}, buffer#tell: #{@buffer.tell}, @packet_start: #{@packet_start}"
|
82
|
+
return packet_size <= @header_size
|
83
|
+
end
|
84
|
+
|
85
|
+
def _packet_size
|
86
|
+
@buffer.tell - @packet_start
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns the remaining number of bytes which can be used in the current packet.
|
90
|
+
def remaining_buffer_space
|
91
|
+
@buffer_capacity - @buffer.tell - @packet_crypto.aead_tag_size
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns the remaining number of bytes which can be used in the current packet
|
95
|
+
def remaining_flight_space
|
96
|
+
@flight_capacity - @buffer.tell - @packet_crypto.aead_tag_size
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns the assembled datagrams
|
100
|
+
def flush
|
101
|
+
end_packet if @packet
|
102
|
+
|
103
|
+
flush_current_datagram
|
104
|
+
datagrams = @datagrams.dup
|
105
|
+
packets = @packets.dup
|
106
|
+
@datagrams = []
|
107
|
+
@packets = []
|
108
|
+
return [datagrams, packets]
|
109
|
+
end
|
110
|
+
|
111
|
+
# Starts a new frame.
|
112
|
+
def start_frame(frame_type:, capacity: 1, handler: nil, handler_args: []) # rubocop:disable Metrics/CyclomaticComplexity
|
113
|
+
if remaining_buffer_space < capacity || (
|
114
|
+
!Quic::Packet::NON_IN_FLIGHT_FRAME_TYPES.include?(frame_type) && remaining_flight_space < capacity
|
115
|
+
)
|
116
|
+
raise QuicPacketBuilderStop
|
117
|
+
end
|
118
|
+
|
119
|
+
@buffer.push_uint_var(frame_type)
|
120
|
+
@packet.is_ack_eliciting = true unless Quic::Packet::NON_ACK_ELICITING_FRAME_TYPES.include?(frame_type)
|
121
|
+
@packet.in_flight = true unless Quic::Packet::NON_IN_FLIGHT_FRAME_TYPES.include?(frame_type)
|
122
|
+
@packet.is_crypto_packet = true if frame_type == Quic::Packet::QuicFrameType::CRYPTO
|
123
|
+
if handler
|
124
|
+
# pp handler_args
|
125
|
+
@packet.delivery_handlers.append([handler, handler_args]) # TODO: what's this?
|
126
|
+
end
|
127
|
+
return @buffer
|
128
|
+
end
|
129
|
+
|
130
|
+
# Starts a new packet.
|
131
|
+
def start_packet(packet_type:, crypto:) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
132
|
+
buf = @buffer
|
133
|
+
|
134
|
+
# finish previous datagrams
|
135
|
+
end_packet if @packet
|
136
|
+
|
137
|
+
# if there is too little space remaining, start a new datagram
|
138
|
+
# FIXME: the limit is arbitrary! (from aioquic)
|
139
|
+
packet_start = buf.tell
|
140
|
+
if @buffer_capacity - packet_start < 128
|
141
|
+
flush_current_datagram
|
142
|
+
packet_start = 0
|
143
|
+
end
|
144
|
+
|
145
|
+
# initialize datagram if needed
|
146
|
+
# rubocop:disable Style/IfUnlessModifier
|
147
|
+
if @datagram_init
|
148
|
+
unless @max_total_bytes.nil?
|
149
|
+
remaining_total_bytes = @max_total_bytes - @total_bytes
|
150
|
+
if remaining_total_bytes < @buffer_capacity
|
151
|
+
@buffer_capacity = remaining_total_bytes
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
@flight_capacity = @buffer_capacity
|
156
|
+
unless @max_flight_bytes.nil?
|
157
|
+
remaining_flight_bytes = @max_flight_bytes - @flight_bytes
|
158
|
+
if remaining_flight_bytes < @flight_capacity
|
159
|
+
@flight_capacity = remaining_flight_bytes
|
160
|
+
end
|
161
|
+
end
|
162
|
+
@datagram_flight_bytes = 0
|
163
|
+
@datagram_init = false
|
164
|
+
end
|
165
|
+
# rubocop:enable Style/IfUnlessModifier
|
166
|
+
|
167
|
+
# calculate header size
|
168
|
+
packet_long_header = Quic::Packet.is_long_header(packet_type)
|
169
|
+
if packet_long_header
|
170
|
+
header_size = 11 + @peer_cid.bytesize + @host_cid.bytesize
|
171
|
+
if (packet_type & Quic::Packet::PACKET_TYPE_MASK) == Quic::Packet::PACKET_TYPE_INITIAL
|
172
|
+
token_length = @peer_token.bytesize
|
173
|
+
header_size += Buffer.size_uint_var(token_length) + token_length
|
174
|
+
end
|
175
|
+
else
|
176
|
+
header_size = 3 + @peer_cid.bytesize
|
177
|
+
end
|
178
|
+
|
179
|
+
# check we have enough space
|
180
|
+
raise QuicPacketBuilderStop if packet_start + header_size >= @buffer_capacity
|
181
|
+
|
182
|
+
# determine ack epoch
|
183
|
+
# rubocop:disable Style/CaseLikeIf
|
184
|
+
epoch = if packet_type == Quic::Packet::PACKET_TYPE_INITIAL
|
185
|
+
TLS::Epoch::INITIAL
|
186
|
+
elsif packet_type == Quic::Packet::PACKET_TYPE_HANDSHAKE
|
187
|
+
TLS::Epoch::HANDSHAKE
|
188
|
+
else
|
189
|
+
TLS::Epoch::ONE_RTT
|
190
|
+
end
|
191
|
+
# rubocop:enable Style/CaseLikeIf
|
192
|
+
|
193
|
+
@header_size = header_size
|
194
|
+
@packet = QuicSentPacket.new.tap do |p|
|
195
|
+
p.epoch = epoch
|
196
|
+
p.in_flight = false
|
197
|
+
p.is_ack_eliciting = false
|
198
|
+
p.is_crypto_packet = false
|
199
|
+
p.packet_number = @packet_number
|
200
|
+
p.packet_type = packet_type
|
201
|
+
p.delivery_handlers = []
|
202
|
+
p.quic_logger_frames = []
|
203
|
+
end
|
204
|
+
@packet_crypto = crypto
|
205
|
+
@packet_long_header = packet_long_header
|
206
|
+
@packet_start = packet_start
|
207
|
+
@packet_type = packet_type
|
208
|
+
@quic_logger_frames = @packet.quic_logger_frames
|
209
|
+
|
210
|
+
buf.seek(@packet_start + @header_size)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Ends the current packet.
|
214
|
+
def end_packet # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
215
|
+
buf = @buffer
|
216
|
+
packet_size = buf.tell - @packet_start
|
217
|
+
|
218
|
+
if packet_size > @header_size
|
219
|
+
# padding to ensure sufficient sample size
|
220
|
+
padding_size = Quic::Packet::PACKET_NUMBER_MAX_SIZE - PACKET_NUMBER_SEND_SIZE + @header_size - packet_size
|
221
|
+
|
222
|
+
# padding for initial datagram
|
223
|
+
if @is_client && @packet_type == Quic::Packet::PACKET_TYPE_INITIAL && @packet.is_ack_eliciting && remaining_flight_space && remaining_flight_space > padding_size # rubocop:disable Layout/LineLength
|
224
|
+
padding_size = remaining_flight_space
|
225
|
+
end
|
226
|
+
|
227
|
+
if padding_size > 0
|
228
|
+
buf.push_bytes("\x00" * padding_size)
|
229
|
+
packet_size += padding_size
|
230
|
+
@packet.in_flight = true
|
231
|
+
|
232
|
+
# log frame
|
233
|
+
if @quic_logger
|
234
|
+
@packet.quic_logger_frames << @quic_logger.encode_padding_frame
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# write header
|
239
|
+
# rubocop:disable Style/IdenticalConditionalBranches
|
240
|
+
if @packet_long_header
|
241
|
+
length = packet_size - @header_size + PACKET_NUMBER_SEND_SIZE + @packet_crypto.aead_tag_size
|
242
|
+
buf.seek(@packet_start)
|
243
|
+
buf.push_uint8(@packet_type | (PACKET_NUMBER_SEND_SIZE - 1))
|
244
|
+
buf.push_uint32(@version)
|
245
|
+
buf.push_uint8(@peer_cid.length)
|
246
|
+
buf.push_bytes(@peer_cid)
|
247
|
+
buf.push_uint8(@host_cid.length)
|
248
|
+
buf.push_bytes(@host_cid)
|
249
|
+
if @packet_type & Quic::Packet::PACKET_TYPE_MASK == Quic::Packet::PACKET_TYPE_INITIAL
|
250
|
+
buf.push_uint_var(@peer_token.length)
|
251
|
+
buf.push_bytes(@peer_token)
|
252
|
+
end
|
253
|
+
buf.push_uint16(length | 0x4000)
|
254
|
+
buf.push_uint16(@packet_number & 0xffff)
|
255
|
+
else
|
256
|
+
buf.seek(@packet_start)
|
257
|
+
buf.push_uint8(@packet_type | ((@spin_bit ? 1 : 0) << 5) | (@packet_crypto.key_phase << 2) | (PACKET_NUMBER_SEND_SIZE - 1))
|
258
|
+
buf.push_bytes(@peer_cid)
|
259
|
+
buf.push_uint16(@packet_number & 0xffff)
|
260
|
+
end
|
261
|
+
# rubocop:enable Style/IdenticalConditionalBranches
|
262
|
+
|
263
|
+
# encrypt in place
|
264
|
+
plain = buf.data_slice(start: @packet_start, ends: @packet_start + packet_size)
|
265
|
+
buf.seek(@packet_start)
|
266
|
+
buf.push_bytes(
|
267
|
+
@packet_crypto.encrypt_packet(
|
268
|
+
plain_header: plain[0...@header_size].force_encoding(Encoding::ASCII_8BIT),
|
269
|
+
plain_payload: plain[@header_size...packet_size].force_encoding(Encoding::ASCII_8BIT),
|
270
|
+
packet_number: @packet_number,
|
271
|
+
),
|
272
|
+
)
|
273
|
+
@packet.sent_bytes = buf.tell - @packet_start
|
274
|
+
@packets << @packet
|
275
|
+
|
276
|
+
@datagram_flight_bytes += @packet.sent_bytes if @packet.in_flight
|
277
|
+
flush_current_datagram unless @packet_long_header
|
278
|
+
@packet_number += 1
|
279
|
+
else # packet_size > @header_size
|
280
|
+
# "cancel" the packet
|
281
|
+
buf.seek(@packet_start)
|
282
|
+
end
|
283
|
+
|
284
|
+
@packet = nil
|
285
|
+
@quic_logger_frames = nil
|
286
|
+
end
|
287
|
+
|
288
|
+
def flush_current_datagram
|
289
|
+
datagram_bytes = @buffer.tell
|
290
|
+
if datagram_bytes > 0 # rubocop:disable Style/GuardClause
|
291
|
+
@datagrams << @buffer.data
|
292
|
+
@flight_bytes += @datagram_flight_bytes
|
293
|
+
@total_bytes += datagram_bytes
|
294
|
+
@datagram_init = true
|
295
|
+
@buffer.seek(0)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raioquic
|
4
|
+
module Quic
|
5
|
+
# Raioquic::Quic::Rangeset
|
6
|
+
# Migrated from aioquic/src/aioquic/quic/rangeset.py
|
7
|
+
class Rangeset
|
8
|
+
def initialize(ranges: [])
|
9
|
+
@ranges = []
|
10
|
+
ranges.each do |r|
|
11
|
+
add(r.first, r.last)
|
12
|
+
end
|
13
|
+
sort
|
14
|
+
end
|
15
|
+
|
16
|
+
def list
|
17
|
+
@ranges
|
18
|
+
end
|
19
|
+
|
20
|
+
def add(start, stop = nil)
|
21
|
+
stop = start + 1 if stop.nil?
|
22
|
+
|
23
|
+
@ranges.each_with_index do |r, i|
|
24
|
+
# the added range is entirely before current item, insert here
|
25
|
+
if stop < r.first
|
26
|
+
@ranges.insert(i, (start...stop))
|
27
|
+
return # rubocop:disable Lint/NonLocalExitFromIterator
|
28
|
+
end
|
29
|
+
|
30
|
+
# the added range is entirely after current item, keep looking
|
31
|
+
next if start > r.last
|
32
|
+
|
33
|
+
# the added range touches the current item, merge it
|
34
|
+
start = [start, r.first].min
|
35
|
+
stop = [stop, r.last].max
|
36
|
+
while i < @ranges.size - 1 && @ranges[i + 1].first <= stop
|
37
|
+
stop = [@ranges[i + 1].last, stop].max
|
38
|
+
@ranges.delete_at(i + 1)
|
39
|
+
end
|
40
|
+
@ranges[i] = start...stop
|
41
|
+
return # rubocop:disable Lint/NonLocalExitFromIterator
|
42
|
+
end
|
43
|
+
# the added range is entirely after all existing items, append it
|
44
|
+
@ranges << (start...stop)
|
45
|
+
sort
|
46
|
+
end
|
47
|
+
|
48
|
+
def bounds
|
49
|
+
@ranges.first&.first...@ranges.last&.last
|
50
|
+
end
|
51
|
+
|
52
|
+
def subtract(start, stop) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
53
|
+
raise RuntimeError if stop < start
|
54
|
+
|
55
|
+
i = 0
|
56
|
+
while i < @ranges.length
|
57
|
+
r = @ranges[i]
|
58
|
+
|
59
|
+
# the removed range is entirely before current item, stop here
|
60
|
+
return if stop <= r.first
|
61
|
+
|
62
|
+
# the removed range is entirely after current item, keep looking
|
63
|
+
if start >= r.last
|
64
|
+
i += 1
|
65
|
+
next
|
66
|
+
end
|
67
|
+
|
68
|
+
# the removed range completely covers the current item, remove it
|
69
|
+
if start <= r.first && stop >= r.last
|
70
|
+
@ranges.delete_at(i)
|
71
|
+
next
|
72
|
+
end
|
73
|
+
|
74
|
+
# the removed range touches the current item
|
75
|
+
if start > r.first
|
76
|
+
@ranges[i] = r.first...start
|
77
|
+
@ranges.insert(i + 1, stop...r.last) if stop < r.last
|
78
|
+
else
|
79
|
+
@ranges[i] = stop...r.last
|
80
|
+
end
|
81
|
+
i += 1
|
82
|
+
end
|
83
|
+
sort
|
84
|
+
end
|
85
|
+
|
86
|
+
def shift
|
87
|
+
@ranges.shift
|
88
|
+
end
|
89
|
+
|
90
|
+
def in?(value)
|
91
|
+
@ranges.any? { |r| r.cover?(value) }
|
92
|
+
end
|
93
|
+
|
94
|
+
def length
|
95
|
+
@ranges.length
|
96
|
+
end
|
97
|
+
|
98
|
+
def eql?(other)
|
99
|
+
return false if other.class != self.class
|
100
|
+
return false if other.length != length
|
101
|
+
|
102
|
+
length.times.all? do |i|
|
103
|
+
@ranges[i] == other.list[i]
|
104
|
+
end
|
105
|
+
end
|
106
|
+
alias == eql?
|
107
|
+
|
108
|
+
private def sort
|
109
|
+
@ranges.sort! { |a, b| a.first <=> b.first }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|