rfscope

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Jul 14, 2026 License: Apache-2.0 Imports: 30 Imported by: 0

Documentation

Overview

Package rfscope is protocol-agnostic RF network analysis — "Wireshark for the RF physical layer". Pointed at any band (a recorded IQ capture or a live SDR) with no prior knowledge of the technology, modulation, framing, or encryption, it produces a structured Scene describing what is on the air and how it behaves: an RF protocol hierarchy, per-channel activity/occupancy, burst timing and periodicity, an emitter/conversation graph, and an entropy/ encryption triage that bridges into the cryptolab toolkit.

rfscope sits at the top of the analysis lattice and imports only downward — survey, carriers, siglab, hunt, the cryptolab engines, and the dsp primitives. None of those import rfscope, so there is no cycle. It adds no DSP of its own; it orchestrates the existing primitives and accumulates their output into one shared, JSON-exportable Scene (the protocol-agnostic peer of hunt.DiscoveredSystem).

Index

Constants

View Source
const CryptolabLinked = false

CryptolabLinked reports whether the cryptolab toolkit is compiled into this binary. The default build links the stub, so it is false; rebuild with -tags cryptolab to enable the registry-driven tool hand-off.

Variables

View Source
var ErrCryptolabNotLinked = errors.New("rfscope: cryptolab toolkit not linked — rebuild with -tags cryptolab")

ErrCryptolabNotLinked is returned by RunCryptolabTool in the default build.

Functions

func DetectKeystreamReuse

func DetectKeystreamReuse(sc *Scene) []keystream.ReuseGroup

DetectKeystreamReuse builds keystream.Frames from the entropy payloads and reports reuse groups (frames sharing an IV/MI) via keystream.FindReuse — the in-binary half of the keystream-reuse workflow. Generic unknown payloads carry no IV, so this fires only once IV/MI recovery is wired for a partial protocol; when it does, the same frames feed `cryptolab ks reuse|mtp`.

func Drain

func Drain(ctx context.Context, src Source, chunk, maxSamples int) ([]complex64, error)

Drain reads the whole source into one buffer, capped at maxSamples (0 = no cap). It is the convenience the offline analyze path uses to materialize a bounded capture; the segmentation engine can also stream Next directly for live use. chunk is the per-read size.

func EmitFrames

func EmitFrames(sc *Scene, w io.Writer) (int, error)

EmitFrames writes the entropy analyzer's recovered payloads as a cryptolab `ks` frames file: one JSON object per line, {label, iv, ct} with iv/ct hex-encoded. The emitted file feeds `cryptolab ks reuse|mtp|extract`, `classify auto`, and `brute` directly. Returns the number of frames written. A frame with no recovered IV carries an empty iv (cryptolab ignores it for reuse but still accepts it for the other tools).

func Register

func Register(a Analyzer)

Register adds an analyzer to the global registry. It panics on a duplicate name, which can only happen from a programming error at init time.

func Run

func Run(ctx context.Context, sc *Scene, in *Input, names ...string) error

Run resolves the requested analyzers (empty names ⇒ all registered) plus their transitive dependencies, orders them with a topological sort over DependsOn, and runs each Analyze in turn against sc. It returns an error on an unknown analyzer name, a dependency cycle, or the first Analyze failure.

func RunCryptolabTool

func RunCryptolabTool(_ context.Context, _, _ string, _ []string) (*cryptolab.Result, error)

RunCryptolabTool is the stub for the default (untagged) build. The in-binary entropy triage (entropy.go) and the ks frames emission (bridge.go) still work; only the registry-driven tool invocation needs the cryptolab build tag.

func Segment

func Segment(ctx context.Context, src Source, cfg SegmentConfig) (*Scene, *Input, error)

Segment reads the wideband span from src, discovers carriers, and slices each into onset→offset bursts — the protocol-agnostic atoms every analyzer consumes. It returns a Scene populated with Bursts and header fields, and an Input whose BurstIQ accessor serves each burst's decimated baseband. It is the producer pre-pass; rfscope.Run then layers the derived views on top.

func Write

func Write(w io.Writer, sc *Scene, f Format) error

Write serializes sc to w in the requested format. It Sanitizes the scene first so a stray NaN/Inf never reaches the encoder (or the SSE/WS wire).

