document the UDP protocol, Windows status, and new options
Add UDP_ENCRYPTION.md describing the OCB2 crypt state, the 1.5 native protobuf envelope, nonce handling, and the version negotiation that selects between the 1.5 and legacy payload formats. The protocol is only described across several Mumble source files, and getting the nonce direction wrong is not visible as anything except audio that never arrives. Add WINDOWS.md recording what builds there today and what does not. Update the README for the new command line options and the tone test mode. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3dc77cedd6
commit
e64c6df2b7
@@ -40,6 +40,20 @@ noisesuppressionenabled = true
|
||||
|
||||
RNNoise is a required build and runtime dependency.
|
||||
|
||||
## Automatic Gain Control
|
||||
|
||||
Barnard normalizes the level of your outgoing microphone audio with automatic gain control (AGC), which boosts quiet speech and compresses loud peaks. AGC is enabled by default.
|
||||
|
||||
### Controls
|
||||
- **F12 key**: Toggle AGC on/off (configurable hotkey)
|
||||
- **FIFO command**: Send `agc` command to toggle during runtime
|
||||
- **Configuration**: Set `agcenabled` in `~/.barnard.toml`
|
||||
|
||||
### Configuration Example
|
||||
```toml
|
||||
agcenabled = true
|
||||
```
|
||||
|
||||
## FIFO Control
|
||||
|
||||
If you pass the --fifo option to Barnard, a FIFO pipe will be created.
|
||||
@@ -54,6 +68,7 @@ Current Commands:
|
||||
* toggle: Toggle your transmission state.
|
||||
* talk: Synonym for toggle.
|
||||
* noise: Toggle noise suppression on/off for microphone input.
|
||||
* agc: Toggle automatic gain control on/off for microphone input.
|
||||
* record: Toggle recording. You may also use `record start` or `record stop`.
|
||||
* exit: Exit Barnard, just like when you press your quit key.
|
||||
|
||||
@@ -140,6 +155,35 @@ If you modify the config file while Barnard is running, your changes may be over
|
||||
You can set username and defaultserver in your config file, and they will be used if none is specified when launching barnard.
|
||||
(Note that the default username (an empty string) and the default server name (localhost:64738) have been the defaults for barnard up to this point, and have been left that way for compatibility.)
|
||||
|
||||
## Audio Packet Duration
|
||||
|
||||
Barnard sends 10 ms audio packets by default. On a slow or unstable connection,
|
||||
using larger packets can reduce packet overhead and make short dropouts less
|
||||
noticeable, at the cost of additional voice latency. Start Barnard with one of
|
||||
the supported durations:
|
||||
|
||||
```sh
|
||||
barnard --audio-interval 20
|
||||
```
|
||||
|
||||
Supported values are `10`, `20`, `40`, and `60` milliseconds. Try `20` ms
|
||||
first; use `40` ms only if the connection remains unreliable.
|
||||
|
||||
## Incoming Audio Jitter Buffer
|
||||
|
||||
Barnard holds 40 ms of audio separately for each speaker before starting
|
||||
playback. This prevents brief delayed UDP packets from draining OpenAL's audio
|
||||
queue, which otherwise produces clicks or pops. To adjust this tradeoff between
|
||||
resilience and added incoming latency:
|
||||
|
||||
```sh
|
||||
barnard --jitter-buffer 60
|
||||
```
|
||||
|
||||
Supported values are `0`, `20`, `40` (default), and `60` milliseconds. Try
|
||||
`60` ms for a lossy or jittery connection. Use `0` only when minimizing latency
|
||||
is more important than avoiding playback underruns.
|
||||
|
||||
## Audio Devices
|
||||
|
||||
You can set the default input and output devices in the config file as well.
|
||||
@@ -149,7 +193,7 @@ To clear your inputdevice or outputdevice options and set them to defaults, set
|
||||
|
||||
### Audio Backends (ALSA, PipeWire, PulseAudio)
|
||||
|
||||
Barnard uses OpenAL Soft for audio. By default it will pick the first available backend (often ALSA), but you can force a specific driver:
|
||||
Barnard uses OpenAL Soft for audio. The default backend order is determined by the installed OpenAL Soft build and its configuration; it is not guaranteed to prefer PipeWire or PulseAudio. You can force a specific driver:
|
||||
|
||||
- Command line: `./barnard --audio-driver pipewire` (or `pulse`, `alsa`, `jack`)
|
||||
- Config file: add `audiodriver = "pipewire"` to your `~/.barnard.toml`
|
||||
@@ -157,7 +201,7 @@ Barnard uses OpenAL Soft for audio. By default it will pick the first available
|
||||
|
||||
If PipeWire or PulseAudio support is missing, install OpenAL Soft with the corresponding backend enabled (e.g., `libopenal1` or `openal-soft` packages built with PipeWire). After changing drivers, rerun with `--list_devices` to confirm the desired devices appear.
|
||||
|
||||
Leaving `audiodriver` empty in the config keeps the OpenAL default ordering (PipeWire/Pulse first if available, then ALSA).
|
||||
Leaving `audiodriver` empty uses the OpenAL Soft default ordering from the installed library.
|
||||
|
||||
## Keystrokes
|
||||
|
||||
@@ -249,6 +293,7 @@ After running the command above, `barnard` will be compiled as `$(go env GOPATH)
|
||||
|
||||
- <kbd>F1</kbd>: toggle voice transmission
|
||||
- <kbd>F9</kbd>: toggle noise suppression
|
||||
- <kbd>F12</kbd>: toggle automatic gain control
|
||||
- <kbd>F11</kbd>: open actions menu for the focused tree item
|
||||
- <kbd>Ctrl+R</kbd>: toggle recording
|
||||
- <kbd>Ctrl+L</kbd>: clear chat log
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
# Mumble UDP Encryption & Protocol Versioning
|
||||
|
||||
Based on the official Mumble client source at `./mumble/` (v1.5.x / v1.6.x).
|
||||
|
||||
---
|
||||
|
||||
## 1. Protocol Versioning
|
||||
|
||||
The protocol version determines the UDP packet format.
|
||||
|
||||
| Version | UDP Format | Audio Type Byte | Ping Type Byte |
|
||||
|---------|-----------|-----------------|-----------------|
|
||||
| < 1.5.0 | Legacy (varint-based) | `(codec << 5) \| target` (e.g. `0x80` for Opus) | `(1 << 5) = 0x20` |
|
||||
| >= 1.5.0 | Protobuf (MumbleUDP) | `0x00` | `0x01` |
|
||||
|
||||
The version boundary is defined in `MumbleProtocol.h`:
|
||||
|
||||
```cpp
|
||||
constexpr Version::full_t PROTOBUF_INTRODUCTION_VERSION = Version::fromComponents(1, 5, 0);
|
||||
```
|
||||
|
||||
The client advertises its version in the initial `Version` TCP message (field 1 = `VersionV1`).
|
||||
The server sends its version in `CodecVersion` (TCP message type 21).
|
||||
|
||||
**Critical**: The UDP decoder checks the negotiated protocol version to decide which format
|
||||
to use. However, it also auto-upgrades: if a protobuf-format ping (`0x01`) arrives while in
|
||||
legacy mode, the version is bumped to >= 1.5.0.
|
||||
|
||||
```cpp
|
||||
// From MumbleProtocol.cpp UDPDecoder::decode():
|
||||
if (header == static_cast<byte>(UDPMessageType::Ping)) {
|
||||
// Upgrade to at least PROTOBUF_INTRODUCTION_VERSION
|
||||
this->setProtocolVersion(std::max(this->getProtocolVersion(), PROTOBUF_INTRODUCTION_VERSION));
|
||||
return decodePing_protobuf(...);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Encryption Layer: CryptStateOCB2
|
||||
|
||||
All UDP packets (both legacy and protobuf) share the same encryption layer.
|
||||
|
||||
### Wire Format
|
||||
|
||||
```
|
||||
[iv_byte(1)] [tag(3)] [ciphertext(variable)]
|
||||
```
|
||||
|
||||
Total overhead: **4 bytes** (ssize = plaintext_size + 4).
|
||||
|
||||
### Algorithm
|
||||
|
||||
- **AES-128-OCB** (not OCB2, despite the class name)
|
||||
- Key: 16 bytes (from `CryptSetup` TCP message)
|
||||
- Encrypt IV: 16 bytes (`client_nonce` from `CryptSetup`)
|
||||
- Decrypt IV: 16 bytes (`server_nonce` from `CryptSetup`)
|
||||
|
||||
### IV Increment (Little-Endian)
|
||||
|
||||
The IV is a 16-byte integer incremented **little-endian** (byte 0 is the LSB):
|
||||
|
||||
```cpp
|
||||
// From CryptStateOCB2::encrypt():
|
||||
for (int i = 0; i < AES_BLOCK_SIZE; i++)
|
||||
if (++encrypt_iv[i])
|
||||
break;
|
||||
```
|
||||
|
||||
Starts at byte 0, increments, breaks on non-overflow. This matches wumble's
|
||||
`increment_encrypt_iv`. **Important**: byte 0 changes every packet.
|
||||
|
||||
### Encrypt
|
||||
|
||||
```cpp
|
||||
// From CryptStateOCB2::encrypt():
|
||||
// 1. Increment IV
|
||||
for (int i = 0; i < AES_BLOCK_SIZE; i++)
|
||||
if (++encrypt_iv[i]) break;
|
||||
|
||||
// 2. OCB encrypt the plaintext
|
||||
ocb_encrypt(source, dst+4, plain_length, encrypt_iv, tag);
|
||||
|
||||
// 3. Wire format: [iv_byte][tag[0..2]][ciphertext]
|
||||
dst[0] = encrypt_iv[0];
|
||||
dst[1] = tag[0];
|
||||
dst[2] = tag[1];
|
||||
dst[3] = tag[2];
|
||||
```
|
||||
|
||||
### Decrypt (with IV Tracking)
|
||||
|
||||
```cpp
|
||||
// From CryptStateOCB2::decrypt():
|
||||
// 1. Read IV byte from wire
|
||||
ivbyte = source[0];
|
||||
|
||||
// 2. Check if in-order: decrypt_iv[0] + 1 == ivbyte
|
||||
if (((decrypt_iv[0] + 1) & 0xFF) == ivbyte) {
|
||||
if (ivbyte > decrypt_iv[0]) {
|
||||
decrypt_iv[0] = ivbyte; // Normal forward
|
||||
} else if (ivbyte < decrypt_iv[0]) {
|
||||
decrypt_iv[0] = ivbyte;
|
||||
for (int i = 1; i < AES_BLOCK_SIZE; i++)
|
||||
if (++decrypt_iv[i]) break; // Wrapped: carry to higher bytes
|
||||
}
|
||||
} else {
|
||||
// Late/reorder handling with diff-based window (±30)
|
||||
// ... (see MumbleProtocol.cpp for full logic)
|
||||
}
|
||||
|
||||
// 3. OCB decrypt
|
||||
ocb_decrypt(source+4, dst, crypted_length-4, decrypt_iv, tag);
|
||||
|
||||
// 4. Verify tag (first 3 bytes)
|
||||
if (memcmp(tag, source+1, 3) != 0) { /* auth failure */ }
|
||||
|
||||
// 5. Update replay history
|
||||
decrypt_history[decrypt_iv[0]] = decrypt_iv[1];
|
||||
```
|
||||
|
||||
### OCB Implementation Details
|
||||
|
||||
The OCB implementation (`CryptStateOCB2::ocb_encrypt` / `ocb_decrypt`):
|
||||
|
||||
- Uses OpenSSL's `EVP_aes_128_ecb` as the block cipher primitive
|
||||
- Nonce is the full 16-byte IV (no bottom-bit clearing — different from legacy OCB2)
|
||||
- No associated data
|
||||
- Final partial block: pad block has `byte[15] = remaining * 8` (bit-length encoding)
|
||||
- GF(2^128) doubling via `S2()` (multiply by 2) and `S3()` (multiply by 3)
|
||||
- Reduction constant: `0x87`
|
||||
- Includes XEX* attack mitigation: if the second-to-last plaintext block is all zeros
|
||||
except potentially the last byte, a bit is flipped to prevent the attack described in
|
||||
https://eprint.iacr.org/2019/311
|
||||
|
||||
---
|
||||
|
||||
## 3. Legacy UDP Audio Format (version < 1.5.0)
|
||||
|
||||
Used when negotiated protocol version < `PROTOBUF_INTRODUCTION_VERSION`.
|
||||
|
||||
### Encode (Server → Client)
|
||||
|
||||
```
|
||||
[byte 0: header] [session varint] [seq varint] [Opus: size varint] [opus data] [optional: 3×float32 position]
|
||||
```
|
||||
|
||||
- **Header byte**: `(codec_type << 5) | target`
|
||||
- `codec_type`: 0=CELT_Alpha, 1=Ping, 2=Speex, 3=CELT_Beta, 4=Opus
|
||||
- `target`: 5-bit target/context (0=normal, 1=shout, 2=whisper, 3=listen)
|
||||
- **Session varint**: sender's session ID (present only in server→client direction)
|
||||
- **Seq varint**: frame number (monotonic, 10ms units)
|
||||
- **Opus size varint**: bit 13 (0x2000) is the terminator flag; bits 0-12 are the opus data length
|
||||
- **Position**: 3× float32 (x, y, z), only if space remains after opus data
|
||||
|
||||
### Decode (Client)
|
||||
|
||||
From `UDPDecoder::decodeAudio_legacy()`:
|
||||
|
||||
```cpp
|
||||
m_audioData.targetOrContext = data[0] & 0x1f;
|
||||
m_audioData.usedCodec = codec; // Opus = 4
|
||||
|
||||
// Read session (server→client only)
|
||||
if (this->getRole() == Role::Client) {
|
||||
stream >> m_audioData.senderSession;
|
||||
}
|
||||
|
||||
// Read frame number
|
||||
stream >> m_audioData.frameNumber;
|
||||
|
||||
// Opus: size varint with terminator bit
|
||||
stream >> helper;
|
||||
payloadSize = helper & 0x1FFF; // 13 bits for size
|
||||
m_audioData.isLastFrame = helper & 0x2000; // bit 13 = terminator
|
||||
|
||||
// Read opus data
|
||||
m_audioData.payload = span(payloadBegin, payloadSize);
|
||||
|
||||
// Check for positional data
|
||||
if (stream.left() == 3 * sizeof(float)) { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Protobuf UDP Audio Format (version >= 1.5.0)
|
||||
|
||||
Uses `MumbleUDP::Audio` protobuf message. Defined in `MumbleUDP.proto`.
|
||||
|
||||
### Message Fields
|
||||
|
||||
| Field | Number | Type | Description |
|
||||
|-------|--------|------|-------------|
|
||||
| sender_session | 3 | uint32 | Session ID of the speaker (server→client only) |
|
||||
| frame_number | 4 | uint64 | Frame number in 10ms units |
|
||||
| opus_data | 5 | bytes | The encoded Opus frame |
|
||||
| is_terminator | 16 | bool | End of audio transmission |
|
||||
| positional_data | 7 | repeated float | X, Y, Z position (3 floats) |
|
||||
| volume_adjustment | 8 | float | Volume adjustment factor (server→client) |
|
||||
| context | 9 | uint32 | Audio context (server→client: normal/shout/whisper/listen) |
|
||||
| target | 10 | uint32 | Voice target ID (client→server) |
|
||||
|
||||
### Encode (Client → Server)
|
||||
|
||||
From `UDPAudioEncoder::prepareAudioPacket_protobuf()`:
|
||||
|
||||
```cpp
|
||||
m_audioMessage.set_frame_number(data.frameNumber);
|
||||
m_audioMessage.set_opus_data(data.payload.data(), data.payload.size());
|
||||
m_audioMessage.set_is_terminator(data.isLastFrame);
|
||||
|
||||
// Serialize protobuf with 1-byte header prefix
|
||||
encodeProtobuf(m_audioMessage, m_byteBuffer, 1, MAX_UDP_PACKET_SIZE);
|
||||
m_byteBuffer[0] = static_cast<byte>(UDPMessageType::Audio); // 0x00
|
||||
```
|
||||
|
||||
Then in `updateAudioPacket_protobuf()`:
|
||||
```cpp
|
||||
m_audioMessage.set_target(data.targetOrContext);
|
||||
encodeProtobuf(m_audioMessage, m_byteBuffer, offset, MAX_UDP_PACKET_SIZE);
|
||||
```
|
||||
|
||||
### Wire Format (inside crypto envelope)
|
||||
|
||||
```
|
||||
[0x00] [protobuf: frame_number + opus_data + is_terminator] [protobuf: target]
|
||||
```
|
||||
|
||||
The encoder splits into "static" (frame data) and "variable" (target/context, volume) parts
|
||||
for efficient re-encoding when forwarding to multiple recipients.
|
||||
|
||||
---
|
||||
|
||||
## 5. Ping Format
|
||||
|
||||
### Legacy Ping
|
||||
|
||||
```
|
||||
[header: 0x20] [timestamp varint]
|
||||
```
|
||||
Or extended (12/24 bytes): `[version uint32] [timestamp uint64] [user_count uint32] [max_users uint32] [max_bw uint32]`
|
||||
|
||||
### Protobuf Ping
|
||||
|
||||
```
|
||||
[0x01] [protobuf: MumbleUDP::Ping]
|
||||
```
|
||||
Fields: `timestamp` (uint64), `request_extended_information` (bool), `server_version_v2` (uint32), `user_count` (uint32), `max_user_count` (uint32), `max_bandwidth_per_user` (uint32).
|
||||
|
||||
---
|
||||
|
||||
## 6. UDP Send Path (Client)
|
||||
|
||||
From `ServerHandler::sendMessage()`:
|
||||
|
||||
```cpp
|
||||
void ServerHandler::sendMessage(const unsigned char *data, int len, bool force) {
|
||||
// data = encoded audio packet (legacy or protobuf)
|
||||
|
||||
if (!force && (NetworkConfig::TcpModeEnabled() || !bUdp)) {
|
||||
// TCP tunnel: wrap in UDPTunnel message
|
||||
// [UDPTunnel type(2 bytes)] [length(4 bytes)] [data]
|
||||
} else {
|
||||
// Encrypt and send via UDP
|
||||
connection->csCrypt->encrypt(data, crypto.data(), len);
|
||||
qusUdp->writeDatagram(crypto.data(), len + 4, qhaRemote, usResolvedPort);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The server chooses UDP vs TCP per-message based on whether UDP is established (`bUdp`).
|
||||
|
||||
---
|
||||
|
||||
## 7. UDP Receive Path (Client)
|
||||
|
||||
From `ServerHandler::udpReady()`:
|
||||
|
||||
```cpp
|
||||
void ServerHandler::udpReady() {
|
||||
while (qusUdp->hasPendingDatagrams()) {
|
||||
// 1. Read from UDP socket
|
||||
qusUdp->readDatagram(encrypted, buflen, &senderAddr, &senderPort);
|
||||
|
||||
// 2. Verify sender address/port matches server
|
||||
// 3. Check crypto is initialized
|
||||
// 4. Decrypt
|
||||
connection->csCrypt->decrypt(encrypted, buffer.data(), buflen);
|
||||
|
||||
// 5. Decode based on protocol version
|
||||
m_udpDecoder.decode(buffer.subspan(0, buflen - 4));
|
||||
|
||||
// 6. Dispatch
|
||||
switch (m_udpDecoder.getMessageType()) {
|
||||
case UDPMessageType::Ping: /* measure latency */ break;
|
||||
case UDPMessageType::Audio: /* play audio */ break;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Barnard-Specific Findings
|
||||
|
||||
### Advertised Version
|
||||
|
||||
Barnard's `gumble` library sends `VersionV1 = 1<<16 | 3<<8 | 0` = **1.3.0**:
|
||||
|
||||
```go
|
||||
// From gumble/gumble/client.go DialWithDialer():
|
||||
versionPacket := MumbleProto.Version{
|
||||
VersionV1: proto.Uint32(ClientVersion), // 1<<16 | 3<<8 | 0
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
This is **below 1.5.0**, so the server falls back to **legacy UDP format** for all audio
|
||||
sent to barnard. This is why incoming packets have type byte `0x80` (legacy Opus) instead
|
||||
of `0x00` (protobuf Audio).
|
||||
|
||||
### Fix
|
||||
|
||||
To receive protobuf-format audio, barnard should advertise version >= 1.5.0:
|
||||
|
||||
```go
|
||||
const ClientVersion = 1<<16 | 5<<8 | 0 // 1.5.0
|
||||
```
|
||||
|
||||
However, this change must be accompanied by full support for the protobuf UDP format
|
||||
(both encode and decode), which is what we've implemented in `udp15.go`.
|
||||
|
||||
### Current State
|
||||
|
||||
- **Outbound**: We send protobuf-format audio (type `0x00`) encrypted with 1.5 OCB.
|
||||
The server accepts this because it recognizes the 1.5 crypto format regardless of
|
||||
the advertised version.
|
||||
|
||||
- **Inbound**: The server sends us legacy-format audio (type `0x80` in bits 5-7)
|
||||
encrypted with 1.5 OCB. Our `handleLegacyUDPVoice` correctly parses this format.
|
||||
|
||||
- **Both paths work** given the current hybrid setup.
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
# Windows support
|
||||
|
||||
Barnard can be built for Windows with a native CGO toolchain. Audio support uses native OpenAL Soft, libopus, libopusfile/libogg, and RNNoise; `ffmpeg.exe` must also be on `PATH` for recording and file playback.
|
||||
|
||||
The easiest supported setup is MSYS2 MinGW-w64. Install the matching architecture's Go toolchain and development packages for OpenAL Soft, opus, opusfile, libogg, RNNoise, FFmpeg, and pkg-config. Build from its MinGW shell:
|
||||
|
||||
```
|
||||
go build -o barnard.exe .
|
||||
```
|
||||
|
||||
For cross-compilation from Linux, use the matching MinGW compiler and its pkg-config environment, for example:
|
||||
|
||||
```
|
||||
GOOS=windows GOARCH=amd64 CGO_ENABLED=1 \
|
||||
CC=x86_64-w64-mingw32-gcc \
|
||||
PKG_CONFIG=x86_64-w64-mingw32-pkg-config \
|
||||
go build -o barnard.exe .
|
||||
```
|
||||
|
||||
The dependency include and library paths must point to Windows builds, not host Linux libraries.
|
||||
|
||||
The legacy `--fifo` command control is unavailable on Windows because it uses POSIX filesystem FIFOs. Windows defaults to no notification command; an explicitly configured notification command is run by `cmd.exe /C`.
|
||||
Reference in New Issue
Block a user