ax25conn

package
v0.13.13 Latest Latest
Warning

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

Go to latest
Published: May 30, 2026 License: GPL-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package ax25conn implements an AX.25 v2.0 / v2.2 LAPB data-link state machine for outbound (client) connections. One graywolf session corresponds to one (local-addr, peer-addr, channel) triple. The package owns its own goroutine per session, consumes non-UI frames from pkg/app/ingress, and emits encoded frames through pkg/txgovernor.

Outbound-only: this package responds to unsolicited inbound SABMs with DM. It is not a BBS host. See .context/2026-05-01-ax25-terminal-brainstorm.md for the design rationale.

Reference implementations: ax25-tools (axcall.c, ax25d.c) and the Linux kernel net/ax25/ stack. See CREDITS.md for the attribution policy and per-function source citations.

Index

Constants

View Source
const (
	// DefaultT1 — outstanding-I-frame ack timer. Kernel AX25_DEF_T1=10s.
	DefaultT1 = 10 * time.Second
	// DefaultT2 — response delay / piggyback timer. Kernel AX25_DEF_T2=3s.
	DefaultT2 = 3 * time.Second
	// DefaultT3 — link-inactivity probe. Kernel AX25_DEF_T3=300s (5min).
	DefaultT3 = 300 * time.Second
	// DefaultIdle — optional auto-disconnect. Kernel AX25_DEF_IDLE=0
	// (disabled). Graywolf v1 inherits the disabled default; advanced
	// panel may surface as a future feature.
	DefaultIdle time.Duration = 0
	// DefaultHeartbeat — housekeeping cadence (clears OWN_RX_BUSY,
	// emits RR(F=0,rsp) when own buffer drains). Kernel hard-codes 5s
	// at ax25_timer.c:50. Not user-tunable per kernel; matched here.
	DefaultHeartbeat = 5 * time.Second
	// DefaultN2 — retry count before giving up. Kernel AX25_DEF_N2=10.
	DefaultN2 = 10
	// DefaultPaclen — max I-field byte count. Kernel AX25_DEF_PACLEN=256.
	DefaultPaclen = 256
	// DefaultWindowMod8 — k for modulo-8. Kernel AX25_DEF_WINDOW=2.
	// Note: K3NA and v2.2 spec examples often quote 4; the kernel ships
	// 2 because it's the safest "default polite" window for shared
	// channels. We match the kernel.
	DefaultWindowMod8 = 2
	// DefaultWindowMod128 — k for modulo-128. Kernel AX25_DEF_EWINDOW=32.
	DefaultWindowMod128 = 32
	// DefaultStatsTick — cadence at which an active session emits
	// OutLinkStats so the UI telemetry panel can sparkline RTT and seq
	// numbers. Fires only while CONNECTED; never blocks.
	DefaultStatsTick = time.Second
)

Default tuning constants. Values inherited from the Linux kernel net/ax25/ defines (AX25_DEF_T1, AX25_DEF_T2, AX25_DEF_T3, AX25_DEF_N2, AX25_DEF_PACLEN, AX25_DEF_WINDOW, AX25_DEF_EWINDOW, AX25_DEF_BACKOFF, AX25_DEF_IDLE) at include/net/ax25.h:148-160 in v6.12. Operators override per-session via the saved AX25SessionProfile or the connect-time advanced panel.

View Source
const (
	RTTClampLo = 1 * time.Millisecond
	RTTClampHi = 30 * time.Second
)

RTT clamps inherited from include/net/ax25.h:20-21:

AX25_T1CLAMPLO = 1 jiffy (treat as 1ms in Go)
AX25_T1CLAMPHI = 30 * HZ (30 seconds)
View Source
const DefaultBackoff = BackoffLinear

Variables

View Source
var (
	// ErrManagerClosed is returned by Open after Close.
	ErrManagerClosed = errors.New("ax25conn: manager closed")
	// ErrChannelAPRSOnly is returned when Open targets a channel whose
	// configured Mode is APRS-only.
	ErrChannelAPRSOnly = errors.New("ax25conn: channel is APRS-only")
	// ErrMaxTotal is returned when MaxTotal sessions are already open.
	ErrMaxTotal = errors.New("ax25conn: max total sessions reached")
	// ErrMaxPerOperator is returned when an operator has hit MaxPerOperator.
	ErrMaxPerOperator = errors.New("ax25conn: per-operator session cap reached")
	// ErrSessionExists is returned when (channel, local, peer) is already
	// bound to a live session.
	ErrSessionExists = errors.New("ax25conn: session already exists for this triple")
)

Sentinel errors returned by Open. Callers can errors.Is these to classify failures without substring-matching error text.

Functions

func EncodeControl

func EncodeControl(c Control, mod128 bool) ([]byte, error)

EncodeControl renders a Control into 1 (mod-8) or 2 (mod-128) bytes. Returns an error on out-of-range NS/NR for the chosen modulus.

Types

type Backoff

type Backoff uint8

Backoff selects the T1-on-retry growth strategy. Kernel default is linear backoff (AX25_DEF_BACKOFF=1, kernel constant naming differs). Value 0 is reserved for "unset" so applyDefaults can promote it to DefaultBackoff without mistakenly overwriting an explicit BackoffNone.

