Remote KISS TNC
Treat a remote KISS server (LoRa digi, AGWPE, another graywolf, or any TNC speaking KISS-over-TCP) as a full graywolf channel — beacon into it, digipeat across it, iGate from it.
Overview
A remote KISS TNC is any device on your network that
exposes a KISS-over-TCP server: the most common example is a LoRa
APRS digipeater running the KISS server alongside its packet
radio, but anything that speaks KISS-over-TCP qualifies (Direwolf
in NCHANNEL mode, a second graywolf instance running
a KISS server, an AGWPE-speaking TNC with a KISS translator, or a
bespoke microcontroller project). Graywolf can dial that
server as a client and stay connected across reconnects, treating
the link as a first-class radio channel.
Why do it? The canonical reason is bridging: you run a VHF APRS station on 144.39 MHz and want traffic heard on the VHF radio to cross-gate onto a local LoRa APRS network (and vice versa), so APRS coverage extends across bands and physical networks without a second radio. Other uses: split a station's RF front end across two hosts, share one iGate uplink across multiple packet radios, or let a mobile digipeater backhaul to a fixed station over IP when in range of Wi-Fi.
When graywolf connects to a remote KISS TNC, the link behaves exactly like an audio-backed channel: received frames flow into the packet log, digipeater rules, iGate, and the dashboard; beacons scheduled on that channel transmit out the TNC link; and digipeater rules can bridge between the remote link and your RF channels. The only thing absent is the software modem — there is no carrier to sense, no PTT to key, and no audio device to attach.
Worked Example: Bridging VHF APRS to a LoRa APRS Digipeater
This example mirrors the classic Direwolf NCHANNEL
setup: graywolf runs a VHF APRS station on channel 1 (audio
modem) and extends coverage onto a local LoRa APRS network by
dialing the LoRa digipeater's KISS server on channel 11. A
cross-channel digipeater rule cross-gates traffic between the
two.
Step 1 — Create a KISS-only channel
- Open Radio → Channels in the web UI.
- Click + Add Channel.
- In the channel-type segmented control at the top of the form, choose KISS-TNC only. The audio device, modem type, and TX timing fields disappear — a KISS-only channel is a logical routing lane, not a modulated radio channel.
- Name it
LoRa(or whatever describes the remote network). Give it a channel number that doesn't collide with your existing RF channels;11is a common choice for a first KISS-only channel. - Click Save. The channel appears on the list with a KISS-TNC only badge. Its backing will show — Unbound until you attach a KISS interface in the next step.
No RF modulation happens on a KISS-only channel. Do not set its modem type, bit rate, or TX timing — those fields are hidden for a reason. The channel is a pointer the digipeater, beacon, and iGate subsystems use to route frames; the actual transmission happens out the KISS interface attached to it.
Step 2 — Create a tcp-client KISS interface
- Open Interfaces → KISS TNC.
- Click + Add Interface.
- Choose Interface Type: TCP Client. (The existing TCP option is a server that accepts inbound clients — that's not what you want here.)
- Set Remote Host and Remote
Port to your LoRa digipeater's KISS server. For
example:
192.168.1.238and8001. - In the Channel picker, select the KISS-only channel you just created (— Unbound → will become ● Live once the dial succeeds).
- Set Mode to TNC. In TNC mode the interface is treated as a radio, not a software client.
- Check the box labeled Transmit from
digipeater / beacon / iGate to this interface. This
is the opt-in flag (
allow_tx_from_governor) that lets graywolf's TX pipeline route frames to the remote TNC. Without this checkbox, the interface only receives. - (Optional) Expand Advanced to tune the reconnect initial / maximum backoff in milliseconds. Defaults are 1 second initial, 5 minutes maximum; the 0.25 jitter fraction is fixed.
- Click Save. Graywolf dials immediately; the row's status badge cycles Connecting → Connected if the peer is reachable. The KISS-only channel's backing on the Channels page flips to ● Live.
Transmit opt-in is off by default on existing TNC-mode server interfaces (that is, rows that existed before the remote-TNC feature landed). Upgrading does not silently enable TX on any interface. Newly-created tcp-client rows default the checkbox on, because the typical intent of dialing a remote TNC is to make it a full participant.
Step 3 — Add a cross-channel digipeater rule
- Open APRS → Digipeater.
- Click + Add Rule.
- Under the rule-type radio group at the top of the form, choose Bridge to another channel. This reveals the To Channel picker. (The default, Repeat on same channel, is the common single-radio digipeater case and hides the second picker.)
- Set From Channel to your VHF channel (for
example channel
1). Set To Channel to your KISS-only channel (11). - Fill in the alias, alias type, max hops, and priority as you
would for any digi rule. For a first bridge experiment,
alias=WIDE/alias_type=widen/max_hops=1is a safe starting point. - If the two channels' backings differ in kind (audio modem → KISS-TNC, or vice versa), graywolf shows an inline backing-diff notice under the pickers. That notice is informational — it is not blocking the save. It exists so you explicitly notice that frames are about to cross physical networks.
- Click Save. Packets meeting the alias on channel 1 now re-emit onto channel 11 (the LoRa TNC link); packets received via the LoRa TNC on channel 11 cross-gate to channel 1 if you add the mirror-direction rule too.
Step 4 — (Optional) Beacon an object onto the LoRa channel
To inject APRS objects (repeaters, events, the classic dog-shaped “woofwoof” object) onto the LoRa network without any RF transmission, create a beacon that targets the KISS-only channel directly:
- Open APRS → Beacons.
- Click + Add Beacon.
- Set Channel to your KISS-only channel
(
11). - Author the beacon as normal (position, status, or object). Configure the comment, SSID, interval, and so on.
- Save. On the beacon's next interval tick, the frame flows through graywolf's TX governor, hits the per-channel dispatcher, and is delivered out the tcp-client to the LoRa digi.
Understanding Channel Backing
Every channel picker across the UI (Beacons, Digipeater, iGate, KISS) renders each channel with a backing glyph and a short label so you can tell at a glance where a frame will be routed:
| Glyph | Text | Meaning |
|---|---|---|
● |
Live | Channel has a backend (audio modem or KISS-TNC) and it is currently up. Frames will transmit. |
○ |
Backend down | Channel has a backend configured but the backend is
not currently running — for example, a tcp-client
is in reconnect backoff, or the modem child has
crashed. Frames submitted now will drop with a
backend_down metric increment. |
— |
Unbound | Channel has no backend configured at all. The
channel exists only in the database; a submit is
accepted by the governor but dropped by the dispatcher
with a no_backend metric increment. A
save warning appears on beacon / digi / iGate forms
that pick an unbound channel. |
Backing kinds are modem (audio input device is attached), kiss-tnc (at least one KISS interface in TNC mode with the TX-opt-in flag is attached), and unbound (neither). A channel may have exactly one kind at a time; attaching an audio device to a channel already serviced by a TNC interface (or vice versa) is rejected at the API with a 400 and a message describing the conflict. A kiss-tnc channel may have multiple TNC interfaces attached — each one receives every TX frame, which is how you would peer two remote TNCs on the same logical channel.
Reconnect Behavior
The tcp-client's supervisor dials the remote host once. When
that connection drops (peer closes the socket, network partition,
EOF on read), the supervisor waits reconnect_init_ms
±25% jitter and dials again. Each subsequent failure
doubles the wait, capped at reconnect_max_ms. On a
successful dial the backoff resets.
On the KISS TNC page, the status column for each interface is a focusable button. Click it (or Tab to it and press Enter) and an inline detail row expands showing:
- Peer address — resolved
host:portof the current connection attempt. - Connected since — timestamp of the most recent successful dial.
- Last error — the connection error from the most recent failed attempt, or empty if currently connected.
- Reconnect count — cumulative dial
successes since the process started. The very first connect
shows
1. - Current backoff — seconds remaining
before the next dial attempt. Coarse-bucketed to
~1m/~3m/ etc. above 30 s; second-precise below 30 s. - Retry now — a button that cancels the current backoff wait and re-dials immediately. Use this when you know the peer is back up (you just restarted the LoRa digipeater, for example) and you don't want to wait out the remaining backoff.
The Retry-now button POSTs to /api/kiss/{id}/reconnect.
That endpoint returns 200 on success, 404
if the interface was deleted, 409 if the row exists
but isn't a tcp-client (nothing to retry), or 503
if the KISS manager is not yet running.
Cross-Channel Digipeater Safety
Cross-channel digipeating can create loops. The classic hazard: you bridge VHF ↔ LoRa in both directions, and both networks are within earshot of each other (two radios sharing the same site, or two digipeaters with overlapping coverage). A frame arrives on VHF, cross-gates to LoRa, is heard by a LoRa node that re-transmits, cross-gates back to VHF, and the loop continues until the path is filled.
Graywolf's standard digipeater dedup (callsign + payload sliding
window, used-bit tracking in the AX.25 path, and priority-ordered
rules) is your loop protection. It is the same mechanism that
keeps same-channel WIDEn-N repeats from storming. The defaults
(30 s dedup window, max_hops=2) are adequate
for most home deployments. For bridged deployments, keep
your digi rules conservative: a single WIDE1-1
fill-in-class rule on each side is safer than wide-area
WIDE2-N. Review the Digipeater page
for dedup-window and alias tuning.
If you hear your own transmissions loop back after a bridge rule goes live, kill the offending rule first, then diagnose. Common causes: (1) the two channels actually share the same RF medium (they shouldn't), (2) the dedup window is too short for the bridge path's end-to-end latency, or (3) a third-party digi upstream is re-injecting.
Referential Integrity: What Happens When You Delete a Channel
Deleting a channel that is referenced by beacons, digipeater rules, KISS interfaces, or the iGate is a destructive operation, and graywolf guards it with a two-step dialog.
-
Click Delete on the channel's card. Graywolf
queries
GET /api/channels/{id}/referrersfor the full list of rows that reference this channel. - Impact dialog. If any references exist, a dialog opens listing them grouped by type: how many beacons, how many digi rules (same-channel vs. bridge), how many KISS interfaces, whether the iGate singleton's RF or TX channel points here, and so on. Each item shows its name or a short label. The primary button is Cancel; a secondary button Remove references… advances to the second dialog.
- Typed-name confirmation. The second dialog requires you to type the channel's name exactly before the red Delete button enables. The button caption adapts to Delete channel (no references) or Delete channel and N references (with cascades). Unreferenced channels still go through this typed gate — it's consistent across every delete.
-
Cascade. On confirm the delete runs in a
single database transaction:
- Beacons on this channel — deleted.
- Digipeater rules From this channel — deleted.
- Digipeater rules To this channel (bridge rules with From ≠ To) — deleted.
- iGate RF filters on this channel — deleted.
- iGate singleton's
rf_channel/tx_channel— cleared to null. The singleton row survives. - KISS interfaces with
Channel == id— channel cleared to zero andneeds_reconfigset totrue. The interface appears on the KISS page with a yellow banner until you edit it and reassign a valid channel. - TX timing rows — deleted.
- PTT config rows — handled by an existing hard foreign-key cascade, unchanged.
If another session adds a new reference between the impact
dialog and the delete click, the server returns 409
Conflict with the updated referrer list and the dialog
reopens with a review-and-try-again message. No silent cascade
of newly-appeared references.
Startup Orphan Warnings
On startup graywolf scans every table with a soft channel foreign key for rows that reference a channel ID that no longer exists. For each affected table the log emits one warn-level line:
graywolf orphaned channel references at startup table=beacon orphan_count=2
This is non-fatal — graywolf boots normally. To fix, open the offending page (Beacons, Digipeater, etc.) and edit each affected row to point at a valid channel, or delete the row. Future releases may offer a one-click cleanup; today the warning is informational so you know the database contains pre-existing drift, most commonly left over from a pre-v0.11 install where delete-without-cascade was the default.
Troubleshooting
My tcp-client never connects
Check, in order:
- Is Remote Host reachable from the machine
running graywolf?
ping/nc -zv host portfrom the graywolf host is the quickest test. - Is the remote TNC actually listening on the configured port?
Run
ss -tlnpornetstat -anon the remote host and look for aLISTENon that port. For Direwolf, check its config forNCHANNEL/KISSPORT. For a LoRa digipeater, consult its documentation — the KISS port is often configurable and off by default. - Is a firewall in the way? Check the peer's host firewall
(
iptables,ufw,firewalld) and any network-level firewalls or NAT rules on the path. - Open the KISS interface's status detail on the graywolf
KISS page. Last error shows the
net.OpErrorfrom the most recent dial attempt (connection refused,no route to host,i/o timeout) — that pinpoints the layer. - The Prometheus metric
graywolf_kiss_client_connected{interface_id="N"}is0while down and1while up, andgraywolf_kiss_client_reconnects_totaladvances on every successful dial. A flat-at-zeroconnectedgauge plus a stuck-at-zero reconnect counter means graywolf has never successfully dialed this peer.
My beacon on the KISS-only channel doesn't transmit
Likely causes:
- Transmit checkbox is off. Open the KISS
interface, confirm Mode is
TNCand the Transmit from digipeater / beacon / iGate to this interface box is checked. Without it, the interface is registered as an RX-only backing for the channel and TX is dropped at the dispatcher. - The tcp-client is in backoff. The status
badge on the KISS page will show Reconnecting
with a countdown. The beacon submit records a
backend_downmetric increment but no frame goes out. - The channel is unbound. If you created the channel but never attached a KISS interface to it, the channel's backing is — Unbound and no frame goes anywhere. The Beacons form also surfaces a save-time warning when you pick an unbound channel.
- Check
graywolf_tx_backend_submits_totalin Prometheus — theoutcomelabel tells you whether the frame was accepted, dropped because the queue was full (backend_busy), dropped because the link was down (backend_down), or failed with an opaque transport error (err).
I see a warning about dual backend
Graywolf forbids a channel from having both a modem and a TNC interface transmit to it. If you see a validation error like “channel N has both an audio input device and a TNC-mode KISS interface with TX opt-in; the combination is not allowed”, pick one:
- Remove the audio input device from the channel (make it a KISS-only channel), or
- Uncheck Transmit from digipeater / beacon / iGate to this interface on the KISS interface. The interface still receives, but TX goes via the modem.
The reason: doubling a TX frame (once out the modem, once out the KISS TNC) is almost always a misconfiguration. If you have a real use case for redundant dual delivery, open an issue — explicit dual-backend may land in a future release with careful loop protection.
The KISS interface is connected but no frames arrive
Confirm the remote TNC is actually delivering frames on the
right KISS port index. KISS-over-TCP lets one connection
multiplex up to 16 logical ports; graywolf's tcp-client maps
KISS port 0 to the configured Channel by default.
If your remote TNC sends on a different port index, the frames
will be received but dispatched to the wrong channel (which, in
a typical setup with one KISS-only channel, is no channel at
all). Check the remote TNC's documentation for how to configure
its KISS port assignment.
Observability
The Remote KISS TNC feature adds several Prometheus metrics, all
exposed on the standard /metrics endpoint:
| Metric | Labels | Description |
|---|---|---|
graywolf_kiss_client_connected |
interface_id, name |
Gauge. 1 when the tcp-client is connected to its peer, 0 otherwise. |
graywolf_kiss_client_reconnects_total |
interface_id |
Counter. Cumulative successful dials. A stuck
counter with connected=0 means the peer
is unreachable. |
graywolf_kiss_client_backoff_seconds |
interface_id |
Gauge. Current backoff wait in seconds. Zero when connected. |
graywolf_kiss_client_tx_drops_total |
interface_id, reason |
Counter. Frames dropped at the per-instance tx
queue. reason=busy = queue full,
reason=down = supervisor in backoff. |
graywolf_kiss_instance_tx_queue_depth |
interface_id |
Gauge. Current depth of the per-interface tx queue. Sustained values near 16 (the queue size) indicate a slow peer. |
graywolf_tx_backend_submits_total |
channel, backend,
instance, outcome |
Counter. Every TX frame per backend instance, with
outcome ok /
backend_busy / backend_down
/ err. |
graywolf_tx_no_backend_total |
channel |
Counter. Frames submitted to a channel with no registered backend. Should stay at zero in a healthy config — a non-zero value is worth alerting on. |
graywolf_tx_backend_duration_seconds |
channel, backend |
Histogram. Per-instance Backend.Submit latency. |
A ready-to-import Grafana dashboard with panels for all of the
above ships in
packaging/grafana/remote-kiss-tnc.json. Load it into
Grafana via Dashboards → New → Import and point it at
your Prometheus data source.
Related Pages
- KISS TNC — server-listen KISS interfaces, the other half of the KISS story.
- Radio Channels — channel types, backing, and TX timing.
- Digipeater — rule authoring, dedup, alias types.
- Monitoring — the Prometheus metrics endpoint and other observability surfaces.