Marshall Frith

Projects

A TAK Server in Pure Python, No TAK.gov Account

Cursor-on-Target over TCP and UDP, SQLite for state, a Leaflet dashboard, and a setup wizard that hands a new operator a QR code.

Standing up a TAK server usually means a TAK.gov account, a manual approval queue, a Java runtime, and a Docker compose file you did not write. This one is a Python package with no dependencies outside the standard library, and it speaks to ATAK, iTAK, and WinTAK over the wire like any other server.

What Cursor-on-Target actually is

CoT is XML. That is the whole protocol, and understanding that removes most of the mystery.

An event has a type, a unique identifier, a time, a stale time, a point with latitude, longitude and error estimates, and a detail block that carries whatever the event type needs. A blue force marker, a chat message, and a delete instruction are all the same envelope with different types inside.

xml
<event version="2.0" uid="MARSHALL-01" type="a-f-G-U-C"
       time="2026-07-28T14:02:11Z" start="2026-07-28T14:02:11Z"
       stale="2026-07-28T14:07:11Z" how="m-g">
  <point lat="38.8895" lon="-77.0353" hae="17.0" ce="9.0" le="9.0"/>
  <detail><contact callsign="MARSHALL"/><__group name="Cyan" role="Team Member"/></detail>
</event>

The type string is a taxonomy, not an identifier. a-f-G-U-C reads as atom, friendly, ground, unit, combat. Once that clicks, most of the ATAK icon set stops being magic.

Two sockets, two jobs

TCP on 8087 is the streaming connection each client holds open. UDP on 6969 is the mesh channel clients use to shout at each other on a local network.

A server that only does TCP works fine right up until someone's radio drops and their handset falls back to mesh. Implementing both is not much extra code and removes an entire category of "it works on my bench" failure.

State lives in SQLite. Events are stale-dated by the protocol itself, so the persistence layer is mostly an expiry sweep and a spatial query.

The setup wizard is the product

A server nobody can stand up is not a server. The dashboard's wizard generates an ATAK data package and a QR code that carries the connection details, so onboarding a new handset is scanning a square rather than typing an IP address into a form with a numeric keyboard while wearing gloves.

python
pkg = datapackage.build(
    name="Field Ops",
    host=server_host, port=8087,
    callsign=callsign, team="Cyan",
)   # -> a mission package zip ATAK pulls from /Marti/sync/

ATAK fetches packages from the /Marti/sync/ endpoints, which the server implements directly. That is the same path the official server uses, so handsets do not need to know or care which one they are talking to.

Why not write a native plugin

Because a native ATAK plugin needs the TAK.gov SDK, which needs an account, which needs manual approval. Almost everything a plugin would do can be done over the wire instead, by a server that speaks the protocol properly and sends the right events.

That is not a workaround. It is a smaller attack surface and a thing you can hand to someone else without an account gate in the middle.

Layered security, described honestly

"Strong encryption" is not a setting, it is a set of choices with different threat models.

  • Mutual TLS proves both ends are who they claim. This is the baseline and it is what the official server does.
  • Sealed payloads keep the detail block unreadable to a server operator who is relaying it. Useful when the transport is not yours.
  • A one-time pad for pre-arranged short messages, where the threat model includes an adversary with more compute than you will ever have.

Each solves a different problem. Stacking them is not the same as making it "more secure" and pretending otherwise is how people end up trusting the wrong layer.

Run it

bash
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
bash
.venv/bin/python server.py --tcp 8087 --udp 6969 --web 8080

Open the dashboard, run the wizard, scan the QR from a handset. If a marker does not appear within a few seconds, the answer is almost always the stale time, not the network.

Comments

Plain text only. Held for review before it appears.