Skip to content

Commit 5107172

Browse files
committed
feat(network): support packet capture file creation
Allows creating packet capture files in the pcap format.
1 parent 15411f4 commit 5107172

5 files changed

Lines changed: 128 additions & 30 deletions

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,24 +42,24 @@ jobs:
4242
- run: rustup component add clippy
4343
- name: cargo hack clippy (x86_64)
4444
run: |
45-
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target x86_64-unknown-none --exclude-features dhcpv4,dns,gem-net,net,rtl8139,virtio-net
46-
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target x86_64-unknown-none --exclude-features dhcpv4,dns,gem-net,net,rtl8139,virtio-net --features pci
45+
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target x86_64-unknown-none --exclude-features dhcpv4,dns,gem-net,net,rtl8139,virtio-net,write-pcap-file
46+
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target x86_64-unknown-none --exclude-features dhcpv4,dns,gem-net,net,rtl8139,virtio-net,write-pcap-file --features pci
4747
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target x86_64-unknown-none --exclude-features gem-net,rtl8139 --features tcp,virtio-net
4848
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target x86_64-unknown-none --exclude-features gem-net,rtl8139 --features pci,tcp,virtio-net
4949
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target x86_64-unknown-none --exclude-features gem-net,virtio-net --features tcp,rtl8139
5050
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target x86_64-unknown-none --exclude-features gem-net,virtio-net --features pci,tcp,rtl8139
5151
- name: cargo hack clippy (aarch64)
5252
run: |
53-
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target aarch64-unknown-none-softfloat --exclude-features dhcpv4,dns,gem-net,net,rtl8139,virtio-net
54-
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target aarch64-unknown-none-softfloat --exclude-features dhcpv4,dns,gem-net,net,rtl8139,virtio-net --features pci
53+
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target aarch64-unknown-none-softfloat --exclude-features dhcpv4,dns,gem-net,net,rtl8139,virtio-net,write-pcap-file
54+
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target aarch64-unknown-none-softfloat --exclude-features dhcpv4,dns,gem-net,net,rtl8139,virtio-net,write-pcap-file --features pci
5555
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target aarch64-unknown-none-softfloat --exclude-features gem-net,rtl8139 --features tcp,virtio-net
5656
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target aarch64-unknown-none-softfloat --exclude-features gem-net,rtl8139 --features pci,tcp,virtio-net
5757
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target aarch64-unknown-none-softfloat --exclude-features gem-net,virtio-net --features tcp,rtl8139
5858
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target aarch64-unknown-none-softfloat --exclude-features gem-net,virtio-net --features pci,tcp,rtl8139
5959
- name: cargo hack clippy (riscv64)
6060
run: |
61-
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target riscv64gc-unknown-none-elf --exclude-features dhcpv4,dns,gem-net,net,rtl8139,virtio-net
62-
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target riscv64gc-unknown-none-elf --exclude-features dhcpv4,dns,gem-net,net,rtl8139,virtio-net --features pci
61+
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target riscv64gc-unknown-none-elf --exclude-features dhcpv4,dns,gem-net,net,rtl8139,virtio-net,write-pcap-file
62+
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target riscv64gc-unknown-none-elf --exclude-features dhcpv4,dns,gem-net,net,rtl8139,virtio-net,write-pcap-file --features pci
6363
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target riscv64gc-unknown-none-elf --exclude-features gem-net,rtl8139 --features tcp,virtio-net
6464
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target riscv64gc-unknown-none-elf --exclude-features gem-net,rtl8139 --features pci,tcp,virtio-net
6565
cargo hack clippy --package hermit-kernel --each-feature --no-dev-deps --target riscv64gc-unknown-none-elf --exclude-features rtl8139,virtio-net --features tcp,gem-net

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,12 @@ dns = ["net", "smoltcp", "smoltcp/socket-dns"]
164164
## Enables pretty-printing the network traffic to the serial device.
165165
net-trace = ["smoltcp?/log", "smoltcp?/verbose"]
166166

167+
## Enables generation of packet capture files. The files are placed under the
168+
## mount point on the host, with the name provided via the HERMIT_PCAP_NAME environment
169+
## variable, or the device name by default.
170+
write-pcap-file = []
171+
172+
167173
#! ### Network Drivers
168174
#!
169175
#! Currently, Hermit does not support multiple network drivers at the same time.

