Skip to content

Commit 48edfd9

Browse files
authored
Merge pull request #2275 from cagatay-y/feat-pcap
feat(network): support packet capture file creation
2 parents b2a28b3 + 33e76ad commit 48edfd9

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
@@ -5,6 +5,8 @@ use smoltcp::iface::{Config, Interface, SocketSet};
55
#[cfg(feature = "net-trace")]
66
use smoltcp::phy::Tracer;
77
use smoltcp::phy::{Device, Medium};
8+
#[cfg(feature = "write-pcap-file")]
9+
use smoltcp::phy::{PcapMode, PcapWriter};
810
#[cfg(feature = "dhcpv4")]
911
use smoltcp::socket::dhcpv4;
1012
#[cfg(feature = "dns")]
@@ -13,6 +15,8 @@ use smoltcp::wire::{EthernetAddress, HardwareAddress, IpCidr, Ipv4Address, Ipv4C
1315

1416
use super::network::{NetworkInterface, NetworkState};
1517
use crate::arch;
18+
#[cfg(feature = "write-pcap-file")]
19+
use crate::drivers::Driver;
1620
use crate::drivers::net::NetworkDriver;
1721

1822
cfg_select! {
@@ -39,19 +43,30 @@ impl<'a> NetworkInterface<'a> {
3943
feature = "rtl8139",
4044
feature = "virtio-net",
4145
) => {
42-
#[cfg_attr(feature = "net-trace", expect(unused_mut))]
46+
#[cfg_attr(any(feature = "net-trace", feature = "write-pcap-file"), expect(unused_mut))]
4347
let Some(mut device) = NETWORK_DEVICE.lock().take() else {
4448
return NetworkState::InitializationFailed;
4549
};
4650
}
4751
_ => {
48-
#[cfg_attr(feature = "net-trace", expect(unused_mut))]
52+
#[cfg_attr(any(feature = "net-trace", feature = "write-pcap-file"), expect(unused_mut))]
4953
let mut device = LoopbackDriver::new();
5054
}
5155
}
5256

5357
let mac = device.get_mac_address();
5458

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

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

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")]
@@ -377,24 +392,21 @@ impl<'a> NetworkInterface<'a> {
377392
feature = "virtio-net",
378393
))]
379394
fn handle_interrupt(&mut self) {
380-
cfg_select! {
381-
feature = "net-trace" => {
382-
self.device.get_mut().handle_interrupt();
383-
}
384-
_ => {
385-
self.device.handle_interrupt();
386-
}
387-
}
395+
self.get_inner_device().handle_interrupt();
388396
}
389397

390398
pub(crate) fn set_polling_mode(&mut self, value: bool) {
391-
cfg_select! {
392-
feature = "net-trace" => {
393-
self.device.get_mut().set_polling_mode(value);
394-
}
395-
_ => {
396-
self.device.set_polling_mode(value);
397-
}
398-
}
399+
self.get_inner_device().set_polling_mode(value);
400+
}
401+
402+
/// Gets the device inside the [smoltcp::phy::Tracer] and [smoltcp::phy::PcapWriter] layers.
403+
fn get_inner_device(&mut self) -> &mut impl NetworkDriver {
404+
let device = &mut self.device;
405+
#[cfg(feature = "net-trace")]
406+
let device = device.get_mut();
407+
#[cfg(feature = "write-pcap-file")]
408+
let device = device.get_mut();
409+
410+
device
399411
}
400412
}

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)