#![cfg(all(
feature = "http2",
feature = "test-helpers",
feature = "extractors",
feature = "session",
feature = "reassembler",
))]
use flowscope::detect::signatures::http2_preface;
use flowscope::driver::{Driver, Event};
use flowscope::extract::{FiveTuple, parse::test_frames::ipv4_tcp};
use flowscope::http2::{Http2Event, Http2Session, PREFACE};
use flowscope::{EndReason, PacketView, Timestamp};
const DATA_PKT: u8 = 0x18;
const HEADERS: u8 = 0x1;
const DATA: u8 = 0x0;
const SETTINGS: u8 = 0x4;
const END_STREAM: u8 = 0x1;
const END_HEADERS: u8 = 0x4;
fn packet(sport: u16, seq: u32, payload: &[u8]) -> Vec<u8> {
ipv4_tcp(
[1; 6],
[2; 6],
[10, 0, 0, 1],
[10, 0, 0, 2],
sport,
8080,
seq,
0,
DATA_PKT,
payload,
)
}
fn response_packet(dport: u16, seq: u32, payload: &[u8]) -> Vec<u8> {
ipv4_tcp(
[2; 6],
[1; 6],
[10, 0, 0, 2],
[10, 0, 0, 1],
8080,
dport,
seq,
0,
DATA_PKT,
payload,
)
}
fn frame(kind: u8, flags: u8, stream: u32, payload: &[u8]) -> Vec<u8> {
let mut v = Vec::new();
let len = payload.len() as u32;
v.extend_from_slice(&[(len >> 16) as u8, (len >> 8) as u8, len as u8]);
v.push(kind);
v.push(flags);
v.extend_from_slice(&stream.to_be_bytes());
v.extend_from_slice(payload);
v
}
fn literal(name: &str, value: &str) -> Vec<u8> {
let mut v = vec![0x40, name.len() as u8];
v.extend_from_slice(name.as_bytes());
v.push(value.len() as u8);
v.extend_from_slice(value.as_bytes());
v
}
fn driver() -> (
Driver<FiveTuple>,
flowscope::driver::SlotHandle<Http2Event, flowscope::extract::FiveTupleKey>,
) {
let mut b = Driver::builder(FiveTuple::bidirectional());
let slot = b.session_on_ports(Http2Session::new(), [8080]);
(b.build(), slot)
}
#[test]
fn h2_stream_events_reach_the_slot_through_the_driver() {
let (mut driver, mut slot) = driver();
let mut events = Vec::new();
let mut req_block = vec![0x82, 0x87]; req_block.extend(literal(":authority", "api.example"));
req_block.extend(literal(":path", "/v1/things"));
let mut client = PREFACE.to_vec();
client.extend(frame(SETTINGS, 0, 0, &[]));
client.extend(frame(HEADERS, END_HEADERS | END_STREAM, 1, &req_block));
driver.track_into(
PacketView::new(&packet(40000, 1000, &client), Timestamp::new(1, 0)),
&mut events,
);
let mut server = frame(SETTINGS, 0, 0, &[]);
server.extend(frame(HEADERS, END_HEADERS, 1, &[0x88])); server.extend(frame(DATA, END_STREAM, 1, b"pong"));
driver.track_into(
PacketView::new(&response_packet(40000, 5000, &server), Timestamp::new(2, 0)),
&mut events,
);
let mut msgs = Vec::new();
slot.drain(&mut msgs);
let authority = msgs.iter().find_map(|m| match &m.message {
Http2Event::Head(h) => h.authority().map(str::to_owned),
_ => None,
});
assert_eq!(authority.as_deref(), Some("api.example"));
let status = msgs.iter().find_map(|m| match &m.message {
Http2Event::Head(h) => h.status(),
_ => None,
});
assert_eq!(status, Some(200));
let body: Vec<u8> = msgs
.iter()
.filter_map(|m| match &m.message {
Http2Event::Body { data, .. } => Some(data.to_vec()),
_ => None,
})
.collect::<Vec<_>>()
.concat();
assert_eq!(body, b"pong");
assert!(
msgs.iter()
.any(|m| matches!(&m.message, Http2Event::End { stream_id: 1, .. })),
"the stream must be reported complete, got {msgs:?}"
);
}
#[test]
fn a_framing_violation_drops_the_h2_parser() {
let (mut driver, _slot) = driver();
let mut events = Vec::new();
let mut wire = PREFACE.to_vec();
wire.extend(frame(HEADERS, 0, 1, &[0x82]));
wire.extend(frame(DATA, 0, 1, b"x"));
driver.track_into(
PacketView::new(&packet(40001, 1000, &wire), Timestamp::new(1, 0)),
&mut events,
);
let closed = events.iter().find_map(|e| match e {
Event::ParserClosed { reason, .. } => Some(*reason),
_ => None,
});
assert_eq!(
closed,
Some(EndReason::ParseError),
"a poisoned h2 parser must be dropped with a reason, got {events:?}"
);
}
#[test]
fn a_mid_stream_join_is_not_a_parse_error() {
let (mut driver, mut slot) = driver();
let mut events = Vec::new();
let mut block = vec![0x82, 0x87];
block.extend(literal(":authority", "late.example"));
block.extend(literal(":path", "/joined"));
driver.track_into(
PacketView::new(
&packet(40002, 1000, &frame(HEADERS, END_HEADERS, 9, &block)),
Timestamp::new(1, 0),
),
&mut events,
);
assert!(
!events.iter().any(|e| matches!(
e,
Event::ParserClosed {
reason: EndReason::ParseError,
..
}
)),
"joining late is not a framing error, got {events:?}"
);
let mut msgs = Vec::new();
slot.drain(&mut msgs);
let head = msgs.iter().find_map(|m| match &m.message {
Http2Event::Head(h) => Some(h),
_ => None,
});
assert_eq!(
head.and_then(|h| h.authority()),
Some("late.example"),
"and the stream must still route"
);
}
#[test]
fn a_well_framed_h2_flow_is_not_torn_down() {
let (mut driver, _slot) = driver();
let mut events = Vec::new();
let mut first = PREFACE.to_vec();
first.extend(frame(SETTINGS, 0, 0, &[]));
driver.track_into(
PacketView::new(&packet(40003, 1000, &first), Timestamp::new(1, 0)),
&mut events,
);
let mut seq = 1000 + first.len() as u32;
for (i, path) in ["/a", "/b", "/c"].iter().enumerate() {
let stream = (i as u32) * 2 + 1;
let mut block = vec![0x82, 0x87];
block.extend(literal(":authority", "h"));
block.extend(literal(":path", path));
let wire = frame(HEADERS, END_HEADERS | END_STREAM, stream, &block);
driver.track_into(
PacketView::new(&packet(40003, seq, &wire), Timestamp::new(i as u32 + 2, 0)),
&mut events,
);
seq += wire.len() as u32;
}
assert!(
!events.iter().any(|e| matches!(
e,
Event::ParserClosed {
reason: EndReason::ParseError,
..
}
)),
"well-framed multiplexed traffic must not be reported as a parse error, got {events:?}"
);
}
#[test]
fn a_heuristic_slot_pins_h2_on_the_preface() {
let (mut driver, mut slot) = {
let mut b = Driver::builder(FiveTuple::bidirectional());
let slot = b.session_heuristic(Http2Session::new(), http2_preface);
(b.build(), slot)
};
let mut events = Vec::new();
let mut block = vec![0x82, 0x87];
block.extend(literal(":authority", "probe.example"));
block.extend(literal(":path", "/pinned"));
let mut wire = PREFACE.to_vec();
wire.extend(frame(SETTINGS, 0, 0, &[]));
wire.extend(frame(HEADERS, END_HEADERS | END_STREAM, 1, &block));
let (first, rest) = wire.split_at(10);
driver.track_into(
PacketView::new(&packet(41000, 1000, first), Timestamp::new(1, 0)),
&mut events,
);
driver.track_into(
PacketView::new(
&packet(41000, 1000 + first.len() as u32, rest),
Timestamp::new(2, 0),
),
&mut events,
);
let mut msgs = Vec::new();
slot.drain(&mut msgs);
let authority = msgs.iter().find_map(|m| match &m.message {
Http2Event::Head(h) => h.authority(),
_ => None,
});
assert_eq!(
authority,
Some("probe.example"),
"the flow must pin to h2 and route, got {msgs:?}"
);
assert!(
!events.iter().any(|e| matches!(
e,
Event::ParserClosed {
reason: EndReason::ParseError,
..
}
)),
"pinning on the preface must not then reject it"
);
}
#[test]
fn a_heuristic_slot_leaves_non_h2_alone() {
let (mut driver, mut slot) = {
let mut b = Driver::builder(FiveTuple::bidirectional());
let slot = b.session_heuristic(Http2Session::new(), http2_preface);
(b.build(), slot)
};
let mut events = Vec::new();
driver.track_into(
PacketView::new(
&packet(41001, 1000, b"GET /ordinary HTTP/1.1\r\nHost: h\r\n\r\n"),
Timestamp::new(1, 0),
),
&mut events,
);
let mut msgs = Vec::new();
slot.drain(&mut msgs);
assert!(
msgs.is_empty(),
"HTTP/1 must not reach the h2 slot: {msgs:?}"
);
}