# 协议说明 ## AES加密算法说明(不同网络下的AesKey的值不一样, 服务器端网络启动的时候采用的随机生成的方式) ```text 算法: AES256 aesKey: 长度为32个字节 iv: 长度为aesKey的前16个字节 blockMode: cbc padding: pkcs7Padding ``` ## 1. 客户端与云端的交互同时使用了TCP和UDP协议 ### 1.1 TCP协议基础说明 ```text 协议格式: <> Len: tcp数据流采用2个字节长度作为分包协议, Len的长度为后面的二进制的字节数 PacketId: 4字节用来标识包ID,用来对应请求和响应; 对于不需要返回值的命令,PacketId的值必须是0 PacketType: 1字节命令编码,具体参考后面的说明 ProtobufData: 所用的message采用protobuf协议进行编码和解码 ``` ### 1.2 UDP协议基础说明 ```text 协议格式: <> 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个字节长度,紧跟在一级编码的后面,即: <> ```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: <> ``` ### 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 消息格式: <> ``` ### 5.6 命令回复 注意Ack里面的PacketId的值必须和下发命令时的PacketId值一致 ```text 消息格式: <> ``` ### 5.7 Event下发 客户端在收到Event后,不需要回复Ack信息 ```text 消息格式: <> ``` ### 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; } ```