// Package mesh 实现 Meshtastic MQTT 数据包的解码与构建。 // 协议实现参考 meshtastic_mqtt_server 工程(MIT License): // - ServiceEnvelope / MeshPacket / Data 使用 protobuf wire 格式手写解析(google.golang.org/protobuf/encoding/protowire) // - 加密包使用频道 PSK + AES-CTR,nonce = little-endian(packetID 8B) + little-endian(fromNum 4B) + 4 字节零 // - channel hash = xor(channelName) ^ xor(psk) package mesh import ( "crypto/aes" "crypto/cipher" "encoding/base64" "encoding/binary" "fmt" "strings" "unicode/utf8" "google.golang.org/protobuf/encoding/protowire" ) // Portnum 常量(与 meshtastic.proto PortNum 一致)。 const ( PortNumUnknown = 0 PortNumTextMessage = 1 PortNumPosition = 3 PortNumNodeInfo = 4 PortNumRouting = 5 PortNumTelemetry = 67 PortNumMapReport = 73 ) // NodeNumBroadcast 是广播目标节点号。 const NodeNumBroadcast uint32 = 0xffffffff // defaultMeshtasticPSK 是 Meshtastic 默认频道密钥(PSK 索引 1)。 var defaultMeshtasticPSK = []byte{ 0xD4, 0xF1, 0xBB, 0x3A, 0x20, 0x29, 0x07, 0x59, 0xF0, 0xBC, 0xFF, 0xAB, 0xCF, 0x4E, 0x69, 0x01, } // Packet 是解码后的 MeshPacket 关键字段。 type Packet struct { From uint32 To uint32 Channel uint32 ID uint32 WantAck bool ViaMQTT bool PKIEncrypted bool Decoded *Data Encrypted []byte } // Data 是 MeshPacket.decoded 中的 Data 子包。 type Data struct { Portnum uint32 Payload []byte } // TextMessage 是一条解码后的文本消息。 type TextMessage struct { From uint32 To uint32 Text string Hex string // 非 UTF-8 时的十六进制表示 } // NodeInfo 是解码后的节点信息(NODEINFO_APP)。 type NodeInfo struct { From uint32 ID string LongName string ShortName string HWModel uint64 Role uint64 PublicKey []byte } // Position 是解码后的位置信息(POSITION_APP)。 type Position struct { From uint32 Latitude *float64 Longitude *float64 Altitude *int32 } // ExpandPSK 展开 Base64 PSK,兼容 Meshtastic 默认索引 PSK 和短 key 补零规则。 func ExpandPSK(pskBase64 string) ([]byte, error) { psk, err := base64.StdEncoding.DecodeString(strings.TrimSpace(pskBase64)) if err != nil { return nil, fmt.Errorf("invalid psk: %w", err) } if len(psk) == 1 { idx := psk[0] if idx == 0 { return []byte{}, nil } key := append([]byte(nil), defaultMeshtasticPSK...) key[len(key)-1] = byte((int(key[len(key)-1]) + int(idx) - 1) & 0xff) return key, nil } if len(psk) > 0 && len(psk) < 16 { return append(psk, make([]byte, 16-len(psk))...), nil } if len(psk) > 16 && len(psk) < 32 { return append(psk, make([]byte, 32-len(psk))...), nil } if len(psk) != 0 && len(psk) != 16 && len(psk) != 24 && len(psk) != 32 { return nil, fmt.Errorf("invalid psk length %d: AES keys must be 16, 24, or 32 bytes", len(psk)) } return psk, nil } // Decode 解码一个 MQTT 消息 payload(ServiceEnvelope), // 返回解码结果(*TextMessage / *NodeInfo / *Position / *GenericPacket)与错误。 // 加密包会用频道 PSK 尝试解密;PKI 加密包(无密钥)返回错误。 func Decode(topic string, raw []byte, key []byte) (any, error) { env, err := parseServiceEnvelope(raw) if err != nil { return nil, fmt.Errorf("service envelope: %w", err) } if env.Packet == nil { return nil, fmt.Errorf("no packet in envelope") } pkt := env.Packet if pkt.Decoded == nil && len(pkt.Encrypted) > 0 { decoded, status := tryDecrypt(pkt, env.ChannelID, key) if decoded == nil { return nil, fmt.Errorf("decrypt failed (%s)", status) } pkt.Decoded = decoded } if pkt.Decoded == nil { return nil, fmt.Errorf("empty packet") } switch pkt.Decoded.Portnum { case PortNumTextMessage: text := string(pkt.Decoded.Payload) msg := &TextMessage{From: pkt.From, To: pkt.To, Text: text} if !utf8.Valid(pkt.Decoded.Payload) { msg.Text = "" msg.Hex = fmt.Sprintf("%x", pkt.Decoded.Payload) } return msg, nil case PortNumNodeInfo: info, err := parseUser(pkt.Decoded.Payload) if err != nil { return nil, err } info.From = pkt.From return info, nil case PortNumPosition: pos, err := parsePosition(pkt.Decoded.Payload) if err != nil { return nil, err } pos.From = pkt.From return pos, nil case PortNumTelemetry: return &GenericPacket{Portnum: PortNumTelemetry, From: pkt.From, PayloadLen: len(pkt.Decoded.Payload)}, nil default: return &GenericPacket{Portnum: pkt.Decoded.Portnum, From: pkt.From, PayloadLen: len(pkt.Decoded.Payload)}, nil } } // GenericPacket 是未详细解码的其他类型包。 type GenericPacket struct { Portnum uint32 From uint32 PayloadLen int } // PortnumName 返回 portnum 的可读名称。 func PortnumName(portnum uint32) string { if name, ok := portNumNames[portnum]; ok { return name } return fmt.Sprintf("PORTNUM_%d", portnum) } // NodeNumToID 把节点号格式化为 !xxxxxxxx。 func NodeNumToID(nodeNum uint32) string { return fmt.Sprintf("!%08x", nodeNum) } // ParseNodeID 解析 !xxxxxxxx 为节点号。 func ParseNodeID(nodeID string) (uint32, error) { value := strings.TrimSpace(nodeID) value = strings.TrimPrefix(value, "!") if len(value) != 8 { return 0, fmt.Errorf("node id must be !xxxxxxxx") } var num uint32 if _, err := fmt.Sscanf(value, "%08x", &num); err != nil { return 0, fmt.Errorf("invalid node id: %w", err) } return num, nil } // tryDecrypt 用频道 PSK 解密 encrypted 载荷(AES-CTR),并解析出 Data 子包。 func tryDecrypt(pkt *Packet, channelID string, key []byte) (*Data, string) { if len(key) == 0 { return nil, "psk disables encryption" } if pkt.Channel != uint32(channelHash(channelID, key)) { return nil, "channel hash mismatch" } plaintext, err := cryptAESCTR(key, pkt.From, pkt.ID, pkt.Encrypted) if err != nil { return nil, err.Error() } decoded, err := parseData(plaintext) if err != nil { return nil, "decrypted bytes are not Data protobuf" } if decoded.Portnum == PortNumUnknown { return nil, "decrypted protobuf has UNKNOWN_APP portnum" } return decoded, "success" } type serviceEnvelope struct { Packet *Packet ChannelID string GatewayID string } func parseServiceEnvelope(payload []byte) (*serviceEnvelope, error) { env := &serviceEnvelope{} err := walkFields(payload, func(num protowire.Number, typ protowire.Type, value any) error { switch num { case 1: b, ok := value.([]byte) if !ok || typ != protowire.BytesType { return nil } packet, err := parseMeshPacket(b) if err != nil { return err } env.Packet = packet case 2: if b, ok := value.([]byte); ok && typ == protowire.BytesType { env.ChannelID = string(b) } case 3: if b, ok := value.([]byte); ok && typ == protowire.BytesType { env.GatewayID = string(b) } } return nil }) return env, err } func parseMeshPacket(payload []byte) (*Packet, error) { pkt := &Packet{} err := walkFields(payload, func(num protowire.Number, typ protowire.Type, value any) error { switch num { case 1: if v, ok := value.(uint32); ok && typ == protowire.Fixed32Type { pkt.From = v } case 2: if v, ok := value.(uint32); ok && typ == protowire.Fixed32Type { pkt.To = v } case 3: if v, ok := value.(uint64); ok && typ == protowire.VarintType { pkt.Channel = uint32(v) } case 4: if b, ok := value.([]byte); ok && typ == protowire.BytesType { decoded, err := parseData(b) if err != nil { return err } pkt.Decoded = decoded } case 5: if b, ok := value.([]byte); ok && typ == protowire.BytesType { pkt.Encrypted = append([]byte(nil), b...) } case 6: if v, ok := value.(uint32); ok && typ == protowire.Fixed32Type { pkt.ID = v } case 10: if v, ok := value.(uint64); ok && typ == protowire.VarintType { pkt.WantAck = v != 0 } case 14: if v, ok := value.(uint64); ok && typ == protowire.VarintType { pkt.ViaMQTT = v != 0 } case 17: if v, ok := value.(uint64); ok && typ == protowire.VarintType { pkt.PKIEncrypted = v != 0 } } return nil }) return pkt, err } func parseData(payload []byte) (*Data, error) { data := &Data{} err := walkFields(payload, func(num protowire.Number, typ protowire.Type, value any) error { switch num { case 1: if v, ok := value.(uint64); ok && typ == protowire.VarintType { data.Portnum = uint32(v) } case 2: if b, ok := value.([]byte); ok && typ == protowire.BytesType { data.Payload = append([]byte(nil), b...) } } return nil }) return data, err } func parseUser(payload []byte) (*NodeInfo, error) { user := &NodeInfo{} err := walkFields(payload, func(num protowire.Number, typ protowire.Type, value any) error { switch num { case 1: user.ID = stringBytes(typ, value) case 2: user.LongName = stringBytes(typ, value) case 3: user.ShortName = stringBytes(typ, value) case 5: user.HWModel = varintValue(typ, value) case 7: user.Role = varintValue(typ, value) case 8: if b, ok := value.([]byte); ok && typ == protowire.BytesType { user.PublicKey = append([]byte(nil), b...) } } return nil }) return user, err } func parsePosition(payload []byte) (*Position, error) { pos := &Position{} err := walkFields(payload, func(num protowire.Number, typ protowire.Type, value any) error { switch num { case 1: if v, ok := value.(uint32); ok && typ == protowire.Fixed32Type { lat := float64(int32(v)) * 1e-7 pos.Latitude = &lat } case 2: if v, ok := value.(uint32); ok && typ == protowire.Fixed32Type { lon := float64(int32(v)) * 1e-7 pos.Longitude = &lon } case 3: if typ == protowire.VarintType { alt := int32(varintValue(typ, value)) pos.Altitude = &alt } } return nil }) return pos, err } // walkFields 遍历 protobuf wire 字段。 func walkFields(payload []byte, handle func(protowire.Number, protowire.Type, any) error) error { for len(payload) > 0 { num, typ, n := protowire.ConsumeTag(payload) if n < 0 { return protowire.ParseError(n) } payload = payload[n:] var value any switch typ { case protowire.VarintType: v, n := protowire.ConsumeVarint(payload) if n < 0 { return protowire.ParseError(n) } value = v payload = payload[n:] case protowire.Fixed32Type: v, n := protowire.ConsumeFixed32(payload) if n < 0 { return protowire.ParseError(n) } value = v payload = payload[n:] case protowire.Fixed64Type: v, n := protowire.ConsumeFixed64(payload) if n < 0 { return protowire.ParseError(n) } value = v payload = payload[n:] case protowire.BytesType: v, n := protowire.ConsumeBytes(payload) if n < 0 { return protowire.ParseError(n) } value = v payload = payload[n:] default: n := protowire.ConsumeFieldValue(num, typ, payload) if n < 0 { return protowire.ParseError(n) } payload = payload[n:] } if err := handle(num, typ, value); err != nil { return err } } return nil } func stringBytes(typ protowire.Type, value any) string { if b, ok := value.([]byte); ok && typ == protowire.BytesType { return string(b) } return "" } func varintValue(typ protowire.Type, value any) uint64 { if v, ok := value.(uint64); ok && typ == protowire.VarintType { return v } return 0 } func xorHash(data []byte) byte { var result byte for _, b := range data { result ^= b } return result } func channelHash(channelName string, key []byte) byte { return xorHash([]byte(channelName)) ^ xorHash(key) } // cryptAESCTR 按 Meshtastic nonce 规则执行 AES-CTR;CTR 加密和解密是同一个 XOR 流操作。 func cryptAESCTR(key []byte, fromNum, packetID uint32, input []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } nonce := make([]byte, aes.BlockSize) binary.LittleEndian.PutUint64(nonce[0:8], uint64(packetID)) binary.LittleEndian.PutUint32(nonce[8:12], fromNum) output := make([]byte, len(input)) cipher.NewCTR(block, nonce).XORKeyStream(output, input) return output, nil } var portNumNames = map[uint32]string{ 0: "UNKNOWN_APP", 1: "TEXT_MESSAGE_APP", 2: "REMOTE_HARDWARE_APP", 3: "POSITION_APP", 4: "NODEINFO_APP", 5: "ROUTING_APP", 6: "ADMIN_APP", 7: "TEXT_MESSAGE_COMPRESSED_APP", 8: "WAYPOINT_APP", 9: "AUDIO_APP", 10: "DETECTION_SENSOR_APP", 11: "ALERT_APP", 12: "KEY_VERIFICATION_APP", 13: "REMOTE_SHELL_APP", 32: "REPLY_APP", 33: "IP_TUNNEL_APP", 34: "PAXCOUNTER_APP", 35: "STORE_FORWARD_PLUSPLUS_APP", 36: "NODE_STATUS_APP", 64: "SERIAL_APP", 65: "STORE_FORWARD_APP", 66: "RANGE_TEST_APP", 67: "TELEMETRY_APP", 68: "ZPS_APP", 69: "SIMULATOR_APP", 70: "TRACEROUTE_APP", 71: "NEIGHBORINFO_APP", 72: "ATAK_PLUGIN", 73: "MAP_REPORT_APP", 74: "POWERSTRESS_APP", 75: "LORAWAN_BRIDGE", 76: "RETICULUM_TUNNEL_APP", 77: "CAYENNE_APP", 78: "ATAK_PLUGIN_V2", 112: "GROUPALARM_APP", 256: "PRIVATE_APP", 257: "ATAK_FORWARDER", 511: "MAX", }