igate

package
v0.13.2 Latest Latest
Warning

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

Go to latest
Published: May 7, 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

This section is empty.

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.

func NewIgateOutput

func NewIgateOutput(ig *Igate) *IgateOutput

NewIgateOutput returns a PacketOutput bound to ig.

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.

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