Skip to content

Commit 20b7fb7

Browse files
committed
feat(net): TCP segmentation offload
1 parent fdad7b9 commit 20b7fb7

3 files changed

Lines changed: 96 additions & 33 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ smp = ["acpi"]
150150
#! ### Network Features
151151

152152
## Enables TCP support.
153-
tcp = ["net", "smoltcp", "smoltcp/socket-tcp"]
153+
tcp = ["net", "smoltcp", "smoltcp/socket-tcp", "smoltcp/segmentation-offload"]
154154

155155
## Enables UDP support.
156156
udp = ["net", "smoltcp", "smoltcp/socket-udp"]
@@ -412,6 +412,7 @@ exclude = [
412412

413413
[patch.crates-io]
414414
safe-mmio = { git = "https://github.com/hermit-os/safe-mmio", branch = "be" }
415+
smoltcp = { git = "https://github.com/cagatay-y/smoltcp", branch = "segmentation-offload" }
415416

416417
[profile.profiling]
417418
inherits = "release"

src/drivers/net/virtio/mod.rs

Lines changed: 91 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@ cfg_select! {
1717
use alloc::boxed::Box;
1818
use alloc::vec::Vec;
1919
use core::mem::{ManuallyDrop, MaybeUninit};
20+
use core::num::NonZeroUsize;
2021
use core::str::FromStr;
2122
use core::{mem, slice};
2223

2324
use smallvec::SmallVec;
24-
use smoltcp::phy::{Checksum, ChecksumCapabilities, DeviceCapabilities};
25+
use smoltcp::phy::{Checksum, ChecksumCapabilities, DeviceCapabilities, PacketMeta};
2526
use smoltcp::wire::{ETHERNET_HEADER_LEN, EthernetFrame, Ipv4Packet, Ipv6Packet};
2627
use virtio::DeviceConfigSpace;
2728
use virtio::net::{ConfigVolatileFieldAccess, Hdr, HdrF};
@@ -180,12 +181,23 @@ pub struct TxQueues {
180181
buf_size: u32,
181182
}
182183

184+
#[expect(
185+
clippy::decimal_literal_representation,
186+
reason = "In the VIRTIO specification the size limit is provided in decimal."
187+
)]
188+
const MAX_BUFFER_SIZE: usize = 65550;
189+
183190
impl TxQueues {
184191
pub fn new(vqs: Vec<VirtQueue>, dev_cfg: &NetDevCfg) -> Self {
185-
Self {
186-
vqs,
187-
buf_size: determine_mtu(dev_cfg).into(),
188-
}
192+
let buf_size = if dev_cfg
193+
.features
194+
.intersects(virtio::net::F::HOST_TSO4 | virtio::net::F::HOST_TSO6)
195+
{
196+
MAX_BUFFER_SIZE.try_into().unwrap()
197+
} else {
198+
determine_mtu(dev_cfg).into()
199+
};
200+
Self { vqs, buf_size }
189201
}
190202

191203
#[allow(dead_code)]
@@ -253,6 +265,7 @@ pub struct TxToken<'a> {
253265
send_vqs: &'a mut TxQueues,
254266
checksums: ChecksumCapabilities,
255267
send_capacity: &'a mut u32,
268+
meta: PacketMeta,
256269
}
257270

258271
impl Drop for TxToken<'_> {
@@ -280,17 +293,34 @@ impl smoltcp::phy::TxToken for TxToken<'_> {
280293

281294
let mut header = Box::new_in(<Hdr as Default>::default(), DeviceAlloc);
282295

296+
let is_large_send = cfg_select! {
297+
feature = "tcp" => token.meta.segmentation_offload_size.is_some(),
298+
_ => false,
299+
};
300+
283301
// If a checksum calculation by the host is necessary, we have to inform the host within the header
284302
// see Virtio specification 5.1.6.2
285303
if let Some((ip_header_len, csum_offset)) =
286-
VirtioNetDriver::should_request_checksum(&token.checksums, &mut packet)
304+
VirtioNetDriver::should_request_checksum(&token.checksums, &mut packet, is_large_send)
287305
{
288306
header.flags = HdrF::NEEDS_CSUM;
289307
header.csum_start =
290308
(u16::try_from(ETHERNET_HEADER_LEN).unwrap() + ip_header_len).into();
291309
header.csum_offset = csum_offset.into();
292310
}
293311

