aethersdr / aethersdr/AetherSDR

Add feature to configure TCI and rigctld bind interfaces

Open
#5,296 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug external devices maintainer-review priority: low protocol safety
Dominant language
C++
Stars
221
Forks
117
Avg merge
2d 7h
Merged PRs (30d)
299

Description

Rewritten 2026-08-31 after maintainer feedback that the original had no use case and read as if it were proposing changes to the TCI/rigctl specs. It was not, and is not. Original text preserved at the bottom.

Scope — no protocol change is proposed

This proposes no change to the TCI or rigctl protocols. TCI is Expert Electronics' spec and rigctl is Hamlib's; we implement both and do not own either. Nothing here alters a wire format, a command, or a handshake, and a conformant third-party client keeps working unchanged.

Everything in scope is our own code, in two places:

  • which address our listener binds to (TciServer.cpp:462, CatPort.cpp:70)
  • optionally, a check before our own call into setTransmit()

Use case

An operator enables TCI so a logger, SDR Console, or a phone app on the shack LAN can drive the rig. Our TCI listener binds to 0.0.0.0, so it is simultaneously reachable from anything else that can route to that host — a guest VLAN, a housemate's laptop, an IoT segment, or the internet if the router has a forward or UPnP hole open.

Anything that reaches the port can send a TX command and key the transmitter. There is no password, no allowlist, and nothing in the log afterwards identifying which client did it: every external client arrives as PttSource::Dax.

The realistic trigger is not an attacker. It is a misconfigured port forward, or a flat network with one more machine on it than the operator was thinking about. The consequence is an unattended transmitter radiating under the operator's licence.

Severity, honestly weighed: behind NAT on a home LAN this is close to theoretical. It is not remote code execution. priority: high was removed and that was the right call.

The ask — one decision, and "no change" is a real answer

The Stage 3/4 arbiter convergence is the actual fix, and any per-surface patch written now is disposable once it lands. So the only question is the interim posture:

option cost
A Bind TCI/rigctl to loopback by default; config opt-in to Any. Smallest defensible change. Breaks anyone currently driving TCI from a second machine until they flip a setting.
B Keep Any. Add a startup log line naming the port and its unauthenticated status, and document it. Zero behaviour change.
C Leave as-is, no doc change. Free.

My preference is B. A is more defensible in the abstract, but I do not think the breakage is worth it ahead of the arbiter. C is a legitimate answer for a ham-radio application where the operator is assumed to control their own network — I would just rather it were a stated position than an unexamined default.

If B or C: close this as intended behaviour and I will open a small docs PR.

Related surfaces

Whatever posture is chosen probably wants to cover these in the same pass, or the same issue reopens under another name: KissTncServer.cpp:29, DvkWavTransfer.cpp:92, ProfileTransfer.cpp:590 also listen(QHostAddress::Any, ...). KISS TNC in particular can key TX via packet semantics.

Companions: #5087 (no identity to log), #5270 (trust model / applet radio binding).


Original report — measurements against origin/main (56edc21b)

Summary

The TCI and rigctl/CAT external surfaces can key the transmitter with no authorization gate and no per-client identity. Any client that can reach the port can transmit. The automation surface — the one the ApX RFC cites as precedent — is gated; these two are not.

This is a property of main independent of #5270; filed separately at the RFC author's request so it does not ride on that thread. Companion to #5087: that issue is "no identity to log", this one is "no identity to authorize".

Measured against origin/main (56edc21b)

Both surfaces reach a keying path directly.

surface site call
TCI src/core/TciServer.cpp:2125 m_model->setTransmit(true, TransmitModel::PttSource::Dax)
rigctl src/core/RigctlProtocol.cpp:968 model->setTransmit(tx, TransmitModel::PttSource::Dax)

Neither has any gate. Grepping both files for ALLOW_TX, allowTx, isTxAllowed, txGate:

src/core/TciServer.cpp      : 0 gate references
src/core/RigctlProtocol.cpp : 0 gate references

Both listen on all interfaces, unauthenticated.

  • TciServer.cpp:462m_server->listen(QHostAddress::Any, port) on a QWebSocketServer in NonSecureMode, with no authentication step in the WebSocket upgrade.
  • CatPort.cpp:70m_tcpServer->listen(QHostAddress::Any, port); CatPort is the rigctl transport (it includes RigctlProtocol.h and owns m_rigctlClients).

The automation surface, by contrast, is gatedAutomationServer.cpp:

  • :2235 QLocalServer (named local socket, not a TCP port)
  • :2240 setSocketOptions(QLocalServer::UserAccessOption)
  • :2311 m_txAllowed = qEnvironmentVariableIsSet("AETHER_AUTOMATION_ALLOW_TX")

So the same capability is inert-until-armed on one surface and open on the other two.

There is no per-client identity to gate on even if one wanted to. ClientState (TciServer.cpp:708) holds socket, protocol and resamplers — no principal, no grants. PttSource (TransmitModel.h:167) is a provenance annotation for downstream consumers, not an arbitration identity: every external client collapses to Dax, which is why #5087 exists.

Why this matters

A station running AetherSDR with TCI enabled on a routable interface is one reachable port away from a third party keying the transmitter. That is an unattended-transmitter and licensing exposure, not only a software defect — the licensee is responsible for what the station radiates.

Worth being precise about severity: this is not a remote code execution, and on a typical home LAN behind NAT the practical exposure is limited. But "limited by the network topology the operator happens to have" is not the same as "gated", and TCI is commonly enabled for legitimate third-party clients, so the port is often deliberately open.

What I am not proposing

I am deliberately not proposing the fix here. The approved Stage 3/4 plan already commits to converging GUI/automation/TCI/CAT/rigctl/DAX onto one engine-owned arbiter, and #5270's trust-model section now depends on that convergence. A per-surface patch bolted on now would likely be thrown away by it, and could give a false sense that the general problem is solved.

What seems worth deciding is the interim position, since the arbiter is not imminent:

  1. Do TCI and rigctl get an AETHER_AUTOMATION_ALLOW_TX-style inert-until-armed gate now, accepting that it is a stopgap with the same known weaknesses (process-global, no per-client granularity)?
  2. Or does the listen address default to loopback, so reaching the surface at all requires a deliberate act?
  3. Or is the current behaviour accepted as intended for these surfaces, and documented as such, so operators know to firewall the ports?

Option 3 is a legitimate answer for a ham-radio application where the operator is assumed to control their own network. I would just rather it were a stated position than an unexamined default — the same request I made of the applet radio binding in #5270.

Environment

  • origin/main at 56edc21b
  • All line references verified against that SHA on 2026-08-27; no local build or runtime testing was involved, and no transmitter was keyed in the course of filing this.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the listener setup at TciServer.cpp:462 and CatPort.cpp:70, then review the related listen calls in KissTncServer.cpp:29, DvkWavTransfer.cpp:92, and ProfileTransfer.cpp:590. The issue requires a maintainer decision among the interim postures before implementation; done means the chosen behavior is implemented or documented consistently, with no protocol changes.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
networking, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.