anytls 0.1.5 → 0.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c797edcda4beba1d06a85d522a65ba7c4a3b7079955ea5640cc6f4320078e05b
4
- data.tar.gz: e0d213605c4574257a28a0231b11dee4484db37aae667dda501532955fc4abf3
3
+ metadata.gz: 21faa499b108b3acc3a415299b217b68af2227f1325759c696540780fe549d23
4
+ data.tar.gz: 3a366b73c675a95347ebfdc05431708e65caddaebd1aed09e654dc3756cb5a10
5
5
  SHA512:
6
- metadata.gz: d8d10834c474ffd7e04c325dcf00b187c2b68984af406591b947cc8dd2ec390d7cd425024d24af87d17de62e1e8217926faae0cb12e9cfc55355a54f85609052
7
- data.tar.gz: 7799f998dd9825273c1de1dd045e64d7cfad11816f8dd1d594a105386e312a630c1cbd5ae62951d575079733048b36ca215726ab7119edb987125d9b9c33d4b1
6
+ metadata.gz: a5858b4a20fb03be5498b0fb8a31e2f4f2f1e91427e868fef5a530cb4fafcbca138d6c38afb72052b2a16c91d01c7417c6d2c56efcd0f45cea5b2acf31e5d72f
7
+ data.tar.gz: 6c8f642a7d1a591f0ca48b7cef7f2f2980a847fba4647427f77e1efd9c63d8dcb810a6e53222028c1b980f570aba0399fbaaab26c7d93b468f5aa7c922240cba
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.6 (2026-10-09)
4
+
5
+ - UoT 在远端 FIN 后继续排空完整数据包,保留 EOF 与原始错误;显式关闭 UoT/缓冲 IO 时释放未读尾部。
6
+ - 修复已有 Stream 缓冲绕过读取超时、BufferedIO 部分读取重置预算的问题,非法超时在设置阶段拒绝。
7
+ - 增量解帧在等待载荷片段时复用已解析的帧头,减少碎片化输入下的重复解析与对象分配。
8
+
3
9
  ## 0.1.5 (2026-10-06)
4
10
 
5
11
  - 修复 v1 多流集中报告目标就绪时,操作队列未排空却进入等待导致整条会话超时的问题。
