diff --git a/Cargo.lock b/Cargo.lock index b77dacedcb2c54..1f7679a8457160 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1286,6 +1286,15 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder_slice" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b294e30387378958e8bf8f4242131b930ea615ff81e8cac2440cea0a6013190" +dependencies = [ + "byteorder", +] + [[package]] name = "bytes" version = "1.10.0" @@ -2018,6 +2027,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive-into-owned" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d94d81e3819a7b06a8638f448bc6339371ca9b6076a99d4a43eece3c4c923" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "derive-where" version = "1.2.7" @@ -3063,6 +3083,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hxdmp" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b27f28a7466846baca75f0a5244e546e44178eb7f1c07a3820f413e91c6b0" + [[package]] name = "hyper" version = "0.14.32" @@ -4486,6 +4512,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "pcap-file" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc1f139757b058f9f37b76c48501799d12c9aa0aa4c0d4c980b062ee925d1b2" +dependencies = [ + "byteorder_slice", + "derive-into-owned", + "thiserror 1.0.69", +] + [[package]] name = "pem" version = "1.1.1" @@ -7902,6 +7939,7 @@ dependencies = [ name = "solana-gossip" version = "2.3.0" dependencies = [ + "anyhow", "assert_matches", "bincode", "bs58", @@ -8481,9 +8519,11 @@ dependencies = [ "bincode", "bytes", "clap 3.2.23", + "hxdmp", "itertools 0.12.1", "log", "nix", + "pcap-file", "rand 0.8.5", "serde", "serde_derive", diff --git a/gossip/Cargo.toml b/gossip/Cargo.toml index 313686fd443ad0..6dc006368efb2f 100644 --- a/gossip/Cargo.toml +++ b/gossip/Cargo.toml @@ -75,12 +75,14 @@ static_assertions = { workspace = true } thiserror = { workspace = true } [dev-dependencies] +anyhow = { workspace = true } bs58 = { workspace = true } criterion = { workspace = true } num_cpus = { workspace = true } rand0-7 = { workspace = true } rand_chacha0-2 = { workspace = true } serial_test = { workspace = true } +solana-net-utils = { workspace = true, features = ["dev-context-only-utils"] } solana-perf = { workspace = true, features = ["dev-context-only-utils"] } solana-runtime = { workspace = true, features = ["dev-context-only-utils"] } solana-sdk = { workspace = true } diff --git a/gossip/src/lib.rs b/gossip/src/lib.rs index 06df390c59213f..7465609d15c64a 100644 --- a/gossip/src/lib.rs +++ b/gossip/src/lib.rs @@ -46,3 +46,5 @@ extern crate solana_frozen_abi_macro; #[macro_use] extern crate solana_metrics; + +mod wire_format_tests; diff --git a/gossip/src/wire_format_tests.rs b/gossip/src/wire_format_tests.rs new file mode 100644 index 00000000000000..d8eee01978d8a9 --- /dev/null +++ b/gossip/src/wire_format_tests.rs @@ -0,0 +1,44 @@ +#![allow(clippy::arithmetic_side_effects)] + +#[cfg(test)] +mod tests { + + use { + crate::protocol::Protocol, serde::Serialize, + solana_net_utils::tooling_for_tests::validate_packet_format, solana_sanitize::Sanitize, + std::path::PathBuf, + }; + + fn parse_gossip(bytes: &[u8]) -> anyhow::Result { + let pkt: Protocol = solana_perf::packet::deserialize_from_with_limit(bytes)?; + pkt.sanitize()?; + Ok(pkt) + } + + fn serialize(pkt: T) -> Vec { + bincode::serialize(&pkt).unwrap() + } + + /// Test the ability of gossip parsers to understand and re-serialize a corpus of + /// packets captured from mainnet. + /// + /// This test requires external files and is not run by default. + /// Export the "GOSSIP_WIRE_FORMAT_PACKETS" variable to run this test + #[test] + fn test_gossip_wire_format() { + solana_logger::setup(); + let path_base = match std::env::var_os("GOSSIP_WIRE_FORMAT_PACKETS") { + Some(p) => PathBuf::from(p), + None => { + eprintln!("Test requires GOSSIP_WIRE_FORMAT_PACKETS env variable, skipping!"); + return; + } + }; + for entry in + std::fs::read_dir(path_base).expect("Expecting env var to point to a directory") + { + let entry = entry.expect("Expecting a readable file"); + validate_packet_format(&entry.path(), parse_gossip, serialize).unwrap(); + } + } +} diff --git a/net-utils/Cargo.toml b/net-utils/Cargo.toml index df1b1a30e1816e..b4967699907559 100644 --- a/net-utils/Cargo.toml +++ b/net-utils/Cargo.toml @@ -15,9 +15,11 @@ anyhow = { workspace = true } bincode = { workspace = true } bytes = { workspace = true } clap = { version = "3.1.5", features = ["cargo"], optional = true } +hxdmp = { version = "0.2.1", optional = true } itertools = { workspace = true } log = { workspace = true } nix = { workspace = true, features = ["socket"] } +pcap-file = { version = "2.0.0", optional = true } rand = { workspace = true } serde = { workspace = true } serde_derive = { workspace = true } @@ -34,7 +36,7 @@ solana-logger = { workspace = true } [features] default = [] clap = ["dep:clap", "dep:solana-logger", "dep:solana-version"] -dev-context-only-utils = [] +dev-context-only-utils = ["dep:pcap-file", "dep:hxdmp"] [lib] name = "solana_net_utils" diff --git a/net-utils/src/lib.rs b/net-utils/src/lib.rs index 8b346111c0d8f5..6e8abeed669065 100644 --- a/net-utils/src/lib.rs +++ b/net-utils/src/lib.rs @@ -2,6 +2,9 @@ mod ip_echo_client; mod ip_echo_server; +#[cfg(feature = "dev-context-only-utils")] +pub mod tooling_for_tests; + pub use ip_echo_server::{ ip_echo_server, IpEchoServer, DEFAULT_IP_ECHO_SERVER_THREADS, MAX_PORT_COUNT_PER_MESSAGE, MINIMUM_IP_ECHO_SERVER_THREADS, diff --git a/net-utils/src/tooling_for_tests.rs b/net-utils/src/tooling_for_tests.rs new file mode 100644 index 00000000000000..3d607baf6f8c34 --- /dev/null +++ b/net-utils/src/tooling_for_tests.rs @@ -0,0 +1,105 @@ +#![allow(clippy::arithmetic_side_effects)] +use { + anyhow::Context, + log::{debug, error, info}, + pcap_file::pcapng::PcapNgReader, + std::{fs::File, io::Write, path::PathBuf}, +}; + +/// Prints a hexdump of a given byte buffer into stdout +pub fn hexdump(bytes: &[u8]) -> anyhow::Result<()> { + hxdmp::hexdump(bytes, &mut std::io::stderr())?; + std::io::stderr().write_all(b"\n")?; + Ok(()) +} + +/// Reads all packets from PCAPNG file +pub struct PcapReader { + reader: PcapNgReader, +} +impl PcapReader { + pub fn new(filename: &PathBuf) -> anyhow::Result { + let file_in = File::open(filename).with_context(|| format!("opening file {filename:?}"))?; + let reader = PcapNgReader::new(file_in).context("pcap reader creation")?; + + Ok(PcapReader { reader }) + } +} + +impl Iterator for PcapReader { + type Item = Vec; + + fn next(&mut self) -> Option { + loop { + let block = match self.reader.next_block() { + Some(block) => block.ok()?, + None => return None, + }; + let data = match block { + pcap_file::pcapng::Block::Packet(ref block) => { + &block.data[0..block.original_len as usize] + } + pcap_file::pcapng::Block::SimplePacket(ref block) => { + &block.data[0..block.original_len as usize] + } + pcap_file::pcapng::Block::EnhancedPacket(ref block) => { + &block.data[0..block.original_len as usize] + } + _ => { + debug!("Skipping unknown block in pcap file"); + continue; + } + }; + + let pkt_payload = data; + // Check if IP header is present, if it is we can safely skip it + // let pkt_payload = if data[0] == 69 { + // &data[20 + 8..] + // } else { + // &data[0..] + // }; + //return Some(data.to_vec().into_boxed_slice()); + return Some(pkt_payload.to_vec()); + } + } +} + +pub fn validate_packet_format( + filename: &PathBuf, + parse_packet: P, + serialize_packet: S, +) -> anyhow::Result +where + P: Fn(&[u8]) -> anyhow::Result, + S: Fn(T) -> Vec, +{ + info!( + "Validating packet format for {} using samples from {filename:?}", + std::any::type_name::() + ); + let reader = PcapReader::new(filename)?; + let mut number = 0; + for data in reader.into_iter() { + number += 1; + match parse_packet(&data) { + Ok(pkt) => { + let reconstructed_bytes = serialize_packet(pkt); + if reconstructed_bytes != data { + error!("Reserialization failed for packet {number} in {filename:?}!"); + error!("Original packet bytes:"); + hexdump(&data)?; + error!("Reserialized bytes:"); + hexdump(&reconstructed_bytes)?; + break; + } + } + Err(e) => { + error!("Found packet {number} that failed to parse with error {e}"); + error!("Problematic packet bytes:"); + hexdump(&data)?; + } + } + } + info!("Packet format checks passed for {number} packets"); + Ok(number) +}