lorawan

package
v0.783.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 29, 2026 License: AGPL-3.0 Imports: 4 Imported by: 0

Documentation

Overview

Package lorawan decodes LoRaWAN PHYPayload frames — the MAC-layer packet format used by LoRaWAN 1.0.x and 1.1 networks. Pure offline parser; no transport, no hardware.

Wrap-vs-native judgement: LoRaWAN is a fully open specification (LoRa Alliance LoRaWAN 1.0.x / 1.1 specifications). The walker is bit-level decoding over a ~12-300 byte frame with a documented MAC-header byte and a FHDR / FPort / FRMPayload split. Wrapping a FAP for this would require an SD-card install + a firmware-fork dependency for a pure parser. Native delivers offline analysis — operators paste a captured PHYPayload (from a Flipper LoRa sub-board, a CatSniffer, or any LoRa SDR) and inspect every MAC-layer field without an antenna attached.

Pairs with the bruce_lora_scan capability (which gets a device-side LoRa scan running) and with future LoRaWAN containerbridge integrations — this Spec is the offline- analyst entry point.

What this package covers:

  • PHYPayload split: MHDR + MACPayload + 4-byte MIC
  • MHDR decode: MType (Join Request / Accept, Confirmed / Unconfirmed Data Up / Down, Rejoin, Proprietary) + Major
  • Data-frame MACPayload walk: FHDR (DevAddr little-endian, FCtrl bitfield, FCnt, FOpts MAC commands), FPort, FRMPayload (surfaced as hex; encrypted under AppSKey)
  • FCtrl bitfield decode with uplink vs downlink interpretation (uplink: ADR / ADRACKReq / ACK / ClassB / FOptsLen; downlink: ADR / RFU / ACK / FPending / FOptsLen)
  • Join Request decode: JoinEUI + DevEUI + DevNonce (all little-endian on the wire)
  • Join Accept decode: AppNonce + NetID + DevAddr + DLSettings
  • RxDelay + optional CFList (encrypted under AppKey; we surface structure when called with the decrypted form)

What this package does NOT cover (deliberately out of scope):

  • AES-CMAC MIC validation (needs NwkSKey / NwkSEncKey)
  • FRMPayload decryption (needs AppSKey)
  • Join Accept decryption (needs AppKey — the Join Accept decoder walks the cleartext-structure form; operators decrypt with their own AppKey before passing)
  • PHY-layer decode (chirp / spreading-factor / CR — those are the SDR's job, not the MAC-layer parser's)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type DataPayload

type DataPayload struct {
	FHDR FHDR `json:"fhdr"`
	// FPort: 1 byte. nil when FRMPayload is empty.
	FPort *int `json:"f_port,omitempty"`
	// FRMPayloadHex is the encrypted application payload as hex.
	// Empty when no FRMPayload is present. We do not decrypt
	// (needs AppSKey / NwkSEncKey out-of-band).
	FRMPayloadHex string `json:"frm_payload_hex,omitempty"`
}

DataPayload is the structured view of a data-frame MACPayload (Unconfirmed/Confirmed Data Up/Down).

type FCtrl

type FCtrl struct {
	Raw int  `json:"raw"`
	ADR bool `json:"adr"`
	// ADRACKReq is set on uplinks only.
	ADRACKReq bool `json:"adr_ack_req,omitempty"`
	ACK       bool `json:"ack"`
	// ClassB is set on uplinks only; FPending on downlinks only.
	ClassB   bool `json:"class_b,omitempty"`
	FPending bool `json:"f_pending,omitempty"`
	FOptsLen int  `json:"f_opts_len"`
}

FCtrl is the decoded 1-byte FCtrl field of an FHDR. Bit interpretations differ between uplink and downlink frames.

type FHDR

