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
+
+ }
+}
+*/