VAFM Client Protocol
VAFM is Vexillum’s analog FM linking mode.
This page describes the current beta VAFM wire protocol and runtime behavior so client developers can build interoperable software clients, radio interface nodes, repeater adapters, gateways, and test tools.
Status
Section titled “Status”| Field | Value |
|---|---|
| Protocol status | Beta |
| Protocol version | 0x01 |
| Audio codec | Opus |
| Audio format | 48 kHz mono, 20 ms frames |
| Required transport profiles | vafm+udp, vafm+tcp |
| Optional transport profiles | vafm+tls, vafm+dtls, vafm+ws, vafm+wss |
VAFM is under active development. Client implementations should be strict about parsing, tolerant of reconnects, and prepared for protocol changes while the beta design matures.
Design overview
Section titled “Design overview”A VAFM client connects to a Vexillum VAFM instance using a supported transport profile, performs a simple session handshake, optionally authenticates with a shared passphrase, sends periodic keepalives, and transmits Opus audio frames when the user or attached radio is transmitting.
The current beta server behavior is a single shared room with one active talker at a time.
A client should handle:
- Session setup with
HELLOandWELCOME - Optional shared-passphrase authentication
- A supported transport profile
- Periodic keepalives
- Opus voice packet transmission
- Forwarded voice packet reception
- Stream end detection
- Explicit disconnects
- Reconnects after transport failure
Transport profiles
Section titled “Transport profiles”VAFM separates the packet protocol from the transport used to carry it.
A VAFM packet always uses the same header, packet type values, codec IDs, callsign encoding, stream behavior, authentication behavior, and payload rules regardless of transport. A transport profile only defines how complete VAFM packets are carried over the underlying network or local link.
This separation lets implementations support different deployment environments without creating a new VAFM dialect every time someone discovers another socket API and decides society needs to suffer.
Required transport profiles:
| Profile | Underlying transport | Framing |
|---|---|---|
vafm+udp |
UDP | One complete VAFM packet per UDP datagram |
vafm+tcp |
TCP | Each VAFM packet is wrapped in a 2-byte big-endian length prefix |
Optional transport profiles:
| Profile | Underlying transport | Framing |
|---|---|---|
vafm+tls |
TLS over TCP | Same framing as vafm+tcp |
vafm+dtls |
DTLS over UDP | Same packet mapping as vafm+udp |
vafm+ws |
WebSocket | One complete VAFM packet per binary WebSocket message |
vafm+wss |
WebSocket over TLS | Same framing as vafm+ws |
A VAFM server instance may listen on more than one transport profile. A client may use any profile supported by the server.
Implementations should treat the transport profile as a connection method only. Transport profiles must not change the VAFM packet format.
Endpoint syntax
Section titled “Endpoint syntax”VAFM endpoints should be represented using URI-style profile names:
The URI scheme identifies the transport profile. The host, port, and path identify the server endpoint for that profile.
For profiles that do not use a path, clients should ignore an empty path or /.
vafm+udp
Section titled “vafm+udp”For vafm+udp, each UDP datagram contains exactly one complete VAFM packet.
Clients must not split a VAFM packet across multiple UDP datagrams.
Clients should be prepared to:
- Lose datagrams
- Receive duplicate datagrams
- Receive packets out of order
- Re-send session setup or keepalive packets after network changes
- Reconnect when no valid server traffic is received for the configured timeout interval
The vafm+udp profile is suitable for low-latency voice where occasional packet
loss is preferable to delayed audio.
vafm+tcp
Section titled “vafm+tcp”For vafm+tcp, each VAFM packet is length-prefixed:
The length field is the size of the VAFM packet only. It does not include the two-byte TCP frame prefix.
TCP clients must handle:
- Partial frames
- Multiple frames in one read
- Clean socket close
- Unexpected socket close
- Invalid frame lengths
A TCP stream parser should buffer incoming bytes and repeatedly extract complete frames.
The vafm+tcp profile is suitable for networks where UDP is blocked or
unreliable, but clients should avoid allowing delayed voice frames to accumulate
during network stalls.
vafm+tls
Section titled “vafm+tls”The vafm+tls profile carries the vafm+tcp framing inside a TLS connection.
Packet framing is identical to vafm+tcp:
The TLS layer provides transport encryption and server authentication when certificates are validated.
Clients using shared-passphrase authentication should prefer vafm+tls,
vafm+dtls, vafm+wss, or another encrypted transport profile when connecting
over untrusted networks.
A client must complete the TLS handshake before sending HELLO.
vafm+dtls
Section titled “vafm+dtls”The vafm+dtls profile carries VAFM packets over DTLS.
Packet mapping is identical to vafm+udp:
Clients must not split a VAFM packet across multiple DTLS datagrams.
The vafm+dtls profile is suitable for encrypted low-latency voice where the
implementation wants UDP-style packet behavior without exposing VAFM payloads
directly on the network.
A client must complete the DTLS handshake before sending HELLO.
vafm+ws
Section titled “vafm+ws”The vafm+ws profile carries VAFM packets over WebSocket.
Each binary WebSocket message contains exactly one complete VAFM packet.
Text WebSocket messages must not be used for VAFM packets.
VAFM packets must not be JSON-encoded, base64-encoded, or otherwise wrapped inside an application-level envelope. The WebSocket message payload is the VAFM packet.
The vafm+ws profile is useful for browser clients, web-based test tools,
dashboards, and environments where WebSocket proxying is easier than raw TCP or
UDP.
vafm+wss
Section titled “vafm+wss”The vafm+wss profile is the secure WebSocket version of vafm+ws.
Packet mapping is identical to vafm+ws:
Clients using browser-based or webview-based implementations should prefer
vafm+wss over vafm+ws when connecting over untrusted networks.
WebSocket receive behavior
Section titled “WebSocket receive behavior”For vafm+ws and vafm+wss, each binary WebSocket message contains one
complete VAFM packet.
A WebSocket client should:
- Reject text messages
- Reject empty binary messages
- Validate each binary message as a complete VAFM packet
- Drop malformed packets
- Treat WebSocket close as a session disconnect
- Reconnect using a new
HELLOafter transport failure
A WebSocket client must not assume that a WebSocket message contains JSON or any other wrapper. The binary message payload is the VAFM packet.
Profile negotiation
Section titled “Profile negotiation”The current beta protocol does not negotiate transport profiles inside the VAFM packet stream.
A client selects a transport profile before sending HELLO, usually by
configuration, URI scheme, service discovery, or user selection.
Servers should reject unsupported transport profiles at the transport layer rather than accepting a connection and later changing VAFM packet behavior.
Security notes
Section titled “Security notes”The VAFM packet format does not provide encryption.
When shared-passphrase authentication is used, the AUTH payload contains the
passphrase bytes. Implementations should avoid sending AUTH over unencrypted
transport profiles on untrusted networks.
Secure deployments should prefer one of:
vafm+tlsvafm+dtlsvafm+wss
Plain vafm+udp, vafm+tcp, and vafm+ws may still be useful on trusted local
networks, tunnels, VPNs, lab setups, and constrained embedded deployments.
Packet format
Section titled “Packet format”All integer fields are big-endian.
Base header size: 32 bytes
Maximum payload size: 4096 bytes
| Offset | Bytes | Field | Notes |
|---|---|---|---|
0 |
4 |
Magic | ASCII VAFM |
4 |
1 |
Version | Must be 0x01 |
5 |
1 |
Type | Packet type enum |
6 |
1 |
Flags | Packet flags bitfield |
7 |
1 |
Codec | Codec enum |
8 |
4 |
StreamID | Unsigned 32-bit integer |
12 |
4 |
Sequence | Unsigned 32-bit integer |
16 |
4 |
TimestampMS | Unsigned 32-bit integer |
20 |
10 |
Callsign | Fixed-width bytes, space padded |
30 |
2 |
PayloadLen | Unsigned 16-bit integer |
32 |
N |
Payload | N = PayloadLen |
Header layout
Section titled “Header layout”Validation rules
Section titled “Validation rules”A client should validate every incoming packet before acting on it.
Required validation:
- Magic must be
VAFM. - Version must be
0x01. - Packet must be at least
32bytes. PayloadLenmust match the actual payload size exactly.PayloadLenmust be less than or equal to4096.VOICEpackets must use codecOPUS_48K_MONO_20MS.- For
vafm+tcpandvafm+tls, the frame length must be greater than0. - For
vafm+tcpandvafm+tls, the frame length must be less than or equal to32 + 4096. - For
vafm+udp,vafm+dtls,vafm+ws, andvafm+wss, each datagram or binary message must contain exactly one complete VAFM packet.
Malformed packets should be dropped and logged as protocol errors.
Packet types
Section titled “Packet types”| Value | Name | Direction | Purpose |
|---|---|---|---|
0x01 |
HELLO |
Client to server | Start or refresh a session |
0x02 |
WELCOME |
Server to client | Confirm session or authentication |
0x03 |
AUTH |
Client to server | Authenticate with shared passphrase |
0x04 |
KEEPALIVE |
Both directions | Keep the session alive |
0x05 |
VOICE |
Both directions | Carry Opus audio |
0x06 |
END |
Both directions | End the current transmission |
0x07 |
ERROR |
Server to client | Report protocol or authorization error |
0x08 |
NAT_HINT |
Client to server | Refresh liveness for NAT behavior |
0x09 |
DISCONNECT |
Either direction | Explicitly close the session |
Packet flags
Section titled “Packet flags”| Value | Name | Purpose |
|---|---|---|
0x01 |
EOT |
End of transmission |
0x02 |
RELAY |
Reserved for future use |
A stream ends when a client receives either:
- A packet with type
END - A
VOICEpacket with theEOTflag set
Codecs
Section titled “Codecs”| Value | Name | Purpose |
|---|---|---|
0x00 |
NONE |
Control packets |
0x01 |
OPUS_48K_MONO_20MS |
Voice packets |
Expected usage:
| Packet type | Expected codec |
|---|---|
HELLO |
NONE |
WELCOME |
NONE |
AUTH |
NONE |
KEEPALIVE |
NONE |
VOICE |
OPUS_48K_MONO_20MS |
END |
NONE |
ERROR |
NONE |
NAT_HINT |
NONE |
DISCONNECT |
NONE |
Callsign encoding
Section titled “Callsign encoding”The callsign field is exactly 10 bytes.
Encoding rules:
- Use uppercase ASCII where possible.
- Trim leading and trailing whitespace before encoding.
- Right-pad with spaces,
0x20, when shorter than 10 bytes. - Truncate to 10 bytes when longer than 10 bytes.
- Receivers should trim leading and trailing spaces when displaying.
Examples:
| Callsign | Encoded bytes |
|---|---|
KC1AWV |
KC1AWV···· |
N0CALL |
N0CALL···· |
LONGCALL123 |
LONGCALL12 |
Spaces are shown as · for readability.
Session handshake
Section titled “Session handshake”A client starts with HELLO.
Basic handshake
Section titled “Basic handshake”After WELCOME, the client may send keepalives and voice packets if
authentication is not required.
Optional authentication
Section titled “Optional authentication”A VAFM server instance may require a shared passphrase.
If authentication is configured, the client still sends HELLO first. After
receiving WELCOME, the client sends AUTH with the raw passphrase bytes in
the packet payload.
Because the current beta authentication payload contains the passphrase bytes,
clients should prefer an encrypted transport profile such as vafm+tls,
vafm+dtls, or vafm+wss when connecting over untrusted networks.
Authenticated handshake
Section titled “Authenticated handshake”On successful authentication, the server sends another WELCOME.
Clients must handle repeated WELCOME packets. A second WELCOME after AUTH
is expected.
Authentication failure
Section titled “Authentication failure”If authentication fails, the server sends:
If a client sends VOICE or END before completing authentication on an
authentication-required server, the server sends:
The unauthorized packet is not forwarded.
Keepalive behavior
Section titled “Keepalive behavior”Clients should send periodic KEEPALIVE packets after a session is established.
Recommended interval:
Server behavior:
- A client
KEEPALIVEupdates session liveness. - The server echoes
KEEPALIVEback to the client.
Clients should treat a received KEEPALIVE as proof that the session is still
alive.
NAT hint behavior
Section titled “NAT hint behavior”NAT_HINT is accepted by the server and updates session liveness.
Current server behavior:
- Accepts
NAT_HINT - Updates liveness
- Does not send a direct response
Clients may use NAT_HINT when they want to refresh NAT state without treating
the packet as a full keepalive exchange.
Voice behavior
Section titled “Voice behavior”Voice packets carry Opus audio.
Required voice format:
| Field | Value |
|---|---|
| Codec | Opus |
| Sample rate | 48 kHz |
| Channels | Mono |
| Frame duration | 20 ms |
| Packet codec ID | 0x01 |
Each VOICE packet payload contains one encoded Opus frame.
A transmitting client should set:
| Field | Recommended behavior |
|---|---|
Type |
VOICE |
Codec |
OPUS_48K_MONO_20MS |
StreamID |
Stable for one transmission |
Sequence |
Increment for each voice frame |
TimestampMS |
Monotonic packet timestamp in milliseconds |
Callsign |
Client callsign |
Payload |
Encoded Opus frame |
Stream behavior
Section titled “Stream behavior”The current beta server behaves as a single shared room with one active talker at a time.
Rules:
- The first accepted
VOICEpacket opens the active stream. - While a stream is active, only packets from that same sender are accepted.
VOICEandENDpackets from other senders are ignored while another sender is active.- Accepted voice packets are forwarded to all other authenticated sessions.
- The sender does not receive its own forwarded voice packets.
- A stream ends on
ENDorEOT. - The server also closes an idle stream if no frame is seen for about 3 seconds.
Ending a transmission
Section titled “Ending a transmission”A client can end a transmission in either of two ways:
- Send an
ENDpacket. - Set the
EOTflag on the finalVOICEpacket.
Recommended behavior:
- Use a stable
StreamIDfor all packets in a transmission. - Increment
Sequencefor each voice frame. - Send
ENDwhen PTT is released. - Also support receiving either
ENDorEOTfrom other clients.
Example end packet:
Session lifetime
Section titled “Session lifetime”Server session behavior:
| Condition | Behavior |
|---|---|
| No activity for about 120 seconds | Session times out |
| Stream-oriented transport disconnects | Session ends immediately |
| Datagram-oriented transport stops receiving valid traffic | Session times out after the normal inactivity interval |
Inbound DISCONNECT |
Session is removed immediately |
| Server shutdown | Server may send DISCONNECT |
Activity includes:
HELLOAUTHKEEPALIVEVOICEENDNAT_HINTDISCONNECT
Disconnect behavior
Section titled “Disconnect behavior”DISCONNECT explicitly closes a VAFM session.
Either side may send it.
After receiving DISCONNECT, the peer should stop using the session
immediately.
For datagram-oriented profiles such as vafm+udp, vafm+dtls, and other
datagram voice delivery, DISCONNECT is the explicit teardown signal because
there may be no socket close event visible to the VAFM packet layer.
The payload is optional and may contain an ASCII reason.
Common reason examples:
Current server behavior:
- Removes the session immediately on inbound
DISCONNECT - Closes the underlying stream connection for stream-oriented sessions
Error packets
Section titled “Error packets”ERROR packets contain an ASCII error reason in the payload.
Current known error payloads:
| Payload | Meaning |
|---|---|
auth_failed |
The provided shared passphrase was incorrect |
auth_required |
The client attempted voice or end behavior before authentication |
Clients should surface errors to logs or UI and decide whether to retry, prompt the user, or disconnect.
Minimal client state machine
Section titled “Minimal client state machine”A VAFM client can be modeled with these states:
Terminal conditions:
- Transport failure
- Session timeout
- Inbound
DISCONNECT - Authentication failure
- User shutdown
Recommended client behavior
Section titled “Recommended client behavior”A robust client should:
- Strictly validate packet magic, version, lengths, and codec.
- Encode callsigns as fixed-width 10-byte fields.
- Send
HELLObefore transmitting. - Handle repeated
WELCOMEpackets. - Send
AUTHwhen required. - Send
KEEPALIVEevery 20 to 30 seconds. - Use Opus 48 kHz mono 20 ms frames for voice.
- Use one
StreamIDper transmission. - Increment
Sequencefor each voice packet. - Send
ENDorEOTwhen PTT ends. - Treat
DISCONNECTas terminal for the current session. - Reconnect with backoff after transport failure.
- For stream-oriented profiles such as
vafm+tcpandvafm+tls, implement buffered frame parsing. - For datagram-oriented profiles such as
vafm+udpandvafm+dtls, tolerate packet loss and reordering. - For message-oriented profiles such as
vafm+wsandvafm+wss, treat each binary message as one complete VAFM packet. - Drop malformed packets instead of trying to repair them.
Minimal packet builder pseudocode
Section titled “Minimal packet builder pseudocode”Minimal session pseudocode
Section titled “Minimal session pseudocode”Stream receive loop pseudocode
Section titled “Stream receive loop pseudocode”Interoperability checklist
Section titled “Interoperability checklist”Before testing a client against Vexillum, confirm:
- Supports at least one required transport profile:
vafm+udporvafm+tcp. - Does not change VAFM packet format based on transport profile.
HELLO/WELCOMEworks overvafm+udp.HELLO/WELCOMEworks overvafm+tcp.- Uses 2-byte big-endian length framing only for stream-oriented profiles that require it.
- Sends exactly one complete VAFM packet per datagram or binary message on datagram/message-oriented profiles.
- Stream reader handles partial frames.
- Stream reader handles multiple frames in one read.
- Datagram/message-oriented reader rejects empty packets or malformed packet boundaries.
- Callsign encoding uses 10-byte padded fields.
AUTHsuccess path works when authentication is required.AUTHfailure path handlesERROR auth_failed.- Unauthorized voice handles
ERROR auth_required. - Prefers encrypted profiles when shared-passphrase authentication is used over untrusted networks.
KEEPALIVEis sent periodically.KEEPALIVEecho is handled.DISCONNECTtears down state cleanly.- Voice uses codec
0x01. - Voice payloads are less than or equal to
4096bytes. - One transmission uses a stable
StreamID. Sequenceincrements during transmission.ENDterminates local transmit state.EOTterminates local transmit state.- Incoming
VOICEpackets are decoded as Opus 48 kHz mono 20 ms frames. - Incoming
ENDorEOTcloses receive stream state. - Reconnect uses backoff after transport failure.
Common implementation mistakes
Section titled “Common implementation mistakes”Sending raw PCM instead of Opus
Section titled “Sending raw PCM instead of Opus”VAFM voice payloads are Opus frames, not raw PCM.
Input audio may be PCM inside the client, but the packet payload must contain encoded Opus audio.
Using the wrong Opus format
Section titled “Using the wrong Opus format”VAFM expects 48 kHz mono 20 ms Opus frames.
Do not send 8 kHz, stereo, variable-length capture chunks, or arbitrary audio buffers as voice payloads.
Forgetting stream framing
Section titled “Forgetting stream framing”For vafm+tcp and vafm+tls, every VAFM packet is wrapped in a 2-byte
big-endian length prefix.
Do not send raw VAFM packets directly over these stream-oriented profiles.
Correct stream frame layout:
For datagram-oriented profiles such as vafm+udp and vafm+dtls, do not add
the 2-byte stream prefix.
Assuming one stream read equals one packet
Section titled “Assuming one stream read equals one packet”Stream-oriented transports do not preserve application message boundaries.
A single read may contain:
- Part of one frame
- One complete frame
- Multiple complete frames
- The end of one frame and the beginning of another
Clients using vafm+tcp or vafm+tls must maintain a receive buffer and
extract complete length-prefixed frames from that buffer.
Not handling repeated WELCOME
Section titled “Not handling repeated WELCOME”When authentication is required, the server sends WELCOME after HELLO and
again after successful AUTH.
Clients should treat this as normal.
Not ending a stream
Section titled “Not ending a stream”When PTT ends, send END or set the EOT flag on the final VOICE packet.
Do not rely on idle timeout as the normal end-of-transmission signal.
Exposing shared passphrases
Section titled “Exposing shared passphrases”If using authentication, treat the shared passphrase as a secret. Do not log it, display it in crash output, or commit it into example configurations.
When shared-passphrase authentication is enabled, clients should use an
encrypted transport profile unless the connection is already protected by a
trusted tunnel, VPN, or private local network. Plain vafm+udp, vafm+tcp, and
vafm+ws do not encrypt the AUTH payload.
Example packet values
Section titled “Example packet values”A basic HELLO packet for callsign KC1AWV with no payload:
| Field | Value |
|---|---|
| Magic | VAFM |
| Version | 0x01 |
| Type | 0x01 |
| Flags | 0x00 |
| Codec | 0x00 |
| StreamID | 0 |
| Sequence | 0 |
| TimestampMS | 0 |
| Callsign | KC1AWV···· |
| PayloadLen | 0 |
| Payload | empty |
A VOICE packet:
| Field | Value |
|---|---|
| Magic | VAFM |
| Version | 0x01 |
| Type | 0x05 |
| Flags | 0x00, or 0x01 for final frame |
| Codec | 0x01 |
| StreamID | Stable ID for this transmission |
| Sequence | Incrementing frame number |
| TimestampMS | Monotonic timestamp |
| Callsign | Client callsign |
| PayloadLen | Opus frame length |
| Payload | Encoded Opus frame |
Versioning
Section titled “Versioning”The current protocol version is:
Clients must reject unsupported versions.
Future protocol versions may add fields, packet types, flags, authentication modes, room behavior, or metadata. Clients should keep the parser strict for the current version and make version handling explicit.
Relationship to Vexillum
Section titled “Relationship to Vexillum”VAFM is implemented as a Vexillum mode runtime. Vexillum provides the server-side implementation of the VAFM protocol, and client developers can build interoperable software that connects to it.
Client developers do not need to use Go or link against Vexillum. The protocol is intentionally simple enough to implement in any language with UDP or TCP networking and Opus support.
Current limitations
Section titled “Current limitations”The current beta behavior has several intentional limitations:
- One shared room
- One active talker at a time
- Shared-passphrase authentication only
- No negotiated codec options
- No room or talkgroup selection in the packet format
- No end-to-end encryption
- No retransmission or jitter buffer behavior specified by the protocol
Client implementations may add their own local audio buffering, device management, reconnect logic, logging, configuration, and user interface behavior.
Development recommendations
Section titled “Development recommendations”When building a VAFM client, start small:
- Implement packet encode/decode.
- Add UDP
HELLO/WELCOME. - Add TCP framing.
- Add keepalive.
- Add authentication.
- Add
DISCONNECT. - Add Opus encode/decode.
- Add receive audio playback.
- Add transmit audio capture.
- Add PTT and stream end behavior.
- Add reconnect and error handling.
- Test against both required transport profiles:
vafm+udpandvafm+tcp. - If implemented, test encrypted and message-oriented profiles such as
vafm+tls,vafm+dtls,vafm+ws, andvafm+wss.
Keep packet handling separate from audio device handling. It makes testing easier and keeps your protocol implementation from becoming tangled with whatever audio stack your operating system decided to invent this decade.
Beta notes
Section titled “Beta notes”This document reflects current VAFM beta behavior.
When server behavior changes, update this document and the associated protocol tests together. Client developers should track protocol version changes and test against the Vexillum server implementation they intend to support.