type FHDR struct {
	// DevAddr is the 32-bit device address. Stored on the wire
	// little-endian; we render the big-endian hex form here so
	// it matches the form network servers / chirpstack use.
	DevAddrHex string `json:"dev_addr_hex"`
	DevAddr    uint32 `json:"dev_addr"`
	FCtrl      FCtrl  `json:"f_ctrl"`
	FCnt       int    `json:"f_cnt"`
	// FOptsHex is the optional MAC-command field (up to 15
	// bytes). Surfaced as hex; we don't dissect MAC commands
	// here (that's a follow-on Spec when a caller materialises).
	FOptsHex string `json:"f_opts_hex,omitempty"`
}

FHDR is the Frame Header — DevAddr + FCtrl + FCnt + FOpts.

type JoinAccept

type JoinAccept struct {
	// AppNonce is the 3-byte network-supplied nonce.
	AppNonceHex string `json:"app_nonce_hex"`
	// NetID is the 3-byte network identifier.
	NetIDHex string `json:"net_id_hex"`
	// DevAddr is the 4-byte device address assigned to the
	// device for this session.
	DevAddrHex string `json:"dev_addr_hex"`
	DevAddr    uint32 `json:"dev_addr"`
	// DLSettings is the downlink-settings byte (RX1 DR offset +
	// RX2 data rate).
	DLSettings int `json:"dl_settings"`
	// RxDelay is the receive delay byte (0-15 seconds).
	RxDelay int `json:"rx_delay"`
	// CFListHex is the optional 16-byte channel-frequency list.
	// Empty when absent.
	CFListHex string `json:"cf_list_hex,omitempty"`
}

JoinAccept is the structured view of a (decrypted) Join Accept MACPayload. Network servers encrypt Join Accept with AppKey using AES-128-ECB before transmit; operators bring the decrypted bytes before calling this decoder.

type JoinRequest

type JoinRequest struct {
	// JoinEUI is the 8-byte EUI-64 of the Join Server (LoRaWAN
	// 1.0.x called this AppEUI). Wire form is little-endian; we
	// render the big-endian hex here so it matches the form
	// printed on device labels.
	JoinEUIHex string `json:"join_eui_hex"`
	// DevEUI is the 8-byte EUI-64 of the device. Same little-
	// endian-on-wire convention.
	DevEUIHex string `json:"dev_eui_hex"`
	// DevNonce is the 2-byte little-endian device-supplied nonce.
	DevNonce int `json:"dev_nonce"`
}

JoinRequest is the structured view of a Join Request MACPayload.

type MHDR

type MHDR struct {
	Raw    int    `json:"raw"`
	MType  int    `json:"mtype"`
	Name   string `json:"mtype_name"`
	Major  int    `json:"major"`
	Uplink bool   `json:"uplink"`
}

MHDR is the 1-byte MAC header.

type MType

type MType int

MType is the 3-bit Message Type field at the top of the MHDR.

const (
	MTypeJoinRequest         MType = 0
	MTypeJoinAccept          MType = 1
	MTypeUnconfirmedDataUp   MType = 2
	MTypeUnconfirmedDataDown MType = 3
	MTypeConfirmedDataUp     MType = 4
	MTypeConfirmedDataDown   MType = 5
	MTypeRejoinRequest       MType = 6
	MTypeProprietary         MType = 7
)
func (m MType) IsUplink() bool

IsUplink reports whether the MType is one of the uplink kinds (Join Request, Confirmed/Unconfirmed Data Up, Rejoin Request). Drives the FCtrl bitfield interpretation.

func (MType) String

func (m MType) String() string

type PHYPayload

type PHYPayload struct {
	// MHDR is the parsed MAC header.
	MHDR MHDR `json:"mhdr"`
	// MIC is the 4-byte Message Integrity Code at the frame end.
	MICHex string `json:"mic_hex"`
	// Data is set for the data-frame MTypes (2-5).
	Data *DataPayload `json:"data,omitempty"`
	// JoinRequest is set when MType == 0.
	JoinRequest *JoinRequest `json:"join_request,omitempty"`
	// JoinAccept is set when MType == 1 (assumes operator has
	// decrypted the body).
	JoinAccept *JoinAccept `json:"join_accept,omitempty"`
	// RejoinRequestHex is set when MType == 6 (Rejoin Request).
	// We surface the raw bytes — Rejoin Type byte then variable
	// fields per LoRaWAN 1.1 §6.2.4.
	RejoinRequestHex string `json:"rejoin_request_hex,omitempty"`
	// ProprietaryHex is set when MType == 7.
	ProprietaryHex string `json:"proprietary_hex,omitempty"`
	// PayloadHex is the raw PHYPayload for callers that want to
	// re-render or audit.
	PayloadHex string `json:"payload_hex"`
}

