igate

package
v0.13.3 Latest Latest
Warning

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

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

Documentation

Overview

Package igate implements graywolf's APRS-IS iGate: bidirectional gatewaying between the RF side (decoded APRS packets coming out of pkg/aprs as PacketOutput submissions) and the APRS-IS internet backbone. It owns a single long-lived TCP session to an APRS-IS server, handles login/keepalive/reconnect, and gates traffic in both directions.

RF→IS suppresses only third-party packets, NOGATE/RFONLY paths, and locally-originated messages echoed back to us by a digipeater — aprsc's IGATE-HINTS explicitly says RX iGates must NOT dedup client-side.

IS→RF is a two-tier policy: directed messages follow the APRS iGate spec (addressed to a station heard directly on RF within 30 min, not a bulletin/NWS broadcast), while non-message traffic (positions, weather, telemetry, …) is gated only when the source shares the iGate's base callsign but is not the iGate itself — so an operator can echo their internet-fed weather station onto local RF without re-broadcasting strangers' traffic. Every IS→RF frame is wrapped in APRS third-party format and passes the operator's filter engine before reaching txgovernor.

The package exposes two adapters: IgateOutput implements aprs.PacketOutput for the RF→IS direction and IgateInput implements aprs.PacketInput for IS→RF. A simulation mode (runtime-toggleable) logs what would be sent to APRS-IS without actually writing to the socket, useful for shakedown tests on a production radio.

IGATE-HINTS compliance audit (https://github.com/hessu/aprsc/blob/main/doc/IGATE-HINTS.md)

  1. Packets modified by iGates — client.go reads with bufio.ReadString('\n') and strips only "\r\n" via strings.TrimRight; whitespace, non-ASCII bytes, and NULs inside the info field are preserved byte-for-byte.
  2. C-string truncation — Go strings are byte-counted (not NUL-terminated); parseTNC2/encodeTNC2 pass the info field as []byte through ax25.Frame.Info, so embedded 0x00 / 0x1C survive intact.
  3. Character encoding — no UTF-8 decoding is applied to APRS-IS lines or info fields; TCP is binary by default in Go's net package.
  4. TX-capable iGate packet selection — shouldForwardISToRF enforces the APRS iGate spec for messages (directed, heard-direct addressee, not a broadcast) plus loop prevention on every IS→RF packet. Non-messages additionally require the source to share the iGate's base callsign but not be the iGate itself — an operator can echo their own SSIDs (e.g. an internet-fed weather station) but not strangers' non-message traffic. Strangers' messages addressed to heard-direct stations still forward normally — that is the iGate's core job. The user filter then applies as a narrower layer.
  5. Third-party wrap — every IS→RF frame passes through wrapThirdParty, producing APRS101 §20 format "}origSrc>origDest[,origPath…],TCPIP,IGATECALL*:info".
  6. Duplicate filtering — RF→IS does NOT dedup (by explicit design; APRS-IS servers dedup content-aware). NOGATE / RFONLY / TCPIP path markers are honored via pathBlocksGating.
  7. DNS caching — client.go builds a fresh net.Dialer on every reconnect; Go's resolver does not cache across calls, so each TCP connection re-resolves the hostname (required for rotate.aprs2.net load balancing).
  8. Multiple connections — supervise() drives a single *client with serialized reconnects; there is no parallel connection to APRS-IS.

Index

Constants

This section is empty.

Variables

View Source
var ErrNotEnabled = errors.New("igate not enabled")

ErrNotEnabled is the sentinel returned by adapters that wrap a runtime-toggleable iGate (the IGateLineSender adapter passed to messages.Service, the simulation toggle closure registered with webapi.RegisterIgate, etc.) when the operator has the iGate disabled. Webapi handlers map this to 503 "igate not available" instead of a generic 500 so a deliberately-off iGate does not surface as an internal error in operator dashboards.

Functions

func ComposeServerFilter

func ComposeServerFilter(base string, tacticals []string) string

ComposeServerFilter appends g/ clauses for tactical callsigns to the operator's base filter, deduping case-insensitively against any g/ already in base. Returns "" on empty input — the caller (client.go buildLogin) substitutes the no-match sentinel. Negation tokens like "-g/X" are opaque: preserved but not mined for dedup. Tacticals are assumed pre-validated by the configstore model.

g/ is the empirically-verified keyword for addressee matching on T2.

Types

type Config

type Config struct {
	// Server is the APRS-IS host:port (required). Typical values are
	// "noam.aprs2.net:14580" or "rotate.aprs2.net:14580".
	Server string
	// StationCallsign is the resolved station identifier (required). The
	// iGate has no per-station override (per design D3: iGate login
	// identity and messaging identity are always the station callsign)
	// so callers resolve via ResolveStationCallsign and pass the result
	// in. The APRS-IS passcode is derived from this at login time via
	// callsign.APRSPasscode — not carried on Config.
	StationCallsign string
	// ServerFilter is the APRS-IS filter string passed at login time
	// (e.g. "m/100" for a 100km radius around the station).
	ServerFilter string
	// SoftwareName and SoftwareVersion appear in the login banner.
	SoftwareName    string
	SoftwareVersion string
	// Rules seeds the IS->RF filter engine.
	Rules []filters.Rule
	// TxChannel is the radio channel IS->RF frames are submitted on.
	TxChannel uint32
	// ChannelModes resolves Channel.Mode at TX time. When the iGate's
	// configured TxChannel is "packet"-mode, the IS->RF runtime gate
	// drops the frame and logs a Warn (see handleISLine). Nil = treat
	// every channel as ChannelModeAPRS (preserves the legacy
	// any-channel-does-anything behavior). Lookup errors are treated
	// as APRS-mode at the gate point (fail-open).
	ChannelModes configstore.ChannelModeLookup
	// Governor is the TX governor for IS->RF submissions. Required for
	// downlink; leave nil for IS->RF=disabled. Declared as the
	// canonical txgovernor.TxSink interface so tests can inject a
	// stub; *txgovernor.Governor satisfies it.
	Governor txgovernor.TxSink
	// SimulationMode starts with log-only APRS-IS sends when true.
	SimulationMode bool
	// Logger is optional; defaults to slog.Default().
	Logger *slog.Logger
	// Registry lets the iGate export its own Prometheus metrics into
	// graywolf's registry without needing pkg/metrics changes.
	Registry prometheus.Registerer
	// RfToIsHook is called after a packet has been successfully gated
	// from RF up to APRS-IS (or would have been, in simulation mode).
	// Optional. Used by the orchestrator to record a distinct
	// packetlog entry for the upload so it can be distinguished from
	// the raw RX entry.
	RfToIsHook func(pkt *aprs.DecodedAPRSPacket, line string)
	// IsRxHook is called for every packet successfully received from
	// APRS-IS, regardless of whether the local IS->RF filter engine
	// would allow it to be transmitted. Used to record IS-heard stations
	// in the packet log / station cache for map display, which must not
	// be coupled to the transmit-gating filter. Optional.
	IsRxHook func(pkt *aprs.DecodedAPRSPacket, line string)
	// LocalOrigin is an optional lookup for locally-originated messages.
	// When non-nil and SuppressLocalMessageReGate is true, the gateway
	// skips RF->IS gating for any message packet whose (source, msg_id)
	// is present in the ring — this prevents our own outbound messages
	// from being re-gated to APRS-IS after a digipeater repeats them
	// back onto RF.
	LocalOrigin LocalOriginRing
	// SuppressLocalMessageReGate enables the LocalOrigin consult step.
	// Defaults to true in Phase 5 wiring; operators can set false to
	// preserve legacy behavior (re-gate every packet).
	SuppressLocalMessageReGate bool
	// contains filtered or unexported fields
}

Config is the iGate's runtime configuration. Fields marked "required" must be set before Start. The orchestrator sources most of these from configstore (igate_config row plus the StationConfig singleton for StationCallsign).

type Igate

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

Igate is the top-level coordinator: one session to APRS-IS, one filter engine, one RF->IS dedup cache, and runtime-toggleable simulation mode.

func New

func New(cfg Config) (*Igate, error)

New constructs an Igate. Call Start to open the APRS-IS session.

func (*Igate) Reconfigure

func (ig *Igate) Reconfigure(serverFilter string, rules []filters.Rule, gov txgovernor.TxSink)

Reconfigure updates the server filter, IS→RF gating rules, and governor at runtime. If the server filter changed, the APRS-IS connection is closed so the supervisor reconnects with the new filter (which is sent at login time). Pass a non-nil governor to enable IS→RF gating, or nil to disable it.

func (*Igate) SendLine

func (ig *Igate) SendLine(line string) error

SendLine writes a pre-formatted TNC-2 line to APRS-IS. Used by the beacon scheduler to duplicate a beacon to APRS-IS when the operator has opted in. Returns an error if the igate is not connected.

func (*Igate) SetSimulationMode

func (ig *Igate) SetSimulationMode(on bool) error

SetSimulationMode toggles simulation-mode at runtime.

func (*Igate) SetTxChannel

func (ig *Igate) SetTxChannel(ch uint32)

SetTxChannel updates the IS→RF channel at runtime. A zero value is ignored. Concurrent with the IS→RF submit path; safe via atomic.

func (*Igate) Start

func (ig *Igate) Start(ctx context.Context) error

Start opens the APRS-IS session and launches the supervising goroutine. Safe to call once; subsequent calls return an error.

func (*Igate) Status

func (ig *Igate) Status() Status

Status returns a runtime snapshot of the iGate for REST consumers.

func (*Igate) Stop

func (ig *Igate) Stop()

Stop cancels the session and waits for the supervisor to exit.

func (*Igate) TxChannel

func (ig *Igate) TxChannel() uint32

TxChannel returns the live IS→RF channel ID. Reads are lock-free.

type IgateInput

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

IgateInput exposes IS->RF frames as an aprs.PacketInput. Consumers (e.g. an audit logger or secondary TX path) can drain it with RecvPacket; frames are also submitted directly through the TX governor by the iGate itself, so IgateInput is optional.

func NewIgateInput

func NewIgateInput(ig *Igate) *IgateInput

NewIgateInput returns a PacketInput bound to ig.

func (*IgateInput) Close

func (i *IgateInput) Close() error

Close drops the reference; the channel itself is owned by Igate.

func (*IgateInput) RecvPacket

func (i *IgateInput) RecvPacket(ctx context.Context) (*aprs.InboundPacket, error)

RecvPacket blocks until an IS->RF frame is available or ctx is done.

type IgateOutput

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

IgateOutput adapts the iGate's RF->IS gating to the aprs.PacketOutput interface so it can be wired into the decoder's fanout alongside LogOutput and the packet log sink. The inner *Igate is held in an atomic pointer so it can be swapped at runtime when the operator toggles the iGate enable flag.

func NewIgateOutput

func NewIgateOutput(ig *Igate) *IgateOutput

NewIgateOutput returns a PacketOutput bound to ig. ig may be nil; the inner pointer can be replaced later via SetIgate.

func (*IgateOutput) Close

func (o *IgateOutput) Close() error

Close is a no-op; the iGate itself owns its lifecycle.

func (*IgateOutput) SendPacket

func (o *IgateOutput) SendPacket(_ context.Context, pkt *aprs.DecodedAPRSPacket) error

SendPacket feeds a decoded RF packet into the iGate for possible forwarding to APRS-IS. Always returns nil — gating errors are logged internally and counted in metrics; they are not caller-visible.

func (*IgateOutput) SetIgate added in v0.13.3

func (o *IgateOutput) SetIgate(ig *Igate)

SetIgate swaps the inner *Igate. Pass nil to disable forwarding (used when the operator turns the iGate off at runtime).

type LocalOriginRing

type LocalOriginRing interface {
	Contains(source, msgID string) bool
}

LocalOriginRing abstracts the (source, msg_id) lookup the iGate needs from pkg/messages.LocalTxRing. Pkg/igate MUST NOT import pkg/messages directly — this narrow interface is the contract.

*messages.LocalTxRing satisfies this trivially via its Contains(source, msgID) method.

type Status

type Status struct {
	Connected      bool      `json:"connected"`
	Server         string    `json:"server"`
	Callsign       string    `json:"callsign"`
	SimulationMode bool      `json:"simulation_mode"`
	LastConnected  time.Time `json:"last_connected,omitempty"`
	Gated          uint64    `json:"rf_to_is_gated"`
	Downlinked     uint64    `json:"is_to_rf_gated"`
	Filtered       uint64    `json:"packets_filtered"`
	DroppedOffline uint64    `json:"rf_to_is_dropped"`
}

Status is the current state exposed via the REST endpoint.

Directories

Path Synopsis
Package filters implements the IS->RF gating rule engine.
Package filters implements the IS->RF gating rule engine.

Jump to

Keyboard shortcuts

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