use bytes::Bytes;
use flowscope::driver::{Driver, Event};
use flowscope::extract::FiveTuple;
use flowscope::http2::{HpackEncoder, Http2Event, Http2Session, PREFACE, write_headers};
use flowscope::{PacketView, Timestamp};
fn tcp_packet(src_port: u16, dst_port: u16, seq: u32, payload: &[u8]) -> Vec<u8> {
let (src_ip, dst_ip) = if src_port == 8080 {
([10, 0, 0, 2], [10, 0, 0, 1])
} else {
([10, 0, 0, 1], [10, 0, 0, 2])
};
let mut v = Vec::new();
v.extend_from_slice(&[0xff; 6]);
v.extend_from_slice(&[0xaa; 6]);
v.extend_from_slice(&[0x08, 0x00]);
let total_len = (20 + 20 + payload.len()) as u16;
v.extend_from_slice(&[0x45, 0x00]);
v.extend_from_slice(&total_len.to_be_bytes());
v.extend_from_slice(&[0, 0, 0x40, 0x00, 64, 6, 0, 0]);
v.extend_from_slice(&src_ip);
v.extend_from_slice(&dst_ip);
v.extend_from_slice(&src_port.to_be_bytes());
v.extend_from_slice(&dst_port.to_be_bytes());
v.extend_from_slice(&seq.to_be_bytes());
v.extend_from_slice(&[0, 0, 0, 0]); v.extend_from_slice(&[0x50, 0x18]); v.extend_from_slice(&[0xff, 0xff, 0, 0, 0, 0]); v.extend_from_slice(payload);
v
}
fn field(name: &str, value: &str) -> (Bytes, Bytes) {
(
Bytes::copy_from_slice(name.as_bytes()),
Bytes::copy_from_slice(value.as_bytes()),
)
}
fn main() {
let (mut driver, mut slot) = {
let mut b = Driver::builder(FiveTuple::bidirectional());
let slot = b.session_on_ports(Http2Session::new(), [8080]);
(b.build(), slot)
};
let mut enc = HpackEncoder::new();
let mut client = PREFACE.to_vec();
client.extend_from_slice(&[0, 0, 0, 0x4, 0, 0, 0, 0, 0]);
for (i, path) in ["/v1/orders", "/v1/customers", "/healthz"]
.iter()
.enumerate()
{
let block = enc
.encode(&[
field(":method", "GET"),
field(":scheme", "https"),
field(":authority", "api.example"),
field(":path", path),
])
.expect("encodable");
client.extend(write_headers(i as u32 * 2 + 1, &block, true, 16_384).unwrap());
}
let mut events = Vec::new();
driver.track_into(
PacketView::new(
&tcp_packet(44000, 8080, 1000, &client),
Timestamp::new(1, 0),
),
&mut events,
);
let mut server_enc = HpackEncoder::new();
let mut server = vec![0, 0, 0, 0x4, 0, 0, 0, 0, 0];
for (i, status) in ["200", "200", "503"].iter().enumerate() {
let block = server_enc
.encode(&[
field(":status", status),
field("content-type", "application/json"),
])
.expect("encodable");
server.extend(write_headers(i as u32 * 2 + 1, &block, true, 16_384).unwrap());
}
driver.track_into(
PacketView::new(
&tcp_packet(8080, 44000, 5000, &server),
Timestamp::new(2, 0),
),
&mut events,
);
let mut msgs = Vec::new();
slot.drain(&mut msgs);
println!("== per-stream events off the slot ==\n");
for m in &msgs {
match &m.message {
Http2Event::Head(h) => {
let what = match (h.method(), h.status()) {
(Some(method), _) => format!("{method} {}", h.path().unwrap_or("?")),
(None, Some(status)) => format!("status {status}"),
_ => "?".to_string(),
};
println!(" stream {:>2} {:?} {what}", h.stream_id, m.side);
}
Http2Event::End { stream_id, dir } => {
println!(" stream {stream_id:>2} {dir:?} end");
}
_ => {}
}
}
println!(
"\nNote the `side` column: it says which peer sent the bytes, not\n\
which request they belong to. Both directions of stream 1 share\n\
a TCP flow with every other stream, so `stream_id` is the key\n\
you route and correlate on."
);
let (mut late, mut late_slot) = {
let mut b = Driver::builder(FiveTuple::bidirectional());
let slot = b.session_on_ports(Http2Session::new(), [8080]);
(b.build(), slot)
};
let mut mid_stream = HpackEncoder::new();
let block = mid_stream
.encode(&[
field(":method", "POST"),
field(":scheme", "https"),
field(":authority", "late.example"),
field(":path", "/joined-late"),
])
.expect("encodable");
let mut late_events = Vec::new();
late.track_into(
PacketView::new(
&tcp_packet(
44001,
8080,
1000,
&write_headers(9, &block, true, 16_384).unwrap(),
),
Timestamp::new(3, 0),
),
&mut late_events,
);
let mut late_msgs = Vec::new();
late_slot.drain(&mut late_msgs);
let routed = late_msgs.iter().find_map(|m| match &m.message {
Http2Event::Head(h) => h.authority(),
_ => None,
});
let torn_down = late_events
.iter()
.any(|e| matches!(e, Event::ParserClosed { reason, .. } if reason.is_parser()));
println!("\n== a flow picked up mid-connection ==\n");
println!(" routed to: {}", routed.unwrap_or("(nothing)"));
println!(" parser torn down: {torn_down}");
println!(
"\n The preface is long gone by the time this flow was seen.\n\
`Http2Parser::new()` would refuse it as BadPreface — right when\n\
you own the socket and know the connection started at byte\n\
zero, wrong when a driver hands you whatever it found. That is\n\
the one behavioural difference between the two constructors.\n\
It tolerates a *missing* preface; it does not resynchronise, so\n\
bytes that are not frame-aligned still fail."
);
}