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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +14 -0
- data/LICENSE +21 -0
- data/README.md +177 -0
- data/docs/ARCHITECTURE.md +87 -0
- data/docs/COMPATIBILITY.md +160 -0
- data/docs/PROVENANCE.md +59 -0
- data/docs/SECURITY.md +38 -0
- data/exe/snell +6 -0
- data/lib/snell/address.rb +26 -0
- data/lib/snell/cli.rb +140 -0
- data/lib/snell/client.rb +141 -0
- data/lib/snell/configuration.rb +193 -0
- data/lib/snell/crypto.rb +77 -0
- data/lib/snell/datagram.rb +14 -0
- data/lib/snell/datagram_socket.rb +68 -0
- data/lib/snell/direct_connector.rb +28 -0
- data/lib/snell/errors.rb +22 -0
- data/lib/snell/limits.rb +43 -0
- data/lib/snell/obfuscation.rb +241 -0
- data/lib/snell/protocol.rb +166 -0
- data/lib/snell/record_reader.rb +133 -0
- data/lib/snell/record_writer.rb +218 -0
- data/lib/snell/replay_cache.rb +40 -0
- data/lib/snell/server.rb +352 -0
- data/lib/snell/session.rb +84 -0
- data/lib/snell/session_pool.rb +100 -0
- data/lib/snell/socks_server.rb +371 -0
- data/lib/snell/stream.rb +203 -0
- data/lib/snell/transport.rb +140 -0
- data/lib/snell/v6/profile.rb +303 -0
- data/lib/snell/version.rb +5 -0
- data/lib/snell.rb +68 -0
- metadata +155 -0
|
@@ -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
|
data/lib/snell/server.rb
ADDED
|
@@ -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
|