Skip to content

Commit 33e76ad

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 33e76ad

4 files changed

Lines changed: 160 additions & 24 deletions

File tree

Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,15 @@ 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 the generation of packet capture files.
168+
##
169+
## The guest path is specified via the `HERMIT_PCAP_PATH` *runtime* environment variable. If a
170+
## complete path is not provided, values without a `/` are interpreted as file names and ones that
171+
## end with a `/` as directories. The default directory is `/root/` and the default file name is the
172+
## network device name. If a file already exists at the path, the file name is suffixed with
173+
## successive numbers.
174+
write-pcap-file = []
175+
167176
#! ### Network Drivers
168177
#!
169178
#! Currently, Hermit does not support multiple network drivers at the same time.

src/executor/device.rs

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use smoltcp::iface::{Config, Interface, SocketSet};
66
#[cfg(feature = "net-trace")]
77
use smoltcp::phy::Tracer;
88
use smoltcp::phy::{Device, Medium};
9+
#[cfg(feature = "write-pcap-file")]
10+
use smoltcp::phy::{PcapMode, PcapWriter};
911
#[cfg(feature = "dhcpv4")]
1012
use smoltcp::socket::dhcpv4;
1113
#[cfg(all(feature = "dns", not(feature = "dhcpv4")))]
@@ -16,6 +18,8 @@ use smoltcp::wire::{IpCidr, Ipv4Address, Ipv4Cidr};
1618

1719
use super::network::{NetworkInterface, NetworkState};
1820
use crate::arch;
21+
#[cfg(feature = "write-pcap-file")]
22+
use crate::drivers::Driver;
1923
use crate::drivers::net::NetworkDriver;
2024

2125
cfg_select! {
@@ -42,19 +46,30 @@ impl<'a> NetworkInterface<'a> {
4246
feature = "rtl8139",
4347
feature = "virtio-net",
4448
) => {
45-
#[cfg_attr(feature = "net-trace", expect(unused_mut))]
49+
#[cfg_attr(any(feature = "net-trace", feature = "write-pcap-file"), expect(unused_mut))]
4650
let Some(mut device) = NETWORK_DEVICE.lock().take() else {
4751
return NetworkState::InitializationFailed;
4852
};
4953
}
5054
_ => {
51-
#[cfg_attr(feature = "net-trace", expect(unused_mut))]
55+
#[cfg_attr(any(feature = "net-trace", feature = "write-pcap-file"), expect(unused_mut))]
5256
let mut device = LoopbackDriver::new();
5357
}
5458
}
5559

5660
let mac = device.get_mac_address();
5761

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

@@ -132,3 +147,102 @@ impl<'a> NetworkInterface<'a> {
132147
}))
133148
}
134149
}
150+
151+
#[cfg(feature = "write-pcap-file")]
152+
pub(in crate::executor) mod pcap_writer {
153+
use core::fmt::Write as _;
154+
155+
use embedded_io::Write as _;
156+
use smoltcp::phy::PcapSink;
157+
158+
use crate::errno::Errno;
159+
use crate::fs::File;
160+
161+
/// Sink for packet captures. If the file Option is None, the writes are ignored.
162+
/// This is useful when we fail to create the sink file at runtime.
163+
pub struct FileSink(Option<File>);
164+
165+
impl FileSink {
166+
pub(super) fn new(device_name: &str) -> Self {
167+
let (parent, file_prefix, extension) = parse_path(device_name);
168+
let mut path = format!("{parent}/{file_prefix}");
169+
let base_len = path.len();
170+
for i in 1.. {
171+
if let Some(extension) = extension {
172+
path.push('.');
173+
path.push_str(extension);
174+
}
175+
match File::create_new(path.as_str()) {
176+
Ok(file) => {
177+
info!("The packet capture will be written to '{path}'.");
178+
return Self(Some(file));
179+
}
180+
Err(Errno::Exist) => {
181+
path.truncate(base_len);
182+
write!(&mut path, " ({i})").unwrap();
183+
}
184+
Err(e) => {
185+
if e == Errno::Noent {
186+
error!("'{parent}/' does not exist. Is it mounted?");
187+
}
188+
error!(
189+
"Error {e:?} encountered while creating the pcap file. No pcap file will be written."
190+
);
191+
break;
192+
}
193+
}
194+
}
195+
Self(None)
196+
}
197+
}
198+
199+
fn parse_path(device_name: &str) -> (&str, &str, Option<&str>) {
200+
let mut parent = "/root";
201+
let mut file_prefix = device_name;
202+
let mut extension = Some("pcap");
203+
204+
if let Some(path) = crate::env::var("HERMIT_PCAP_PATH").filter(|var| !var.is_empty()) {
205+
let file_name = if let Some((l, r)) = path.rsplit_once('/') {
206+
parent = l;
207+
if r.is_empty() { None } else { Some(r) }
208+
} else {
209+
Some(path.as_str())
210+
};
211+
212+
if let Some(file_name) = file_name {
213+
(file_prefix, extension) = if let Some((l, r)) = file_name.rsplit_once('.')
214+
&& !l.is_empty()
215+
{
216+
(l, Some(r))
217+
} else {
218+
(file_name, None)
219+
}
220+
};
221+
}
222+
(parent, file_prefix, extension)
223+
}
224+
225+
impl PcapSink for FileSink {
226+
fn write(&mut self, data: &[u8]) {
227+
let Some(file) = self.0.as_mut() else {
228+
trace!("No file to write packet capture.");
229+
return;
230+
};
231+
232+
if let Err(err) = file.write(data) {
233+
error!("Error while writing to the pcap file: {err}");
234+
}
235+
}
236+
237+
fn flush(&mut self) {
238+
let Some(file) = self.0.as_mut() else {
239+
trace!("No file to write packet capture.");
240+
return;
241+
};
242+
243+
if let Err(err) = file.flush() {
244+
error!("Error while flushing the pcap file: {err}");
245+
}
246+
}
247+
}
248+
}

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 MaybePcapDevice = 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 MaybeTracerDevice = cfg_select! {
78+
feature = "net-trace" => {
79+
smoltcp::phy::Tracer<MaybePcapDevice>
80+
}
81+
_ => MaybePcapDevice,
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: MaybeTracerDevice,
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)