anytls 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 +11 -0
- data/LICENSE +21 -0
- data/README.md +143 -0
- data/bin/anytls-client +7 -0
- data/bin/anytls-server +7 -0
- data/docs/ARCHITECTURE.md +105 -0
- data/docs/SOURCES.md +15 -0
- data/docs/VALIDATION.md +100 -0
- data/docs/validation/runtime-ruby34.json +8 -0
- data/docs/validation/runtime-ruby40.json +8 -0
- data/docs/validation/stress-c1.json +25 -0
- data/docs/validation/stress-c128.json +26 -0
- data/docs/validation/stress-c32.json +26 -0
- data/docs/validation/stress-sequential-10000.json +26 -0
- data/lib/anytls/address.rb +53 -0
- data/lib/anytls/buffered_io.rb +185 -0
- data/lib/anytls/byte_buffer.rb +61 -0
- data/lib/anytls/cli/client_listener.rb +91 -0
- data/lib/anytls/cli/options.rb +160 -0
- data/lib/anytls/cli/runner.rb +89 -0
- data/lib/anytls/cli/shutdown.rb +36 -0
- data/lib/anytls/client.rb +129 -0
- data/lib/anytls/connection_uri.rb +96 -0
- data/lib/anytls/deadline.rb +34 -0
- data/lib/anytls/dialer.rb +70 -0
- data/lib/anytls/errors.rb +23 -0
- data/lib/anytls/inbound/http_proxy.rb +224 -0
- data/lib/anytls/inbound/mixed.rb +60 -0
- data/lib/anytls/inbound/socks4.rb +55 -0
- data/lib/anytls/inbound/socks5.rb +138 -0
- data/lib/anytls/io_helpers.rb +87 -0
- data/lib/anytls/log.rb +15 -0
- data/lib/anytls/padding/scheme.rb +121 -0
- data/lib/anytls/padding/writer.rb +55 -0
- data/lib/anytls/protocol/authentication.rb +44 -0
- data/lib/anytls/protocol/decoder.rb +39 -0
- data/lib/anytls/protocol/frame.rb +40 -0
- data/lib/anytls/protocol/settings.rb +53 -0
- data/lib/anytls/protocol/socks_address.rb +59 -0
- data/lib/anytls/relay.rb +49 -0
- data/lib/anytls/server.rb +220 -0
- data/lib/anytls/session.rb +593 -0
- data/lib/anytls/session_pool.rb +166 -0
- data/lib/anytls/stream.rb +151 -0
- data/lib/anytls/tls_config.rb +76 -0
- data/lib/anytls/transport/tls_connection.rb +117 -0
- data/lib/anytls/uot/connection.rb +122 -0
- data/lib/anytls/uot/server.rb +97 -0
- data/lib/anytls/version.rb +6 -0
- data/lib/anytls.rb +40 -0
- metadata +203 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnyTLS
|
|
4
|
+
# 独占租用会话,优先复用创建序号最新的空闲会话。
|
|
5
|
+
# 正在创建的会话计入容量;空闲保留策略不会主动拨号补齐。
|
|
6
|
+
class SessionPool
|
|
7
|
+
def initialize(max_sessions: 128, min_idle_sessions: 5, idle_timeout: 30, idle_check_interval: 30, reuse: true,
|
|
8
|
+
&factory)
|
|
9
|
+
raise ArgumentError, "session factory required" unless factory
|
|
10
|
+
unless max_sessions.is_a?(Integer) && max_sessions.positive?
|
|
11
|
+
raise ConfigurationError,
|
|
12
|
+
"max_sessions must be positive"
|
|
13
|
+
end
|
|
14
|
+
unless min_idle_sessions.is_a?(Integer) && min_idle_sessions >= 0
|
|
15
|
+
raise ConfigurationError, "min_idle_sessions must be nonnegative"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
[idle_timeout, idle_check_interval].each do |seconds|
|
|
19
|
+
Deadline.new(seconds)
|
|
20
|
+
raise ConfigurationError, "idle periods must be positive" unless seconds&.positive?
|
|
21
|
+
end
|
|
22
|
+
raise ConfigurationError, "reuse must be boolean" unless [true, false].include?(reuse)
|
|
23
|
+
|
|
24
|
+
@max_sessions = max_sessions
|
|
25
|
+
@min_idle_sessions = min_idle_sessions
|
|
26
|
+
@idle_timeout = idle_timeout
|
|
27
|
+
@interval = idle_check_interval
|
|
28
|
+
@reuse = reuse
|
|
29
|
+
@factory = factory
|
|
30
|
+
@mutex = Mutex.new
|
|
31
|
+
@changed = ConditionVariable.new
|
|
32
|
+
@sessions = {}
|
|
33
|
+
@idle = {}
|
|
34
|
+
@creating = 0
|
|
35
|
+
@sequence = 0
|
|
36
|
+
@closed = false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# 锁内预留容量,锁外拨号。池关闭后,
|
|
40
|
+
# 迟到的创建结果会被拒绝并关闭,不会交给租用者。
|
|
41
|
+
def acquire(timeout: 5)
|
|
42
|
+
deadline = Deadline.new(timeout)
|
|
43
|
+
sequence = @mutex.synchronize do
|
|
44
|
+
start_maintenance
|
|
45
|
+
loop do
|
|
46
|
+
raise ClosedError, "client closed" if @closed
|
|
47
|
+
|
|
48
|
+
if @reuse && !@idle.empty?
|
|
49
|
+
session = @idle.keys.max_by(&:sequence)
|
|
50
|
+
@idle.delete(session)
|
|
51
|
+
if session.closed?
|
|
52
|
+
@sessions.delete(session)
|
|
53
|
+
next
|
|
54
|
+
end
|
|
55
|
+
@sessions[session] = :leased
|
|
56
|
+
return session
|
|
57
|
+
end
|
|
58
|
+
if @sessions.size + @creating < @max_sessions
|
|
59
|
+
@creating += 1
|
|
60
|
+
@sequence += 1
|
|
61
|
+
break @sequence
|
|
62
|
+
end
|
|
63
|
+
deadline.wait(@changed, @mutex)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
session = nil
|
|
68
|
+
begin
|
|
69
|
+
session = @factory.call(sequence, deadline.remaining)
|
|
70
|
+
@mutex.synchronize do
|
|
71
|
+
raise ClosedError, "client closed during dial" if @closed || session.closed?
|
|
72
|
+
|
|
73
|
+
@sessions[session] = :leased
|
|
74
|
+
end
|
|
75
|
+
session
|
|
76
|
+
rescue StandardError
|
|
77
|
+
session&.close
|
|
78
|
+
raise
|
|
79
|
+
ensure
|
|
80
|
+
@mutex.synchronize do
|
|
81
|
+
@creating -= 1
|
|
82
|
+
@changed.broadcast
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# 会话已无流时归还;需要丢弃的会话在锁外关闭,
|
|
88
|
+
# 以便其所有者回调安全地从池中移除会话。
|
|
89
|
+
def release(session)
|
|
90
|
+
discard = @mutex.synchronize do
|
|
91
|
+
if @closed || !@reuse || session.closed?
|
|
92
|
+
@sessions.delete(session)
|
|
93
|
+
@idle.delete(session)
|
|
94
|
+
true
|
|
95
|
+
elsif @sessions.key?(session)
|
|
96
|
+
@sessions[session] = :idle
|
|
97
|
+
@idle[session] = Deadline.now
|
|
98
|
+
false
|
|
99
|
+
end
|
|
100
|
+
ensure
|
|
101
|
+
@changed.broadcast
|
|
102
|
+
end
|
|
103
|
+
session.close(wait: false) if discard
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def remove(session)
|
|
107
|
+
@mutex.synchronize do
|
|
108
|
+
@sessions.delete(session)
|
|
109
|
+
@idle.delete(session)
|
|
110
|
+
@changed.broadcast
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def stats
|
|
115
|
+
@mutex.synchronize do
|
|
116
|
+
{ sessions: @sessions.size, idle_sessions: @idle.size, creating_sessions: @creating,
|
|
117
|
+
created_sessions: @sequence }.freeze
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def close
|
|
122
|
+
sessions = @mutex.synchronize do
|
|
123
|
+
@closed = true
|
|
124
|
+
@changed.broadcast
|
|
125
|
+
result = @sessions.keys
|
|
126
|
+
@sessions.clear
|
|
127
|
+
@idle.clear
|
|
128
|
+
result
|
|
129
|
+
end
|
|
130
|
+
sessions.each { |session| session.close(wait: false) }
|
|
131
|
+
sessions.each(&:close)
|
|
132
|
+
@maintenance&.join unless @maintenance == Thread.current
|
|
133
|
+
@mutex.synchronize { @changed.wait(@mutex) while @creating.positive? }
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
def start_maintenance
|
|
140
|
+
return if @maintenance || !@reuse || @closed
|
|
141
|
+
|
|
142
|
+
@maintenance = Thread.new do
|
|
143
|
+
loop do
|
|
144
|
+
expired = @mutex.synchronize do
|
|
145
|
+
@changed.wait(@mutex, @interval) unless @closed
|
|
146
|
+
break if @closed
|
|
147
|
+
|
|
148
|
+
retained = @idle.keys.sort_by { |session| -session.sequence }.first(@min_idle_sessions)
|
|
149
|
+
result = @idle.filter_map do |session, since|
|
|
150
|
+
session if !retained.include?(session) && Deadline.now - since >= @idle_timeout
|
|
151
|
+
end
|
|
152
|
+
result.each do |session|
|
|
153
|
+
@idle.delete(session)
|
|
154
|
+
@sessions.delete(session)
|
|
155
|
+
end
|
|
156
|
+
result
|
|
157
|
+
end
|
|
158
|
+
break unless expired
|
|
159
|
+
|
|
160
|
+
expired.each(&:close)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
@maintenance.name = "anytls-pool"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnyTLS
|
|
4
|
+
# 虚拟字节流,不暴露共享 TLS 文件描述符,也不冒充真实 IO。
|
|
5
|
+
# 写入串行执行并分帧;成功写入仅确认本地传输完成,
|
|
6
|
+
# FIN 同时关闭该虚拟流的两个方向。
|
|
7
|
+
class Stream
|
|
8
|
+
attr_reader :id
|
|
9
|
+
|
|
10
|
+
def initialize(session, id, receive_limit: 262_144)
|
|
11
|
+
@session = session
|
|
12
|
+
@id = id
|
|
13
|
+
@buffer = ByteBuffer.new(limit: receive_limit)
|
|
14
|
+
@mutex = Mutex.new
|
|
15
|
+
@changed = ConditionVariable.new
|
|
16
|
+
@closed = false
|
|
17
|
+
@writing = false
|
|
18
|
+
@connection_status = :unconfirmed
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def connection_status
|
|
22
|
+
@mutex.synchronize { @connection_status }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def closed?
|
|
26
|
+
@mutex.synchronize { @closed }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def inspect
|
|
30
|
+
"#<#{self.class} id=#{id} closed=#{closed?}>"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def readpartial(length, outbuf = nil, timeout: nil)
|
|
34
|
+
result = @buffer.readpartial(length, deadline: Deadline.new(timeout))
|
|
35
|
+
outbuf ? outbuf.replace(result) : result
|
|
36
|
+
rescue EOFError
|
|
37
|
+
outbuf&.clear
|
|
38
|
+
raise
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def read(length, outbuf = nil, timeout: nil)
|
|
42
|
+
raise ArgumentError, "length must be nonnegative" unless length.is_a?(Integer) && length >= 0
|
|
43
|
+
|
|
44
|
+
deadline = Deadline.new(timeout)
|
|
45
|
+
result = String.new(encoding: Encoding::BINARY)
|
|
46
|
+
begin
|
|
47
|
+
result << @buffer.readpartial(length - result.bytesize, deadline: deadline) while result.bytesize < length
|
|
48
|
+
rescue EOFError
|
|
49
|
+
if result.empty? && length.positive?
|
|
50
|
+
outbuf&.clear
|
|
51
|
+
return nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
outbuf ? outbuf.replace(result) : result
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def write(bytes, timeout: nil)
|
|
58
|
+
raise TypeError, "write expects a String" unless bytes.is_a?(String)
|
|
59
|
+
|
|
60
|
+
deadline = Deadline.new(timeout)
|
|
61
|
+
acquire_writer(deadline)
|
|
62
|
+
begin
|
|
63
|
+
offset = 0
|
|
64
|
+
while offset < bytes.bytesize
|
|
65
|
+
ensure_open!
|
|
66
|
+
chunk = bytes.byteslice(offset, 16_384).b.freeze
|
|
67
|
+
@session.write_stream(id, chunk, deadline: deadline)
|
|
68
|
+
offset += chunk.bytesize
|
|
69
|
+
end
|
|
70
|
+
offset
|
|
71
|
+
ensure
|
|
72
|
+
@mutex.synchronize do
|
|
73
|
+
@writing = false
|
|
74
|
+
@changed.broadcast
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def flush
|
|
80
|
+
self
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def close
|
|
84
|
+
return nil if closed?
|
|
85
|
+
|
|
86
|
+
@session.close_stream(id)
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# 等待目标连接的 SYNACK,并非认证回执;v1 对端无法确认目标,
|
|
91
|
+
# 因此等待会持续到超时或流关闭。
|
|
92
|
+
def wait_connected(timeout: 5)
|
|
93
|
+
deadline = Deadline.new(timeout)
|
|
94
|
+
@mutex.synchronize do
|
|
95
|
+
deadline.wait(@changed, @mutex) while @connection_status == :unconfirmed && !@closed
|
|
96
|
+
raise @terminal_error if @terminal_error
|
|
97
|
+
raise ClosedError, "stream closed" if @closed
|
|
98
|
+
|
|
99
|
+
@connection_status
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def buffered_bytes
|
|
104
|
+
@buffer.size
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# 由会话驱动的操作;这些方法不得回调应用代码。
|
|
108
|
+
def receive(bytes)
|
|
109
|
+
@buffer.push(bytes)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def confirm
|
|
113
|
+
@mutex.synchronize do
|
|
114
|
+
@connection_status = :confirmed unless @closed
|
|
115
|
+
@changed.broadcast
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def finish(error = nil, local: false)
|
|
120
|
+
@mutex.synchronize do
|
|
121
|
+
return if @closed
|
|
122
|
+
|
|
123
|
+
@closed = true
|
|
124
|
+
@terminal_error = error
|
|
125
|
+
@connection_status = :failed if error
|
|
126
|
+
@changed.broadcast
|
|
127
|
+
end
|
|
128
|
+
@buffer.finish(error, discard: local)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def ensure_open!
|
|
134
|
+
@mutex.synchronize do
|
|
135
|
+
raise @terminal_error if @terminal_error
|
|
136
|
+
raise ClosedError, "stream closed" if @closed
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def acquire_writer(deadline)
|
|
141
|
+
@mutex.synchronize do
|
|
142
|
+
deadline.wait(@changed, @mutex) while @writing && !@closed
|
|
143
|
+
raise @terminal_error if @terminal_error
|
|
144
|
+
raise ClosedError, "stream closed" if @closed
|
|
145
|
+
|
|
146
|
+
deadline.check!
|
|
147
|
+
@writing = true
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnyTLS
|
|
4
|
+
# 在 socket 使用前构建并冻结 TLS 上下文;客户端验证证书链,
|
|
5
|
+
# 传输层校验对端名称,自签名证书必须显式启用。
|
|
6
|
+
class TLSConfig
|
|
7
|
+
attr_reader :sni, :insecure, :context
|
|
8
|
+
|
|
9
|
+
def initialize(server: false, cert: nil, key: nil, ca_file: nil, sni: nil, insecure: false,
|
|
10
|
+
self_signed: false, min_version: nil, max_version: nil)
|
|
11
|
+
[server, insecure, self_signed].each do |value|
|
|
12
|
+
raise ConfigurationError, "TLS flags must be boolean" unless [true, false].include?(value)
|
|
13
|
+
end
|
|
14
|
+
if self_signed && (cert || key)
|
|
15
|
+
raise ConfigurationError,
|
|
16
|
+
"self-signed and certificate files are mutually exclusive"
|
|
17
|
+
end
|
|
18
|
+
raise ConfigurationError, "certificate and key must be supplied together" if cert.nil? != key.nil?
|
|
19
|
+
raise ConfigurationError, "server certificate and key are required" if server && !self_signed && !cert
|
|
20
|
+
raise ConfigurationError, "self-signed is a server option" if self_signed && !server
|
|
21
|
+
|
|
22
|
+
Address.new(host: sni, port: 0) unless sni.nil?
|
|
23
|
+
|
|
24
|
+
@sni = sni&.dup&.freeze
|
|
25
|
+
@insecure = insecure
|
|
26
|
+
@context = OpenSSL::SSL::SSLContext.new
|
|
27
|
+
if server
|
|
28
|
+
@context.cert, @context.key, chain = self_signed ? generate_certificate : load_certificate(cert, key)
|
|
29
|
+
@context.extra_chain_cert = chain if chain&.any?
|
|
30
|
+
unless @context.cert.check_private_key(@context.key)
|
|
31
|
+
raise ConfigurationError,
|
|
32
|
+
"certificate does not match private key"
|
|
33
|
+
end
|
|
34
|
+
else
|
|
35
|
+
# 显式 CA 必须在 set_params 前设置,避免加载到进程共享的默认信任库。
|
|
36
|
+
@context.ca_file = ca_file if ca_file
|
|
37
|
+
@context.set_params(verify_mode: insecure ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER)
|
|
38
|
+
# 显式使用 IP 作为 SNI 配置时,发送的 SNI 与证书验证名称有所区别。
|
|
39
|
+
@context.verify_hostname = false
|
|
40
|
+
end
|
|
41
|
+
@context.min_version = min_version || OpenSSL::SSL::TLS1_2_VERSION
|
|
42
|
+
@context.max_version = max_version if max_version
|
|
43
|
+
@context.freeze
|
|
44
|
+
freeze
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def inspect
|
|
48
|
+
"#<#{self.class} insecure=#{insecure}>"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def load_certificate(cert, key)
|
|
54
|
+
certificates = cert.is_a?(OpenSSL::X509::Certificate) ? [cert] : OpenSSL::X509::Certificate.load_file(cert)
|
|
55
|
+
private_key = key.respond_to?(:private?) ? key : OpenSSL::PKey.read(File.binread(key))
|
|
56
|
+
[certificates.first, private_key, certificates.drop(1)]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def generate_certificate
|
|
60
|
+
key = OpenSSL::PKey::EC.generate("prime256v1")
|
|
61
|
+
certificate = OpenSSL::X509::Certificate.new
|
|
62
|
+
certificate.version = 2
|
|
63
|
+
certificate.serial = SecureRandom.random_number(2**128)
|
|
64
|
+
certificate.subject = certificate.issuer = OpenSSL::X509::Name.parse("/CN=localhost")
|
|
65
|
+
certificate.public_key = key
|
|
66
|
+
certificate.not_before = Time.now - 60
|
|
67
|
+
certificate.not_after = Time.now + (86_400 * 30)
|
|
68
|
+
factory = OpenSSL::X509::ExtensionFactory.new
|
|
69
|
+
factory.subject_certificate = factory.issuer_certificate = certificate
|
|
70
|
+
certificate.add_extension(factory.create_extension("basicConstraints", "CA:TRUE", true))
|
|
71
|
+
certificate.add_extension(factory.create_extension("subjectAltName", "DNS:localhost,IP:127.0.0.1,IP:::1"))
|
|
72
|
+
certificate.sign(key, OpenSSL::Digest.new("SHA256"))
|
|
73
|
+
[certificate, key]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnyTLS
|
|
4
|
+
module Transport
|
|
5
|
+
# 只有 Session 所有者线程可调用 TLS 方法;待重试的 SSL 写入
|
|
6
|
+
# 在 WANT_READ/WANT_WRITE 重试期间保持完全相同的字节和长度。
|
|
7
|
+
class TLSConnection
|
|
8
|
+
attr_reader :io, :write_wait
|
|
9
|
+
|
|
10
|
+
def initialize(socket, config:, server:, host: nil, cancel_io: nil, cancelled: nil)
|
|
11
|
+
@io = socket
|
|
12
|
+
@config = config
|
|
13
|
+
@server = server
|
|
14
|
+
@cancel_io = cancel_io
|
|
15
|
+
@cancelled = cancelled
|
|
16
|
+
@verification_name = config.sni || host
|
|
17
|
+
@ssl = OpenSSL::SSL::SSLSocket.new(socket, config.context)
|
|
18
|
+
@ssl.sync_close = true
|
|
19
|
+
@ssl.hostname = @verification_name if !server && @verification_name && !ip_literal?(@verification_name)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def handshake(timeout: 5)
|
|
23
|
+
deadline = Deadline.new(timeout)
|
|
24
|
+
loop do
|
|
25
|
+
raise ClosedError, "TLS setup cancelled" if @cancelled&.call
|
|
26
|
+
|
|
27
|
+
deadline.check!
|
|
28
|
+
result = @server ? @ssl.accept_nonblock(exception: false) : @ssl.connect_nonblock(exception: false)
|
|
29
|
+
break unless result.is_a?(Symbol)
|
|
30
|
+
|
|
31
|
+
wait_for_io(result, deadline)
|
|
32
|
+
end
|
|
33
|
+
@ssl.post_connection_check(@verification_name) unless @server || @config.insecure
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def authenticate(password_digest, scheme:, timeout: 5)
|
|
37
|
+
if @server
|
|
38
|
+
Protocol::Authentication.read(self, password_digest: password_digest, timeout: timeout)
|
|
39
|
+
else
|
|
40
|
+
IOHelpers.write_all(self, Protocol::Authentication.encode(password_digest, scheme: scheme), timeout: timeout)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def read_nonblock(length, exception: false)
|
|
45
|
+
@ssl.read_nonblock(length, exception: exception)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def write_nonblock(bytes, exception: false)
|
|
49
|
+
@ssl.write_nonblock(bytes, exception: exception)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def wait_for_io(direction, deadline)
|
|
53
|
+
deadline.check!
|
|
54
|
+
raise ClosedError, "TLS setup cancelled" if @cancelled&.call
|
|
55
|
+
|
|
56
|
+
readers = @cancel_io ? [@cancel_io] : []
|
|
57
|
+
readers << @io if direction == :wait_readable
|
|
58
|
+
writers = direction == :wait_writable ? [@io] : []
|
|
59
|
+
ready = IO.select(readers, writers, nil, deadline.remaining)
|
|
60
|
+
raise ClosedError, "TLS setup cancelled" if @cancelled&.call
|
|
61
|
+
raise TimeoutError, "TLS setup timed out" unless ready
|
|
62
|
+
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def pending
|
|
67
|
+
@ssl.pending
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def inspect
|
|
71
|
+
"#<#{self.class} role=#{@server ? 'server' : 'client'}>"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def begin_write(bytes)
|
|
75
|
+
raise ProtocolError, "overlapping TLS writes" if @write_buffer
|
|
76
|
+
|
|
77
|
+
@write_buffer = bytes
|
|
78
|
+
@write_wait = nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def advance_write
|
|
82
|
+
result = @ssl.write_nonblock(@write_buffer, exception: false)
|
|
83
|
+
if result.is_a?(Symbol)
|
|
84
|
+
@write_wait = result
|
|
85
|
+
return result
|
|
86
|
+
end
|
|
87
|
+
raise TransportError, "TLS write made no progress" unless result.positive?
|
|
88
|
+
|
|
89
|
+
@write_wait = nil
|
|
90
|
+
if result == @write_buffer.bytesize
|
|
91
|
+
@write_buffer = nil
|
|
92
|
+
:complete
|
|
93
|
+
else
|
|
94
|
+
@write_buffer = @write_buffer.byteslice(result..)
|
|
95
|
+
:progress
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def close
|
|
100
|
+
# 失败写入仍占有 SSL 重试状态时,不执行阻塞式 TLS 关闭握手。
|
|
101
|
+
@io.close unless @io.closed?
|
|
102
|
+
@ssl.sysclose
|
|
103
|
+
rescue IOError, SystemCallError, OpenSSL::SSL::SSLError
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def ip_literal?(host)
|
|
110
|
+
IPAddr.new(host)
|
|
111
|
+
true
|
|
112
|
+
rescue IPAddr::InvalidAddressError
|
|
113
|
+
false
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
|
|
5
|
+
module AnyTLS
|
|
6
|
+
# UDP-over-TCP v2 消息定界;地址类型标记与 SOCKS 地址格式不同。
|
|
7
|
+
module UoT
|
|
8
|
+
MAGIC_HOST = "sp.v2.udp-over-tcp.arpa"
|
|
9
|
+
|
|
10
|
+
# 在字节流上提供数据包接口,读取和写入分别串行化。
|
|
11
|
+
# 数据包部分传输失败时关闭流,防止从错误的消息边界继续解析。
|
|
12
|
+
class Connection
|
|
13
|
+
# 线协议长度上限;操作系统可能施加更低的 UDP 大小限制。
|
|
14
|
+
MAX_PACKET_SIZE = 65_535
|
|
15
|
+
attr_reader :destination
|
|
16
|
+
|
|
17
|
+
def initialize(stream, destination: nil, client: true, timeout: nil)
|
|
18
|
+
@stream = stream
|
|
19
|
+
@destination = destination
|
|
20
|
+
@write_gate = Queue.new
|
|
21
|
+
@read_gate = Queue.new
|
|
22
|
+
@write_gate << true
|
|
23
|
+
@read_gate << true
|
|
24
|
+
if client
|
|
25
|
+
target = destination || Address.new(host: "0.0.0.0", port: 0)
|
|
26
|
+
request = [destination ? 1 : 0].pack("C") + Protocol::SocksAddress.encode(target)
|
|
27
|
+
IOHelpers.write_all(stream, request, timeout: timeout)
|
|
28
|
+
end
|
|
29
|
+
rescue StandardError
|
|
30
|
+
stream.close unless stream.closed?
|
|
31
|
+
raise
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.accept(stream, timeout: nil)
|
|
35
|
+
deadline = Deadline.new(timeout)
|
|
36
|
+
mode = IOHelpers.read_exact(stream, 1, timeout: deadline.remaining).unpack1("C")
|
|
37
|
+
raise ProtocolError, "invalid UoT mode" unless [0, 1].include?(mode)
|
|
38
|
+
|
|
39
|
+
destination = Protocol::SocksAddress.read(stream, timeout: deadline.remaining)
|
|
40
|
+
new(stream, destination: mode == 1 ? destination : nil, client: false)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def send_packet(payload, destination: nil, timeout: nil)
|
|
44
|
+
deadline = Deadline.new(timeout)
|
|
45
|
+
payload = String(payload).b
|
|
46
|
+
raise BufferLimitExceeded, "UDP payload exceeds #{MAX_PACKET_SIZE} bytes" if payload.bytesize > MAX_PACKET_SIZE
|
|
47
|
+
|
|
48
|
+
target = destination || @destination
|
|
49
|
+
raise ArgumentError, "destination is required" unless target
|
|
50
|
+
if @destination && (target.host != @destination.host || target.port != @destination.port)
|
|
51
|
+
raise ArgumentError, "connected UoT destination cannot change"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
bytes = @destination ? +"".b : encode_address(target)
|
|
55
|
+
bytes << [payload.bytesize].pack("n") << payload
|
|
56
|
+
with_gate(@write_gate, deadline) { IOHelpers.write_all(@stream, bytes, timeout: deadline.remaining) }
|
|
57
|
+
payload.bytesize
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def receive_packet(timeout: nil)
|
|
61
|
+
deadline = Deadline.new(timeout)
|
|
62
|
+
with_gate(@read_gate, deadline) do
|
|
63
|
+
destination = @destination || read_address(deadline)
|
|
64
|
+
length = IOHelpers.read_exact(@stream, 2, timeout: deadline.remaining).unpack1("n")
|
|
65
|
+
raise BufferLimitExceeded, "UDP payload exceeds #{MAX_PACKET_SIZE} bytes" if length > MAX_PACKET_SIZE
|
|
66
|
+
|
|
67
|
+
[IOHelpers.read_exact(@stream, length, timeout: deadline.remaining), destination]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def close
|
|
72
|
+
IOHelpers.close(@stream) unless @stream.closed?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def closed? = @stream.closed?
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def with_gate(gate, deadline)
|
|
80
|
+
deadline.check!
|
|
81
|
+
acquired = gate.pop(timeout: deadline.remaining)
|
|
82
|
+
raise TimeoutError, "datagram operation timed out waiting for another operation" unless acquired
|
|
83
|
+
raise ClosedError, "datagram connection closed" if closed?
|
|
84
|
+
|
|
85
|
+
begin
|
|
86
|
+
yield
|
|
87
|
+
rescue Error, IOError, SystemCallError
|
|
88
|
+
# 部分数据包无法从原消息边界恢复传输。
|
|
89
|
+
close
|
|
90
|
+
raise
|
|
91
|
+
end
|
|
92
|
+
ensure
|
|
93
|
+
gate << true if acquired
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def encode_address(address)
|
|
97
|
+
encoded = Protocol::SocksAddress.encode(address).dup
|
|
98
|
+
encoded.setbyte(0, { 1 => 0, 4 => 1, 3 => 2 }.fetch(encoded.getbyte(0)))
|
|
99
|
+
encoded
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def read_address(deadline)
|
|
103
|
+
type = IOHelpers.read_exact(@stream, 1, timeout: deadline.remaining).unpack1("C")
|
|
104
|
+
host = case type
|
|
105
|
+
when 0 then IPAddr.ntop(IOHelpers.read_exact(@stream, 4, timeout: deadline.remaining))
|
|
106
|
+
when 1 then IPAddr.ntop(IOHelpers.read_exact(@stream, 16, timeout: deadline.remaining))
|
|
107
|
+
when 2
|
|
108
|
+
size = IOHelpers.read_exact(@stream, 1, timeout: deadline.remaining).unpack1("C")
|
|
109
|
+
raise ProtocolError, "empty UoT domain" if size.zero?
|
|
110
|
+
|
|
111
|
+
IOHelpers.read_exact(@stream, size, timeout: deadline.remaining)
|
|
112
|
+
else
|
|
113
|
+
raise ProtocolError, "invalid UoT address type"
|
|
114
|
+
end
|
|
115
|
+
port = IOHelpers.read_exact(@stream, 2, timeout: deadline.remaining).unpack1("n")
|
|
116
|
+
Address.new(host: host, port: port)
|
|
117
|
+
rescue ArgumentError => error
|
|
118
|
+
raise ProtocolError, "invalid UoT address", cause: error
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|