# AnyTLS 代码架构与协议实现详解
本文档详细解析 AnyTLS 的代码结构与核心协议实现细节。
## 1. 总体架构
AnyTLS 是一个基于 TLS 1.3 的隧道代理工具,旨在将 SOCKS5 流量封装在标准的 TLS 连接中。其核心设计理念是**多路复用 (Multiplexing)**,即在一条物理 TLS 连接(Session)上承载多个逻辑数据流(Stream)。
### 核心组件
* **Client (`bin/client`)**: 本地 SOCKS5 服务器,负责接收用户请求,通过 TLS 连接转发给 Server。
* **Server (`bin/server`)**: 远程 TLS 服务器,负责解包 TLS 流量并转发到目标网站。
* **Session (`proxy/session/inner.rs`)**: 管理一条物理 TLS 连接,负责帧(Frame)的读写、分发和心跳维护。
* **Stream (`proxy/session/stream.rs`)**: 代表一个逻辑连接(如一次网页请求),提供类似 `TcpStream` 的读写接口。
* **Pipe (`proxy/pipe`)**: 用于 Stream 与 Session 之间的高效异步数据缓冲通道。
---
## 2. 客户端流程 (`bin/client/main.rs`)
### 2.1 入口与初始化
* 程序启动后监听本地 SOCKS5 端口(默认 1080)。
* 初始化 `Client` 结构体 (`proxy/session/client.rs`),配置连接池参数(空闲超时、最小空闲数等)。
* **TLS 配置**: 使用 `rustls`,并配置了一个“危险”的证书验证器 `AllowAnyCertVerifier`,允许连接自签名证书的服务器。
### 2.2 SOCKS5 握手
当浏览器发起连接时:
1. `handle_connection` 处理标准的 SOCKS5 握手(无需认证)。
2. 解析目标地址(IPv4/IPv6/Domain)。
3. **建立隧道**: 调用 `client.create_stream()` 获取一个虚拟流 (`Stream`)。
4. **发送目标地址**: 将 SOCKS5 请求中的目标地址直接写入 `Stream`。
5. **数据转发**: 启动两个异步任务 (`c2p`, `p2c`) 在本地 TCP 连接和虚拟 `Stream` 之间双向拷贝数据。
### 2.3 连接池管理 (`proxy/session/client.rs`)
这是客户端的核心优化逻辑:
* **复用策略**:
1. 优先从 `idle_sessions` 池中获取空闲会话。
2. 其次尝试复用活跃但负载较低(Stream 数 < 3)的会话。
3. 最后才建立新的 TLS 连接。
* **空闲回收**: `spawn_idle_waiter` 监控会话状态,当会话所有流关闭后,将其放回空闲池以备下次秒开。
---
## 3. 服务端流程 (`bin/server/main.rs`)
### 3.1 监听与接收
* 监听 TCP 端口(默认 8443)。
* 使用 `mkcert` 动态生成自签名证书。
* **TCP 优化**: 开启 `TCP_NODELAY` 和 `TCP_KEEPALIVE`,确保低延迟和死链检测。
### 3.2 握手与鉴权
1. **TLS 握手**: 建立标准 TLS 1.3 连接。
2. **应用层鉴权**:
* 读取前 34 字节:32 字节密码哈希 (SHA256) + 2 字节 Padding 长度。
* 验证密码,失败则断开。
* 读取并丢弃指定长度的 Padding 数据(用于混淆流量特征)。
3. **会话创建**: 创建 `Session::new_server`,并传入回调函数处理新流。
### 3.3 目标连接
当 Session 收到新流请求 (`CMD_SYN`):
1. 触发回调,读取流中的第一个数据包(包含 SOCKS5 目标地址)。
2. `TcpStream::connect` 连接目标网站。
3. **数据转发**: 在目标 TCP 连接和虚拟 `Stream` 之间双向拷贝数据。
---
## 4. 核心协议实现 (`proxy/session`)
### 4.1 帧结构 (`frame.rs`)
所有数据在 TLS 隧道中都被封装为“帧”:
```
+------+------+--------+------+
| 1B | 4B | 2B | Var |
+------+------+--------+------+
```
* **CMD (1B)**: 命令类型 (SYN, PSH, FIN, HEART_REQUEST 等)。
* **SID (4B)**: 流 ID,用于区分不同的请求。
* **LEN (2B)**: 数据载荷长度。
* **DATA**: 实际数据。
### 4.2 会话管理 (`inner.rs`)
`Session` 是协议的心脏,负责多路复用:
* **读循环 (`recv_loop`)**:
* 从 TLS 连接读取字节流。
* 解析帧头,提取 `CMD` 和 `SID`。
* **分发**:
* `CMD_PSH`: 找到对应的 `Stream`,将数据写入其 `Pipe`。
* `CMD_SYN`: (服务端) 创建新 `Stream` 并触发回调。
* `CMD_FIN`: 关闭对应 `Stream` 的写入端。
* `CMD_HEART_*`: 处理心跳,保持连接活跃。
* **写操作**:
* 使用 `mpsc::channel` 接收各 `Stream` 发来的帧。
* 通过 `tokio::io::split` 分离出的 `WriteHalf` 写入 TLS 连接。
* **关键优化**: 每次写入后强制 `flush()`,配合 `TCP_NODELAY` 消除缓冲延迟。
### 4.3 流抽象 (`stream.rs`)
`Stream` 模拟了标准的 `AsyncRead` / `AsyncWrite` 接口:
* **写数据**: 将数据封装为 `CMD_PSH` 帧,通过 `frame_tx` 发送给 Session。
* **读数据**: 从内部的 `PipeReader` 读取由 Session 分发来的数据。
* **关闭**: 发送 `CMD_FIN` 帧。为防止死锁,发送操作在后台异步任务中执行。
### 4.4 管道机制 (`proxy/pipe/io_pipe.rs`)
为了解决异步读写中的死锁和数据竞争,实现了一个自定义的 `Pipe`:
* **结构**: 包含一个 `mpsc` 通道(用于数据传输)和一个字节缓冲区(用于暂存未读完的数据)。
* **死锁解决**:
* `read` 操作在等待数据时**不持有锁**。
* 利用 `mpsc` 的关闭特性(Sender Drop -> Receiver Return None)来优雅地处理 EOF,确保所有缓冲数据被读完后再返回结束信号。
---
## 5. 关键技术细节总结
1. **并发模型**: 使用 Tokio 的异步任务 (`tokio::spawn`) 处理每个连接和流,实现高并发。
2. **死锁预防**:
* Session 的读写分离 (`split`)。
* Stream 发送控制帧(如 FIN)时的异步化。
* Pipe 读取时的锁释放机制。
3. **性能优化**:
* **连接复用**: 减少 TLS 握手开销。
* **Zero Copy**: 在可能的情况下尽量减少内存拷贝(虽然 Frame 封装不可避免会有一次拷贝)。
* **即时发送**: 强制 Flush 避免 Nagle 算法和 TLS 缓冲带来的延迟。
4. **健壮性**:
* **Keepalive**: 应用层心跳 + TCP 层 Keepalive 双重保障,快速检测死链。
* **优雅关闭**: 确保 Session 和 Stream 的状态在网络异常时能正确同步,防止僵尸连接。
---
## 6. 核心数据流向总结
以下图表展示了从用户发起请求到目标网站接收数据的完整流向:
```mermaid
sequenceDiagram
participant User as User (Browser)
participant C_Main as Client (Main)
participant C_Stream as Client Stream
participant C_Session as Client Session
participant Network as TLS Network
participant S_Session as Server Session
participant S_Stream as Server Stream
participant S_Main as Server (Main)
participant Target as Target Website
Note over User, C_Main: SOCKS5 协议
User->>C_Main: 发送数据 (Plain)
Note over C_Main, C_Stream: 写入虚拟流
C_Main->>C_Stream: stream.write(data)
Note over C_Stream, C_Session: 封装为帧 (Frame)
C_Stream->>C_Session: frame_tx.send(CMD_PSH)
Note over C_Session, Network: 加密并发送
C_Session->>Network: TLS Write + Flush
Note over Network, S_Session: 接收并解密
Network->>S_Session: TLS Read
Note over S_Session, S_Stream: 解析帧并分发
S_Session->>S_Stream: stream.push_data(data) (写入 Pipe)
Note over S_Stream, S_Main: 读取虚拟流
S_Main->>S_Stream: stream.read(buf) (从 Pipe 读取)
Note over S_Main, Target: 转发到目标
S_Main->>Target: TCP Write
```
---
## 7. 多路复用 (Multiplexing) 原理 (版本 3 里 已经 删除)
AnyTLS 使用单条 TLS 连接承载多个逻辑流(Stream),极大地减少了握手延迟并提高了传输效率。
```mermaid
graph LR
subgraph Client_Side [Client Side]
UA[User A] -->|Data| SA[Stream 1]
UB[User B] -->|Data| SB[Stream 2]
UC[User C] -->|Data| SC[Stream 3]
SA -.->|Frame: ID=1| Mux["Client Session<br/>(Multiplexer)"]
SB -.->|Frame: ID=2| Mux
SC -.->|Frame: ID=3| Mux
end
Mux ==>|"TLS Tunnel<br/>Frames: [ID=1][ID=2][ID=1][ID=3]..."| Demux["Server Session<br/>(Demultiplexer)"]
subgraph Server_Side [Server Side]
Demux -.->|Frame: ID=1| SSA[Stream 1]
Demux -.->|Frame: ID=2| SSB[Stream 2]
Demux -.->|Frame: ID=3| SSC[Stream 3]
SSA -->|Data| TA[Target A]
SSB -->|Data| TB[Target B]
SSC -->|Data| TC[Target C]
end
style Mux fill:#f96,stroke:#333,stroke-width:2px
style Demux fill:#f96,stroke:#333,stroke-width:2px
```
## 8. 关于 FIN 包
- 服务端 主动 发送 FIN 包
- 这种情况会按“远端半关闭”处理,不会把它当成“整条逻辑流已经结束”。
- 更具体地说,服务端发来的 FIN 只说明“服务端这一侧以后不再向客户端发送数据了”,
不等于“客户端也必须立刻停止发送”,更不等于“这条 session 现在就可以回收到 idle pool”。
否则就会把一个还没真正双向结束的流过早复用,重新撞上你前面刚抓到的那类错位问题。
- 这样处理:
- 收到服务端 FIN 后,把“远端发送方向”标记为关闭。
- 客户端读侧在把已缓冲数据读完后,向本地应用返回 EOF。
- 客户端写侧继续允许本地应用发送剩余数据,只要本地应用还没 EOF。
- 这条 session 不能进入 idle pool。
- 等客户端自己也发出 FIN,也就是本地发送方向关闭后,才认为这条逻辑流真正结束。
- 只有本地和远端两个方向都关闭后,才允许复用这条 session。
- 如果客户端在远端 FIN 之后继续写,但服务端到目标的写入失败,就把它升级成错误并终止这条 session。
- 客户端 发送 FIN 给 服务端 后
- 必须 等到 服务端 发回的 FIN 包
- 这里的 FIN 兼具“我这边不再发送请求数据了”和“这一条逻辑流可以安全回收到 idle pool”两层语义。
- 如果客户端在自己发完 FIN 后就立刻把 session 视为可复用,会出现问题:
上一条响应还没完全从服务端回完,下一条请求就抢先复用了同一条会话,
结果把旧流尾部和新流的 TLS 握手串在一起,触发 tlsv1 alert decode error。
这也是为什么现在客户端必须等服务端那边真正结束当前目标连接、回 FIN 之后,再把逻辑流关闭并放回 idle pool。
- 协议层必须等到“服务端回 FIN 或其他明确的流结束信号”之后,才能把这条 session 当成可复用。
否则只能选择另一种更保守的语义:客户端一发 FIN 就直接销毁整条 session,不做复用。
现在既想保留复用,就必须等服务端确认这条逻辑流真的结束了。