data/README.md CHANGED
@@ -4,7 +4,7 @@ Ruby 3.4+ 的 AnyTLS gem 与命令行客户端、服务端。参考版本固定
4
4
  [`anytls-go` 的 `fd6167a`](https://github.com/anytls/anytls-go/tree/fd6167acd6d73b9fa3e607659951847fbc9e6c50)。
5
5
  核心代码不依赖 Rails。
6
6
 
7
- 当前版本为 `0.1.5`,Ruby 实现使用 [MIT 许可证](LICENSE)。项目及参考代码的许可边界见
7
+ 当前版本为 `0.1.6`,Ruby 实现使用 [MIT 许可证](LICENSE)。项目及参考代码的许可边界见
8
8
  [SOURCES](docs/SOURCES.md)。
9
9
 
10
10
  ## 使用
@@ -72,6 +72,7 @@ end
72
72
  不提供 `to_io` 或真正的 TCP half-close。
73
73
  远端结束后可继续排空接收数据;显式 `close` 会丢弃未读数据,即使远端已经结束。
74
74
  大块写入按最多 65,535 字节分片,每片完成后才提交下一片;排队与发送共用本次 write 的超时预算。
75
+ 读取的超时预算也适用于已有缓冲数据;`read(0)` 仍立即返回空字符串。
75
76
 
76
77
  AnyTLS 没有独立的认证成功回执;首次连接或 v1 peer 可能先返回尚未确认目标连接的
77
78
  Stream。`connection_status` 区分 `:unconfirmed`、`:confirmed`、`:failed`。远端拒绝、认证
@@ -92,6 +93,8 @@ end
92
93
  上限约束,可能以 `EMSGSIZE` 拒绝较大的包;线协议上限不是网络发送保证。
93
94
  SOCKS5 UDP ASSOCIATE 通过同一 UoT 能力实现。
94
95
  转发资源与必需线程全部创建后才报告就绪;准备失败不会先回复成功。
96
+ 远端 FIN 后 `closed?` 为 true,`receive_packet` 仍可排空已完整接收的数据包,随后报告 EOF
97
+ 或原始故障;显式 `close` 会丢弃未读数据。不完整的数据包读取失败后关闭流,不能继续解析。
95
98
 
96
99
  ```ruby
97
100
  tls = AnyTLS::TLSConfig.new(server: true, cert: "fullchain.pem", key: "private-key.pem")
data/docs/ARCHITECTURE.md CHANGED
@@ -28,6 +28,7 @@
28
28
  每个协议帧由 `command:uint8 + stream_id:uint32be + payload_length:uint16be + payload` 组成。
29
29
  增量 Decoder 能处理任意 header/body 分割;不把一次 TLS read 当成一帧。payload 最大
30
30
  65,535 字节,公开 Stream 大 write 在编码前分片。Decoder 先按声明长度完成帧边界解析;
31
+ 完整帧头只解析一次,等待载荷片段时保留该帧头,输出完整帧后再解析下一个。
31
32
  Session 与 Go 参考保持一致:未知命令的空 payload 可忽略,非空 payload 则拒绝并结束 session。
32
33
 
33
34
  Session 先完成 TLS 与认证,再推进 SETTINGS/SYN/PSH 等协议状态。服务端收到 SYN 时分配
@@ -75,6 +76,9 @@ Session 状态锁只保护短操作;锁内不等待网络、不回调调用者
75
76
 
76
77
  `Deadline` 使用 `CLOCK_MONOTONIC`。同一读写操作中的等待消耗同一预算,不在部分进度后
77
78
  重新计时;排队和锁等待也会消耗预算。连接、握手、目标拨号和关闭具有单独边界。
79
+ Stream 在消费已有缓冲前也检查截止时间,已排空的流继续报告原 EOF/错误,零长度 read 不消费预算。
80
+ BufferedIO 复用 `Deadline` 与 `IOHelpers.read_partial`;一次 read 的所有片段使用同一截止时间,
81
+ 并受握手阶段截止时间约束。预读字节不能绕过阶段预算,非法超时不会覆盖现有配置。
78
82
 
79
83
  | 限制 | 当前默认 |
80
84
  | --- | --- |
@@ -116,6 +120,9 @@ UoT 与 SOCKS5 UDP association 使用启动门:全部必需线程取得后才
116
120
  创建失败则唤醒尚未启动的线程退出、回收 socket/stream,并报告失败。后续延迟创建地址族
117
121
  资源失败也会结束转发并触发统一清理。UoT 非固定模式在每条流内最多保留一个数字 IP 与
118
122
  端口对应的解析结果,目标改变时更新;域名结果不缓存,继续逐包解析。
123
+ UoT 的串行读取门不把底层 `closed?` 当成数据耗尽:FIN 后仍可排空完整数据包,再报告 EOF
124
+ 或底层错误;写入门继续拒绝关闭后的写入。UoT 和 BufferedIO 显式关闭时始终委托底层幂等 close,
125
+ 确保已远端结束的 Stream 尾部也被释放。BufferedIO 同时丢弃自己的预读缓冲。
119
126
 
120
127
  同一 TCP 连接天然存在队首阻塞;有限缓冲只能限制 Ruby 自身的资源占用和慢消费者影响。
121
128
  当前没有扩展协议增加 per-stream credit,不承诺无限慢消费者仍可无损持续接收。
data/docs/VALIDATION.md CHANGED
@@ -25,7 +25,7 @@
25
25
 
26
26
  CI 的 Ubuntu / Ruby 4.0 任务在验证通过后上传 `anytls-gem` artifact,包含 gem 和 `SHA256SUMS`。
27
27
  发布前须确认同一提交的完整 Ruby 矩阵和 Go 互通任务均通过,下载该 artifact 并核对校验和。
28
- 运行 `bundle exec ruby script/verify_package.rb /path/to/anytls-0.1.5.gem` 可验证已有包与当前源码
28
+ 运行 `bundle exec ruby script/verify_package.rb /path/to/anytls-0.1.6.gem` 可验证已有包与当前源码
29
29
  的文件内容、元数据和依赖一致,并隔离安装该包;提供路径时不会重新构建 gem。
30
30
  GitHub Release 和 RubyGems 必须使用这一份验证过的包,发布后下载两处产物并核对 SHA256。
31
31
 
@@ -17,11 +17,13 @@ module AnyTLS
17
17
  # 在协议阶段边界重新设定绝对读取预算;握手完成后清除预算,
18
18
  # 允许已建立的隧道持续空闲。
19
19
  def timeout=(value)
20
+ deadline = Deadline.new(value)
20
21
  @timeout = value
21
- @deadline = value && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + value)
22
+ @deadline = deadline
22
23
  end
23
24
 
24
25
  def peek(length)
26
+ @deadline.check!
25
27
  fill(length)
26
28
  @buffer.byteslice(0, length)
27
29
  end
@@ -29,11 +31,12 @@ module AnyTLS
29
31
  def read(length = nil, outbuf = nil, timeout: @timeout)
30
32
  raise ArgumentError, "negative length" if length&.negative?
31
33
 
34
+ deadline = read_deadline(timeout)
32
35
  data = +"".b
33
36
  if length
34
- data << readpartial(length - data.bytesize, timeout: timeout) while data.bytesize < length
37
+ data << read_chunk(length - data.bytesize, deadline) while data.bytesize < length
35
38
  else
36
- loop { data << readpartial(16_384, timeout: timeout) }
39
+ loop { data << read_chunk(16_384, deadline) }
37
40
  end
38
41
  outbuf ? outbuf.replace(data) : data
39
42
  rescue EOFError
@@ -49,13 +52,7 @@ module AnyTLS
49
52
  raise ArgumentError, "negative length" if length.negative?
50
53
  return outbuf ? outbuf.replace("") : +"".b if length.zero?
51
54
 
52
- data = if @buffer.empty?
53
- read_underlying(length, timeout)
54
- else
55
- @buffer.slice!(0, length)
56
- end
57
- raise EOFError if data.nil? || data.empty?
58
-
55
+ data = read_chunk(length, read_deadline(timeout))
59
56
  outbuf ? outbuf.replace(data) : data
60
57
  rescue EOFError
61
58
  outbuf&.clear
@@ -66,6 +63,7 @@ module AnyTLS
66
63
  raise ArgumentError, "separator must not be empty" if separator.nil? || separator.empty?
67
64
 
68
65
  loop do
66
+ @deadline.check!
69
67
  if (index = @buffer.index(separator))
70
68
  size = index + separator.bytesize
71
69
  raise BufferLimitExceeded, "HTTP line exceeds limit" if size > limit
@@ -74,7 +72,7 @@ module AnyTLS
74
72
  end
75
73
  raise BufferLimitExceeded, "HTTP line exceeds limit" if @buffer.bytesize >= limit
76
74
 
77
- @buffer << read_underlying([4096, limit - @buffer.bytesize].min, @timeout)
75
+ @buffer << read_underlying([4096, limit - @buffer.bytesize].min, @deadline)
78
76
  end
79
77
  rescue EOFError
80
78
  return nil if @buffer.empty?
@@ -100,7 +98,9 @@ module AnyTLS
100
98
  end
101
99
 
102
100
  def close
103
- IOHelpers.close(@io) unless @io.closed?
101
+ @buffer.clear
102
+ # 远端 FIN 只结束底层流;显式关闭仍须委托它释放尚未读取的尾部。
103
+ IOHelpers.close(@io)
104
104
  rescue IOError, SystemCallError
105
105
  nil
106
106
  end
@@ -134,37 +134,29 @@ module AnyTLS
134
134
  private
135
135
 
136
136
  def fill(length)
137
- @buffer << read_underlying([length - @buffer.bytesize, 4096].max, @timeout) while @buffer.bytesize < length
137
+ @buffer << read_underlying([length - @buffer.bytesize, 4096].max, @deadline) while @buffer.bytesize < length
138
138
  end
139
139
 
140
- def read_underlying(length, timeout)
141
- if @deadline
142
- remaining = @deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
143
- raise TimeoutError, "read timed out" unless remaining.positive?
140
+ # 一次 read 与整个握手阶段各有预算,选择更早的截止时间并贯穿所有片段。
141
+ def read_deadline(timeout)
142
+ deadline = Deadline.new(timeout)
143
+ remaining = @deadline.remaining
144
+ remaining && (timeout.nil? || remaining <= timeout) ? @deadline : deadline
145
+ end
144
146
 
145
- timeout = timeout ? [timeout, remaining].min : remaining
146
- end
147
- unless @io.respond_to?(:read_nonblock)
148
- return @io.readpartial(length, timeout: timeout)
149
- end
147
+ def read_chunk(length, deadline)
148
+ deadline.check!
149
+ result = @buffer.empty? ? read_underlying(length, deadline) : @buffer.slice!(0, length)
150
+ raise EOFError if result.nil? || result.empty?
150
151
 
151
- deadline = timeout && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout)
152
- loop do
153
- result = @io.read_nonblock(length, exception: false)
154
- case result
155
- when :wait_readable, :wait_writable
156
- remaining = deadline && (deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC))
157
- raise TimeoutError, "read timed out" if remaining && remaining <= 0
158
-
159
- ready = IO.select(result == :wait_readable ? [@io] : nil, result == :wait_writable ? [@io] : nil, nil,
160
- remaining)
161
- raise TimeoutError, "read timed out" unless ready
162
- when nil
163
- raise EOFError
164
- else
165
- return result
166
- end
167
- end
152
+ result
153
+ end
154
+
155
+ def read_underlying(length, deadline)
156
+ result = IOHelpers.read_partial(@io, length, deadline: deadline)
157
+ raise EOFError if result.nil? || result.empty?
158
+
159
+ result
168
160
  end
169
161
 
170
162
  def account_line(line)
@@ -83,6 +83,8 @@ module AnyTLS
83
83
 
84
84
  raise EOFError, "end of stream"
85
85
  end
86
+ # 已有缓冲也要计入排队和部分读取的总预算;超时不能继续消费字节。
87
+ deadline.check!
86
88
  count = [length, @size].min
87
89
  result = nil
88
90
  remaining = count
@@ -15,11 +15,14 @@ module AnyTLS
15
15
  frames = []
16
16
  offset = 0
17
17
  while @buffer.bytesize - offset >= HEADER_SIZE
18
- command, stream_id, length = @buffer.unpack("CNn", offset:)
18
+ # 载荷可以拆成任意小片段;完整头部只解析一次,直到整帧到齐。
19
+ @header ||= @buffer.unpack("CNn", offset:)
20
+ command, stream_id, length = @header
19
21
  break if @buffer.bytesize - offset < HEADER_SIZE + length
20
22
 
21
23
  frames << Frame.new(command:, stream_id:, payload: @buffer.byteslice(offset + HEADER_SIZE, length))
22
24
  offset += HEADER_SIZE + length
25
+ @header = nil
23
26
  end
24
27
  @buffer = @buffer.byteslice(offset..) if offset.positive?
25
28
  frames
@@ -27,7 +27,7 @@ module AnyTLS
27
27
  IOHelpers.write_all(stream, request, timeout: timeout)
28
28
  end
29
29
  rescue StandardError
30
- stream.close unless stream.closed?
30
+ stream.close
31
31
  raise
32
32
  end
33
33
 
@@ -53,12 +53,15 @@ module AnyTLS
53
53
 
54
54
  bytes = @destination ? +"".b : encode_address(target)
55
55
  bytes << [payload.bytesize].pack("n") << payload
56
- with_gate(@write_gate, deadline) { IOHelpers.write_all(@stream, bytes, timeout: deadline.remaining) }
56
+ with_gate(@write_gate, deadline, writing: true) do
57
+ IOHelpers.write_all(@stream, bytes, timeout: deadline.remaining)
58
+ end
57
59
  payload.bytesize
58
60
  end
59
61
 
60
62
  def receive_packet(timeout: nil)
61
63
  deadline = Deadline.new(timeout)
64
+ # FIN 后仍可排空完整数据包;EOF 和原始故障由底层读取报告。
62
65
  with_gate(@read_gate, deadline) do
63
66
  destination = @destination || read_address(deadline)
64
67
  length = IOHelpers.read_exact(@stream, 2, timeout: deadline.remaining).unpack1("n")
@@ -69,18 +72,20 @@ module AnyTLS
69
72
  end
70
73
 
71
74
  def close
72
- IOHelpers.close(@stream) unless @stream.closed?
75
+ IOHelpers.close(@stream)
73
76
  end
74
77
 
75
78
  def closed? = @stream.closed?
76
79
 
77
80
  private
78
81
 
79
- def with_gate(gate, deadline)
82
+ def with_gate(gate, deadline, writing: false)
80
83
  deadline.check!
81
84
  acquired = gate.pop(timeout: deadline.remaining)
82
85
  raise TimeoutError, "datagram operation timed out waiting for another operation" unless acquired
83
- raise ClosedError, "datagram connection closed" if closed?
86
+
87
+ deadline.check!
88
+ raise ClosedError, "datagram connection closed" if writing && closed?
84
89
 
85
90
  begin
86
91
  yield
@@ -2,5 +2,5 @@
2
2
 
3
3
  module AnyTLS
4
4
  # gemspec、命令行和对端标识共用的唯一版本来源。
5
- VERSION = "0.1.5"
5
+ VERSION = "0.1.6"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anytls
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - AnyTLS Ruby contributors