Skip to content

Commit

Permalink
test: add pcap testing crate
Browse files Browse the repository at this point in the history
  • Loading branch information
lrstewart committed Jun 13, 2024
1 parent eb40a12 commit d45f082
Show file tree
Hide file tree
Showing 12 changed files with 399 additions and 0 deletions.
27 changes: 27 additions & 0 deletions .github/workflows/ci_rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ env:
RUST_NIGHTLY_TOOLCHAIN: nightly-2024-01-01
ROOT_PATH: bindings/rust
EXAMPLE_WORKSPACE: bindings/rust-examples
PCAP_TEST_PATH: tests/pcap

jobs:
generate:
Expand Down Expand Up @@ -256,3 +257,29 @@ jobs:
- name: Check MSRV of s2n-tokio
run: grep "rust-version = \"$(cat ${{env.ROOT_PATH}}/rust-toolchain)\"" ${{env.ROOT_PATH}}/s2n-tls-tokio/Cargo.toml

pcaps:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: true

- name: Install Rust toolchain
id: toolchain
run: |
rustup toolchain install stable --component clippy
rustup override set stable
- name: Generate bindings
working-directory: ${{env.ROOT_PATH}}
run: ./generate.sh --skip-tests

- name: Run lints
working-directory: ${{env.PCAP_TEST_PATH}}
run: |
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
- name: Run tests
working-directory: ${{env.PCAP_TEST_PATH}}
run: cargo test
2 changes: 2 additions & 0 deletions tests/pcap/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target/
Cargo.lock
15 changes: 15 additions & 0 deletions tests/pcap/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "pcap"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
anyhow = "1.0.86"
hex = "0.4.3"
rtshark = "2.7.1"

[dev-dependencies]
# We want to test against the latest, local version of s2n
s2n-tls-sys = { path = "../../bindings/rust/s2n-tls-sys" }
s2n-tls = { path = "../../bindings/rust/s2n-tls", features = ["unstable-fingerprint"] }
Binary file added tests/pcap/data/fragmented_ch.pcap
Binary file not shown.
Binary file added tests/pcap/data/multiple_hellos.pcap
Binary file not shown.
Binary file added tests/pcap/data/tls12.pcap
Binary file not shown.
Binary file added tests/pcap/data/tls13.pcap
Binary file not shown.
42 changes: 42 additions & 0 deletions tests/pcap/src/capture.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use rtshark::Metadata;
use rtshark::Packet;
use rtshark::RTShark;

pub(crate) fn read_all(mut tshark: RTShark) -> Vec<Packet> {
let mut packets = Vec::new();
while let Ok(Some(packet)) = tshark.read() {
packets.push(packet)
}
packets
}

pub(crate) fn get_metadata<'a>(packet: &'a Packet, key: &'a str) -> Option<&'a str> {
let (layer_name, _) = key.split_once('.').expect("key is layer");
packet
.layer_name(layer_name)
.and_then(|layer| layer.metadata(key))
.map(Metadata::value)
}

pub(crate) fn get_all_metadata<'a>(packet: &'a Packet, key: &'a str) -> Vec<&'a str> {
let (layer_name, _) = key.split_once('.').expect("key is layer");
if let Some(layer) = packet.layer_name(layer_name) {
layer
.iter()
.filter(|metadata| metadata.name() == key)
.map(Metadata::value)
.collect()
} else {
Vec::new()
}
}

pub fn all_pcaps() -> impl Iterator<Item = String> {
std::fs::read_dir("data")
.expect("Missing test pcap file")
.filter_map(Result::ok)
.filter_map(|entry| {
let path = entry.path();
path.to_str().map(std::string::ToString::to_string)
})
}
83 changes: 83 additions & 0 deletions tests/pcap/src/client_hello.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use crate::handshake_message::Builder as MessageBuilder;
use crate::handshake_message::Message;
use anyhow::*;
use std::option::Option;

use crate::capture::*;

#[derive(Debug, Clone, Default)]
pub struct ClientHello {
pub message: Message,
pub ja3_hash: Option<String>,
pub ja3_str: Option<String>,
}

impl ClientHello {
const JA3_HASH: &'static str = "tls.handshake.ja3";
const JA3_STR: &'static str = "tls.handshake.ja3_full";

fn from_message(message: Message) -> Self {
let packet = &message.packet;
let ja3_hash = get_metadata(packet, Self::JA3_HASH).map(str::to_string);
let ja3_str = get_metadata(packet, Self::JA3_STR).map(str::to_string);
Self {
message,
ja3_hash,
ja3_str,
}
}
}

#[derive(Debug, Clone, Default)]
pub struct Builder(MessageBuilder);

