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,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
|
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: []
|