347 lines
9.7 KiB
Markdown
347 lines
9.7 KiB
Markdown
# 协议说明
|
||
|
||
## AES加密算法说明(不同网络下的AesKey的值不一样, 服务器端网络启动的时候采用的随机生成的方式)
|
||
|
||
```text
|
||
算法: AES256
|
||
aesKey: 长度为32个字节
|
||
iv: 长度为aesKey的前16个字节
|
||
blockMode: cbc
|
||
padding: pkcs7Padding
|
||
```
|
||
## 1. 客户端与云端的交互同时使用了TCP和UDP协议
|
||
|
||
### 1.1 TCP协议基础说明
|
||
```text
|
||
协议格式: <<Len:16, PacketId:32, PacketType:8, ProtobufData/binary>>
|
||
|
||
Len: tcp数据流采用2个字节长度作为分包协议, Len的长度为后面的二进制的字节数
|
||
PacketId: 4字节用来标识包ID,用来对应请求和响应; 对于不需要返回值的命令,PacketId的值必须是0
|
||
PacketType: 1字节命令编码,具体参考后面的说明
|
||
ProtobufData: 所用的message采用protobuf协议进行编码和解码
|
||
```
|
||
### 1.2 UDP协议基础说明
|
||
|
||
```text
|
||
协议格式: <<PacketType:8, ProtobufData/binary>>
|
||
|
||
PacketType: 1字节命令编码,具体参考后面的说明
|
||
ProtobufData: 所用的message采用protobuf协议进行编码和解码
|
||
```
|
||
|
||
## 2. protobuf消息
|
||
参考文档`message.proto`里面的定义
|
||
|
||
## 3. PacketType编码说明
|
||
|
||
### 3.1 一级编码
|
||
```text
|
||
enum CommandType: UInt8 {
|
||
// 为了建立完整的请求和响应的对应关系,部分请求没有数据返回时;服务器端返回空数据
|
||
case empty = 0x00
|
||
|
||
case registerSuper = 0x01
|
||
case registerSuperAck = 0x02
|
||
case registerSuperNak = 0x04
|
||
|
||
case unregisterSuper = 0x05
|
||
|
||
case queryInfo = 0x06
|
||
case peerInfo = 0x07
|
||
|
||
// TCP连接需要心跳机制来保持,客户端需要定时向服务器端发送心跳包
|
||
case ping = 0x08
|
||
case pong = 0x09
|
||
|
||
// 事件类型, 服务器端主动推送到客户端的事件;客户端在收到Event后,不需要向服务端发送Ack
|
||
case event = 0x10
|
||
|
||
// 推送命令消息, 服务器端主动推送到客户端的命令; 需要返回值(管理后台的部分操作需要反馈信息)
|
||
case command = 0x11
|
||
case commandAck = 0x12
|
||
|
||
// 流量统计, 客户端统计的端上的流量信息;定期上报即可;服务器端收到后没有返回值
|
||
case flowTracer = 0x15
|
||
|
||
// 客户端之间相互打洞
|
||
case register = 0x20
|
||
case registerAck = 0x21
|
||
|
||
// 客户端通过UDP周期性上报自己的Nat信息;需要依靠该方式保持客户端在Nat的洞不会被Nat设备关闭
|
||
case stunRequest = 0x30
|
||
case stunReply = 0x31
|
||
|
||
// 客户端通过UDP请求判断自己的Nat类型,并且在stunRequest请求中上报
|
||
case stunProbe = 0x32
|
||
case stunProbeReply = 0x33
|
||
|
||
// 数据类型
|
||
case data = 0xFF
|
||
}
|
||
```
|
||
### 3.2 二级编码(Event和Command指令存在二级编码)
|
||
二级编码占用1个字节长度,紧跟在一级编码的后面,即: <<Len:16, PacketId:32, PacketType:8, 二级编码:8, ProtobufData/binary>>
|
||
|
||
```text
|
||
Event编码
|
||
|
||
enum SDLEventType: UInt8 {
|
||
// 有新的ip加入到当前网络
|
||
case knownIp = 0x01
|
||
// ip地址离开当前网络
|
||
case dropIp = 0x02
|
||
// ip地址对应的nat信息发生了编码,需要重新打洞
|
||
case natChanged = 0x03
|
||
// 需要发送打洞请求
|
||
case sendRegister = 0x04
|
||
// 网络关闭
|
||
case networkShutdown = 0xFF
|
||
}
|
||
|
||
Command编码
|
||
|
||
enum SDLCommandType: UInt8 {
|
||
// 网络地址改变,当node被move的时候网络会发生改变
|
||
case changeNetwork = 0x01
|
||
}
|
||
```
|
||
|
||
## 4. 交互说明
|
||
|
||
### 4.1 基于公共类型定义
|
||
```text
|
||
|
||
message SDLV4Info {
|
||
uint32 port = 1;
|
||
bytes v4 = 2;
|
||
uint32 nat_type = 3;
|
||
}
|
||
|
||
// ipv6信息,目前未支持!!
|
||
message SDLV6Info {
|
||
uint32 port = 1;
|
||
bytes v6 = 2;
|
||
}
|
||
|
||
// 设备网络地址信息
|
||
message SDLDevAddr {
|
||
uint32 network_id = 1;
|
||
uint32 net_addr = 2;
|
||
uint32 net_bit_len = 3;
|
||
}
|
||
|
||
```
|
||
|
||
### 5. TCP交互
|
||
|
||
### 5.1 客户端建立到服务端后,需要先发送RegisterSuper消息
|
||
由于时基于tcp长连接方式,因此理论上一个连接上只需要请求一次;服务器端会绑定相关信息
|
||
```text
|
||
请求:
|
||
|
||
message SDLRegisterSuper {
|
||
uint32 version = 1;
|
||
string installed_channel = 2;
|
||
string client_id = 3;
|
||
SDLDevAddr dev_addr = 4;
|
||
string pub_key = 5;
|
||
string token = 6;
|
||
}
|
||
|
||
响应:
|
||
|
||
// 服务器验证通过后返回
|
||
message SDLRegisterSuperAck {
|
||
SDLDevAddr dev_addr = 1;
|
||
bytes aes_key = 2;
|
||
bytes known_ips = 3;
|
||
uint32 upgrade_type = 4;
|
||
optional string upgrade_prompt = 5;
|
||
optional string upgrade_address = 6;
|
||
}
|
||
|
||
// 服务验证失败返回
|
||
message SDLRegisterSuperNak {
|
||
uint32 error_code = 1;
|
||
string error_message = 2;
|
||
}
|
||
|
||
```
|
||
|
||
### 5.2 查询ip对应的PeerInfo
|
||
```text
|
||
请求:
|
||
message SDLQueryInfo {
|
||
uint32 dst_ip = 1;
|
||
}
|
||
|
||
响应:
|
||
成功:
|
||
|
||
// 目前未支持ipv6,因此SDLV6Info的值为空
|
||
message SDLPeerInfo {
|
||
SDLV4Info v4_info = 1;
|
||
optional SDLV6Info v6_info = 2;
|
||
}
|
||
|
||
失败:
|
||
返回empty: <<Len:16, PacketId:32, 0x00>>
|
||
```
|
||
|
||
### 5.3 客户端主动发起Ping
|
||
服务器端无返回, 服务器在15秒内没有收到任何ping包;会关闭掉当前的tcp连接
|
||
```text
|
||
请求:
|
||
<<0:32, 0x08>>
|
||
```
|
||
|
||
### 5.4 主动上报当前节点的流量信息
|
||
服务器端无返回, 请求是的packetId值必须是: 0
|
||
```text
|
||
请求:
|
||
message SDLFlows {
|
||
// 服务器转发流量
|
||
uint32 forward_num = 1;
|
||
// p2p直接流量
|
||
uint32 p2p_num = 2;
|
||
// 接收的流量
|
||
uint32 inbound_num = 3;
|
||
}
|
||
|
||
```
|
||
|
||
### 5.5 命令下发
|
||
```text
|
||
消息格式: <<Len:16, PacketId:32, 0x11:8, Command子类型:8, ProtobufOfEvent/binary>>
|
||
```
|
||
|
||
### 5.6 命令回复
|
||
注意Ack里面的PacketId的值必须和下发命令时的PacketId值一致
|
||
```text
|
||
消息格式: <<Len:16, PacketId:32, 0x12:8, ProtobufOfEventAck/binary>>
|
||
```
|
||
|
||
### 5.7 Event下发
|
||
客户端在收到Event后,不需要回复Ack信息
|
||
```text
|
||
消息格式: <<Len:16, 0:32, 0x10:8, Event子类型:8, ProtobufOfEvent/binary>>
|
||
```
|
||
|
||
### 5.8 Unregister取消注册
|
||
无返回,服务器端收到后会关闭掉当前连接
|
||
```text
|
||
请求:
|
||
<<0:32, 0x05>>
|
||
```
|
||
|
||
## 6. UDP交互
|
||
|
||
### 6.1 StunRequest请求(10s发送一次)
|
||
```text
|
||
请求:
|
||
message SDLStunRequest {
|
||
uint32 cookie = 1;
|
||
string client_id = 2;
|
||
uint32 network_id = 3;
|
||
uint32 ip = 4;
|
||
uint32 nat_type = 5;
|
||
}
|
||
|
||
响应:
|
||
message SDLStunReply {
|
||
uint32 cookie = 1;
|
||
}
|
||
```
|
||
|
||
### 6.2 StunProbe请求
|
||
|
||
```text
|
||
请求:
|
||
message SDLStunProbe {
|
||
uint32 cookie = 1;
|
||
uint32 attr = 2;
|
||
}
|
||
|
||
响应:
|
||
message SDLStunProbeReply {
|
||
uint32 cookie = 1;
|
||
uint32 port = 2;
|
||
uint32 ip = 3;
|
||
}
|
||
|
||
Attr值的说明:
|
||
enum SDLProbeAttr: UInt8 {
|
||
// 正常响应
|
||
case none = 0
|
||
// 服务器在收到消息,用相同IP地址,但是Port不相同的Socket响应
|
||
case port = 1
|
||
// 服务器在收到消息,同时改变IP地址和Port的Socket响应
|
||
case peer = 2
|
||
}
|
||
|
||
Nat类型说明:
|
||
enum NatType: UInt8, Encodable {
|
||
case blocked = 0 // 网络不通
|
||
case noNat = 1 // 当前设备在公网IP下
|
||
case fullCone = 2 // 完全对称型Nat
|
||
case portRestricted = 3 // 端口限制型
|
||
case coneRestricted = 4 // Ip限制型
|
||
case symmetric = 5 // 完全对称型
|
||
}
|
||
Nat类型的判断逻辑
|
||
func getNatType() async -> NatType {
|
||
let addressArray = config.stunProbeSocketAddressArray
|
||
// step1: ip1:port1 <---- ip1:port1
|
||
guard let natAddress1 = await getNatAddress(remoteAddress: addressArray[0][0], attr: .none) else {
|
||
return .blocked
|
||
}
|
||
|
||
// 网络没有在nat下
|
||
if natAddress1 == self.udpHole?.localAddress {
|
||
return .noNat
|
||
}
|
||
|
||
// step2: ip2:port2 <---- ip2:port2
|
||
guard let natAddress2 = await getNatAddress(remoteAddress: addressArray[1][1], attr: .none) else {
|
||
return .blocked
|
||
}
|
||
|
||
// 如果natAddress2 的IP地址与上次回来的IP是不一样的,它就是对称型NAT; 这次的包也一定能发成功并收到
|
||
// 如果ip地址变了,这说明{dstIp, dstPort, srcIp, srcPort}, 其中有一个变了;则用新的ip地址
|
||
NSLog("nat_address1: \(natAddress1), nat_address2: \(natAddress2)")
|
||
if let ipAddress1 = natAddress1.ipAddress, let ipAddress2 = natAddress2.ipAddress, ipAddress1 != ipAddress2 {
|
||
return .symmetric
|
||
}
|
||
|
||
// step3: ip1:port1 <---- ip2:port2 (ip地址和port都变的情况)
|
||
// 如果能收到的,说明是完全锥形 说明是IP地址限制锥型NAT,如果不能收到说明是端口限制锥型。
|
||
if let natAddress3 = await getNatAddress(remoteAddress: addressArray[0][0], attr: .peer) {
|
||
NSLog("nat_address1: \(natAddress1), nat_address2: \(natAddress2), nat_address3: \(natAddress3)")
|
||
return .fullCone
|
||
}
|
||
|
||
// step3: ip1:port1 <---- ip1:port2 (port改变情况)
|
||
// 如果能收到的说明是IP地址限制锥型NAT,如果不能收到说明是端口限制锥型。
|
||
if let natAddress4 = await getNatAddress(remoteAddress: addressArray[0][0], attr: .port) {
|
||
NSLog("nat_address1: \(natAddress1), nat_address2: \(natAddress2), nat_address4: \(natAddress4)")
|
||
return .coneRestricted
|
||
} else {
|
||
return .portRestricted
|
||
}
|
||
}
|
||
```
|
||
|
||
### 6.3 StunData请求
|
||
```text
|
||
消息体: 其中只有data字段里面的数据使用了aes加密
|
||
|
||
message SDLData {
|
||
uint32 network_id = 1;
|
||
uint32 src_ip = 2;
|
||
uint32 dst_ip = 3;
|
||
bool is_p2p = 4;
|
||
uint32 ttl = 5;
|
||
bytes data = 6;
|
||
}
|
||
|
||
``` |