//! PacketFlow – Unified network packet parsing abstraction
//!
//! This module provides the [`PacketFlow`] structure, which represents a
//! fully parsed network packet across multiple layers:
//!
//! - Data Link (L2)
//! - Internet (L3)
//! - Transport (L4)
//! - Application (L7, best-effort)
//!
//! The parsing model is **layered and progressive**: each layer is parsed
//! from the payload of the previous one. Unsupported protocols do **not**
//! cause a hard failure and are represented as `None`, allowing partial
//! decoding of real-world traffic.
//!
//! ## Design goals
//!
//! - Deterministic, allocation-free parsing using `&[u8]` references
//! - Clear separation between protocol layers
//! - Robust handling of unknown or unsupported protocols
//! - Suitable for network auditing, traffic analysis and post-capture inspection
//!
//! This module does **not** perform stream reassembly or session tracking.
//! It expects a complete packet buffer (e.g. from PCAP capture).
use application::Application;
use internet::Internet;
use serde::Serialize;
use transport::Transport;
use crate::{
DataLink,
errors::{ParsedPacketError, internet::InternetError, transport::TransportError},
owned::PacketFlowOwned,
};
pub mod application;
pub mod data_link;
pub mod internet;
pub mod transport;
/// A fully or partially parsed network packet flow.
///
/// `PacketFlow` represents a packet parsed across protocol layers.
/// Each layer is optional except for the data link layer, which is mandatory.
///
/// Unsupported or unrecognized protocols do **not** fail parsing and instead
/// result in `None` for the corresponding layer.
///
/// The structure borrows from the original packet buffer (`&[u8]`) and is
/// therefore zero-copy.
#[derive(Debug, Clone, Serialize, Eq)]
pub struct PacketFlow<'a> {
/// Data link layer (mandatory).
#[serde(flatten)]
pub data_link: DataLink<'a>,
/// Internet layer (optional).
#[serde(flatten)]
pub internet: Option<Internet<'a>>,
/// Transport layer (optional).
#[serde(flatten)]
pub transport: Option<Transport<'a>>,
/// Application layer (optional, best-effort).
#[serde(flatten)]
pub application: Option<Application>,
}
impl<'a> TryFrom<&'a [u8]> for PacketFlow<'a> {
type Error = ParsedPacketError;
#[inline(always)]
fn try_from(packets: &'a [u8]) -> Result<Self, Self::Error> {
Self::parse_impl(packets)
}
}
impl<'a> PartialEq for PacketFlow<'a> {
fn eq(&self, other: &Self) -> bool {
self.data_link == other.data_link
&& self.internet == other.internet
&& self.transport == other.transport
&& self.application == other.application
}
}
use std::hash::{Hash, Hasher};
impl<'a> Hash for PacketFlow<'a> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.data_link.hash(state);
self.internet.hash(state);
self.transport.hash(state);
self.application.hash(state);
}
}
impl<'a> PacketFlow<'a> {
fn parse_application_from_transport(transport: &Transport<'a>) -> Option<Application> {
let payload = transport.payload?;
if payload.is_empty() {
return None;
}
let parsed = Application::try_from(payload).ok();
if matches!(
parsed.as_ref().map(|app| app.application_protocol.as_str()),
Some("OPC UA")
) {
return parsed;
}
if is_opcua_tcp_port(transport.source_port) || is_opcua_tcp_port(transport.destination_port)
{
return Some(Application {
application_protocol: "OPC UA".to_string(),
});
}
parsed
}
/// Converts this borrowed [`PacketFlow`] into an owned version.
///
/// This performs the necessary allocations to detach from the original
/// packet buffer and is suitable for storage, serialization or cross-thread
/// usage.
pub fn to_owned(&self) -> PacketFlowOwned {
PacketFlowOwned::from(self.clone())
}
#[inline(always)]
fn parse_impl(packets: &'a [u8]) -> Result<Self, ParsedPacketError> {
let data_link = DataLink::try_from(packets)?;
let internet = match Internet::try_from(data_link.payload) {
Ok(internet) => Some(internet),
Err(InternetError::UnsupportedProtocol) => None,
Err(e) => return Err(e.into()),
};
let transport = match internet.as_ref() {
Some(internet) => {
match Transport::try_from_parts(internet.payload_protocol, internet.payload) {
Ok(transport) => Some(transport),
Err(TransportError::UnsupportedProtocol) => None,
Err(e) => return Err(e.into()),
}
}
None => None,
};
let application = transport
.as_ref()
.and_then(Self::parse_application_from_transport);
Ok(PacketFlow {
data_link,
internet,
transport,
application,
})
}
// -------------------------------------------------------------------------
// Timed parsing (feature-gated) — does NOT change PacketFlow API/fields.
// Uses crate::timing helpers so "feature off" has zero impact elsewhere.
// -------------------------------------------------------------------------
#[cfg(feature = "parse_timing")]
#[inline(always)]
fn parse_impl_timed(
packets: &'a [u8],
timing: &mut crate::timing::ParseTiming,
) -> Result<Self, ParsedPacketError> {
use crate::timing::{elapsed_ns, now};
let total_t0 = now();
let t0 = now();
let data_link = DataLink::try_from(packets)?;
timing.l2_ns = elapsed_ns(t0);
let t0 = now();
let internet = match Internet::try_from(data_link.payload) {
Ok(internet) => Some(internet),
Err(InternetError::UnsupportedProtocol) => None,
Err(e) => return Err(e.into()),
};
timing.l3_ns = elapsed_ns(t0);
let t0 = now();
let transport = match internet.as_ref() {
Some(internet) => {
match Transport::try_from_parts(internet.payload_protocol, internet.payload) {
Ok(transport) => Some(transport),
Err(TransportError::UnsupportedProtocol) => None,
Err(e) => return Err(e.into()),
}
}
None => None,
};
timing.l4_ns = elapsed_ns(t0);
let t0 = now();
let application = transport
.as_ref()
.and_then(Self::parse_application_from_transport);
timing.l7_ns = elapsed_ns(t0);
timing.total_ns = elapsed_ns(total_t0);
Ok(PacketFlow {
data_link,
internet,
transport,
application,
})
}
/// Parses a raw packet buffer into a [`PacketFlow`] and fills timing data.
///
/// This is feature-gated (`parse_timing`) and does not affect normal parsing.
///
/// Convention:
/// - `l*_ns` is the cost of the *attempt* (so it may be >0 even if unsupported).
#[cfg(feature = "parse_timing")]
#[inline(always)]
pub fn try_from_timed(
packets: &'a [u8],
timing: &mut crate::timing::ParseTiming,
) -> Result<Self, ParsedPacketError> {
*timing = crate::timing::ParseTiming::default();
Self::parse_impl_timed(packets, timing)
}
}
fn is_opcua_tcp_port(port: Option<u16>) -> bool {
matches!(port, Some(4840 | 12001))
}
#[cfg(test)]
mod tests {
use crate::parse::transport::protocols::TransportProtocol;
use super::*;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
fn sample_ipv6_tcp_packet() -> Vec<u8> {
hex::decode("646ee0eafa83feaa818ec86486dd6500000004d8063d2a0014504007080a00000000000020012a04cec011a3a3971842f3918f5dec7d01bbe8007dc6e5a6f851bd128018007a628400000101080abd9295c7f7f13851edd897cab0251b0fcd0ce6976e5ffc596564fa9d1986ad4dcd59e5481c0ffa357590175e992da0a3ec8d32403b4ebb23b181e8916f5aa518eef4e126efe31b847f56868867cf26b0acd92680833ceefa8fb7fe1c3f1d96d1693b677b26d76acc7ff4e0e9ff9f5a6b5176689100891de5596ed15c93ec2d87570b13c73c95881562dcebad7acacf6dee4f8872ab2e07dacc00abf8534f8465b3f70a9362dd466bed097dd3943c49e60254c2d1d11e8a43db7b2bb20fac75bd2d12e61a135fc08fb817cf2779363052d5b8698712a0681510513bcd0d3095af28c63ae243006d44d792faa21d5a866c88e8948074e1bf9969d6bf965a796346553d7b64384ccf6a8ac5203aa1820ed3f46a3b656c5bb4670c6240da14f82c8e27cb1be60439c9aa0f9f58f716194deaf5ca10bba3f71d3beb73d878e0768f8ac20e7d1d984bdcdcd44bf861ae99c12a7307fc4ede845580bb97903da6b640403bdfd317b65b97d279d8b9ca5df881b305cf0ebe82d1aa4fd32fb9463653d11ede2327dbbf82453870017c4b6f69daeca416bbe5a1138c62e0da69dd8568b017b1c6b9def2ad9f5ae04bab9add00ddc790ebda970a5c80d44334c1a0a03ce1428efa2d6c260cf78e6442313fd5eacdf578572ef6ab4df6d6b6d9b889b2be67f0c8ae5d87923ce89df59386b8ecb29aeb1e6c5c5be465e3ad4b62c167443068a268ff6067be0f637f5e9994635c09d73c2bc5c5bc76f8fb2b1f00417ee67792cea34ab05451468de91524dbfe127463824e1d3fcc03fda2ff01ae8d21c242b996bc9b138a0ce211166af40b21a32b0b202aa8587430f03a46e9fe87f5991c132cc9c09ce36757888d913da28261e07e537d66f8c76abbd0cfd60236c880dfe49ad48a8ba9dfefe0efede8b004c7fc86b914fc4f4dd067f8dee3c8ecd89a47eafd438523d8ffd9fd1fa5797cd446fe019f8b5cd4cf0bb6e6800f1c06f04dafbc009b558abeb5821cd5c5a6f9b24fe47606ca098290845ba5ac42aa994844553f7522efa08f99b7e62a858cdd1d7376b552fa2ddd87d4f8945292a31654f4032a9e6dc86584bc882bfd063e439fb701da038b23791a0706a1672bd6d70234ceef5340c975a473f8f524743672a284e22098d525b6ff48c54c0d79fe2d67ea4b5619536ef182fe181def5c640961138ed1e7bbb795475295ca3418b8ab5b594307f7338e5689b2fea6aed83a08c356f4e4d072dad9b5b3e38bd9a4c5a632c5f024e892e85341da285eb2098a7d1d114ba8662e6f5c33513cc0d5d0d0186ae7aadab3334d03a8644c3774a16bd985cc198f48012bbe5d9c952472936e7b06c9e663ddb0cdc0fdbcf07e19d11064fe5f9e6f81d7440981331f2faab3f69466af1cd7d8a28c99f680ed88a24e27e53ae2b6d2323aa7592a0d169094eaf5134d421f66934a21e75a6d6532caa0c2c86697ba0b4c3cc484081ef8c94f2609a8b648527ae6926d72eecba718f51e61ce405f36c25e20978e40d5d9dc76dec606e73d2056c15a69fbe16963a09e1ac0a4fcbf922d747d8f29e708f241f565b5a18832a65ff7e41a7ec7ec8b903d7ce05cf298beac641d1c94d8f8eeb7c3622b84a50dfb8df3db8d121ebda13838104f129150d8e8f07804295d30e59e184c4f4b007e3e62420a4fc8e293144f38f828de4ff74c888589252d1de11bc017fc772a183240f682")
.expect("invalid test hex fixture")
}
// fn sample_ipv6_udp_dhcpv6_silicit() -> Vec<u8> {
// hex::decode("333300010002080027fe8f9586dd60000000003c1101fe800000000000000a0027fffefe8f95ff02000000000000000000000001000202220223003cad08011008740001000e000100011c39cf88080027fe8f9500060004001700180008000200000019000c27fe8f9500000e1000001518")
// .expect("invalid test hex fixture")
// }
#[test]
fn packetflow_try_from_valid_packet_should_succeed() {
let packet = sample_ipv6_tcp_packet();
let result = PacketFlow::try_from(packet.as_slice());
assert!(result.is_ok());
}
#[test]
fn packetflow_try_from_empty_packet_should_fail() {
let packet: &[u8] = &[];
let result = PacketFlow::try_from(packet);
assert!(result.is_err());
}
#[test]
fn packetflow_should_parse_data_link_layer() {
let packet = sample_ipv6_tcp_packet();
let flow = PacketFlow::try_from(packet.as_slice()).unwrap();
assert!(!flow.data_link.payload.is_empty());
}
#[test]
fn packetflow_should_parse_internet_layer_for_known_fixture() {
let packet = sample_ipv6_tcp_packet();
let flow = PacketFlow::try_from(packet.as_slice()).unwrap();
assert!(flow.internet.is_some());
let internet = flow.internet.as_ref().unwrap();
assert_eq!(internet.protocol_name, "IPv6");
assert!(internet.source.is_some());
assert!(internet.destination.is_some());
assert!(internet.payload_protocol.is_some());
assert!(!internet.payload.is_empty());
}
#[test]
fn packetflow_should_parse_transport_layer_for_known_fixture() {
let packet = sample_ipv6_tcp_packet();
let flow = PacketFlow::try_from(packet.as_slice()).unwrap();
assert!(flow.transport.is_some());
let transport = flow.transport.as_ref().unwrap();
assert_eq!(transport.protocol, TransportProtocol::Tcp);
}
#[test]
fn packetflow_application_layer_is_best_effort() {
let packet = sample_ipv6_tcp_packet();
let flow = PacketFlow::try_from(packet.as_slice()).unwrap();
if let Some(transport) = &flow.transport {
match transport.payload {
Some(payload) => {
if flow.application.is_some() {
assert!(!payload.is_empty());
}
}
None => {
assert!(flow.application.is_none());
}
}
}
}
#[test]
fn packetflow_partial_eq_same_packet_should_be_equal() {
let packet = sample_ipv6_tcp_packet();
let flow_a = PacketFlow::try_from(packet.as_slice()).unwrap();
let flow_b = PacketFlow::try_from(packet.as_slice()).unwrap();
assert_eq!(flow_a, flow_b);
}
#[test]
fn packetflow_hash_same_packet_should_match() {
let packet = sample_ipv6_tcp_packet();
let flow_a = PacketFlow::try_from(packet.as_slice()).unwrap();
let flow_b = PacketFlow::try_from(packet.as_slice()).unwrap();
let mut hasher_a = DefaultHasher::new();
let mut hasher_b = DefaultHasher::new();
flow_a.hash(&mut hasher_a);
flow_b.hash(&mut hasher_b);
assert_eq!(hasher_a.finish(), hasher_b.finish());
}
#[test]
fn packetflow_to_owned_should_preserve_semantic_content() {
let packet = sample_ipv6_tcp_packet();
let flow = PacketFlow::try_from(packet.as_slice()).unwrap();
let owned = flow.to_owned();
assert_eq!(owned.data_link, flow.to_owned().data_link);
match (&owned.internet, &flow.internet) {
(Some(owned_internet), Some(flow_internet)) => {
assert_eq!(owned_internet.source_ip, flow_internet.source);
assert_eq!(owned_internet.ip_source_type, flow_internet.source_type);
assert_eq!(owned_internet.destination_ip, flow_internet.destination);
assert_eq!(
owned_internet.ip_destination_type,
flow_internet.destination_type
);
assert_eq!(owned_internet.protocol, flow_internet.protocol_name);
}
(None, None) => {}
_ => panic!("owned.internet and flow.internet differ"),
}
match (&owned.transport, &flow.transport) {
(Some(owned_transport), Some(flow_transport)) => {
assert_eq!(
owned_transport.protocol,
format!("{:?}", flow_transport.protocol)
);
assert_eq!(owned_transport.source_port, flow_transport.source_port);
assert_eq!(
owned_transport.destination_port,
flow_transport.destination_port
);
}
(None, None) => {}
_ => panic!("owned.transport and flow.transport differ"),
}
assert_eq!(owned.application, flow.to_owned().application);
}
}