SetUDP15Crypto was initializing udp15Server with setup15(key,
serverNonce, clientNonce), which set decryptIV=clientNonce.
But the server encrypts with server_nonce, so the client-side
decryptIV must be server_nonce.
The incoming IV byte from the server was 0x58, but our decryptIV[0]
was 0xab (client_nonce[0]). Diff of -83, way outside the ±30
acceptance window.
Now udp15Server.decryptIV is directly set to server_nonce.
Wumble prepends Bytes[0_u8] before the protobuf fields when building
the plaintext for encryption. My encodeUDPAudio was missing this type
byte, so the server decrypted our packets and saw raw protobuf without
a message type indicator — silently dropping them.
Also added type-byte dispatch on receive side: 0x00 = audio, 0x01 = ping.
Two critical bugs found by testing against wumble's Crystal OCB:
1. IV increment was big-endian (byte[15] first). Wumble uses
little-endian (byte[0] first). Since byte[0] is what goes on
the wire, it never changed — server saw every packet as a
replay. Changed to little-endian increment matching wumble's
increment_encrypt_iv.
2. In ocb15Decrypt's final partial block, the checksum was
computed from recovered plaintext XOR pad (= ciphertext).
But ocb15Encrypt computes it from ciphertext XOR pad
(= plaintext). Now both use ciphertext XOR pad.
Added udp15_test.go with:
- TestIncrementIV: verifies little-endian overflow behavior
- TestOCB15RoundTrip: encrypt→decrypt for 0-100 byte inputs
- TestCryptState15RoundTrip: full pipeline with real crypto keys
- TestUDPAudioProtobuf: protobuf encode/decode round-trip
connect() starts the tone generator but didn't set b.Tx=true,
so setTransmit would start a second instance. Now b.Tx is set
so the UI toggle works as a stop/restart instead of double-start.
- Snapshot c.udpConn to local variable to avoid race with disconnect
cleanup goroutine nil'ing it between the nil check and Write call.
- Move per-packet hex dumps from Info to Debug level.
- Only hex-dump the first received packet at Info level.
The old gumble library uses the legacy UDPVoice format:
[plaintext header: type|target, session, seq] [OCB2 encrypted payload]
Modern Murmur 1.5+ servers speak the native UDP protocol:
[iv_byte(1)] [tag(3)] [OCB encrypted MumbleUDP.Audio protobuf]
This is why zero incoming UDP packets were seen — the server
silently dropped all legacy-format datagrams.
Changes:
- New udp15.go: MumbleUDP.Audio protobuf encode/decode, OCB
encrypt/decrypt matching wumble's implementation, cryptState15
with IV-increment nonce and replay-protection history
- udp.go: ping uses 1.5 native format (type 0x01, encrypted
protobuf), reader delegates to HandleUDPPacket15
- client.go: WriteAudio tries WriteAudioUDP15 first
- crypt.go: SetUDP15Crypto called from handleCryptSetup
OCB variant matches wumble's crypt_state.cr exactly: full 16-byte
nonce (incremented IV), no associated data, final block padding
with byte[15] = remaining*8.
Logs every step of UDP send/receive:
SEND path (WriteAudioUDP):
- counter, nonce, plaintext len, opus len, final flag
- full packet hex dump after encryption
RECEIVE path (udpReadRoutine -> handleUDPPacket -> handleUDPTunnel):
- raw packet hex on arrival
- type byte, audio type, target parsed
- session ID, sequence number, header/encrypted lengths
- user lookup result
- cryptIn state, counter, nonce for decryption
- encrypted payload hex
- decrypt success/failure with error detail
- plaintext hex after decryption
- dispatch to handleUDPTunnel
UDPTUNNEL path (handleUDPTunnel):
- session, seq, audio length, final flag, buffer remaining
- Opus decode success/failure
- pcm sample count after decode
CRYPTO SETUP (cryptState.setup):
- key, IV, encrypted IV, computed nonce prefix
All at log.Info level so they show up with -log=info (no --debug needed).
Adds Config.DisableUDP field. When set:
- DialWithDialer skips startUDP() entirely (no UDP socket, no UDP reader)
- WriteAudio skips WriteAudioUDP and goes straight to TCP-tunneled audio
Useful for debugging crypto/transport issues — rules out UDP-specific
problems like NAT, firewall, or OCB2 counter mismatches.
setTransmit, OnMicVolumeUp, OnMicVolumeDown, and the file-playback
auto-transmit path all dereferenced b.Stream without checking for nil.
In tone-test mode b.Stream is never set (OpenAL is skipped), so guard
all call sites.
Adds a --tone-test flag that bypasses all OpenAL/soundcard
initialization and instead:
- Generates a 440 Hz sine wave at 48kHz mono, encodes it via Opus,
and sends it to the Mumble server.
- Saves all incoming decoded audio to a raw PCM file (s16le,
stereo, 48kHz) specified by --tone-out (default: incoming.pcm).
Useful for end-to-end testing of the Opus encode/decode pipeline
and Mumble transport without requiring physical audio hardware.
Usage:
barnard --server HOST --tone-test --tone-out /tmp/in.pcm
ffplay -f s16le -ar 48000 -ac 2 incoming.pcm
Add info-level logging showing which device OpenAL opens and with what
format. Also add nil checks for inputDevice/outputDevice pointers to
prevent panics when config fields are unset.
When -auto-transmit is passed, barnard automatically keys up the
microphone as soon as the connection sync completes. Useful for
testing, bots, and unattended operation.
Usage:
barnard -auto-transmit -server=mumble.example.com -username=test_bot
- Suppress 'incomplete fields' log spam after crypto is initialized
(server sends partial CryptSetup for key rotation)
- Log 'CryptSetup updated' at debug when keys are refreshed
- Add warning log when UDP decryption fails (OCB auth error)
- Add debug log for UDP packets from unknown sessions
- Add debug log for UDP packets before crypto is ready
When UDP audio transport is active, ignore UDPTunnel packets (type 1)
on the TCP connection. Without this, audio packets arrive via both
TCP and UDP, causing the per-user sequence tracker to advance by 2
per frame. The gap detection then generates spurious PLC frames for
every single audio packet, producing log spam and audio artifacts.
The readRoutine now checks c.udpActive before dispatching packet
type 1.
CryptSetup can arrive during the initial sync (before handleServerSync
completes). Previously startUDP() was called after the sync finished,
so if CryptSetup arrived first, crypto was initialized but the UDP
socket didn't exist yet — causing a permanent fallback to TCP.
Move startUDP() to immediately after writing the Version/Authenticate
packets and before waiting for the sync to complete. The UDP socket
is now ready when CryptSetup arrives.
The -logfile flag writes log output to a file in addition to stderr,
so users can run barnard in the TUI without log spam on screen.
Usage:
barnard -log=debug -logfile=/tmp/barnard.log -server=mumble.example.com
Add a lightweight log package (log/log.go) with debug/info/warn/error
levels. Wire it up via a -log flag (default: warn). Logging covers:
UDP transport:
- Socket open/failure, CryptSetup receipt, crypto initialization
- First UDP audio send confirmation
- UDP read errors and packet counts
- TCP fallback reason (no socket, waiting for crypto)
Audio pipeline:
- Source (mic) routine start with config details
- Audio stream start/end per remote user
- First audio stream creation for each user
- Packet loss detection with sequence gaps and PLC generation
- Decoder reset on sequence reordering
Usage: barnard -log=debug -server=mumble.example.com
Levels: debug, info, warn, error
Rewrite the OCB implementation from scratch using the OCB2 algorithm
that Mumble uses (not RFC 7253 OCB3). Key fixes:
- L_* base value is now E_K(0^128), not the initial offset
- Proper bottom-bit clearing for non-15-byte nonces
- Correct GF(2^128) doubling for offset updates
- Fixed ntz(0) infinite loop
- Fixed AD offset processing to use L_ntz scheme
Add comprehensive tests:
- Round-trip encrypt/decrypt for various sizes, with/without AD
- Tag verification (tampered data, wrong key, wrong nonce)
- Deterministic output verification
- Initial offset computation against known values
- cryptState setup, encrypt/decrypt, counter-based nonce derivation
- Nonce byte-aligned processing for all valid OCB nonce lengths (1-15)
- Big-endian counter endianness verification
- Benchmarks for encrypt and decrypt
pycryptodome comparison tests removed: pycryptodome 3.23.0 does not
match RFC 7253 test vectors and implements a different OCB variant.
OpenSSL CLI doesn't support AEAD ciphers.
Add native UDP support for audio transport, which provides much lower
latency than tunneling audio over TCP. The implementation:
- Adds OCB-AES128 encryption (RFC 7253) for UDP audio packets, as
required by the Mumble protocol.
- Implements the CryptSetup handler to receive encryption keys and
nonces from the server.
- Opens a UDP socket to the server after TCP connection and starts
reading encrypted audio once the CryptSetup is received.
- Modifies WriteAudio to try UDP first with automatic fallback to
the existing TCP tunnel when UDP is unavailable or encryption
is not set up.
- Adds UDP ping packets to keep NAT bindings alive.
- Cleans up the UDP connection on disconnect.
The OCB implementation is a self-contained pure-Go implementation
using only crypto/aes and crypto/cipher from the standard library.
processAudioPacket accepted reclaim as a closure that captured the
outer goroutine's emptyBufs variable. When reclaim() appended buffers
inside the function, it mutated the outer variable, but the function
also returned its own copy. The caller then overwrote the outer
variable with the returned copy, losing the buffers reclaimed by
reclaim(). This caused OpenAL buffer starvation and audio dropouts.
Move reclaim() calls to the main loop before processAudioPacket, and
remove the reclaim parameter from the function. The caller now always
owns the emptyBufs lifecycle.
Add a jitter buffer in OnAudioStream that collects incoming audio packets,
sorts by sequence number, and releases them in order after a small initial
delay (3 packets, ~30ms). This prevents out-of-order playback from network
jitter and reordered UDP packets.
Add a Sequence field to AudioPacket so the jitter buffer can reorder by
sequence number. PLC frames generated by dispatchPLC now also carry the
correct sequence number so they integrate with the jitter buffer.
Add microphone capture error reporting: when CaptureSamples returns fewer
bytes than expected, report the failure to the user via a callback. The
error is reported once when the failure starts and again when the mic
recovers. Wire this up in client.go to display status messages.
User.AudioSource, Boost, Volume, and LocallyMuted were accessed from
both the OnAudioStream audio goroutine and the UI goroutine without
synchronization, a data race under the Go memory model.
Replace direct field access with thread-safe getter/setter methods
protected by a per-user mutex:
- SetAudioSource/GetAudioSource for the OpenAL source pointer
- SetBoost/Boost for the audio boost multiplier
- SetVolume/Volume for the volume level
- SetLocallyMuted/LocallyMuted for the local mute state
Update all call sites across config/, barnard.go, client.go,
ui_tree.go, and stream.go.
micVolume was accessed from both the UI goroutine (SetMicVolume,
GetMicVolume) and the audio source goroutine (sourceRoutine) without
synchronization, a data race under the Go memory model.
Switch to atomic.Uint32 storing float32 bits to make reads and writes
safe across goroutines.
DisableStereoEncoder now resets the stereo encoder so stale encoder
state from one file playback session does not bleed into the next.
Without this, the Opus encoder's internal state machine could produce
a glitch at the start of the next file playback.
AudioMaximumFrameSize (5760) already accounts for stereo by including
the AudioChannels factor. The Decode method was multiplying by
AudioChannels again, allocating 11520 int16s per decode call instead
of the needed 5760. This wasted ~21KB per audio packet (2MB/s at
100 pps). Use frameSize directly and derive total sample count from
the decoder's configured channel count.
Close audio stream channels in handleUserRemove to prevent goroutine
leaks. Each remote user that sends audio spawns a goroutine in
OnAudioStream that blocks on an unbuffered channel; without explicit
closure, these goroutines leaked on every user disconnect.
Add Opus Packet Loss Concealment for detected sequence gaps. When a
sequence number discontinuity indicates lost packets, feed empty data
to the Opus decoder to produce PLC frames that bridge the gap. This
replaces the previous approach of resetting the decoder, which caused
audible glitches/silence on packet loss. Extracted dispatchAudio helper
to avoid code duplication between real and PLC frame delivery.
Add a defensive floor of 8000 bps in the Encode method, preventing the
encoder from being configured with an invalidly low bitrate even if
AudioDataBytes is somehow set to a very small value.
The auto-bitrate formula 'bandwidth/840 - 42' produces negative values
for servers with bandwidth below ~35 kbps. A negative AudioDataBytes
would cause the Opus encoder to be configured with zero or negative
bitrate, producing invalid output.
Add a floor of 10 bytes per frame (8 kbps), the minimum usable Opus
bitrate for intelligible voice.
Lower maxGain from 8x to 4x (12dB) and target level from 18% to 12%.
The previous 8x gain would amplify quiet noise floors by 18dB,
transforming room tone and electrical hum into audible static. The
compression threshold is raised from 70% to 85% with a gentler 2:1
ratio instead of 3:1, reducing pumping distortion. The soft limiter
threshold is relaxed from 0.90 to 0.95 with a 0.2 knee instead of
0.1, providing more headroom before limiting engages.
These changes reduce the distortion and unnatural 'processed' sound
that other users would hear from barnard clients.
Replace SetBitrateToMax() with dynamic bitrate configuration based on
the per-frame byte budget (AudioDataBytes). Previously, the encoder
always targeted maximum bitrate (~510 kbps) and relied on output
truncation when frames exceeded the available bandwidth. This could
produce truncated/invalid Opus frames when the server's max bandwidth
is lower than the encoder's target.
The bitrate is now recalculated on every Encode() call as:
maxDataBytes * 8 * 100 (bits per second for 10ms frames)
This ensures the encoder produces frames that fit within the byte
budget without truncation.
Remove automatic stereo encoder selection based on buffer size. Voice
transmission in the Mumble protocol uses mono Opus with the AppVoIP
profile. The stereo encoder (AppAudio profile) is only appropriate for
file playback, which is controlled by the explicit EnableStereoEncoder
flag.
Additionally, downmix stereo microphone input to mono before sending
when no file playback is active. Previously, a stereo mic would cause
the stereo encoder to be selected via the buffer-size heuristic,
sending non-standard stereo Opus that could confuse other clients'
decoders and cause distorted audio.
This fixes 'user c has issues hearing audio from user a with distorted
audio' when user a has a stereo-input USB headset or similar device.
Track per-user audio sequence numbers to detect UDP packet loss gaps.
When a sequence discontinuity is detected (loss, reorder, or burst gap),
reset the Opus decoder state to prevent permanent audio corruption.
Also reset the decoder when Decode() returns an error, instead of
leaving the decoder in a corrupted state that produces static/popping
for the remainder of the session.
This fixes the 'random static/popping from user b but no other clients
hear it' symptom, which occurs when one client experiences packet loss
affecting only its own per-user decoder instance.
Make the F11 action menu close through a shared close action so Escape and the explicit Close actions menu item follow the same path. Treat Escape-prefixed Up and Down events as close inputs while the action menu is active, which handles termbox InputAlt behavior after pressing Escape. Preserve and restore the user/channel tree selection across action menu open and close, and preserve selection across live tree rebuilds.
Tested with: GOCACHE=/tmp/barnard-go-cache go test ./...