Skip to content

Commit 4876b5b

Browse files
cagatay-ymkroening
andcommitted
feat(network): support packet capture file creation
Allows creating packet capture files in the pcap format. Co-authored-by: Martin Kröning <mkroening@posteo.net>
1 parent 5127c77 commit 4876b5b

5 files changed

Lines changed: 134 additions & 30 deletions

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ jobs:
4545
run: |
4646
cargo clippy --package hermit-kernel --target x86_64-unknown-none --features tcp,virtio-console,virtio-fs,virtio-net,virtio-vsock
4747
cargo clippy --package hermit-kernel --target x86_64-unknown-none --no-default-features --features tcp,virtio-console,virtio-fs,virtio-net,virtio-vsock
48-
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
49-
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
48+
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
49+
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
5050
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
5151
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
5252
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
@@ -55,8 +55,8 @@ jobs:
5555
run: |
5656
cargo clippy --package hermit-kernel --target aarch64-unknown-none-softfloat --features tcp,virtio-console,virtio-fs,virtio-net,virtio-vsock
5757
cargo clippy --package hermit-kernel --target aarch64-unknown-none-softfloat --no-default-features --features tcp,virtio-console,virtio-fs,virtio-net,virtio-vsock
58-
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
59-
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
58+
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
59+
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
6060
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
6161
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
6262
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
@@ -65,8 +65,8 @@ jobs:
6565
run: |
6666
cargo clippy --package hermit-kernel --target riscv64gc-unknown-none-elf --features tcp,virtio-console,virtio-fs,virtio-net,virtio-vsock
6767
cargo clippy --package hermit-kernel --target riscv64gc-unknown-none-elf --no-default-features --features tcp,virtio-console,virtio-fs,virtio-net,virtio-vsock
68-
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
69-
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
68+
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
69+
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
7070
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
7171
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
7272
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: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ 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+
smoltcp::phy::{PcapMode, PcapWriter},
20+
};
1621

1722
use super::network::{NetworkInterface, NetworkState};
1823
use crate::arch;
@@ -42,19 +47,30 @@ impl<'a> NetworkInterface<'a> {
4247
feature = "rtl8139",
4348
feature = "virtio-net",
4449
) => {
45-
#[cfg_attr(feature = "net-trace", expect(unused_mut))]
50+
#[cfg_attr(any(feature = "net-trace", feature = "write-pcap-file"), expect(unused_mut))]
4651
let Some(mut device) = NETWORK_DEVICE.lock().take() else {
4752
return NetworkState::InitializationFailed;
4853
};
4954
}
5055
_ => {
51-
#[cfg_attr(feature = "net-trace", expect(unused_mut))]
56+
#[cfg_attr(any(feature = "net-trace", feature = "write-pcap-file"), expect(unused_mut))]
5257
let mut device = LoopbackDriver::new();
5358
}
5459
}
5560

5661
let mac = device.get_mac_address();
5762

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

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

src/executor/network.rs

Lines changed: 33 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,24 @@ static LOCAL_ENDPOINT: AtomicU16 = AtomicU16::new(0);
6367
pub(crate) static NIC: InterruptTicketMutex<NetworkState<'_>> =
6468
InterruptTicketMutex::new(NetworkState::Missing);
6569

70+
type InnerDevice = cfg_select! {
71+
feature = "write-pcap-file" => {
72+
smoltcp::phy::PcapWriter<NetworkDevice, crate::executor::device::pcap_writer::FileSink>
73+
}
74+
_ => NetworkDevice,
75+
};
76+
77+
type InterfaceDevice = cfg_select! {
78+
feature = "net-trace" => {
79+
smoltcp::phy::Tracer<InnerDevice>
80+
}
81+
_ => InnerDevice,
82+
};
83+
6684
pub(crate) struct NetworkInterface<'a> {
6785
pub(super) iface: smoltcp::iface::Interface,
6886
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,
87+
pub(super) device: InterfaceDevice,
7388
#[cfg(feature = "dhcpv4")]
7489
pub(super) dhcp_handle: SocketHandle,
7590
#[cfg(feature = "dns")]
@@ -368,24 +383,21 @@ impl<'a> NetworkInterface<'a> {
368383
feature = "virtio-net",
369384
))]
370385
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-
}
386+
self.get_inner_device().handle_interrupt();
379387
}
380388

381389
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-
}
390+
self.get_inner_device().set_polling_mode(value);
391+
}
392+
393+
/// Gets the device inside the [smoltcp::phy::Tracer] and [smoltcp::phy::PcapWriter] layers.
394+
fn get_inner_device(&mut self) -> &mut impl NetworkDriver {
395+
let device = &mut self.device;
396+
#[cfg(feature = "net-trace")]
397+
let device = device.get_mut();
398+
#[cfg(feature = "write-pcap-file")]
399+
let device = device.get_mut();
400+
401+
device
390402
}
391403
}

src/lib.rs

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

185185
// Initialize Drivers
186186
drivers::init();
187+
// The filesystem needs to be initialized before network to allow writing packet captures to a file.
188+
fs::init();
187189
crate::executor::init();
188190

189191
syscalls::init();
190-
fs::init();
191192
#[cfg(feature = "shell")]
192193
shell::init();
193194

0 commit comments

Comments
 (0)