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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +11 -0
  3. data/LICENSE +21 -0
  4. data/README.md +143 -0
  5. data/bin/anytls-client +7 -0
  6. data/bin/anytls-server +7 -0
  7. data/docs/ARCHITECTURE.md +105 -0
  8. data/docs/SOURCES.md +15 -0
  9. data/docs/VALIDATION.md +100 -0
  10. data/docs/validation/runtime-ruby34.json +8 -0
  11. data/docs/validation/runtime-ruby40.json +8 -0
  12. data/docs/validation/stress-c1.json +25 -0
  13. data/docs/validation/stress-c128.json +26 -0
  14. data/docs/validation/stress-c32.json +26 -0
  15. data/docs/validation/stress-sequential-10000.json +26 -0
  16. data/lib/anytls/address.rb +53 -0
  17. data/lib/anytls/buffered_io.rb +185 -0
  18. data/lib/anytls/byte_buffer.rb +61 -0
  19. data/lib/anytls/cli/client_listener.rb +91 -0
  20. data/lib/anytls/cli/options.rb +160 -0
  21. data/lib/anytls/cli/runner.rb +89 -0
  22. data/lib/anytls/cli/shutdown.rb +36 -0
  23. data/lib/anytls/client.rb +129 -0
  24. data/lib/anytls/connection_uri.rb +96 -0
  25. data/lib/anytls/deadline.rb +34 -0
  26. data/lib/anytls/dialer.rb +70 -0
  27. data/lib/anytls/errors.rb +23 -0
  28. data/lib/anytls/inbound/http_proxy.rb +224 -0
  29. data/lib/anytls/inbound/mixed.rb +60 -0
  30. data/lib/anytls/inbound/socks4.rb +55 -0
  31. data/lib/anytls/inbound/socks5.rb +138 -0
  32. data/lib/anytls/io_helpers.rb +87 -0
  33. data/lib/anytls/log.rb +15 -0
  34. data/lib/anytls/padding/scheme.rb +121 -0
  35. data/lib/anytls/padding/writer.rb +55 -0
  36. data/lib/anytls/protocol/authentication.rb +44 -0
  37. data/lib/anytls/protocol/decoder.rb +39 -0
  38. data/lib/anytls/protocol/frame.rb +40 -0
  39. data/lib/anytls/protocol/settings.rb +53 -0
  40. data/lib/anytls/protocol/socks_address.rb +59 -0
  41. data/lib/anytls/relay.rb +49 -0
  42. data/lib/anytls/server.rb +220 -0
  43. data/lib/anytls/session.rb +593 -0
  44. data/lib/anytls/session_pool.rb +166 -0
  45. data/lib/anytls/stream.rb +151 -0
  46. data/lib/anytls/tls_config.rb +76 -0
  47. data/lib/anytls/transport/tls_connection.rb +117 -0
  48. data/lib/anytls/uot/connection.rb +122 -0
  49. data/lib/anytls/uot/server.rb +97 -0
  50. data/lib/anytls/version.rb +6 -0
  51. data/lib/anytls.rb +40 -0
  52. 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