312+
#[cfg(feature = "tcp")]
313+
if let Some(gso_size) = token.meta.segmentation_offload_size {
314+
header.gso_type = match EthernetFrame::new_unchecked(&packet).ethertype() {
315+
smoltcp::wire::EthernetProtocol::Ipv4 => virtio::net::HdrGso::Tcpv4.into(),
316+
smoltcp::wire::EthernetProtocol::Ipv6 => virtio::net::HdrGso::Tcpv6.into(),
317+
_ => unreachable!(
318+
"We don't advertise segmentation offload support for any other protocol."
319+
),
320+
};
321+
header.gso_size = gso_size.get().into();
322+
}
323+
294324
let buff_tkn = AvailBufferToken::new(
295325
SmallVec::from_buf([BufferElem::Sized(header), BufferElem::Vector(packet)]),
296326
SmallVec::new(),
@@ -303,6 +333,10 @@ impl smoltcp::phy::TxToken for TxToken<'_> {
303333

304334
result
305335
}
336+
337+
fn set_meta(&mut self, meta: PacketMeta) {
338+
self.meta = meta;
339+
}
306340
}
307341

308342
pub struct RxToken<'a> {
@@ -436,6 +470,22 @@ impl smoltcp::phy::Device for VirtioNetDriver {
436470
device_capabilities.max_burst_size =
437471
Some(usize::try_from(self.inner.send_capacity).unwrap() / usize::from(BUFF_PER_PACKET));
438472
device_capabilities.checksum = self.checksums.clone();
473+
474+
// We only support segmentation offload on and enable the corresponding feature of smoltcp for TCP.
475+
#[cfg(feature = "tcp")]
476+
{
477+
device_capabilities.segmentation.tcpv4 = self
478+
.dev_cfg
479+
.features
480+
.contains(virtio::net::F::HOST_TSO4)
481+
.then_some(NonZeroUsize::new(MAX_BUFFER_SIZE).unwrap());
482+
device_capabilities.segmentation.tcpv6 = self
483+
.dev_cfg
484+
.features
485+
.contains(virtio::net::F::HOST_TSO6)
486+
.then_some(NonZeroUsize::new(MAX_BUFFER_SIZE).unwrap());
487+
}
488+
439489
device_capabilities
440490
}
441491

