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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +4 -1
- data/docs/ARCHITECTURE.md +7 -0
- data/docs/VALIDATION.md +1 -1
- data/lib/anytls/buffered_io.rb +31 -39
- data/lib/anytls/byte_buffer.rb +2 -0
- data/lib/anytls/protocol/decoder.rb +4 -1
- data/lib/anytls/uot/connection.rb +10 -5
- data/lib/anytls/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 21faa499b108b3acc3a415299b217b68af2227f1325759c696540780fe549d23
|
|
4
|
+
data.tar.gz: 3a366b73c675a95347ebfdc05431708e65caddaebd1aed09e654dc3756cb5a10
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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
|
|
data/lib/anytls/buffered_io.rb
CHANGED
|
@@ -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 =
|
|
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 <<
|
|
37
|
+
data << read_chunk(length - data.bytesize, deadline) while data.bytesize < length
|
|
35
38
|
else
|
|
36
|
-
loop { data <<
|
|
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 =
|
|
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, @
|
|
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
|
-
|
|
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, @
|
|
137
|
+
@buffer << read_underlying([length - @buffer.bytesize, 4096].max, @deadline) while @buffer.bytesize < length
|
|
138
138
|
end
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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)
|
data/lib/anytls/byte_buffer.rb
CHANGED
|
@@ -15,11 +15,14 @@ module AnyTLS
|
|
|
15
15
|
frames = []
|
|
16
16
|
offset = 0
|
|
17
17
|
while @buffer.bytesize - offset >= HEADER_SIZE
|
|
18
|
-
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
86
|
+
|
|
87
|
+
deadline.check!
|
|
88
|
+
raise ClosedError, "datagram connection closed" if writing && closed?
|
|
84
89
|
|
|
85
90
|
begin
|
|
86
91
|
yield
|
data/lib/anytls/version.rb
CHANGED