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

  1. Open Radio → Channels in the web UI.
  2. Click + Add Channel.
  3. 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.
  4. Name it LoRa (or whatever describes the remote network). Give it a channel number that doesn't collide with your existing RF channels; 11 is a common choice for a first KISS-only channel.
  5. 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.
i

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

  1. Open Interfaces → KISS TNC.
  2. Click + Add Interface.
  3. Choose Interface Type: TCP Client. (The existing TCP option is a server that accepts inbound clients — that's not what you want here.)
  4. Set Remote Host and Remote Port to your LoRa digipeater's KISS server. For example: 192.168.1.238 and 8001.
  5. In the Channel picker, select the KISS-only channel you just created (— Unbound → will become ● Live once the dial succeeds).
  6. Set Mode to TNC. In TNC mode the interface is treated as a radio, not a software client.
  7. 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.
  8. (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.
  9. Click Save. Graywolf dials immediately; the row's status badge cycles ConnectingConnected 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

  1. Open APRS → Digipeater.
  2. Click + Add Rule.
  3. 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.)
  4. Set From Channel to your VHF channel (for example channel 1). Set To Channel to your KISS-only channel (11).
  5. 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=1 is a safe starting point.
  6. 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.
  7. 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:

  1. Open APRS → Beacons.
  2. Click + Add Beacon.
  3. Set Channel to your KISS-only channel (11).
  4. Author the beacon as normal (position, status, or object). Configure the comment, SSID, interval, and so on.
  5. 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:

GlyphTextMeaning
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:

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.

  1. Click Delete on the channel's card. Graywolf queries GET /api/channels/{id}/referrers for the full list of rows that reference this channel.
  2. 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.
  3. 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.
  4. 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 and needs_reconfig set to true. 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:

  1. Is Remote Host reachable from the machine running graywolf? ping / nc -zv host port from the graywolf host is the quickest test.
  2. Is the remote TNC actually listening on the configured port? Run ss -tlnp or netstat -an on the remote host and look for a LISTEN on that port. For Direwolf, check its config for NCHANNEL/KISSPORT. For a LoRa digipeater, consult its documentation — the KISS port is often configurable and off by default.
  3. 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.
  4. Open the KISS interface's status detail on the graywolf KISS page. Last error shows the net.OpError from the most recent dial attempt (connection refused, no route to host, i/o timeout) — that pinpoints the layer.
  5. The Prometheus metric graywolf_kiss_client_connected{interface_id="N"} is 0 while down and 1 while up, and graywolf_kiss_client_reconnects_total advances on every successful dial. A flat-at-zero connected gauge 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:

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:

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:

MetricLabelsDescription
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