diff --git a/gotatun/src/packet/ipv4/mod.rs b/gotatun/src/packet/ipv4/mod.rs index e79ba953..ed29252b 100644 --- a/gotatun/src/packet/ipv4/mod.rs +++ b/gotatun/src/packet/ipv4/mod.rs @@ -18,7 +18,10 @@ use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, TryFromBytes, Unali mod protocol; pub use protocol::*; -use crate::packet::{DecodeError, Decoder, PseudoHeaderV4, Tcp, TcpDecoder, Udp, UdpDecoder}; +use crate::packet::{ + DecodeError, Decoder, PseudoHeaderV4, Tcp, TcpDecoder, Udp, UdpDecoder, + util::checksum_ipv4_with_skip, +}; use super::util::size_must_be; @@ -434,9 +437,9 @@ impl Ipv4Header { self.flags_and_fragment_offset.fragment_offset() } - /// Compute expected header checksum. + /// Compute expected header checksum. Only valid if IP header contains no optinal fields. pub fn compute_checksum(&self) -> u16 { - crate::packet::util::checksum_ipv4_with_skip(self.as_bytes()) + checksum_ipv4_with_skip(self.as_bytes()) } } @@ -461,12 +464,35 @@ where .map_err(|_| eyre!("IPv4 packet was larger than {}", u16::MAX))?; Ok(()) } + + // /// Compute expected IP header checksum. + // pub fn calculate_ip_checksum(&self) -> u16 { + // self.header.compute_checksum() + // } + + // /// Update IP header checksum field. + // pub fn update_ip_checksum(&mut self) { + // let checksum = self.header.compute_checksum(); + // self.header.header_checksum.set(checksum); + // } } impl

