Skip to content

fix(connlib): maintain packet order across GSO batches#8920

Merged
thomaseizinger merged 3 commits into
mainfrom
fix/gso-batches
Apr 29, 2025
Merged

fix(connlib): maintain packet order across GSO batches#8920
thomaseizinger merged 3 commits into
mainfrom
fix/gso-batches

Conversation

@thomaseizinger

@thomaseizinger thomaseizinger commented Apr 29, 2025

Copy link
Copy Markdown
Member

Despite our efforts in #8912, the current implementation still does not do enough to maintain packet ordering across GSO batches.

At present, we very aggressively batch packets of the same length together. This however is too eager when we consider packet flows such as the following:

9:03:49.585143 IP 10.128.15.241.3000 > 100.69.109.138.53474: Flags [.], seq 1:1229, ack 524, win 249, options [nop,nop,TS val 3862031964 ecr 1928356896], length 1228
09:03:49.585151 IP 10.128.15.241.3000 > 100.69.109.138.53474: Flags [P.], seq 1229:2063, ack 524, win 249, options [nop,nop,TS val 3862031964 ecr 1928356896], length 834
09:03:49.585157 IP 10.128.15.241.3000 > 100.69.109.138.53474: Flags [P.], seq 2063:3094, ack 524, win 249, options [nop,nop,TS val 3862031964 ecr 1928356896], length 1031
09:03:49.585187 IP 10.128.15.241.3000 > 100.69.109.138.53474: Flags [.], seq 3094:4322, ack 524, win 249, options [nop,nop,TS val 3862031964 ecr 1928356896], length 1228
09:03:49.585188 IP 10.128.15.241.3000 > 100.69.109.138.53474: Flags [P.], seq 4322:5156, ack 524, win 249, options [nop,nop,TS val 3862031964 ecr 1928356896], length 834
09:03:49.585227 IP 10.128.15.241.3000 > 100.69.109.138.53474: Flags [.], seq 5156:6384, ack 524, win 249, options [nop,nop,TS val 3862031964 ecr 1928356896], length 1228
09:03:49.585228 IP 10.128.15.241.3000 > 100.69.109.138.53474: Flags [P.], seq 6384:7612, ack 524, win 249, options [nop,nop,TS val 3862031964 ecr 1928356896], length 1228
09:03:49.585230 IP 10.128.15.241.3000 > 100.69.109.138.53474: Flags [P.], seq 7612:8249, ack 524, win 249, options [nop,nop,TS val 3862031964 ecr 1928356896], length 637
09:03:49.585846 IP 10.128.15.241.3000 > 100.69.109.138.53474: Flags [.], seq 8249:9477, ack 524, win 249, options [nop,nop,TS val 3862031964 ecr 1928356896], length 1228
09:03:49.585851 IP 10.128.15.241.3000 > 100.69.109.138.53474: Flags [P.], seq 9477:10705, ack 524, win 249, options [nop,nop,TS val 3862031964 ecr 1928356896], length 1228

As we can see here, the remote sends us packet batches of varying lengths:

  • 1228, 834
  • 1031
  • 1228, 834
  • 1228, 1228, 637
  • 1228, 1228

1228 represents a "full" TCP packet so any packet following a full-packet SHOULD be grouped together into a GSO batch.

Currently, we are batching all the 1228 packets together and we ignore the fact that there were actually smaller sized packets inbetween those that belong together.

To mitigate this, we refactor the GsoQueue to remove the segment_size from the binning key of our map and instead only group batches by their source, destination and ECN information. Within such a connection, we then create an ordered list of batches. A new batch is started if the length differs or we have previously pushed a packet that isn't of the length of the batch, therefore signalling the end of the batch.

The result here looks very promising (this is loading blog.firezone.dev via the lynx browser from within the headless-client docker container, so going through a Gateway running this PR):

main this PR
Screenshot From 2025-04-29 10-32-00 image

Related: #8899

