-
Notifications
You must be signed in to change notification settings - Fork 4
fix(examples): resoudre le LINKTYPE par paquet dans scan_pcaps #78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,12 +12,32 @@ | |
| //! Avec `--focus`, chaque trame détectée comme PROTO est détaillée | ||
| //! (n° de trame, ports, premiers octets du payload) pour auditer les | ||
| //! faux positifs. | ||
| //! | ||
| //! ## Pourquoi ne pas passer par libpcap | ||
| //! | ||
| //! Un pcapng peut décrire **plusieurs interfaces**, chacune avec son propre | ||
| //! LINKTYPE, et chaque paquet référence la sienne par `interface_id`. Lire un | ||
| //! tel fichier avec un unique `get_datalink()` en début de fichier revient à | ||
| //! appliquer le LINKTYPE de la première interface à tous les paquets ; la | ||
| //! lecture s'interrompt au premier changement, et le fichier compte alors zéro | ||
| //! trame sans que rien ne le signale (issue #74). | ||
| //! | ||
| //! `pcap-file` donne accès aux blocs de description d'interface, donc au | ||
| //! LINKTYPE réel de chaque paquet. `packet_parser::parse` prend justement le | ||
| //! `LinkType` en paramètre : c'est l'itération qui était en cause, pas la | ||
| //! crate. | ||
|
|
||
| use packet_parser::{LinkType, parse}; | ||
| use pcap::Capture; | ||
| use pcap_file::pcap::PcapReader; | ||
| use pcap_file::pcapng::{Block, PcapNgReader}; | ||
| use std::collections::BTreeMap; | ||
| use std::fs::File; | ||
| use std::io::{BufReader, Read, Seek, SeekFrom}; | ||
| use std::path::{Path, PathBuf}; | ||
|
|
||
| /// Premier mot d'un Section Header Block, qui identifie un pcapng. | ||
| const PCAPNG_SHB_MAGIC: u32 = 0x0A0D_0D0A; | ||
|
|
||
| fn collect_pcaps(path: &Path, out: &mut Vec<PathBuf>) { | ||
| if path.is_dir() { | ||
| let mut entries: Vec<_> = match std::fs::read_dir(path) { | ||
|
|
@@ -36,6 +56,146 @@ fn collect_pcaps(path: &Path, out: &mut Vec<PathBuf>) { | |
| } | ||
| } | ||
|
|
||
| /// Issue de la lecture d'un fichier. | ||
| /// | ||
| /// `truncated_by` est le point clé : une lecture qui s'arrête sur une erreur | ||
| /// n'est pas une lecture complète, et l'ancienne boucle `while let Ok(..)` | ||
| /// confondait les deux. Un fichier partiellement lu doit le dire. | ||
| #[derive(Default)] | ||
| struct ReadOutcome { | ||
| frames: usize, | ||
| truncated_by: Option<String>, | ||
| } | ||
|
|
||
| /// Applique `on_packet` à chaque paquet du fichier, avec le `LinkType` de | ||
| /// l'interface dont il provient. | ||
| fn for_each_packet( | ||
| path: &Path, | ||
| on_packet: &mut impl FnMut(LinkType, &[u8]), | ||
| ) -> std::io::Result<ReadOutcome> { | ||
| let mut file = BufReader::new(File::open(path)?); | ||
|
|
||
| let mut magic = [0u8; 4]; | ||
| file.read_exact(&mut magic)?; | ||
| file.seek(SeekFrom::Start(0))?; | ||
|
|
||
| // Le SHB est le seul bloc dont le type est fixe et connu d'avance ; tout | ||
| // le reste (pcap classique, quel que soit son boutisme ou sa resolution | ||
| // temporelle) est delegue a PcapReader, qui lit son propre en-tete. | ||
| if u32::from_be_bytes(magic) == PCAPNG_SHB_MAGIC { | ||
| Ok(read_pcapng(file, on_packet)) | ||
| } else { | ||
| Ok(read_pcap(file, on_packet)) | ||
| } | ||
| } | ||
|
|
||
| fn read_pcapng( | ||
| reader: BufReader<File>, | ||
| on_packet: &mut impl FnMut(LinkType, &[u8]), | ||
| ) -> ReadOutcome { | ||
| let mut outcome = ReadOutcome::default(); | ||
|
|
||
| let mut reader = match PcapNgReader::new(reader) { | ||
| Ok(r) => r, | ||
| Err(e) => { | ||
| outcome.truncated_by = Some(format!("en-tete pcapng illisible ({e})")); | ||
| return outcome; | ||
| } | ||
| }; | ||
|
|
||
| // LINKTYPE par interface, indexe par `interface_id`. Une nouvelle section | ||
| // remet la numerotation a zero (RFC pcapng §4.2). | ||
| let mut linktypes: Vec<LinkType> = Vec::new(); | ||
|
|
||
| // `next_block` rend `None` en fin de fichier et `Some(Err)` sur erreur de | ||
| // lecture : ce sont deux choses differentes. Sur erreur, on s'arrete — le | ||
| // parseur sous-jacent ne consomme pas les octets fautifs, donc reessayer | ||
| // bouclerait — mais on le signale au lieu de faire passer une lecture | ||
| // amputee pour une lecture complete. | ||
| while let Some(block) = reader.next_block() { | ||
| let block = match block { | ||
| Ok(block) => block, | ||
| Err(e) => { | ||
| outcome.truncated_by = Some(format!( | ||
| "bloc illisible apres {} trames ({e})", | ||
| outcome.frames | ||
| )); | ||
| break; | ||
| } | ||
| }; | ||
|
|
||
| match block { | ||
| Block::SectionHeader(_) => linktypes.clear(), | ||
| Block::InterfaceDescription(idb) => { | ||
| linktypes.push(LinkType::from(u32::from(idb.linktype))); | ||
| } | ||
| Block::EnhancedPacket(epb) => { | ||
| if let Some(&link_type) = linktypes.get(epb.interface_id as usize) { | ||
| outcome.frames += 1; | ||
| on_packet(link_type, &epb.data); | ||
| } | ||
| } | ||
| // Bloc obsolete (pcapng pre-1.0), conserve par certains outils. | ||
| Block::Packet(pb) => { | ||
| if let Some(&link_type) = linktypes.get(pb.interface_id as usize) { | ||
| outcome.frames += 1; | ||
| on_packet(link_type, &pb.data); | ||
| } | ||
| } | ||
| // Le Simple Packet Block ne porte pas d'`interface_id` : il n'est | ||
| // valide que si la section decrit une seule interface. | ||
| Block::SimplePacket(spb) => { | ||
| if let [link_type] = linktypes[..] { | ||
| outcome.frames += 1; | ||
| on_packet(link_type, &spb.data); | ||
|
Comment on lines
+148
to
+150
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In a valid pcapng section with multiple Interface Description Blocks, a Simple Packet Block is associated with the first interface (interface 0); it does not require the section to contain exactly one interface. Matching only Useful? React with 👍 / 👎. |
||
| } | ||
| } | ||
| _ => {} | ||
| } | ||
| } | ||
|
|
||
| outcome | ||
| } | ||
|
|
||
| fn read_pcap(reader: BufReader<File>, on_packet: &mut impl FnMut(LinkType, &[u8])) -> ReadOutcome { | ||
| let mut outcome = ReadOutcome::default(); | ||
|
|
||
| let mut reader = match PcapReader::new(reader) { | ||
| Ok(r) => r, | ||
| Err(e) => { | ||
| outcome.truncated_by = Some(format!("en-tete pcap illisible ({e})")); | ||
| return outcome; | ||
| } | ||
| }; | ||
|
|
||
| // Un pcap classique n'a qu'un seul LINKTYPE, dans son en-tete de fichier. | ||
| let link_type = LinkType::from(u32::from(reader.header().datalink)); | ||
|
|
||
| // `next_raw_packet` plutot que `next_packet` : ce dernier rejette | ||
| // `orig_len > snap_len` comme invalide, ce qui est trop strict. Une trame | ||
| // tronquee a la capture a legitimement une longueur de fil superieure au | ||
| // snaplen declare, et libpcap l'accepte. Sur le corpus, cette validation | ||
| // faisait tomber une capture de 3 584 trames a 4. On ne lit de toute | ||
| // facon que `data`. | ||
| while let Some(packet) = reader.next_raw_packet() { | ||
| match packet { | ||
| Ok(packet) => { | ||
| outcome.frames += 1; | ||
| on_packet(link_type, &packet.data); | ||
| } | ||
| Err(e) => { | ||
| outcome.truncated_by = Some(format!( | ||
| "paquet illisible apres {} trames ({e})", | ||
| outcome.frames | ||
| )); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| outcome | ||
| } | ||
|
|
||
| fn main() -> Result<(), Box<dyn std::error::Error>> { | ||
| let mut args: Vec<String> = std::env::args().skip(1).collect(); | ||
| let focus = args | ||
|
|
@@ -57,25 +217,22 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { | |
| } | ||
|
|
||
| let mut grand_total: BTreeMap<String, usize> = BTreeMap::new(); | ||
| let mut incomplets = Vec::new(); | ||
|
|
||
| for file in &files { | ||
| let mut cap = match Capture::from_file(file) { | ||
| Ok(c) => c, | ||
| Err(e) => { | ||
| eprintln!("{}: illisible ({e})", file.display()); | ||
| continue; | ||
| } | ||
| }; | ||
|
|
||
| let link_type = LinkType::from(cap.get_datalink().0 as u32); | ||
|
|
||
| let mut tally: BTreeMap<String, usize> = BTreeMap::new(); | ||
| let mut frame_no = 0usize; | ||
| while let Ok(packet) = cap.next_packet() { | ||
|
|
||
| let mut on_packet = |link_type: LinkType, data: &[u8]| { | ||
| frame_no += 1; | ||
| let Ok(flow) = parse(link_type, packet.data) else { | ||
| *tally.entry("<erreur L2>".into()).or_default() += 1; | ||
| continue; | ||
| // L'erreur porte son LINKTYPE : sur une capture multi-interfaces, | ||
| // un total d'erreurs anonyme ne dit pas s'il manque un decodeur de | ||
| // liaison ou si les trames sont reellement corrompues. | ||
| let Ok(flow) = parse(link_type, data) else { | ||
| *tally | ||
| .entry(format!("<erreur L2 lt={link_type}>")) | ||
| .or_default() += 1; | ||
| return; | ||
| }; | ||
|
|
||
| // La détection applicative pertinente est celle du flux le plus | ||
|
|
@@ -114,9 +271,21 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { | |
| } | ||
|
|
||
| *tally.entry(label).or_default() += 1; | ||
| } | ||
| }; | ||
|
|
||
| println!("\n== {} ({frame_no} trames)", file.display()); | ||
| let outcome = match for_each_packet(file, &mut on_packet) { | ||
| Ok(outcome) => outcome, | ||
| Err(e) => { | ||
| eprintln!("{}: illisible ({e})", file.display()); | ||
| continue; | ||
| } | ||
| }; | ||
|
|
||
| println!("\n== {} ({} trames)", file.display(), outcome.frames); | ||
| if let Some(raison) = &outcome.truncated_by { | ||
| println!(" ⚠ lecture incomplete : {raison}"); | ||
| incomplets.push(file.display().to_string()); | ||
| } | ||
| for (proto, count) in &tally { | ||
| println!(" {proto:<12} {count}"); | ||
| *grand_total.entry(proto.clone()).or_default() += count; | ||
|
|
@@ -128,5 +297,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { | |
| println!(" {proto:<12} {count}"); | ||
| } | ||
|
|
||
| // Un total de corpus construit sur des lectures amputees est trompeur : | ||
| // c'est exactement ce que faisait l'ancienne boucle, en silence. | ||
| if !incomplets.is_empty() { | ||
| println!( | ||
| "\n⚠ {} fichier(s) lus incompletement, les totaux ci-dessus sont partiels :", | ||
| incomplets.len() | ||
| ); | ||
| for file in &incomplets { | ||
| println!(" {file}"); | ||
| } | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an Enhanced Packet Block references an out-of-range
interface_id, this branch silently skips it, leavestruncated_byunset, and excludes it from the frame count; the analogous Packet Block branch behaves the same way. Thus a malformed or partially generated pcapng is presented as a complete scan even though packet blocks were discarded, defeating the new incomplete-read reporting; record an error or otherwise mark the outcome incomplete instead.Useful? React with 👍 / 👎.