snell 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.
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+
5
+ module Snell
6
+ # 拥有 socket,处理非阻塞 I/O 的部分读写;期限按 Fiber 保存,
7
+ # 避免并发上传和下载覆盖彼此的时间预算。
8
+ class Transport
9
+ attr_reader :socket
10
+
11
+ def initialize(socket, timeout: nil)
12
+ @socket = socket
13
+ @timeout = timeout
14
+ @deadlines = {}
15
+ @waiters = {}
16
+ @closed = false
17
+ end
18
+
19
+ # 嵌套范围可以缩短期限,不能延长外层预算。
20
+ # 期限约束 socket 等待,不能抢占 CPU 或原生加密调用。
21
+ def with_timeout(seconds)
22
+ fiber = Fiber.current
23
+ old = @deadlines[fiber]
24
+ deadline = monotonic + seconds
25
+ @deadlines[fiber] = old ? [old, deadline].min : deadline
26
+ yield
27
+ ensure
28
+ old ? @deadlines[fiber] = old : @deadlines.delete(fiber)
29
+ end
30
+
31
+ # 全部碎片共享一个绝对期限;未读到字节时的正常 EOF
32
+ # 使用 EOFError,读到部分字节后的 EOF 使用 ProtocolError。
33
+ def read_exactly(length)
34
+ raise ArgumentError, "length must be nonnegative" unless length.is_a?(Integer) && length >= 0
35
+
36
+ deadline = operation_deadline
37
+ result = +"".b
38
+ while result.bytesize < length
39
+ chunk = read_nonblock(length - result.bytesize, deadline)
40
+ if chunk.nil?
41
+ raise EOFError, "Transport reached EOF" if result.empty?
42
+
43
+ raise ProtocolError, "Truncated transport data"
44
+ end
45
+ result << chunk
46
+ end
47
+ result
48
+ end
49
+
50
+ def read_some(length = 16_384)
51
+ raise ArgumentError, "length must be positive" unless length.is_a?(Integer) && length.positive?
52
+
53
+ read_nonblock(length, operation_deadline)
54
+ end
55
+
56
+ def write_all(bytes)
57
+ bytes = String(bytes).b
58
+ deadline = operation_deadline
59
+ offset = 0
60
+ while offset < bytes.bytesize
61
+ check_open!
62
+ result = @socket.write_nonblock(bytes.byteslice(offset..), exception: false)
63
+ case result
64
+ when :wait_writable then wait(:writable, deadline)
65
+ when :wait_readable then wait(:readable, deadline)
66
+ else
67
+ raise IOError, "Transport write made no progress" unless result&.positive?
68
+
69
+ offset += result
70
+ end
71
+ end
72
+ offset
73
+ end
74
+
75
+ def close_write
76
+ @socket.shutdown(Socket::SHUT_WR) unless closed?
77
+ rescue Errno::ENOTCONN, Errno::EPIPE, IOError
78
+ nil
79
+ end
80
+
81
+ def close
82
+ return if @closed
83
+
84
+ @closed = true
85
+ # 显式唤醒阻塞的 Fiber,避免依赖关闭 IO 自动唤醒的非通用行为;
86
+ # 遍历副本,因为恢复执行的等待者会注销自己。
87
+ @waiters.to_a.each do |fiber, scheduler|
88
+ scheduler.raise(fiber, ClosedError, "Transport is closed") if scheduler && fiber.alive?
89
+ end
90
+ @socket.close unless @socket.closed?
91
+ rescue IOError, Errno::EBADF
92
+ nil
93
+ end
94
+
95
+ def closed?
96
+ @closed || @socket.closed?
97
+ end
98
+
99
+ private
100
+
101
+ def read_nonblock(length, deadline)
102
+ loop do
103
+ check_open!
104
+ result = @socket.read_nonblock(length, exception: false)
105
+ case result
106
+ when :wait_readable then wait(:readable, deadline)
107
+ when :wait_writable then wait(:writable, deadline)
108
+ else return result
109
+ end
110
+ end
111
+ end
112
+
113
+ def wait(direction, deadline)
114
+ remaining = deadline && (deadline - monotonic)
115
+ raise TimeoutError, "Transport operation timed out" if remaining && remaining <= 0
116
+
117
+ @waiters[Fiber.current] = Fiber.scheduler
118
+ ready = @socket.public_send("wait_#{direction}", remaining)
119
+ raise TimeoutError, "Transport operation timed out" unless ready
120
+ ensure
121
+ @waiters.delete(Fiber.current)
122
+ end
123
+
124
+ def operation_deadline
125
+ explicit = @deadlines[Fiber.current]
126
+ default = @timeout && (monotonic + @timeout)
127
+ return explicit || default unless explicit && default
128
+
129
+ [explicit, default].min
130
+ end
131
+
132
+ def monotonic
133
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
134
+ end
135
+
136
+ def check_open!
137
+ raise ClosedError, "Transport is closed" if closed?
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,303 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Snell
4
+ module V6
5
+ # 整数运算由协议形态定义;Ruby 整数显式截断到协议位宽,
6
+ # 确保溢出在不同平台上具有一致含义。
7
+ class Profile
8
+ MASK = (1 << 64) - 1
9
+ G = 0x9e3779b97f4a7c15
10
+ D = 0xd6e8feb86659fd93
11
+ A = 0xe7037ed1a0b428db
12
+ SMALL_A = 0x8f3907f7b2b80c35
13
+ B = 0x589965cc75374cc3
14
+ SMALL_B = 0x33a213ec50ffe2e9
15
+ NAMESPACES = {
16
+ profile: [5, 0xb46c2e7d9a1538f1], prefix: [0, 0x5d9217c083e64ab9],
17
+ motif: [2, 0xa71f0c54d8396e2b], salt: [3, 0x3e8a91b52740f6cd],
18
+ mixing: [16, 0xc9f4260b7d1e835a], chunk: [21, 0x62d0b5e19c4a783f],
19
+ write: [28, 0x917b3c48e6a205d4]
20
+ }.freeze
21
+ BIT_ROWS = %w[
22
+ 01020408102040800102040810204080 03050911214181060a122242820c1824
23
+ 070b132343830d193161c10e1c3870e0 0f172747871b3363c31d3971e13c78f0
24
+ f8f4ecdcbc7cf2e6ce9e3ef1e3c78f1f fcfaf6eedebe7ef9f5edddbd7df3e7db
25
+ fefdfbf7efdfbf7ffefdfbf7efdfbf7f
26
+ ].map { |hex| [hex].pack("H*").freeze }.freeze
27
+
28
+ attr_reader :salt_prefix_length, :initial_chunk, :maximum_chunk, :chunk_increment, :idle_reset
29
+
30
+ def initialize(psk)
31
+ unless psk.is_a?(String) && psk.bytesize.between?(12, 255)
32
+ raise ArgumentError, "version 6 psk must contain 12 to 255 bytes"
33
+ end
34
+
35
+ secret = Crypto.blake2b256(["8d41a7135ce209bb702fd6943318c06e4a9125fdb80377ac"].pack("H*") + psk.b)
36
+ words = secret.unpack("Q<4")
37
+ @namespaces = NAMESPACES.transform_values do |label, seed|
38
+ mix((label * D) ^ ((seed + 0xa0761d6478bd642f) & MASK) ^ words[0] ^
39
+ ((words[1] + G) & MASK) ^ rotate(words[2], 17) ^ rotate(words[3], 53))
40
+ end.freeze
41
+ initialize_padding
42
+ initialize_mixing
43
+ initialize_sizes
44
+ @generator = scalar(6, 0) % 4
45
+ @g = [[24, 128], [16, 96], [16, 96], [0, 9], [1, 8], [7, 23]].each_with_index.map do |bounds, index|
46
+ select(scalar(6, index + 1), *bounds)
47
+ end.freeze
48
+ freeze
49
+ end
50
+
51
+ def inspect
52
+ "#<#{self.class}>"
53
+ end
54
+
55
+ def prf(label, input_x, input_y, namespace: namespace_for(label))
56
+ value = mix(@namespaces.fetch(namespace) ^ (((input_y & 0xffff_ffff) * B) + SMALL_B) ^
57
+ ((label & 0xffff_ffff) * G) ^ (((input_x & 0xffff_ffff) * A) + SMALL_A))
58
+ (value ^ (value >> 32)) & 0xffff_ffff
59
+ end
60
+
61
+ def bytes(label, sequence, length)
62
+ seed = @namespaces.fetch(namespace_for(label)) ^
63
+ (((sequence & 0xffff_ffff) * D) + 0xb57de1f3f82cb33f) ^
64
+ (label * 0xa24baed4963ee407) ^ ((length * 0x165667b19e3779f9) + 0x0d4cd3e7b14a36d7)
65
+ result = +"".b
66
+ ((length + 7) / 8).times { |index| result << [mix(seed + ((index + 1) * G))].pack("Q<") }
67
+ result.byteslice(0, length)
68
+ end
69
+
70
+ def padding(sequence, length)
71
+ result = bytes(0, sequence, length)
72
+ motif = bytes(2, sequence, 32) if @generator == 3
73
+ percent = select(prf(1, sequence, 0), @bit_min, @bit_max)
74
+ row = BIT_ROWS[(((percent * 8) + 50) / 100).clamp(1, 7) - 1]
75
+ result.bytesize.times do |index|
76
+ value = result.getbyte(index)
77
+ shaped = case @generator
78
+ when 0
79
+ source = row.getbyte((((value + index) & 255) ^ value) & 15)
80
+ rotation = (((value + index) & 255) ^ (value >> 4)) & 7
81
+ ((source << rotation) | (source >> (8 - rotation))) & 255
82
+ when 1
83
+ bucket = value % (@g[0] + @g[1] + @g[2])
84
+ if bucket < @g[0]
85
+ 32 + (((value + index) & 255) % 95)
86
+ elsif bucket < @g[0] + @g[1]
87
+ 128 + (((value ^ index) & 255) % 64)
88
+ else
89
+ 192 + (((value + (7 * index)) & 255) % 64)
90
+ end
91
+ when 2
92
+ ((value + (16 * (index % 4)) + 48) & 240) | (((value & 15) + @g[3] + (index % 2)) % 10)
93
+ when 3
94
+ period = [@g[5], 5].max
95
+ position = index % period
96
+ if position < period - 3
97
+ (((@g[4] + 3) * index) & 255) ^ motif.getbyte((index % (4 * @g[4])) % 32)
98
+ elsif position < period - 1
99
+ 48 | (value % 10)
100
+ else
101
+ value
102
+ end
103
+ end
104
+ result.setbyte(index, shaped)
105
+ end
106
+ result
107
+ end
108
+
109
+ def prefix_length(sequence)
110
+ select(prf(33, sequence, 0), @prefix_min, @prefix_max)
111
+ end
112
+
113
+ def salt_block(salt)
114
+ block = padding(0xffff_ffff, 16 + @salt_prefix_length)
115
+ salt.bytes.each_with_index { |value, index| block.setbyte(@salt_positions[index], value ^ salt_mask(index)) }
116
+ block
117
+ end
118
+
119
+ def extract_salt(block)
120
+ raise ProtocolError, "invalid shaped salt length" unless block.bytesize == 16 + @salt_prefix_length
121
+
122
+ (0...16).map { |index| block.getbyte(@salt_positions[index]) ^ salt_mask(index) }.pack("C*")
123
+ end
124
+
125
+ # 有意原地修改两个自有协议缓冲区;交换操作可逆,
126
+ # 读取器先恢复密文与 padding,再验证 AEAD 标签。
127
+ def mix!(padding, payload, sequence)
128
+ overlap = [padding.bytesize, payload.bytesize].min
129
+ @mix_rounds.times do |round|
130
+ if @mix_kind == 1
131
+ start = (round % 2) * @mix_block
132
+ while start + @mix_block <= overlap
133
+ (start...(start + @mix_block)).each { |index| exchange(padding, payload, index) }
134
+ start += @mix_block * 2
135
+ end
136
+ else
137
+ stride = @mix_stride + (round % 3)
138
+ first = @mix_offset
139
+ first += prf(3, sequence, round) if @mix_kind == 2
140
+ (first % stride).step(overlap - 1, stride) { |index| exchange(padding, payload, index) }
141
+ end
142
+ end
143
+ nil
144
+ end
145
+
146
+ # 流量形态的分块限制独立于 uint16 记录载荷容量。
147
+ # 首块上限为首条记录的额外封装预留空间。
148
+ def chunk_size(sequence, state)
149
+ size = case @chunk_kind
150
+ when 0 then state
151
+ when 1 then @chunk_choices[prf(38, sequence, state) % 8]
152
+ when 2 then state + (prf(39, sequence, state) % ((2 * @chunk_jitter) + 1)) - @chunk_jitter
153
+ end
154
+ size = size.clamp(64, @maximum_chunk)
155
+ sequence.zero? ? [size, @first_chunk_cap].min : size
156
+ end
157
+
158
+ def padding_length(sequence, payload_length)
159
+ padded = sequence < @initial_padded || (payload_length.positive? && payload_length <= @small_payload) ||
160
+ (sequence % @padding_interval).zero?
161
+ base = padded ? select(prf(34, sequence, payload_length), @pad_min, @pad_max) : 0
162
+ prefix = prefix_length(sequence)
163
+ salt_length = sequence.zero? ? 16 + @salt_prefix_length : 0
164
+ frame_length = salt_length + prefix + 23 + base + payload_length + (payload_length.positive? ? 16 : 0)
165
+ target = write_target(sequence, frame_length)
166
+ length = base + (target - frame_length).clamp(0, 730)
167
+ if sequence.zero?
168
+ minimum = [((payload_length + (payload_length.positive? ? 55 : 39)) + 2) / 3, 192].max
169
+ length = [length, minimum - @salt_prefix_length - prefix].max
170
+ length = [length, @pad_max + 730].min
171
+ end
172
+ length
173
+ end
174
+
175
+ private
176
+
177
+ def initialize_padding
178
+ @pad_min = field(7, 24, 160)
179
+ @pad_max = [@pad_min + field(8, 160, 960), 730].min
180
+ @initial_padded = field(9, 2, 8)
181
+ @padding_interval = field(10, 2, 11)
182
+ @small_payload = field(11, 96, 768)
183
+ @bit_min = field(12, 24, 41)
184
+ @bit_max = field(13, 58, 76)
185
+ @prefix_min, @prefix_max = prefix_bounds(0, 8, 80)
186
+ salt_min, salt_max = prefix_bounds(0x7053, 16, 96)
187
+ @salt_prefix_length = select(scalar(33, 0x7053), salt_min, salt_max)
188
+ @salt_stride = field(18, 17, 251, 0x51a7)
189
+ positions = (0...(16 + @salt_prefix_length)).to_a
190
+ field(17, 1, 4, 0x51a7).times do |round|
191
+ positions.each_index do |index|
192
+ value = mix(((index * B) + SMALL_B) ^ @namespaces.fetch(:salt) ^
193
+ 0xdaa66d2c7ddf743f ^ (((0x51a7 + round) * A) + SMALL_A))
194
+ other = index + (((value ^ (value >> 32)) & 0xffff_ffff) % (positions.length - index))
195
+ positions[index], positions[other] = positions[other], positions[index]
196
+ end
197
+ end
198
+ @salt_positions = positions.freeze
199
+ end
200
+
201
+ def initialize_mixing
202
+ @mix_kind = scalar(16, 0) % 3
203
+ @mix_rounds = field(17, 1, 3)
204
+ @mix_stride = field(18, 2, 13)
205
+ @mix_offset = field(19, 0, 15)
206
+ @mix_block = field(20, 8, 64)
207
+ end
208
+
209
+ def initialize_sizes
210
+ @initial_chunk = field(22, 512, 1460)
211
+ @first_chunk_cap = [field(22, 256, 768, 0xf17c), @initial_chunk, 768].min.clamp(256, 768)
212
+ @maximum_chunk = [field(23, 8192, 16_383), @initial_chunk].max
213
+ @chunk_increment = [field(24, 1024, 4096), 2920].min
214
+ @chunk_jitter = [field(25, 16, 192), 182].min
215
+ @idle_reset = field(27, 12, 90)
216
+ @chunk_kind = scalar(21, 0) % 3
217
+ @write_kind = scalar(28, 0) % 3
218
+ @initial_targets = field(29, 4, 8)
219
+ @write_jitter = field(32, 8, 96)
220
+ @spread_percent = field(28, 8, 48, 0x504c)
221
+ @chunk_choices = Array.new(8) { |i| field(26, 4096, @maximum_chunk, i) }.freeze
222
+ @write_choices = Array.new(8) { |i| field(30, 320, 1460, i) }.freeze
223
+ @initial_choices = Array.new(8) { |i| field(31, 360, 1460, i) }.freeze
224
+ end
225
+
226
+ def write_target(sequence, length)
227
+ return [length, 65_535].min if length > 1459
228
+
229
+ target = if sequence < @initial_targets
230
+ @initial_choices[sequence]
231
+ else
232
+ @write_choices[prf(35, sequence,
233
+ length) % 8]
234
+ end
235
+ if @write_kind == 2
236
+ target = [target + (prf(36, sequence, 0) % ((2 * @write_jitter) + 1)) - @write_jitter,
237
+ 1].max
238
+ end
239
+ spread = [730, length * @spread_percent / 100].min
240
+ if prf(35, sequence, spread).even?
241
+ target = [target + spread, 65_535].min
242
+ elsif target > spread / 2
243
+ target -= spread / 2
244
+ end
245
+ while target < length
246
+ candidate = @write_choices[prf(37, sequence, target) % 8]
247
+ target = candidate > target ? candidate : [target + @pad_max, 65_535].min
248
+ end
249
+ target
250
+ end
251
+
252
+ def prefix_bounds(domain, low, high)
253
+ minimum = field(14, low, high, domain)
254
+ maximum = [minimum + field(15, 16, 160, domain), 128].min
255
+ [[minimum, maximum].min, maximum]
256
+ end
257
+
258
+ def salt_mask(index)
259
+ ((index * @salt_stride) & 255) ^ (prf(2, 0x51a7, index, namespace: :salt) & 255)
260
+ end
261
+
262
+ def field(label, low, high, domain = 0)
263
+ select(scalar(label, domain), low, high)
264
+ end
265
+
266
+ def scalar(label, domain)
267
+ prf(label, 0, domain)
268
+ end
269
+
270
+ def select(value, low, high)
271
+ low + (value % (high - low + 1))
272
+ end
273
+
274
+ def namespace_for(label)
275
+ case label
276
+ when 0, 1, 14, 15, 33, 34 then :prefix
277
+ when 2 then :motif
278
+ when 3, 16, 17, 18, 19, 20 then :mixing
279
+ when 21..26, 38, 39 then :chunk
280
+ when 28..32, 35, 36, 37 then :write
281
+ else :profile
282
+ end
283
+ end
284
+
285
+ def mix(value)
286
+ value &= MASK
287
+ value = ((value ^ (value >> 30)) * 0xbf58476d1ce4e5b9) & MASK
288
+ value = ((value ^ (value >> 27)) * 0x94d049bb133111eb) & MASK
289
+ value ^ (value >> 31)
290
+ end
291
+
292
+ def rotate(value, count)
293
+ ((value << count) | (value >> (64 - count))) & MASK
294
+ end
295
+
296
+ def exchange(first, second, index)
297
+ value = first.getbyte(index)
298
+ first.setbyte(index, second.getbyte(index))
299
+ second.setbyte(index, value)
300
+ end
301
+ end
302
+ end
303
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Snell
4
+ VERSION = "0.1.0"
5
+ end
data/lib/snell.rb ADDED
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async"
4
+ require "async/semaphore"
5
+ require_relative "snell/version"
6
+ require_relative "snell/errors"
7
+ require_relative "snell/limits"
8
+ require_relative "snell/replay_cache"
9
+ require_relative "snell/transport"
10
+ require_relative "snell/address"
11
+ require_relative "snell/datagram"
12
+ require_relative "snell/protocol"
13
+ require_relative "snell/crypto"
14
+ require_relative "snell/record_reader"
15
+ require_relative "snell/record_writer"
16
+ require_relative "snell/obfuscation"
17
+ require_relative "snell/direct_connector"
18
+ require_relative "snell/session"
19
+ require_relative "snell/session_pool"
20
+ require_relative "snell/stream"
21
+ require_relative "snell/datagram_socket"
22
+ require_relative "snell/client"
23
+ require_relative "snell/server"
24
+ require_relative "snell/configuration"
25
+ require_relative "snell/socks_server"
26
+ require_relative "snell/cli"
27
+
28
+ # 公开加载入口;require 只定义 API,不建立网络连接。
29
+ module Snell
30
+ # 复用当前 Async 任务;没有任务时创建 reactor 并等待执行完成。
31
+ def self.run(&block)
32
+ raise ArgumentError, "A block is required" unless block
33
+
34
+ if Async::Task.current?
35
+ block.call(Async::Task.current)
36
+ else
37
+ Async(&block).wait
38
+ end
39
+ end
40
+
41
+ def self.reactor!
42
+ raise Error, "Use Snell.run or an Async task" unless Async::Task.current?
43
+
44
+ Fiber.scheduler
45
+ end
46
+
47
+ def self.address(value)
48
+ case value
49
+ when Address then value
50
+ when Array then Address.new(host: value.fetch(0), port: value.fetch(1))
51
+ when Hash then Address.new(**value.transform_keys(&:to_sym))
52
+ else raise ArgumentError, "Expected Address, [host, port], or {host:, port:}"
53
+ end
54
+ end
55
+
56
+ def self.mode(value)
57
+ raise ArgumentError, "Invalid mode" unless value.is_a?(Symbol) || value.is_a?(String)
58
+
59
+ name = value.to_sym
60
+ name == :"unsafe-raw" ? :raw : name
61
+ end
62
+
63
+ def self.obfs(value)
64
+ raise ArgumentError, "Invalid obfuscation" unless value.is_a?(Symbol) || value.is_a?(String)
65
+
66
+ value.to_sym
67
+ end
68
+ end
metadata ADDED
@@ -0,0 +1,155 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: snell
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Snell Ruby contributors
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: async
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '2.46'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '3'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '2.46'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '3'
32
+ - !ruby/object:Gem::Dependency
33
+ name: io-endpoint
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '0.19'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '0.20'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0.19'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '0.20'
52
+ - !ruby/object:Gem::Dependency
53
+ name: openssl
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '3.3'
59
+ - - "<"
60
+ - !ruby/object:Gem::Version
61
+ version: '5'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '3.3'
69
+ - - "<"
70
+ - !ruby/object:Gem::Version
71
+ version: '5'
72
+ - !ruby/object:Gem::Dependency
73
+ name: rbnacl
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: 7.1.2
79
+ - - "<"
80
+ - !ruby/object:Gem::Version
81
+ version: '8'
82
+ type: :runtime
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 7.1.2
89
+ - - "<"
90
+ - !ruby/object:Gem::Version
91
+ version: '8'
92
+ description: A Ruby implementation of Snell TCP streams and UDP associations with
93
+ bounded asynchronous I/O.
94
+ executables:
95
+ - snell
96
+ extensions: []
97
+ extra_rdoc_files: []
98
+ files:
99
+ - CHANGELOG.md
100
+ - LICENSE
101
+ - README.md
102
+ - docs/ARCHITECTURE.md
103
+ - docs/COMPATIBILITY.md
104
+ - docs/PROVENANCE.md
105
+ - docs/SECURITY.md
106
+ - exe/snell
107
+ - lib/snell.rb
108
+ - lib/snell/address.rb
109
+ - lib/snell/cli.rb
110
+ - lib/snell/client.rb
111
+ - lib/snell/configuration.rb
112
+ - lib/snell/crypto.rb
113
+ - lib/snell/datagram.rb
114
+ - lib/snell/datagram_socket.rb
115
+ - lib/snell/direct_connector.rb
116
+ - lib/snell/errors.rb
117
+ - lib/snell/limits.rb
118
+ - lib/snell/obfuscation.rb
119
+ - lib/snell/protocol.rb
120
+ - lib/snell/record_reader.rb
121
+ - lib/snell/record_writer.rb
122
+ - lib/snell/replay_cache.rb
123
+ - lib/snell/server.rb
124
+ - lib/snell/session.rb
125
+ - lib/snell/session_pool.rb
126
+ - lib/snell/socks_server.rb
127
+ - lib/snell/stream.rb
128
+ - lib/snell/transport.rb
129
+ - lib/snell/v6/profile.rb
130
+ - lib/snell/version.rb
131
+ homepage: https://github.com/gatework/snell
132
+ licenses:
133
+ - MIT
134
+ metadata:
135
+ rubygems_mfa_required: 'true'
136
+ source_code_uri: https://github.com/gatework/snell
137
+ changelog_uri: https://github.com/gatework/snell/blob/main/CHANGELOG.md
138
+ rdoc_options: []
139
+ require_paths:
140
+ - lib
141
+ required_ruby_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '3.4'
146
+ required_rubygems_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ requirements: []
152
+ rubygems_version: 3.6.9
153
+ specification_version: 4
154
+ summary: Async Ruby client and server for the Snell proxy protocol
155
+ test_files: []