src/executor/device.rs

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ use smoltcp::socket::dns;
1313
use smoltcp::wire::{EthernetAddress, HardwareAddress};
1414
#[cfg(not(feature = "dhcpv4"))]
1515
use smoltcp::wire::{IpCidr, Ipv4Address, Ipv4Cidr};
16+
#[cfg(feature = "write-pcap-file")]
17+
use {
18+
crate::drivers::Driver,
19+
crate::fs::File,
20+
embedded_io::Write,
21+
smoltcp::phy::{PcapMode, PcapSink, PcapWriter},
22+
};
1623

1724
use super::network::{NetworkInterface, NetworkState};
1825
use crate::arch;
@@ -42,19 +49,26 @@ impl<'a> NetworkInterface<'a> {
4249
feature = "rtl8139",
4350
feature = "virtio-net",
4451
) => {
45-
#[cfg_attr(feature = "net-trace", expect(unused_mut))]
52+
#[cfg_attr(any(feature = "net-trace", feature = "write-pcap-file"), expect(unused_mut))]
4653
let Some(mut device) = NETWORK_DEVICE.lock().take() else {
4754
return NetworkState::InitializationFailed;
4855
};
4956
}
5057
_ => {
51-
#[cfg_attr(feature = "net-trace", expect(unused_mut))]
58+
#[cfg_attr(any(feature = "net-trace", feature = "write-pcap-file"), expect(unused_mut))]
5259
let mut device = LoopbackDriver::new();
5360
}
5461
}
5562

5663
let mac = device.get_mac_address();
5764

65+
#[cfg_attr(feature = "net-trace", expect(unused_mut))]
66+
#[cfg(feature = "write-pcap-file")]
67+
let mut device = {
68+
let default_name = device.get_name();
69+
PcapWriter::new(device, FileSink::new(default_name), PcapMode::Both)
70+
};
71+
5872
#[cfg(feature = "net-trace")]
5973
let mut device = Tracer::new(device, |timestamp, printer| trace!("{timestamp} {printer}"));
6074

@@ -132,3 +146,67 @@ impl<'a> NetworkInterface<'a> {
132146
}))
133147
}
134148
}
149+
150+
#[cfg(feature = "write-pcap-file")]
151+
/// Sink for packet captures. If the file Option is None, the writes are ignored.
152+
/// This is useful when we fail to create the sink file at runtime.
153+
pub(in crate::executor) struct FileSink(Option<File>);
154+
155+
#[cfg(feature = "write-pcap-file")]
156+
impl FileSink {
157+
fn new(default_name: &str) -> Self {
158+
use core::ops::ControlFlow;
159+
160+
use crate::errno::Errno;
161+
162+
let file_name_base = option_env!("HERMIT_PCAP_NAME").unwrap_or(default_name);
163+
let mut file_name = format!("{file_name_base}.pcap");
164+
let file = (1..)
165+
.try_for_each(|i| {
166+
let path = format!("/root/{file_name}");
167+
match File::create_new(path.as_str()) {
168+
Err(Errno::Exist) => {
169+
file_name = format!("{file_name_base} ({i}).pcap");
170+
ControlFlow::Continue(())
171+
}
172+
r => ControlFlow::Break(r),
173+
}
174+
})
175+
.break_value()
176+
.unwrap();
177+
178+
if let Err(e) = file {
179+
if e == Errno::Noent {
180+
error!("/root is not mounted. Are there any mount points for the VM?");
181+
}
182+
error!(
183+
"Error {e:?} encountered while creating the pcap file. No pcap file will be written."
184+
);
185+
} else {
186+
info!(
187+
"The packet capture will be written to a file called \"{file_name}\" under the mount point."
188+
);
189+
}
190+
191+
FileSink(file.ok())
192+
}
193+
}
194+
195+
#[cfg(feature = "write-pcap-file")]
196+
impl PcapSink for FileSink {
197+
fn write(&mut self, data: &[u8]) {
198+
if let Some(file) = self.0.as_mut()
199+
&& let Some(err) = file.write(data).err()
200+
{
201+
error!("Error while writing to the pcap file: {err}");
202+
}
203+
}
204+
205+
fn flush(&mut self) {
206+
if let Some(file) = self.0.as_mut()
207+
&& let Some(err) = file.flush().err()
208+
{
209+
error!("Error while flushing the pcap file: {err}");
210+
}
211+
}
212+
}