Types

type Analyzer

type Analyzer interface {
	Name() string
	Synopsis() string
	DependsOn() []string
	Analyze(ctx context.Context, sc *Scene, in *Input) error
}

Analyzer is one named analysis pass over a Scene. Analyzers register themselves from init() (mirroring the cryptolab Tool registry and the ccdecoder pipeline factories), so adding one is a new file plus a blank import in the CLI wiring. DependsOn names analyzers that must run first; Run topologically sorts on it.

func Analyzers

func Analyzers() []Analyzer

Analyzers returns every registered analyzer sorted by name.

func Lookup

func Lookup(name string) (Analyzer, bool)

Lookup returns the analyzer registered under name.

type Anomaly

type Anomaly struct {
	Severity  string `json:"severity"` // "note" | "warn" | "alert"
	Kind      string `json:"kind"`     // "intermittent" | "hopper" | "wide-carrier" | …
	FreqHz    uint32 `json:"freq_hz,omitempty"`
	EmitterID uint64 `json:"emitter_id,omitempty"`
	Detail    string `json:"detail"`
}

Anomaly is one RF expert-info finding.

type Bin

type Bin struct {
	TSec       float64 `json:"t_sec"`
	PowerDbFS  float32 `json:"power_dbfs"`
	Occupied   bool    `json:"occupied"`
	BurstCount int     `json:"burst_count"`
}

Bin is one time-bucket of a channel's activity timeline.

type Burst

type Burst struct {
	ID       uint64  `json:"id"`
	FreqHz   uint32  `json:"freq_hz"`
	StartSec float64 `json:"start_sec"`
	EndSec   float64 `json:"end_sec"`
	PeakDbFS float32 `json:"peak_dbfs"`
	SNRDb    float32 `json:"snr_db"`

	OccupiedBwHz     uint32  `json:"occupied_bw_hz"`
	ChannelPowerDBFS float64 `json:"channel_power_dbfs"`
	ACPRUpperDB      float64 `json:"acpr_upper_db"`
	ACPRLowerDB      float64 `json:"acpr_lower_db"`
	// SpectralFlatness is the Wiener entropy 0..1: ≈1 noise-like/spread, ≪1 a
	// tone or narrow carrier — a high-value protocol-agnostic discriminator.
	SpectralFlatness float64 `json:"spectral_flatness"`

	Class    survey.SignalClass   `json:"class"`
	Features survey.ClassFeatures `json:"features"`

	// EmitterID back-references the owning emitter once clustered (0 = none).
	EmitterID uint64 `json:"emitter_id,omitempty"`
}

