9.7 KiB
9.7 KiB
协议说明
AES加密算法说明(不同网络下的AesKey的值不一样, 服务器端网络启动的时候采用的随机生成的方式)
算法: AES256
aesKey: 长度为32个字节
iv: 长度为aesKey的前16个字节
blockMode: cbc
padding: pkcs7Padding
1. 客户端与云端的交互同时使用了TCP和UDP协议
1.1 TCP协议基础说明
协议格式: <<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协议基础说明
协议格式: <<PacketType:8, ProtobufData/binary>>
PacketType: 1字节命令编码,具体参考后面的说明
ProtobufData: 所用的message采用protobuf协议进行编码和解码
2. protobuf消息
参考文档`message.proto`里面的定义
3. PacketType编码说明
3.1 一级编码
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>>
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 基于公共类型定义
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长连接方式,因此理论上一个连接上只需要请求一次;服务器端会绑定相关信息
请求:
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
请求:
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连接
请求:
<<0:32, 0x08>>
5.4 主动上报当前节点的流量信息
服务器端无返回, 请求是的packetId值必须是: 0
请求:
message SDLFlows {
// 服务器转发流量
uint32 forward_num = 1;
// p2p直接流量
uint32 p2p_num = 2;
// 接收的流量
uint32 inbound_num = 3;
}
5.5 命令下发
消息格式: <<Len:16, PacketId:32, 0x11:8, Command子类型:8, ProtobufOfEvent/binary>>
5.6 命令回复
注意Ack里面的PacketId的值必须和下发命令时的PacketId值一致
消息格式: <<Len:16, PacketId:32, 0x12:8, ProtobufOfEventAck/binary>>
5.7 Event下发
客户端在收到Event后,不需要回复Ack信息
消息格式: <<Len:16, 0:32, 0x10:8, Event子类型:8, ProtobufOfEvent/binary>>
5.8 Unregister取消注册
无返回,服务器端收到后会关闭掉当前连接
请求:
<<0:32, 0x05>>
6. UDP交互
6.1 StunRequest请求(10s发送一次)
请求:
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请求
请求:
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请求
消息体: 其中只有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;
}