@@ -463,6 +513,7 @@ impl smoltcp::phy::Device for VirtioNetDriver {
463513
send_vqs: &mut self.inner.send_vqs,
464514
checksums: self.checksums.clone(),
465515
send_capacity: &mut self.inner.send_capacity,
516+
meta: PacketMeta::default(),
466517
},
467518
))
468519
}
@@ -479,6 +530,7 @@ impl smoltcp::phy::Device for VirtioNetDriver {
479530
send_vqs: &mut self.inner.send_vqs,
480531
checksums: self.checksums.clone(),
481532
send_capacity: &mut self.inner.send_capacity,
533+
meta: PacketMeta::default(),
482534
})
483535
}
484536
}
@@ -578,34 +630,40 @@ impl VirtioNetDriver<Init> {
578630
fn should_request_checksum<T: AsRef<[u8]> + AsMut<[u8]>>(
579631
checksums: &ChecksumCapabilities,
580632
frame: T,
633+
is_large_send: bool,
581634
) -> Option<(u16, u16)> {
582635
if checksums.tcp.tx() && checksums.udp.tx() {
583636
return None;
584637
}
585638

586639
let ip_header_len: u16;
587-
let ip_packet_len: usize;
588640
let protocol;
589641
let pseudo_header_checksum;
590642
let mut ethernet_frame = EthernetFrame::new_unchecked(frame);
643+
// We cannot use the `total_len` methods of the packet structures as they read the length
644+
// fields in the headers, which may be set to 0 when the frame is to be segmented and the
645+
// actual length does not fit into the 16 bits of the field.
646+
let ip_packet_len = ethernet_frame.payload_mut().len();
591647
match ethernet_frame.ethertype() {
592648
smoltcp::wire::EthernetProtocol::Ipv4 => {
593649
let ip_packet = Ipv4Packet::new_unchecked(&*ethernet_frame.payload_mut());
594650
ip_header_len = ip_packet.header_len().into();
595-
ip_packet_len = ip_packet.total_len().into();
596651
protocol = ip_packet.next_header();
597-
pseudo_header_checksum =
598-
partial_checksum::ipv4_pseudo_header_partial_checksum(&ip_packet);
652+
pseudo_header_checksum = partial_checksum::ipv4_pseudo_header_partial_checksum(
653+
&ip_packet,
654+
is_large_send,
655+
);
599656
}
600657
smoltcp::wire::EthernetProtocol::Ipv6 => {
601658
let ip_packet = Ipv6Packet::new_unchecked(&*ethernet_frame.payload_mut());
602659
ip_header_len = ip_packet.header_len().try_into().expect(
603660
"VIRTIO does not support IP headers that are longer than u16::MAX bytes.",
604661
);
605-
ip_packet_len = ip_packet.total_len();
606662
protocol = ip_packet.next_header();
607-
pseudo_header_checksum =
608-
partial_checksum::ipv6_pseudo_header_partial_checksum(&ip_packet);
663+
pseudo_header_checksum = partial_checksum::ipv6_pseudo_header_partial_checksum(
664+
&ip_packet,
665+
is_large_send,
666+
);
609667
}
610668
// If the Ethernet protocol is not one of these two above, for which we know there may be a checksum field,
611669
// we default to not asking for checksum, as otherwise the frame will be corrupted by the device trying
@@ -673,7 +731,9 @@ impl VirtioNetDriver<Uninit> {
673731
// Multiqueue support
674732
| virtio::net::F::MQ
675733
// Checksum calculation can partially be offloaded to the device
676-
| virtio::net::F::CSUM;
734+
| virtio::net::F::CSUM
735+
| virtio::net::F::HOST_TSO4
736+
| virtio::net::F::HOST_TSO6;
677737

678738
// Currently the driver does NOT support the features below.
679739
// In order to provide functionality for these, the driver
@@ -949,27 +1009,40 @@ mod partial_checksum {
9491009
/// [RFC 9293 subsection 3.1](https://www.rfc-editor.org/rfc/rfc9293.html#section-3.1-6.18.1) WITHOUT the final inversion.
9501010
pub(super) fn ipv4_pseudo_header_partial_checksum<T: AsRef<[u8]>>(
9511011
packet: &Ipv4Packet<T>,
1012+
is_large_send: bool,
9521013
) -> u16 {
9531014
let padded_protocol = u16::from(u8::from(packet.next_header()));
954-
let payload_len = packet.total_len() - u16::from(packet.header_len());
955-
9561015
let mut sum = addr_sum(&packet.src_addr().octets());
9571016
sum = ones_complement_add(sum, addr_sum(&packet.dst_addr().octets()));
9581017
sum = ones_complement_add(sum, padded_protocol);
959-
ones_complement_add(sum, payload_len)
1018+
if is_large_send {
1019+
// We don't know the packet's length, which will be determined by the device during segmentation.
1020+
// The VIRTIO specification does not specify how partial checksums should be handled for large
1021+
// sends, but excluding the length field is consistent with how
1022+
// [Windows](https://learn.microsoft.com/en-us/windows-hardware/drivers/network/offloading-the-segmentation-of-large-tcp-packets)
1023+
// and [Intel NICs](https://www.intel.com/content/dam/doc/manual/pci-pci-x-family-gbe-controllers-software-dev-manual.pdf)
1024+
// handle this case.
1025+
sum
1026+
} else {
1027+
ones_complement_add(sum, packet.total_len() - u16::from(packet.header_len()))
1028+
}
9601029
}
9611030

9621031
/// Calculates the checksum for the IPv6 pseudo-header as described in
9631032
/// [RFC 8200 subsection 8.1](https://www.rfc-editor.org/rfc/rfc8200.html#section-8.1) WITHOUT the final inversion.
9641033
pub(super) fn ipv6_pseudo_header_partial_checksum<T: AsRef<[u8]>>(
9651034
packet: &Ipv6Packet<T>,
1035+
is_large_send: bool,
9661036
) -> u16 {
9671037
warn!("The IPv6 partial checksum implementation is untested!");
9681038
let padded_protocol = u16::from(u8::from(packet.next_header()));
9691039

9701040
let mut sum = addr_sum(&packet.src_addr().octets());
9711041
sum = ones_complement_add(sum, addr_sum(&packet.dst_addr().octets()));
972-
sum = ones_complement_add(sum, packet.payload_len());
1042+
// Refer to the comment related to `is_large_send` in `ipv4_pseudo_header_partial_checksum`.
1043+
if !is_large_send {
1044+
sum = ones_complement_add(sum, packet.payload_len());
1045+
}
9731046
ones_complement_add(sum, padded_protocol)
9741047
}
9751048

0 commit comments

Comments
 (0)