sdlan/Protocol.md
2026-05-30 17:08:50 +08:00

768 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SDLAN 协议说明
本文档描述 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
algorithm:
aes AES-256
chacha20 ChaCha20
key:
长度 32 字节。
RegisterSuperAck.key 会使用客户端 RegisterSuper.pub_key 中的 RSA 公钥加密。
AES 参数:
blockMode: cbc
padding: pkcs7Padding
iv: key 的前 16 字节
ChaCha20 参数:
key: RegisterSuperAck.key 解密后的 32 字节密钥
region_id: RegisterSuperAck.region_id用于客户端侧逻辑分区/nonce 生成
```
`SDLData.data` 字段承载隧道中的原始二层/三层数据Client 需要按 `RegisterSuperAck.algorithm``key` 做加解密;包头中的 `network_id``src_mac``dst_mac``ttl``session_token``identity_id` 不参与该字段加密。
## 2. 通用应用层帧
QUIC 和 SSL/TLS 接入最终交给 `sdlan_session:handle_frame/2` 的应用层帧格式一致:
```text
Frame = <<PacketType:8, ProtobufPayload/binary>>
```
- `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`
```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;
}
message SDLV6Info {
uint32 port = 1;
bytes v6 = 2;
}
```
- `SDLV4Info.port`:服务端观察到的客户端 NAT 端口。
- `SDLV4Info.v4`4 字节 IPv4 地址,网络字节序。
- `SDLV4Info.nat_type`Client 上报的 NAT 类型。
- `SDLV6Info.port`IPv6 辅助通道端口。
- `SDLV6Info.v6`16 字节 IPv6 地址。
### 4.2 NAT 类型约定
```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
```
## 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 = <<Len:16, Frame:Len/binary>>
Frame = <<PacketType:8, ProtobufPayload/binary>>
```
- `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
<<Len:16, 0x4f:8, SDLWelcome/binary>>
```
```protobuf
message SDLWelcome {
uint32 version = 1;
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
<<Len:16, 0x01:8, SDLRegisterSuper/binary>>
```
```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
<<Len:16, 0x02:8, SDLRegisterSuperAck/binary>>
```
```protobuf
message SDLRegisterSuperAck {
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
<<Len:16, 0x04:8, SDLRegisterSuperNak/binary>>
```
```protobuf
message SDLRegisterSuperNak {
uint32 error_code = 1;
string error_message = 2;
}
```
- `error_code`:错误码。网络/API 错误当前使用 `4`,服务内部错误当前使用 `5`API 返回业务错误时透传 API code。
- `error_message`:错误描述。服务端发送 NAK 后关闭连接。
### 5.5 QUIC 心跳
```text
Client -> Server
<<Len:16, 0x08:8>>
Server -> Client
<<Len:16, 0x09:8>>
```
- Client 只在注册成功后发送 Ping。
- 服务端收到 Ping 后立即回复 Pong并记录本轮心跳已收到。
- 服务端按 `heartbeat_sec` 检查上一周期是否收到过 Ping如果没有收到关闭连接。
### 5.6 QUIC 查询 PeerInfo
```text
Client -> Server
<<Len:16, 0x06:8, SDLQueryInfo/binary>>
```
```protobuf
message SDLQueryInfo {
bytes dst_mac = 1;
}
```
- `dst_mac`:目标节点 MAC。
- 服务端查找目标节点的 NAT 映射和 IPv6 信息。
- 如果目标存在,服务端同时会向目标节点推送 `SDLEvent.SendRegister`,让目标主动向查询方发送打洞包,提高 P2P 成功率。
响应:
```text
Server -> Client
<<Len:16, 0x07:8, SDLPeerInfo/binary>>
```
```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
<<Len:16, 0x50:8, SDLArpRequest/binary>>
```
```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
<<Len:16, 0x51:8, SDLArpResponse/binary>>
```
```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
<<Len:16, 0xb0:8, SDLPolicyRequest/binary>>
```
```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
<<Len:16, 0xb1:8, SDLPolicyResponse/binary>>
```
```protobuf
message SDLPolicyResponse {
uint32 src_identity_id = 1;
uint32 dst_identity_id = 2;
uint32 version = 3;
bytes rules = 4;
}
```
- `rules`:稀疏序列化规则列表,每条规则格式为 `<<Proto:8, Port:16>>`
- `Proto`:协议号,例如 TCP/UDP 对应的协议编号。
- `Port`:允许访问的目标端口。
- 服务端只下发 allow 规则deny 规则在服务端侧已过滤。
- 如果请求解析或规则查询失败,当前服务端可能不返回响应包。
### 5.9 QUIC ExposedService 查询
```text
Client -> Server
<<Len:16, 0xb2:8, SDLExposedServiceRequest/binary>>
```
```protobuf
message SDLExposedServiceRequest {
uint32 version = 1;
}
```
- `version`Client 当前缓存版本,服务端响应中原样返回。
响应:
```text
Server -> Client
<<Len:16, 0xb3:8, SDLExposedServiceResponse/binary>>
```
```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
<<Len:16, 0x10:8, SDLEvent/binary>>
```
```protobuf
message SDLEvent {
message NatChanged {
bytes mac = 1;
uint32 ip = 2;
}
message SendRegister {
bytes dst_mac = 1;
uint32 nat_ip = 2;
uint32 nat_port = 3;
uint32 nat_type = 4;
optional SDLV6Info v6_info = 5;
}
message ExposedServiceChanged {
}
message NetworkShutdown {
string message = 1;
}
oneof event {
NatChanged nat_changed = 1;
SendRegister send_register = 2;
NetworkShutdown shutdown = 3;
ExposedServiceChanged exposed_service_changed = 4;
}
}
```
- `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
Server -> Client
<<Len:16, 0x11:8, SDLCommand/binary>>
```
```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
<<Len:16, 0x12:8, SDLCommandAck/binary>>
```
```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
<<Len:16, 0x05:8>>
```
- 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 = <<Len:16, Frame:Len/binary>>
Frame = <<PacketType:8, ProtobufPayload/binary>>
```
- `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 = <<PacketType:8, ProtobufPayload/binary>>
```
### 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;
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;
}
```
- `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 资源。