#![allow(unused_imports)]
use crate::{
Dfa, Error, MatchId, MessageBuffer,
h2::{action::*, hpack},
};
use as_bytes::AsBytes;
use httlib_huffman as huffman;
use http::HeaderName;
use plain::Plain;
use std::collections::HashMap;
use std::mem::MaybeUninit;
use std::net::SocketAddr;
use tracing::{Level, debug, warn};
use types::*;
pub use types::{ip4_addr, ip4_conn};
use xbpf::libbpf::{
self as libbpf_rs, ErrorKind, Link, MapCore, MapFlags, MapHandle, OpenObject,
skel::{OpenSkel, Skel, SkelBuilder},
};
extern crate plain;
const MAX_MATCHES: u8 = 32;
const DYNAMIC_TABLE_BASE: u32 = 62;
pub struct Parser {
dfa: Dfa<Action>,
num_matches: u8,
parse_fns: HashMap<MessageBuffer, String>,
matched_fn: Option<String>,
extract_fns: HashMap<MessageBuffer, String>,
get_dynamic_table_entry_fn: Option<String>,
captures: HashMap<Vec<u8>, MatchId>,
}
xbpf::include_bpf!("h2/parser");
#[allow(dead_code)]
impl Parser {
pub fn new() -> Parser {
let dfa = hpack::dfa();
Parser {
dfa,
num_matches: 0,
parse_fns: HashMap::new(),
matched_fn: None,
extract_fns: HashMap::new(),
get_dynamic_table_entry_fn: None,
captures: HashMap::new(),
}
}
pub fn parse_fn<S: ToString>(mut self, parse_fn: S, msg_buf: MessageBuffer) -> Parser {
self.parse_fns.insert(msg_buf, parse_fn.to_string());
self
}
pub fn matched_fn<S: ToString>(mut self, matched_fn: S) -> Parser {
self.matched_fn = Some(matched_fn.to_string());
self
}
pub fn extract_fn<S: ToString>(mut self, extract_fn: S, msg_buf: MessageBuffer) -> Parser {
self.extract_fns.insert(msg_buf, extract_fn.to_string());
self
}
pub fn get_dynamic_table_entry<S: ToString>(mut self, get_dynamic_table_entry_fn: S) -> Parser {
self.get_dynamic_table_entry_fn = Some(get_dynamic_table_entry_fn.to_string());
self
}
pub fn capture_hdr<H: AsRef<[u8]>>(&mut self, name: H) -> Result<MatchId, Error> {
let name = name.as_ref();
if let Some(&mid) = self.captures.get(name) {
return Ok(mid);
}
let mut name_encoded = Vec::new();
huffman::encode(name, &mut name_encoded)?;
let mid = self.new_match()?;
self.dfa
.start_pattern(S_NAME)
.push_bytes(&name_encoded)
.with(Action::capture(mid));
self.captures.insert(name.to_vec(), mid);
Ok(mid)
}
fn new_match(&mut self) -> Result<MatchId, Error> {
if self.num_matches >= MAX_MATCHES {
return Err(Error::MatchLimitExceeded(MAX_MATCHES as usize));
}
let id = MatchId(self.num_matches);
self.num_matches += 1;
Ok(id)
}
fn populate_static_table(&self, static_table: &MapHandle) -> Result<(), Error> {
let insert = |idx: u32, key: &str, val: Option<&str>| -> Result<(), Error> {
let mut hf_key = Vec::new();
huffman::encode(key.as_bytes(), &mut hf_key)?;
let mut hf_val = Vec::new();
if let Some(val) = val {
huffman::encode(val.as_bytes(), &mut hf_val)?;
}
let key_len = hf_key.len() as u8;
let val_len = hf_val.len() as u8;
hf_key.resize(128, 0);
hf_val.resize(128, 0);
let hf = header_field {
key: hf_key.try_into().unwrap(),
key_len,
val: hf_val.try_into().unwrap(),
val_len,
key_huff: 1,
val_huff: 1,
};
let idx = unsafe { idx.as_bytes() };
let hf = unsafe { hf.as_bytes() };
static_table.update(&idx, &hf, MapFlags::ANY)?;
Ok(())
};
let (st_keys, st_hfs) = hpack::create_header_maps();
for (key, vals) in st_hfs.iter() {
for (val, idx) in vals.iter() {
insert(*idx as u32, key, Some(val))?;
}
}
for (key, idx) in st_keys.iter() {
insert(*idx as u32, key, None)?;
}
static_table.freeze()?;
Ok(())
}
pub fn attach(self, target: i32) -> Result<AttachedParser, Error> {
let skel_builder = ParserSkelBuilder::default();
let mut open_obj: MaybeUninit<OpenObject> = MaybeUninit::uninit();
let mut open_skel = skel_builder.open(&mut open_obj)?;
if tracing::event_enabled!(target: "bpf", Level::TRACE) {
open_skel.progs.parse_msg.set_log_level(1);
open_skel.progs.parse_skb.set_log_level(1);
open_skel.progs.parse_buf.set_log_level(1);
}
for mut prog in open_skel.open_object_mut().progs_mut() {
prog.set_autoload(false);
}
for (msg_buf, func) in &self.parse_fns {
let prog = match msg_buf {
MessageBuffer::Msg => &mut open_skel.progs.parse_msg,
MessageBuffer::Skb => &mut open_skel.progs.parse_skb,
MessageBuffer::DynPtr => &mut open_skel.progs.parse_buf,
};
prog.set_autoload(true);
prog.set_attach_target(target, Some(func.clone()))?;
}
if let Some(func) = &self.matched_fn {
let prog = &mut open_skel.progs.matched;
prog.set_autoload(true);
prog.set_attach_target(target, Some(func.clone()))?;
}
for (msg_buf, func) in &self.extract_fns {
let prog = match msg_buf {
MessageBuffer::Msg => &mut open_skel.progs.extract_match_msg,
MessageBuffer::Skb => &mut open_skel.progs.extract_match_skb,
MessageBuffer::DynPtr => {
todo!(
"the parser extracts a match from a msg or an skb, not from a {msg_buf:?}"
)
}
};
prog.set_autoload(true);
prog.set_attach_target(target, Some(func.clone()))?;
}
if let Some(func) = &self.get_dynamic_table_entry_fn {
let prog = &mut open_skel.progs.get_dt_entry;
prog.set_autoload(true);
prog.set_attach_target(target, Some(func.clone()))?;
}
self.inject(&mut open_skel)?;
let skel = open_skel.load()?;
xbpf::tracing::try_init(skel.object())?;
let mut links = Vec::new();
for msg_buf in self.parse_fns.keys() {
links.push(match msg_buf {
MessageBuffer::Msg => skel.progs.parse_msg.attach()?,
MessageBuffer::Skb => skel.progs.parse_skb.attach()?,
MessageBuffer::DynPtr => skel.progs.parse_buf.attach()?,
});
}
if self.matched_fn.is_some() {
links.push(skel.progs.matched.attach()?);
}
for msg_buf in self.extract_fns.keys() {
links.push(match msg_buf {
MessageBuffer::Msg => skel.progs.extract_match_msg.attach()?,
MessageBuffer::Skb => skel.progs.extract_match_skb.attach()?,
MessageBuffer::DynPtr => {
todo!(
"the parser extracts a match from a msg or an skb, not from a {msg_buf:?}"
)
}
});
}
if self.get_dynamic_table_entry_fn.is_some() {
links.push(skel.progs.get_dt_entry.attach()?);
}
let id = skel.maps.static_table.info()?.info.id;
let static_table = MapHandle::from_map_id(id)?;
self.populate_static_table(&static_table)?;
debug!("Beeper http/2 attached");
let dynamic_table_info = MapHandle::try_from(&skel.maps.dynamic_table_info)?;
let dynamic_table = MapHandle::try_from(&skel.maps.dynamic_table)?;
let continued_blocks = MapHandle::try_from(&skel.maps.continued_blocks)?;
Ok(AttachedParser {
dynamic_table_info,
dynamic_table,
continued_blocks,
links,
})
}
fn inject(&self, skel: &mut OpenParserSkel) -> Result<(), Error> {
let Some(data) = skel.maps.rodata_data.as_mut() else {
panic!("the parser program has no read-only data to inject into");
};
let num_states = self.dfa.num_states() as usize;
if num_states > data.s2ts.len() {
warn!(
"the patterns take {num_states} states, the parser holds {}",
data.s2ts.len()
);
return Err(Error::ParserExceedsStateLimit);
}
let mut action_idx = HashMap::new();
action_idx.insert(None, 0usize);
for (from, input, to, action) in self.dfa.iter_transitions() {
let new_action_idx = action_idx.len();
let action = *action_idx.entry(action).or_insert(new_action_idx);
if action >= data.a2as.len() {
warn!(
"the patterns take more actions than the {} the parser holds",
data.a2as.len()
);
return Err(Error::ParserExceedsStateLimit);
}
let input = input as usize;
if input >= data.s2ts[0].len() {
warn!("the patterns read inputs the parser has no column for: {input}");
return Err(Error::ParserExceedsStateLimit);
}
data.s2ts[from.0 as usize][input] = trans {
state: to.0,
action: action as u16,
};
}
for (action, i) in action_idx {
let Some(action) = action else { continue };
data.a2as[i] = action.into();
}
Ok(())
}
}
pub struct AttachedParser {
dynamic_table_info: MapHandle,
dynamic_table: MapHandle,
continued_blocks: MapHandle,
#[allow(dead_code)]
links: Vec<Link>,
}
#[repr(C)]
#[derive(Default, Clone)]
pub struct DynamicTableInfo {
pub count: u32,
pub size: u32,
pub max_size: u32,
pub deleted: u32,
pub dirty: u32,
}
unsafe impl Plain for DynamicTableInfo {}
impl AttachedParser {
pub fn dynamic_table_info(
&self,
local: SocketAddr,
remote: SocketAddr,
) -> Option<Result<DynamicTableInfo, Error>> {
let conn = ip4_conn {
local: local.into(),
remote: remote.into(),
};
let key = unsafe { conn.as_bytes() };
let val = match self.dynamic_table_info.lookup(key, MapFlags::empty()) {
Ok(val) => val?,
Err(err) => return Some(Err(err.into())),
};
let info: Result<&DynamicTableInfo, _> = plain::from_bytes(&val);
match info {
Ok(info) => Some(Ok(info.clone())),
Err(err) => Some(Err(err.into())),
}
}
pub fn forget_conn(&self, local: SocketAddr, remote: SocketAddr) -> Result<(), Error> {
let conn = ip4_conn {
local: local.into(),
remote: remote.into(),
};
let key = unsafe { conn.as_bytes() };
if let Some(val) = self.dynamic_table_info.lookup(key, MapFlags::empty())? {
let info: &DynamicTableInfo = plain::from_bytes(&val)?;
let first = DYNAMIC_TABLE_BASE + info.deleted;
for idx in first..first + info.count {
let entry = dynamic_table_key { conn, idx };
delete_if_present(&self.dynamic_table, unsafe { entry.as_bytes() })?;
}
delete_if_present(&self.dynamic_table_info, key)?;
}
delete_if_present(&self.continued_blocks, key)?;
Ok(())
}
}
fn delete_if_present(map: &MapHandle, key: &[u8]) -> Result<(), Error> {
match map.delete(key) {
Err(e) if e.kind() != ErrorKind::NotFound => Err(e.into()),
_ => Ok(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pseudo_header::{METHOD, PATH, STATUS};
fn hdr(i: u8) -> HeaderName {
HeaderName::from_bytes(format!("x-{i}").as_bytes()).unwrap()
}
#[test]
fn a_parser_captures_at_most_max_matches_ranges() {
let mut parser = Parser::new();
for i in 0..MAX_MATCHES {
let mid = parser.capture_hdr(&hdr(i)).expect("capture header");
assert_eq!(u8::from(mid), i);
}
assert!(matches!(
parser.capture_hdr(&hdr(MAX_MATCHES)),
Err(Error::MatchLimitExceeded(limit)) if limit == MAX_MATCHES as usize
));
}
#[test]
fn the_same_header_is_captured_under_one_match_id() {
let names: [&dyn AsRef<[u8]>; 4] = [&hdr(0), &METHOD, &PATH, &STATUS];
for name in names {
let name = name.as_ref();
let mut parser = Parser::new();
let first = parser.capture_hdr(name).expect("capture header");
let second = parser.capture_hdr(name).expect("capture header again");
assert_eq!(
first,
second,
"capturing {} twice handed out two ids for one range",
String::from_utf8_lossy(name)
);
}
}
}