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)
- 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.
- 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.
- 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.
- 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.
- Third-party wrap — every IS→RF frame passes through wrapThirdParty, producing APRS101 §20 format "}origSrc>origDest[,origPath…],TCPIP,IGATECALL*:info".
- 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.
- 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).
- Multiple connections — supervise() drives a single *client with serialized reconnects; there is no parallel connection to APRS-IS.
Index ¶
- func ComposeServerFilter(base string, tacticals []string) string
- type Config
- type Igate
- func (ig *Igate) Reconfigure(serverFilter string, rules []filters.Rule, gov txgovernor.TxSink)
- func (ig *Igate) SendLine(line string) error
- func (ig *Igate) SetSimulationMode(on bool) error
- func (ig *Igate) SetTxChannel(ch uint32)
- func (ig *Igate) Start(ctx context.Context) error
- func (ig *Igate) Status() Status
- func (ig *Igate) Stop()
- func (ig *Igate) TxChannel() uint32
- type IgateInput
- type IgateOutput
- type LocalOriginRing
- type Status
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ComposeServerFilter ¶
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 (*Igate) Reconfigure ¶
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 ¶
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 ¶
SetSimulationMode toggles simulation-mode at runtime.
func (*Igate) SetTxChannel ¶
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 ¶
Start opens the APRS-IS session and launches the supervising goroutine. Safe to call once; subsequent calls return an error.
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 ¶
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.