impl Builder {
pub fn inner(&mut self) -> &mut MessageBuilder {
&mut self.0
}

pub fn build(mut self) -> Result<Vec<ClientHello>> {
self.0.set_type(1);

let mut client_hellos = Vec::new();
for message in self.0.build()? {
client_hellos.push(ClientHello::from_message(message));
}
Ok(client_hellos)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn multiple_hellos() -> Result<()> {
let mut builder = Builder::default();
builder
.inner()
.set_capture_file("data/multiple_hellos.pcap");
let hellos = builder.build()?;
assert_eq!(hellos.len(), 5);
Ok(())
}

#[test]
fn from_pcaps() -> Result<()> {
let pcaps = all_pcaps();

for pcap in pcaps {
let mut builder = Builder::default();
builder.inner().set_capture_file(&pcap);
let hellos = builder.build().unwrap();

assert!(!hellos.is_empty());
for hello in hellos {
assert!(hello.ja3_hash.is_some());
assert!(hello.ja3_str.is_some());
}
}

Ok(())
}
}
171 changes: 171 additions & 0 deletions tests/pcap/src/handshake_message.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
use anyhow::*;
use rtshark::Packet;
use rtshark::RTSharkBuilder;
use std::collections::HashSet;

use crate::capture::*;

#[derive(Debug, Clone, Default)]
pub struct Message {
pub message_type: String,
pub packet: Packet,
pub payloads: Vec<String>,
pub frame_num: String,
}

impl Message {
const FRAGMENT: &'static str = "tls.handshake.fragment";
const FRAGMENTS_COUNT: &'static str = "tls.handshake.fragment.count";

pub fn to_bytes(&self) -> Result<Vec<u8>> {
let mut bytes = Vec::new();
for payload in &self.payloads {
let hex = payload.replace(':', "");
bytes.extend(&hex::decode(&hex)?[5..]);
}
Ok(bytes)
}

fn from_packet(packet: Packet, frame_num: String, tcp_payloads: &Vec<Packet>) -> Result<Self> {
let message_type = get_metadata(&packet, Builder::MESSAGE_TYPE)
.context("Missing handshake message type")?
.to_string();

let mut payload_frames = get_all_metadata(&packet, Self::FRAGMENT);
if payload_frames.is_empty() {
payload_frames.push(&frame_num);
}
let payload_frames: HashSet<&str> = HashSet::from_iter(payload_frames);

let mut payloads = Vec::new();
for packet in tcp_payloads {
if let Some(frame) = get_metadata(packet, Builder::FRAME_NUM) {
if payload_frames.contains(frame) {
let payload = get_metadata(packet, Builder::TCP_PAYLOAD)
.context("Missing tcp payload")?;
payloads.push(payload.to_string());
}
}
}

let count = get_metadata(&packet, Self::FRAGMENTS_COUNT).unwrap_or("1");
if count != payloads.len().to_string() {
bail!("Unable to find all tcp payloads for tls message")
}

Ok(Self {
message_type,
packet,
payloads,
frame_num,
})
}
}

#[derive(Debug, Clone, Default)]
pub struct Builder {
message_type: Option<u16>,
capture_file: Option<String>,
}

impl Builder {
const TCP_PAYLOAD: &'static str = "tcp.payload";
const FRAME_NUM: &'static str = "frame.number";
const MESSAGE_TYPE: &'static str = "tls.handshake.type";

pub(crate) fn set_type(&mut self, message_type: u16) -> &mut Self {
self.message_type = Some(message_type);
self
}

pub fn set_capture_file(&mut self, file: &str) -> &mut Self {
self.capture_file = Some(file.to_string());
self
}

fn build_from_capture(self, capture: rtshark::RTSharkBuilderReady) -> Result<Vec<Message>> {
let tcp_capture = capture
.display_filter(Self::TCP_PAYLOAD)
.metadata_whitelist(Self::TCP_PAYLOAD)
.metadata_whitelist(Self::FRAME_NUM)
.spawn()?;
let payloads = read_all(tcp_capture);

let filter = if let Some(message_type) = self.message_type {
format!("{} == {}", Self::MESSAGE_TYPE, message_type)
} else {
Self::MESSAGE_TYPE.to_string()
};
let message_capture = capture.display_filter(&filter).spawn()?;

let mut messages = Vec::new();
for packet in read_all(message_capture) {
let frame_num = get_metadata(&packet, Builder::FRAME_NUM)
.context("Missing frame number")?
.to_string();

let context_msg = format!("Failed to parse frame {}", &frame_num);
let message =
Message::from_packet(packet, frame_num, &payloads).context(context_msg)?;

messages.push(message);
}
Ok(messages)
}

pub(crate) fn build(mut self) -> Result<Vec<Message>> {
let file = self
.capture_file
.take()
.context("No capture file provided")?;
let capture = RTSharkBuilder::builder().input_path(&file);
self.build_from_capture(capture)
.with_context(|| format!("Failed to parse capture file {}", &file))
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn fragmentation() -> Result<()> {
let mut builder = Builder::default();
builder.set_capture_file("data/fragmented_ch.pcap");
let messages = builder.build()?;

let first = messages.first().unwrap();
assert_eq!(first.payloads.len(), 3);
let bytes = first.to_bytes()?;
// Correct length read from wireshark
assert_eq!(bytes.len(), 16262);

Ok(())
}

#[test]
fn multiple_handshakes() -> Result<()> {
let mut builder = Builder::default();
builder.set_capture_file("data/multiple_hellos.pcap");
let messages = builder.build()?;
let count = messages.iter().filter(|m| m.message_type == "1").count();
assert_eq!(count, 5);
Ok(())
}

#[test]
fn from_pcaps() -> Result<()> {
let pcaps = all_pcaps();

for pcap in pcaps {
let pcap: String = pcap;

let mut builder = Builder::default();
builder.set_capture_file(&pcap);
let messages = builder.build().unwrap();
assert!(!messages.is_empty())
}

Ok(())
}
}
3 changes: 3 additions & 0 deletions tests/pcap/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod capture;
pub mod client_hello;
pub mod handshake_message;
Loading

0 comments on commit d45f082

Please sign in to comment.