-
Notifications
You must be signed in to change notification settings - Fork 1
add wireguard fragmentation helpers #82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
FlorianUekermann
merged 1 commit into
v0
from
florian/obs-3199-fragmentation-support-shared-code-in-api-crate
Feb 23, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| /.idea | ||
| /.devcontainer | ||
| /target | ||
| result* |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| use super::{usize_from, WgFragmentHeaderData, WG_FRAGMENT_MESSAGE_HEADER_SIZE}; | ||
| use bytes::Bytes; | ||
| use std::num::NonZeroU32; | ||
|
|
||
| pub enum ReassembleResult { | ||
| /// Message was not fragmented, returned as-is. | ||
| NotFragmented(Bytes), | ||
| /// Message was reassembled from two fragments. | ||
| Reassembled(Bytes), | ||
| /// A single fragment was received with no matching counterpart; the max message size that caused fragmentation. | ||
| UnmatchedFragment { max_message_size: u16 }, | ||
| } | ||
|
|
||
| pub struct WgFragmentBuffer { | ||
| max_fragment_size: u16, | ||
| buffer: Vec<Option<Bytes>>, | ||
| } | ||
|
|
||
| impl WgFragmentBuffer { | ||
| pub fn new(len: NonZeroU32, max_fragment_size: u16) -> Self { | ||
| Self { | ||
| max_fragment_size, | ||
| buffer: vec![None; usize_from(len)], | ||
| } | ||
| } | ||
|
|
||
| /// Process a potentially fragmented WG message. | ||
| /// | ||
| /// Returns the message if available, or the max message size from the fragment header if the fragment was buffered. | ||
| /// | ||
| /// Incorrect reassembly is theoretically possible if two fragmented messages are exactly `u32::MAX` or a multiple thereof apart, all intervening messages sharing the same buffer slot are lost, and only complementary halves of each survive. WireGuard's authenticated encryption will detect this downstream. | ||
| pub fn reassemble(&mut self, new_message: Bytes) -> ReassembleResult { | ||
| let Some(new_header) = WgFragmentHeaderData::from_message(&new_message) else { | ||
| return ReassembleResult::NotFragmented(new_message); | ||
| }; | ||
| let max_message_size = new_header.mtu; | ||
| if new_message.len() > usize_from(self.max_fragment_size) { | ||
| tracing::error!( | ||
| message_id = "zg4h9td3", | ||
| message_len = new_message.len(), | ||
| max_fragment_size = self.max_fragment_size, | ||
| "ignoring oversized WG message fragment" | ||
| ); | ||
| return ReassembleResult::UnmatchedFragment { max_message_size }; | ||
| } | ||
| let buffer_len = self.buffer.len(); | ||
| let buffer_element = &mut self.buffer[usize_from(new_header.message_idx) % buffer_len]; | ||
| match buffer_element { | ||
| None => { | ||
| *buffer_element = Some(new_message); | ||
| ReassembleResult::UnmatchedFragment { max_message_size } | ||
| } | ||
| Some(old_message) => { | ||
| let old_header = WgFragmentHeaderData::from_message(old_message).unwrap(); | ||
| if old_header.message_idx != new_header.message_idx { | ||
| *buffer_element = Some(new_message); | ||
| return ReassembleResult::UnmatchedFragment { max_message_size }; | ||
| } | ||
| if old_header.second_fragment == new_header.second_fragment { | ||
| *buffer_element = Some(new_message); | ||
| return ReassembleResult::UnmatchedFragment { max_message_size }; | ||
| } | ||
| let (first_msg, second_msg) = if old_header.second_fragment { | ||
| (new_message.as_ref(), old_message.as_ref()) | ||
| } else { | ||
| (old_message.as_ref(), new_message.as_ref()) | ||
| }; | ||
| let first_data = &first_msg[WG_FRAGMENT_MESSAGE_HEADER_SIZE..]; | ||
| let second_data = &second_msg[WG_FRAGMENT_MESSAGE_HEADER_SIZE..]; | ||
| let mut reassembled = Vec::with_capacity(first_data.len() + second_data.len()); | ||
| reassembled.extend_from_slice(first_data); | ||
| reassembled.extend_from_slice(second_data); | ||
| *buffer_element = None; | ||
| ReassembleResult::Reassembled(reassembled.into()) | ||
| } | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| use bytes::BufMut; | ||
|
|
||
| pub mod merge; | ||
| pub mod split; | ||
|
|
||
| // WireGuard uses message types 1-4. We picked 170, mid-range in the unassigned space, to avoid collisions with extensions that claim values near the boundaries. | ||
| const WG_FRAGMENT_MESSAGE_TYPE: u8 = 170; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a comment with where this magic value comes from? (AKA what values are used by wireguard and what range we can use) |
||
|
|
||
| const WG_FRAGMENT_MESSAGE_HEADER_SIZE: usize = 1 // message type | ||
| + 1 // 1st (lowest) bit: fragment index; 2nd-8th bit: reserved | ||
| + 2 // max message size (limit that caused fragmentation, little-endian) | ||
| + 4; // message index (little-endian) | ||
|
|
||
| pub struct WgFragmentHeaderData { | ||
| pub message_idx: u32, | ||
| pub second_fragment: bool, | ||
| pub mtu: u16, | ||
| } | ||
|
|
||
| impl WgFragmentHeaderData { | ||
| pub fn header_bytes(&self) -> [u8; WG_FRAGMENT_MESSAGE_HEADER_SIZE] { | ||
| let mut header = [0u8; WG_FRAGMENT_MESSAGE_HEADER_SIZE]; | ||
| let mut buf = &mut header[..]; | ||
| buf.put_u8(WG_FRAGMENT_MESSAGE_TYPE); | ||
| buf.put_u8(u8::from(self.second_fragment)); | ||
| buf.put_u16_le(self.mtu); | ||
| buf.put_u32_le(self.message_idx); | ||
| header | ||
| } | ||
| pub fn from_message(message: &[u8]) -> Option<Self> { | ||
| let (header, _data) = message.split_at_checked(WG_FRAGMENT_MESSAGE_HEADER_SIZE)?; | ||
| let (message_type, header) = header.split_at(1); | ||
| if message_type != [WG_FRAGMENT_MESSAGE_TYPE] { | ||
| return None; | ||
| } | ||
| let (flags, header) = header.split_at(1); | ||
| let second_fragment = (flags[0] & 1) != 0; | ||
| let (mtu, header) = header.split_at(2); | ||
| let mtu = u16::from_le_bytes(mtu.try_into().unwrap()); | ||
| let (message_idx, header) = header.split_at(4); | ||
| let message_idx = u32::from_le_bytes(message_idx.try_into().unwrap()); | ||
| _ = header; | ||
|
|
||
| Some(Self { | ||
| message_idx, | ||
| second_fragment, | ||
| mtu, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| fn usize_from(x: impl Into<u32>) -> usize { | ||
| const _: () = assert!(usize::BITS >= 32, "usize smaller than u32"); | ||
| let x_u32: u32 = x.into(); | ||
| x_u32 as usize | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| use super::{WgFragmentHeaderData, WG_FRAGMENT_MESSAGE_HEADER_SIZE}; | ||
| use bytes::Bytes; | ||
|
|
||
| #[derive(Debug, Default)] | ||
| pub struct WgMessageFragmenter { | ||
| next_message_index: u32, | ||
| } | ||
|
|
||
| impl WgMessageFragmenter { | ||
| pub fn fragment(&mut self, message: Bytes, max_message_size: u16) -> (Bytes, Option<Bytes>) { | ||
| if message.len() <= usize::from(max_message_size) { | ||
| return (message, None); | ||
| } | ||
|
|
||
| let message_idx = self.next_message_index; | ||
| self.next_message_index = self.next_message_index.wrapping_add(1); | ||
|
|
||
| let split_point = message.len() / 2; | ||
|
|
||
| let first_header = WgFragmentHeaderData { | ||
| message_idx, | ||
| second_fragment: false, | ||
| mtu: max_message_size, | ||
| }; | ||
| let second_header = WgFragmentHeaderData { | ||
| message_idx, | ||
| second_fragment: true, | ||
| mtu: max_message_size, | ||
| }; | ||
|
|
||
| let mut first_frag = Vec::with_capacity(WG_FRAGMENT_MESSAGE_HEADER_SIZE + split_point); | ||
| first_frag.extend_from_slice(&first_header.header_bytes()); | ||
| first_frag.extend_from_slice(&message[..split_point]); | ||
|
|
||
| let remainder = message.len() - split_point; | ||
| let mut second_frag = Vec::with_capacity(WG_FRAGMENT_MESSAGE_HEADER_SIZE + remainder); | ||
| second_frag.extend_from_slice(&second_header.header_bytes()); | ||
| second_frag.extend_from_slice(&message[split_point..]); | ||
|
|
||
| (first_frag.into(), Some(second_frag.into())) | ||
| } | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.