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,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
|