PHYPayload is the top-level decoded frame.

func Decode

func Decode(hexBlob string) (PHYPayload, error)

Decode parses a hex-encoded LoRaWAN PHYPayload frame. Tolerates ':' / '-' / '_' / whitespace separators.

func DecodeBytes

func DecodeBytes(b []byte) (PHYPayload, error)

DecodeBytes is the byte-slice variant of Decode.

type ReplayAnalysis added in v0.406.0

type ReplayAnalysis struct {
	FramesAnalyzed int                             `json:"frames_analyzed"`
	Streams        int                             `json:"streams"`
	PerStream      map[string]*ReplayStreamSummary `json:"per_stream"`
	Observations   []ReplayObservation             `json:"observations"`
	Notes          []string                        `json:"notes,omitempty"`
}

ReplayAnalysis is the structured result of AnalyzeReplay.

func AnalyzeReplay added in v0.406.0

func AnalyzeReplay(frames []ReplayFrame) (*ReplayAnalysis, error)

AnalyzeReplay inspects an ordered sequence of decoded LoRaWAN data frames for replay / frame-counter-reuse — the attack the spec's mandatory FCnt check exists to stop (replayed captured uplinks; ABP devices that reset their counter). Frames are grouped by (DevAddr, direction); the uplink and downlink counters are independent, so they are tracked separately.

Two deterministic, KEY-FREE signals (FCnt is cleartext in the FHDR), each an OBSERVATION with its benign explanation:

  • fcnt_reuse (warning): a frame counter value reappears for a stream after the device had already moved past it — a rolling counter must not repeat, so this is the core replay signature. CONSECUTIVE equal counters are NOT flagged: a confirmed frame is legitimately retransmitted with the same FCnt until acknowledged (collapsed to one logical transmission). Benign explanation: the operator's own tooling re-sent a captured frame.
  • fcnt_regression (warning): a counter lower than the running max for the stream. Counters increase monotonically by design. Benign explanation: a 16-bit FCnt rollover (65535 -> 0), an ABP device that was power-cycled, or frames fed out of capture order.

type ReplayFrame added in v0.406.0

type ReplayFrame struct {
	DevAddr string `json:"dev_addr"`
	FCnt    int    `json:"fcnt"`
	MType   string `json:"mtype,omitempty"`
}

ReplayFrame is one already-decoded LoRaWAN data frame supplied by the caller (e.g. from the lorawan PHYPayload decoder). Only the cleartext FHDR fields are needed — no session key.

  • DevAddr is the 4-byte device address (the FHDR field), grouping frames by end-device.
  • FCnt is the frame counter from the FHDR (the 16-bit on-air value).
  • MType is the message type string ("UnconfirmedDataUp", "ConfirmedDataDown", …); its Up/Down direction separates the independent uplink and downlink counters. Empty = treated as one stream (with a note).

type ReplayObservation added in v0.406.0

type ReplayObservation struct {
	Kind      string `json:"kind"`     // "fcnt_reuse" | "fcnt_regression"
	Severity  string `json:"severity"` // "warning"
	DevAddr   string `json:"dev_addr"`
	Direction string `json:"direction"`
	Detail    string `json:"detail"`
}

ReplayObservation is one flagged signal — an OBSERVATION with its benign explanation, never a verdict.

type ReplayStreamSummary added in v0.406.0

type ReplayStreamSummary struct {
	Frames               int `json:"frames"`
	LogicalTransmissions int `json:"logical_transmissions"`
	Retransmissions      int `json:"retransmissions"`
	MaxFCnt              int `json:"max_fcnt"`
}

ReplayStreamSummary is the per-(device,direction) roll-up.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL