8000 fix(voice): detect RTP padding with the correct bit and strip it after decryption by phpboyscout · Pull Request #594 · disgoorg/disgo · GitHub
[go: up one dir, main page]

Skip to content

fix(voice): detect RTP padding with the correct bit and strip it after decryption - #594

Merged
topi314 merged 1 commit into
disgoorg:masterfrom
phpboyscout:fix/rtp-padding-bit
Aug 25, 2026
Merged

topi314 merged 1 commit into
disgoorg:masterfrom
phpboyscout:fix/rtp-padding-bit

Conversation

@phpboyscout
Copy link
Copy Markdown

Full Disclosure: this issue was identified and debugged with the help of an AI agent.

Fixes #593

What this fixes

ReadPacket tested the RTP padding flag as 0x04. Per RFC 3550 §5.1 the first octet is V=0xC0, P=0x20, X=0x10, CC=0x0F, so 0x04 lands inside the CSRC count nibble. Discord audio carries CC=0, so padding was never detected and never removed.

Under DAVE this causes hard decrypt failures. Padding left attached moves the frame's 0xFAFA terminating marker away from the end of the buffer, libdave stops recognising the frame as encrypted, and returns a generic decryption failure. Measured receive-side loss was 3.2% to 11.1% depending on client, and 0.00% with this change.

This is a regression from 1dc5f5bf (feat: DAVE (#507)), which changed the mask from a correct 0x20. The extension and CSRC masks alongside it are correct and match the bit indices documented in the comment above them.

Changes

  1. Named constants for the first-octet masks, placed beside the comment documenting the bit indices, so the three masks can be checked against their documentation at a glance.
  2. Padding is stripped after decryption. ReadPacket previously read the length from u.receiveBuffer[n-1] before decryption, where that octet is ciphertext. This was latent only because the mask never matched. Had it matched, payloads would have been truncated arbitrarily.
  3. Padding-only packets are skipped. They carry no media, advance the sequence number without advancing the timestamp, and otherwise reach the decryptor as an empty frame.

stripRTPPadding leaves a payload unchanged when the count is zero or exceeds the payload, so a malformed packet degrades to a decrypt failure rather than silent corruption.

Tests

voice/udp_conn_test.go covers:

  • padding removal through ReadPacket at 1, 4 and 32 bytes, plus the unpadded case
  • the padding-only packet being skipped rather than returned
  • stripRTPPadding with absent, zero and implausible counts
  • a guard that the three masks match their documented bit indices and do not overlap

All four ReadPacket and padding-only assertions fail on master and pass with this change. The unpadded case passes in both, so the tests discriminate rather than failing indiscriminately.

The tests use godave.NewNoopSession and a passthrough encrypter, so they need no network, no CGO and no libdave.

Verification

Verified against a live Discord voice channel. Receive-side frame loss fell from 3.2% to 11.1% down to 0.00% across 1979 frames with zero sequence gaps. Full voice package tests, go build ./... and go vet all pass.

I have not tested this against video, or with several speakers at once. Both go through ReadPacket, so a second opinion on those paths would be worth having before merging.

…r decryption

ReadPacket tested the RTP padding flag as 0x04. RFC 3550 section 5.1 lays the first
octet out as V=0xC0, P=0x20, X=0x10, CC=0x0F, so 0x04 falls inside the CSRC count
nibble. Discord audio carries a CSRC count of zero, so the flag was never true and
padding was never removed. The neighbouring extension and CSRC masks are correct and
match the bit indices the comment above them documents, which is what made the slip
hard to see.

Padding was also removed before decryption, using the last octet of the packet to
size it. That octet is ciphertext at that point, so the length read was meaningless;
had the flag ever been true, the payload would have been truncated by an arbitrary
amount. Padding sits at the end of the encrypted payload and can only be measured
once the payload is plaintext, so it is now stripped after decryption.

Finally, a packet may consist entirely of padding. Such a packet carries no media,
advancing the sequence number without advancing the timestamp, and passing it on
leaves the decryptor with an empty frame. Those packets are now skipped.

The effect on a DAVE session is that padding left attached moves the frame's
terminating marker away from the end of the buffer, so libdave does not recognise the
frame as encrypted and returns a generic decryption failure. Measured against a live
Discord voice channel, receive-side frame loss fell from between three and eleven
percent to zero, the higher figures coming from mobile clients where bandwidth probing
produces more padded packets.

Adds tests covering padding removal across several padding lengths, the padding-only
case, the helper's handling of absent and implausible counts, and a guard that the
three first-octet masks match the documented bit indices and do not overlap.
@topi314
topi314 merged commit 63fa748 into disgoorg:master Aug 25, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

fix(voice): RTP padding bit is tested as 0x04 instead of 0x20, breaking DAVE decryption

2 participants

0