Copilot AI review requested due to automatic review settings April 29, 2025 00:05
@vercel

vercel Bot commented Apr 29, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
firezone ✅ Ready (Inspect) Visit Preview 💬 Add feedback Apr 29, 2025 0:12am

@thomaseizinger thomaseizinger requested a review from jamilbk April 29, 2025 00:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR refactors the batching logic used for sending UDP datagrams via GSO. The primary changes include updating the Ecn enum to support ordering, removing segment_size from the batching key in favor of grouping by connection (src, dst, and ECN), and updating the enqueue and datagram draining logic with corresponding tests to preserve packet ordering.

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
rust/ip-packet/src/lib.rs Updated the Ecn enum derivations to include Hash, PartialOrd, and Ord for key use.
rust/connlib/tunnel/src/io/gso_queue.rs Refactored the GsoQueue to group batches by connection and use a VecDeque for order.
rust/connlib/tunnel/src/io.rs Removed the call to handle_timeout from send_network to reflect batching changes.
Comments suppressed due to low confidence (1)

rust/connlib/tunnel/src/io/gso_queue.rs:60

  • [nitpick] Consider renaming the shadowed 'batch_size' variable (for example, to 'current_batch_size') to avoid confusion with the tuple's batch size value.
let batch_size = *batch_size;

// A batch is considered "ongoing" if so far we have only pushed packets of the same length.
let batch_is_ongoing = buffer.len() % batch_size == 0;

if batch_is_ongoing && payload_len <= batch_size {

Copilot AI Apr 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition allows packets with a smaller length than the established batch size to be appended to the ongoing batch, which contradicts the intent to start a new batch when the packet length differs. Consider changing the condition to 'if batch_is_ongoing && payload_len == batch_size' so that only packets matching the batch size continue the current batch.

Suggested change
if batch_is_ongoing && payload_len <= batch_size {
if batch_is_ongoing && payload_len == batch_size {

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intended, we allow for one packet that isn't of the current batch size.

The upcoming order preserving GSO queue will not need this.
@thomaseizinger

thomaseizinger commented Apr 29, 2025

Copy link
Copy Markdown
Member Author

If this ends up fixing things on Apple (i.e. #8899), I'll send a follow-up PR with some changelog entries. This primarily affects the Gateway actually because it was re-ordering packets as it sent them to the Clients.

@jamilbk jamilbk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@thomaseizinger thomaseizinger added this pull request to the merge queue Apr 29, 2025
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Apr 29, 2025
@thomaseizinger thomaseizinger added this pull request to the merge queue Apr 29, 2025
Merged via the queue into main with commit fde8d08 Apr 29, 2025
@thomaseizinger thomaseizinger deleted the fix/gso-batches branch April 29, 2025 01:08
github-merge-queue Bot pushed a commit that referenced this pull request Apr 29, 2025
github-merge-queue Bot pushed a commit that referenced this pull request Apr 30, 2025
When a platform's network driver does not support GSO, `quinn-udp`
detects that and disables segmentation offloading:

> 04-30 11:32:49.161 19612 19836 I connlib : quinn_udp::imp:
`libc::sendmsg` failed with I/O error (os error 5); halting segmentation
offload

What this means is that it sets an internal field that sets the GSO
batch-size to 1 (instead of the default 32). We then use this batch-size
to compute, how we are meant to chunk up the already batched datagrams.
As a consequence of #8920, we are now also using a "feature" of GSO
where the last datagram in a GSO batch is allowed to be less than the
segment size.

The combination of these two features now makes it possible that we are
passing a datagram to the kernel where the `segment_size` is greater
than the actual length. Android's Linux kernel doesn't seem to like that
an barfs when being passed such a datagram with an IO error 5.

The long-term fix is to sanitise this within `quinn-udp` but in the
short-term, we can do this ourselves as part of the loop where we
segment the datagrams.

---------

Signed-off-by: Thomas Eizinger <thomas@eizinger.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
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.

3 participants