const (
	BackoffNone        Backoff = 1 // T1 = 2 * RTT regardless of retries
	BackoffLinear      Backoff = 2 // T1 = (2 + 2*n2count) * RTT
	BackoffExponential Backoff = 3 // T1 = (2 << n2count) * RTT, capped at 8 * RTT
)

type Clock

type Clock interface {
	Now() time.Time
	AfterFunc(d time.Duration, f func()) Stopper
}

Clock abstracts time so tests can drive deadlines deterministically. AfterFunc schedules f to run once after d, returning a Stopper that cancels the pending callback. The fake-clock test harness iterates scheduled callbacks on advance().

type Condition

type Condition uint8

Condition is a bitfield mirroring the Linux kernel's ax25->condition byte. Zeroed wholesale at link-establish per ax25_std_subr.c:37.

const (
	// CondACKPending — we received an in-order I-frame without P=1
	// and have not yet emitted an RR/RNR; T2 will fire if no
	// piggyback opportunity arrives. Kernel AX25_COND_ACK_PENDING.
	CondACKPending Condition = 1 << iota
	// CondReject — we sent REJ for an out-of-order I-frame and are
	// waiting for the missing N(S). Don't send another REJ until
	// CondReject clears. Kernel AX25_COND_REJECT.
	CondReject
	// CondPeerRxBusy — peer sent RNR; suspend our I-frame TX.
	// Kernel AX25_COND_PEER_RX_BUSY.
	CondPeerRxBusy
	// CondOwnRxBusy — our receive buffer is full; we're sending RNR
	// in lieu of RR. Cleared by the heartbeat once buffer drains.
	// Kernel AX25_COND_OWN_RX_BUSY.
	CondOwnRxBusy
)

func (*Condition) Clear

func (c *Condition) Clear(b Condition)

func (Condition) Has

func (c Condition) Has(b Condition) bool

func (*Condition) Set

func (c *Condition) Set(b Condition)

type Control

type Control struct {
	Kind FrameKind
	NS   uint8 // I-frames only
	NR   uint8 // I, RR, RNR, REJ, SREJ
	PF   bool  // poll/final
}

Control is the parsed form of an AX.25 control field. NS/NR are in the range 0..7 for mod-8 and 0..127 for mod-128.

func ParseControl

func ParseControl(b []byte, mod128 bool) (Control, error)

ParseControl decodes 1 byte (mod-8) or 2 bytes (mod-128) into a Control. Behavior follows AX.25 v2.0 §3.4 / v2.2 §4.2.

type Event

type Event struct {
	Kind  EventKind
	Frame *Frame // EventFrameRX only
	Data  []byte // EventDataTX only
}

Event is one input dispatched to the per-session run loop.

type EventKind

type EventKind uint8

EventKind categorizes the inputs to the session goroutine.

const (
	EventFrameRX    EventKind = iota + 1
	EventDataTX               // operator typed bytes
	EventConnect              // operator pressed Connect
	EventDisconnect           // operator pressed Disconnect
	EventAbort                // operator pressed Abort
	EventT1Expiry
	EventT2Expiry
	EventT3Expiry
	EventHeartbeat // 5s housekeeping tick
	EventStatsTick // 1Hz CONNECTED telemetry emission
	EventShutdown  // manager tearing us down
)

type Frame

type Frame struct {
	Source, Dest ax25.Address
	Path         []ax25.Address
	Control      Control
	PID          byte // I and UI only
	Info         []byte
	IsCommand    bool // direwolf-style C-bit polarity (dest C set, source C clear)
	Mod128       bool
}

Frame is a connected-mode AX.25 frame. The address layout is identical to UI frames; only the control field and presence of PID/Info differ.

func Decode

func Decode(raw []byte, mod128 bool) (*Frame, error)

Decode parses connected-mode frame bytes.

func (*Frame) Encode

func (f *Frame) Encode() ([]byte, error)

Encode emits the wire bytes (no FCS — modem appends).

func (*Frame) ToAX25Frame

func (f *Frame) ToAX25Frame() (*ax25.Frame, error)

ToAX25Frame projects f into the *ax25.Frame surface that pkg/txgovernor and TxHook consumers observe. Sets ConnectedControl to the encoded bytes; UI Control byte is left zero.

type FrameKind

type FrameKind uint8

FrameKind enumerates the AX.25 frame types decodable from the control field. See AX.25 v2.0 §3.4 / v2.2 §4.2.

const (
	FrameInvalid FrameKind = iota
	FrameI
	FrameRR
	FrameRNR
	FrameREJ
	FrameSREJ
	FrameSABM
	FrameSABME
	FrameDISC
	FrameDM
	FrameUA
	FrameFRMR
	FrameUI
	FrameXID
	FrameTEST
)

func (FrameKind) String

func (k FrameKind) String() string

type LinkStats

type LinkStats struct {
	State      State
	VS, VR, VA uint8
	RC         int
	PeerBusy   bool
	OwnBusy    bool
	FramesTX   uint64
	FramesRX   uint64
	BytesTX    uint64
	BytesRX    uint64
	RTT        time.Duration
}

