sdlan/Protocol.md
2025-05-12 11:54:24 +08:00

9.7 KiB
Raw Permalink Blame History

协议说明

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;
}