Skip to content

frame: fix UDP datagram boundary recovery after parse errors#246

Closed
gavrilaf wants to merge 1 commit into
bluenviron:mainfrom
gavrilaf:fix/udp-client-resync
Closed

frame: fix UDP datagram boundary recovery after parse errors#246
gavrilaf wants to merge 1 commit into
bluenviron:mainfrom
gavrilaf:fix/udp-client-resync

Conversation

@gavrilaf

Copy link
Copy Markdown

Problem

When using EndpointUDPClient over a real UDP link (e.g. a radio-link tunnel), a single malformed or out-of-order datagram produces dozens of spurious parse error invalid magic byte errors:

2026/04/11 15:55:46 parse error invalid magic byte: 0
2026/04/11 15:55:46 parse error invalid magic byte: ff
2026/04/11 15:55:46 parse error invalid magic byte: 2b

The root cause: bufio.Reader treats the UDP connection as a byte stream. After a parse error it continues scanning byte-by-byte inside the same datagram, generating one error per remaining byte. The next datagram then starts mid-buffer, corrupting further frames.

This does not affect TCP or serial endpoints because those are true byte streams where byte-by-byte scanning is the correct recovery strategy.

Solution

Add EndpointUDPClientPacket — identical to EndpointUDPClient but marks the channel as packet-oriented. After any ReadError, DiscardBuffered() drops all bytes still buffered from the current datagram so the next Read() begins on a fresh UDP packet boundary.

One bad datagram → exactly one EventParseError → normal parsing resumes.

Changes

  • pkg/frame: add Reader.DiscardBuffered() to flush the bufio buffer
  • pkg/frame: ReadWriter allocates a 65536-byte buffer in packet-oriented mode so one bufio fill always covers exactly one datagram
  • pkg/frame/v2_frame: unmarshal header with Peek + Discard instead of peekAndDiscard so an incompatibility-flag error leaves header bytes in the buffer for DiscardBuffered to clean up
  • channel: call DiscardBuffered() on ReadError when PacketOriented
  • channel_provider: detect packetOrientedEndpoint interface and set the flag
  • endpoint_udp_client_packet: new EndpointUDPClientPacket endpoint type

Tests

  • TestEndpointUDPClientPacketRecoverAfterMalformedDatagrams: sends 2 junk datagrams followed by a valid one — verifies exactly 2 EventParseError events and correct frame received after
  • TestEndpointUDPClientRecoverAfterMalformedDatagram: existing-style test in endpoint_client_test.go
  • TestReaderRecoverAfterInvalidV2IncompatibilityFlag: unit test for the Peek+Discard header change

On a packet-oriented transport (UDP), a single malformed datagram used
to produce dozens of spurious "invalid magic byte" errors because bufio
scanned the bad datagram byte-by-byte looking for the next magic byte.

Add EndpointUDPClientPacket: identical to EndpointUDPClient but marks
the channel as PacketOriented. After any ReadError, DiscardBuffered()
drops all bytes still buffered from the current datagram so the next
Read() begins on a fresh UDP packet boundary.

Key changes:
- pkg/frame: Reader.DiscardBuffered() discards remaining buffered bytes
- pkg/frame: ReadWriter uses a 65536-byte buffer in PacketOriented mode
  so a single bufio fill always covers exactly one datagram
- pkg/frame/v2_frame: unmarshal header with Peek+Discard instead of
  peekAndDiscard so an incompatibility-flag error leaves the header
  bytes in the buffer for DiscardBuffered to clean up
- channel: call DiscardBuffered on ReadError when PacketOriented
- channel_provider: detect packetOrientedEndpoint and set the flag
- endpoint_udp_client_packet: new EndpointUDPClientPacket type
@aler9

aler9 commented May 16, 2026

Copy link
Copy Markdown
Member

Hello, the idea is good but this PR contains too many unrelated things.

I'm assuming this PR is intended to drop entire datagrams in case of a read error, as the title and description mention. The following things are unrelated:

pkg/frame: ReadWriter allocates a 65536-byte buffer in packet-oriented mode so one bufio fill always covers exactly one datagram

This is meant to support decoding multiple Mavlink frames coalesced in the same UDP packet. Useful, but another topic.

pkg/frame/v2_frame: unmarshal header with Peek + Discard instead of peekAndDiscard so an incompatibility-flag error leaves header bytes in the buffer for DiscardBuffered to clean up

This is the opposite of the aim of the PR: what's the meaning of leaving bytes in the read buffer while you are going to discard them later?

endpoint_udp_client_packet: new EndpointUDPClientPacket endpoint type

Why creating a whole new endpoint instead of using the existing one?

We can support datagram-based parsing but in an orderly fashion.

@aler9

aler9 commented May 16, 2026

Copy link
Copy Markdown
Member

I've moved the recovery mechanism in #260. With respect to this patch, i also made sure that a Mavlink frame can never spawn into two UDP packets. Thanks for providing the original implementation.

aler9 added a commit that referenced this pull request May 16, 2026
When the endpoint is datagram-based and there's an error, discard
buffered data and skip directly to the next datagram.
aler9 added a commit that referenced this pull request May 16, 2026
When the endpoint is datagram-based and there's an error, discard
buffered data and skip directly to the next datagram.
aler9 added a commit that referenced this pull request May 16, 2026
When the endpoint is datagram-based and there's an error, discard
buffered data and skip directly to the next datagram.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants