优化机器人ack

This commit is contained in:
2026-06-14 19:56:20 +08:00
parent 757eb852fd
commit 67330d4656
7 changed files with 381 additions and 9 deletions
+48
View File
@@ -38,6 +38,13 @@ type NodeInfoBuildOptions struct {
PublicKey []byte
}
// AckBuildOptions 描述构造一个 Routing-NONE ACKPSK 频道路径)所需字段。
// RequestID 是被 ACK 原始包的 packet_id(写入 Data.request_id, tag 6)。
type AckBuildOptions struct {
PacketBuildOptions
RequestID uint32
}
func BuildTextMessageServiceEnvelope(opts TextMessageBuildOptions) ([]byte, error) {
if opts.FromNodeNum == 0 {
return nil, fmt.Errorf("from node number is required")
@@ -91,6 +98,47 @@ func BuildNodeInfoServiceEnvelope(opts NodeInfoBuildOptions) ([]byte, error) {
return buildServiceEnvelope(packet, opts.ChannelID, opts.GatewayID), nil
}
// BuildAckServiceEnvelope 构造一个 PSK 频道上的 Routing ACKerror_reason=NONE)。
// 与固件 MeshModule::allocAckNak/Router::sendAckNak 行为对齐:
// - portnum = ROUTING_APP(5)
// - Data.request_id = 原始包 ID
// - Routing.which_variant = error_reasonerror_reason = NONE(0)
//
// 用 PacketBuildOptions 中 ChannelID + PSK 加密;调用方负责把 ToNodeNum 设为原 from。
func BuildAckServiceEnvelope(opts AckBuildOptions) ([]byte, error) {
if opts.RequestID == 0 {
return nil, fmt.Errorf("ack request_id is required")
}
if opts.ChannelID == "" {
return nil, fmt.Errorf("channel id is required")
}
data := buildAckDataPacket(opts.RequestID)
packet, err := buildMeshPacket(opts.PacketBuildOptions, data)
if err != nil {
return nil, err
}
if strings.TrimSpace(opts.GatewayID) == "" {
opts.GatewayID = NodeNumToID(opts.FromNodeNum)
}
return buildServiceEnvelope(packet, opts.ChannelID, opts.GatewayID), nil
}
// buildAckDataPacket 构造 Data { portnum=ROUTING_APP, payload=Routing{error_reason=NONE}, request_id=req }。
func buildAckDataPacket(requestID uint32) []byte {
// Routing payload: oneof variant=error_reason(tag 3), value=NONE(0) → 0x18 0x00
routing := protowire.AppendTag(nil, 3, protowire.VarintType)
routing = protowire.AppendVarint(routing, 0)
var out []byte
out = protowire.AppendTag(out, 1, protowire.VarintType)
out = protowire.AppendVarint(out, uint64(routingApp))
out = protowire.AppendTag(out, 2, protowire.BytesType)
out = protowire.AppendBytes(out, routing)
out = protowire.AppendTag(out, 6, protowire.Fixed32Type)
out = protowire.AppendFixed32(out, requestID)
return out
}
func NodeNumToID(nodeNum uint32) string {
return nodeNumToID(nodeNum)
}
+34
View File
@@ -167,6 +167,40 @@ func TestBuildNodeInfoTruncatesNanopbStrings(t *testing.T) {
}
}
func TestBuildAckServiceEnvelopeRoundTrip(t *testing.T) {
key, err := ExpandPSK("AQ==")
if err != nil {
t.Fatalf("ExpandPSK: %v", err)
}
const requestID uint32 = 0xabcd1234
raw, err := BuildAckServiceEnvelope(AckBuildOptions{
PacketBuildOptions: PacketBuildOptions{
FromNodeNum: 0x10101010,
ToNodeNum: 0x20202020,
PacketID: 0x30303030,
ChannelID: "LongFast",
GatewayID: "!10101010",
PSK: key,
Encrypt: true,
ViaMQTT: true,
},
RequestID: requestID,
})
if err != nil {
t.Fatalf("BuildAckServiceEnvelope: %v", err)
}
valid, _, record := MQTTPP("msh/2/e/LongFast/!10101010", raw, key, Options{})
if !valid {
t.Fatalf("MQTTPP not valid: %#v", record)
}
if record["portnum"] != "ROUTING_APP" {
t.Fatalf("portnum = %v", record["portnum"])
}
if record["type"] != "routing" {
t.Fatalf("type = %v", record["type"])
}
}
func TestParseNodeID(t *testing.T) {
num, err := ParseNodeID("!1234abcd")
if err != nil {
+6
View File
@@ -53,6 +53,7 @@ type meshPacket struct {
Decoded *dataPacket
Encrypted []byte
ID uint32
WantAck bool
ViaMQTT bool
PKIEncrypted bool
PayloadVariant string
@@ -259,6 +260,10 @@ func parseMeshPacket(payload []byte) (*meshPacket, error) {
if v, ok := value.(uint32); ok && typ == protowire.Fixed32Type {
packet.ID = v
}
case 10:
if v, ok := value.(uint64); ok && typ == protowire.VarintType {
packet.WantAck = v != 0
}
case 14:
if v, ok := value.(uint64); ok && typ == protowire.VarintType {
packet.ViaMQTT = v != 0
@@ -684,6 +689,7 @@ func describePacket(topic string, env *serviceEnvelope, key []byte, opts Options
"packet_to_num": packet.To,
"packet_id": packet.ID,
"payload_variant": packet.PayloadVariant,
"want_ack": packet.WantAck,
"via_mqtt": packet.ViaMQTT,
"pki_encrypted": packet.PKIEncrypted,
}
+60 -1
View File
@@ -94,7 +94,66 @@ func BuildPKITextMessageServiceEnvelope(opts PKITextMessageBuildOptions) ([]byte
return buildServiceEnvelope(packet, PKIChannelID, opts.GatewayID), nil
}
// pkiSharedKey 用 X25519 计算共享密钥,再做一次 SHA-256(与固件一致)
// PKIAckBuildOptions 描述构造一个 PKI 加密的 Routing-NONE ACK 所需字段
type PKIAckBuildOptions struct {
FromNodeNum uint32 // 我们(机器人)的节点号
ToNodeNum uint32 // 原发送者
PacketID uint32 // 新生成的 ACK 自身的 packet id
RequestID uint32 // 被 ACK 的原始包 packet id
GatewayID string
ViaMQTT bool
SenderPrivate []byte
RecipientPub []byte
SenderPublic []byte
}
// BuildPKIAckServiceEnvelope 构造一条 PKI 加密的 Routing-NONE ACK,与固件
// MeshModule::allocAckNak + Router::perhapsEncode (PKI 分支) 行为对齐。
func BuildPKIAckServiceEnvelope(opts PKIAckBuildOptions) ([]byte, error) {
if opts.FromNodeNum == 0 {
return nil, fmt.Errorf("from node number is required")
}
if opts.ToNodeNum == 0 || opts.ToNodeNum == NodeNumBroadcast {
return nil, fmt.Errorf("pki ack requires a non-broadcast destination")
}
if opts.PacketID == 0 {
return nil, fmt.Errorf("packet id is required")
}
if opts.RequestID == 0 {
return nil, fmt.Errorf("request id is required")
}
if len(opts.SenderPrivate) != 32 || len(opts.RecipientPub) != 32 {
return nil, fmt.Errorf("pki keys must be 32 bytes each")
}
if strings.TrimSpace(opts.GatewayID) == "" {
opts.GatewayID = NodeNumToID(opts.FromNodeNum)
}
plaintext := buildAckDataPacket(opts.RequestID)
sharedKey, err := pkiSharedKey(opts.SenderPrivate, opts.RecipientPub)
if err != nil {
return nil, err
}
var extraNonceBuf [4]byte
if _, err := rand.Read(extraNonceBuf[:]); err != nil {
return nil, err
}
extraNonce := binary.LittleEndian.Uint32(extraNonceBuf[:])
ciphertext, auth, err := aesCCMEncrypt(sharedKey, pkiNonce(opts.PacketID, opts.FromNodeNum, extraNonce), plaintext)
if err != nil {
return nil, err
}
encrypted := make([]byte, 0, len(ciphertext)+pkcOverhead)
encrypted = append(encrypted, ciphertext...)
encrypted = append(encrypted, auth...)
encrypted = append(encrypted, extraNonceBuf[:]...)
packet := buildPKIMeshPacket(opts.FromNodeNum, opts.ToNodeNum, opts.PacketID, opts.ViaMQTT, encrypted, opts.SenderPublic)
return buildServiceEnvelope(packet, PKIChannelID, opts.GatewayID), nil
}
func pkiSharedKey(privateKey, publicKey []byte) ([]byte, error) {
curve := ecdh.X25519()
priv, err := curve.NewPrivateKey(privateKey)
+64
View File
@@ -207,3 +207,67 @@ func TestMQTTPPDecryptsPKIWithResolver(t *testing.T) {
t.Fatalf("pki_encrypted = %v", record["pki_encrypted"])
}
}
func TestBuildPKIAckRoundTrip(t *testing.T) {
curve := ecdh.X25519()
botPriv, _ := curve.GenerateKey(rand.Reader)
devicePriv, _ := curve.GenerateKey(rand.Reader)
const fromNum uint32 = 0x0000beef // bot
const toNum uint32 = 0xfeed0000 // 原 device
const ackPacketID uint32 = 0xaaaa5555
const requestID uint32 = 0xdeadbeef
raw, err := BuildPKIAckServiceEnvelope(PKIAckBuildOptions{
FromNodeNum: fromNum,
ToNodeNum: toNum,
PacketID: ackPacketID,
RequestID: requestID,
GatewayID: NodeNumToID(fromNum),
ViaMQTT: true,
SenderPrivate: botPriv.Bytes(),
RecipientPub: devicePriv.PublicKey().Bytes(),
SenderPublic: botPriv.PublicKey().Bytes(),
})
if err != nil {
t.Fatalf("BuildPKIAckServiceEnvelope: %v", err)
}
// 设备侧解密
env, err := parseServiceEnvelope(raw)
if err != nil {
t.Fatalf("parse: %v", err)
}
if env.ChannelID != PKIChannelID {
t.Fatalf("channel_id = %q", env.ChannelID)
}
pkt := env.Packet
if !pkt.PKIEncrypted || pkt.From != fromNum || pkt.To != toNum || pkt.ID != ackPacketID {
t.Fatalf("ack header mismatch: %+v", pkt)
}
encryptedLen := len(pkt.Encrypted) - pkcOverhead
cipher := pkt.Encrypted[:encryptedLen]
auth := pkt.Encrypted[encryptedLen : encryptedLen+8]
extraNonce := binary.LittleEndian.Uint32(pkt.Encrypted[encryptedLen+8:])
sharedKey, err := pkiSharedKey(devicePriv.Bytes(), botPriv.PublicKey().Bytes())
if err != nil {
t.Fatalf("shared: %v", err)
}
plain, err := aesCCMDecrypt(sharedKey, pkiNonce(ackPacketID, fromNum, extraNonce), cipher, auth)
if err != nil {
t.Fatalf("decrypt: %v", err)
}
data, err := parseDataPacket(plain)
if err != nil {
t.Fatalf("data: %v", err)
}
if data.Portnum != routingApp {
t.Fatalf("portnum = %d, want ROUTING_APP(%d)", data.Portnum, routingApp)
}
// Routing payload 解析: 期望 oneof error_reason=NONE(0),即 wire 字节 0x18 0x00
wantRouting := []byte{0x18, 0x00}
if !bytes.Equal(data.Payload, wantRouting) {
t.Fatalf("routing payload = % x, want % x", data.Payload, wantRouting)
}
}