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,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "async/semaphore"
5
+ require_relative "v6/profile"
6
+
7
+ module Snell
8
+ # 一个出站方向,以完整应用写入为单位串行化。
9
+ # 连接池中的逻辑流共享连续的盐值、nonce 与形态序列;
10
+ # 部分写入失败后停用写入器,不重放应用数据。
11
+ class RecordWriter
12
+ attr_reader :max_payload
13
+
14
+ def initialize(transport, psk:, version:, role:, mode: :default)
15
+ raise ArgumentError, "psk must be a nonempty String" unless psk.is_a?(String) && !psk.empty?
16
+ raise ArgumentError, "unsupported Snell version" unless [4, 5, 6].include?(version)
17
+ raise ArgumentError, "unsupported record mode" unless %i[default unshaped raw].include?(mode)
18
+ raise ArgumentError, "record mode requires version 6" if version != 6 && mode != :default
19
+ if version == 6 && !psk.bytesize.between?(12, 255)
20
+ raise ArgumentError, "version 6 psk must contain 12 to 255 bytes"
21
+ end
22
+
23
+ Protocol.validate_role!(role)
24
+ @transport = transport
25
+ @psk = psk.b.freeze
26
+ @version = version
27
+ @mode = mode
28
+ @role = role
29
+ @max_payload = version == 6 ? 65_535 : 16_383
30
+ @cipher = nil
31
+ @written_records = 0
32
+ @failed = false
33
+ @semaphore = Async::Semaphore.new(1)
34
+ @profile = V6::Profile.new(psk) if version == 6 && mode == :default
35
+ @chunk_state = @profile ? @profile.initial_chunk : 1421
36
+ @written_payload = 0
37
+ @last_write_at = nil
38
+ @initial_padding = SecureRandom.random_number(256) + 256 if version != 6
39
+ @first_payload = true
40
+ end
41
+
42
+ # 报文或头部必须完整落在一条记录中;超限报参数错误,不拆分。
43
+ # 空应用写入不产生记录,只有 write_eof 显式发送逻辑 EOF。
44
+ def write_record(bytes)
45
+ validate_payload!(bytes)
46
+ raise ArgumentError, "record payload exceeds #{@max_payload} bytes" if bytes.bytesize > @max_payload
47
+ return 0 if bytes.empty?
48
+
49
+ @semaphore.acquire do
50
+ reset_idle_state
51
+ emit_record(bytes.b)
52
+ advance_write
53
+ end
54
+ bytes.bytesize
55
+ end
56
+
57
+ # 按当前协议形态拆分流写入,并等待下游写完以维持背压。
58
+ def write_stream(bytes)
59
+ validate_payload!(bytes)
60
+ return 0 if bytes.empty?
61
+
62
+ @semaphore.acquire do
63
+ reset_idle_state
64
+ offset = 0
65
+ while offset < bytes.bytesize
66
+ length = [bytes.bytesize - offset, stream_limit].min
67
+ emit_record(bytes.byteslice(offset, length).b)
68
+ offset += length
69
+ yield_to_reactor if (@written_records % 16).zero?
70
+ end
71
+ advance_write
72
+ end
73
+ bytes.bytesize
74
+ end
75
+
76
+ # 请求头必须完整放入首条记录;应用数据可跟在请求头之后,
77
+ # 剩余部分继续以普通流记录发送。
78
+ def write_request(header, early_data: "")
79
+ validate_payload!(header)
80
+ validate_payload!(early_data)
81
+ raise ArgumentError, "request header must not be empty" if header.empty?
82
+
83
+ @semaphore.acquire do
84
+ reset_idle_state
85
+ limit = stream_limit
86
+ raise ArgumentError, "request header exceeds first-record capacity" if header.bytesize > limit
87
+
88
+ prefix_length = [early_data.bytesize, limit - header.bytesize].min
89
+ emit_record(header.b + early_data.byteslice(0, prefix_length).b)
90
+ offset = prefix_length
91
+ while offset < early_data.bytesize
92
+ length = [early_data.bytesize - offset, stream_limit].min
93
+ emit_record(early_data.byteslice(offset, length).b)
94
+ offset += length
95
+ yield_to_reactor if (@written_records % 16).zero?
96
+ end
97
+ advance_write
98
+ end
99
+ header.bytesize + early_data.bytesize
100
+ end
101
+
102
+ # 零载荷记录关闭逻辑方向,物理 socket 仍可用于反向读取或复用。
103
+ def write_eof
104
+ @semaphore.acquire do
105
+ reset_idle_state
106
+ emit_record("".b)
107
+ advance_write
108
+ end
109
+ nil
110
+ end
111
+
112
+ def inspect
113
+ "#<#{self.class} version=#{@version} mode=#{@mode} role=#{@role}>"
114
+ end
115
+
116
+ private
117
+
118
+ def validate_payload!(bytes)
119
+ raise ArgumentError, "payload must be a String" unless bytes.is_a?(String)
120
+ raise ProtocolError, "record writer is no longer usable" if @failed
121
+ end
122
+
123
+ def stream_limit
124
+ return @profile.chunk_size(@written_records, @chunk_state) if @profile
125
+ return @max_payload if @version == 6
126
+ if @first_payload
127
+ return 1460 - (@cipher ? 0 : 16) - 23 - 16 - @initial_padding
128
+ end
129
+
130
+ if @role == :server
131
+ [1421 + ((@written_payload / (192 * 1024)) * 1421), @max_payload].min
132
+ else
133
+ [@chunk_state, @max_payload].min
134
+ end
135
+ end
136
+
137
+ def reset_idle_state
138
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
139
+ interval = @profile ? @profile.idle_reset : 31
140
+ return unless @last_write_at && now - @last_write_at > interval
141
+
142
+ @chunk_state = @profile ? @profile.initial_chunk : 1421
143
+ @written_payload = 0
144
+ end
145
+
146
+ def advance_write
147
+ @last_write_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
148
+ @chunk_state = [@chunk_state + 1421, @max_payload].min unless @profile
149
+ end
150
+
151
+ def emit_record(payload)
152
+ completed = false
153
+ raise ProtocolError, "record writer is no longer usable" if @failed
154
+
155
+ result = @profile ? emit_shaped_record(payload) : emit_legacy_record(payload)
156
+ completed = true
157
+ result
158
+ ensure
159
+ @failed = true unless completed
160
+ end
161
+
162
+ def emit_legacy_record(payload)
163
+ padding_length = @version != 6 && @first_payload && !payload.empty? ? @initial_padding : 0
164
+ padding = SecureRandom.random_bytes(padding_length)
165
+ header = [4, 0, padding.bytesize, payload.bytesize].pack("Cnnn")
166
+ if @mode == :raw
167
+ wire = header + payload
168
+ else
169
+ wire = +"".b
170
+ unless @cipher
171
+ salt = SecureRandom.random_bytes(Crypto::SALT_BYTES)
172
+ @cipher = Crypto::Cipher.new(Crypto.derive_key(@psk, salt))
173
+ wire << salt
174
+ end
175
+ wire << @cipher.encrypt(header)
176
+ encrypted_payload = payload.empty? ? +"".b : @cipher.encrypt(payload)
177
+ [padding.bytesize, encrypted_payload.bytesize].min.times do |index|
178
+ next if index.odd?
179
+
180
+ previous = padding.getbyte(index)
181
+ padding.setbyte(index, encrypted_payload.getbyte(index))
182
+ encrypted_payload.setbyte(index, previous)
183
+ end
184
+ wire << padding << encrypted_payload
185
+ end
186
+ @transport.write_all(wire)
187
+ @written_records += 1
188
+ @written_payload += payload.bytesize
189
+ @first_payload = false unless payload.empty?
190
+ payload.bytesize
191
+ end
192
+
193
+ def emit_shaped_record(payload)
194
+ sequence = @written_records & 0xffff_ffff
195
+ wire = +"".b
196
+ unless @cipher
197
+ salt = SecureRandom.random_bytes(Crypto::SALT_BYTES)
198
+ @cipher = Crypto::Cipher.new(Crypto.derive_key(@psk, salt))
199
+ wire << @profile.salt_block(salt)
200
+ end
201
+ prefix = @profile.padding(sequence, @profile.prefix_length(sequence))
202
+ padding = @profile.padding(sequence, @profile.padding_length(sequence, payload.bytesize))
203
+ header = [4, 0, padding.bytesize, payload.bytesize].pack("Cnnn")
204
+ wire << prefix << @cipher.encrypt(header, aad: prefix)
205
+ encrypted_payload = payload.empty? ? +"".b : @cipher.encrypt(payload, aad: padding)
206
+ @profile.mix!(padding, encrypted_payload, sequence) unless payload.empty?
207
+ wire << padding << encrypted_payload
208
+ @transport.write_all(wire)
209
+ @written_records += 1
210
+ @chunk_state = [@chunk_state + @profile.chunk_increment, @profile.maximum_chunk].min
211
+ payload.bytesize
212
+ end
213
+
214
+ def yield_to_reactor
215
+ Async::Task.current?&.yield
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Snell
4
+ # 有界内存缓存,记录首条记录认证成功后的盐值。共享此对象可
5
+ # 共享重放证据,但不提供持久化或跨进程防护。
6
+ class ReplayCache
7
+ def initialize(max_entries: 65_536, ttl: 300, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
8
+ raise ArgumentError, "max_entries must be positive" unless max_entries.is_a?(Integer) && max_entries.positive?
9
+ raise ArgumentError, "ttl must be positive" unless ttl.is_a?(Numeric) && ttl.positive? && ttl.finite?
10
+
11
+ @max_entries = max_entries
12
+ @ttl = ttl
13
+ @clock = clock
14
+ @entries = {}
15
+ @mutex = Mutex.new
16
+ end
17
+
18
+ # 显式 raw 模式没有认证盐值;nil 不构成身份认证证据。
19
+ # 使用单调时钟按插入顺序清除过期项;容量满时拒绝新盐值,
20
+ # 避免为了接入新连接而删除尚未过期的重放证据。
21
+ def accept!(salt)
22
+ return true if salt.nil?
23
+
24
+ key = salt.b.dup.freeze
25
+ @mutex.synchronize do
26
+ now = @clock.call
27
+ @entries.shift while !@entries.empty? && @entries.first.last <= now
28
+ raise AuthenticationError, "Replayed authentication salt" if @entries.key?(key)
29
+ raise CapacityError, "Replay cache is full" if @entries.size >= @max_entries
30
+
31
+ @entries[key] = now + @ttl
32
+ end
33
+ nil
34
+ end
35
+
36
+ def size
37
+ @mutex.synchronize { @entries.size }
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,352 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Snell
4
+ # reactor 内的服务入口,负责准入、请求认证与有界转发。
5
+ # 注入连接器可控制目标访问;一个会话承载顺序 TCP 请求,
6
+ # 或专用 ping/UDP 交互,不在同一连接上多路复用活跃流。
7
+ class Server
8
+ attr_reader :local_address
9
+
10
+ def initialize(psk:, version: 5, mode: :default, users: nil, obfs: :none,
11
+ obfs_host: nil, obfs_uri: "/", limits: {}, connector: DirectConnector.new,
12
+ replay_cache: ReplayCache.new)
13
+ @owner = Snell.reactor!
14
+ raise ArgumentError, "psk must be a nonempty String" unless psk.is_a?(String) && !psk.empty?
15
+
16
+ @psk = String(psk).dup.freeze
17
+ @version = version
18
+ @mode = Snell.mode(mode)
19
+ @obfs = Snell.obfs(obfs)
20
+ @obfs_host = obfs_host
21
+ @obfs_uri = obfs_uri
22
+ @limits = limits.is_a?(Limits) ? limits : Limits.new(limits)
23
+ @connector = connector
24
+ @replay_cache = replay_cache
25
+ @counters = { accepted: 0, rejected: 0, completed: 0, failed: 0, reused: 0 }
26
+ @connections = {}
27
+ @handshakes = 0
28
+ @udp_count = 0
29
+ @tokens = @limits[:handshake_burst].to_f
30
+ @token_time = monotonic
31
+ @stopping = false
32
+ update_users(users)
33
+ validate!
34
+ end
35
+
36
+ def run(listen:)
37
+ check_owner!
38
+ raise Error, "Server is already running" if @listener
39
+ raise ClosedError, "Server has stopped" if @stopping
40
+
41
+ @listener = DirectConnector.new.bind(Snell.address(listen))
42
+ @local_address = @listener.local_address
43
+ until @stopping
44
+ accepted = @listener.accept_nonblock(exception: false)
45
+ if accepted == :wait_readable
46
+ # 部分调度器不会因另一 Fiber 关闭 IO 而唤醒等待者。
47
+ @listener.wait_readable(0.1)
48
+ next
49
+ end
50
+ socket, peer = accepted
51
+ if admit?
52
+ # 先登记连接再创建任务,Async 子任务会立即启动并可能让出执行权。
53
+ @connections[socket] = nil
54
+ Async::Task.current.async { handle_admitted(socket, peer) }
55
+ else
56
+ socket.close
57
+ end
58
+ end
59
+ rescue IOError, Errno::EBADF
60
+ raise unless @stopping
61
+ ensure
62
+ @listener&.close unless @listener&.closed?
63
+ @listener = nil
64
+ end
65
+
66
+ # 嵌入式接入入口;包括拒绝接入时,也负责关闭传入的 socket。
67
+ def handle(socket, peer: nil)
68
+ check_owner!
69
+ unless admit?
70
+ socket.close
71
+ raise CapacityError, "Server admission limit reached"
72
+ end
73
+ @connections[socket] = nil
74
+ handle_admitted(socket, peer)
75
+ end
76
+
77
+ # 停止准入,等待正在执行的任务度过宽限期,再关闭连接并取消任务。
78
+ def stop(grace: @limits[:shutdown_grace])
79
+ check_owner!
80
+ raise ArgumentError, "grace must be nonnegative" unless grace.is_a?(Numeric) && grace >= 0 && grace.finite?
81
+
82
+ @stopping = true
83
+ @listener&.close unless @listener&.closed?
84
+ deadline = monotonic + grace
85
+ task = Async::Task.current
86
+ sleep([deadline - monotonic, 0.01].min) while !@connections.empty? && monotonic < deadline
87
+ @connections.keys.each { |socket| socket.close unless socket.closed? } # rubocop:disable Style/HashEachMethods -- closing can remove entries
88
+ @connections.values.compact.each { |active| active.stop unless active.equal?(task) }
89
+ nil
90
+ end
91
+
92
+ # 完整校验替换数据后,才发布冻结的用户快照。
93
+ # nil 表示仅使用 PSK;已建立的转发保留原接入决定,
94
+ # 复用连接上的后续请求读取当前快照重新认证。
95
+ def update_users(users)
96
+ check_owner!
97
+ snapshot = if users.nil?
98
+ nil
99
+ else
100
+ raise ArgumentError, "users must be a nonempty Array" unless users.is_a?(Array) && !users.empty?
101
+
102
+ users.each_with_object({}) do |entry, result|
103
+ raise ArgumentError, "user must be an object" unless entry.is_a?(Hash)
104
+ unless entry.keys.all? { |key| key.is_a?(String) || key.is_a?(Symbol) }
105
+ raise ArgumentError, "Invalid user field"
106
+ end
107
+
108
+ fields = entry.transform_keys(&:to_sym)
109
+ raise ArgumentError, "Duplicate user field" unless fields.size == entry.size
110
+ raise ArgumentError, "Unknown user fields" unless (fields.keys - %i[name user_key]).empty?
111
+
112
+ key = fields[:user_key]
113
+ name = fields.fetch(:name, "user")
114
+ unless key.is_a?(String) && key.bytesize.between?(1, 255) && name.is_a?(String) && !name.empty?
115
+ raise ArgumentError, "user requires a name and a 1..255 byte user_key"
116
+ end
117
+ raise ArgumentError, "Duplicate user key" if result.key?(key.b)
118
+
119
+ result[key.b.dup.freeze] = name.dup.freeze
120
+ end.freeze
121
+ end
122
+ @users = snapshot
123
+ nil
124
+ end
125
+
126
+ # 返回连接处理结果与当前资源计数,不代表目标已确认接收数据。
127
+ def stats
128
+ check_owner!
129
+ @counters.merge(active_connections: @connections.size, handshakes: @handshakes,
130
+ udp_associations: @udp_count).freeze
131
+ end
132
+
133
+ def inspect
134
+ "#<#{self.class} version=#{@version} mode=#{@mode} active_connections=#{@connections.size}>"
135
+ end
136
+
137
+ private
138
+
139
+ def handle_admitted(socket, _peer)
140
+ @connections[socket] = Async::Task.current
141
+ handshake_pending = true
142
+ transport = Transport.new(socket, timeout: @limits[:idle_timeout])
143
+ transport = Obfuscation.wrap(transport, mode: @obfs, role: :server, host: @obfs_host, uri: @obfs_uri)
144
+ session = Session.new(transport, psk: @psk, version: @version, mode: @mode, role: :server, limits: @limits)
145
+ request = transport.with_timeout(@limits[:handshake_timeout]) do
146
+ first = session.read_record
147
+ @replay_cache.accept!(session.reader.authenticated_salt)
148
+ Protocol.decode_request(first)
149
+ end
150
+ @handshakes -= 1
151
+ handshake_pending = false
152
+ loop do
153
+ session.activate!
154
+ if request.command.zero?
155
+ session.write_record(Protocol.encode_reply(kind: 1))
156
+ break
157
+ end
158
+ authenticate!(request)
159
+ case request.command
160
+ when 1, 5
161
+ break unless relay_tcp(session, request)
162
+ when 6
163
+ relay_udp(session, request)
164
+ break
165
+ end
166
+ break if @stopping
167
+
168
+ session.ready!
169
+ request = Protocol.decode_request(session.read_record)
170
+ @counters[:reused] += 1
171
+ end
172
+ @counters[:completed] += 1
173
+ rescue EOFError
174
+ @counters[:completed] += 1
175
+ rescue Error, IOError, SystemCallError, SocketError, Async::TimeoutError
176
+ # 对端失败只影响当前已接入的物理连接。
177
+ @counters[:failed] += 1
178
+ nil
179
+ ensure
180
+ @handshakes -= 1 if handshake_pending
181
+ session&.close
182
+ socket.close unless socket.closed?
183
+ @connections.delete(socket)
184
+ end
185
+
186
+ def relay_tcp(session, request)
187
+ begin
188
+ socket = @connector.connect(request.destination, timeout: @limits[:dial_timeout])
189
+ rescue SystemCallError, SocketError, TimeoutError
190
+ send_error(session, 1, "Target connection failed")
191
+ return false
192
+ end
193
+ target = Transport.new(socket, timeout: @limits[:idle_timeout])
194
+ target.write_all(request.remaining) unless request.remaining.empty?
195
+ upload_error = nil
196
+ upload_eof = false
197
+ # 上传逻辑 EOF 只半关闭目标写方向,下载仍继续进行;
198
+ # 两个方向都正常结束后,命令 5 才能复用此会话。
199
+ upload = Async::Task.current.async do
200
+ loop do
201
+ bytes = session.read_record
202
+ if bytes.empty?
203
+ upload_eof = true
204
+ break
205
+ end
206
+ target.write_all(bytes)
207
+ end
208
+ rescue EOFError
209
+ upload_eof = false
210
+ rescue Error, IOError, SystemCallError => e
211
+ upload_error = e
212
+ target.close
213
+ ensure
214
+ target.close_write
215
+ end
216
+ replied = false
217
+ while (bytes = target.read_some([@limits[:buffer_bytes], session.writer.max_payload - 1].min))
218
+ if replied
219
+ session.write_stream(bytes)
220
+ else
221
+ session.write_stream(Protocol.encode_reply(kind: 0, payload: bytes))
222
+ replied = true
223
+ end
224
+ end
225
+ return false if upload_error
226
+
227
+ unless replied
228
+ send_error(session, request.command == 5 ? 0x65 : 0xff, request.command == 5 ? "Remote EOF" : "end of file")
229
+ return false
230
+ end
231
+ return false if request.command == 1
232
+
233
+ session.write_eof
234
+ upload.wait
235
+ upload_eof && upload_error.nil?
236
+ ensure
237
+ upload&.stop
238
+ target&.close
239
+ end
240
+
241
+ # 每个解析出的地址族使用一个 socket,接收子任务由该关联持有;
242
+ # RecordWriter 将它们对共享下行方向的写入串行化。
243
+ def relay_udp(session, request)
244
+ raise ProtocolError, "UDP request contains unexpected data" unless request.remaining.empty?
245
+ raise CapacityError, "UDP association limit reached" if @udp_count >= @limits[:udp_associations]
246
+
247
+ @udp_count += 1
248
+ counted = true
249
+ sockets = {}
250
+ receivers = []
251
+ session.write_record(Protocol.encode_reply(kind: 0))
252
+ loop do
253
+ record = session.read_record
254
+ break if record.empty?
255
+
256
+ datagram = Protocol.decode_packet(record, role: :server)
257
+ address = Async::Task.current.with_timeout(@limits[:dial_timeout]) { @connector.udp(datagram.address) }
258
+ socket = sockets[address.afamily] ||= begin
259
+ udp = Socket.new(address.afamily, Socket::SOCK_DGRAM)
260
+ begin
261
+ udp.bind(Addrinfo.udp(address.ipv6? ? "::" : "0.0.0.0", 0))
262
+ # 先移交给关联的清理集合,再启动可能让出执行权的接收任务。
263
+ sockets[address.afamily] = udp
264
+ receivers << Async::Task.current.async { receive_udp(udp, session) }
265
+ udp
266
+ ensure
267
+ # 绑定失败或登记前被取消时,仍由创建者释放 socket。
268
+ udp.close unless sockets[address.afamily].equal?(udp) || udp.closed?
269
+ end
270
+ end
271
+ loop do
272
+ sent = socket.sendmsg_nonblock(datagram.payload, 0, address, exception: false)
273
+ break unless sent == :wait_writable
274
+
275
+ raise TimeoutError, "UDP send timed out" unless socket.wait_writable(@limits[:idle_timeout])
276
+ end
277
+ end
278
+ ensure
279
+ sockets&.each_value { |socket| socket.close unless socket.closed? }
280
+ receivers&.each(&:stop)
281
+ @udp_count -= 1 if counted
282
+ end
283
+
284
+ def receive_udp(socket, session)
285
+ loop do
286
+ result = socket.recvfrom_nonblock(65_535, exception: false)
287
+ if result == :wait_readable
288
+ raise TimeoutError, "UDP receive timed out" unless socket.wait_readable(@limits[:idle_timeout])
289
+
290
+ next
291
+ end
292
+ payload, source = result
293
+ address = Address.new(host: source.ip_address, port: source.ip_port)
294
+ packet = Protocol.encode_packet(payload, address, role: :server)
295
+ next if packet.bytesize > session.writer.max_payload
296
+
297
+ session.write_record(packet)
298
+ end
299
+ rescue Error, IOError, SystemCallError
300
+ session.close
301
+ end
302
+
303
+ def authenticate!(request)
304
+ snapshot = @users
305
+ raise AuthenticationError, "Unknown user" if snapshot && !snapshot.key?(request.user_key)
306
+ end
307
+
308
+ def send_error(session, code, message)
309
+ session.write_record(Protocol.encode_reply(kind: 2, code: code, message: message))
310
+ end
311
+
312
+ def admit?
313
+ if @stopping || @connections.size >= @limits[:max_connections] || @handshakes >= @limits[:handshake_concurrency]
314
+ @counters[:rejected] += 1
315
+ return false
316
+ end
317
+
318
+ now = monotonic
319
+ @tokens = [@limits[:handshake_burst], @tokens + ((now - @token_time) * @limits[:handshake_rate])].min
320
+ @token_time = now
321
+ if @tokens < 1
322
+ @counters[:rejected] += 1
323
+ return false
324
+ end
325
+
326
+ @tokens -= 1
327
+ @handshakes += 1
328
+ @counters[:accepted] += 1
329
+ true
330
+ end
331
+
332
+ def check_owner!
333
+ raise Error, "Server belongs to another reactor" unless Fiber.scheduler.equal?(@owner)
334
+ end
335
+
336
+ def validate!
337
+ raise ArgumentError, "Server version must be 5 or 6" unless [5, 6].include?(@version)
338
+ raise ArgumentError, "Invalid mode" unless %i[default unshaped raw].include?(@mode)
339
+ raise ArgumentError, "Mode requires version 6" unless @version == 6 || @mode == :default
340
+ raise ArgumentError, "Invalid obfuscation" unless %i[none http tls].include?(@obfs)
341
+ raise ArgumentError, "Obfuscation is unavailable with version 6" if @version == 6 && @obfs != :none
342
+ if @version == 6 && !@psk.bytesize.between?(12, 255)
343
+ raise ArgumentError, "version 6 psk must contain 12 to 255 bytes"
344
+ end
345
+ raise ArgumentError, "psk must not be empty" if @psk.empty?
346
+ end
347
+
348
+ def monotonic
349
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
350
+ end
351
+ end
352
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Snell
4
+ # 拥有物理传输及两个独立的记录方向;连接池状态每次只承载
5
+ # 一个逻辑流,ready! 不重置加密状态。
6
+ class Session
7
+ attr_reader :reader, :writer, :state, :transport, :last_used
8
+
9
+ def initialize(transport, psk:, version:, mode:, role:, limits:)
10
+ @transport = transport
11
+ @reader = RecordReader.new(transport, psk: psk, version: version, mode: mode, role: role)
12
+ @writer = RecordWriter.new(transport, psk: psk, version: version, mode: mode, role: role)
13
+ @limits = limits
14
+ @state = :ready
15
+ @last_used = monotonic
16
+ end
17
+
18
+ def activate!
19
+ raise ClosedError, "Session is not available" unless @state == :ready
20
+
21
+ @state = :active
22
+ end
23
+
24
+ def draining!
25
+ @state = :draining unless closed?
26
+ end
27
+
28
+ def ready!
29
+ raise ClosedError, "Session is closed" if closed?
30
+
31
+ @state = :ready
32
+ @last_used = monotonic
33
+ end
34
+
35
+ def read_record(timeout: @limits[:idle_timeout])
36
+ perform(timeout) { @reader.read_record }
37
+ end
38
+
39
+ def write_record(bytes)
40
+ perform { @writer.write_record(bytes) }
41
+ end
42
+
43
+ def write_stream(bytes)
44
+ perform { @writer.write_stream(bytes) }
45
+ end
46
+
47
+ def write_request(header, early_data: "")
48
+ perform { @writer.write_request(header, early_data: early_data) }
49
+ end
50
+
51
+ def write_eof
52
+ perform { @writer.write_eof }
53
+ end
54
+
55
+ def close
56
+ @state = :closed
57
+ @transport.close
58
+ end
59
+
60
+ def closed?
61
+ @state == :closed || @transport.closed?
62
+ end
63
+
64
+ def inspect
65
+ "#<#{self.class} state=#{@state}>"
66
+ end
67
+
68
+ private
69
+
70
+ # 取消可能绕过 StandardError 捕获;未完成的操作都在 ensure
71
+ # 中关闭连接,包括处理部分记录时被中断的情况。
72
+ def perform(timeout = @limits[:idle_timeout], &)
73
+ result = @transport.with_timeout(timeout, &)
74
+ completed = true
75
+ result
76
+ ensure
77
+ close unless completed
78
+ end
79
+
80
+ def monotonic
81
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
82
+ end
83
+ end
84
+ end