More protocols

This commit is contained in:
2026-02-17 23:30:49 +01:00
parent 62a90a468d
commit 74a517a22a
15 changed files with 2268 additions and 21 deletions

View File

@@ -1,6 +1,7 @@
package aprs package aprs
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
@@ -46,6 +47,25 @@ func (a Address) Secret() int16 {
return h & 0x7fff return h & 0x7fff
} }
func (a Address) MarshalJSON() ([]byte, error) {
return json.Marshal(a.String())
}
func (a *Address) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
p, err := ParseAddress(s)
if err != nil {
return err
}
a.Call = p.Call
a.SSID = p.SSID
a.IsRepeated = p.IsRepeated
return nil
}
func ParseAddress(s string) (Address, error) { func ParseAddress(s string) (Address, error) {
r := strings.HasSuffix(s, "*") r := strings.HasSuffix(s, "*")
if r { if r {

View File

@@ -0,0 +1,188 @@
package aprsis
import (
"bufio"
"bytes"
"fmt"
"net"
"strings"
"sync"
"git.maze.io/go/ham/protocol"
"github.com/sirupsen/logrus"
)
const (
DefaultListenAddr = ":14580"
DefaultServerAddr = "rotate.aprs2.net:14580"
)
type Proxy struct {
Logger *logrus.Logger
Filter string
server string
listen net.Listener
packets chan *protocol.Packet
}
func NewProxy(listen, server string) (*Proxy, error) {
if _, err := net.ResolveTCPAddr("tcp", server); err != nil {
return nil, fmt.Errorf("aprsis: error resolving %q: %v", server, err)
}
listenAddr, err := net.ResolveTCPAddr("tcp", listen)
if err != nil {
return nil, fmt.Errorf("aprsis: error listening on %s: %v", listen, err)
}
listener, err := net.ListenTCP("tcp", listenAddr)
if err != nil {
return nil, fmt.Errorf("aprsis: error listening on %s: %v", listen, err)
}
proxy := &Proxy{
Logger: logrus.New(),
server: server,
listen: listener,
}
go proxy.accept()
return proxy, nil
}
func (proxy *Proxy) Close() error {
if proxy.packets != nil {
close(proxy.packets)
proxy.packets = nil
}
return proxy.listen.Close()
}
func (proxy *Proxy) RawPackets() <-chan *protocol.Packet {
if proxy.packets == nil {
proxy.packets = make(chan *protocol.Packet, 16)
}
return proxy.packets
}
func (proxy *Proxy) accept() {
for {
client, err := proxy.listen.Accept()
if err != nil {
proxy.Logger.Errorf("aprs-is proxy: error accepting client: %v", err)
continue
}
go proxy.handle(client)
}
}
func (proxy *Proxy) handle(client net.Conn) {
defer func() { _ = client.Close() }()
host, _, _ := net.SplitHostPort(client.RemoteAddr().String())
proxy.Logger.Infof("aprs-is proxy[%s]: new connection", host)
server, err := net.Dial("tcp", proxy.server)
if err != nil {
proxy.Logger.Warnf("aprs-is proxy[%s]: can't connecto to APRS-IS server %s: %v", host, proxy.server, err)
return
}
defer func() { _ = server.Close() }()
var (
wait sync.WaitGroup
call string
)
wait.Go(func() { proxy.proxy(client, server, host, "->", &call) })
wait.Go(func() { proxy.proxy(server, client, host, "<-", nil) })
wait.Wait()
}
func (proxy *Proxy) proxy(dst, src net.Conn, host, dir string, call *string) {
defer func() {
if tcp, ok := dst.(*net.TCPConn); ok {
_ = tcp.CloseWrite()
} else {
_ = dst.Close()
}
}()
reader := bufio.NewReader(src)
for {
line, err := reader.ReadBytes('\n')
if err != nil {
proxy.Logger.Warnf("aprs-is proxy[%s]: %s read error: %v", host, src.RemoteAddr(), err)
return
}
// proxy to remote unaltered
if len(line) > 0 {
if _, err = dst.Write(line); err != nil {
proxy.Logger.Warnf("aprs-is proxy[%s]: %s write error: %v", host, dst.RemoteAddr(), err)
return
}
}
// parse line
line = bytes.TrimRight(line, "\r\n")
if len(line) > 0 {
proxy.Logger.Tracef("aprs-is proxy[%s]: %s %s", host, dir, string(line))
if call != nil && strings.HasPrefix(string(line), "# logresp ") {
// server responds to client login
part := strings.SplitN(string(line), " ", 5)
if len(part) > 4 && part[3] == "verified," {
*call = part[2]
proxy.Logger.Infof("aprs-is proxy[%s]: logged in as %s", host, *call)
if proxy.Filter != "" {
proxy.Logger.Tracef("aprs-is proxy[%s]: %s filter %s", host, dir, proxy.Filter)
if _, err = fmt.Fprintf(src, "filter %s\r\n", proxy.Filter); err != nil {
proxy.Logger.Warnf("aprs-is proxy[%s]: %s write error: %v", host, src.RemoteAddr(), err)
return
}
}
}
}
if !isCommand(line) {
proxy.handleRawPacket(line)
}
}
}
}
func (proxy *Proxy) handleRawPacket(data []byte) {
if proxy.packets == nil {
return
}
select {
case proxy.packets <- &protocol.Packet{
Protocol: "aprs",
Raw: data,
}:
default:
proxy.Logger.Warn("aprs-is proxy: raw packet channel full, dropping packet")
}
}
func isCommand(line []byte) bool {
if len(line) == 0 {
return true
}
if line[0] == '#' {
return true
}
if i := bytes.IndexByte(line, ' '); i > -1 {
switch strings.ToLower(string(line[:i])) {
case "user", "filter":
return true
}
}
return false
}

View File

@@ -140,7 +140,7 @@ func (o OmniDFStrength) Directivity() float64 {
// Packet contains an APRS packet. // Packet contains an APRS packet.
type Packet struct { type Packet struct {
// Raw packet (as captured from the air or APRS-IS). // Raw packet (as captured from the air or APRS-IS).
Raw string `json:"raw"` Raw string `json:"-"`
// Src is the source address. // Src is the source address.
Src Address `json:"src"` Src Address `json:"src"`

View File

@@ -1,12 +0,0 @@
module git.maze.io/go/ham/protocol/meshcore/crypto/jwt
go 1.25.6
replace git.maze.io/go/ham => ../../../..
require (
git.maze.io/go/ham v0.0.0-20260214171233-1d56998dd300
github.com/golang-jwt/jwt/v5 v5.3.1
)
require filippo.io/edwards25519 v1.1.0 // indirect

View File

@@ -1,4 +0,0 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=

View File

@@ -10,6 +10,7 @@ import (
) )
func init() { func init() {
SigningMethod = new(SigningMethodEd25519)
jwt.RegisterSigningMethod(SigningMethod.Alg(), func() jwt.SigningMethod { jwt.RegisterSigningMethod(SigningMethod.Alg(), func() jwt.SigningMethod {
return SigningMethod return SigningMethod
}) })
@@ -17,10 +18,6 @@ func init() {
var SigningMethod jwt.SigningMethod var SigningMethod jwt.SigningMethod
func init() {
SigningMethod = new(SigningMethodEd25519)
}
type SigningMethodEd25519 struct{} type SigningMethodEd25519 struct{}
func (m *SigningMethodEd25519) Alg() string { func (m *SigningMethodEd25519) Alg() string {

View File

@@ -0,0 +1,141 @@
package crypto
import (
"crypto/aes"
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"golang.org/x/crypto/curve25519"
)
type SharedSecret struct {
aBytes [32]byte
}
func MakeSharedSecret(key []byte) SharedSecret {
var secret SharedSecret
copy(secret.aBytes[:], key)
return secret
}
func MakeSharedSecretFromGroupSecret(group []byte) SharedSecret {
var secret SharedSecret
copy(secret.aBytes[:16], group)
return secret
}
func (ss *SharedSecret) HMAC(message []byte) uint16 {
h := hmac.New(sha256.New, ss.aBytes[:])
h.Write(message)
r := h.Sum(nil)
return binary.BigEndian.Uint16(r[:2])
}
func (ss *SharedSecret) key() []byte {
return ss.aBytes[:aes.BlockSize]
}
func (ss *SharedSecret) Decrypt(text []byte) ([]byte, error) {
block, err := aes.NewCipher(ss.key())
if err != nil {
return nil, err
}
length := len(text)
message := make([]byte, length)
copy(message, text)
const chunkSize = aes.BlockSize
remain := length % chunkSize
if remain > 0 {
padding := chunkSize - remain
message = append(message, make([]byte, padding)...)
}
for i, l := 0, len(message); i < l; i += chunkSize {
block.Decrypt(message[i:i+chunkSize], message[i:i+chunkSize])
}
return message[:length], nil
}
func (ss *SharedSecret) MACThenDecrypt(text []byte, mac uint16) ([]byte, error) {
if our := ss.HMAC(text); our != mac {
return nil, fmt.Errorf("expected MAC %04X, got %04X", mac, our)
}
return ss.Decrypt(text)
}
func (ss *SharedSecret) Encrypt(message []byte) ([]byte, error) {
block, err := aes.NewCipher(ss.key())
if err != nil {
return nil, err
}
text := make([]byte, len(message))
copy(text, message)
const chunkSize = aes.BlockSize
remain := len(text) % chunkSize
if remain > 0 {
padding := chunkSize - remain
text = append(text, make([]byte, padding)...)
}
for i, l := 0, len(text); i < l; i += chunkSize {
block.Encrypt(text[i:i+chunkSize], text[i:i+chunkSize])
}
return text, nil
}
func (ss *SharedSecret) EncryptThenMAC(message []byte) (uint16, []byte, error) {
text, err := ss.Encrypt(message)
if err != nil {
return 0, nil, err
}
return ss.HMAC(text), text, nil
}
type StaticSecret [32]byte
func (ss StaticSecret) PublicKey() (*PublicKey, error) {
pub, err := curve25519.X25519(ss[:], curve25519.Basepoint)
if err != nil {
return nil, err
}
return NewPublicKey(pub)
}
func (ss StaticSecret) DiffieHellman(other *PublicKey) (SharedSecret, error) {
shared, err := curve25519.X25519(ss[:], other.Bytes())
if err != nil {
return SharedSecret{}, err
}
var montgomery SharedSecret
copy(montgomery.aBytes[:], shared)
return montgomery, nil
}
type Signature [64]byte
func (sig Signature) MarshalJSON() ([]byte, error) {
s := hex.EncodeToString(sig[:])
return json.Marshal(s)
}
func (sig *Signature) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
copy((*sig)[:], []byte(s))
return nil
}

View File

@@ -0,0 +1,75 @@
package meshcore
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"git.maze.io/go/ham/protocol/meshcore/crypto"
)
type Identity struct {
Name string
PrivateKey *crypto.PrivateKey
}
func (id *Identity) Hash() uint8 {
return id.PrivateKey.PublicKey()[0]
}
type Contact struct {
Name string
PublicKey *crypto.PublicKey
}
func (contact *Contact) Hash() uint8 {
return contact.PublicKey.Bytes()[0]
}
type Group struct {
Name string `json:"name"`
Hash [32]byte `json:"hash"`
Secret crypto.SharedSecret `json:"-"`
}
func (group Group) ChannelHash() uint8 {
return group.Hash[0]
}
func (group *Group) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"name": group.Name,
"hash": hex.EncodeToString(group.Hash[:]),
})
}
func (group *Group) UnmarshalJSON(b []byte) error {
var kv = make(map[string]string)
if err := json.Unmarshal(b, &kv); err != nil {
return err
}
group.Name = kv["name"]
h, err := hex.DecodeString(kv["hash"])
if err != nil {
return err
}
copy(group.Hash[:], h)
return nil
}
func PublicGroup(name string) *Group {
h := sha256.Sum256([]byte(name))
return &Group{
Name: name,
Hash: sha256.Sum256(h[:16]),
Secret: crypto.MakeSharedSecretFromGroupSecret(h[:16]),
}
}
func SecretGroup(name string, key []byte) *Group {
return &Group{
Name: name,
Hash: sha256.Sum256(key),
Secret: crypto.MakeSharedSecret(key),
}
}

341
protocol/meshcore/node.go Normal file
View File

@@ -0,0 +1,341 @@
package meshcore
import (
"bytes"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"log"
"strings"
"sync"
"git.maze.io/go/ham/protocol"
"git.maze.io/go/ham/protocol/meshcore/crypto"
)
const (
maxCompanionFrameSize = 172
)
type Node struct {
OnPacket (*Packet)
driver nodeDriver
}
type NodeInfo struct {
Manufacturer string `json:"manufacturer"`
FirmwareVersion string `json:"firmware_version"`
Type NodeType `json:"node_type"`
Name string `json:"name"`
Power uint8 `json:"power"`
MaxPower uint8 `json:"max_power"`
PublicKey *crypto.PublicKey `json:"public_key"`
Position *Position `json:"position"`
Frequency float64 `json:"frequency"` // in MHz
Bandwidth float64 `json:"bandwidth"` // in kHz
SpreadingFactor uint8 `json:"sf"`
CodingRate uint8 `json:"cr"`
}
// NewCompanion connects to a companion device type (over serial, TCP or BLE).
func NewCompanion(conn io.ReadWriteCloser) (*Node, error) {
driver := newCompanionDriver(conn)
if err := driver.Setup(); err != nil {
return nil, err
}
return &Node{
driver: driver,
}, nil
}
func (dev *Node) Close() error {
return dev.driver.Close()
}
func (dev *Node) Packets() <-chan *Packet {
return dev.driver.Packets()
}
func (dev *Node) RawPackets() <-chan *protocol.Packet {
return dev.driver.RawPackets()
}
func (dev *Node) Info() *NodeInfo {
return dev.driver.Info()
}
type nodeDriver interface {
Setup() error
Close() error
Packets() <-chan *Packet
RawPackets() <-chan *protocol.Packet
Info() *NodeInfo
}
type CompanionError struct {
Code byte
}
func (err CompanionError) Error() string {
switch err.Code {
case companionErrCodeUnsupported:
return "meshcore: companion: unsupported"
case companionErrCodeNotFound:
return "meshcore: companion: not found"
case companionErrCodeTableFull:
return "meshcore: companion: table full"
case companionErrCodeBadState:
return "meshcore: companion: bad state"
case companionErrCodeFileIOError:
return "meshcore: companion: file input/output error"
case companionErrCodeIllegalArgument:
return "meshcore: companion: illegal argument"
default:
return fmt.Sprintf("meshcore: companion: unknown error code %#02x", err.Code)
}
}
type companionDriver struct {
conn io.ReadWriteCloser
mu sync.Mutex
packets chan *Packet
rawPackets chan *protocol.Packet
info NodeInfo
}
func newCompanionDriver(conn io.ReadWriteCloser) *companionDriver {
return &companionDriver{
conn: conn,
}
}
func (drv *companionDriver) Close() error {
return drv.conn.Close()
}
func (drv *companionDriver) Setup() (err error) {
if err = drv.sendAppStart(); err != nil {
return
}
if err = drv.sendDeviceInfo(); err != nil {
return
}
go drv.poll()
return
}
func (drv *companionDriver) Packets() <-chan *Packet {
if drv.packets == nil {
drv.packets = make(chan *Packet, 16)
}
return drv.packets
}
func (drv *companionDriver) RawPackets() <-chan *protocol.Packet {
if drv.rawPackets == nil {
drv.rawPackets = make(chan *protocol.Packet, 16)
}
return drv.rawPackets
}
func (drv *companionDriver) Info() *NodeInfo {
return &drv.info
}
func (drv *companionDriver) readFrame() ([]byte, error) {
var frame [3 + maxCompanionFrameSize]byte
for {
n, err := drv.conn.Read(frame[:])
if err != nil {
return nil, err
} else if n < 3 {
continue
}
if frame[0] != '>' {
// not a companion frame
continue
}
size := int(binary.LittleEndian.Uint16(frame[1:]))
if size > maxCompanionFrameSize {
return nil, fmt.Errorf("meshcore: companion sent frame size of %d, which exceeds maximum of %d", size, maxCompanionFrameSize)
}
// Make sure we have read all bytes
o := n
for (o - 3) < size {
if n, err = drv.conn.Read(frame[o:]); err != nil {
return nil, err
}
o += n
}
//log.Printf("read %d:\n%s", size, hex.Dump(frame[:3+size]))
return frame[3 : 3+size], nil
}
}
func (drv *companionDriver) writeFrame(b []byte) (err error) {
if len(b) > maxCompanionFrameSize {
return fmt.Errorf("meshcore: companion: frame size %d exceed maximum of %d", len(b), maxCompanionFrameSize)
}
var frame [3 + maxCompanionFrameSize]byte
frame[0] = '<'
binary.LittleEndian.PutUint16(frame[1:], uint16(len(b)))
n := copy(frame[3:], b)
//log.Printf("send %d:\n%s", n, hex.Dump(frame[:3+n]))
_, err = drv.conn.Write(frame[:3+n])
return
}
func (drv *companionDriver) writeCommand(cmd byte, args []byte, wait ...byte) ([]byte, error) {
drv.mu.Lock()
defer drv.mu.Unlock()
if err := drv.writeFrame(append([]byte{cmd}, args...)); err != nil {
return nil, err
}
return drv.wait(wait...)
}
func (drv *companionDriver) wait(wait ...byte) ([]byte, error) {
for {
b, err := drv.readFrame()
if err != nil {
return nil, err
}
if len(b) < 1 {
continue
}
switch {
case b[0] == companionResponseError:
return nil, CompanionError{Code: b[1]}
case b[0] >= 0x80:
drv.handlePushFrame(b)
continue
case bytes.Contains(wait, b[:1]):
return b, nil
case wait == nil:
return b, nil
}
}
}
func (drv *companionDriver) handlePushFrame(b []byte) {
switch b[0] {
case companionPushAdvert:
case companionPushMessageWaiting:
case companionPushLogRXData:
drv.handleRXData(b[1:])
}
}
func (drv *companionDriver) handleRXData(b []byte) {
if len(b) < 2+minPacketSize {
return
}
if drv.packets == nil {
return // not listening for packets, discard
}
packet := new(Packet)
if err := packet.UnmarshalBytes(b[2:]); err == nil {
packet.SNR = float64(b[0]) / 4
packet.RSSI = int8(b[1])
select {
case drv.packets <- packet:
default:
log.Printf("meshcore: packet channel full, dropping packet")
}
if drv.rawPackets != nil {
select {
case drv.rawPackets <- &protocol.Packet{
Protocol: "meshcore",
SNR: packet.SNR,
RSSI: packet.RSSI,
Raw: packet.Raw,
}:
default:
log.Printf("meshcore: raw packet channel full, dropping packet")
}
}
}
}
func (drv *companionDriver) sendAppStart() (err error) {
var b []byte
if b, err = drv.writeCommand(companionAppStart, append(make([]byte, 8), []byte("git.maze.io/go/ham")...), companionResponseSelfInfo); err != nil {
return fmt.Errorf("meshcore: can't send application start: %v", err)
}
log.Printf("companion app start response:\n%s", hex.Dump(b))
const expect = 1 + 1 + 1 + 1 + 32 + 4 + 4 + 1 + 1 + 1 + 1 + 4 + 4 + 1 + 1
if len(b) < expect {
return fmt.Errorf("companion: expected %d bytes of self info, got %d", expect, len(b))
}
if b[0] != companionResponseSelfInfo {
return fmt.Errorf("companion: expected self info response, got %#02x", b[0])
}
b = b[1:]
drv.info.Type = NodeType(b[0])
drv.info.Power = b[1]
drv.info.MaxPower = b[2]
drv.info.PublicKey, _ = crypto.NewPublicKey(b[3 : 3+crypto.PublicKeySize])
drv.info.Position = new(Position)
drv.info.Position.Unmarshal(b[35:])
//drv.info.HasMultiACKs = b[43] != 0
//drv.info.AdvertLocationPolicy = b[44]
//drv.info.TelemetryFlags = b[45]
//drv.info.ManualAddContacts = b[46]
drv.info.Frequency = decodeFrequency(b[47:])
drv.info.Bandwidth = decodeFrequency(b[51:])
drv.info.SpreadingFactor = b[55]
drv.info.CodingRate = b[56]
drv.info.Name = strings.TrimRight(string(b[57:]), "\x00")
return
}
func (drv *companionDriver) sendDeviceInfo() (err error) {
var b []byte
if b, err = drv.writeCommand(companionDeviceQuery, []byte{0x03}, companionResponseDeviceInfo); err != nil {
return
}
const expect = 4 + 4 + 12 + 40 + 20
if len(b) < expect {
return fmt.Errorf("companion: expected %d bytes of self info, got %d", expect, len(b))
}
if b[0] != companionResponseDeviceInfo {
return fmt.Errorf("companion: expected device info response, got %#02x", b[0])
}
b = b[1:]
drv.info.Manufacturer = decodeCString(b[19:59])
drv.info.FirmwareVersion = decodeCString(b[59:79])
return
}
func (drv *companionDriver) poll() {
for {
if _, err := drv.wait(); err != nil {
log.Printf("meshcore: companion %s fatal error: %v", drv.info.Name, err)
return
}
}
}

View File

@@ -0,0 +1,124 @@
package meshcore
const (
companionErrCodeUnsupported byte = 1 + iota
companionErrCodeNotFound
companionErrCodeTableFull
companionErrCodeBadState
companionErrCodeFileIOError
companionErrCodeIllegalArgument
)
// companion command bytes
const (
companionAppStart byte = 1 + iota //
companionSendTextMessage //
companionSendChannelTextMessage //
companionGetContacts // with optional 'since' (for efficient sync)
companionGetDeviceTime //
companionSetDeviceTime //
companionSendSelfAdvert //
companionSetAdvertName //
companionAddUpdateContact //
companionSyncMessages //
companionSetRadioParams //
companionSetRadioTXPower //
companionResetPath //
companionSetAdvertLatLon //
companionRemoveContact //
companionShareContact //
companionExportContact //
companionImportContact //
companionReboot //
companionGetBatteryAndStorage // was CMD_GetBATTERY_VOLTAGE
companionSetTuningParams //
companionDeviceQuery //
companionExportPrivateKey //
companionImportPrivateKey //
companionSendRawData //
companionSendLogin //
companionSendStatusRequest //
companionHasConnection //
companionLogout // 'Disconnect'
companionGetContactByKey //
companionGetChannel //
companionSetChannel //
companionSignStart //
companionSignData //
companionSignFinish //
companionSendTracePath //
companionSetDevicePIN //
companionSetOtherParams //
companionSendTelemetryRequest //
companionGetCustomVars //
companionSetCustomVar //
companionGetAdvertPath //
companionGetTuningParams //
_ // parked
_ // parked
_ // parked
_ // parked
_ // parked
_ // parked
companionSendBinaryRequest //
companionFactoryReset //
companionSendPathDiscoveryRequest //
_ // parked
companionSetFloodScope // v8+
companionSendControlData // v8+
companionGetStats // v8+, second byte is stats type
companionSendAnonymousRequest //
companionSetAutoAddConfig //
companionGetAutoAddConfig //
)
// companion response bytes
const (
companionResponseOK byte = iota
companionResponseError
companionResponseContactsStart // first reply to CMD_GetCONTACTS
companionResponseContact // multiple of these (after CMD_GetCONTACTS)
companionResponseEndOfContacts // last reply to CMD_GetCONTACTS
companionResponseSelfInfo // reply to CMD_APP_START
companionResponseSent // reply to CMD_SEND_TXT_MSG
companionResponseContactMessageReceived // a reply to CMD_SYNC_NEXT_MESSAGE (ver < 3)
companionResponseChannelMessageReceived // a reply to CMD_SYNC_NEXT_MESSAGE (ver < 3)
companionResponseCurrentTime // a reply to CMD_GetDEVICE_TIME
companionResponseNoMoreMessages // a reply to CMD_SYNC_NEXT_MESSAGE
companionResponseExportContact //
companionResponseBatteryAndStorage // a reply to a CMD_GetBATT_AND_STORAGE
companionResponseDeviceInfo // a reply to CMD_DEVICE_QEURY
companionResponsePrivateKey // a reply to CMD_EXPORT_PRIVATE_KEY
companionResponseDisabled //
companionResponseContactMessageReceivedV3 // a reply to CMD_SYNC_NEXT_MESSAGE (ver >= 3)
companionResponseChannelMessageReceivedV3 // a reply to CMD_SYNC_NEXT_MESSAGE (ver >= 3)
companionResponseChannelInfo // a reply to CMD_GetCHANNEL
companionResponseSignatureStart //
companionResponseSignature //
companionResponseCustomVars //
companionResponseAdvertPath //
companionResponseTuningParams //
companionResponseStats // v8+, second byte is stats type
companionResponseAutoAddConfig //
)
// companion push code bytes
const (
companionPushAdvert byte = 0x80 + iota
companionPushPathUpdated
companionPushSendConfirmed
companionPushMessageWaiting
companionPushRawData
companionPushLoginSuccess
companionPushLoginFailure
companionPushStatusResponse
companionPushLogRXData
companionPushTraceData
companionPushNewAdvert
companionPushTelemetryResponse
companionPushBinaryResponse
companionPushPathDiscoveryResponse
companionPushControlData
companionPushContactDeleted
companionPushContactsFull
)

302
protocol/meshcore/packet.go Normal file
View File

@@ -0,0 +1,302 @@
package meshcore
import (
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"strings"
)
const (
minPacketSize = 2
maxPathSize = 64
maxPayloadSize = 184
)
type Packet struct {
// SNR is the signal-to-noise ratio.
SNR float64 `json:"snr"`
// RSSI is the received signal strength indicator (in dBm).
RSSI int8 `json:"rssi"`
// Raw bytes (optional).
Raw []byte `json:"raw,omitempty"`
// RouteType is the type of route for this packet.
RouteType RouteType `json:"route_type"`
// PayloadType is the type of payload for this packet.
PayloadType PayloadType `json:"payload_type"`
// TransportCodes are set by transport route types.
TransportCodes []uint16 `json:"transport_codes,omitempty"`
// Path are repeater hashes.
Path []byte `json:"path"`
// Payload is the raw (encoded) payload.
Payload []byte `json:"payload,omitempty"`
}
func Decode(data []byte) (Payload, error) {
packet := new(Packet)
if err := packet.UnmarshalBytes(data); err != nil {
return nil, err
}
return packet.Decode()
}
func (packet *Packet) Decode() (Payload, error) {
var payload Payload
switch packet.PayloadType {
case TypeRequest:
payload = &Request{Raw: packet}
case TypeResponse:
payload = &Response{Raw: packet}
case TypeText:
payload = &Text{Raw: packet}
case TypeAck:
payload = &Acknowledgement{Raw: packet}
case TypeAdvert:
payload = &Advert{Raw: packet}
case TypeGroupText:
payload = &GroupText{Raw: packet}
case TypeGroupData:
payload = &GroupData{Raw: packet}
case TypeAnonRequest:
payload = &AnonymousRequest{Raw: packet}
case TypePath:
payload = &Path{Raw: packet}
case TypeTrace:
payload = &Trace{Raw: packet}
case TypeMultipart:
payload = &Multipart{Raw: packet}
case TypeControl:
payload = &Control{Raw: packet}
case TypeRawCustom:
payload = &RawCustom{Raw: packet}
default:
return nil, fmt.Errorf("meshcore: invalid payload type %#02x", packet.PayloadType)
}
if err := payload.Unmarshal(packet.Payload); err != nil {
return nil, err
}
return payload, nil
}
func (packet *Packet) String() string {
s := []string{
packet.RouteType.String(),
packet.PayloadType.String(),
}
if len(packet.TransportCodes) == 2 {
s = append(s, fmt.Sprintf("%02X%02X", packet.TransportCodes[0], packet.TransportCodes[1]))
}
if len(packet.Path) > 0 {
s = append(s, formatPath(packet.Path))
} else {
s = append(s, "direct")
}
return strings.Join(s, " ")
}
func (packet *Packet) MarshalBytes() []byte {
var (
data [1 + 4 + 1 + maxPathSize + maxPayloadSize]byte
offset int
)
data[offset] = byte(packet.RouteType&0x03) | byte((packet.PayloadType&0x0f)<<2)
offset += 1
if packet.RouteType.HasTransportCodes() {
binary.LittleEndian.PutUint16(data[offset:], packet.TransportCodes[0])
offset += 2
binary.LittleEndian.PutUint16(data[offset:], packet.TransportCodes[1])
offset += 2
}
data[offset] = byte(len(packet.Path))
offset += 1
offset += copy(data[offset:], packet.Path)
offset += copy(data[offset:], packet.Payload)
return data[:offset]
}
func (packet *Packet) UnmarshalBytes(data []byte) error {
if len(data) < minPacketSize {
return io.ErrUnexpectedEOF
}
packet.Raw = make([]byte, len(data))
copy(packet.Raw, data)
packet.RouteType = RouteType(data[0] & 0x03)
packet.PayloadType = PayloadType((data[0] >> 2) & 0x0f)
offset := 1
if packet.RouteType.HasTransportCodes() {
if len(data) < minPacketSize+4 {
return io.ErrUnexpectedEOF
}
packet.TransportCodes = []uint16{
binary.LittleEndian.Uint16(data[offset+0:]),
binary.LittleEndian.Uint16(data[offset+2:]),
}
offset += 4
} else {
packet.TransportCodes = nil
}
pathLength := int(data[offset])
offset += 1
if pathLength > maxPathSize {
return fmt.Errorf("meshcore: path length %d exceeds maximum of %d", pathLength, maxPathSize)
} else if pathLength > len(data[offset:]) {
return io.ErrUnexpectedEOF
}
packet.Path = make([]byte, pathLength)
offset += copy(packet.Path, data[offset:])
payloadLength := len(data[offset:])
if payloadLength > maxPayloadSize {
return fmt.Errorf("meshcore: payload length %d exceeds maximum of %d", payloadLength, maxPayloadSize)
}
packet.Payload = make([]byte, payloadLength)
copy(packet.Payload, data[offset:])
return nil
}
func (packet *Packet) Hash() []byte {
h := sha256.New()
h.Write([]byte{byte(packet.PayloadType)})
if packet.PayloadType == TypeTrace {
h.Write(packet.Path)
}
h.Write(packet.Payload)
return h.Sum(nil)[:8]
}
func (packet *Packet) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
SNR float64 `json:"snr"`
RSSI int8 `json:"rssi"`
RouteType RouteType `json:"route_type"`
PayloadType PayloadType `json:"payload_type"`
TransportCodes []uint16 `json:"transport_codes,omitempty"`
Path string `json:"path"`
Payload string `json:"payload,omitempty"`
Hash string `json:"hash"`
}{
packet.SNR,
packet.RSSI,
packet.RouteType,
packet.PayloadType,
packet.TransportCodes,
hex.EncodeToString(packet.Path),
hex.EncodeToString(packet.Payload),
hex.EncodeToString(packet.Hash()),
})
}
type RouteType byte
// Route types.
const (
TransportFlood RouteType = iota
Flood
Direct
TransportDirect
)
// HasTransportCodes indicates if this route type has transport codes in the packet.
func (rt RouteType) HasTransportCodes() bool {
return rt == TransportFlood || rt == TransportDirect
}
// IsDirect is a direct routing type.
func (rt RouteType) IsDirect() bool {
return rt == Direct || rt == TransportDirect
}
// IsFlood is a flood routing type.
func (rt RouteType) IsFlood() bool {
return rt == Flood || rt == TransportFlood
}
func (rt RouteType) String() string {
switch rt {
case TransportFlood:
return "F[T]"
case Flood:
return "F"
case Direct:
return "D"
case TransportDirect:
return "D[T]"
default:
return "?"
}
}
type PayloadType byte
// Payload types.
const (
TypeRequest PayloadType = iota
TypeResponse
TypeText
TypeAck
TypeAdvert
TypeGroupText
TypeGroupData
TypeAnonRequest
TypePath
TypeTrace
TypeMultipart
TypeControl
_ // reserved
_ // reserved
_ // reserved
TypeRawCustom
)
func (pt PayloadType) String() string {
switch pt {
case TypeRequest:
return "request"
case TypeResponse:
return "response"
case TypeText:
return "text"
case TypeAck:
return "ack"
case TypeAdvert:
return "advert"
case TypeGroupText:
return "group text"
case TypeGroupData:
return "group data"
case TypeAnonRequest:
return "anon request"
case TypePath:
return "path"
case TypeTrace:
return "trace"
case TypeMultipart:
return "multipart"
case TypeControl:
return "control"
case TypeRawCustom:
return "raw custom"
default:
return fmt.Sprintf("invalid %02x", byte(pt))
}
}

View File

@@ -0,0 +1,107 @@
package meshcore
import (
"encoding/hex"
"testing"
)
func TestDecode(t *testing.T) {
tests := []struct {
Name string
Data []byte
Payload Payload
}{
{
"request/direct",
mustHexDecode("02000F6950F496ACE4B62F77C1CB31FF24E2FF27AAE9"),
&Request{},
},
{
"request/flood",
mustHexDecode("01152E4A72D025293EA6C0F2897D45FECC0162331DBFC5B903A2DE5FF12F012505AA12BA64258639F20EC3"),
&Request{},
},
{
"response",
mustHexDecode("05064CE383CC2BFEB1ADC8757CAB3B1F05AC4019AA77CF860278F7FD"),
&Response{},
},
{
"text",
mustHexDecode("092C1D9FF5EAE2A69AAAEBCE27F5317E1FB64FEC3E985EDFD27ACC905A5B48BADAFECCCC6AB8662CFAD9D6724E26667F5FC2B7B4D7DC7BAE8F08DE13AD9C42188249"),
&Text{},
},
{
"ack",
mustHexDecode("0D0152ED9C3667"),
&Acknowledgement{},
},
{
"advert/room",
mustHexDecode("110178DE95A0BB204F245F70BD08DEF93BAA4AEB9C4AD3DE3D0A81ECF888BDF830857D9BC96266A1CDDBA8F0EFF4DE1D59390C17BCBA0ABF4B452A0E24568E8BA18606A771553B095B1AA1F249E9CA22ECE6A1FB9494D00ABB31E0566D6E96A762D8F285D3C40393000000000000000030373120526F6F6D"),
&Advert{},
},
{
"advert/repeater",
mustHexDecode("11003E4E39B98F8923DCC1E1A4C65908C9C5002CF2A8CC837F4F8B1F5A62BAE9759C20BE4466E081F73083C86AF7F7F7BBA227A9176DB7B80C45DC9454F61A799C90F9B06251F6D79F07C26D5A34A2669577CE6B93CC2D02FF20A04D99AEE5E2AD89C2CF690D929EED0F03933F5E004E4C2D564E4C2D434E54522D52503031"),
&Advert{},
},
{
"grouptext",
mustHexDecode("15011C113EF72DDDBF0FAB26DF1AB902D8062FD94AF9CF337ACB8634E55105B9D77BE8E87BB3AB67CFA4E01044B7AC5EB8B510BD1B6CA395FC991CAFB6D338CDE8599DB45360"),
&GroupText{},
},
{
"groupdata",
mustHexDecode("1940EE145428BE90740D2B92838D171164ED40286C45EB40E1390323628D0422A98FA752108D5E7361A4D8B55D98E6EF7B7D6BB6289745C3288D92E09326274911544D9B376EDCF9FCE418B34E38806DB9F402649A683BA4633AA2602E13A455408E9E00CCAD3A2284BA2B7CEAF7EDDBB76FDE5C63D34A5D14C72C8F6ED6BAB2ADAB9BA033D83B"),
&GroupData{},
},
{
"anonrequest",
mustHexDecode("1D094379CC88D0E9451C457325C3AC56D53937E22FA358D0AF64562BC038ABF99ABC2911595A13A3748573DCC0B799BEE196939DDDAE210D4D979C665FF2"),
&AnonymousRequest{},
},
{
"path",
mustHexDecode("210720CCBED9FE20431125CC1EEA3480E9B06064A5F3B21E890BF91653"),
&Path{},
},
{
"trace",
mustHexDecode("E50DE8626B887827CCE9FE43CCCDFEB3EA419F807FD2F7838AA6D3B98A8F7490BA37F6"),
&Trace{},
},
{
"control",
mustHexDecode("AD17348E1E5B0FB88C22E948BE3A835161548C561DFBB1C91AC382C06746F6569B2D3911A749A57ACC"),
&Control{},
},
{
"multipart",
mustHexDecode("2A04A0BC07E0B2157ECCBA9E2A04A0BCC35A61DE61F43B18E1283721A28871ABD4B00AAEC753B8BEB4141DF364F71E0073E80F5F94B61FA046DA3B35EDF37DFC43F0A0E7"),
&Multipart{},
},
{
"raw",
mustHexDecode("7D0A83F2A5A7441F88D10E3F7CEA3B62D383C7C5DA58F5613057EA12730F543CC38E9B3CA5EAB84621714B0E56F8B3A7D51B83108B9E942F3C7C0A6A73DC2699363F9AE7D2D35558"),
&RawCustom{},
},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
payload, err := Decode(test.Data)
if err != nil {
t.Fatal(err)
}
t.Logf("%s %s", payload.Packet(), payload)
})
}
}
func mustHexDecode(s string) []byte {
b, err := hex.DecodeString(s)
if err != nil {
panic(err)
}
return b
}

View File

@@ -0,0 +1,854 @@
package meshcore
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"log"
"time"
"git.maze.io/go/ham/protocol/meshcore/crypto"
)
var (
zeroTime time.Time
zeroPositionBytes = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
)
type Payload interface {
fmt.Stringer
// Packet returns the underlying raw packet (if available, can be nil).
Packet() *Packet
// Marhal encodes the payload to bytes.
Marshal() []byte
// Unmarshal decodes the payload from bytes.
Unmarshal([]byte) error
}
type Acknowledgement struct {
// Raw packet (optional).
Raw *Packet `json:"-"`
// Checksum of message timestamp, text and sender public key.
Checksum uint32 `json:"checksum"`
}
func (ack *Acknowledgement) String() string {
return fmt.Sprintf("checksum=%08X", ack.Checksum)
}
func (ack *Acknowledgement) Packet() *Packet {
return ack.Raw
}
func (ack *Acknowledgement) Marshal() []byte {
var b [4]byte
binary.BigEndian.PutUint32(b[:], ack.Checksum)
return b[:]
}
func (ack *Acknowledgement) Unmarshal(b []byte) error {
if len(b) < 4 {
return io.ErrUnexpectedEOF
}
ack.Checksum = binary.BigEndian.Uint32(b)
return nil
}
type RequestType byte
const (
GetStats RequestType = iota + 1
KeepAlive
GetTelemetryData
GetMinMaxAvgData
GetAccessList
GetNeighbors
GetOwnerInfo
)
func (rt RequestType) String() string {
switch rt {
case GetStats:
return "get stats"
case KeepAlive:
return "keep alive"
case GetTelemetryData:
return "get telemetry data"
case GetMinMaxAvgData:
return "get min/max/avg data"
case GetAccessList:
return "get access list"
case GetNeighbors:
return "get neighbors"
case GetOwnerInfo:
return "get owner info"
default:
return fmt.Sprintf("invalid %#02x", byte(rt))
}
}
type Request struct {
// Raw packet (optional).
Raw *Packet `json:"-"`
EncryptedData
// Only available after successful decryption:
Content *RequestContent `json:"content,omitempty"`
}
type RequestContent struct {
// Time of sending the request.
Time time.Time `json:"time"`
// Type of request.
Type RequestType `json:"type"`
// Data is the request payload.
Data []byte `json:"data"`
}
func (req *RequestContent) String() string {
return fmt.Sprintf("time=%s, type=%s, data=(%d bytes)", req.Time, req.Type.String(), len(req.Data))
}
func (req *Request) String() string {
if req.Content == nil {
return req.EncryptedData.String()
}
return req.Content.String()
}
func (req *Request) Packet() *Packet {
return req.Raw
}
type Response struct {
// Raw packet (optional).
Raw *Packet `json:"-"`
EncryptedData
// Only available after successful decryption:
Content *ResponseContent `json:"content,omitempty"`
}
type ResponseContent struct {
// Tag.
Tag uint32 `json:"tag"`
// Content of the response.
Content []byte `json:"content"`
}
func (res *ResponseContent) String() string {
return fmt.Sprintf("tag=%08X content=%q", res.Tag, res.Content)
}
func (res *Response) String() string {
if res.Content == nil {
return res.EncryptedData.String()
}
return res.Content.String()
}
func (res *Response) Packet() *Packet {
return res.Raw
}
type TextType byte
const (
PlainText TextType = iota
CLICommand
SignedPlainText
)
type Text struct {
// Raw packet (optional).
Raw *Packet `json:"-"`
EncryptedData
// Only available after successful decryption:
Content *TextContent `json:"content,omitempty"`
}
type TextContent struct {
// Time of sending the message.
Time time.Time `json:"time"`
// Type of text.
Type TextType `json:"type"`
// Attempt for thie packet.
Attempt uint8 `json:"attempt"`
// Message contains the text message.
Message []byte `json:"message"`
}
func (text *Text) Packet() *Packet {
return text.Raw
}
type EncryptedData struct {
// Destination hash is the first byte of the recipient public key.
Destination byte `json:"dst"`
// Source hash is the first byte of the sender public key.
Source byte `json:"src"`
// CipherMAC is the message authenticator.
CipherMAC uint16 `json:"cipher_mac"`
// CipherText is the encrypted message.
CipherText []byte `json:"cipher_text"`
}
func (enc *EncryptedData) String() string {
return fmt.Sprintf("dst=%02X src=%02X mac=%04X text=<encrypted %d bytes>",
enc.Destination,
enc.Source,
enc.CipherMAC,
len(enc.CipherText))
}
func (enc *EncryptedData) Marshal() []byte {
b := make([]byte, 4)
b[0] = enc.Destination
b[1] = enc.Source
binary.BigEndian.PutUint16(b[2:], enc.CipherMAC)
return append(b, enc.CipherText...)
}
func (enc *EncryptedData) Unmarshal(b []byte) error {
if len(b) < 4 {
return io.ErrUnexpectedEOF
}
enc.Destination = b[0]
enc.Source = b[1]
enc.CipherMAC = binary.BigEndian.Uint16(b[2:])
enc.CipherText = make([]byte, len(b)-4)
copy(enc.CipherText, b[4:])
return nil
}
type GroupText struct {
// Raw packet (optional).
Raw *Packet `json:"-"`
EncryptedGroupData
// Only available after successful decryption:
Content *GroupTextContent `json:"content,omitempty"`
}
type GroupTextContent struct {
// Group this was sent on (not part of the packet).
Group *Group `json:"group"`
// Time of sending.
Time time.Time `json:"time"`
// Flags is generally 0x00 indicating a plain text message.
Type TextType `json:"type"`
// Attempt is the number of retries.
Attempt uint8 `json:"attempt"`
// Text sent to the group. Typically contains a "<name>: " prefix.
Text string `json:"text"`
}
func (text *GroupTextContent) String() string {
return fmt.Sprintf("time=%s type=%02X attempt=%d text=%q",
text.Time,
text.Type,
text.Attempt,
text.Text)
}
func (text *GroupText) String() string {
if text.Content == nil {
return text.EncryptedGroupData.String()
}
return text.Content.String()
}
func (text *GroupText) Packet() *Packet {
return text.Raw
}
func (text *GroupText) Decrypt(group *Group) error {
b, err := group.Secret.MACThenDecrypt(text.CipherText, text.CipherMAC)
if err != nil {
return err
}
if len(b) < 5 {
return io.ErrUnexpectedEOF
}
text.Content = &GroupTextContent{
Group: group,
Time: decodeTime(b),
Type: TextType(b[4] >> 2),
Attempt: b[4] & 0x03,
Text: decodeCString(b[5:]),
}
return nil
}
type GroupData struct {
// Raw packet (optional).
Raw *Packet `json:"-"`
EncryptedGroupData
// Content contains the group datagram.
Content []byte `json:"content,omitempty"`
}
func (data *GroupData) String() string {
if data.Content == nil {
return data.EncryptedGroupData.String()
}
return fmt.Sprintf("data=%q", data.Content)
}
func (data *GroupData) Packet() *Packet {
return data.Raw
}
func (data *GroupData) Decrypt(group *Group) error {
b, err := group.Secret.MACThenDecrypt(data.CipherText, data.CipherMAC)
if err != nil {
return err
}
data.Content = b
return nil
}
type EncryptedGroupData struct {
// ChannelHash is the first byte of the channel public key.
ChannelHash byte `json:"channel_hash"`
// CipherMAC is the message authenticator.
CipherMAC uint16 `json:"cipher_mac"`
// CipherText is the encrypted message.
CipherText []byte `json:"cipher_text"`
}
func (enc *EncryptedGroupData) String() string {
return fmt.Sprintf("channel=%02X mac=%04X text=<encrypted %d bytes>",
enc.ChannelHash,
enc.CipherMAC,
len(enc.CipherText))
}
func (enc *EncryptedGroupData) Marshal() []byte {
b := make([]byte, 3)
b[0] = enc.ChannelHash
binary.BigEndian.PutUint16(b[1:], enc.CipherMAC)
return append(b, enc.CipherText...)
}
func (enc *EncryptedGroupData) Unmarshal(b []byte) error {
if len(b) < 3 {
return io.ErrUnexpectedEOF
}
enc.ChannelHash = b[0]
enc.CipherMAC = binary.BigEndian.Uint16(b[1:])
enc.CipherText = make([]byte, len(b)-3)
copy(enc.CipherText, b[3:])
return nil
}
type NodeType byte
const (
Chat NodeType = iota + 1
Repeater
Room
Sensor
)
func (nt NodeType) String() string {
switch nt {
case Chat:
return "chat"
case Repeater:
return "repeater"
case Room:
return "room"
case Sensor:
return "sensor"
default:
return fmt.Sprintf("<unknown %02X>", byte(nt))
}
}
const (
advertHasPosition = 0x10
advertHasFeature1 = 0x20
advertHasFeature2 = 0x40
advertHasName = 0x80
)
type Advert struct {
// Raw packet (optional).
Raw *Packet `json:"-"`
PublicKey *crypto.PublicKey `json:"public_key"`
Time time.Time `json:"time"`
Signature crypto.Signature `json:"signature"`
Type NodeType `json:"node_type"`
Position *Position `json:"position,omitempty"`
Feature1 *uint16 `json:"feature1,omitempty"`
Feature2 *uint16 `json:"feature2,omitempty"`
Name string `json:"name,omitempty"`
}
func (adv *Advert) String() string {
return fmt.Sprintf("type=%s position=%s name=%q key=%s",
adv.Type,
adv.Position,
adv.Name,
formatPublicKey(adv.PublicKey.Bytes()))
}
func (adv *Advert) Packet() *Packet {
return adv.Raw
}
func (adv *Advert) Marshal() []byte {
b := make([]byte, crypto.PublicKeySize+4+64+1)
copy(b, adv.PublicKey.Bytes())
encodeTime(b[crypto.PublicKeySize:], adv.Time)
copy(b[crypto.PublicKeySize+4:], adv.Signature[:])
const flagsOffset = crypto.PublicKeySize + 4 + 64
b[flagsOffset] = byte(adv.Type) & 0x7
if adv.Position != nil {
b[flagsOffset] |= advertHasPosition
b = append(b, adv.Position.Marshal()...)
}
if adv.Feature1 != nil {
b[flagsOffset] |= advertHasFeature1
b = append(b, byte(*adv.Feature1))
b = append(b, byte(*adv.Feature1>>8))
}
if adv.Feature2 != nil {
b[flagsOffset] |= advertHasFeature2
b = append(b, byte(*adv.Feature2))
b = append(b, byte(*adv.Feature2>>8))
}
if len(adv.Name) > 0 {
b[flagsOffset] |= advertHasName
b = append(b, []byte(adv.Name)...)
}
return b
}
func (adv *Advert) Unmarshal(b []byte) error {
if len(b) < crypto.PublicKeySize+4+64+1 {
return io.ErrUnexpectedEOF
}
var (
n int
err error
)
// parse public key
if adv.PublicKey, err = crypto.NewPublicKey(b[:crypto.PublicKeySize]); err != nil {
return err
}
n += crypto.PublicKeySize
// parse time
adv.Time = decodeTime(b[n:])
log.Printf("time: %s", adv.Time)
n += 4
// parse signature
n += copy(adv.Signature[:], b[n:])
// parse flags
flags, rest := b[n], b[n+1:]
adv.Type = NodeType(flags & 0x07)
if flags&advertHasPosition != 0 {
if len(rest) < 8 {
return io.ErrUnexpectedEOF
}
// nb: some repeaters have no location and send 0,0; we're going to ignore these
if !bytes.Equal(rest[:8], zeroPositionBytes) {
adv.Position = new(Position)
if err = adv.Position.Unmarshal(rest); err != nil {
return err
}
}
rest = rest[8:]
}
if flags&advertHasFeature1 != 0 {
if len(rest) < 2 {
return io.ErrUnexpectedEOF
}
adv.Feature1 = new(uint16)
*adv.Feature1, rest = binary.LittleEndian.Uint16(rest), rest[2:]
}
if flags&advertHasFeature2 != 0 {
if len(rest) < 2 {
return io.ErrUnexpectedEOF
}
adv.Feature2 = new(uint16)
*adv.Feature2, rest = binary.LittleEndian.Uint16(rest), rest[2:]
}
if flags&advertHasName != 0 {
adv.Name = string(rest)
}
return nil
}
type AnonymousRequest struct {
// Raw packet (optional).
Raw *Packet `json:"-"`
// Destination hash is the first byte of the recipient public key.
Destination byte `json:"dst"`
// PublicKey of the sender.
PublicKey *crypto.PublicKey `json:"public_key"`
// CipherMAC is the message authenticator.
CipherMAC uint16 `json:"cipher_mac"`
// CipherText is the encrypted message.
CipherText []byte `json:"cipher_text"`
}
func (req *AnonymousRequest) String() string {
return fmt.Sprintf("dst=%02X key=%s mac=%02X text=<encrypted %d bytes>",
req.Destination,
formatPublicKey(req.PublicKey.Bytes()),
req.CipherMAC,
len(req.CipherText))
}
func (req *AnonymousRequest) Packet() *Packet {
return req.Raw
}
func (req *AnonymousRequest) Marshal() []byte {
b := make([]byte, 1+crypto.PublicKeySize+2)
b[0] = req.Destination
n := 1 + copy(b[1:], req.PublicKey.Bytes())
binary.BigEndian.PutUint16(b[n:], req.CipherMAC)
return append(b, req.CipherText...)
}
func (req *AnonymousRequest) Unmarshal(b []byte) error {
if len(b) < 1+crypto.PublicKeySize+2 {
return io.ErrUnexpectedEOF
}
var err error
req.Destination = b[0]
if req.PublicKey, err = crypto.NewPublicKey(b[1 : 1+crypto.PublicKeySize]); err != nil {
return err
}
req.CipherMAC = binary.BigEndian.Uint16(b[1+crypto.PublicKeySize:])
req.CipherText = make([]byte, len(b)-1-crypto.PublicKeySize-2)
copy(req.CipherText, b[1+crypto.PublicKeySize+2:])
return nil
}
type Path struct {
// Raw packet (optional).
Raw *Packet `json:"-"`
EncryptedData
}
func (path *Path) Packet() *Packet {
return path.Raw
}
type Trace struct {
// Raw packet (optional).
Raw *Packet `json:"-"`
Tag uint32 `json:"tag"`
AuthCode uint32 `json:"authcode"`
Flags byte `json:"flags"`
Path []byte `json:"path"`
}
func (trace *Trace) String() string {
return fmt.Sprintf("tag=%08X authcode=%08X flags=%02X path=%s",
trace.Tag,
trace.AuthCode,
trace.Flags,
formatPath(trace.Path))
}
func (trace *Trace) Packet() *Packet {
return trace.Raw
}
func (trace *Trace) Marshal() []byte {
b := make([]byte, 4+4+1)
binary.LittleEndian.PutUint32(b[0:], trace.Tag)
binary.LittleEndian.PutUint32(b[4:], trace.AuthCode)
b[8] = trace.Flags
return append(b, trace.Path...)
}
func (trace *Trace) Unmarshal(b []byte) error {
if len(b) < 9 {
return io.ErrUnexpectedEOF
}
trace.Tag = binary.LittleEndian.Uint32(b[0:])
trace.AuthCode = binary.LittleEndian.Uint32(b[4:])
trace.Flags = b[8]
trace.Path = make([]byte, len(b)-9)
copy(trace.Path, b[9:])
return nil
}
type Multipart struct {
// Raw packet (optional).
Raw *Packet `json:"-"`
Remaining uint8 `json:"remaining"`
Type PayloadType `json:"type"`
Data []byte `json:"data"`
}
func (multi *Multipart) String() string {
return fmt.Sprintf("remaining=%d type=%s data=%q",
multi.Remaining,
multi.Type.String(),
multi.Data)
}
func (multi *Multipart) Packet() *Packet {
return multi.Raw
}
func (multi *Multipart) Marshal() []byte {
return append([]byte{
multi.Remaining<<4 | byte(multi.Type&0x0F),
}, multi.Data...)
}
func (multi *Multipart) Unmarshal(b []byte) error {
if len(b) < 1 {
return io.ErrUnexpectedEOF
}
multi.Remaining = b[0] >> 4
multi.Type = PayloadType(b[0] & 0x0F)
multi.Data = make([]byte, len(b)-1)
copy(multi.Data, b[1:])
return nil
}
type ControlType byte
const (
DiscoverRequest ControlType = 0x80
DiscoverResponse ControlType = 0x90
)
type Control struct {
// Raw packet (optional).
Raw *Packet `json:"-"`
// Type of control packet.
Type ControlType `json:"type"`
// Request for discovery.
Request *ControlDiscoverRequest `json:"request,omitempty"`
// Response for discovery.
Response *ControlDiscoverResponse `json:"response,omitempty"`
// Data contains the data bytes for unknown/unparsed control types.
Data []byte `json:"data"`
}
type ControlDiscoverRequest struct {
Flags byte `json:"flags"` // upper 4 bits
PrefixOnly bool `json:"prefix_only"` // lower 1 bit
TypeFilter byte `json:"type_filter"`
Tag uint32 `json:"tag"`
Since *time.Time `json:"since,omitempty"`
}
func (req ControlDiscoverRequest) String() string {
var since string
if req.Since != nil {
since = " " + req.Since.String()
}
return fmt.Sprintf("flags=%X prefixonly=%t filter=%08b tag=%08X"+since,
req.Flags,
req.PrefixOnly,
req.TypeFilter,
req.Tag)
}
type ControlDiscoverResponse struct {
Flags byte `json:"flags"` // upper 4 bits
NodeType NodeType `json:"node_type"` // lower 4 bits
SNR float64 `json:"snr"`
Tag uint32 `json:"tag"`
PublicKey []byte `json:"public_key"` // 8 or 32 bytes
}
func (res ControlDiscoverResponse) String() string {
return fmt.Sprintf("flags=%X node=%s snr=%g tag=%08X key=%s",
res.Flags,
res.NodeType.String(),
res.SNR,
res.Tag,
formatPublicKey(res.PublicKey))
}
func (control *Control) String() string {
switch {
case control.Request != nil:
return "request " + control.Request.String()
case control.Response != nil:
return "response " + control.Response.String()
default:
return fmt.Sprintf("type=%02X data=%q", control.Type, control.Data)
}
}
func (control *Control) Packet() *Packet {
return control.Raw
}
func (control *Control) Marshal() []byte {
b := []byte{byte(control.Type)}
switch control.Type {
case DiscoverRequest:
if control.Request != nil {
b = append(b,
control.Request.Flags<<4, // 1
control.Request.TypeFilter, // 2
0x00, 0x00, 0x00, 0x00, // 3-6: tag
0x00, 0x00, 0x00, 0x00, // 7-10: since
)
if control.Request.PrefixOnly {
b[1] |= 0x01
}
binary.LittleEndian.PutUint32(b[3:], control.Request.Tag)
if control.Request.Since != nil {
encodeTime(b[7:], *control.Request.Since)
}
}
case DiscoverResponse:
if control.Response != nil {
b = append(b,
control.Response.Flags<<4|byte(control.Response.NodeType&0x0F), // 1
byte(control.Response.SNR*4), // 2
0x00, 0x00, 0x00, 0x00, // 3-6: tag
)
binary.LittleEndian.PutUint32(b[3:], control.Response.Tag)
}
b = append(b, control.Response.PublicKey...)
default:
b = append(b, control.Data...)
}
return b
}
func (control *Control) Unmarshal(b []byte) error {
if len(b) < 1 {
return io.ErrUnexpectedEOF
}
control.Type = ControlType(b[0] >> 4)
switch control.Type {
case DiscoverRequest:
if len(b) < 7 {
return io.ErrUnexpectedEOF
}
control.Request = &ControlDiscoverRequest{
Flags: b[1] >> 4,
PrefixOnly: b[1]&0x01 != 0,
TypeFilter: b[2],
Tag: binary.LittleEndian.Uint32(b[3:]),
}
if len(b) >= 11 {
since := decodeTime(b[7:])
control.Request.Since = &since
}
case DiscoverResponse:
if len(b) < 15 {
return io.ErrUnexpectedEOF
}
control.Response = &ControlDiscoverResponse{
Flags: b[1] >> 4,
NodeType: NodeType(b[1] & 0x0F),
SNR: float64(int8(b[2])) / 4,
Tag: binary.LittleEndian.Uint32(b[3:]),
}
control.Response.PublicKey = make([]byte, len(b)-6)
copy(control.Response.PublicKey, b[6:])
default:
control.Data = make([]byte, len(b)-1)
copy(control.Data, b[1:])
}
return nil
}
type RawCustom struct {
// Raw packet (optional).
Raw *Packet `json:"-"`
// Data in the payload.
Data []byte `json:"data"`
}
func (raw *RawCustom) String() string {
return fmt.Sprintf("data=%q", raw.Data)
}
func (raw *RawCustom) Packet() *Packet {
return raw.Raw
}
func (raw *RawCustom) Marshal() []byte {
return raw.Data
}
func (raw *RawCustom) Unmarshal(b []byte) error {
raw.Data = make([]byte, len(b))
copy(raw.Data, b)
return nil
}
var (
_ Payload = (*Acknowledgement)(nil)
_ Payload = (*Request)(nil)
_ Payload = (*Response)(nil)
_ Payload = (*Text)(nil)
_ Payload = (*GroupText)(nil)
_ Payload = (*GroupData)(nil)
)