LinkStats is the snapshot the bridge can render in the telemetry side panel.

type Manager

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

Manager owns the per-process LAPB session table. Caps are process-local — they limit concurrent live sessions inside one graywolf instance. After a graywolf restart, prior sessions are gone and operators may reconnect freely. The same operator across multiple browser sessions or API tokens accumulates against the cap inside this process.

func NewManager

func NewManager(cfg ManagerConfig) *Manager

NewManager constructs a Manager. Caps default to 4 per operator, 64 total when zero.

func (*Manager) Close

func (m *Manager) Close()

Close cancels every running session. Safe to call multiple times.

func (*Manager) Count

func (m *Manager) Count() int

Count returns the number of live sessions (for tests and metrics).

func (*Manager) Dispatch

func (m *Manager) Dispatch(channel uint32, f *Frame)

Dispatch routes a non-UI frame to the matching session. Caller is the pkg/app rxfanout; per the brainstorm §4.1, the frame is guaranteed non-UI by the caller.

func (*Manager) LastPath

func (m *Manager) LastPath(sid uint64) []ax25.Address

LastPath returns the path the most recent inbound frame for sid traversed (digipeater chain), normalized to drop the trailing not-yet-repeated entries that should not appear on inbound frames. Empty if no inbound frame has been routed to the session yet.

func (*Manager) Open

func (m *Manager) Open(scfg SessionConfig, operator string) (uint64, *Session, error)

Open creates and starts a new session bound to (Channel, Local, Peer) from scfg. Operator is the caller's identity for cap accounting. Returns the session id and the *Session handle the bridge uses to push events.

func (*Manager) Run

func (m *Manager) Run(ctx context.Context)

Run blocks until ctx is done; provided for symmetric shape with other graywolf services. Sessions are managed via Open; Run is a no-op loop that just waits for shutdown.

type ManagerConfig

type ManagerConfig struct {
	TxSink         txgovernor.TxSink
	ChannelModes   configstore.ChannelModeLookup
	Logger         *slog.Logger
	MaxPerOperator int // default 4
	MaxTotal       int // default 64
}

ManagerConfig is the static configuration for the per-process session manager.

type OutEvent

type OutEvent struct {
	Kind    OutEventKind
	State   State     // OutStateChange
	Data    []byte    // OutDataRX
	Stats   LinkStats // OutLinkStats
	ErrCode string    // OutError
	ErrMsg  string    // OutError
}

OutEvent is the envelope the session emits via its Observer hook.

type OutEventKind

type OutEventKind uint8

OutEventKind enumerates events the session emits to its observer (the WebSocket bridge in production, a test sink in unit tests).

const (
	OutStateChange OutEventKind = iota + 1
	OutDataRX
	OutLinkStats
	OutError
)

type Session

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

Session is the per-link LAPB state machine driver.

func NewSession

func NewSession(cfg SessionConfig) (*Session, error)

NewSession constructs a Session. The returned Session is in StateDisconnected; the caller drives it via Submit + Run.

func (*Session) Run

func (s *Session) Run(ctx context.Context)

Run blocks until ctx is cancelled or EventShutdown is processed. Manager invokes Run in a goroutine. Each iteration drains pending timer bits before reading the channel so timer events never starve under RX bursts.

func (*Session) Snapshot

func (s *Session) Snapshot() LinkStats

Snapshot returns a goroutine-safe copy of the current LinkStats. Callable from any goroutine; the session goroutine mutates stats under the same mutex via mutateStats / setStateStats helpers.

func (*Session) State

func (s *Session) State() State

State returns the session's current LAPB state. Not goroutine-safe against concurrent transitions; use only for tests/stats display when the session is stable.

func (*Session) Submit

func (s *Session) Submit(ev Event)

Submit places an event into the input queue.

type SessionConfig

type SessionConfig struct {
	Local, Peer ax25.Address
	Path        []ax25.Address
	Channel     uint32
	Mod128      bool
	T1, T2, T3  time.Duration
	Heartbeat   time.Duration
	StatsTick   time.Duration
	N2          int
	Paclen      int
	Window      int
	Backoff     Backoff

	TxSink   txgovernor.TxSink
	Clock    Clock
	Logger   *slog.Logger
	Observer func(OutEvent) // non-blocking; manager fans to bridge
}

SessionConfig captures the per-session knobs. Required fields: Local, Peer, Channel, TxSink. Defaults are filled in for missing timer/window/paclen.

type State

type State uint8

State enumerates the LAPB data-link states (K3NA 1988, AX.25 v2.2 §6).

const (
	StateDisconnected State = iota
	StateAwaitingConnection
	StateConnected
	StateTimerRecovery
	StateAwaitingRelease
)

func (State) String

func (s State) String() string

type Stopper

type Stopper interface {
	Stop() bool
}

Stopper cancels a pending AfterFunc callback. The Stop() return is the same as time.Timer.Stop(): true if the callback had not yet fired, false otherwise.

Jump to

Keyboard shortcuts

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