Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
31ffe3c
fix(mumble): security fix, ATAK VX voice plugin auth, and direction e…
TX-RX May 1, 2026
cbc19e2
feat(mumble): auto-sync Mumble channels to OTS groups
TX-RX May 1, 2026
23eb6f5
feat(mumble): grant MakeTempChannel for VX direct/conference calls
TX-RX May 10, 2026
84f30ca
feat(mumble): scheduled cleanup of idle event channels
TX-RX May 10, 2026
5f0cc8d
docs(mumble): add operator guide for VX voice plugin support
TX-RX May 10, 2026
3d8104a
chore: gitignore .claude/ for per-project Claude Code settings
TX-RX May 10, 2026
979ed60
fix(mumble): preserve Murmur creator-admin on temps + elevate OTS adm…
TX-RX May 11, 2026
3de9d52
feat(mumble): log post-VX-setACL state on temp channels
TX-RX May 11, 2026
1df8f60
perf(mumble): in-memory suppress tracking — no Ice traffic for non-OU…
TX-RX May 11, 2026
747d108
perf(mumble): drop per-temp ACL hooks; push @auth grant to parent via…
TX-RX May 11, 2026
896c864
docs(mumble): reverse-engineered VX direct-call protocol
TX-RX May 11, 2026
7d7da14
docs(mumble): refresh operator guide for inheritance-based ACL strategy
TX-RX May 11, 2026
fcc97be
fix(mumble): keep the Ice watchdog running so OTS recovers from Murmu…
TX-RX May 11, 2026
edef58a
perf(mumble): tighten Ice client timeout, thread pool, and retry
TX-RX May 11, 2026
ee31a94
fix(mumble): post-review resilience + ACL concurrency hardening
TX-RX May 11, 2026
84e3195
fix(mumble): use locked helpers + refresh stale config descriptions
TX-RX May 11, 2026
70e78cd
fix(mumble): flip connected=False when _sync_all_servers hits Connect…
TX-RX May 11, 2026
4a10e55
docs(mumble): scrub private interop references; clarify VX version
TX-RX May 11, 2026
d495d96
fix(mumble): disable direction-based mute — IN/OUT both grant speak
TX-RX Jul 17, 2026
fa1746b
align mumble_authenticator.py with deployed server (TRIM callsign mat…
TX-RX Jul 17, 2026
9209ceb
harden Vx callsign normalization + add authenticator unit tests
TX-RX Jul 17, 2026
8239db3
ci(mumble): satisfy lint + SAST gates on the voice-integration branch
TX-RX Jul 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,5 @@ instance/*
.webassets-cache
.env

# Claude Code per-project settings (local overrides, transcripts, etc.)
.claude/
100 changes: 100 additions & 0 deletions docs/mumble-callsign-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Callsign whitespace handling in Mumble auth

This doc captures the state of callsign-whitespace hygiene across the OTS
write path and the Mumble authenticator's lookup path, and lists the work
that remains to make new rows immune to the same class of bug.

## TL;DR

- **Lookup side (landed in this branch):** `MumbleAuthenticator.resolve_identity`
compares against `func.trim(EUD.callsign)` on every callsign-equality
branch, so a stale leading or trailing space on the EUD row no longer
prevents a Mumble client from being matched back to its OTS user.
- **Write side (follow-up PR needed):** the data ingestion paths in
`EudHandler`, `cot_parser`, and `meshtastic_controller` assign
`eud.callsign = contact.attrs["callsign"]` (or the equivalent) without
stripping, so new EUD rows can still be inserted with padded callsigns.
The trim at lookup time tolerates this, but the right defensive layer
is also to strip on the way in.
- **Data hygiene:** existing deployments with rows already tainted by
padded callsigns benefit from a one-time
`UPDATE euds SET callsign = TRIM(callsign) WHERE callsign != TRIM(callsign)`
(and the equivalent on `certificates`). Not strictly required after the
lookup-side fix, but recommended for log readability and to keep
display strings clean.

## The failure mode

1. ATAK publishes a self-CoT message whose `<contact callsign="Some User "/>`
attribute carries a stray trailing (or leading) space.
2. The OTS EUD handler stores the value verbatim into `euds.callsign`.
No `.strip()` along the way.
3. The user later connects to Murmur via the XV or VX voice plugin. The
plugin trims the callsign client-side, replaces spaces with underscores,
and sends `Some_User---<suffix>` as the Mumble username.
4. Murmur's pre-Ice cert check uses `nameToId(name)` (via the Ice
authenticator) to ask OTS "does a user named `Some_User---<suffix>`
exist?" `resolve_identity` walks its lookup chain, gets to the
underscore-to-space branch (`spaced = "Some User"`), and compares
against `EUD.callsign == "Some User"`. The DB row is `"Some User "`
(with a trailing space), so the comparison fails. `resolve_identity`
returns `None`. `authenticate()` returns `(-1, None, None)`.
5. Murmur reports the rejection as
`<N:Some_User---<suffix>(-1)> Rejected connection from <ip>: Wrong
certificate or password for existing user`. The error message looks
like a TLS / cert-hash mismatch but is actually a name-lookup miss.

The lookup-side fix in this branch closes step 4 by trimming the stored
column before comparison.

## Why the lookup-side fix is the right place

The Mumble plugins (XV, VX) already trim the callsign on the client side
before constructing the Mumble username, so the search keys reaching
`resolve_identity` (the original `username`, the suffix-stripped
`base_callsign`, and the underscore-spaced `spaced`) are all clean. The
asymmetry is entirely on the stored-column side, which means a single
`func.trim(EUD.callsign) == X` swap on each comparison rescues every
affected lookup.

Trim-on-read does not modify the row, so display strings rendered from
`eud.callsign` elsewhere in OTS (mission rosters, group membership
panels, Mumble channel-creation messages) still show whatever is stored.
That's why the data cleanup is still recommended: the auth works without
it, but operators see padded names in admin UIs and logs until the
underlying row is normalized.

## Write-side follow-up (separate PR)

The following sites all write a raw `contact.attrs["callsign"]` (or
similar) straight to a callsign column. A future PR should `.strip()`
each before assignment, so new rows cannot reintroduce the bug.

| File | Line(s) | Notes |
| --- | --- | --- |
| `opentakserver/eud_handler/EudHandler.py` | 385, 687 | `eud.callsign = self.callsign` on EUD create + update; `self.callsign` is captured at line 511 from `contact.attrs["callsign"]` without trim |
| `opentakserver/cot_parser/cot_parser.py` | 244, 728 | mission-UID + marker writes both source `contact.attrs["callsign"]` |
| `opentakserver/controllers/meshtastic_controller.py` | 316, 359 | writes EUD callsign from Meshtastic device long-name |
| `opentakserver/blueprints/marti_api/mission_marti_api.py` | 862, 1040, 1987 | mission invitation and uid writes |
| `opentakserver/blueprints/ots_api/marker_api.py` | 119 | marker name from request JSON; bleach-cleaned but not stripped |

A single helper (`def _clean_callsign(s): return s.strip() if s else s`)
applied at each of these write sites is the minimal patch. The same
helper can be reused in the existing CoT validation path so future
attributes don't slip through.

## One-time data cleanup (deployment-level, optional)

```sql
BEGIN;
UPDATE euds SET callsign = TRIM(callsign) WHERE callsign IS NOT NULL AND callsign != TRIM(callsign);
UPDATE certificates SET callsign = TRIM(callsign) WHERE callsign IS NOT NULL AND callsign != TRIM(callsign);
COMMIT;
```

Idempotent and reversible. Not required for auth to work (the lookup-side
trim handles padded rows transparently), but recommended for display
hygiene and to keep `EUD.callsign != TRIM(EUD.callsign)` a useful
invariant for future linting. The same cleanup can be packaged as an
Alembic migration once the write-side fix lands so a single upgrade
takes care of both code and data.
Loading
Loading