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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 56a137e1d309ae5f93c6d0766cac36bd25d0f6b7246a41dc91de7b07c1ae3c9d
4
+ data.tar.gz: 2051953545edec2a5fda7709f942b79f6eb1adc6c882aae2d6e645db9e464af7
5
+ SHA512:
6
+ metadata.gz: 9d2e182c0ea5fd15b42b6c4032e9462ade916cc4863a7295acd4d495ec004a9ff23998bf2808bc6bdcb2a2e5b6d75ab2199859eb7b8b407032a7ec1b286d680b
7
+ data.tar.gz: 28a8faeb2f4948725d016321bed77ab8a19752f3f40a0279c14603361864b028c854063c0e68af8690d2e7cf9363cc30ae942c90cbdbbe9075098ba06252fc76
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-10-01)
4
+
5
+ - Initial Ruby implementation of AnyTLS, a gem API and client/server command-line programs.
6
+ - TLS certificate verification is enabled by default; explicit CA files use an isolated trust store.
7
+ - Explicit resource limits and local TLS/cross-language validation entry points.
8
+ - Socket shutdown wakes blocked readers before closing, including on Linux.
9
+ - HTTP Upgrade requests with a body are rejected instead of silently dropping the body.
10
+ - Invalid timeout values are rejected before dialing, and invalid padding does not leave wake sockets open.
11
+ - The independent Ruby implementation is available under the MIT license.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AnyTLS Ruby contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # AnyTLS Ruby
2
+
3
+ Ruby 3.4+ 的 AnyTLS gem 与命令行客户端、服务端。参考版本固定为
4
+ [`anytls-go` 的 `fd6167a`](https://github.com/anytls/anytls-go/tree/fd6167acd6d73b9fa3e607659951847fbc9e6c50)。
5
+ 核心代码不依赖 Rails。
6
+
7
+ 当前版本为 `0.1.0`,Ruby 实现使用 [MIT 许可证](LICENSE)。项目及参考代码的许可边界见
8
+ [SOURCES](docs/SOURCES.md)。
9
+
10
+ ## 使用
11
+
12
+ 安装 gem 后可直接运行两个命令:
13
+
14
+ ```sh
15
+ gem install anytls
16
+ anytls-client --help
17
+ anytls-server --help
18
+ ```
19
+
20
+ 从源码运行:
21
+
22
+ ```sh
23
+ bundle install
24
+ bundle exec ruby -Ilib bin/anytls-server --help
25
+ bundle exec ruby -Ilib bin/anytls-client --help
26
+ ```
27
+
28
+ 服务端默认监听 `0.0.0.0:8443`,正常部署显式提供证书和私钥:
29
+
30
+ ```sh
31
+ bundle exec ruby -Ilib bin/anytls-server \
32
+ --password-file /path/to/password \
33
+ --cert /path/to/fullchain.pem --key /path/to/private-key.pem
34
+ ```
35
+
36
+ 客户端默认在 `127.0.0.1:1080` 提供混合 SOCKS4/4a、SOCKS5、HTTP 代理:
37
+
38
+ ```sh
39
+ bundle exec ruby -Ilib bin/anytls-client \
40
+ --server proxy.example.com:8443 --password-file /path/to/password
41
+ ```
42
+
43
+ 客户端默认验证 TLS 证书与主机名。`--sni` 设置验证名称和 SNI,`--ca-file`
44
+ 提供自有 CA。开发环境可让服务端使用 `--self-signed`,客户端需要显式选择
45
+ `--insecure` 或信任该证书;关闭验证不会成为默认行为。
46
+
47
+ `--server` 也接受 `anytls://PASSWORD@HOST:PORT/?sni=NAME&insecure=1#LABEL`。
48
+ 密码中的保留字符应百分号编码;显式命令行参数优先于 URI,URI 密码优先于
49
+ `ANYTLS_PASSWORD` 环境变量。`--password` 与 `--password-file` 互斥。密码文件只移除
50
+ 末尾一次 LF/CRLF,保留密码自身的空格。诊断信息不回显密码、完整连接 URI 或 payload。
51
+
52
+ 兼容参考程序的 `-l`、`-s`、`-p`、`-m`、`-sni`、`-dr` 和 `-padding-scheme`。
53
+ `-dr`/`--disable-reuse` 是诊断选项,每个连接使用新 session。
54
+ INT/TERM 经唤醒 socket 交给正常线程处理,停止监听后清理现有连接。
55
+
56
+ ## Ruby API
57
+
58
+ ```ruby
59
+ require "anytls"
60
+
61
+ AnyTLS::Client.open(host: "proxy.example.com", port: 8443, password: ENV.fetch("ANYTLS_PASSWORD")) do |client|
62
+ client.connect("example.com", 80, timeout: 5) do |stream|
63
+ stream.write("GET / HTTP/1.0\r\nHost: example.com\r\n\r\n", timeout: 5)
64
+ puts stream.readpartial(4096, timeout: 5)
65
+ end
66
+ end
67
+ ```
68
+
69
+ `Stream#readpartial` 返回二进制 String,正常结束抛 `EOFError`;`read(length)` 遵循指定
70
+ 长度的 Ruby IO 读取习惯;`write` 成功返回写入字节数。写入成功只表示完成本地 TLS
71
+ 发送,不证明目标应用已处理。错误或超时后的数据不自动重放。Stream 没有独立 fd,
72
+ 不提供 `to_io` 或真正的 TCP half-close。
73
+
74
+ AnyTLS 没有独立的认证成功回执;首次连接或 v1 peer 可能先返回尚未确认目标连接的
75
+ Stream。`connection_status` 区分 `:unconfirmed`、`:confirmed`、`:failed`。远端拒绝、认证
76
+ 失败和连接关闭也可能在后续读写体现,不能仅凭创建 Stream 就声称远端目标已连接。
77
+
78
+ UDP-over-TCP v2 支持固定目标与逐包目标:
79
+
80
+ ```ruby
81
+ target = AnyTLS::Address.new(host: "127.0.0.1", port: 5353)
82
+ client.open_datagrams(destination: target, timeout: 5) do |datagrams|
83
+ datagrams.send_packet("request".b, timeout: 5)
84
+ payload, source = datagrams.receive_packet(timeout: 5)
85
+ end
86
+ ```
87
+
88
+ 省略固定 `destination:` 时,每次 `send_packet` 必须提供 `destination:`。UoT 的 16 位长度
89
+ 字段可承载最多 65,535 字节,空 datagram 有效。实际 UDP socket 还受地址族与系统发送
90
+ 上限约束,可能以 `EMSGSIZE` 拒绝较大的包;线协议上限不是网络发送保证。
91
+ SOCKS5 UDP ASSOCIATE 通过同一 UoT 能力实现。
92
+
93
+ ```ruby
94
+ tls = AnyTLS::TLSConfig.new(server: true, cert: "fullchain.pem", key: "private-key.pem")
95
+ server = AnyTLS::Server.new(password: ENV.fetch("ANYTLS_PASSWORD"), tls: tls)
96
+ server.run # 阻塞;由其他正常线程调用 server.stop(grace: 5)
97
+ ```
98
+
99
+ 构造 Client 不拨号;首次连接时才启动会话池。`min_idle_sessions` 是回收时保留已有空闲
100
+ session 的数量,不会主动创建连接填满池。默认最多 128 个 session,空闲超时与检查间隔
101
+ 各 30 秒。客户端租用空闲 session;并发请求需要时建立新的 session。服务端默认最多
102
+ 256 个活动 stream。`Client#stats`、`Server#stats` 返回冻结的计数快照。
103
+
104
+ ## 功能与边界
105
+
106
+ 支持 AnyTLS 帧、设置协商、目标连接报告、会话复用、heartbeat、padding 更新和 UDP-over-TCP v2;
107
+ 混合代理支持 SOCKS4/4a CONNECT、SOCKS5 CONNECT/UDP ASSOCIATE、HTTP CONNECT、普通 HTTP
108
+ 转发、keep-alive、chunked body 和 HTTP Upgrade。SOCKS BIND、SOCKS 用户名密码认证、
109
+ SOCKS UDP 分片和 UoT v1 不在当前接口范围。
110
+
111
+ HTTP 转发使用 `protocol-http1` 解析消息边界,拒绝冲突 Content-Length/Transfer-Encoding
112
+ 及重复 Host。带请求体的 HTTP Upgrade 返回 400,避免丢弃消息体;无体 Upgrade 继续支持。
113
+ HTTPS 目标使用 CONNECT。代理客户端监听仅默认绑定回环地址;扩大监听
114
+ 范围时由部署环境限制访问者。
115
+
116
+ 每条 TLS 连接只有一个 owner Thread。协议接收按 stream/session 字节预算限流;慢消费者
117
+ 超过预算会被关闭,避免无限增长或阻塞其他 stream。TLS/TCP 本身的队首阻塞仍然存在。
118
+ 设计和状态边界见 [ARCHITECTURE](docs/ARCHITECTURE.md)。
119
+
120
+ ## 验证
121
+
122
+ ```sh
123
+ bundle exec rake test # 单元、真实本地 TLS、CLI;跨语言测试独立执行
124
+ bundle exec rake lint
125
+ bundle exec rake package # 临时 GEM_HOME 中离线安装依赖和构建包,再检查 require/CLI
126
+ bundle exec rake stress # 有界本地 TLS 并发场景,打印测量结果并检查线程回收
127
+ ```
128
+
129
+ 跨语言验收需自行取得固定 commit 的参考源码并构建;构建物不进入 Ruby gem:
130
+
131
+ ```sh
132
+ git clone https://github.com/anytls/anytls-go.git /tmp/anytls-go
133
+ git -C /tmp/anytls-go checkout fd6167acd6d73b9fa3e607659951847fbc9e6c50
134
+ cd /tmp/anytls-go
135
+ go build -o /tmp/anytls-go-client ./cmd/client
136
+ go build -o /tmp/anytls-go-server ./cmd/server
137
+ cd /path/to/anytls-ruby
138
+ ANYTLS_GO_CLIENT=/tmp/anytls-go-client ANYTLS_GO_SERVER=/tmp/anytls-go-server \
139
+ ANYTLS_REQUIRE_INTEROP=1 bundle exec rake interop
140
+ ```
141
+
142
+ CI 配置覆盖 Ruby 3.4/4.0 × Linux/macOS,以及固定 Go 参考双向互通。
143
+ 配置存在不等于远端 CI 已执行;验证证据与运行命令见 [VALIDATION](docs/VALIDATION.md)。
data/bin/anytls-client ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "anytls"
5
+ require "anytls/cli/runner"
6
+
7
+ exit AnyTLS::CLI::Runner.run(:client)
data/bin/anytls-server ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "anytls"
5
+ require "anytls/cli/runner"
6
+
7
+ exit AnyTLS::CLI::Runner.run(:server)
@@ -0,0 +1,105 @@
1
+ # 模块、所有权与协议边界
2
+
3
+ ## 分层
4
+
5
+ | 层 | 类型 | 责任 |
6
+ | --- | --- | --- |
7
+ | 公共入口 | `Client`、`Server` | 连接生命周期、块形式资源管理、目标拨号与入站接入 |
8
+ | 复用 | `SessionPool` | 按创建序号选择最新空闲 session,限制连接数,回收空闲 session |
9
+ | 协议运行 | `Session` | 唯一 owner Thread、帧 dispatch、stream 状态、设置与 padding 协商 |
10
+ | 虚拟 IO | `Stream`、`ByteBuffer` | 同步读写 API、串行化写调用、有界接收队列、EOF 与错误传播 |
11
+ | TLS | `TLSConfig`、`Transport::TLSConnection` | TLS 参数、证书校验、握手、认证字节、非阻塞读写与重试 |
12
+ | 协议值与编码 | `Address`、`Protocol::*` | 网络序字节编码、长度与类型校验、增量解帧、目标地址、设置 |
13
+ | Padding | `Padding::Scheme`、`Padding::Writer` | 不可变原始方案及 MD5 标识、有限规则、发送片段与 WASTE |
14
+ | UDP | `UoT::Connection`、`UoT::Server` | UoT v2 握手与包边界、UDP socket 资源 |
15
+ | 入站应用 | `Inbound::Mixed/Socks4/Socks5/HTTPProxy` | SOCKS/HTTP 握手、目标解析、协议回复、UDP association |
16
+ | IO 支撑 | `Deadline`、`IOHelpers`、`BufferedIO`、`Dialer`、`Relay` | 单调时间预算、有限缓冲、真实 TCP IO 和双向搬运 |
17
+ | 交付 | `CLI::Options/Runner/ClientListener/Shutdown` | 参数、秘密来源、监听限制、信号转发与退出码 |
18
+
19
+ 核心仅占 `AnyTLS` 命名空间;显式 `require_relative` 加载,不 monkey-patch Ruby 类。
20
+ 构造配置对象和加载 gem 不监听端口或拨号。协议对象保持普通 Ruby 类和少量不可变值,
21
+ 不模拟 Go interface、goroutine/channel,也不引入 ActiveSupport。
22
+
23
+ ## 线协议与状态
24
+
25
+ 认证前导为密码 SHA-256 的 32 字节摘要、2 字节 padding 长度及 padding。摘要是原始字节,
26
+ 不是 hex 字符串。认证前导必须在 TLS 内传输。
27
+
28
+ 每个协议帧由 `command:uint8 + stream_id:uint32be + payload_length:uint16be + payload` 组成。
29
+ 增量 Decoder 能处理任意 header/body 分割;不把一次 TLS read 当成一帧。payload 最大
30
+ 65,535 字节,公开 Stream 大 write 在编码前分片。Decoder 先按声明长度完成帧边界解析;
31
+ Session 与 Go 参考保持一致:未知命令的空 payload 可忽略,非空 payload 则拒绝并结束 session。
32
+
33
+ Session 先完成 TLS 与认证,再推进 SETTINGS/SYN/PSH 等协议状态。服务端收到 SYN 时分配
34
+ Stream,并将目标地址的读取与拨号交给有限 worker。v2 的 SYNACK 表达目标连接结果,
35
+ 不是独立的身份认证回执;v1 或尚未获知 peer 版本时保留 `unconfirmed` 状态。
36
+ FIN 结束整个虚拟 stream,不提供 read/write 两侧独立关闭语义。
37
+
38
+ Client pool 同一时刻将一个 session 租给一条业务 stream,完成后将其归还并复用。
39
+ 服务端 Session 解码器支持多条 stream 的帧,入站数量另受 session 和 server 限制。
40
+ 旧空闲连接检测、心跳和 peer stream-ID 限制都在 session 生命周期中处理,不由应用代理层重试。
41
+
42
+ ## 线程与 TLS 所有权
43
+
44
+ Session owner 唯一持有 TLS IO 使用权;应用线程通过同步提交请求与 completion 等待写入结果。
45
+ owner 运行具体的 AnyTLS 事件循环:处理请求、选择待发片段、推进 TLS write/read、dispatch
46
+ 完整帧、根据方向和最近 deadline 等待 TLS fd 或 wakeup socket。它不是通用调度器。
47
+
48
+ 同一 `SSLSocket` 不由 reader/writer 两线程直接共用。待重试的 `SSL_write` 保持完全相同
49
+ 的数据和长度;只有成功返回正整数后才推进偏移。write 返回 WANT_WRITE 时,下一次 TLS
50
+ IO 只能重试该 write;不能插入 read。write 返回 WANT_READ 时可以读,但下一笔应用 write
51
+ 仍需等待当前 write 完成。[OpenSSL 维护者说明](https://github.com/openssl/openssl/discussions/30934)
52
+ 和 [SSL_write 文档](https://docs.openssl.org/3.0/man3/SSL_write/) 是此约束依据。
53
+
54
+ 握手、认证与关闭也归 owner。取消通过 wakeup socket,而不是向线程注入异常。
55
+ 发生发送超时后关闭整个 session,不跳过半帧或透明重放请求。
56
+ 证书 Context 配置完成后冻结,不能在其他连接使用时修改。
57
+
58
+ Session 状态锁只保护短操作;锁内不等待网络、不回调调用者代码。Stream 写调用串行化,
59
+ 一个写调用内的帧保持顺序。双向 Relay 使用有限的线程,并在一端结束时关闭另一端以唤醒等待。
60
+
61
+ ## 时间预算与资源限制
62
+
63
+ `Deadline` 使用 `CLOCK_MONOTONIC`。同一读写操作中的等待消耗同一预算,不在部分进度后
64
+ 重新计时;排队和锁等待也会消耗预算。连接、握手、目标拨号和关闭具有单独边界。
65
+
66
+ | 限制 | 当前默认 |
67
+ | --- | --- |
68
+ | 客户端/服务端 TLS session 数 | 128 |
69
+ | 服务端活动业务 stream 数 | 256 |
70
+ | 单 session stream 数 | 32 |
71
+ | 单 stream 接收缓冲 | 256 KiB |
72
+ | 单 session 接收缓冲 | 4 MiB |
73
+ | 单 session 提交发送预算 | 1 MiB,计入编码和 padding 预留 |
74
+ | Stream 数据写片段 | 16 KiB |
75
+ | UoT 包长度字段 | 65,535 字节;实际 UDP socket 另受地址族/系统大小限制 |
76
+ | CLI 混合入站连接 | 256 |
77
+
78
+ 接收 owner 不能阻塞等待单个 Stream 消费。超过接收预算时终止对应 stream,并通知 peer;
79
+ 控制帧预算也不能无限增长。发送预算不足时让提交者等待。限制按 bytes 计费,不能只用
80
+ 队列条目数代表内存大小。上限约束 Ruby 可见缓冲,不宣称它是进程 RSS 的精确上界。
81
+
82
+ 同一 TCP 连接天然存在队首阻塞;有限缓冲只能限制 Ruby 自身的资源占用和慢消费者影响。
83
+ 当前没有扩展协议增加 per-stream credit,不承诺无限慢消费者仍可无损持续接收。
84
+
85
+ ## 秘密、错误与清理
86
+
87
+ Client 默认验证证书和目标名称;`insecure: true` 必须显式选择。服务端必须有证书与私钥,
88
+ 或显式选择开发用途的临时自签名证书。配置加载错误不回显密码、URI 或私钥内容。
89
+
90
+ 参数错误使用 `ArgumentError`/`ConfigurationError`,协议和传输错误使用 `AnyTLS::Error`
91
+ 下的具体类型。正常 EOF 与故障分开;成功写入不是目标应用确认。`inspect` 和 CLI 日志不输出秘密。
92
+
93
+ `close` 可重复调用。`ensure` 负责 socket、session、stream 与线程回收,保留原始业务错误。
94
+ INT/TERM trap 只向 pipe 写字节;实际清理在正常线程执行。没有 gem publish、在线下载设备配置
95
+ 或全局代理设置等隐藏副作用。
96
+
97
+ 服务端 `stop(grace:)` 先停止监听,允许已经建立的 stream 在宽限期继续传输;到期再关闭
98
+ 剩余 session/目标 socket 并等待 worker 退出。并发 `stop` 服从第一次停止请求确定的边界,
99
+ listener 退出时的清理不能提前缩短宽限期。关闭单个资源失败也必须继续释放剩余资源与计数。
100
+
101
+ ## 可扩展边界
102
+
103
+ 未来如果实际并发测量证明 native threads 不合适,可以整体迁移到单一 Async owner task;
104
+ 线协议 codec、Padding、Stream 契约与 TLS 重试不变量继续保留。当前不维护两套运行时、
105
+ 不自建插件注册系统、不将 HTTP/2 flow control 状态复制到 AnyTLS。
data/docs/SOURCES.md ADDED
@@ -0,0 +1,15 @@
1
+ # Sources and licensing
2
+
3
+ The protocol reference is [anytls/anytls-go](https://github.com/anytls/anytls-go),
4
+ reviewed at commit `fd6167a`. This project is an independent Ruby implementation.
5
+ The upstream repository did not provide a license grant in the reviewed checkout.
6
+ This notice does not assign an upstream license or grant permission to redistribute
7
+ upstream source or binaries. Cross-language tests obtain the reference separately.
8
+
9
+ The independent Ruby implementation is released under the MIT license in
10
+ [`LICENSE`](../LICENSE). This license applies to the Ruby project only and does not
11
+ grant rights to the upstream Go source or binaries. Building and testing the gem
12
+ does not publish it.
13
+
14
+ The implementation uses the Ruby OpenSSL bindings and `protocol-http1`; their own
15
+ license terms remain applicable. No upstream Go source is included in the gem.
@@ -0,0 +1,100 @@
1
+ # 验证入口与证据范围
2
+
3
+ 本地 gate:
4
+
5
+ | 命令 | 证明范围 |
6
+ | --- | --- |
7
+ | `bundle exec rake test` | codec/状态机与失败测试、真实回环 TLS、混合代理、CLI 进程生命周期 |
8
+ | `bundle exec rake integration` | Ruby 客户端/服务端经真实 TCP+OpenSSL 的端到端路径 |
9
+ | `bundle exec rake interop` | Go→Go 基线、Ruby→Go 和 Go→Ruby 的 TCP/UDP 路径 |
10
+ | `bundle exec rake lint` | 项目 Ruby 规则与静态错误检测 |
11
+ | `bundle exec rake package` | 实际 gem 文件列表、临时 GEM_HOME 中依赖安装、包加载与两个 executable |
12
+ | `bundle exec rake stress` | 有界本地并发数据完整性、阶段耗时和线程回收 |
13
+
14
+ 普通 `test` 不包含跨语言测试。显式运行 interop 时若缺少参考二进制直接失败,
15
+ 不把 skip 算作互通成功。参考源码固定 commit 为
16
+ `fd6167acd6d73b9fa3e607659951847fbc9e6c50`。
17
+
18
+ `package` 使用已安装依赖的 gem cache,不访问网络,也不回退到全局 GEM_PATH;缺缓存会
19
+ 明确失败。OpenSSL native extension 在临时安装目录编译,因此需要 Ruby/OpenSSL 开发工具。
20
+ 产物与临时安装在验证后移除,不执行上传或发布。
21
+
22
+ ## 发布验证
23
+
24
+ CI 的 Ubuntu / Ruby 4.0 任务在验证通过后上传 `anytls-gem` artifact,包含 gem 和 `SHA256SUMS`。
25
+ 发布前须确认同一提交的完整 Ruby 矩阵和 Go 互通任务均通过,下载该 artifact 并核对校验和。
26
+ 运行 `bundle exec ruby script/verify_package.rb /path/to/anytls-0.1.0.gem` 可验证已有包与当前源码
27
+ 的文件内容、元数据和依赖一致,并隔离安装该包;提供路径时不会重新构建 gem。
28
+ GitHub Release 和 RubyGems 必须使用这一份验证过的包,发布后下载两处产物并核对 SHA256。
29
+
30
+ ## 本机执行记录
31
+
32
+ 2026-10-01,macOS arm64,Ruby 4.0.7 / 3.4.11,openssl gem 4.0.2,OpenSSL 3.6.4:
33
+
34
+ 运行时指纹:[Ruby 4.0](validation/runtime-ruby40.json)、[Ruby 3.4](validation/runtime-ruby34.json)。
35
+ Ruby 3.4 的正确本地命令同时固定 PATH 和 Bundler 入口,依赖位于独立的 `ruby/3.4.0` 目录:
36
+
37
+ ```sh
38
+ PATH=/opt/homebrew/opt/ruby@3.4/bin:$PATH BUNDLE_PATH=tmp/validation-ruby34 \
39
+ /opt/homebrew/opt/ruby@3.4/bin/bundle exec rake test
40
+ ```
41
+
42
+ 以下记录仅使用实际运行时指纹核对后的执行。
43
+
44
+ - 真实 TLS/mixed integration:包含证书验证、TLS 1.2/1.3、完整证书链、TCP 与 UDP,以及混合代理端到端。
45
+ - Ruby 4.0.7 最终完整本地 suite:156 tests、990 assertions,通过,无 skips(seed 855)。
46
+ - Ruby 3.4.11 最终完整本地 suite:156 tests、990 assertions,通过,无 skips(seed 13099)。
47
+ - Go 固定参考 Go→Go 基线、双向 TCP/UDP 互通:两个 Ruby 版本各 3 tests、15 assertions,通过,无 skips。
48
+ - CLI 客户端/服务端进程通信和 SIGTERM:1 test、7 assertions,通过。
49
+ - CLI admission 的 EMFILE / ThreadError 故障注入:2 tests、10 assertions,通过;已有连接保持
50
+ 可用,未归属 socket 关闭,后续接入恢复。
51
+ - 默认 stress:16 并发 × 20 次、每条 32 KiB;320 条 stream 的 payload 全部一致,
52
+ 最终清理后只剩主线程,fd 从 7 回到 7。修复 graceful shutdown 后已再验证此场景。
53
+ 这是本地回环验证,不是生产吞吐承诺,也未与 Go 做性能比较。
54
+ - 两个 Ruby 版本的隔离包验证:临时目录构建 gem、离线安装 8 个运行依赖/项目 gem、
55
+ 编译 OpenSSL extension、隔离 `require` 和两个 CLI `--help`,均通过。
56
+ - 全库 RuboCop:Ruby 3.4 和 4.0 均检查 74 个 Ruby 文件,无违规。
57
+
58
+ 两个版本在 `-w` 下的 CLI 线程创建故障注入用例会产生预期的方法临时替换警告;没有未处理
59
+ worker 异常。完整证书链用例验证客户端仅信任 root 即可验证 leaf+intermediate,同时拒绝
60
+ 缺失 intermediate 和不匹配的 hostname。
61
+
62
+ 以上为本机最终 gate 记录。Linux 和 GitHub 远端 CI 未执行,不能从本机结果推定通过。
63
+
64
+ ### 并发与回收测量
65
+
66
+ 同一进程包含 Ruby 客户端、Ruby 服务端与 echo fixture,50 ms 间隔采样 threads/fd/RSS;
67
+ 采样峰值可能低于瞬时峰值。表格中的内存和连接数是整套本地场景的消耗,不是单个部署端的消耗。
68
+
69
+ | 场景 | Stream 数 / 每条 payload | 秒 | p95 / p99 ms | 采样峰值线程 / fd / RSS KiB | 原始 JSON |
70
+ | --- | --- | --- | --- | --- | --- |
71
+ | 1 并发 | 20 / 32 KiB | 0.023 | 1.969 / 4.002 | 9 / 16 / 55,536 | [c1](validation/stress-c1.json) |
72
+ | 32 并发 | 640 / 32 KiB | 0.577 | 35.466 / 52.739 | 229 / 275 / 119,984 | [c32](validation/stress-c32.json) |
73
+ | 128 并发 | 2,560 / 32 KiB | 3.084 | 203.655 / 286.761 | 890 / 1,039 / 238,352 | [c128](validation/stress-c128.json) |
74
+ | 10,000 顺序连接 | 10,000 / 64 B | 4.547 | 0.564 / 0.866 | 12 / 18 / 56,576 | [sequential](validation/stress-sequential-10000.json) |
75
+
76
+ 以上四个场景均数据一致、0 错误;结束后 threads 为 1,fd 从 7 回到 7。10,000 顺序连接
77
+ 只创建 1 个 TLS session,证明该场景复用生效。不同并发下尾延迟和资源消耗明显增长,
78
+ 不能把这些数字解释为固定性能保证。
79
+
80
+ 初次 32 并发尝试被当前 shell 的 soft NOFILE=256 限制阻塞,出现拨号失败/EOF/timeout,
81
+ 并未通过。后续仅为验证子进程执行 `ulimit -n 4096`,再完成 32/128 并发与 10,000 顺序场景。
82
+ 实际采样峰值 fd 为 275/1,039,确认原限制不足。stress 脚本现在先检查描述符预算,条件
83
+ 不足直接解释并退出;库本身不修改用户进程的资源上限。
84
+
85
+ ## 扩展测量
86
+
87
+ ```sh
88
+ ulimit -n 4096
89
+ ANYTLS_STRESS_CONCURRENCY=64 ANYTLS_STRESS_ITERATIONS=100 \
90
+ ANYTLS_STRESS_BYTES=32768 bundle exec rake stress
91
+ ```
92
+
93
+ 压力参数有上限;默认测试不是无限运行守护进程。报告零字节损坏、零未处理错误、所有 worker
94
+ 有界退出和连接资源回收,再报告吞吐与延迟。不设置脱离部署环境的吞吐合格线;回归比较须固定
95
+ 机器、运行时、证书类型、并发、消息大小和测试时长。
96
+
97
+ 关键失败用例覆盖拆分 header/body、短写与 TLS wait 方向、写入预算、未知命令、错误密码、
98
+ 证书与名称不匹配、目标拨号失败、断连、缓慢消费、close 与等待交错。
99
+ mock 只证明状态与协议不变量;真实 TLS 测试证明本地适配器和端到端路径;二者都不代表
100
+ 真实公网、生产路由、任意防火墙/代理环境或所有 OpenSSL 版本验收。
@@ -0,0 +1,8 @@
1
+ {
2
+ "ruby": "ruby 3.4.11 (2026-09-23 revision 592f1ffdb3) +PRISM [arm64-darwin25]",
3
+ "ruby_executable": "/opt/homebrew/Cellar/ruby@3.4/3.4.11/bin/ruby",
4
+ "ruby_engine": "ruby",
5
+ "platform": "arm64-darwin25",
6
+ "openssl_gem": "4.0.2",
7
+ "openssl_library": "OpenSSL 3.6.4 25 Aug 2026"
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "ruby": "ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [arm64-darwin25]",
3
+ "ruby_executable": "/opt/homebrew/Cellar/ruby/4.0.7/bin/ruby",
4
+ "ruby_engine": "ruby",
5
+ "platform": "arm64-darwin25",
6
+ "openssl_gem": "4.0.2",
7
+ "openssl_library": "OpenSSL 3.6.4 25 Aug 2026"
8
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "ruby": "ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [arm64-darwin25]",
3
+ "openssl": "OpenSSL 3.6.4 25 Aug 2026",
4
+ "concurrency": 1,
5
+ "streams": 20,
6
+ "bytes_per_stream": 32768,
7
+ "seconds": 0.023,
8
+ "mib_per_second": 54.88,
9
+ "p95_ms": 1.969,
10
+ "p99_ms": 4.002,
11
+ "sampled_peak_threads": 9,
12
+ "remaining_threads": 1,
13
+ "baseline_fds": 7,
14
+ "sampled_peak_fds": 16,
15
+ "remaining_fds": 7,
16
+ "baseline_rss_kib": 36656,
17
+ "sampled_peak_rss_kib": 55536,
18
+ "client_stats_before_close": {
19
+ "sessions": 1,
20
+ "idle_sessions": 1,
21
+ "creating_sessions": 0,
22
+ "created_sessions": 1
23
+ },
24
+ "errors": 0
25
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "ruby": "ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [arm64-darwin25]",
3
+ "openssl": "OpenSSL 3.6.4 25 Aug 2026",
4
+ "concurrency": 128,
5
+ "streams": 2560,
6
+ "bytes_per_stream": 32768,
7
+ "descriptor_soft_limit": 4096,
8
+ "seconds": 3.084,
9
+ "mib_per_second": 51.878,
10
+ "p95_ms": 203.655,
11
+ "p99_ms": 286.761,
12
+ "sampled_peak_threads": 890,
13
+ "remaining_threads": 1,
14
+ "baseline_fds": 7,
15
+ "sampled_peak_fds": 1039,
16
+ "remaining_fds": 7,
17
+ "baseline_rss_kib": 36656,
18
+ "sampled_peak_rss_kib": 238352,
19
+ "client_stats_before_close": {
20
+ "sessions": 128,
21
+ "idle_sessions": 128,
22
+ "creating_sessions": 0,
23
+ "created_sessions": 128
24
+ },
25
+ "errors": 0
26
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "ruby": "ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [arm64-darwin25]",
3
+ "openssl": "OpenSSL 3.6.4 25 Aug 2026",
4
+ "concurrency": 32,
5
+ "streams": 640,
6
+ "bytes_per_stream": 32768,
7
+ "descriptor_soft_limit": 4096,
8
+ "seconds": 0.577,
9
+ "mib_per_second": 69.329,
10
+ "p95_ms": 35.466,
11
+ "p99_ms": 52.739,
12
+ "sampled_peak_threads": 229,
13
+ "remaining_threads": 1,
14
+ "baseline_fds": 7,
15
+ "sampled_peak_fds": 275,
16
+ "remaining_fds": 7,
17
+ "baseline_rss_kib": 36720,
18
+ "sampled_peak_rss_kib": 119984,
19
+ "client_stats_before_close": {
20
+ "sessions": 32,
21
+ "idle_sessions": 32,
22
+ "creating_sessions": 0,
23
+ "created_sessions": 32
24
+ },
25
+ "errors": 0
26
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "ruby": "ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [arm64-darwin25]",
3
+ "openssl": "OpenSSL 3.6.4 25 Aug 2026",
4
+ "concurrency": 1,
5
+ "streams": 10000,
6
+ "bytes_per_stream": 64,
7
+ "descriptor_soft_limit": 4096,
8
+ "seconds": 4.547,
9
+ "mib_per_second": 0.268,
10
+ "p95_ms": 0.564,
11
+ "p99_ms": 0.866,
12
+ "sampled_peak_threads": 12,
13
+ "remaining_threads": 1,
14
+ "baseline_fds": 7,
15
+ "sampled_peak_fds": 18,
16
+ "remaining_fds": 7,
17
+ "baseline_rss_kib": 36832,
18
+ "sampled_peak_rss_kib": 56576,
19
+ "client_stats_before_close": {
20
+ "sessions": 1,
21
+ "idle_sessions": 1,
22
+ "creating_sessions": 0,
23
+ "created_sessions": 1
24
+ },
25
+ "errors": 0
26
+ }
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+
5
+ module AnyTLS
6
+ # SOCKS 目标地址值;端口零也可表示尚未绑定的 UDP 端点。
7
+ Address = Data.define(:host, :port) do
8
+ def initialize(host:, port:)
9
+ raise TypeError, "host must be a String" unless host.is_a?(String)
10
+ raise TypeError, "port must be an Integer" unless port.is_a?(Integer)
11
+ raise ArgumentError, "port must be between 0 and 65535" unless (0..65_535).cover?(port)
12
+ unless host.ascii_only? && host.bytesize.between?(1, 255) && !host.match?(%r{[\x00-\x20\x7f/@%?\[\]\\#]})
13
+ raise ArgumentError, "host must be an ASCII IP address or domain of 1 to 255 bytes"
14
+ end
15
+
16
+ begin
17
+ ip = IPAddr.new(host)
18
+ host = ip.to_s
19
+ rescue IPAddr::InvalidAddressError
20
+ raise ArgumentError, "invalid IP address" if host.include?(":")
21
+ end
22
+ super(host: host.dup.freeze, port:)
23
+ end
24
+
25
+ def ip?
26
+ !ip_address.nil?
27
+ end
28
+
29
+ def ipv4?
30
+ ip_address&.ipv4? || false
31
+ end
32
+
33
+ def ipv6?
34
+ ip_address&.ipv6? || false
35
+ end
36
+
37
+ def domain?
38
+ !ip?
39
+ end
40
+
41
+ def to_s
42
+ ipv6? ? "[#{host}]:#{port}" : "#{host}:#{port}"
43
+ end
44
+
45
+ private
46
+
47
+ def ip_address
48
+ IPAddr.new(host)
49
+ rescue IPAddr::InvalidAddressError
50
+ nil
51
+ end
52
+ end
53
+ end