Skip to content
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

wire format validation PoC #5147

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions gossip/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
2 changes: 2 additions & 0 deletions gossip/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,5 @@ extern crate solana_frozen_abi_macro;

#[macro_use]
extern crate solana_metrics;

mod wire_format_tests;
44 changes: 44 additions & 0 deletions gossip/src/wire_format_tests.rs
Original file line number Diff line number Diff line change
@@ -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<Protocol> {
let pkt: Protocol = solana_perf::packet::deserialize_from_with_limit(bytes)?;
pkt.sanitize()?;
Ok(pkt)
}

fn serialize<T: Serialize>(pkt: T) -> Vec<u8> {
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();
}
}
}
4 changes: 3 additions & 1 deletion net-utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions net-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
105 changes: 105 additions & 0 deletions net-utils/src/tooling_for_tests.rs
Original file line number Diff line number Diff line change
@@ -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<File>,
}
impl PcapReader {
pub fn new(filename: &PathBuf) -> anyhow::Result<Self> {
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<u8>;

fn next(&mut self) -> Option<Self::Item> {
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<T, P, S>(
filename: &PathBuf,
parse_packet: P,
serialize_packet: S,
) -> anyhow::Result<usize>
where
P: Fn(&[u8]) -> anyhow::Result<T>,
S: Fn(T) -> Vec<u8>,
{
info!(
"Validating packet format for {} using samples from {filename:?}",
std::any::type_name::<T>()
);
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)
}
Loading