TINC 源码解读 第一篇 - 协议深度解析
本文是 TINC 源码解读系列的第一篇,重点分析 TINC 的协议设计。TINC 采用分层架构,使用 Meta Protocol 处理控制平面,Packet Protocol 处理数据平面,SPTPS 提供额外的加密保证。
1. 协议概览⌗
TINC 使用两个独立的协议层:
- Meta Protocol - TCP 连接上的文本协议(连接建立、拓扑交换)
- Packet Protocol - UDP 连接上的二进制协议(数据包转发)
协议版本⌗
PROT_MAJOR = 17 // 主版本号(不兼容时变化)
PROT_MINOR = 7 // 次版本号(向后兼容)
2. Meta 协议 - 控制平面⌗
消息类型枚举⌗
Meta Protocol 定义了 20 多种消息类型,按功能分类:
// 认证阶段
ID // 连接标识
METAKEY // 元数据密钥交换
CHALLENGE // 密码挑战
CHAL_REPLY // 挑战回复
ACK // 确认
// 连接管理
STATUS // 状态信息
ERROR // 错误消息
TERMREQ // 终止请求
// 保活
PING // 心跳 Ping
PONG // 心跳 Pong
// 拓扑交换
ADD_SUBNET // 添加子网
DEL_SUBNET // 删除子网
ADD_EDGE // 添加边/链路
DEL_EDGE // 删除边/链路
// 密钥交换
KEY_CHANGED // 密钥已更改
REQ_KEY // 请求加密密钥
ANS_KEY // 应答加密密钥
// 数据转发
PACKET // TCP 转发数据包
// Tinc 1.1+ 扩展
REQ_PUBKEY // 请求公钥
ANS_PUBKEY // 应答公钥
SPTPS_PACKET // SPTPS 加密包
UDP_INFO // UDP 地址信息
MTU_INFO // MTU 信息
核心消息格式⌗
ID 消息 - 连接起点⌗
格式: ID name version
示例: 0 mynode 17.7
含义:
├─ 0 - 消息类型标识
├─ mynode - 节点名称
└─ 17.7 - 协议版本 (MAJOR.MINOR)
METAKEY 消息 - 加密参数协商⌗
格式: METAKEY cipher digest keylength [加密的密钥]
示例: 1 aes-256-cbc sha512 32 [128字节加密数据]
含义:
├─ 1 - 消息类型
├─ aes-256-cbc - 对称加密算法
├─ sha512 - HMAC 摘要算法
└─ 32 - 密钥长度(字节)
特点:
└─ 加密密钥使用对方的公钥进行 RSA 加密
CHALLENGE 消息 - 身份验证⌗
格式: CHALLENGE <hash>
验证过程:
1. 接收方生成随机 challenge (16 字节)
2. 计算 hash = HMAC-SHA256(challenge, shared_key)
3. 发送 CHALLENGE hash
4. 发起方计算相同的 hash
5. 验证 hash 相等
ADD_EDGE 消息 - 拓扑通告⌗
格式: ADD_EDGE node_from node_to address port weight options
示例: ADD_EDGE nodeA nodeB 192.168.1.100 655 100 0
解析:
├─ ADD_EDGE - 消息类型
├─ nodeA - 源节点
├─ nodeB - 目标节点
├─ 192.168.1.100 - 目标地址(实际网络地址)
├─ 655 - UDP 端口
├─ 100 - 权重(用于 Dijkstra)
└─ 0 - 选项标志
选项标志:
├─ 0x0001: OPTION_INDIRECT (间接连接)
├─ 0x0002: OPTION_TCPONLY (仅 TCP)
├─ 0x0004: OPTION_PMTU_DISCOVERY
└─ 0x0008: OPTION_CLAMP_MSS
ADD_SUBNET 消息 - 子网通告⌗
格式: ADD_SUBNET owner_node network/prefixlength
IPv4 示例: ADD_SUBNET node1 192.168.1.0/24
IPv6 示例: ADD_SUBNET node1 2001:db8::/32
MAC 示例: ADD_SUBNET node1 ff:ff:ff:ff:ff:ff
用途:
└─ 通告该节点负责路由的子网地址
REQ_KEY 和 ANS_KEY - 数据加密密钥交换⌗
REQ_KEY 格式: REQ_KEY originator destination
含义: originator 请求与 destination 的通信密钥
ANS_KEY 格式: ANS_KEY from to cipher_id digest_id key mac_length
示例: ANS_KEY nodeB nodeA 1 3 4ae0b0a82d6e0078 16
解析:
├─ nodeB - 密钥所有者
├─ nodeA - 请求方
├─ 1 - 对称加密算法 ID
├─ 3 - HMAC 摘要算法 ID
├─ 4ae0...78 - 128 位加密密钥
└─ 16 - MAC 长度
PING 和 PONG - 保活消息⌗
PING 格式: PING [随机数]
PONG 格式: PONG [随机数]
用途:
├─ 检测连接是否仍然活跃
├─ 防止连接因不活动而被关闭
├─ 检测单向连接失败
└─ 通过随机数防止已知明文攻击
3. Meta 协议握手流程⌗
完整的连接建立过程⌗
发起方 响应方
─────────────────────────────────────
TCP 连接 ─────────────→
← 接受连接
发送 ID ──────────→
✓ 验证版本
← 发送 ID
✓ 验证版本
发送 METAKEY ─────→
(加密的元密钥)
✓ 解密元密钥
← 发送 METAKEY
✓ 解密元密钥
从此以后,所有通信使用
协商的加密密钥加密
发送 CHALLENGE ────→
(hash = HMAC(...))
✓ 验证 hash
← CHAL_REPLY
✓ 双向认证成功
发送 ACK ─────────→
← ACK
✓ 连接完全建立
握手状态机⌗
[TCP Connected]
↓
[ID Exchanged]
├─ 校验版本号
├─ 确定协议版本
└─ 准备加密
↓
[METAKEY Exchanged]
├─ 选择加密算法
├─ 交换元数据密钥
└─ 开始加密通信
↓
[CHALLENGE Issued]
├─ 单向认证
├─ 验证对方公钥
└─ 生成共享密钥
↓
[CHAL_REPLY Received]
├─ 双向认证完成
├─ 确认对方身份
└─ 建立信任
↓
[ACK Sent/Received]
├─ 连接活跃
├─ 准备数据交换
└─ 拓扑同步
↓
[ACTIVE]
├─ 双向 PING/PONG
├─ 转发 ADD_EDGE
├─ 转发 ADD_SUBNET
├─ 转发 REQ_KEY/ANS_KEY
└─ 转发数据包
4. Packet Protocol - 数据平面⌗
UDP 数据包格式⌗
加密前的包结构⌗
Byte: 0-3 4-... ... ...+16
+----------+---------------+--------+--------+
| seqno | payload | padding| MAC |
| (32 bit) | (VPN data) | |(256bit)|
+----------+---------------+--------+--------+
+-----------------------------------+--------+
Covered by HMAC
加密后的完整 UDP 包⌗
+-------------------------------------------------+
| UDP Header (20 bytes) |
| +--- Source IP |
| +--- Dest IP |
| +--- Source Port (1194 or configured) |
| +--- Dest Port |
+-------------------------------------------------+
| Encrypted Payload (variable length) |
| +--- Nonce/IV (optional) |
| +--- Ciphertext: |
| | +--- seqno (sequence number) |
| | +--- VPN data (virtual network packet) |
| | +--- HMAC (message authentication code) |
| +--- Authentication Tag (AEAD encryption) |
+-------------------------------------------------+
VPN 数据包结构⌗
虚拟网络上传输的是标准 Ethernet Frame:
+----------+----------+----------+---------+-----+
| Dest MAC | Src MAC | EtherType| Payload | CRC |
| (6 Byte) | (6 Byte) | (2 Byte) |(var len)| (4) |
+----------+----------+----------+---------+-----+
EtherType:
+-- 0x0800: IPv4
+-- 0x86DD: IPv6
+-- 0x0806: ARP
支持的加密算法⌗
对称加密:
├─ ChaCha20-Poly1305 (推荐,AEAD)
├─ AES-256-GCM (AEAD)
├─ AES-256-CBC (需要 HMAC)
└─ null cipher (调试用,无加密)
消息摘要:
├─ SHA512 (推荐)
├─ SHA256
├─ SHA1
└─ MD5 (遗留,不安全)
密钥长度:
├─ 256 bit (标准)
└─ 128 bit (弱密钥,不推荐)
5. SPTPS 协议 - 安全的点对点通信⌗
SPTPS 设计目的⌗
SPTPS(Simple Peer-to-Peer Security)是一个轻量级的加密层,用于在 UDP 上建立直接的加密通道,不需要依赖 Meta Protocol 的握手。
与 Meta Protocol 的区别:
- Meta Protocol: 文本协议,TCP,用于控制平面
- SPTPS: 二进制协议,UDP,用于数据平面持久化会话
SPTPS 记录类型⌗
#define SPTPS_HANDSHAKE 128 // 密钥交换和认证
#define SPTPS_ALERT 129 // 警告或错误消息
#define SPTPS_CLOSE 130 // 应用程序关闭连接
SPTPS 握手状态⌗
typedef enum sptps_state_t {
SPTPS_KEX = 1, // 等待第一个密钥交换记录
SPTPS_SECONDARY_KEX = 2, // 准备接收二次密钥交换
SPTPS_SIG = 3, // 等待签名记录
SPTPS_ACK = 4, // 等待确认记录
} sptps_state_t;
SPTPS 密钥交换结构⌗
struct sptps_kex_t {
uint8_t version; // 版本号
uint8_t nonce[32]; // 随机数
uint8_t pubkey[32]; // 临时 ECDH 公钥 (Curve25519)
}
// 总大小: 65 字节
SPTPS 握手流程⌗
Initiator Responder
─────────────────────────────────────
生成临时密钥对
(Curve25519 ECDH)
发送 HANDSHAKE ────────────→
payload: kex1
(version, nonce, pubkey)
收到 kex1
生成临时密钥对
计算共享密钥
← 发送 HANDSHAKE
payload: kex2
收到 kex2
计算共享密钥
验证: ECDH(A_priv, B_pub)
== ECDH(B_priv, A_pub) ✓
使用 PRF 派生会话密钥
发送签名记录 ───────────────→
签署握手数据
验证签名
← 发送 ACK 记录
验证 ACK
握手完成 ✓
双向 AEAD 加密通信开始
密钥派生⌗
Shared_Secret = ECDH(my_private_key, peer_public_key)
Session_Keys = PRF-SHA512(
Shared_Secret,
label = "SPTPS key expansion"
+ initiator_pubkey
+ responder_pubkey
)
派生密钥 (共 64 字节):
├─ Key_A (32 字节): Initiator → Responder
├─ Key_B (32 字节): Responder → Initiator
└─ 每个方向使用不同密钥 (前向保密)
6. 身份验证与安全机制⌗
签名与验证⌗
每个节点都有持久的 ECDSA/EdDSA 密钥对:
公钥位置: hosts 目录 ($TINC_HOME/hosts/nodename)
私钥位置: 配置目录 ($TINC_HOME/rsa_key.priv, ed25519_key.priv)
认证流程:
1. 交换 ID → 校验版本
2. 交换 METAKEY → 协商加密算法
3. 计算 challenge hash
4. 发送 CHALLENGE + CHAL_REPLY
5. 验证签名:数据已被对方的私钥签署
6. 发送 ACK
验证公钥:
├─ 从 hosts 目录读取对方的公钥
├─ 验证签名: Verify(signature, data, peer_pubkey)
├─ 若验证失败,拒绝连接
└─ 若验证成功,建立信任
重放保护⌗
Meta Protocol 重放保护:
├─ past_request_tree 跟踪已处理的请求
├─ 相同请求在一定时间内被忽略
└─ 防止攻击者重复发送拓扑更新
数据包重放保护:
├─ 序列号 (seqno): 递增计数器
├─ 验证 seqno > last_seqno
├─ 丢弃重复包
└─ 使用滑动窗口防止乱序问题
7. 拓扑同步协议⌗
完整的拓扑交换过程⌗
当两个节点建立连接后,会进行完整的拓扑交换:
Node A ──────TCP──────→ Node B
1. 身份验证完成
(ID, METAKEY, CHALLENGE, ACK)
2. Node A 发送其知道的拓扑:
ADD_EDGE nodeA nodeC 10.0.0.3 655 50 0
ADD_EDGE nodeA nodeD 10.0.0.4 655 100 0
ADD_SUBNET nodeA 172.16.1.0/24
ADD_SUBNET nodeC 172.16.2.0/24
3. Node B 发送其知道的拓扑:
ADD_EDGE nodeB nodeE 10.0.0.5 655 50 0
ADD_SUBNET nodeB 172.16.4.0/24
ADD_SUBNET nodeE 172.16.5.0/24
4. 每当拓扑变化时:
└─ 通过 ADD_EDGE/ADD_SUBNET 发布增量更新
5. 当边或节点失败时:
└─ 广播 DEL_EDGE/DEL_SUBNET
拓扑维护机制⌗
变化检测:
├─ 新节点加入 → ADD_EDGE + ADD_SUBNET
├─ 节点离开 → DEL_EDGE + DEL_SUBNET
├─ 链路权重变化 → ADD_EDGE (新权重)
└─ 密钥更新 → KEY_CHANGED
广播机制:
├─ 每个连接都转发其收到的拓扑消息
├─ 防环:记录消息源,不回复给源
└─ 定期同步:每个连接建立时交换完整拓扑
8. 协议扩展 (Tinc 1.1+)⌗
新增消息类型⌗
REQ_PUBKEY / ANS_PUBKEY
格式: REQ_PUBKEY from to [timestamp]
ANS_PUBKEY from to [key_data]
用途:
├─ 请求/应答节点的公钥
├─ 用于拓展的密钥交换
└─ 支持在线密钥更新
UDP_INFO
格式: UDP_INFO originator address:port
用途:
├─ 通告节点可用的 UDP 地址
├─ 支持多地址(IPv4, IPv6)
└─ 加速 UDP 直连建立
MTU_INFO
格式: MTU_INFO node mtu
用途:
├─ 通告路径的最大传输单元
├─ 支持 PMTU 发现
└─ 优化包大小
SPTPS_PACKET
用途:
├─ 使用 SPTPS 加密的 TCP 包
├─ 在 meta 协议上层的加密
└─ 增强安全性
9. 安全性分析⌗
已实现的安全特性⌗
| 特性 | 实现 | 说明 |
|---|---|---|
| 前向保密 (PFS) | ✓ | ECDH 临时密钥,会话独立 |
| 数据完整性 | ✓ | HMAC/AEAD 认证 |
| 数据机密性 | ✓ | 对称加密 (AES/ChaCha20) |
| 重放保护 | ✓ | 序列号 + 滑动窗口 |
| 身份认证 | ✓ | ECDSA/EdDSA 签名 |
| 密钥交换 | ✓ | ECDH (P-256/Curve25519) |
| 已知明文保护 | ✓ | Ping/Pong 随机值 |
潜在的风险和对策⌗
| 风险 | 严重度 | 现状 |
|---|---|---|
| 密钥重用 | 中 | 支持密钥轮换 (KEY_CHANGED) |
| 时钟偏差 | 低 | 支持时钟偏差检测 |
| 实现漏洞 | 高 | 定期安全审计 |
| 算法选择 | 低 | 支持现代算法 (ChaCha20) |
| 密钥缓存 | 中 | 支持定期过期 |
10. 连接到完整通信的时间线⌗
时间 事件
──────────────────────────────────────────────
0ms TCP 连接建立
↓
10ms 交换 ID (版本协商)
↓
20ms 交换 METAKEY (加密参数)
↓
30ms 发送 CHALLENGE (单向认证)
↓
40ms 接收 CHAL_REPLY (双向认证)
↓
50ms 从此以后所有通信加密
↓
60ms 交换 ADD_EDGE (拓扑)
↓
100ms 交换 ADD_SUBNET (子网)
↓
150ms 定期 PING/PONG (保活)
↓
∞ 数据转发 & 拓扑维护
11. 协议调试⌗
启用协议日志⌗
# 启用详细的协议调试输出
tincd -n myvpn -d DEBUG
# 典型输出示例:
[DEBUG] Sending ID to node1: "0 mynode 17.7"
[DEBUG] Sending METAKEY to node1: cipher=1 digest=3 maclength=16
[DEBUG] Sending CHALLENGE to node1
[DEBUG] Sending ACK to node1
[DEBUG] Sending ADD_EDGE node1 node2 192.168.1.2 655 100 0
[DEBUG] Sending ADD_SUBNET node2 172.16.2.0/24
[DEBUG] Sending PING to node1
[DEBUG] Received PONG from node1
网络包捕获⌗
# 捕获 Meta Protocol (TCP 655)
tcpdump -i eth0 -n tcp port 655
# 捕获 Packet Protocol (UDP 1194)
tcpdump -i eth0 -n udp port 1194
# 使用 Wireshark 分析
wireshark -i eth0
总结⌗
TINC 协议是一个分层的、设计精良的 VPN 协议体系:
核心设计⌗
Meta Protocol (控制平面):
- TCP 文本协议,用于控制和管理
- 握手、认证、拓扑交换
- 可靠但相对低频
Packet Protocol (数据平面):
- UDP 二进制协议,用于数据转发
- 高效、低延迟
- 支持快速加密/解密
SPTPS (加密层):
- 额外的 UDP 加密层
- 前向保密和轻量级握手
- 支持直接 P2P 加密通信
安全特性⌗
✅ 现代密钥交换 (ECDH)
✅ 强力数字签名 (ECDSA/EdDSA)
✅ 前向保密 (PFS)
✅ 重放保护
✅ 完整性认证 (HMAC/AEAD)
✅ 灵活的算法选择
性能考虑⌗
- Meta Protocol 低频交互,控制开销小
- Packet Protocol 用 UDP,转发延迟低
- 数据密钥缓存,避免频繁协商
- 支持直接 P2P(避免中继)
这种精心设计的两层协议架构使 TINC 成为一个既可靠又高效的 VPN 解决方案,是现代 VPN 软件的最佳实践。
系列预告⌗
- 第一篇:TINC 协议深度解析
- 第二篇:TINC 模块架构与代码组织
- 第三篇:UDP 打洞机制
- 第四篇:TINC 项目 - 模块依赖与数据流图