Ipv4> where P: TryFromBytes + Immutable + KnownLayout, { + fn header_bytes(&self) -> eyre::Result<&[u8]> { + let header_len = usize::from(self.header.ihl()) * size_of::(); + + if header_len < Ipv4Header::LEN { + bail!("Invalid IHL"); + }; + + self.as_bytes() + .get(..header_len) + .ok_or(eyre!("IHL larger than header")) + } + fn options_and_payload_bytes(&self) -> eyre::Result<(&[u8], &[u8])> { let header_len = usize::from(self.header.ihl()) * size_of::(); @@ -495,6 +521,18 @@ where let payload = P::try_ref_from_bytes(bytes).map_err(|e| eyre!("{e}"))?; Ok(payload) } + + /// Compute expected IP header checksum. + pub fn calculate_ip_checksum(&self) -> eyre::Result { + Ok(checksum_ipv4_with_skip(self.header_bytes()?)) + } + + /// Update IP header checksum field. + pub fn update_ip_checksum(&mut self) -> eyre::Result<()> { + let checksum = self.calculate_ip_checksum()?; + self.header.header_checksum.set(checksum); + Ok(()) + } } impl Debug for Ipv4Header { diff --git a/gotatun/src/tun/tun_async_device.rs b/gotatun/src/tun/tun_async_device.rs index 4bbaafe8..70023e71 100644 --- a/gotatun/src/tun/tun_async_device.rs +++ b/gotatun/src/tun/tun_async_device.rs @@ -11,8 +11,15 @@ //! Implementations of [`IpSend`] and [`IpRecv`] for the [`tun`] crate. +mod linux; +mod tso; +mod virtio; + +use bytes::BytesMut; use tokio::{sync::watch, time::sleep}; +use tso::try_enable_tso; use tun::AbstractDevice; +use zerocopy::IntoBytes; use crate::{ packet::{Ip, Packet, PacketBufPool}, @@ -20,7 +27,7 @@ use crate::{ tun::{IpRecv, IpSend, MtuWatcher}, }; -use std::{convert::Infallible, io, iter, sync::Arc, time::Duration}; +use std::{convert::Infallible, io, ops::Deref, sync::Arc, time::Duration}; /// Error from [`TunDevice`]. #[derive(Debug, thiserror::Error)] @@ -57,6 +64,8 @@ struct TunDeviceState { /// Task which monitors TUN device MTU. Aborted when dropped. _mtu_monitor: Task, + + vnet_header: bool, } impl TunDevice { @@ -79,10 +88,21 @@ impl TunDevice { tun_config.platform_config(|p| { p.enable_routing(false); }); + + // HACK: always enable tso + #[cfg(target_os = "linux")] + tun_config.platform_config(|p| { + p.vnet_hdr(true); + }); + let vnet_hdr = cfg!(target_os = "linux"); + // TODO: for wintun, must set path or enable signature check // we should upstream to `tun` let tun = tun::create_as_async(&tun_config).map_err(Error::OpenTun)?; - let tun = TunDevice::from_tun_device(tun)?; + // HACK: always enable tso + try_enable_tso(tun.deref()).unwrap(); + let tun = TunDevice::from_tun_device(tun, vnet_hdr)?; + Ok(tun) } @@ -104,7 +124,7 @@ impl TunDevice { } /// Construct from a [`tun::AsyncDevice`]. - pub fn from_tun_device(tun: tun::AsyncDevice) -> Result { + pub fn from_tun_device(tun: tun::AsyncDevice, vnet_header: bool) -> Result { #[cfg(target_os = "linux")] if tun.packet_information() { return Err(Error::UnsupportedFeature("packet_information".to_string())); @@ -140,6 +160,7 @@ impl TunDevice { state: Arc::new(TunDeviceState { mtu: rx.into(), _mtu_monitor: mtu_monitor, + vnet_header, }), }) } @@ -152,11 +173,28 @@ impl TunDevice { impl IpSend for TunDevice { async fn send(&mut self, packet: Packet) -> io::Result<()> { + let mut packet = packet.into_bytes(); + if self.state.vnet_header { + let header = virtio::VirtioNetHeader { + flags: virtio::Flags::new(), + gso_type: virtio::GsoType::VIRTIO_NET_HDR_GSO_NONE, + hdr_len: 0, + gso_size: 0, + csum_start: 0, + csum_offset: 0, + }; + let mut buf = + BytesMut::with_capacity(header.as_bytes().len() + packet.as_bytes().len()); + buf.extend_from_slice(header.as_bytes()); + buf.extend_from_slice(packet.as_bytes()); + *packet.buf_mut() = buf; + } self.tun.send(&packet.into_bytes()).await?; Ok(()) } } +#[cfg(not(any(target_os = "linux", target_os = "android")))] impl IpRecv for TunDevice { async fn recv<'a>( &'a mut self, @@ -175,3 +213,55 @@ impl IpRecv for TunDevice { self.state.mtu.clone() } } + +#[cfg(any(target_os = "linux", target_os = "android"))] +impl IpRecv for TunDevice { + async fn recv<'a>( + &'a mut self, + pool: &mut PacketBufPool, + ) -> io::Result> + 'a> { + use bytes::BytesMut; + use either::Either; + use zerocopy::FromBytes; + + use crate::tun::tun_async_device::virtio::VirtioNetHeader; + + // FIXME: pool buffers have a cap of 4096, but we need more + //let mut packet = pool.get(); + let _ = pool; + + let mut buf = BytesMut::zeroed(usize::from(u16::MAX)); + let n = self.tun.recv(&mut buf).await?; + buf.truncate(n); + + if self.state.vnet_header { + let vnet_hdr = buf.split_to(size_of::()); + let vnet_hdr = *VirtioNetHeader::ref_from_bytes(&vnet_hdr).unwrap(); + + let packet = Packet::from_bytes(buf) + .try_into_ipvx() + .map_err(|e| io::Error::other(e.to_string()))?; + + // TODO + let mtu = 1200; + + // TODO: if segmentation and checksum offload is disabled, + // we could take a more efficient branch where we do not need to check + // packet length, and whether it's an IP/TCP packet. + match packet { + Either::Left(ipv4_packet) => { + tso::new_tso_iter_ipv4(ipv4_packet, usize::from(vnet_hdr.gso_size)) + } + Either::Right(ipv6_packet) => { + tso::new_tso_iter_ipv6(ipv6_packet, usize::from(vnet_hdr.gso_size)) + } + } + } else { + todo!("vnet_hdr is disabled") + } + } + + fn mtu(&self) -> MtuWatcher { + self.state.mtu.clone() + } +} diff --git a/gotatun/src/tun/tun_async_device/linux.rs b/gotatun/src/tun/tun_async_device/linux.rs new file mode 100644 index 00000000..e69de29b diff --git a/gotatun/src/tun/tun_async_device/tso.rs b/gotatun/src/tun/tun_async_device/tso.rs new file mode 100644 index 00000000..7f73e8ab --- /dev/null +++ b/gotatun/src/tun/tun_async_device/tso.rs @@ -0,0 +1,440 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// This file incorporates work covered by the following copyright and +// permission notice: +// +// Copyright (c) Mullvad VPN AB. All rights reserved. +// +// SPDX-License-Identifier: MPL-2.0 + +use crate::packet::{ + Ip, IpNextProtocol, Ipv4, Ipv4Header, Ipv4VersionIhl, Ipv6, Ipv6Header, Packet, Tcp, TcpHeader, +}; +use bytes::BytesMut; +use duplicate::duplicate_item; +use libc::{TUN_F_CSUM, TUN_F_TSO4, TUN_F_TSO6, TUNSETOFFLOAD}; +use std::io; +use std::os::fd::AsRawFd; +use zerocopy::{FromBytes, IntoBytes}; + +/// Enable TCP offloading on the given tun device +/// +/// Returns `EINVAL` if TSO is not supported (pre Linux 2.6) +/// +/// +pub fn try_enable_tso(tun: &impl AsRawFd) -> io::Result<()> { + // TODO: ask the OS what linux version we're running. + let linux_version = (6, 16); + + let offload_flags = match linux_version { + v if v >= (6, 2) => { + TUN_F_CSUM // checksum offload, this is required for TSO + | TUN_F_TSO4 // TCP segmentation offload (IPv4) + | TUN_F_TSO6 // TCP segmentation offload (IPv6) + // TODO: TUN_F_USO4 + // TODO: TUN_F_USO6 + } + + v if v >= (2, 6) => { + TUN_F_CSUM // checksum offload, this is required for TSO + | TUN_F_TSO4 // TCP segmentation offload (IPv4) + | TUN_F_TSO6 // TCP segmentation offload (IPv6) + } + + _ => return Err(io::ErrorKind::InvalidInput.into()), + }; + + let tun_fd = tun.as_raw_fd(); + + // SAFETY: TODO: perfectly safe + let status = unsafe { libc::ioctl(tun_fd, TUNSETOFFLOAD, offload_flags) }; + if status != 0 { + return Err(io::Error::last_os_error()); + } + + Ok(()) +} + +#[duplicate_item( + new_tso_iter_ipvx IpvX IpvXHeader CoalescedIpvX; + [new_tso_iter_ipv4] [Ipv4] [Ipv4Header] [CoalescedIpv4]; + [new_tso_iter_ipv6] [Ipv6] [Ipv6Header] [CoalescedIpv6]; + )] +pub fn new_tso_iter_ipvx(ipvx_packet: Packet, gso_size: usize) -> io::Result { + let packet_len = ipvx_packet.as_bytes().len(); + + match ipvx_packet.header.next_protocol() { + IpNextProtocol::Tcp => { + let mut tcp_packet = ipvx_packet + .try_into_tcp() + .map_err(|e| io::Error::other(e.to_string()))?; + + // TODO: also check gso_type + if 0 < gso_size && gso_size < packet_len { + let tcp_options_len = tcp_packet + .payload + .options() + .expect("We've validated the TCP packet") + .len(); + let header_len = IpvXHeader::LEN + TcpHeader::LEN + tcp_options_len; + + let mut packet = tcp_packet.into_bytes(); + + // Split the giant packet into IP/TCP header and giant payload. The payload + // will be segmented, and the header will be prepended to each segment. + let headers = packet.buf_mut().split_to(header_len); + let payload = packet; + let mut headers = Packet::from_bytes(headers); + + // Update IP header length field + IpvX::::mut_from_bytes(headers.as_mut_bytes()) + .expect("`headers` contains Ip/Tcp headers") + .try_update_ip_len() + .expect("IP packet is not too large"); + + let headers = Packet::::try_from(headers) + .and_then(|headers| headers.try_into_tcp()) + .expect("We're copying valid IP/TCP headers"); + + // Length of the giant payload. + let payload_len = packet_len - header_len; + + // Target size of the segment payloads + // TODO: does gso_size already exclude headers? + let segment_payload_len = gso_size + .checked_sub(header_len) + .unwrap_or_else(|| panic!("gso_size ({gso_size}) must be greater than the length of the IP/TCP headers ({header_len})")); + + // We'll need this many segments + let segment_count = payload_len.div_ceil(segment_payload_len); + + // TODO: segmentation should not block the next tun.read + return Ok(TsoIter::CoalescedIpvX { + // TODO: consider using a pool + buf: BytesMut::with_capacity((header_len + gso_size) * segment_count), + segment_payload_len, + headers, + payload, + i: 0, + }); + } + + tcp_packet.update_tcp_checksum(); + + Ok(TsoIter::SinglePacket { + packet: Some(tcp_packet.into()), + }) + } + _ => Ok(TsoIter::SinglePacket { + packet: Some(ipvx_packet.into()), + }), + } +} + +/// An iterator that segments a large TCP packet into smaller TCP packets. +pub enum TsoIter { + SinglePacket { + packet: Option>, + }, + CoalescedIpv4 { + buf: BytesMut, + + i: usize, + segment_payload_len: usize, + + headers: Packet>, + payload: Packet<[u8]>, + }, + CoalescedIpv6 { + buf: BytesMut, + + i: usize, + segment_payload_len: usize, + + headers: Packet>, + payload: Packet<[u8]>, + }, +} + +impl Iterator for TsoIter { + type Item = Packet; + + fn next(&mut self) -> Option { + match self { + TsoIter::SinglePacket { packet } => packet.take(), + + TsoIter::CoalescedIpv4 { + buf, + i, + segment_payload_len, + headers, + payload, + } => { + if payload.is_empty() { + return None; + } + + // TODO: remove me + if cfg!(debug_assertions) { + tracing::info!( + name: "TSO (v4)", + i=i, + buf_len=buf.len(), + payload_len=payload.len(), + segment_payload_len=segment_payload_len, + ); + } + + let len = payload.len().min(*segment_payload_len); + let segment_payload = payload.buf_mut().split_to(len).freeze(); + + let is_last_segment = payload.is_empty(); + + // Headers from the original TSO packet + let ipv4_header = &headers.header; + let tcp_header = &headers.payload.header; + let tcp_options = headers.payload.options(); + let tcp_options = tcp_options.expect("We've validated the TCP header"); + + let seq_num = (*segment_payload_len).wrapping_mul(*i) as u32; + let seq_num = seq_num.wrapping_add(tcp_header.seq_num.get()); + + // TODO: explain how identification works and why we need to inc it + let identification = ipv4_header.identification.get(); + let identification = identification.wrapping_add(*i as u16); + + let total_len = (const { Ipv4Header::LEN + TcpHeader::LEN } + + tcp_options.len() + + segment_payload.len()) as u16; + + // Use them to construct the headers for this segment + let mut segment_headers = Ipv4 { + header: Ipv4Header { + version_and_ihl: Ipv4VersionIhl::new().with_version(4).with_ihl(5), + dscp_and_ecn: ipv4_header.dscp_and_ecn, + total_len: total_len.into(), + + identification: identification.into(), + + // TODO: handle this field + // we should never receive fragmented ip packets, i *think*. + flags_and_fragment_offset: ipv4_header.flags_and_fragment_offset, + + time_to_live: ipv4_header.time_to_live, + + protocol: IpNextProtocol::Tcp, + header_checksum: 0.into(), + + source_address: ipv4_header.source_address, + destination_address: ipv4_header.destination_address, + }, + payload: TcpHeader { + source_port: tcp_header.source_port, + destination_port: tcp_header.destination_port, + + seq_num: seq_num.into(), + ack_num: tcp_header.ack_num, + + data_offset: tcp_header.data_offset, + flags: tcp_header.flags, + window: tcp_header.window, + checksum: 0.into(), + urgent_pointer: tcp_header.urgent_pointer, + }, + }; + + if !is_last_segment { + segment_headers.payload.set_fin(false); + segment_headers.payload.set_psh(false); + } + + // Copy the data into the large `buf` allocation and split it off into Packet. + buf.extend_from_slice(segment_headers.as_bytes()); + buf.extend_from_slice(tcp_options); + buf.extend_from_slice(&segment_payload); + let packet = Packet::from_bytes(buf.split()); + + let mut packet = packet + .try_into_ipvx() + .map(|either| either.expect_left("The packet is IPv4")) + .and_then(|packet| packet.try_into_tcp()) + .expect("we've correctly initialized the packet"); + + packet.update_tcp_checksum(); + let ip_checksum = packet.header.compute_checksum(); + packet.header.header_checksum.set(ip_checksum); + + *i += 1; + + Some(packet.into()) + } + + TsoIter::CoalescedIpv6 { + buf, + i, + segment_payload_len, + headers, + payload, + } => { + if payload.is_empty() { + return None; + } + + // TODO: remove me + if cfg!(debug_assertions) { + tracing::info!( + name: "TSO (v6)", + i=i, + buf_len=buf.len(), + payload_len=payload.len(), + segment_payload_len=segment_payload_len, + ); + } + + let len = payload.len().min(*segment_payload_len); + let segment_payload = payload.buf_mut().split_to(len).freeze(); + + let is_last_segment = payload.is_empty(); + + // Headers from the original TSO packet + let ipv6_header = &headers.header; + let tcp_header = &headers.payload.header; + let tcp_options = headers.payload.options().unwrap_or_default(); + + let seq_num = (*segment_payload_len).wrapping_mul(*i) as u32; + let seq_num = seq_num.wrapping_add(tcp_header.seq_num.get()); + + // Use them to construct the headers for this segment + let mut segment_headers = Ipv6 { + header: Ipv6Header { + version_traffic_flow: headers.header.version_traffic_flow, + payload_length: (TcpHeader::LEN + + tcp_options.len() + + segment_payload.len()) + .try_into() + .unwrap(), + + next_header: IpNextProtocol::Tcp, + hop_limit: headers.header.hop_limit, + + source_address: ipv6_header.source_address, + destination_address: ipv6_header.destination_address, + }, + + // TODO: deduplicate with CoalescedIpv4 + payload: TcpHeader { + source_port: tcp_header.source_port, + destination_port: tcp_header.destination_port, + + seq_num: seq_num.into(), + ack_num: tcp_header.ack_num, + + data_offset: tcp_header.data_offset, + flags: tcp_header.flags, + window: tcp_header.window, + checksum: 0.into(), + urgent_pointer: tcp_header.urgent_pointer, + }, + }; + + if !is_last_segment { + segment_headers.payload.set_fin(false); + segment_headers.payload.set_psh(false); + } + + // Copy the data into the large `buf` allocation and split it off into Packet. + buf.extend_from_slice(segment_headers.as_bytes()); + buf.extend_from_slice(tcp_options); + buf.extend_from_slice(&segment_payload); + let packet = Packet::from_bytes(buf.split()); + + let mut packet = packet + .try_into_ipvx() + .map(|either| either.expect_right("The packet is IPv6")) + .and_then(|packet| packet.try_into_tcp()) + .expect("we've correctly initialized the packet"); + + packet.update_tcp_checksum(); + + *i += 1; + + Some(packet.into()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::Ipv4Addr; + + #[test] + fn test_tso_split() { + // TODO: test with TCP options + + let tcp = Tcp { + header: TcpHeader { + source_port: 111.into(), + destination_port: 222.into(), + seq_num: 1000.into(), + ack_num: 444.into(), + data_offset: crate::packet::TcpDataOffset::no_options(), + // TODO: more flags? + flags: crate::packet::TcpFlags::new() + .with_syn(true) + .with_ack(true) + .with_fin(true), + window: 555.into(), + checksum: 0.into(), // TODO + urgent_pointer: 666.into(), + }, + options_and_payload: *b"1st segment!\02nd segment!\03rd segment?\0", + }; + + let mut ip = Ipv4 { + header: Ipv4Header { + identification: 1212.into(), + ..Ipv4Header::new_for_length( + Ipv4Addr::new(1, 2, 3, 4), + Ipv4Addr::new(4, 3, 2, 1), + IpNextProtocol::Tcp, + tcp.as_bytes().len().try_into().unwrap(), + ) + }, + payload: tcp, + }; + + let payload_segment_size = 13; + let mtu = payload_segment_size + size_of::>(); + let expected_payloads: Vec = ip + .payload + .options_and_payload + .chunks(payload_segment_size) + .map(|bytes| std::str::from_utf8(bytes).unwrap().to_string()) + .collect(); + + ip.update_ip_checksum(); + + let packet = Packet::copy_from(ip.as_bytes()); + let packet = packet.try_into_ipvx().unwrap().unwrap_left(); + let packet = packet.try_into_tcp().unwrap(); + + let segmented_packets: Vec<_> = new_tso_iter_ipv4(packet.into(), mtu) + .unwrap() + .map(|packet| packet.try_into_ipvx().unwrap().unwrap_left()) + .map(|packet| packet.try_into_tcp().unwrap()) + .collect(); + + println!("tso count: {}", segmented_packets.len()); + for (packet, expected_payload) in segmented_packets.into_iter().zip(expected_payloads) { + let payload = packet.payload.payload().unwrap(); + assert_eq!(payload, expected_payload.as_bytes()); + println!("{:#?}", &*packet); + } + + panic!() // TODO: remove me + } +} diff --git a/gotatun/src/tun/tun_async_device/virtio.rs b/gotatun/src/tun/tun_async_device/virtio.rs new file mode 100644 index 00000000..8138014e --- /dev/null +++ b/gotatun/src/tun/tun_async_device/virtio.rs @@ -0,0 +1,240 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// This file incorporates work covered by the following copyright and +// permission notice: +// +// Copyright (c) Mullvad VPN AB. All rights reserved. +// +// SPDX-License-Identifier: MPL-2.0 + +//! Implementation of the Virtio net header. +//! +//! The header can be enabled on TUN devices using the [libc::IFF_VNET_HDR]-flag, +//! or using [tun::PlatformConfig::vnet_hdr], and enables use of GSO. + +use bitfield_struct::bitfield; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned}; + +/// See [module](self) docs. +/// +/// Definition in linux include/uapi/linux/virtio_net.h +#[repr(C)] +#[derive(Debug, FromBytes, IntoBytes, KnownLayout, Unaligned, Immutable, PartialEq, Eq)] +pub struct VirtioNetPacket { + pub header: VirtioNetHeader, + pub payload: Payload, +} + +/// See [module](self) docs. +/// +/// Definition in linux include/uapi/linux/virtio_net.h +#[repr(C, packed)] +#[derive( + Clone, Copy, Debug, FromBytes, IntoBytes, KnownLayout, Unaligned, Immutable, PartialEq, Eq, +)] +pub struct VirtioNetHeader { + pub flags: Flags, + + pub gso_type: GsoType, + + /// Ethernet + IP + tcp/udp headers + pub hdr_len: u16, + + /// Bytes to append to `hdr_len` per frame + pub gso_size: u16, + + /// Position to start checksumming from + pub csum_start: u16, + + /// Offset after that to place checksum + pub csum_offset: u16, +} + +/// A field of [VirtioNetHeader]. +#[repr(transparent)] +#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Unaligned, Immutable, PartialEq, Eq)] +pub struct GsoType(u8); + +impl GsoType { + /// Not a GSO frame + pub const VIRTIO_NET_HDR_GSO_NONE: GsoType = GsoType(0); + + /// GSO frame, IPv4 TCP (TSO) + pub const VIRTIO_NET_HDR_GSO_TCPV4: GsoType = GsoType(1); + + /// GSO frame, IPv4 UDP (UFO) + pub const VIRTIO_NET_HDR_GSO_UDP: GsoType = GsoType(3); + + /// GSO frame, IPv6 TCP + pub const VIRTIO_NET_HDR_GSO_TCPV6: GsoType = GsoType(4); + + /// GSO frame, IPv4& IPv6 UDP (USO) + pub const VIRTIO_NET_HDR_GSO_UDP_L4: GsoType = GsoType(5); + + /// TCP has ECN set + pub const VIRTIO_NET_HDR_GSO_ECN: GsoType = GsoType(0x80); +} + +impl std::fmt::Debug for GsoType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let name = match *self { + GsoType::VIRTIO_NET_HDR_GSO_NONE => "VIRTIO_NET_HDR_GSO_NONE ", + GsoType::VIRTIO_NET_HDR_GSO_TCPV4 => "VIRTIO_NET_HDR_GSO_TCPV4 ", + GsoType::VIRTIO_NET_HDR_GSO_UDP => "VIRTIO_NET_HDR_GSO_UDP", + GsoType::VIRTIO_NET_HDR_GSO_TCPV6 => "VIRTIO_NET_HDR_GSO_TCPV6", + GsoType::VIRTIO_NET_HDR_GSO_UDP_L4 => "VIRTIO_NET_HDR_GSO_UDP_L4", + GsoType::VIRTIO_NET_HDR_GSO_ECN => "VIRTIO_NET_HDR_GSO_ECN", + GsoType(..) => "UNKNOWN_GSO_TYPE", + }; + + f.debug_tuple(name).field(&self.0).finish() + } +} + +/// A field of [VirtioNetHeader]. +#[bitfield(u8)] +#[derive(FromBytes, IntoBytes, KnownLayout, Unaligned, Immutable, PartialEq, Eq)] +pub struct Flags { + /// Use csum_start, csum_offset + pub needs_csum: bool, + /// Csum is valid + pub data_valid: bool, + /// rsc info in csum_ fields + pub rsc_info: bool, + + #[bits(5)] + _reserved: u8, +} + +// TODO: handle VIRTIO_NET_HDR_GSO_NONE and VIRTIO_NET_HDR_F_NEEDS_CSUM + +/* + * TODO: + * +/// Split big packet with multiple segments into independent IP packets +/// +/// The unsegmented packet is a VirtioNetHeader followed by an IP header, and a TCP header. +/// Followed by GSO segment-sized (specified in VirtioNetHeader) TCP segments. +fn gso_split(packet: &mut VirtioNetPacket<[u8]>) { + let hdr = &packet.header; + let payload = &mut packet.payload; + + let ip = Ip::try_ref_from_bytes(payload).unwrap(); + + // Clear IPv4 checksum + if ip.header.version() == 4 { + let ipv4 = Ipv4::<[u8]>::mut_from_bytes(payload).unwrap(); + ipv4.header.header_checksum = 0.into(); + let mut ipv4_id ip.header.identification; + } else { + panic!("no IPv6"); + } + + // Check GSO type (UDP or TCP) + // And clear TCP/UDP checksum + match hdr.gso_type { + GsoType::VIRTIO_NET_HDR_GSO_TCPV4 | GsoType::VIRTIO_NET_HDR_GSO_TCPV6 => { + // FIXME: IPv6 + let tcp = Ipv4::::mut_from_bytes(payload).unwrap(); + tcp.header.header_checksum = 0.into(); + } + GsoType::VIRTIO_NET_HDR_GSO_UDP => { + let udp = Ipv4::::mut_from_bytes(payload).unwrap(); + udp.header.header_checksum = 0.into(); + } + // FIXME: handle VIRTIO_NET_HDR_GSO_UDP_L4 + // see https://github.com/WireGuard/wireguard-go/blob/f333402bd9cbe0f3eeb02507bd14e23d7d639280/tun/tun_linux.go#L421 + // TODO: Is it actually unreachable? + _ => unreachable!(), + }; + let hdr = &packet.header; + let payload = &mut packet.payload; + + let ip = Ip::try_ref_from_bytes(payload).unwrap(); + + // Clear IPv4 checksum + if ip.header.version() == 4 { + let ipv4 = Ipv4::<[u8]>::mut_from_bytes(payload).unwrap(); + ipv4.header.header_checksum = 0.into(); + let mut ipv4_id ip.header.identification; + } else { + panic!("no IPv6"); + } + + // Check GSO type (UDP or TCP) + // And clear TCP/UDP checksum + match hdr.gso_type { + GsoType::VIRTIO_NET_HDR_GSO_TCPV4 | GsoType::VIRTIO_NET_HDR_GSO_TCPV6 => { + // FIXME: IPv6 + let tcp = Ipv4::::mut_from_bytes(payload).unwrap(); + tcp.header.header_checksum = 0.into(); + } + GsoType::VIRTIO_NET_HDR_GSO_UDP => { + let udp = Ipv4::::mut_from_bytes(payload).unwrap(); + udp.header.header_checksum = 0.into(); + } + // FIXME: handle VIRTIO_NET_HDR_GSO_UDP_L4 + // see https://github.com/WireGuard/wireguard-go/blob/f333402bd9cbe0f3eeb02507bd14e23d7d639280/tun/tun_linux.go#L421 + // TODO: Is it actually unreachable? + _ => unreachable!(), + }; + + let ip_header_len = usize::from(hdr.csum_start); + let ip_header = &payload[..ip_header_len]; + let transport_header_end = usize::from(hdr.hdr_len); + let transport_header_len = usize::from(hdr.hdr_len) - ip_header_len; + let transport_header = &payload[ip_header_len..transport_header_end]; + + let first_segment_index = ip_header_len + usize::from(hdr.csum_offset); + + let (header, segments) = payload.split_at_mut(first_segment_index); + + for (i, segment) in segments.chunks(usize::from(hdr.gso_size)).enumerate() { + let mut out = BytesMut::new(); + + // copy the IP header, plus the TCP or UDP header that follows. + out.extend_from_slice(&header[..usize::from(hdr.hdr_len)]); + out.extend_from_slice(segment); + + // FIXME: + // IPv6: Set payload length field + + match (ip_version, proto) { + (4, tcp) => { + // IPv4: Increment ID field, set total length, compute checksum + + // TODO: Do we need to care about ipv4 options or ipv6 extra headers? + let segment = Ipv4::::mut_from_bytes(&mut out) + .expect("header + segment should be large enough"); + + // TODO: improve + ipv4_id += 1; + segment.header.identification = ipv4_id; + + + // TCP: Set sequence and flags + // TODO: update TCP sequence number + let tcp_header = &mut segment..payload.header; + tcp_header.seq_num = first_tcp_seq_num + gso_size * i; + + if !last_tcp_segment { + tcp_header.set_fin(false); + tcp_header.set_psh(false); + } + // TODO: update TCP flags + // - only the last segmented packet may have FIN and PSH set. + } + (_, udp) => { + // UDP: Set header length field + } + } + + out.extend_from_slice(&payload[..usize::from(hdr.hdr_len)]); + out.extend_from_slice(segment); + // TODO: Compute UDP/TCP checksum + + } +} +*/