src/executor/network.rs

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ pub(crate) enum NetworkState<'a> {
4545
feature = "virtio-net",
4646
))]
4747
pub(crate) fn network_handler() {
48-
NIC.lock().as_nic_mut().unwrap().handle_interrupt();
48+
// It is possible for us to receive interrupts before we are done with initializing the network interface.
49+
// This may for example be caused by an interrupt that was meant for the filesystem driver.
50+
if let Ok(nic) = NIC.lock().as_nic_mut() {
51+
nic.handle_interrupt();
52+
}
4953
}
5054

5155
impl<'a> NetworkState<'a> {
@@ -63,13 +67,25 @@ static LOCAL_ENDPOINT: AtomicU16 = AtomicU16::new(0);
6367
pub(crate) static NIC: InterruptTicketMutex<NetworkState<'_>> =
6468
InterruptTicketMutex::new(NetworkState::Missing);
6569

70+
type InterfaceDevice = cfg_select! {
71+
all(feature = "net-trace", feature = "write-pcap-file") => {
72+
smoltcp::phy::Tracer<smoltcp::phy::PcapWriter<NetworkDevice, crate::executor::device::FileSink>>
73+
}
74+
feature = "net-trace" => {
75+
smoltcp::phy::Tracer<NetworkDevice>
76+
}
77+
feature = "write-pcap-file" => {
78+
smoltcp::phy::PcapWriter<NetworkDevice, crate::executor::device::FileSink>
79+
}
80+
_ => {
81+
NetworkDevice
82+
}
83+
};
84+
6685
pub(crate) struct NetworkInterface<'a> {
6786
pub(super) iface: smoltcp::iface::Interface,
6887
pub(super) sockets: SocketSet<'a>,
69-
#[cfg(feature = "net-trace")]
70-
pub(super) device: smoltcp::phy::Tracer<NetworkDevice>,
71-
#[cfg(not(feature = "net-trace"))]
72-
pub(super) device: NetworkDevice,
88+
pub(super) device: InterfaceDevice,
7389
#[cfg(feature = "dhcpv4")]
7490
pub(super) dhcp_handle: SocketHandle,
7591
#[cfg(feature = "dns")]
@@ -368,24 +384,21 @@ impl<'a> NetworkInterface<'a> {
368384
feature = "virtio-net",
369385
))]
370386
fn handle_interrupt(&mut self) {
371-
cfg_select! {
372-
feature = "net-trace" => {
373-
self.device.get_mut().handle_interrupt();
374-
}
375-
_ => {
376-
self.device.handle_interrupt();
377-
}
378-
}
387+
self.get_inner_device().handle_interrupt();
379388
}
380389

381390
pub(crate) fn set_polling_mode(&mut self, value: bool) {
382-
cfg_select! {
383-
feature = "net-trace" => {
384-
self.device.get_mut().set_polling_mode(value);
385-
}
386-
_ => {
387-
self.device.set_polling_mode(value);
388-
}
389-
}
391+
self.get_inner_device().set_polling_mode(value);
392+
}
393+
394+
/// Gets the device inside the [smoltcp::phy::Tracer] and [smoltcp::phy::PcapWriter] layers.
395+
fn get_inner_device(&mut self) -> &mut impl NetworkDriver {
396+
let device = &mut self.device;
397+
#[cfg(feature = "net-trace")]
398+
let device = device.get_mut();
399+
#[cfg(feature = "write-pcap-file")]
400+
let device = device.get_mut();
401+
402+
device
390403
}
391404
}

src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,11 @@ extern "C" fn initd(_arg: usize) {
175175

176176
// Initialize Drivers
177177
drivers::init();
178+
// The filesystem needs to be initialized before network to allow writing packet captures to a file.
179+
fs::init();
178180
crate::executor::init();
179181

180182
syscalls::init();
181-
fs::init();
182183
#[cfg(feature = "shell")]
183184
shell::init();
184185

0 commit comments

Comments
 (0)