The protocol
This is what a feeder, relay or client must speak to interoperate. The reference is the wire crate in the Tally repository, the only code there that encodes or decodes the protocol, and every binary uses it. Integers are big-endian throughout.
Transport
Every hop is Noise_IK_25519_ChaChaPoly_BLAKE2s over TCP. Each Noise message is sent behind a two-byte length, at most 65,535 bytes with its tag, and carries whole records only.
Identity is one Ed25519 key. The X25519 key Noise authenticates is derived from it (libsodium’s sk_to_curve25519), and the initiator’s first handshake message carries its 32-byte Ed25519 public key, which the responder checks against the key the handshake proved. One key names an antenna everywhere.
The preamble
Before the handshake the initiator sends 16 plaintext bytes. They are also the Noise prologue, so a changed byte fails the handshake, and they let the handshake itself change later, and a relay hold an old and a new key while it rotates.
Versions
Then the initiator sends Hello with the lowest and highest wire versions it speaks and its role. The responder answers Welcome with the version both will speak from then on, or Goodbye with a reason and how long to wait. From the first byte to Welcome takes at most 15 seconds, and every send after it has a deadline of 30. Wire version 1 is the only version so far.
A relay answers every wire version a feeder ever shipped, forever. Installed feeders do not update, and the rest of the protocol rests on this rule.
Records
A record is a kind byte, a two-byte body length, and the body. These are the kinds wire version 1 knows:
Rules for change
Feeders never update, so these are the rules that let the protocol change without them:
- An unknown kind is skipped, unless its high bit (0x80) marks it critical, which ends the session cleanly. New records are therefore not critical unless ignoring them would be wrong.
- A body may carry bytes after the fields a reader knows, and they are ignored. Fields are added by appending them.
- A reader keeps an enum value it does not know as the value Other, never an error.
- A signed form keeps the same prefix in every format version and ends with its signature over everything before it, so a relay can verify, route and deduplicate a format it does not otherwise read.
- An observation’s format is BEAST’s own type byte for 1090 MHz. 978 MHz UAT takes new values and needs no new record, and relays forward formats they do not know.
The signed batch
A batch is about a second of one receiver’s receptions. Format 1 is laid out as:
Observation formats: 1 is Mode A/C (2 bytes), 2 Mode-S short (7), 3 Mode-S long (14), D a 978 MHz UAT downlink, U a UAT ground uplink, each written as its ASCII character.
The signature covers the ASCII text tally batch, a zero byte, and every byte of the batch before the signature. A relay’s own deduplicated batches sign tally relay batch instead, so one can never pass for the other.
A batch’s identity is the BLAKE2s-256 hash of all its bytes, signature included. Relays deduplicate on it, and the next batch’s Previous field names it.
The first 49 bytes (format, key, boot id and sequence) keep their place in every future format, and the signature always comes last.
Counters are never calibrated before MLAT, by anyone: a relay’s MLAT needs the raw counter to solve.