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,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
module AnyTLS
|
|
7
|
+
module Inbound
|
|
8
|
+
# 支持无认证的 SOCKS5 CONNECT 与 UDP ASSOCIATE;拒绝 BIND 和 UDP 分片。
|
|
9
|
+
# UDP 来源限定为 TCP 对端 IP 和选定的源端口;
|
|
10
|
+
# 控制连接关闭时,同时关闭 UDP 转发资源及工作线程。
|
|
11
|
+
class Socks5
|
|
12
|
+
def initialize(client:, logger: nil, handshake_timeout: 5)
|
|
13
|
+
@client = client
|
|
14
|
+
@logger = logger
|
|
15
|
+
@handshake_timeout = handshake_timeout
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def handle(io)
|
|
19
|
+
version, count = exact(io, 2).unpack("CC")
|
|
20
|
+
raise ProtocolError, "invalid SOCKS5 greeting" unless version == 5 && count.positive?
|
|
21
|
+
|
|
22
|
+
methods = exact(io, count).bytes
|
|
23
|
+
unless methods.include?(0)
|
|
24
|
+
io.write("\x05\xff".b)
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
io.write("\x05\x00".b)
|
|
28
|
+
version, command, reserved = exact(io, 3).unpack("CCC")
|
|
29
|
+
raise ProtocolError, "invalid SOCKS5 request" unless version == 5 && reserved.zero?
|
|
30
|
+
|
|
31
|
+
destination = Protocol::SocksAddress.read(io, timeout: @handshake_timeout)
|
|
32
|
+
case command
|
|
33
|
+
when 1 then connect(io, destination)
|
|
34
|
+
when 3 then associate(io, destination)
|
|
35
|
+
else reply(io, 7)
|
|
36
|
+
end
|
|
37
|
+
rescue ProtocolError, ArgumentError
|
|
38
|
+
reply(io, 8)
|
|
39
|
+
rescue TimeoutError
|
|
40
|
+
reply(io, 6)
|
|
41
|
+
rescue RemoteError, TransportError, SocketError, SystemCallError
|
|
42
|
+
reply(io, 5)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def exact(io, length)
|
|
48
|
+
IOHelpers.read_exact(io, length, timeout: @handshake_timeout)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def reply(io, status, address = Address.new(host: "0.0.0.0", port: 0))
|
|
52
|
+
return if @request_replied
|
|
53
|
+
|
|
54
|
+
io.write([5, status, 0].pack("CCC") + Protocol::SocksAddress.encode(address))
|
|
55
|
+
@request_replied = true
|
|
56
|
+
rescue IOError, SystemCallError, Error
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def connect(io, destination)
|
|
61
|
+
raise ProtocolError, "invalid destination port" if destination.port.zero?
|
|
62
|
+
|
|
63
|
+
stream = @client.connect(destination.host, destination.port, timeout: @handshake_timeout)
|
|
64
|
+
reply(io, 0)
|
|
65
|
+
io.timeout = nil
|
|
66
|
+
Relay.copy(io, stream)
|
|
67
|
+
ensure
|
|
68
|
+
stream&.close
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def associate(io, requested_source)
|
|
72
|
+
socket = UDPSocket.new(io.addr[0] == "AF_INET6" ? Socket::AF_INET6 : Socket::AF_INET)
|
|
73
|
+
socket.bind(io.addr[3], 0)
|
|
74
|
+
packets = @client.open_datagrams(timeout: @handshake_timeout)
|
|
75
|
+
reply(io, 0, Address.new(host: socket.addr[3], port: socket.addr[1]))
|
|
76
|
+
io.timeout = nil
|
|
77
|
+
source_ip = IPAddr.new(io.peeraddr[3]).native.to_s
|
|
78
|
+
source_port = requested_source.port.zero? ? nil : requested_source.port
|
|
79
|
+
peer = nil
|
|
80
|
+
peer_lock = Mutex.new
|
|
81
|
+
done = Queue.new
|
|
82
|
+
workers = []
|
|
83
|
+
workers << packet_worker(done) do
|
|
84
|
+
loop do
|
|
85
|
+
wire, sender = socket.recvfrom(UoT::Connection::MAX_PACKET_SIZE + 263)
|
|
86
|
+
next unless IPAddr.new(sender[3]).native.to_s == source_ip
|
|
87
|
+
next if source_port && sender[1] != source_port
|
|
88
|
+
next unless wire.bytesize >= 4 && wire.byteslice(0, 3) == "\0\0\0".b
|
|
89
|
+
|
|
90
|
+
begin
|
|
91
|
+
input = StringIO.new(wire.byteslice(3..))
|
|
92
|
+
destination = Protocol::SocksAddress.read(input)
|
|
93
|
+
payload = input.read
|
|
94
|
+
next if payload.bytesize > UoT::Connection::MAX_PACKET_SIZE || destination.port.zero?
|
|
95
|
+
|
|
96
|
+
accepted = peer_lock.synchronize do
|
|
97
|
+
peer ||= [sender[3], sender[1]]
|
|
98
|
+
peer == [sender[3], sender[1]]
|
|
99
|
+
end
|
|
100
|
+
next unless accepted
|
|
101
|
+
|
|
102
|
+
packets.send_packet(payload, destination: destination)
|
|
103
|
+
rescue ProtocolError, EOFError, ArgumentError
|
|
104
|
+
next
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
workers << packet_worker(done) do
|
|
109
|
+
loop do
|
|
110
|
+
payload, destination = packets.receive_packet
|
|
111
|
+
target = peer_lock.synchronize { peer }
|
|
112
|
+
next unless target
|
|
113
|
+
|
|
114
|
+
wire = "\0\0\0".b + Protocol::SocksAddress.encode(destination) + payload
|
|
115
|
+
socket.send(wire, 0, target[0], target[1])
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
workers << packet_worker(done) { loop { io.readpartial(1024) } }
|
|
119
|
+
done.pop
|
|
120
|
+
ensure
|
|
121
|
+
socket&.close
|
|
122
|
+
packets&.close
|
|
123
|
+
io.close
|
|
124
|
+
workers&.each(&:join)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def packet_worker(done, &block)
|
|
128
|
+
Thread.new do
|
|
129
|
+
block.call
|
|
130
|
+
rescue IOError, SystemCallError, Error, SocketError
|
|
131
|
+
nil
|
|
132
|
+
ensure
|
|
133
|
+
done << true
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnyTLS
|
|
4
|
+
# 为原生 socket、StringIO 和支持超时的流提供完整二进制读写。
|
|
5
|
+
# 部分进度消耗同一预算;辅助方法不重放失败写入。
|
|
6
|
+
module IOHelpers
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Linux 上仅关闭描述符不能唤醒另一线程的 select;先终止 socket 通信。
|
|
10
|
+
# 未连接的 UDP socket 可能拒绝 shutdown,但仍需完成关闭。
|
|
11
|
+
def close(io)
|
|
12
|
+
if io.is_a?(BasicSocket) && !io.closed?
|
|
13
|
+
begin
|
|
14
|
+
io.shutdown(Socket::SHUT_RDWR)
|
|
15
|
+
rescue IOError, SystemCallError
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
io.close
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def read_exact(io, length, timeout: nil)
|
|
23
|
+
raise ArgumentError, "negative length" unless length.is_a?(Integer) && length >= 0
|
|
24
|
+
|
|
25
|
+
deadline = Deadline.new(timeout)
|
|
26
|
+
output = String.new(encoding: Encoding::BINARY)
|
|
27
|
+
while output.bytesize < length
|
|
28
|
+
chunk = read_partial(io, length - output.bytesize, deadline: deadline)
|
|
29
|
+
raise EOFError, "unexpected end of input" if chunk.nil? || chunk.empty?
|
|
30
|
+
|
|
31
|
+
output << chunk
|
|
32
|
+
end
|
|
33
|
+
output
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def read_partial(io, length, deadline: Deadline.new)
|
|
37
|
+
deadline.check!
|
|
38
|
+
if io.respond_to?(:read_nonblock)
|
|
39
|
+
loop do
|
|
40
|
+
result = io.read_nonblock(length, exception: false)
|
|
41
|
+
return result unless result.is_a?(Symbol)
|
|
42
|
+
|
|
43
|
+
wait(io, result, deadline)
|
|
44
|
+
end
|
|
45
|
+
elsif io.is_a?(StringIO)
|
|
46
|
+
io.read(length)
|
|
47
|
+
else
|
|
48
|
+
io.readpartial(length, timeout: deadline.remaining)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def write_all(io, bytes, timeout: nil)
|
|
53
|
+
deadline = Deadline.new(timeout)
|
|
54
|
+
offset = 0
|
|
55
|
+
while offset < bytes.bytesize
|
|
56
|
+
deadline.check!
|
|
57
|
+
part = bytes.byteslice(offset..)
|
|
58
|
+
count = nil
|
|
59
|
+
if io.respond_to?(:write_nonblock)
|
|
60
|
+
loop do
|
|
61
|
+
count = io.write_nonblock(part, exception: false)
|
|
62
|
+
break unless count.is_a?(Symbol)
|
|
63
|
+
|
|
64
|
+
wait(io, count, deadline)
|
|
65
|
+
end
|
|
66
|
+
elsif io.is_a?(StringIO)
|
|
67
|
+
count = io.write(part)
|
|
68
|
+
else
|
|
69
|
+
count = io.write(part, timeout: deadline.remaining)
|
|
70
|
+
end
|
|
71
|
+
raise TransportError, "write made no progress" unless count.is_a?(Integer) && count.positive?
|
|
72
|
+
|
|
73
|
+
offset += count
|
|
74
|
+
end
|
|
75
|
+
offset
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def wait(io, direction, deadline)
|
|
79
|
+
return io.wait_for_io(direction, deadline) if io.respond_to?(:wait_for_io)
|
|
80
|
+
|
|
81
|
+
deadline.check!
|
|
82
|
+
readable = direction == :wait_readable ? [io] : nil
|
|
83
|
+
writable = direction == :wait_writable ? [io] : nil
|
|
84
|
+
raise TimeoutError, "I/O timed out" unless IO.select(readable, writable, nil, deadline.remaining)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
data/lib/anytls/log.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnyTLS
|
|
4
|
+
# 可选诊断边界;日志器异常作为返回值交给调用者检查。
|
|
5
|
+
module Log
|
|
6
|
+
# 诊断失败不能替换传输异常,也不能中断资源清理。
|
|
7
|
+
# 调用者可保留返回的异常,供诊断接口查询。
|
|
8
|
+
def self.emit(logger, level, message)
|
|
9
|
+
logger&.public_send(level, message)
|
|
10
|
+
nil
|
|
11
|
+
rescue StandardError => error
|
|
12
|
+
error
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module AnyTLS
|
|
7
|
+
# 经过校验的线协议填充方案,以及每个会话独立的明文分片。
|
|
8
|
+
module Padding
|
|
9
|
+
# 保存不可变的原始字节和校验后的范围;MD5 仅标识填充方案,
|
|
10
|
+
# 不用于认证或完整性保护。
|
|
11
|
+
class Scheme
|
|
12
|
+
CHECK = -1
|
|
13
|
+
MAX_RULES = 256
|
|
14
|
+
MAX_STOP = 256
|
|
15
|
+
MAX_SEGMENTS = 64
|
|
16
|
+
MAX_PACKET_BYTES = 262_144
|
|
17
|
+
MAX_PADDING_BYTES = 1_048_576
|
|
18
|
+
DEFAULT_RAW = <<~SCHEME.chomp.freeze
|
|
19
|
+
stop=8
|
|
20
|
+
0=30-30
|
|
21
|
+
1=100-400
|
|
22
|
+
2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000
|
|
23
|
+
3=9-9,500-1000
|
|
24
|
+
4=500-1000
|
|
25
|
+
5=500-1000
|
|
26
|
+
6=500-1000
|
|
27
|
+
7=500-1000
|
|
28
|
+
SCHEME
|
|
29
|
+
|
|
30
|
+
attr_reader :raw, :md5, :stop, :max_padding_bytes
|
|
31
|
+
|
|
32
|
+
def self.default
|
|
33
|
+
new(DEFAULT_RAW)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def initialize(raw)
|
|
37
|
+
raise TypeError, "padding scheme must be a String" unless raw.is_a?(String)
|
|
38
|
+
raise ConfigurationError, "padding scheme exceeds frame limit" if raw.bytesize > Protocol::MAX_PAYLOAD
|
|
39
|
+
|
|
40
|
+
@raw = raw.b.freeze
|
|
41
|
+
@md5 = Digest::MD5.hexdigest(@raw).freeze
|
|
42
|
+
@rules = parse_rules
|
|
43
|
+
freeze
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def sizes(packet, random: SecureRandom)
|
|
47
|
+
raise ArgumentError, "packet number must be a nonnegative Integer" unless packet.is_a?(Integer) && packet >= 0
|
|
48
|
+
|
|
49
|
+
@rules.fetch(packet, []).map do |rule|
|
|
50
|
+
next CHECK if rule == CHECK
|
|
51
|
+
|
|
52
|
+
minimum, maximum = rule
|
|
53
|
+
next minimum if minimum == maximum
|
|
54
|
+
|
|
55
|
+
# 与 Go 参考实现一致:非固定范围不包含上界。
|
|
56
|
+
minimum + if random.respond_to?(:random_number)
|
|
57
|
+
random.random_number(maximum - minimum)
|
|
58
|
+
else
|
|
59
|
+
random.rand(maximum - minimum)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def parse_rules
|
|
67
|
+
settings = Protocol::Settings.decode(@raw)
|
|
68
|
+
stop_text = settings.fetch("stop") { raise ConfigurationError, "padding scheme requires stop" }
|
|
69
|
+
unless stop_text.match?(/\A\d+\z/) && (0..MAX_STOP).cover?(stop_text.to_i)
|
|
70
|
+
raise ConfigurationError, "padding stop must be between 0 and #{MAX_STOP}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
@stop = stop_text.to_i
|
|
74
|
+
raise ConfigurationError, "too many padding rules" if settings.size > MAX_RULES + 1
|
|
75
|
+
|
|
76
|
+
budget = 0
|
|
77
|
+
@max_padding_bytes = 0
|
|
78
|
+
rules = settings.each_with_object({}) do |(packet, value), result|
|
|
79
|
+
next if packet == "stop"
|
|
80
|
+
unless packet.match?(/\A(?:0|[1-9]\d*)\z/) && packet.to_i < MAX_STOP
|
|
81
|
+
raise ConfigurationError, "invalid padding packet number"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
tokens = value.split(",", -1)
|
|
85
|
+
raise ConfigurationError, "too many padding segments" if tokens.size > MAX_SEGMENTS
|
|
86
|
+
|
|
87
|
+
ranges = tokens.map { |token| parse_range(token) }.freeze
|
|
88
|
+
if packet == "0" && (ranges.size != 1 || ranges.first == CHECK)
|
|
89
|
+
raise ConfigurationError, "authentication padding requires one range"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
packet_bytes = ranges.sum { |range| range == CHECK ? 0 : range.last + Protocol::HEADER_SIZE }
|
|
93
|
+
raise ConfigurationError, "padding packet exceeds byte budget" if packet_bytes > MAX_PACKET_BYTES
|
|
94
|
+
|
|
95
|
+
@max_padding_bytes = [@max_padding_bytes, packet_bytes].max
|
|
96
|
+
budget += packet_bytes
|
|
97
|
+
raise ConfigurationError, "padding scheme exceeds byte budget" if budget > MAX_PADDING_BYTES
|
|
98
|
+
|
|
99
|
+
result[packet.to_i] = ranges
|
|
100
|
+
end
|
|
101
|
+
rules.freeze
|
|
102
|
+
rescue ProtocolError => error
|
|
103
|
+
raise ConfigurationError, "invalid padding scheme", cause: error
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def parse_range(token)
|
|
107
|
+
return CHECK if token == "c"
|
|
108
|
+
|
|
109
|
+
match = /\A(\d+)-(\d+)\z/.match(token)
|
|
110
|
+
raise ConfigurationError, "invalid padding range" unless match
|
|
111
|
+
|
|
112
|
+
minimum, maximum = match.captures.map(&:to_i).minmax
|
|
113
|
+
unless minimum.positive? && maximum <= Protocol::MAX_PAYLOAD
|
|
114
|
+
raise ConfigurationError, "padding lengths must be between 1 and 65535"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
[minimum, maximum].freeze
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnyTLS
|
|
4
|
+
module Padding
|
|
5
|
+
# 生成 TLS 明文写入片段;部分写入由传输层负责推进。
|
|
6
|
+
# 一次编码只推进一次包计数,不因片段数量而重复推进。
|
|
7
|
+
class Writer
|
|
8
|
+
attr_reader :packet_count, :scheme
|
|
9
|
+
|
|
10
|
+
def initialize(scheme, random: SecureRandom)
|
|
11
|
+
raise TypeError, "scheme must be a Padding::Scheme" unless scheme.is_a?(Scheme)
|
|
12
|
+
|
|
13
|
+
@scheme = scheme
|
|
14
|
+
@random = random
|
|
15
|
+
@packet_count = 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def encode(bytes)
|
|
19
|
+
raise TypeError, "bytes must be a String" unless bytes.is_a?(String)
|
|
20
|
+
|
|
21
|
+
@packet_count += 1
|
|
22
|
+
remaining = bytes.b
|
|
23
|
+
return [remaining] if @packet_count >= @scheme.stop
|
|
24
|
+
|
|
25
|
+
chunks = []
|
|
26
|
+
@scheme.sizes(@packet_count, random: @random).each do |size|
|
|
27
|
+
if size == Scheme::CHECK
|
|
28
|
+
break if remaining.empty?
|
|
29
|
+
|
|
30
|
+
next
|
|
31
|
+
end
|
|
32
|
+
if remaining.bytesize > size
|
|
33
|
+
chunks << remaining.byteslice(0, size)
|
|
34
|
+
remaining = remaining.byteslice(size..)
|
|
35
|
+
elsif remaining.empty?
|
|
36
|
+
# 保持 Go 参考实现的纯填充长度语义:载荷长度加帧头长度。
|
|
37
|
+
chunks << waste(size)
|
|
38
|
+
else
|
|
39
|
+
padding_size = size - remaining.bytesize - Protocol::HEADER_SIZE
|
|
40
|
+
chunks << (padding_size.positive? ? remaining + waste(padding_size) : remaining)
|
|
41
|
+
remaining = "".b
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
chunks << remaining unless remaining.empty?
|
|
45
|
+
chunks
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def waste(size)
|
|
51
|
+
Protocol::Frame.new(command: Protocol::WASTE, stream_id: 0, payload: "\0".b * size).encode
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module AnyTLS
|
|
6
|
+
module Protocol
|
|
7
|
+
# 仅在 TLS 内传输的认证前导:SHA-256 原始字节、uint16 长度和填充。
|
|
8
|
+
# 读取仅消费该认证记录;协议没有独立的对端认证回执。
|
|
9
|
+
module Authentication
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def encode(password_digest, scheme:)
|
|
13
|
+
validate_digest!(password_digest)
|
|
14
|
+
length = scheme.sizes(0).first || 0
|
|
15
|
+
password_digest.b + [length].pack("n") + ("\0".b * length)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# 该解析器消费认证记录,调用语义与 SocksAddress.read 一致。
|
|
19
|
+
# rubocop:disable-next Naming/PredicateMethod
|
|
20
|
+
def read(io, password_digest:, timeout: nil)
|
|
21
|
+
validate_digest!(password_digest)
|
|
22
|
+
deadline = Deadline.new(timeout)
|
|
23
|
+
header = IOHelpers.read_exact(io, 34, timeout: deadline.remaining)
|
|
24
|
+
digest = header.byteslice(0, 32)
|
|
25
|
+
length = header.unpack1("n", offset: 32)
|
|
26
|
+
IOHelpers.read_exact(io, length, timeout: deadline.remaining) if length.positive?
|
|
27
|
+
unless OpenSSL.fixed_length_secure_compare(digest, password_digest.b)
|
|
28
|
+
raise AuthenticationError, "authentication failed"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def validate_digest!(digest)
|
|
35
|
+
unless digest.is_a?(String) && digest.bytesize == 32
|
|
36
|
+
raise ArgumentError,
|
|
37
|
+
"password digest must contain 32 raw bytes"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private_class_method :validate_digest!
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnyTLS
|
|
4
|
+
module Protocol
|
|
5
|
+
# 增量解析任意传输片段,不假定 TLS 记录边界就是协议帧边界。
|
|
6
|
+
class Decoder
|
|
7
|
+
def initialize
|
|
8
|
+
@buffer = String.new(encoding: Encoding::BINARY)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def feed(bytes)
|
|
12
|
+
raise TypeError, "bytes must be a String" unless bytes.is_a?(String)
|
|
13
|
+
|
|
14
|
+
@buffer << bytes.b
|
|
15
|
+
frames = []
|
|
16
|
+
offset = 0
|
|
17
|
+
while @buffer.bytesize - offset >= HEADER_SIZE
|
|
18
|
+
command, stream_id, length = @buffer.unpack("CNn", offset:)
|
|
19
|
+
break if @buffer.bytesize - offset < HEADER_SIZE + length
|
|
20
|
+
|
|
21
|
+
frames << Frame.new(command:, stream_id:, payload: @buffer.byteslice(offset + HEADER_SIZE, length))
|
|
22
|
+
offset += HEADER_SIZE + length
|
|
23
|
+
end
|
|
24
|
+
@buffer = @buffer.byteslice(offset..) if offset.positive?
|
|
25
|
+
frames
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def finish!
|
|
29
|
+
raise ProtocolError, "truncated frame at end of connection" unless @buffer.empty?
|
|
30
|
+
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def buffered_bytes
|
|
35
|
+
@buffer.bytesize
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnyTLS
|
|
4
|
+
# AnyTLS 线协议常量与编解码;命令语义由会话状态机校验。
|
|
5
|
+
module Protocol
|
|
6
|
+
WASTE = 0
|
|
7
|
+
SYN = 1
|
|
8
|
+
PSH = 2
|
|
9
|
+
FIN = 3
|
|
10
|
+
SETTINGS = 4
|
|
11
|
+
ALERT = 5
|
|
12
|
+
UPDATE_PADDING_SCHEME = 6
|
|
13
|
+
SYNACK = 7
|
|
14
|
+
HEART_REQUEST = 8
|
|
15
|
+
HEART_RESPONSE = 9
|
|
16
|
+
SERVER_SETTINGS = 10
|
|
17
|
+
HEADER_SIZE = 7
|
|
18
|
+
MAX_PAYLOAD = 65_535
|
|
19
|
+
MAX_STREAM_ID = 0xffff_ffff
|
|
20
|
+
|
|
21
|
+
# 持有不可变载荷,使用七字节网络序帧头;在 pack 前校验范围,
|
|
22
|
+
# 避免非法数值在线协议中被静默截断。
|
|
23
|
+
Frame = Data.define(:command, :stream_id, :payload) do
|
|
24
|
+
def initialize(command:, stream_id:, payload: "".b)
|
|
25
|
+
raise ArgumentError, "command must be a byte" unless command.is_a?(Integer) && (0..255).cover?(command)
|
|
26
|
+
unless stream_id.is_a?(Integer) && (0..MAX_STREAM_ID).cover?(stream_id)
|
|
27
|
+
raise ArgumentError, "stream ID must be an unsigned 32-bit integer"
|
|
28
|
+
end
|
|
29
|
+
raise TypeError, "payload must be a String" unless payload.is_a?(String)
|
|
30
|
+
raise ArgumentError, "frame payload exceeds 65535 bytes" if payload.bytesize > MAX_PAYLOAD
|
|
31
|
+
|
|
32
|
+
super(command:, stream_id:, payload: payload.b.freeze)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def encode
|
|
36
|
+
[command, stream_id, payload.bytesize].pack("CNn") + payload
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnyTLS
|
|
4
|
+
module Protocol
|
|
5
|
+
# 换行分隔的 UTF-8 键值编解码;保留未知字段用于协商扩展。
|
|
6
|
+
# 格式非法、重复或超长条目直接拒绝,不做隐式修正。
|
|
7
|
+
module Settings
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def encode(settings)
|
|
11
|
+
raise TypeError, "settings must be a Hash" unless settings.is_a?(Hash)
|
|
12
|
+
|
|
13
|
+
result = settings.map do |key, value|
|
|
14
|
+
key = key.to_s if key.is_a?(Symbol)
|
|
15
|
+
validate_pair!(key, value)
|
|
16
|
+
"#{key}=#{value}"
|
|
17
|
+
end.join("\n")
|
|
18
|
+
raise ProtocolError, "settings exceed frame limit" if result.bytesize > MAX_PAYLOAD
|
|
19
|
+
|
|
20
|
+
result.b
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def decode(bytes)
|
|
24
|
+
raise TypeError, "settings must be a String" unless bytes.is_a?(String)
|
|
25
|
+
raise ProtocolError, "settings exceed frame limit" if bytes.bytesize > MAX_PAYLOAD
|
|
26
|
+
|
|
27
|
+
text = bytes.dup.force_encoding(Encoding::UTF_8)
|
|
28
|
+
raise ProtocolError, "settings must be UTF-8" unless text.valid_encoding?
|
|
29
|
+
|
|
30
|
+
text.split("\n").each_with_object({}) do |line, settings|
|
|
31
|
+
key, value = line.split("=", 2)
|
|
32
|
+
validate_pair!(key, value)
|
|
33
|
+
raise ProtocolError, "duplicate setting" if settings.key?(key)
|
|
34
|
+
|
|
35
|
+
settings[key.freeze] = value.freeze
|
|
36
|
+
end.freeze
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def validate_pair!(key, value)
|
|
40
|
+
unless key.is_a?(String) && key.match?(/\A[a-zA-Z0-9_-]+\z/) && value.is_a?(String)
|
|
41
|
+
raise ProtocolError, "invalid settings entry"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
utf8 = value.dup.force_encoding(Encoding::UTF_8)
|
|
45
|
+
unless utf8.valid_encoding? && !utf8.match?(/[\x00-\x1f\x7f]/)
|
|
46
|
+
raise ProtocolError, "invalid settings value"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private_class_method :validate_pair!
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
module AnyTLS
|
|
6
|
+
module Protocol
|
|
7
|
+
# 流目标地址和 UoT 握手共用的 SOCKS 地址编解码。
|
|
8
|
+
# read 只消费一个地址;decode 还会拒绝地址后的多余字节。
|
|
9
|
+
module SocksAddress
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def encode(address)
|
|
13
|
+
raise TypeError, "address must be an AnyTLS::Address" unless address.is_a?(Address)
|
|
14
|
+
|
|
15
|
+
host = if address.ipv4?
|
|
16
|
+
"\x01".b + IPAddr.new(address.host).hton
|
|
17
|
+
elsif address.ipv6?
|
|
18
|
+
"\x04".b + IPAddr.new(address.host).hton
|
|
19
|
+
else
|
|
20
|
+
[3, address.host.bytesize].pack("CC") + address.host.b
|
|
21
|
+
end
|
|
22
|
+
host + [address.port].pack("n")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def read(io, timeout: nil)
|
|
26
|
+
deadline = Deadline.new(timeout)
|
|
27
|
+
reader = ->(length) { IOHelpers.read_exact(io, length, timeout: deadline.remaining) }
|
|
28
|
+
type = reader.call(1).unpack1("C")
|
|
29
|
+
host = case type
|
|
30
|
+
when 1 then IPAddr.new_ntoh(reader.call(4)).to_s
|
|
31
|
+
when 4 then IPAddr.new_ntoh(reader.call(16)).to_s
|
|
32
|
+
when 3
|
|
33
|
+
length = reader.call(1).unpack1("C")
|
|
34
|
+
raise ProtocolError, "empty destination domain" if length.zero?
|
|
35
|
+
|
|
36
|
+
reader.call(length)
|
|
37
|
+
else
|
|
38
|
+
raise ProtocolError, "unsupported address type"
|
|
39
|
+
end
|
|
40
|
+
port = reader.call(2).unpack1("n")
|
|
41
|
+
begin
|
|
42
|
+
Address.new(host:, port:)
|
|
43
|
+
rescue ArgumentError => error
|
|
44
|
+
raise ProtocolError, "invalid destination address", cause: error
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def decode(bytes)
|
|
49
|
+
raise TypeError, "address must be a String" unless bytes.is_a?(String)
|
|
50
|
+
|
|
51
|
+
io = StringIO.new(bytes.b)
|
|
52
|
+
address = read(io)
|
|
53
|
+
raise ProtocolError, "trailing bytes after destination address" unless io.eof?
|
|
54
|
+
|
|
55
|
+
address
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|