Burst is one onset→offset activity segment on one frequency — the atom every downstream statistic is built from. The spectral fields (OccupiedBwHz, ChannelPowerDBFS, ACPR*, SpectralFlatness) come from spectrum.ComputeOccupancy (PR #831); Class/Features come from survey.Classify.

func (Burst) DurationSec

func (b Burst) DurationSec() float64

DurationSec is the burst's on-air time.

type Channel

type Channel struct {
	FreqHz          uint32             `json:"freq_hz"`
	OccupiedBwHz    uint32             `json:"occupied_bw_hz"`
	DominantClass   survey.SignalClass `json:"dominant_class"`
	BurstCount      int                `json:"burst_count"`
	AirtimeSec      float64            `json:"airtime_sec"`
	DutyCycle       float64            `json:"duty_cycle"`
	OccupancyPct    float64            `json:"occupancy_pct"`
	BurstRatePerMin float64            `json:"burst_rate_per_min"`
	MedianPowerDbFS float32            `json:"median_power_dbfs"`
	MedianFlatness  float64            `json:"median_flatness"`

	Timeline []Bin `json:"timeline,omitempty"`

	BurstLenHist     Hist    `json:"burst_len_hist"`
	InterArrivalHist Hist    `json:"inter_arrival_hist"`
	PeriodSec        float64 `json:"period_sec"`
	PeriodConfidence float64 `json:"period_confidence"`
}

Channel is the per-frequency, time-aggregated view — the I/O-graph row.

type Conversation

type Conversation struct {
	ID          uint64   `json:"id"`
	EmitterIDs  []uint64 `json:"emitter_ids"`
	Kind        string   `json:"kind"`
	Correlation float64  `json:"correlation"`
	Exchanges   int      `json:"exchanges"`
}

Conversation links temporally correlated emitters — the generic conversation/endpoint graph (request/response, paired TDMA slots, a hop sequence, or consistently co-active emitters).

type Emitter

type Emitter struct {
	ID              uint64      `json:"id"`
	Label           string      `json:"label"`
	Fingerprint     Fingerprint `json:"fingerprint"`
	BurstIDs        []uint64    `json:"burst_ids,omitempty"`
	FirstSec        float64     `json:"first_sec"`
	LastSec         float64     `json:"last_sec"`
	TotalAirtimeSec float64     `json:"total_airtime_sec"`
	// HopSet holds >1 frequency when the emitter is a frequency hopper.
	HopSet []uint32 `json:"hop_set,omitempty"`
}

Emitter is a cluster of bursts sharing an RF fingerprint — the protocol- agnostic generalization of hunt's per-site/per-unit identity.

type EntropyResult

type EntropyResult struct {
	EmitterID          uint64  `json:"emitter_id"`
	FreqHz             uint32  `json:"freq_hz"`
	Bytes              int     `json:"bytes"`
	Class              string  `json:"class"`
	Confidence         float64 `json:"confidence"`
	EntropyBits        float64 `json:"entropy_bits"`
	IndexOfCoincidence float64 `json:"index_of_coincidence"`
	ChiSquareUniform   float64 `json:"chi_square_uniform"`
	KeyLen             int     `json:"key_len,omitempty"`
	LooksRandom        bool    `json:"looks_random"`
	Recommended        string  `json:"recommended"`
	// contains filtered or unexported fields
}

EntropyResult is the byte-level triage of one emitter's recovered payload. It mirrors the cryptolab `classify auto` taxonomy so an operator can take the recommended next command straight into the cryptolab toolkit. The raw payload is carried (unserialized) for the cryptolab bridge.

type FileSource

type FileSource struct {
	// contains filtered or unexported fields
}

FileSource streams an on-disk IQ capture (u8/f32) in chunks, decoding through the same siglab.SampleFormat decoders the replay path uses so rfscope and replay read captures identically.

func OpenFile

func OpenFile(path string, format siglab.SampleFormat, centerHz uint32, rateHz float64) (*FileSource, error)

OpenFile opens an IQ capture for streaming. centerHz/rateHz describe the capture (the segmentation engine stamps absolute frequencies from them).

func (*FileSource) CenterHz

func (s *FileSource) CenterHz() uint32

func (*FileSource) Close

func (s *FileSource) Close() error

Close releases the underlying file.

func (*FileSource) Next

func (s *FileSource) Next(ctx context.Context, n int) ([]complex64, error)

Next reads up to n samples from the file. It returns io.EOF (with any final partial chunk on the preceding call) once the file is exhausted.

func (*FileSource) SampleRateHz

func (s *FileSource) SampleRateHz() float64

type Fingerprint

type Fingerprint struct {
	CenterHz          uint32  `json:"center_hz"`
	OccupiedBwHz      uint32  `json:"occupied_bw_hz"`
	ClassFamily       string  `json:"class_family"`
	MedianPowerDbFS   float32 `json:"median_power_dbfs"`
	MedianBurstLenSec float64 `json:"median_burst_len_sec"`
	SpectralFlatness  float64 `json:"spectral_flatness"`
	PeriodSec         float64 `json:"period_sec"`
}

Fingerprint is the feature vector emitters are clustered on.

type Format

type Format int

Format is an rfscope scene serialization. It mirrors the multi-format export convention the siglab and hunt subcommands use.

const (
	// FormatJSON is one indented JSON object for the whole scene.
	FormatJSON Format = iota
	// FormatJSONL is one JSON record per line: a scene header, then each burst,
	// channel, emitter, conversation, and anomaly — a stream-friendly form.
	FormatJSONL
	// FormatYAML is a single YAML document.
	FormatYAML
	// FormatCSV is the burst table (one row per burst).
	FormatCSV
	// FormatSummary is the human-readable hierarchy + channel table report.
	FormatSummary
)

func ParseFormat

func ParseFormat(s string) (Format, error)

ParseFormat maps a -format flag value to a Format.

func (Format) FileExtension

func (f Format) FileExtension() string

FileExtension returns the conventional file extension for the format.

func (Format) String

func (f Format) String() string

type HierNode

type HierNode struct {
	Depth    int                `json:"depth"` // 0=class, 1=bandwidth bucket, 2=protocol candidate
	Label    string             `json:"label"`
	Class    survey.SignalClass `json:"class,omitempty"`
	Protocol string             `json:"protocol,omitempty"`
	Stats    HierStats          `json:"stats"`
}

HierNode is one row of the flattened hierarchy tree.

type HierStats

type HierStats struct {
	EmitterCount int     `json:"emitter_count"`
	BurstCount   int     `json:"burst_count"`
	AirtimeSec   float64 `json:"airtime_sec"`
	SpectrumPct  float64 `json:"spectrum_pct"`
	TimePct      float64 `json:"time_pct"`
	SymbolVolume int64   `json:"symbol_volume"`
	ByteVolume   int64   `json:"byte_volume"`
}

HierStats are the aggregate figures attached to a hierarchy node.

type Hist

type Hist struct {
	Counts []int     `json:"counts,omitempty"`
	Edges  []float64 `json:"edges,omitempty"`
}

Hist is a simple histogram pairing bin counts with their edges, matching the (counts, edges) shape dsp/stats.Histogram returns.

type Input

type Input struct {
	// SampleRateHz is the capture's IQ sample rate.
	SampleRateHz float64
	// Window is the observation window in seconds (the scene's time span).
	Window float64
	// Log is the run logger; never nil (Run substitutes a discard logger).
	Log *slog.Logger
	// BurstIQ returns the decimated baseband IQ of one burst and its rate, or an
	// error if the source can no longer be read. Analyzers that only need the
	// burst metadata (timeline, timing) ignore it; classifiers/identifiers call
	// it on demand.
	BurstIQ func(b Burst) (iq []complex64, rateHz float64, err error)
}

Input is the read-only material an Analyzer works from. The segmented bursts already live on the Scene; Input adds the run-level parameters and a lazy accessor back to each burst's decimated baseband IQ, so a long capture is never held whole in memory.

type LiveSource

type LiveSource struct {
	// contains filtered or unexported fields
}

LiveSource adapts a hunt.IQSource (the live, broker-backed SDR provider the daemon wires) to a scene Source by tuning it once to a fixed center and streaming sequential captures.

func NewLiveSource

func NewLiveSource(src hunt.IQSource, centerHz uint32) (*LiveSource, error)

NewLiveSource tunes src to centerHz and returns it as a streaming Source.

func (*LiveSource) CenterHz

func (s *LiveSource) CenterHz() uint32

func (*LiveSource) Next

func (s *LiveSource) Next(ctx context.Context, n int) ([]complex64, error)

func (*LiveSource) SampleRateHz

func (s *LiveSource) SampleRateHz() float64

type MemSource

type MemSource struct {
	// contains filtered or unexported fields
}

MemSource is a deterministic in-memory Source for tests: it serves a buffer once, in chunks, then returns io.EOF.

func NewMemSource

func NewMemSource(iq []complex64, centerHz uint32, rateHz float64) *MemSource

NewMemSource builds an in-memory Source over iq.

func (*MemSource) CenterHz

func (s *MemSource) CenterHz() uint32

func (*MemSource) Next

func (s *MemSource) Next(ctx context.Context, n int) ([]complex64, error)

func (*MemSource) SampleRateHz

func (s *MemSource) SampleRateHz() float64

type ProtocolHierarchy

type ProtocolHierarchy struct {
	Total HierStats  `json:"total"`
	Nodes []HierNode `json:"nodes,omitempty"`
}

ProtocolHierarchy is the class→bandwidth→protocol tree, flattened depth-first with a Depth field for rendering.

type Scene

type Scene struct {
	Source     string    `json:"source"`
	StartedAt  time.Time `json:"started_at"`
	FinishedAt time.Time `json:"finished_at"`
	CenterHz   uint32    `json:"center_hz"`
	SpanHz     uint32    `json:"span_hz"`
	// WindowSec is the observation window: how much signal time the scene covers.
	WindowSec float64 `json:"window_sec"`
	// NoiseFloorDbFS is the scene-wide robust noise floor (spectrum.Percentile).
	NoiseFloorDbFS float32 `json:"noise_floor_dbfs"`

	Bursts        []Burst           `json:"bursts,omitempty"`
	Channels      []Channel         `json:"channels,omitempty"`
	Emitters      []Emitter         `json:"emitters,omitempty"`
	Conversations []Conversation    `json:"conversations,omitempty"`
	Hierarchy     ProtocolHierarchy `json:"hierarchy"`
	Anomalies     []Anomaly         `json:"anomalies,omitempty"`

	AnalyzerOutputs map[string]any `json:"analyzer_outputs,omitempty"`
}

Scene is the accumulating, protocol-agnostic model of an RF environment over a capture or a live window. Bursts are the raw atom; Channels, Emitters, Conversations, Hierarchy and Anomalies are derived views populated by the registered Analyzers. AnalyzerOutputs carries each analyzer's own structured detail keyed by analyzer name (mirroring siglab.Result.Detail), so an analyzer can emit more than the shared model captures.

func (*Scene) Sanitize

func (s *Scene) Sanitize()

Sanitize clamps every non-finite float in the scene to a safe value so a marshal (and the SSE/WS wire that carries it) never sees NaN/±Inf. Call before any JSON encode or stream send.

type SegmentConfig

type SegmentConfig struct {
	// FFTSize is the wideband FFT used for carrier discovery (power of two).
	FFTSize int
	// ChannelTargetRateHz is the per-channel decimated baseband rate the
	// downconverter targets. ~50 kHz comfortably holds any land-mobile / IoT
	// narrowband channel while keeping per-burst buffers small.
	ChannelTargetRateHz float64
	// PeakThresholdDb / MinSpacingHz tune carrier discovery (carriers.DetectPeaks).
	PeakThresholdDb float32
	MinSpacingHz    uint32
	// MaxChannels caps how many discovered carriers are analyzed (strongest first).
	MaxChannels int
	// EnvelopeRateHz is the per-channel power-envelope frame rate the onset state
	// machine runs at. Higher resolves shorter bursts at more CPU.
	EnvelopeRateHz float64
	// OnThreshDb/OffThreshDb/Debounce/NoiseAlpha are the onset state-machine knobs,
	// lifted from the DMR onset detector.
	OnThreshDb  float64
	OffThreshDb float64
	Debounce    int
	NoiseAlpha  float64
	// MinBurstSec drops bursts shorter than this (debounce noise).
	MinBurstSec float64
	// MaxSamples caps how many wideband samples are read (0 = whole capture). The
	// CLI sets this from a -window/-duration flag.
	MaxSamples int
	// Now stamps the scene; nil ⇒ time.Now.
	Now func() time.Time
}

SegmentConfig tunes the segmentation pre-pass. The zero value is valid; every field falls back to a sensible default via withDefaults.

type Source

type Source interface {
	// Next returns the next chunk of up to n complex samples. The returned slice
	// may be shorter than n only at end-of-stream; an empty slice with io.EOF
	// marks the end. ctx cancellation is honored.
	Next(ctx context.Context, n int) ([]complex64, error)
	// CenterHz is the source's absolute center frequency.
	CenterHz() uint32
	// SampleRateHz is the IQ sample rate.
	SampleRateHz() float64
}

Source is a sequential producer of wideband IQ chunks at one center frequency and sample rate. Both an offline capture file and a bounded live SDR capture implement it, so the segmentation engine and every analyzer run identically on recorded and on-air IQ — "both modes equally" is satisfied at the core, not bolted on. Unlike hunt.IQSource (which retunes per sweep step), a Source streams a single span the scene analyzer characterizes as a whole.

Directories

Path Synopsis
Package webserver is the optional rfscope web console backend: a self-contained HTTP server that stages an uploaded IQ capture, runs the rfscope segmentation + analyzers over it, and returns the structured Scene (plus a cryptolab `ks` frames file for any unknown payloads).
Package webserver is the optional rfscope web console backend: a self-contained HTTP server that stages an uploaded IQ capture, runs the rfscope segmentation + analyzers over it, and returns the structured Scene (plus a cryptolab `ks` frames file for any unknown payloads).

Jump to

Keyboard shortcuts

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