# 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 资源。


===端口说明====
组件  默认端口/配置 」 说明 」
QUIC | 443 |  ALPN `punchnet/1.0` |控制面主入口，允许 1条双向 stream。
SSL/TLS |  1443  |  ALPN `punchnet/1.0` |控制面备用入口，与 QUIC 共用`sdlan session`
STUN |  1365  | `acceptor_nums=5`  |UDP_NAT 上报、探测和服务端转发
STUN port assist  | 1366  |同节点变端 probe reply
STUN peer assist |配置的 `punchnet.aioe.tech:1366`|转发到另一辅助节点后回复。
IPv6 assist | 1367 |IPv6 可达性探测。
