raioquic 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.containerignore +4 -0
  3. data/.rubocop.yml +93 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +84 -0
  6. data/Containerfile +6 -0
  7. data/Gemfile +24 -0
  8. data/Gemfile.lock +113 -0
  9. data/LICENSE +28 -0
  10. data/README.md +48 -0
  11. data/Rakefile +16 -0
  12. data/Steepfile +8 -0
  13. data/example/curlcatcher.rb +18 -0
  14. data/example/interoperability/README.md +9 -0
  15. data/example/interoperability/aioquic/aioquic_client.py +47 -0
  16. data/example/interoperability/aioquic/aioquic_server.py +34 -0
  17. data/example/interoperability/key.pem +28 -0
  18. data/example/interoperability/localhost-unasuke-dev.crt +21 -0
  19. data/example/interoperability/quic-go/sample_server.go +61 -0
  20. data/example/interoperability/raioquic_client.rb +42 -0
  21. data/example/interoperability/raioquic_server.rb +43 -0
  22. data/example/parse_curl_example.rb +108 -0
  23. data/lib/raioquic/buffer.rb +202 -0
  24. data/lib/raioquic/core_ext.rb +54 -0
  25. data/lib/raioquic/crypto/README.md +5 -0
  26. data/lib/raioquic/crypto/aesgcm.rb +52 -0
  27. data/lib/raioquic/crypto/backend/aead.rb +52 -0
  28. data/lib/raioquic/crypto/backend.rb +12 -0
  29. data/lib/raioquic/crypto.rb +10 -0
  30. data/lib/raioquic/quic/configuration.rb +81 -0
  31. data/lib/raioquic/quic/connection.rb +2776 -0
  32. data/lib/raioquic/quic/crypto.rb +317 -0
  33. data/lib/raioquic/quic/event.rb +69 -0
  34. data/lib/raioquic/quic/logger.rb +272 -0
  35. data/lib/raioquic/quic/packet.rb +471 -0
  36. data/lib/raioquic/quic/packet_builder.rb +301 -0
  37. data/lib/raioquic/quic/rangeset.rb +113 -0
  38. data/lib/raioquic/quic/recovery.rb +528 -0
  39. data/lib/raioquic/quic/stream.rb +343 -0
  40. data/lib/raioquic/quic.rb +20 -0
  41. data/lib/raioquic/tls.rb +1659 -0
  42. data/lib/raioquic/version.rb +5 -0
  43. data/lib/raioquic.rb +12 -0
  44. data/misc/export_x25519.py +43 -0
  45. data/misc/gen_rfc8448_keypair.rb +90 -0
  46. data/raioquic.gemspec +37 -0
  47. data/sig/raioquic/buffer.rbs +37 -0
  48. data/sig/raioquic/core_ext.rbs +7 -0
  49. data/sig/raioquic/crypto/aesgcm.rbs +20 -0
  50. data/sig/raioquic/crypto/backend/aead.rbs +11 -0
  51. data/sig/raioquic/quic/configuration.rbs +34 -0
  52. data/sig/raioquic/quic/connection.rbs +277 -0
  53. data/sig/raioquic/quic/crypto.rbs +88 -0
  54. data/sig/raioquic/quic/event.rbs +51 -0
  55. data/sig/raioquic/quic/logger.rbs +57 -0
  56. data/sig/raioquic/quic/packet.rbs +157 -0
  57. data/sig/raioquic/quic/packet_builder.rbs +76 -0
  58. data/sig/raioquic/quic/rangeset.rbs +17 -0
  59. data/sig/raioquic/quic/recovery.rbs +142 -0
  60. data/sig/raioquic/quic/stream.rbs +87 -0
  61. data/sig/raioquic/tls.rbs +444 -0
  62. data/sig/raioquic.rbs +9 -0
  63. 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