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,471 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../buffer"
4
+ require_relative "rangeset"
5
+ require_relative "../crypto/aesgcm"
6
+ require "socket"
7
+ require "ipaddr"
8
+
9
+ module Raioquic
10
+ module Quic
11
+ # Raioquic::Quic::Packet
12
+ # Migrated from aioquic/src/aioquic/quic/packet.py
13
+ class Packet
14
+ PACKET_LONG_HEADER = 0x80
15
+ PACKET_FIXED_BIT = 0x40
16
+ PACKET_SPIN_BIT = 0x20
17
+
18
+ PACKET_TYPE_INITIAL = PACKET_LONG_HEADER | PACKET_FIXED_BIT | 0x00
19
+ PACKET_TYPE_ZERO_RTT = PACKET_LONG_HEADER | PACKET_FIXED_BIT | 0x10
20
+ PACKET_TYPE_HANDSHAKE = PACKET_LONG_HEADER | PACKET_FIXED_BIT | 0x20
21
+ PACKET_TYPE_RETRY = PACKET_LONG_HEADER | PACKET_FIXED_BIT | 0x30
22
+ PACKET_TYPE_ONE_RTT = PACKET_FIXED_BIT
23
+ PACKET_TYPE_MASK = 0xf0
24
+
25
+ CONNECTION_ID_MAX_SIZE = 20
26
+ PACKET_NUMBER_MAX_SIZE = 4
27
+ RETRY_AEAD_KEY_VERSION_1 = ["be0c690b9f66575a1d766b54e368c84e"].pack("H*") # https://www.rfc-editor.org/rfc/rfc9001.html#section-5.8
28
+ RETRY_AEAD_NONCE_VERSION_1 = ["461599d35d632bf2239825bb"].pack("H*") # https://www.rfc-editor.org/rfc/rfc9001.html#section-5.8
29
+ RETRY_INTEGRITY_TAG_SIZE = 16
30
+ STATELESS_RESET_TOKEN_SIZE = 16
31
+
32
+ class QuicErrorCode
33
+ NO_ERROR = 0x0
34
+ INTERNAL_ERROR = 0x1
35
+ CONNECTION_REFUSED = 0x2
36
+ FLOW_CONTROL_ERROR = 0x3
37
+ STREAM_LIMIT_ERROR = 0x4
38
+ STREAM_STATE_ERROR = 0x5
39
+ FINAL_SIZE_ERROR = 0x6
40
+ FRAME_ENCODING_ERROR = 0x7
41
+ TRANSPORT_PARAMETER_ERROR = 0x8
42
+ CONNECTION_ID_LIMIT_ERROR = 0x9
43
+ PROTOCOL_VIOLATION = 0xa
44
+ INVALID_TOKEN = 0xb
45
+ APPLICATION_ERROR = 0xc
46
+ CRYPTO_BUFFER_EXCEEDED = 0xd
47
+ KEY_UPDATE_ERROR = 0xe
48
+ AEAD_LIMIT_REACHED = 0xf
49
+ CRYPTO_ERROR = 0x100
50
+ end
51
+
52
+ class QuicProtocolVersion
53
+ NEGOTIATION = 0x0
54
+ VERSION_1 = 0x00000001
55
+ end
56
+
57
+ class QuicHeader
58
+ attr_accessor :is_long_header
59
+ attr_accessor :version
60
+ attr_accessor :packet_type
61
+ attr_accessor :destination_cid
62
+ attr_accessor :source_cid
63
+ attr_accessor :token
64
+ attr_accessor :integrity_tag
65
+ attr_accessor :rest_length
66
+ end
67
+
68
+ # Recover a packet number from a truncated packet number.
69
+ # See: Appendix A - Sample Packet Number Decoding Algorithm
70
+ def self.decode_packet_number(truncated:, num_bits:, expected:)
71
+ window = 1 << num_bits
72
+ half_window = (window / 2).floor
73
+ candidate = (expected & ~(window - 1)) | truncated
74
+
75
+ if candidate <= expected - half_window && candidate < (1 << 62) - window
76
+ candidate + window
77
+ elsif candidate > expected + half_window && candidate >= window
78
+ candidate - window
79
+ else
80
+ candidate
81
+ end
82
+ end
83
+
84
+ # Calculate the integrity tag for a RETRY packet.
85
+ def self.get_retry_integrity_tag(packet_without_tag:, original_destination_cid:)
86
+ buf = Buffer.new(capacity: 1 + original_destination_cid.size + packet_without_tag.size)
87
+ buf.push_uint8(original_destination_cid.size)
88
+ buf.push_bytes(original_destination_cid)
89
+ buf.push_bytes(packet_without_tag)
90
+ aead_key = RETRY_AEAD_KEY_VERSION_1
91
+ aead_nonce = RETRY_AEAD_NONCE_VERSION_1
92
+ aead = ::Raioquic::Crypto::AESGCM.new(aead_key)
93
+ integrity_tag = aead.encrypt(nonce: aead_nonce, data: "", associated_data: buf.data)
94
+ raise RuntimeError if integrity_tag.length != RETRY_INTEGRITY_TAG_SIZE
95
+
96
+ integrity_tag
97
+ end
98
+
99
+ def self.get_spin_bit(first_byte)
100
+ (first_byte.unpack1("C") & PACKET_SPIN_BIT) != 0
101
+ end
102
+
103
+ def self.is_draft_version(_version)
104
+ return false # raioquic drops draft version's implementation
105
+ end
106
+
107
+ def self.is_long_header(first_byte)
108
+ (first_byte & PACKET_LONG_HEADER) != 0
109
+ end
110
+
111
+ def self.pull_quic_header(buf:, host_cid_length:) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
112
+ first_byte = buf.pull_uint8
113
+ integrity_tag = ""
114
+ token = ""
115
+
116
+ if is_long_header(first_byte)
117
+ version = buf.pull_uint32
118
+ destination_cid_length = buf.pull_uint8
119
+ raise ValueError, "Destination CID is too long (#{destination_cid_length} bytes)" if destination_cid_length > CONNECTION_ID_MAX_SIZE
120
+
121
+ destination_cid = buf.pull_bytes(destination_cid_length)
122
+ source_cid_length = buf.pull_uint8
123
+ raise ValueError, "Souce CID is too long (#{source_cid_length} bytes)" if source_cid_length > CONNECTION_ID_MAX_SIZE
124
+
125
+ source_cid = buf.pull_bytes(source_cid_length)
126
+
127
+ if version == QuicProtocolVersion::NEGOTIATION
128
+ packet_type = nil
129
+ rest_length = buf.capacity - buf.tell
130
+ else
131
+ raise ValueError, "Packet fixed bit is zero" if (first_byte & PACKET_FIXED_BIT) == 0
132
+
133
+ packet_type = first_byte & PACKET_TYPE_MASK
134
+ # rubocop:disable Style/CaseLikeIf
135
+ if packet_type == PACKET_TYPE_INITIAL
136
+ token_length = buf.pull_uint_var
137
+ token = buf.pull_bytes(token_length)
138
+ rest_length = buf.pull_uint_var
139
+ elsif packet_type == PACKET_TYPE_RETRY
140
+ token_length = buf.capacity - buf.tell - RETRY_INTEGRITY_TAG_SIZE
141
+ token = buf.pull_bytes(token_length)
142
+ integrity_tag = buf.pull_bytes(RETRY_INTEGRITY_TAG_SIZE)
143
+ rest_length = 0
144
+ else
145
+ rest_length = buf.pull_uint_var
146
+ end
147
+
148
+ # check remainder length
149
+ raise ValueError, "Packet payload is truncated" if rest_length > buf.capacity - buf.tell
150
+ end
151
+
152
+ return QuicHeader.new.tap do |hdr|
153
+ hdr.is_long_header = true
154
+ hdr.version = version
155
+ hdr.packet_type = packet_type
156
+ hdr.destination_cid = destination_cid
157
+ hdr.source_cid = source_cid
158
+ hdr.token = token
159
+ hdr.integrity_tag = integrity_tag
160
+ hdr.rest_length = rest_length
161
+ end
162
+ else
163
+ # short header packet
164
+ raise ValueError, "Packet fixed bit is zero" if (first_byte & PACKET_FIXED_BIT) == 0
165
+
166
+ packet_type = first_byte & PACKET_TYPE_MASK
167
+ destination_cid = buf.pull_bytes(host_cid_length)
168
+
169
+ return QuicHeader.new.tap do |hdr|
170
+ hdr.is_long_header = false
171
+ hdr.version = nil
172
+ hdr.packet_type = packet_type
173
+ hdr.destination_cid = destination_cid
174
+ hdr.source_cid = ""
175
+ hdr.token = ""
176
+ hdr.integrity_tag = integrity_tag
177
+ hdr.rest_length = buf.capacity - buf.tell
178
+ end
179
+ end
180
+ end
181
+
182
+ def self.encode_quic_retry(version:, source_cid:, destination_cid:, original_destination_cid:, retry_token:)
183
+ buf = Buffer.new(capacity: 7 + destination_cid.size + source_cid.size + retry_token.size + RETRY_INTEGRITY_TAG_SIZE)
184
+ buf.push_uint8(PACKET_TYPE_RETRY)
185
+ buf.push_uint32(version)
186
+ buf.push_uint8(destination_cid.length)
187
+ buf.push_bytes(destination_cid)
188
+ buf.push_uint8(source_cid.length)
189
+ buf.push_bytes(source_cid)
190
+ buf.push_bytes(retry_token)
191
+ buf.push_bytes(get_retry_integrity_tag(packet_without_tag: buf.data, original_destination_cid: original_destination_cid))
192
+
193
+ return buf.data
194
+ end
195
+
196
+ def self.encode_quic_version_negotiation(source_cid:, destination_cid:, supported_versions:)
197
+ buf = Buffer.new(capacity: 7 + destination_cid.length + source_cid.length + (4 * supported_versions.length))
198
+ buf.push_uint8(get_urandom_byte | PACKET_LONG_HEADER)
199
+ buf.push_uint32(QuicProtocolVersion::NEGOTIATION)
200
+ buf.push_uint8(destination_cid.length)
201
+ buf.push_bytes(destination_cid)
202
+ buf.push_uint8(source_cid.length)
203
+ buf.push_bytes(source_cid)
204
+ supported_versions.each do |version|
205
+ buf.push_uint32(version)
206
+ end
207
+ buf.data
208
+ end
209
+
210
+ # private
211
+ def self.get_urandom_byte
212
+ Random.urandom(1)[0].unpack1("C")
213
+ end
214
+ private_class_method :get_urandom_byte
215
+
216
+ QuicPreferredAddress = _ = Struct.new( # rubocop:disable Naming/ConstantName
217
+ :ipv4_address,
218
+ :ipv6_address,
219
+ :connection_id,
220
+ :stateless_reset_token,
221
+ )
222
+
223
+ QuicTransportParameters = _ = Struct.new( # rubocop:disable Naming/ConstantName
224
+ :original_destination_connection_id,
225
+ :max_idle_timeout,
226
+ :stateless_reset_token,
227
+ :max_udp_payload_size,
228
+ :initial_max_data,
229
+ :initial_max_stream_data_bidi_local,
230
+ :initial_max_stream_data_bidi_remote,
231
+ :initial_max_stream_data_uni,
232
+ :initial_max_streams_bidi,
233
+ :initial_max_streams_uni,
234
+ :ack_delay_exponent,
235
+ :max_ack_delay,
236
+ :disable_active_migration,
237
+ :preferred_address,
238
+ :active_connection_id_limit,
239
+ :initial_source_connection_id,
240
+ :retry_source_connection_id,
241
+ :max_datagram_frame_size,
242
+ :quantum_readiness,
243
+ )
244
+
245
+ PARAMS = {
246
+ 0x00 => { name: :original_destination_connection_id, type: :bytes },
247
+ 0x01 => { name: :max_idle_timeout, type: :int },
248
+ 0x02 => { name: :stateless_reset_token, type: :bytes },
249
+ 0x03 => { name: :max_udp_payload_size, type: :int },
250
+ 0x04 => { name: :initial_max_data, type: :int },
251
+ 0x05 => { name: :initial_max_stream_data_bidi_local, type: :int },
252
+ 0x06 => { name: :initial_max_stream_data_bidi_remote, type: :int },
253
+ 0x07 => { name: :initial_max_stream_data_uni, type: :int },
254
+ 0x08 => { name: :initial_max_streams_bidi, type: :int },
255
+ 0x09 => { name: :initial_max_streams_uni, type: :int },
256
+ 0x0a => { name: :ack_delay_exponent, type: :int },
257
+ 0x0b => { name: :max_ack_delay, type: :int },
258
+ 0x0c => { name: :disable_active_migration, type: :bool },
259
+ 0x0d => { name: :preferred_address, type: :quicpreferredaddress },
260
+ 0x0e => { name: :active_connection_id_limit, type: :int },
261
+ 0x0f => { name: :initial_source_connection_id, type: :bytes },
262
+ 0x10 => { name: :retry_source_connection_id, type: :bytes },
263
+ # extensions
264
+ 0x0020 => { name: :max_datagram_frame_size, type: :int },
265
+ 0x0c37 => { name: :quantum_readiness, type: :bytes },
266
+ }.freeze
267
+
268
+ def self.pull_quic_preferred_address(buf)
269
+ ipv4_address = nil
270
+ ipv4_host = buf.pull_bytes(4)
271
+ ipv4_port = buf.pull_uint16
272
+
273
+ # rubocop:disable Style/IfUnlessModifier
274
+ if ipv4_host != "\x00\x00\x00\x00"
275
+ ipv4_address = { host: IPAddr.new_ntoh(ipv4_host), port: ipv4_port }
276
+ end
277
+
278
+ ipv6_address = nil
279
+ ipv6_host = buf.pull_bytes(16)
280
+ ipv6_port = buf.pull_uint16
281
+
282
+ if ipv6_host != "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
283
+ ipv6_address = { host: IPAddr.new_ntoh(ipv6_host), port: ipv6_port }
284
+ end
285
+ # rubocop:enable Style/IfUnlessModifier
286
+
287
+ connection_id_length = buf.pull_uint8
288
+ connection_id = buf.pull_bytes(connection_id_length)
289
+ stateless_reset_token = buf.pull_bytes(16)
290
+
291
+ QuicPreferredAddress.new.tap do |addr|
292
+ addr.ipv4_address = ipv4_address
293
+ addr.ipv6_address = ipv6_address
294
+ addr.connection_id = connection_id
295
+ addr.stateless_reset_token = stateless_reset_token
296
+ end
297
+ end
298
+
299
+ def self.push_quic_preferred_address(buf:, preferred_address:)
300
+ if preferred_address[:ipv4_address]
301
+ buf.push_bytes(preferred_address[:ipv4_address][:host].hton)
302
+ buf.push_uint16(preferred_address[:ipv4_address][:port])
303
+ else
304
+ buf.push_bytes("\x00\x00\x00\x00\x00\x00")
305
+ end
306
+
307
+ if preferred_address[:ipv6_address]
308
+ buf.push_bytes(preferred_address[:ipv6_address][:host].hton)
309
+ buf.push_uint16(preferred_address[:ipv6_address][:port])
310
+ else
311
+ buf.push_bytes("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
312
+ end
313
+
314
+ buf.push_uint8(preferred_address[:connection_id].bytesize)
315
+ buf.push_bytes(preferred_address[:connection_id])
316
+ buf.push_bytes(preferred_address[:stateless_reset_token])
317
+ end
318
+
319
+ def self.pull_quic_transport_parameters(buf) # rubocop:disable Metrics/PerceivedComplexity
320
+ params = QuicTransportParameters.new
321
+ while !buf.eof # rubocop:disable Style/NegatedWhile
322
+ param_id = buf.pull_uint_var
323
+ param_len = buf.pull_uint_var
324
+ param_start = buf.tell
325
+ if PARAMS.key? param_id
326
+ param = PARAMS[param_id]
327
+ # rubocop:disable Style/ConditionalAssignment
328
+ if param[:type] == :int
329
+ params[param[:name]] = buf.pull_uint_var
330
+ elsif param[:type] == :bytes
331
+ params[param[:name]] = buf.pull_bytes(param_len)
332
+ elsif param[:type] == :quicpreferredaddress
333
+ params[param[:name]] = pull_quic_preferred_address(buf)
334
+ else
335
+ params[param[:name]] = true
336
+ end
337
+ # rubocop:enable Style/ConditionalAssignment
338
+ else
339
+ # skip unknown parameter
340
+ buf.pull_bytes(param_len)
341
+ end
342
+ raise RuntimeError if buf.tell != param_start + param_len
343
+ end
344
+ params
345
+ end
346
+
347
+ def self.push_quic_transport_parameters(buf:, params:)
348
+ PARAMS.each do |param_id, param_obj|
349
+ param_value = params[param_obj[:name]]
350
+ if param_value # rubocop:disable Style/Next
351
+ param_buf = Buffer.new(capacity: 65536)
352
+ # aaaa
353
+ if param_obj[:type] == :int
354
+ param_buf.push_uint_var(param_value)
355
+ elsif param_obj[:type] == :bytes
356
+ param_buf.push_bytes(param_value.to_s)
357
+ elsif param_obj[:type] == :quicpreferredaddress
358
+ push_quic_preferred_address(buf: param_buf, preferred_address: param_value)
359
+ end
360
+ # rubocop:enable Style/CaseLikeIf
361
+ buf.push_uint_var(param_id)
362
+ buf.push_uint_var(param_buf.tell)
363
+ buf.push_bytes(param_buf.data)
364
+ end
365
+ end
366
+ end
367
+
368
+ class QuicFrameType
369
+ PADDING = 0x00
370
+ PING = 0x01
371
+ ACK = 0x02
372
+ ACK_ECN = 0x03
373
+ RESET_STREAM = 0x04
374
+ STOP_SENDING = 0x05
375
+ CRYPTO = 0x06
376
+ NEW_TOKEN = 0x07
377
+ STREAM_BASE = 0x08
378
+ MAX_DATA = 0x10
379
+ MAX_STREAM_DATA = 0x11
380
+ MAX_STREAMS_BIDI = 0x12
381
+ MAX_STREAMS_UNI = 0x13
382
+ DATA_BLOCKED = 0x14
383
+ STREAM_DATA_BLOCKED = 0x15
384
+ STREAMS_BLOCKED_BIDI = 0x16
385
+ STREAMS_BLOCKED_UNI = 0x17
386
+ NEW_CONNECTION_ID = 0x18
387
+ RETIRE_CONNECTION_ID = 0x19
388
+ PATH_CHALLENGE = 0x1A
389
+ PATH_RESPONSE = 0x1B
390
+ TRANSPORT_CLOSE = 0x1C
391
+ APPLICATION_CLOSE = 0x1D
392
+ HANDSHAKE_DONE = 0x1E
393
+ DATAGRAM = 0x30
394
+ DATAGRAM_WITH_LENGTH = 0x31
395
+ end
396
+
397
+ NON_ACK_ELICITING_FRAME_TYPES = [
398
+ QuicFrameType::ACK,
399
+ QuicFrameType::ACK_ECN,
400
+ QuicFrameType::PADDING,
401
+ QuicFrameType::TRANSPORT_CLOSE,
402
+ QuicFrameType::APPLICATION_CLOSE,
403
+ ].freeze
404
+ NON_IN_FLIGHT_FRAME_TYPES = [
405
+ QuicFrameType::ACK,
406
+ QuicFrameType::ACK_ECN,
407
+ QuicFrameType::TRANSPORT_CLOSE,
408
+ QuicFrameType::APPLICATION_CLOSE,
409
+ ].freeze
410
+ PROBING_FRAME_TYPES = [
411
+ QuicFrameType::PATH_CHALLENGE,
412
+ QuicFrameType::PATH_RESPONSE,
413
+ QuicFrameType::PADDING,
414
+ QuicFrameType::NEW_CONNECTION_ID,
415
+ ].freeze
416
+
417
+ QuicResetStreamFrame = _ = Struct.new( # rubocop:disable Naming/ConstantName
418
+ :error_code,
419
+ :final_size,
420
+ :stream_id,
421
+ )
422
+
423
+ QuicStopSendingFrame = _ = Struct.new( # rubocop:disable Naming/ConstantName
424
+ :error_code,
425
+ :stream_id,
426
+ )
427
+
428
+ QuicStreamFrame = _ = Struct.new( # rubocop:disable Naming/ConstantName
429
+ :data,
430
+ :fin,
431
+ :offset,
432
+ )
433
+
434
+ def self.pull_ack_frame(buf)
435
+ rangeset = Rangeset.new
436
+ ends = buf.pull_uint_var # largeset acknowledged
437
+ delay = buf.pull_uint_var
438
+ ack_range_count = buf.pull_uint_var
439
+ ack_count = buf.pull_uint_var # first ack range
440
+ rangeset.add(ends - ack_count, ends + 1)
441
+ ends -= ack_count
442
+ ack_range_count.times do
443
+ ends -= buf.pull_uint_var + 2
444
+ ack_count = buf.pull_uint_var
445
+ rangeset.add(ends - ack_count, ends + 1)
446
+ ends -= ack_count
447
+ end
448
+ [rangeset, delay]
449
+ end
450
+
451
+ def self.push_ack_frame(buf:, rangeset:, delay:)
452
+ ranges = rangeset.length
453
+ index = ranges - 1
454
+ r = rangeset.list[index]
455
+ buf.push_uint_var(r.last - 1)
456
+ buf.push_uint_var(delay)
457
+ buf.push_uint_var(index)
458
+ buf.push_uint_var(r.last - 1 - r.first)
459
+ start = r.first
460
+ while index > 0
461
+ index -= 1
462
+ r = rangeset.list[index]
463
+ buf.push_uint_var(start - r.last - 1)
464
+ buf.push_uint_var(r.last - r.first - 1)
465
+ start = r.first
466
+ end
467
+ return ranges
468
+ end
469
+ end
470
+ end
471
+ end