84
protocol/meshcore/util.go Normal file
View File

@@ -0,0 +1,84 @@
package meshcore
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"strings"
"time"
"git.maze.io/go/ham/protocol/meshcore/crypto"
)
type Position struct {
Latitude float64
Longitude float64
}
func (pos *Position) String() string {
if pos == nil {
return "<unknown>"
}
return fmt.Sprintf("%f,%f", pos.Latitude, pos.Longitude)
}
func (pos *Position) Marshal() []byte {
var buf [8]byte
binary.LittleEndian.PutUint32(buf[0:], uint32(pos.Latitude*1e6))
binary.LittleEndian.PutUint32(buf[4:], uint32(pos.Longitude*1e6))
return buf[:]
}
func (pos *Position) Unmarshal(b []byte) error {
if len(b) < 8 {
return io.ErrUnexpectedEOF
}
pos.Latitude = float64(binary.LittleEndian.Uint32(b[0:])) / 1e6
pos.Longitude = float64(binary.LittleEndian.Uint32(b[4:])) / 1e6
return nil
}
func encodeTime(b []byte, t time.Time) {
binary.LittleEndian.PutUint32(b, uint32(t.Unix()))
}
func decodeTime(b []byte) time.Time {
return time.Unix(int64(binary.LittleEndian.Uint32(b)), 0).UTC()
}
func encodeFrequency(b []byte, f float64) {
binary.LittleEndian.PutUint32(b, uint32(f*1e3))
}
func decodeFrequency(b []byte) float64 {
return float64(int64(binary.LittleEndian.Uint32(b))) / 1e3
}
func decodeCString(b []byte) string {
if i := bytes.IndexByte(b, 0x00); i > -1 {
return string(b[:i])
}
return string(b)
}
func formatPath(path []byte) string {
p := make([]string, len(path))
for i, node := range path {
p[i] = fmt.Sprintf("%02X", node)
}
return strings.Join(p, ">")
}
func formatPublicKey(b []byte) string {
switch len(b) {
case 8:
return fmt.Sprintf("<%016x>", b)
case crypto.PublicKeySize:
return fmt.Sprintf("<%06x…%06x>", b[:3], b[29:])
case crypto.PrivateKeySize:
return formatPublicKey(b[32:])
default:
return "<unknown>"
}
}

30
protocol/packet.go Normal file
View File

@@ -0,0 +1,30 @@
package protocol
// Packet represents a raw packet.
type Packet struct {
Protocol string `json:"protocol"` // Protocol name
SNR float64 `json:"snr"` // Signal-to-noise Ratio
RSSI int8 `json:"rssi"` // Received Signal Strength Indicator
Raw []byte `json:"raw"` // Raw packet
}
type Device interface {
// Close the device.
Close() error
}
// Receiver of packets.
type Receiver interface {
Device
// RawPackets starts receiving raw packets.
RawPackets() <-chan *Packet
}
// Transmitter of packets.
type Transmitter interface {
Device
// SendRawPacket sends a raw (encoded) packet.
SendRawPacket(*Packet) error
}