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