第 6 章:连接、握手与加密 —— 从 socket 到“属于某个 Torrent 的 Peer”
一个 TCP/µTP 连接刚建立时,只是一条匿名字节流。握手层必须识别 torrent、验证对端、协商扩展与加密,再把连接的所有权交给 Peer 消息层。
一、四层连接栈
层次职责非常清楚:socket 负责传输,Peer I/O 负责可控字节流,Handshake 负责连接身份,Peer Messages 负责长期协议。
二、tr_peer_socket 抹平 TCP 与 µTP
TCP 使用内核 stream socket;µTP 在 UDP 之上提供可靠有序传输。上层不应到处判断协议,因此两者实现共同的 socket 抽象:读、写、关闭、地址、是否 µTP。
Peer Manager 选择 transport 后创建 outgoing socket;Session 收到入站 TCP/µTP 时创建 incoming socket。两者随后都进入 tr_peerIo。
三、tr_peerIo 是连接的“字节阀门”
它持有:
- 输入/输出 buffer;
tr_peer_socket;- info hash 与远端地址;
- 上下行
tr_bandwidth节点; - MSE 加解密 filter;
- LTEP/Fast/DHT 支持位;
CanRead / DidWrite / GotError三个回调。
握手期与消息期复用同一个 I/O 对象,只替换 callbacks。这个设计避免在交接时迁移 socket buffer:对端可能把握手和第一条 BitTorrent 消息一次写进网络包,剩余字节仍留在 peerIo 输入 buffer,切换回调后可继续解析。
读回调返回 ReadState:
| 状态 | 含义 |
|---|---|
Now | buffer 里可能还有足够数据,立刻继续解析 |
Later | 数据不足,等下次可读事件 |
Break | 本轮停止循环,通常用于回调所有权刚切换 |
Err | 协议或连接错误,关闭路径 |
四、标准 BitTorrent 握手恰好 68 字节
1 byte pstrlen = 19
19 bytes "BitTorrent protocol"
8 bytes reserved feature bits
20 bytes info_hash
20 bytes peer_id保留位中 Transmission 关注 LTEP、Fast Extension 和 DHT。入站连接在读到前 48 字节(协议名 + flags + info hash)时就能找到 torrent 并回复握手,不必等远端 peer id 全部到达。
握手验证的关键问题包括:
- 协议名是否正确;
- info hash 是否对应本 Session 中运行的 torrent;
- 远端 peer id 是否与本机相同;
- 扩展位是否与后续行为一致;
- incoming 连接是否允许绑定到该 torrent。
五、为什么握手是状态机而不是一次 read(68)
网络 read 可能只返回任意前缀;MSE 又引入多轮 Diffie-Hellman 与 padding。tr_handshake::State 因此区分:
- 双向共有:
AwaitingHandshake、AwaitingPeerId; - incoming MSE:
AwaitingYa/PadA/CryptoProvide/PadC/Ia; - outgoing MSE:
AwaitingYb/Vc/CryptoSelect/PadD。
can_read() 循环调用当前状态对应的 reader。每个 reader 只在数据足够时消费 buffer,并返回 Now/Later/Break/Err。
六、MSE 协商在保护什么
Message Stream Encryption 通过 Diffie-Hellman 共享秘密与 torrent key 协商 plaintext 或 RC4 流。它主要用于协议流量混淆和避免简单识别,不应等同于现代端到端身份认证协议。
Session 的 encryption mode 决定策略:要求加密、偏好加密、偏好明文。Outgoing 在 MSE 失败且策略允许时可以 reconnect 后尝试明文;要求加密时则直接失败。
生成 DH key 成本较高。tr_handshake 有一个最大 32 项的静态 DH pool:只有在完全没从对端读到数据时才回收 key,避免把已经参与交互的密钥材料随意复用。这是性能优化与安全边界的折中。
七、30 秒超时与失败记忆
Handshake 创建 timer,约 30 秒未完成就失败。结果对象包含:
- 共享
peerIo; - 可选 peer id;
- 是否从对端读到任何内容;
- 是否连接成功。
µTP outgoing 失败时 Mediator 可以记录该地址的 µTP 失败状态,使后续候选选择转向 TCP。Peer 记录也会积累 fruitless connection 次数,避免持续重试无效节点。
八、完成交接:短生命周期对象退场
握手完成回调回到 Peer Manager。Manager 会重新确认:
- torrent 仍存在且运行;
- 地址没有被 ban/block;
- 没有重复连接;
- peer id 不与本机或现有连接冲突;
- swarm 仍有容量。
通过后创建 tr_peerMsgsImpl,把 peerIo callbacks 从 Handshake 切换到消息解析器,并把 Peer 挂到 swarm。tr_handshake 随后销毁。
这里体现了清晰的对象生命周期:Handshake 只活在身份协商阶段,不把临时状态永久塞进 Peer。
九、入站与出站为何共享同一状态机
二者差异主要是:谁先知道 info hash、谁先发送哪段 MSE 数据、连接失败能否重拨。共享一个状态机保证协议校验、feature bit 解析、超时与完成回调一致,减少两套实现产生安全差异。
Mediator 提供 torrent(info_hash) 和 torrent_from_obfuscated(hash),让 incoming MSE 在不知道明文 info hash 时也能映射到任务,同时不让 Handshake 直接遍历 Session 内部容器。
十、常见误解
- 握手成功不代表可以下载:对端之后仍可能 choke、没有我们需要的 piece,或请求窗口为零。
- 连接数不等于活跃 Peer 数:Handshake、connected、interested、unchoked、transferring 是不同阶段。
- 加密不是匿名:Tracker、DHT、IP 地址和流量行为仍可暴露参与关系。
- µTP 不是“UDP 不可靠模式”:可靠性与拥塞控制由 libutp 实现,上层仍看到有序字节流。
源码锚点
libtransmission/peer-socket.h:TCP/µTP 统一接口libtransmission/peer-io.h、peer-io.cc:缓冲、带宽与回调libtransmission/handshake.h、handshake.cc:68 字节握手与 MSE 状态机libtransmission/peer-mse.cc:加解密 filter 与 DHlibtransmission/peer-mgr.cc:握手创建、完成校验与 Peer 接管