From 153d8e7ec55608deb604601c397436c54f57d6ea Mon Sep 17 00:00:00 2001 From: anlicheng <244108715@qq.com> Date: Sat, 30 May 2026 17:08:50 +0800 Subject: [PATCH] fix docs --- Protocol.md | 980 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 700 insertions(+), 280 deletions(-) diff --git a/Protocol.md b/Protocol.md index c0f969f..b06f764 100644 --- a/Protocol.md +++ b/Protocol.md @@ -1,347 +1,767 @@ -# 协议说明 +# SDLAN 协议说明 -## AES加密算法说明(不同网络下的AesKey的值不一样, 服务器端网络启动的时候采用的随机生成的方式) +本文档描述 Client 端和当前服务端的交互协议。当前控制面支持两种接入方式: + +- QUIC 接入:实现位于 `src/quic`,主入口为 `sdlan_quic_server` 和 `sdlan_quic_transport`。 +- SSL/TLS 接入:实现位于 `src/ssl`,主入口为 `sdlan_ssl_server` 和 `sdlan_ssl_transport`。 + +两种接入方式在传输层不同,但进入 `sdlan_session` 后使用同一套应用层包类型和 protobuf 消息。Client 完成控制面注册后,还需要通过 UDP STUN 服务上报 NAT 映射并承载节点之间的数据转发。 + +## 1. 加密说明 + +服务端在网络启动时为每个网络生成独立密钥,Client 通过 `RegisterSuper` 响应拿到当前网络的加密参数。 ```text -算法: AES256 -aesKey: 长度为32个字节 -iv: 长度为aesKey的前16个字节 -blockMode: cbc -padding: pkcs7Padding -``` -## 1. 客户端与云端的交互同时使用了TCP和UDP协议 +algorithm: + aes AES-256 + chacha20 ChaCha20 -### 1.1 TCP协议基础说明 -```text -协议格式: <> +key: + 长度 32 字节。 + RegisterSuperAck.key 会使用客户端 RegisterSuper.pub_key 中的 RSA 公钥加密。 -Len: tcp数据流采用2个字节长度作为分包协议, Len的长度为后面的二进制的字节数 -PacketId: 4字节用来标识包ID,用来对应请求和响应; 对于不需要返回值的命令,PacketId的值必须是0 -PacketType: 1字节命令编码,具体参考后面的说明 -ProtobufData: 所用的message采用protobuf协议进行编码和解码 -``` -### 1.2 UDP协议基础说明 +AES 参数: + blockMode: cbc + padding: pkcs7Padding + iv: key 的前 16 字节 -```text -协议格式: <> - -PacketType: 1字节命令编码,具体参考后面的说明 -ProtobufData: 所用的message采用protobuf协议进行编码和解码 +ChaCha20 参数: + key: RegisterSuperAck.key 解密后的 32 字节密钥 + region_id: RegisterSuperAck.region_id,用于客户端侧逻辑分区/nonce 生成 ``` -## 2. protobuf消息 - 参考文档`message.proto`里面的定义 +`SDLData.data` 字段承载隧道中的原始二层/三层数据,Client 需要按 `RegisterSuperAck.algorithm` 和 `key` 做加解密;包头中的 `network_id`、`src_mac`、`dst_mac`、`ttl`、`session_token`、`identity_id` 不参与该字段加密。 -## 3. PacketType编码说明 +## 2. 通用应用层帧 -### 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个字节长度,紧跟在一级编码的后面,即: <> +QUIC 和 SSL/TLS 接入最终交给 `sdlan_session:handle_frame/2` 的应用层帧格式一致: ```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 - } +Frame = <> ``` -## 4. 交互说明 +- `PacketType`:1 字节包类型,取值见第 3 节。 +- `ProtobufPayload`:protobuf 编码后的消息体。没有消息体的包只包含 `PacketType`。 +- Event 和 Command 当前已经改为 protobuf `oneof` 结构,不再使用旧文档中的二级编码字节。 + +## 3. 包类型定义 + +包类型以 `include/sdlan.hrl` 为准。 + +| 宏 | 值 | 方向 | 消息 | 说明 | +| --- | --- | --- | --- | --- | +| `PACKET_EMPTY` | `0x00` | 双向 | 无 | 空包,当前主流程未使用。 | +| `PACKET_REGISTER_SUPER` | `0x01` | Client -> Server | `SDLRegisterSuper` | 控制连接注册,必须在收到 Welcome 后发送。 | +| `PACKET_REGISTER_SUPER_ACK` | `0x02` | Server -> Client | `SDLRegisterSuperAck` | 注册成功,返回网络加密参数和 `session_token`。 | +| `PACKET_REGISTER_SUPER_NAK` | `0x04` | Server -> Client | `SDLRegisterSuperNak` | 注册失败,返回错误码和错误消息,随后连接关闭。 | +| `PACKET_UNREGISTER` | `0x05` | Client -> Server | 无 | 主动注销当前控制连接,服务端清理网络绑定后关闭连接。 | +| `PACKET_QUERY_INFO` | `0x06` | Client -> Server | `SDLQueryInfo` | 查询目标 MAC 的 NAT/IPv6 信息,用于发起 P2P 打洞。 | +| `PACKET_PEER_INFO` | `0x07` | Server -> Client | `SDLPeerInfo` | `QUERY_INFO` 的响应。找不到目标时返回空 `v4_info/v6_info`。 | +| `PACKET_PING` | `0x08` | Client -> Server | 无 | 控制连接心跳。 | +| `PACKET_PONG` | `0x09` | Server -> Client | 无 | 心跳响应。 | +| `PACKET_EVENT` | `0x10` | Server -> Client | `SDLEvent` | 服务端主动事件推送,Client 不需要 Ack。 | +| `PACKET_COMMAND` | `0x11` | Server -> Client | `SDLCommand` | 服务端主动命令推送,需要 Client 回 `COMMAND_ACK`。 | +| `PACKET_COMMAND_ACK` | `0x12` | Client -> Server | `SDLCommandAck` | 命令处理结果,`pkt_id` 必须等于 `SDLCommand.pkt_id`。 | +| `PACKET_FLOW_TRACER` | `0x15` | Client -> Server | 历史保留 | `sdlan_session` 当前未处理该包。 | +| `PACKET_REGISTER` | `0x20` | Client <-> Client | `SDLRegister` | 节点之间 UDP 打洞握手请求。 | +| `PACKET_REGISTER_ACK` | `0x21` | Client <-> Client | `SDLRegisterAck` | 节点之间 UDP 打洞握手响应。 | +| `PACKET_STUN_REQUEST` | `0x30` | Client -> STUN | `SDLStunRequest` | 周期上报 NAT 映射和 IPv6 信息,同时维持 NAT 映射。 | +| `PACKET_STUN_REPLY` | `0x31` | STUN -> Client | `SDLStunReply` | STUN 心跳响应。 | +| `PACKET_STUN_PROBE` | `0x32` | Client -> STUN | `SDLStunProbe` | NAT 类型探测请求。 | +| `PACKET_STUN_PROBE_REPLY` | `0x33` | STUN -> Client | `SDLStunProbeReply` | NAT 类型探测响应,返回服务端看到的公网 IP/端口。 | +| `PACKET_STUN_PROBE_RELAY` | `0x3a` | STUN 内部 | 内部二进制 | STUN 辅助节点内部转发探测响应,Client 不直接使用。 | +| `PACKET_WELCOME` | `0x4f` | Server -> Client | `SDLWelcome` | 控制连接建立后服务端首先下发的欢迎包。 | +| `PACKET_ARP_REQUEST` | `0x50` | Client -> Server | `SDLArpRequest` | 查询虚拟网络 IP 对应的 MAC。 | +| `PACKET_ARP_RESPONSE` | `0x51` | Server -> Client | `SDLArpResponse` | ARP 查询响应。 | +| `PACKET_POLICY_REQUEST` | `0xb0` | Client -> Server | `SDLPolicyRequest` | 查询源身份到目标身份的访问规则。 | +| `PACKET_POLICY_REPLY` | `0xb1` | Server -> Client | `SDLPolicyResponse` | 权限规则响应。 | +| `PACKET_EXPOSED_SERVICE_REQUEST` | `0xb2` | Client -> Server | `SDLExposedServiceRequest` | 查询当前节点暴露服务端口列表。 | +| `PACKET_EXPOSED_SERVICE_RESPONSE` | `0xb3` | Server -> Client | `SDLExposedServiceResponse` | 暴露服务端口响应。 | +| `PACKET_STUN_DATA` | `0xff` | Client <-> STUN/Client | `SDLData` | UDP 数据包,支持服务端转发和 P2P 直连。 | + +STUN 探测属性同样定义在 `include/sdlan.hrl`: -### 4.1 基于公共类型定义 ```text +STUN_ATTR_CHANGE_NONE = 0 使用收到请求的同一个 socket 响应 +STUN_ATTR_CHANGE_PORT = 1 通过 stun_peer_assist 路径转发响应,通常用于测试变化 IP/端口路径 +STUN_ATTR_CHANGE_PEER = 2 通过 stun_port_assist 路径响应,通常用于测试变化端口路径 +``` +## 4. Protobuf 消息 + +protobuf 定义以 `proto/sdlan.proto` 为准,Erlang 生成文件为 `src/sdlan_pb.erl` 和 `include/sdlan_pb.hrl`。 + +### 4.1 公共类型 + +```protobuf 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交互 +- `SDLV4Info.port`:服务端观察到的客户端 NAT 端口。 +- `SDLV4Info.v4`:4 字节 IPv4 地址,网络字节序。 +- `SDLV4Info.nat_type`:Client 上报的 NAT 类型。 +- `SDLV6Info.port`:IPv6 辅助通道端口。 +- `SDLV6Info.v6`:16 字节 IPv6 地址。 + +### 4.2 NAT 类型约定 -### 5.1 客户端建立到服务端后,需要先发送RegisterSuper消息 - 由于时基于tcp长连接方式,因此理论上一个连接上只需要请求一次;服务器端会绑定相关信息 ```text -请求: +0 blocked 网络不可达 +1 noNat 当前设备在公网地址下 +2 fullCone Full Cone NAT +3 portRestricted Port Restricted Cone NAT +4 coneRestricted Restricted Cone NAT +5 symmetric Symmetric NAT +``` -message SDLRegisterSuper { +## 5. QUIC 接入 + +QUIC 接入代码位于 `src/quic`。服务端监听配置来自 `quic_server`: + +- 默认端口:`443` +- ALPN:`punchnet/1.0` +- 服务端允许 1 条双向 stream:`peer_bidi_stream_count => 1` +- 默认最大应用包:`max_packet_size = 16384` +- 默认心跳间隔:`heartbeat_sec = 15` + +### 5.1 QUIC 传输帧 + +QUIC 在单条双向 stream 上承载控制面消息。stream 上的实际传输格式为: + +```text +QuicStreamData = <> +Frame = <> +``` + +- `Len`:2 字节无符号长度,表示后续 `Frame` 字节数,不包含 `Len` 自身。 +- `Frame`:第 2 节定义的通用应用层帧。 +- 服务端接收时支持一次 QUIC data 中包含多个完整 frame,也支持半包累积。 +- 当 `Len > max_packet_size` 时,服务端关闭连接,原因是 `frame_too_large`。 + +### 5.2 QUIC 连接建立流程 + +1. Client 与服务端完成 QUIC/TLS 握手,ALPN 必须匹配 `punchnet/1.0`。 +2. Client 打开一条双向 stream。 +3. 服务端接受第一条 stream 后立即发送 `PACKET_WELCOME`。 +4. Client 收到 Welcome 后发送 `PACKET_REGISTER_SUPER`。 +5. 注册成功后服务端返回 `PACKET_REGISTER_SUPER_ACK` 并进入 `registered` 状态。 +6. 注册失败时服务端返回 `PACKET_REGISTER_SUPER_NAK`,随后关闭连接。 +7. 进入 `registered` 后,Client 必须按 Welcome 中的 `heartbeat_sec` 周期发送 `PACKET_PING`。 + +### 5.3 QUIC Welcome + +```text +Server -> Client +<> +``` + +```protobuf +message SDLWelcome { uint32 version = 1; - string installed_channel = 2; - string client_id = 3; - SDLDevAddr dev_addr = 4; - string pub_key = 5; - string token = 6; + uint32 max_bidi_streams = 2; + uint32 max_packet_size = 3; + uint32 heartbeat_sec = 4; + SDLV6Info ipv6_assist = 5; } +``` -响应: +- `version`:协议版本,当前为 `1`。 +- `max_bidi_streams`:服务端允许的双向 stream 数,当前为 `1`。 +- `max_packet_size`:单个应用层 frame 最大长度。 +- `heartbeat_sec`:心跳间隔,Client 应以小于或等于该值的周期发送 Ping。 +- `ipv6_assist`:IPv6 辅助器地址。未配置时为空。 -// 服务器验证通过后返回 +### 5.4 QUIC RegisterSuper + +```text +Client -> Server +<> +``` + +```protobuf +message SDLRegisterSuper { + string client_id = 1; + uint32 network_id = 2; + bytes mac = 3; + uint32 ip = 4; + uint32 mask_len = 5; + string hostname = 6; + string pub_key = 7; + string access_token = 8; +} +``` + +- `client_id`:客户端节点 ID,不能为空。 +- `network_id`:客户端要加入的网络 ID。 +- `mac`:虚拟网卡 MAC,不能为空,且不能是广播或组播 MAC。 +- `ip`:HTTP 接口已分配的虚拟网络 IPv4,按 32 位整数传输。 +- `mask_len`:虚拟网络掩码长度。 +- `hostname`:客户端主机名,服务端用于维护域名映射。 +- `pub_key`:客户端 RSA 公钥 PEM,服务端用它加密网络密钥。 +- `access_token`:客户端通过 HTTP 登录或令牌换取的访问凭证,服务端注册时调用 API 校验。 + +注册成功: + +```text +Server -> Client +<> +``` + +```protobuf 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; + string algorithm = 1; + bytes key = 2; + uint32 region_id = 3; + bytes session_token = 4; } +``` -// 服务验证失败返回 +- `algorithm`:当前网络加密算法,取值为 `aes` 或 `chacha20`。 +- `key`:使用 `pub_key` 加密后的网络密钥,Client 需要用私钥解密。 +- `region_id`:服务端根据虚拟 IP 生成的逻辑分区 ID。 +- `session_token`:本次控制会话 token。后续 UDP `SDLStunRequest` 和 `SDLData` 必须携带,用于校验端和网络绑定关系。 + +注册失败: + +```text +Server -> Client +<> +``` + +```protobuf message SDLRegisterSuperNak { uint32 error_code = 1; string error_message = 2; } - ``` -### 5.2 查询ip对应的PeerInfo +- `error_code`:错误码。网络/API 错误当前使用 `4`,服务内部错误当前使用 `5`,API 返回业务错误时透传 API code。 +- `error_message`:错误描述。服务端发送 NAK 后关闭连接。 + +### 5.5 QUIC 心跳 + ```text -请求: - message SDLQueryInfo { - uint32 dst_ip = 1; +Client -> Server +<> + +Server -> Client +<> +``` + +- Client 只在注册成功后发送 Ping。 +- 服务端收到 Ping 后立即回复 Pong,并记录本轮心跳已收到。 +- 服务端按 `heartbeat_sec` 检查上一周期是否收到过 Ping;如果没有收到,关闭连接。 + +### 5.6 QUIC 查询 PeerInfo + +```text +Client -> Server +<> +``` + +```protobuf +message SDLQueryInfo { + bytes dst_mac = 1; +} +``` + +- `dst_mac`:目标节点 MAC。 +- 服务端查找目标节点的 NAT 映射和 IPv6 信息。 +- 如果目标存在,服务端同时会向目标节点推送 `SDLEvent.SendRegister`,让目标主动向查询方发送打洞包,提高 P2P 成功率。 + +响应: + +```text +Server -> Client +<> +``` + +```protobuf +message SDLPeerInfo { + bytes dst_mac = 1; + optional SDLV4Info v4_info = 2; + optional SDLV6Info v6_info = 3; +} +``` + +- `dst_mac`:原样返回查询的目标 MAC。 +- `v4_info`:目标节点最近一次 `STUN_REQUEST` 上报形成的公网 IPv4/端口和 NAT 类型。 +- `v6_info`:目标节点上报的 IPv6 辅助信息。没有时为空。 +- 找不到目标或目标没有可用 NAT 信息时,服务端仍返回 `SDLPeerInfo`,但 `v4_info` 和 `v6_info` 为空。 + +### 5.7 QUIC ARP 查询 + +```text +Client -> Server +<> +``` + +```protobuf +message SDLArpRequest { + uint32 target_ip = 1; + uint32 origin_ip = 2; + bytes context = 3; +} +``` + +- `target_ip`:要查询 MAC 的虚拟网络 IPv4。 +- `origin_ip`:发起查询的虚拟网络 IPv4。 +- `context`:Client 自定义上下文,服务端原样回写,方便客户端匹配本地 ARP 请求。 + +响应: + +```text +Server -> Client +<> +``` + +```protobuf +message SDLArpResponse { + uint32 target_ip = 1; + bytes target_mac = 2; + uint32 origin_ip = 3; + bytes context = 4; +} +``` + +- `target_mac`:查询成功时为目标 MAC;查询失败时为空字节串。 +- 其它字段与请求一致或对应请求上下文。 + +### 5.8 QUIC Policy 查询 + +```text +Client -> Server +<> +``` + +```protobuf +message SDLPolicyRequest { + uint32 src_identity_id = 1; + uint32 dst_identity_id = 2; + uint32 version = 3; +} +``` + +- `src_identity_id`:源端身份 ID。 +- `dst_identity_id`:目标端身份 ID。 +- `version`:Client 侧规则版本,服务端原样回写,Client 用于判断是否覆盖本地缓存。 + +响应: + +```text +Server -> Client +<> +``` + +```protobuf +message SDLPolicyResponse { + uint32 src_identity_id = 1; + uint32 dst_identity_id = 2; + uint32 version = 3; + bytes rules = 4; +} +``` + +- `rules`:稀疏序列化规则列表,每条规则格式为 `<>`。 +- `Proto`:协议号,例如 TCP/UDP 对应的协议编号。 +- `Port`:允许访问的目标端口。 +- 服务端只下发 allow 规则,deny 规则在服务端侧已过滤。 +- 如果请求解析或规则查询失败,当前服务端可能不返回响应包。 + +### 5.9 QUIC ExposedService 查询 + +```text +Client -> Server +<> +``` + +```protobuf +message SDLExposedServiceRequest { + uint32 version = 1; +} +``` + +- `version`:Client 当前缓存版本,服务端响应中原样返回。 + +响应: + +```text +Server -> Client +<> +``` + +```protobuf +message SDLExposedServiceResponse { + uint32 version = 1; + repeated uint32 tcp_ports = 2; + repeated uint32 udp_ports = 3; +} +``` + +- `tcp_ports`:当前节点允许暴露的 TCP 端口列表。 +- `udp_ports`:当前节点允许暴露的 UDP 端口列表。 +- 服务端只返回 `1..65534` 范围内的端口。 +- API 查询失败时当前服务端可能不返回响应包。 + +### 5.10 QUIC Event 推送 + +```text +Server -> Client +<> +``` + +```protobuf +message SDLEvent { + message NatChanged { + bytes mac = 1; + uint32 ip = 2; } -响应: - 成功: - - // 目前未支持ipv6,因此SDLV6Info的值为空 - message SDLPeerInfo { - SDLV4Info v4_info = 1; - optional SDLV6Info v6_info = 2; + message SendRegister { + bytes dst_mac = 1; + uint32 nat_ip = 2; + uint32 nat_port = 3; + uint32 nat_type = 4; + optional SDLV6Info v6_info = 5; } - 失败: - 返回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; + message ExposedServiceChanged { } -``` - -### 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 NetworkShutdown { + string message = 1; } - -响应: - message SDLStunReply { - uint32 cookie = 1; + + oneof event { + NatChanged nat_changed = 1; + SendRegister send_register = 2; + NetworkShutdown shutdown = 3; + ExposedServiceChanged exposed_service_changed = 4; } +} ``` -### 6.2 StunProbe请求 +- `nat_changed`:某个 MAC 的虚拟 IP 或 NAT 映射发生变化。Client 应清理对应 peer 缓存、ARP 缓存或重新查询 peer 信息。 +- `send_register`:服务端要求当前 Client 向 `dst_mac` 对应节点发送 UDP `PACKET_REGISTER` 打洞请求。`nat_ip/nat_port/nat_type/v6_info` 是目标可达地址信息。 +- `exposed_service_changed`:当前节点暴露服务配置变化。Client 应重新发送 `PACKET_EXPOSED_SERVICE_REQUEST` 获取端口列表。 +- `shutdown`:网络关闭。Client 应停止当前网络会话并释放本地资源。 +- Event 不需要 Client 回复 Ack。 + +### 5.11 QUIC Command 下发和 ACK ```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 - } - } +Server -> Client +<> ``` -### 6.3 StunData请求 -```text -消息体: 其中只有data字段里面的数据使用了aes加密 +```protobuf +message SDLCommand { + uint32 pkt_id = 1; + message ExitNodeControl { + int32 action = 1; + string remark = 2; + } + + oneof command { + ExitNodeControl exit_node = 2; + } +} +``` + +- `pkt_id`:服务端生成的命令序号,Client 回复时必须原样带回。 +- `exit_node.action`:出口节点控制动作,具体动作值由业务层约定。 +- `exit_node.remark`:操作备注或调试说明。 + +Client 处理完成后回复: + +```text +Client -> Server +<> +``` + +```protobuf +message SDLCommandAck { + uint32 pkt_id = 1; + int32 code = 2; + string message = 3; + bytes data = 4; +} +``` + +- `pkt_id`:必须与收到的 `SDLCommand.pkt_id` 一致。 +- `code`:处理结果码,建议 `0` 表示成功,非 `0` 表示失败。 +- `message`:处理结果说明。 +- `data`:可选业务数据。 +- 服务端收到 ACK 后,会把结果转发给等待该命令响应的内部调用方。 + +### 5.12 QUIC Unregister + +```text +Client -> Server +<> +``` + +- Client 主动退出网络时发送。 +- 服务端收到后调用 `sdlan_network:unregister/3` 清理当前 `client_id + mac` 的绑定,并关闭控制连接。 + +## 6. SSL/TLS 接入 + +SSL/TLS 接入代码位于 `src/ssl`。服务端通过 Ranch SSL 监听,配置来自 `ssl_server`: + +- 默认端口:`1443` +- ALPN:`punchnet/1.0` +- TLS 版本:`tlsv1.3`、`tlsv1.2` +- 默认最大应用包:`max_packet_size = 16384` +- 默认心跳间隔:`heartbeat_sec = 15` + +### 6.1 SSL/TLS 传输帧 + +SSL/TLS 接入使用 Erlang socket `{packet, 2}` 分包。对非 Erlang Client 来说,线上格式仍然是: + +```text +TlsStreamData = <> +Frame = <> +``` + +- `Len`:2 字节无符号长度,表示后续 `Frame` 字节数,不包含 `Len` 自身。 +- 服务端 `ssl` 层会自动剥离 `Len` 后把 `Frame` 交给 `sdlan_session`。 +- 服务端发送时也依赖 `{packet, 2}` 自动加长度前缀。 +- 单个发送包长度必须不超过 `65535`,业务上应遵守 Welcome 中的 `max_packet_size`。 + +### 6.2 SSL/TLS 连接建立流程 + +1. Client 与服务端建立 TCP 连接。 +2. Client 完成 TLS 握手,ALPN 使用 `punchnet/1.0`。 +3. 服务端 TLS 握手成功后立即发送 `PACKET_WELCOME`。 +4. Client 收到 Welcome 后发送 `PACKET_REGISTER_SUPER`。 +5. 注册成功后服务端返回 `PACKET_REGISTER_SUPER_ACK` 并进入 `registered` 状态。 +6. 注册失败时服务端返回 `PACKET_REGISTER_SUPER_NAK`,随后关闭连接。 +7. 进入 `registered` 后,Client 必须按 Welcome 中的 `heartbeat_sec` 周期发送 `PACKET_PING`。 + +### 6.3 SSL/TLS 消息交互 + +SSL/TLS 接入的应用层消息与 QUIC 完全一致,区别只在传输层: + +- Welcome:见 `5.3`,包类型 `0x4f`。 +- RegisterSuper / RegisterSuperAck / RegisterSuperNak:见 `5.4`,包类型 `0x01/0x02/0x04`。 +- Ping / Pong:见 `5.5`,包类型 `0x08/0x09`。 +- QueryInfo / PeerInfo:见 `5.6`,包类型 `0x06/0x07`。 +- ARP 查询:见 `5.7`,包类型 `0x50/0x51`。 +- Policy 查询:见 `5.8`,包类型 `0xb0/0xb1`。 +- ExposedService 查询:见 `5.9`,包类型 `0xb2/0xb3`。 +- Event 推送:见 `5.10`,包类型 `0x10`。 +- Command / CommandAck:见 `5.11`,包类型 `0x11/0x12`。 +- Unregister:见 `5.12`,包类型 `0x05`。 + +Client 如果同时支持 QUIC 和 SSL/TLS,建议抽象出同一个应用层 `Frame` 编解码模块,仅替换底层连接、握手和 stream/socket 读写逻辑。 + +## 7. UDP STUN 和数据转发 + +UDP 逻辑不属于 `src/quic` 或 `src/ssl`,但它依赖控制面注册返回的 `session_token` 和网络密钥,是 Client 完整接入流程的一部分。相关服务端实现位于: + +- `src/sdlan_stun.erl` +- `src/sdlan_stun_port_assist.erl` +- `src/sdlan_stun_peer_assist.erl` + +UDP 包没有 2 字节长度前缀: + +```text +UdpPacket = <> +``` + +### 7.1 STUN Request + +```text +Client -> STUN +<<0x30:8, SDLStunRequest/binary>> +``` + +```protobuf +message SDLStunRequest { + string client_id = 1; + uint32 network_id = 2; + bytes mac = 3; + uint32 ip = 4; + uint32 nat_type = 5; + optional SDLV6Info v6_info = 6; + bytes session_token = 7; +} +``` + +- `client_id/network_id/mac/ip`:必须与控制面 `RegisterSuper` 一致。 +- `nat_type`:Client 通过 `STUN_PROBE` 判断出的 NAT 类型。 +- `v6_info`:Client 可用的 IPv6 辅助信息,没有时为空。 +- `session_token`:控制面 `RegisterSuperAck.session_token`,服务端用它校验当前 Client 的 NAT 上报。 +- 服务端以 UDP 源地址和源端口作为该节点的最新 NAT 映射,并通知其它节点 `nat_changed`。 + +响应: + +```text +STUN -> Client +<<0x31:8, SDLStunReply/binary>> +``` + +```protobuf +message SDLStunReply { +} +``` + +- 空消息体,仅表示服务端已收到并处理 STUN 心跳。 +- Client 需要周期性发送该包维持 NAT 映射,建议周期小于 NAT 超时时间。 + +### 7.2 STUN Probe + +```text +Client -> STUN +<<0x32:8, SDLStunProbe/binary>> +``` + +```protobuf +message SDLStunProbe { + uint32 cookie = 1; + uint32 attr = 2; + uint32 step = 3; +} +``` + +- `cookie`:Client 生成的随机值,用于匹配响应。 +- `attr`:探测属性,取值见第 3 节 `STUN_ATTR_*`。 +- `step`:Client 侧探测步骤编号。当前 `sdlan_stun` 主响应代码未写回该字段,响应里通常为 protobuf 默认值 `0`,Client 需要兼容。 + +响应: + +```text +STUN -> Client +<<0x33:8, SDLStunProbeReply/binary>> +``` + +```protobuf +message SDLStunProbeReply { + uint32 cookie = 1; + uint32 step = 2; + uint32 port = 3; + uint32 ip = 4; +} +``` + +- `cookie`:原样返回请求中的 `cookie`。 +- `step`:当前实现通常为默认值 `0`。 +- `port`:服务端看到的 Client 公网 UDP 源端口。 +- `ip`:服务端看到的 Client 公网 IPv4,按 32 位整数传输。 + +典型 NAT 判断流程: + +1. 向主 STUN 地址发送 `attr = 0`,拿到公网地址 A。 +2. 如果公网地址 A 等于本地 UDP 地址,则为 `noNat`。 +3. 向另一组 STUN 地址发送 `attr = 0`,拿到公网地址 B。 +4. 如果 A 和 B 的 IP 不同,则倾向判断为 `symmetric`。 +5. 向主 STUN 地址发送 `attr = 1`,如果能收到辅助 peer 路径响应,则说明 NAT 对来源 IP/端口限制较少。 +6. 向主 STUN 地址发送 `attr = 2`,如果能收到辅助端口路径响应,则说明 NAT 对来源端口限制较少;如果收不到则按端口限制型或降级策略处理。 +7. 任何关键步骤无响应时,可按 `blocked` 或降级策略处理。 + +### 7.3 Client 间 UDP 打洞 + +当 Client 通过 `QUERY_INFO` 或 `send_register` 事件拿到对端 NAT 地址后,向对端 UDP 地址发送: + +```text +Client A -> Client B +<<0x20:8, SDLRegister/binary>> +``` + +```protobuf +message SDLRegister { + uint32 network_id = 1; + bytes src_mac = 2; + bytes dst_mac = 3; +} +``` + +- `network_id`:当前虚拟网络 ID。 +- `src_mac`:发送方 MAC。 +- `dst_mac`:目标方 MAC。 +- 该包用于在双方 NAT 设备上建立映射,不经过控制连接。 + +对端收到后回复: + +```text +Client B -> Client A +<<0x21:8, SDLRegisterAck/binary>> +``` + +```protobuf +message SDLRegisterAck { + uint32 network_id = 1; + bytes src_mac = 2; + bytes dst_mac = 3; +} +``` + +- `src_mac`:ACK 发送方 MAC。 +- `dst_mac`:ACK 目标方 MAC。 +- 收到 ACK 后,Client 可以优先使用 P2P 直连发送 `SDLData`。 + +### 7.4 UDP 数据包 + +```text +Client -> STUN/Peer +<<0xff:8, SDLData/binary>> +``` + +```protobuf message SDLData { uint32 network_id = 1; - uint32 src_ip = 2; - uint32 dst_ip = 3; - bool is_p2p = 4; + bytes src_mac = 2; + bytes dst_mac = 3; + bool is_p2p = 4; uint32 ttl = 5; bytes data = 6; + bytes session_token = 7; + uint32 identity_id = 8; } +``` -``` \ No newline at end of file +- `network_id`:当前虚拟网络 ID。 +- `src_mac`:发送方 MAC。 +- `dst_mac`:目标 MAC。广播或组播 MAC 会触发服务端向网络内其它在线节点广播转发。 +- `is_p2p`:Client 直连发送时为 `true`;服务端转发时会改写为 `false`。 +- `ttl`:转发跳数。STUN 服务端转发前会执行 `ttl - 1`。 +- `data`:加密后的真实业务数据。 +- `session_token`:控制面注册返回的会话 token。 +- `identity_id`:源端身份 ID,对端用于本地权限判断。 + +服务端转发逻辑: + +1. STUN 服务端收到 `PACKET_STUN_DATA` 后解析 `SDLData`。 +2. 按 `network_id` 找到当前网络 ETS 表。 +3. 校验 `src_mac` 当前在线且存在 endpoint。 +4. 如果 `dst_mac` 是广播或组播,转发给除 `src_mac` 外所有有 NAT hole 的 endpoint。 +5. 如果 `dst_mac` 是单播,查找目标 endpoint 的 NAT hole 并转发。 +6. 转发前将 `ttl` 减 1,并把 `is_p2p` 改为 `false`。 +7. 服务端按网络带宽限制做限流;限流或找不到目标时丢弃。 + +## 8. 推荐 Client 完整接入流程 + +1. 通过 HTTP/API 完成登录和网络/IP 分配,拿到 `access_token`、`network_id`、虚拟 IP、掩码、身份 ID 等信息。 +2. 优先尝试 QUIC 接入;如果网络环境不支持 QUIC,可降级到 SSL/TLS 接入。 +3. 完成控制连接握手并接收 `PACKET_WELCOME`。 +4. 发送 `PACKET_REGISTER_SUPER`。 +5. 收到 `PACKET_REGISTER_SUPER_ACK` 后,用私钥解密 `key`,保存 `algorithm`、`region_id`、`session_token`。 +6. 启动控制连接心跳,按 `heartbeat_sec` 周期发送 `PACKET_PING` 并处理 `PACKET_PONG`。 +7. 使用 `PACKET_STUN_PROBE` 判断 NAT 类型。 +8. 周期性发送 `PACKET_STUN_REQUEST`,上报 NAT 类型、IPv6 信息和 `session_token`。 +9. 有目标通信需求时,先使用 `PACKET_ARP_REQUEST` 或本地缓存确定目标 MAC。 +10. 通过 `PACKET_POLICY_REQUEST` 查询访问策略,按 `rules` 在本地做放行判断。 +11. 通过 `PACKET_QUERY_INFO` 查询目标 NAT 信息,并根据 `SDLEvent.SendRegister` 做双向 UDP 打洞。 +12. P2P 打洞成功后优先直连发送 `PACKET_STUN_DATA`;失败时通过 STUN 服务端转发。 +13. 处理服务端推送的 `SDLEvent` 和 `SDLCommand`。Command 必须回复 `SDLCommandAck`。 +14. Client 主动退出时发送 `PACKET_UNREGISTER` 并关闭本地控制连接和 UDP 资源。