From 25b2896d1046e142972f1e51fddb4f0d0a0f91de Mon Sep 17 00:00:00 2001 From: Laurin Brandner Date: Tue, 1 Sep 2026 10:40:53 +0200 Subject: [PATCH 1/5] h2: use dfa to match lengths --- .github/workflows/ci.yml | 1 - beeper/src/dfa.rs | 23 +- beeper/src/h2/hpack.rs | 416 ++++++++++++++++++++++++++++ beeper/src/h2/mod.rs | 1 + beeper/src/h2/parser.bpf.c | 537 +++++++++++++++++++++++-------------- beeper/src/h2/parser.rs | 97 +++++-- 6 files changed, 850 insertions(+), 225 deletions(-) create mode 100644 beeper/src/h2/hpack.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 844dafd..2c48eae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,5 +66,4 @@ jobs: - name: Run HTTP/1.1 tests run: cargo test --profile=${{ matrix.profile }} --locked --verbose -p beeper --test h1 -- --include-ignored - name: Run HTTP/2 tests - if: matrix.os == 'ubuntu-26.04' run: cargo test --profile=${{ matrix.profile }} --locked --verbose -p beeper --test h2 -- --include-ignored diff --git a/beeper/src/dfa.rs b/beeper/src/dfa.rs index bb82ccb..2dfb435 100644 --- a/beeper/src/dfa.rs +++ b/beeper/src/dfa.rs @@ -344,15 +344,29 @@ pub(crate) struct Dfa { impl Dfa { /// Creates a DFA that holds nothing but [`INIT_STATE`] and [`ANY_STATE`]. pub fn new() -> Dfa { + Dfa::with_reserved_states(2) + } + + /// Creates a DFA that leaves the first `reserved` state ids to the caller. + /// + /// A parser that anchors its patterns at states it lays out itself, as the + /// HTTP/2 one anchors them at the root of its field name trie, keeps those + /// ids and lets the patterns take the ones above them. + pub fn with_reserved_states(reserved: u16) -> Dfa { Dfa { num_captures: 0, num_matches: 0, - num_states: 2, + num_states: reserved.max(2), edges: HashMap::new(), actions: HashMap::new(), } } + /// Returns the number of states the DFA has, the reserved ones included. + pub fn num_states(&self) -> u16 { + self.num_states + } + /// Starts a new pattern. /// /// A `head` pattern is anchored at [`INIT_STATE`] and therefore only @@ -361,6 +375,13 @@ impl Dfa { pub fn start_pattern<'a>(&'a mut self, head: bool) -> DfaBuilder<'a> { trace!("start_pattern; head={:?}", head); let state = if head { INIT_STATE } else { ANY_STATE }; + self.start_pattern_at(state) + } + + /// Starts a new pattern anchored at `state`, which the caller has to have + /// reserved with [`Dfa::with_reserved_states`]. + pub fn start_pattern_at<'a>(&'a mut self, state: StateId) -> DfaBuilder<'a> { + trace!("start_pattern_at; state={:?}", state); DfaBuilder::new(self, state) } diff --git a/beeper/src/h2/hpack.rs b/beeper/src/h2/hpack.rs new file mode 100644 index 0000000..2441fd6 --- /dev/null +++ b/beeper/src/h2/hpack.rs @@ -0,0 +1,416 @@ +//! The shape of an HPACK header field representation, compiled into +//! transitions. +//! +//! A field is a sequence of integers and strings, and which one comes next is +//! decided by the bytes read so far, so it can be walked with the same +//! automaton as the field names themselves. Section 6 of RFC 7541 spells the +//! representations out. +//! +//! The value an integer carries lives on the transition rather than in the +//! state it leads to, which is what keeps the automaton small: every index a +//! representation can carry is a transition of its own, but all of them lead to +//! the same handful of states. +//! +//! The state ids and action kinds below must stay in sync with the `S_*` and +//! `H2A_*` constants of h2/parser.bpf.c. + +use std::collections::HashMap; + +/// A field name that matched no pattern. +const S_DEAD: u16 = 2; + +/// At the first byte of a field representation. +const S_FIELD: u16 = 3; + +/// At the first byte of the length of a field name. +const S_KEY_LEN: u16 = 4; + +/// At the first byte of the length of a field value. +const S_VAL_LEN: u16 = 5; + +/// The root of the trie of the field names to capture. +pub(super) const S_NAME: u16 = 6; + +/// The continuation of the index of an indexed field. +const S_IDX7_CONT: u16 = 7; + +/// The continuation of the name index of a field that is added to the dynamic +/// table. +const S_IDX6_CONT: u16 = 8; + +/// The continuation of the name index of a field that is not. +const S_IDX4_CONT: u16 = 9; + +/// The continuation of a dynamic table size update. +const S_STG_CONT: u16 = 10; + +/// The continuation of the length of a field name. +const S_KEY_LEN_CONT: u16 = 11; + +/// The continuation of the length of a Huffman coded field name. +const S_KEY_LEN_CONT_HUFF: u16 = 12; + +/// The continuation of the length of a field value. +const S_VAL_LEN_CONT: u16 = 13; + +/// The continuation of the length of a Huffman coded field value. +const S_VAL_LEN_CONT_HUFF: u16 = 14; + +/// The number of state ids the ones above reserve. +pub(super) const S_RESERVED: u16 = 15; + +/// The string the action describes is Huffman coded. +const F_HUFF: u8 = 1 << 0; + +/// The field the action describes is added to the dynamic table. +const F_ADD_DT: u8 = 1 << 1; + +/// The integer the action describes is spread over several bytes, so the parser +/// takes it from its accumulator rather than from [`Action::val`]. +const F_CONT: u8 = 1 << 2; + +/// What the parser does upon taking a transition. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(super) enum Kind { + /// Nothing. + None, + + /// A field spelled out by nothing but an index, which addresses the entry + /// both its name and its value are read from. + Indexed, + + /// A field whose name is an index and whose value is spelled out. + IdxName, + + /// A field whose name is spelled out as well. + LitName, + + /// The length of a field name. + KeyLen, + + /// The length of a field value. + ValLen, + + /// A dynamic table size update. + TableSize, + + /// The first byte of an integer that does not fit into the prefix of that + /// byte, carrying the prefix maximum the integer is counted from. + IntStart, + + /// A byte of such an integer that is not its last one either. + IntCont, + + /// The name of the field being read just matched a pattern. + Capture, + + /// The representation is malformed. + Err, +} + +impl Kind { + /// Returns the number the BPF parser identifies the kind by. + fn id(self) -> u8 { + match self { + Kind::None => 0, + Kind::Indexed => 1, + Kind::IdxName => 2, + Kind::LitName => 3, + Kind::KeyLen => 4, + Kind::ValLen => 5, + Kind::TableSize => 6, + Kind::IntStart => 7, + Kind::IntCont => 8, + Kind::Capture => 9, + Kind::Err => 10, + } + } +} + +/// A single action of the automaton, as the BPF parser reads it. +/// +/// `val` is an index, a length or a table size, depending on `kind`. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(super) struct Action { + pub kind: Kind, + pub val: u16, + pub flags: u8, +} + +impl Action { + /// The action of a transition that does nothing. + pub const NONE: Action = Action { + kind: Kind::None, + val: 0, + flags: 0, + }; + + /// Returns the action of a transition of `kind` carrying `val`. + const fn new(kind: Kind, val: u16, flags: u8) -> Action { + Action { kind, val, flags } + } + + /// Returns the action capturing the value of the field whose name the + /// automaton just matched, under the id `cid`. + pub const fn capture(cid: u16) -> Action { + Action::new(Kind::Capture, cid, 0) + } + + /// Returns the kind and flags the BPF parser reads the action with. + pub fn encode(&self) -> (u8, u8) { + (self.kind.id(), self.flags) + } +} + +/// A single transition, as the BPF parser reads it out of its table. +#[derive(Clone, Copy, Debug)] +pub(super) struct Edge { + pub from: u16, + pub input: u8, + pub to: u16, + + /// The index of the entry of the action table the transition carries. + pub action: u16, +} + +/// The transitions and actions the BPF parser is injected with. +/// +/// Actions are interned, so the transitions of an index and those of a length +/// only take up as many entries as there are distinct values they can carry. +pub(super) struct Table { + edges: Vec, + actions: Vec, + interned: HashMap, +} + +impl Table { + /// Creates a table holding the transitions of every representation of RFC + /// 7541, ready to take the field name patterns. + pub fn new() -> Table { + let mut table = Table { + edges: Vec::new(), + actions: Vec::new(), + interned: HashMap::new(), + }; + + let none = table.intern(Action::NONE); + assert_eq!(none, 0, "the action of a transition that has none is 0"); + + table.push_field_row(); + table.push_length_rows(); + table.push_continuation_rows(); + + table + } + + /// Returns the transitions of the automaton. + pub fn edges(&self) -> &[Edge] { + &self.edges + } + + /// Returns the actions the transitions carry, indexed by [`Edge::action`]. + pub fn actions(&self) -> &[Action] { + &self.actions + } + + /// Returns the index of the entry `action` is held under, adding it if the + /// table does not carry it yet. + fn intern(&mut self, action: Action) -> u16 { + if let Some(idx) = self.interned.get(&action) { + return *idx; + } + + let idx = self.actions.len() as u16; + self.actions.push(action); + let _ = self.interned.insert(action, idx); + + idx + } + + /// Appends the transition `input` takes from `from` to `to`. + pub fn push_edge(&mut self, from: u16, input: u8, to: u16, action: Action) { + let action = self.intern(action); + self.edges.push(Edge { + from, + input, + to, + action, + }); + } + + /// Appends the transitions of the first byte of a representation, see + /// section 6 of RFC 7541. + fn push_field_row(&mut self) { + // an indexed field, 7 bit prefix. The index 0 is not used + self.push_edge(S_FIELD, 0x80, S_DEAD, Action::new(Kind::Err, 0, 0)); + for idx in 1..0x7F { + let input = 0x80 | idx as u8; + self.push_edge(S_FIELD, input, S_FIELD, Action::new(Kind::Indexed, idx, 0)); + } + self.push_edge(S_FIELD, 0xFF, S_IDX7_CONT, Action::new(Kind::IntStart, 0x7F, 0)); + + // a literal field that is added to the dynamic table, 6 bit prefix + self.push_edge( + S_FIELD, + 0x40, + S_KEY_LEN, + Action::new(Kind::LitName, 0, F_ADD_DT), + ); + for idx in 1..0x3F { + let input = 0x40 | idx as u8; + let action = Action::new(Kind::IdxName, idx, F_ADD_DT); + self.push_edge(S_FIELD, input, S_VAL_LEN, action); + } + self.push_edge(S_FIELD, 0x7F, S_IDX6_CONT, Action::new(Kind::IntStart, 0x3F, 0)); + + // a dynamic table size update, 5 bit prefix + for size in 0..0x1F { + let input = 0x20 | size as u8; + self.push_edge(S_FIELD, input, S_FIELD, Action::new(Kind::TableSize, size, 0)); + } + self.push_edge(S_FIELD, 0x3F, S_STG_CONT, Action::new(Kind::IntStart, 0x1F, 0)); + + // a literal field that is not, either because it is never to be indexed + // or because it is only not indexed here, 4 bit prefix. Beeper reads + // both the same way + for base in [0x00u8, 0x10] { + self.push_edge(S_FIELD, base, S_KEY_LEN, Action::new(Kind::LitName, 0, 0)); + for idx in 1..0x0F { + let input = base | idx as u8; + let action = Action::new(Kind::IdxName, idx, 0); + self.push_edge(S_FIELD, input, S_VAL_LEN, action); + } + let input = base | 0x0F; + self.push_edge(S_FIELD, input, S_IDX4_CONT, Action::new(Kind::IntStart, 0x0F, 0)); + } + } + + /// Appends the transitions of the byte announcing the length of a name and + /// of the one announcing the length of a value, see section 5.2 of RFC + /// 7541. Both carry the Huffman bit in their top bit and a 7 bit prefix. + fn push_length_rows(&mut self) { + let rows = [ + ( + S_KEY_LEN, + Kind::KeyLen, + S_NAME, + S_KEY_LEN_CONT, + S_KEY_LEN_CONT_HUFF, + ), + ( + S_VAL_LEN, + Kind::ValLen, + S_FIELD, + S_VAL_LEN_CONT, + S_VAL_LEN_CONT_HUFF, + ), + ]; + + for (from, kind, to, cont, cont_huff) in rows { + for (base, flags, cont) in [(0x00u8, 0, cont), (0x80u8, F_HUFF, cont_huff)] { + for len in 0..0x7F { + let input = base | len as u8; + self.push_edge(from, input, to, Action::new(kind, len, flags)); + } + + let input = base | 0x7F; + self.push_edge(from, input, cont, Action::new(Kind::IntStart, 0x7F, 0)); + } + } + } + + /// Appends the transitions of the bytes an integer that did not fit into + /// the prefix of its first byte is spread over, see section 5.1 of RFC + /// 7541. The top bit of every one of them says whether another follows. + fn push_continuation_rows(&mut self) { + let rows = [ + (S_IDX7_CONT, Kind::Indexed, S_FIELD, 0), + (S_IDX6_CONT, Kind::IdxName, S_VAL_LEN, F_ADD_DT), + (S_IDX4_CONT, Kind::IdxName, S_VAL_LEN, 0), + (S_STG_CONT, Kind::TableSize, S_FIELD, 0), + (S_KEY_LEN_CONT, Kind::KeyLen, S_NAME, 0), + (S_KEY_LEN_CONT_HUFF, Kind::KeyLen, S_NAME, F_HUFF), + (S_VAL_LEN_CONT, Kind::ValLen, S_FIELD, 0), + (S_VAL_LEN_CONT_HUFF, Kind::ValLen, S_FIELD, F_HUFF), + ]; + + for (from, kind, to, flags) in rows { + for input in 0..0x80u8 { + let action = Action::new(kind, 0, flags | F_CONT); + self.push_edge(from, input, to, action); + self.push_edge(from, 0x80 | input, from, Action::new(Kind::IntCont, 0, 0)); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + + /// Returns the states the structure is walked with. `S_DEAD` and `S_NAME` + /// are left out, as the patterns are what gives those their transitions. + fn structure_states() -> Vec { + (S_FIELD..S_RESERVED).filter(|s| *s != S_NAME).collect() + } + + #[test] + fn every_structure_state_has_a_transition_for_every_byte() { + let table = Table::new(); + + for state in structure_states() { + let inputs: HashSet = table + .edges() + .iter() + .filter(|edge| edge.from == state) + .map(|edge| edge.input) + .collect(); + + assert_eq!(inputs.len(), 256, "state {state} does not read every byte"); + } + } + + #[test] + fn no_state_reads_a_byte_twice() { + let table = Table::new(); + let mut seen = HashSet::new(); + + for edge in table.edges() { + assert!( + seen.insert((edge.from, edge.input)), + "state {} reads {:#04x} twice", + edge.from, + edge.input + ); + } + } + + #[test] + fn no_transition_leads_to_a_state_without_transitions() { + let table = Table::new(); + let from: HashSet = table.edges().iter().map(|edge| edge.from).collect(); + + for edge in table.edges() { + assert!( + edge.to == S_DEAD || edge.to == S_NAME || from.contains(&edge.to), + "state {} leads nowhere", + edge.to + ); + } + } + + #[test] + fn the_action_of_a_transition_without_one_is_zero() { + let table = Table::new(); + assert_eq!(table.actions()[0], Action::NONE); + } + + #[test] + fn actions_are_interned() { + let table = Table::new(); + let unique: HashSet = table.actions().iter().copied().collect(); + + assert_eq!(unique.len(), table.actions().len()); + } +} diff --git a/beeper/src/h2/mod.rs b/beeper/src/h2/mod.rs index f29c8f8..f2f75c4 100644 --- a/beeper/src/h2/mod.rs +++ b/beeper/src/h2/mod.rs @@ -9,6 +9,7 @@ use std::collections::HashMap; use std::net::SocketAddr; +mod hpack; mod parser; pub use parser::{AttachedParser, Parser, ip4_addr, ip4_conn}; diff --git a/beeper/src/h2/parser.bpf.c b/beeper/src/h2/parser.bpf.c index f49c78f..44f1e26 100644 --- a/beeper/src/h2/parser.bpf.c +++ b/beeper/src/h2/parser.bpf.c @@ -9,26 +9,6 @@ // dynamic table so that fields which are only referenced by index can be // resolved as well. -// The part of a header field the parser is currently reading. HPACK encodes a -// field as a sequence of integers and strings, and the parser needs to know -// which one it is looking at to interpret the bytes it reads. -enum h2_parse_state { - // integers - H2_IDX = 0, - H2_KEY_LEN = 1, - H2_VAL_LEN = 2, - - // strings - H2_KEY = 3, - H2_VAL = 4, -}; - -// Whether the parser is reading a string rather than an integer. -#define PS_IS_STR(ps) (ps > H2_VAL_LEN) - -// The state reading the string a length integer announces. -#define PS_LEN_TO_STR(ps) (ps + 2) - // The number of bytes of a name or a value that are kept in a table entry. // Longer fields are truncated, which bounds the copies for the verifier. // `header_field`, which both tables are made of, is declared in beeper.h so @@ -101,23 +81,126 @@ struct { __type(value, struct dynamic_table_info); } dynamic_table_info SEC(".maps"); -// Parsing is complete, the rest of the message is not a header anymore. -const u16 a_done = 1 << 14; +// The states the shape of a header field representation is walked with. HPACK +// spells a field out as a sequence of integers and strings, and which one comes +// next is decided by the bytes read so far, so it is the DFA that keeps track +// of it rather than the parser. +// +// User space fills the rows of `s2ts` these index, and hands out the ids from +// `S_RESERVED` on to the states of the field name trie. They must stay in sync +// with the state ids of h2/hpack.rs. + +// A field name that matched no pattern. It carries no transition of its own, so +// the parser stays in it until the name it is reading ends. +#define S_DEAD 2 + +// At the first byte of a field representation. +#define S_FIELD 3 + +// At the first byte of the length of a field name, respectively of a value. +#define S_KEY_LEN 4 +#define S_VAL_LEN 5 + +// The root of the trie of the field names to capture. +#define S_NAME 6 + +// The continuation of an integer that did not fit into the prefix of its first +// byte. There is one state per representation, as what the integer means +// differs, and one per Huffman bit for the lengths, as that bit is announced by +// the first byte but is only recorded once the last one has been read. +#define S_IDX7_CONT 7 +#define S_IDX6_CONT 8 +#define S_IDX4_CONT 9 +#define S_STG_CONT 10 +#define S_KEY_LEN_CONT 11 +#define S_KEY_LEN_CONT_HUFF 12 +#define S_VAL_LEN_CONT 13 +#define S_VAL_LEN_CONT_HUFF 14 + +// The number of state ids the ones above reserve. +#define S_RESERVED 15 + +// What the parser does upon taking a transition. Must stay in sync with the +// action kinds of h2/hpack.rs. + +// Nothing. +#define H2A_NONE 0 + +// A field spelled out by nothing but an index: `val` addresses the entry of the +// static or the dynamic table both its name and its value are read from. +#define H2A_INDEXED 1 -// The value of the field whose name the DFA just matched is to be captured -// under the id in the low bits. -const u16 a_start_capture = 1 << 13; +// A field whose name is an index and whose value is spelled out: `val` +// addresses the entry the name is read from. +#define H2A_IDX_NAME 2 -const u16 a_id_mask = 0x0FFF; +// A field whose name is spelled out as well. +#define H2A_LIT_NAME 3 -const u16 s_any = 1; +// The length of a field name, respectively of a value: `val` counts the bytes +// it occupies on the wire. +#define H2A_KEY_LEN 4 +#define H2A_VAL_LEN 5 -#define MAX_STATES 2048 +// A dynamic table size update: `val` is the size the peer resizes to. +#define H2A_TABLE_SIZE 6 + +// The first byte of an integer that does not fit into the prefix of that byte: +// `val` is the prefix maximum the integer is counted from. +#define H2A_INT_START 7 + +// A byte of such an integer that is not its last one either. +#define H2A_INT_CONT 8 + +// The name of the field being read just matched a pattern, `val` being the id +// its value is to be captured under. +#define H2A_CAPTURE 9 + +// The representation is malformed. There is no telling where the next field +// starts, so the rest of the block is dropped. +#define H2A_ERR 10 + +// The string the action describes is Huffman coded. +#define H2F_HUFF (1 << 0) + +// The field the action describes is added to the dynamic table. +#define H2F_ADD_DT (1 << 1) + +// The integer the action describes is spread over several bytes, so it is to be +// read out of the accumulator rather than out of `val`. +#define H2F_CONT (1 << 2) + +// A single action of the DFA. `val` is an index, a length or a table size, +// depending on `kind`. +// +// Actions are kept in a table of their own because they do not fit into the 16 +// bits `struct trans` carries; the action of a transition is the index of its +// entry. Keeping them on the transition rather than on the state it leads to is +// what keeps the automaton small: every index a representation can carry is a +// transition of its own, but all of them lead to the same handful of states. +struct h2_action { + u16 val; + u8 kind; + u8 flags; +}; + +// these restrictions are needed to make the verifier happy. All three are +// masked onto an index, so all three have to be powers of two. +#define MAX_STATES 1024 #define MAX_TRANS 256 +#define MAX_ACTIONS 1024 -// The transition table of the DFA, indexed by state and input byte. User space -// fills it in before the program is loaded, after which it is read-only. +// The transition table of the DFA, indexed by state and input byte, and the +// actions its transitions carry. User space fills both in before the program is +// loaded, after which they are read-only. volatile const struct trans s2ts[MAX_STATES][MAX_TRANS]; +volatile const struct h2_action a2as[MAX_ACTIONS]; + +// Reads the action a transition carries. Transition 0 is the one a state +// without a transition for the byte it read falls back to, and carries none. +static __always_inline struct h2_action _action(u16 id) { + return a2as[id & (MAX_ACTIONS - 1)]; +} // The length of the longest code of the HPACK Huffman code. #define HPACK_HUFF_MAXLEN 30 @@ -291,14 +374,17 @@ static __always_inline void _extract_match(const struct msg_ctx *ctx, const stru } // Follows the transition `input` takes out of `state`. A state that has no -// transition for `input` falls back to `s_any`. +// transition for `input` falls back to `S_DEAD`, which has none either: the +// rows the shape of a representation is walked with carry a transition per +// byte, so this only happens while a field name is being read, and a name that +// took a byte no pattern has cannot match one anymore. static __always_inline void _next(u16 state, u8 input, u16 *next_state, u16 *action) { state &= MAX_STATES - 1; input &= MAX_TRANS - 1; struct trans t = s2ts[state][input]; if (t.state == 0 && t.action == 0) { - *next_state = s_any; + *next_state = S_DEAD; *action = 0; return; } @@ -307,85 +393,6 @@ static __always_inline void _next(u16 state, u8 input, u16 *next_state, u16 *act *action = t.action; } -// The number of bits the first byte of a field representation carries the -// integer in, indexed by its top nibble. It is what identifies the -// representation as well: 7 bits for an indexed field, 6 for a literal that is -// added to the dynamic table, 5 for a table size update and 4 for a literal -// that is not indexed. -static const u8 hpack_prefix_len[16] = { - 4, 4, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7 -}; - -// Moves on to the part of the field that follows the one that just ended, `c` -// being its first byte. `n` is set to the prefix length of the integer to read -// next, `j` to the number of bytes still to read and `k` is cleared, ready to -// take the next integer. -// -// A field is either fully indexed, in which case the next byte starts a new -// field, or it spells out its value, and possibly its name, which is what the -// index of 0 and the representations with a 6 or 4 bit prefix indicate. -static __always_inline int _next_hpack(u8 c, enum h2_parse_state *ps __arg_nonnull, u32 *n __arg_nonnull, u32 *k __arg_nonnull, u8 *j __arg_nonnull) { - if (*ps == H2_KEY_LEN || *ps == H2_VAL_LEN) { - *ps = PS_LEN_TO_STR(*ps); - *j = *k-1; - *n = 0; - } - else if (*ps == H2_IDX && *k == 0 && (*n == 6 || *n == 4)) { - *ps = H2_KEY_LEN; - *j = 0; - *n = 7; - } - else if ((*ps == H2_IDX && (*n == 6 || *n == 4)) || *ps == H2_KEY) { - *ps = H2_VAL_LEN; - *j = 0; - *n = 7; - } - else { - *ps = H2_IDX; - *j = 0; - *n = hpack_prefix_len[c >> 4]; - } - - *k = 0; - - return 0; -} - -// Feeds the byte `c` to the HPACK decoder. `ps` is the part of the field being -// read, `k` the integer that is being accumulated, i.e. an index or the length -// of a string, `n` the number of bits its first byte carries and `m` the shift -// of the continuation byte to come. `j` counts the bytes still to read, which -// for an integer is only ever 0 or 1, as its continuation is announced by the -// top bit of every byte. -// -// A caller that finds `j` back at 0 with `ps` at an integer state has just read -// the last byte of that integer, and one that finds `ps` at a string state has -// just read a byte of the string. -static __always_inline void _parse_hpack(u8 c, enum h2_parse_state *ps, u32 *n, u32 *m, u32 *k, u8 *j, bool *huff) { - if (*j > 0) { - if (PS_IS_STR(*ps)) { - *j -= 1; - } - else { - *k += (c & 127) * (1 << *m); - *m += 7; - *j = ((c & 128) == 128); - } - - return; - } - - _next_hpack(c, ps, n, k, j); - *m = 0; - - if (!PS_IS_STR(*ps)) { - u8 mask = (1 << *n) - 1; - *k = c & mask; - *j = (*k == mask); - *huff = (c & 0x80) != 0; - } -} - // Looks up the oldest entry of the dynamic table, i.e. the one HPACK evicts // first. `*entry` is NULL if the table is empty. static __always_inline void _get_lru_dynamic_table_entry(const struct ip4_conn *conn __arg_nonnull, struct dynamic_table_info *dt_info __arg_nonnull, struct dynamic_table_entry **entry) { @@ -414,20 +421,21 @@ static __always_inline void _get_table_entry(const struct ip4_conn *conn __arg_n } } -// Walks the DFA over `key`, the Huffman encoded name of a field the peer only -// referenced by index, and returns the id of the capture it matched, or -1 if -// the name matches no pattern. `s` is left in the state the walk ended in. -static __always_inline s8 _match_header_key(const u8 *key __arg_nonnull, u16 key__sz, u16 *s __arg_nonnull) { - u8 j = 0; - u16 a = 0; +// Walks the name trie over `key`, the name of a field the peer only referenced +// by index, and returns the id of the capture it matched, or -1 if the name +// matches no pattern. +// +// It is only reached through `_run_action`, so the walk is verified once rather +// than as part of every byte of the block the parser reads. +static __always_inline int _match_header_key(const u8 *key __arg_nonnull, u16 key__sz) { + u16 s = S_NAME; + u16 j = 0; bpf_for(j, 0, key__sz) { - u8 c = key[j]; - _next(*s, c, s, &a); + u16 a = 0; + _next(s, key[j], &s, &a); - if ((a & a_start_capture) != 0) { - u8 cid = a & a_id_mask & MAX_MATCH_MASK; - return cid; - } + struct h2_action act = _action(a); + if (act.kind == H2A_CAPTURE) return act.val & MAX_MATCH_MASK; } return -1; @@ -598,11 +606,155 @@ static __always_inline int _parse_stg_from(const struct msg_ctx *ctx, u16 start, return i; } +// Everything the parser carries from one byte of a header block to the next, +// along with the transition it is about to run. The DFA holds the shape of a +// representation, so what is left is the integer a multi byte length or index +// accumulates into, how many bytes of the string that was announced are still +// to come, and the field being assembled out of the two. +struct h2_parse_state { + // the state of the DFA + u16 s; + + // the integer being accumulated and the shift of its next byte + u32 k; + u32 m; + + // the bytes of the string that was announced that are still to be read, and + // whether they are a name, which is walked so that it can match a pattern, + // rather than a value, which is only counted + u32 skip; + bool is_key; + + // the capture the name of the field being read matched, or -1 + s8 cid; + + // whether the peer adds the field being read to its dynamic table + u8 add_to_dt; + + // the name of that field + struct hdr_match key; + + // the offset of the byte being read, and the action and the integer of the + // transition it took + u32 i; + u32 v; + u8 kind; + u8 flags; +}; + +// Runs the action of the transition `ps` holds, which is what turns the parts +// of a field the DFA picked out into a capture, into an entry of the mirrored +// dynamic table, or into both. +// +// It is a program of its own so that it is verified once rather than as part of +// every byte of the block the parser reads. +// +// Returns 0, or -1 if the block cannot be read any further. +__noinline __weak int _run_action(const struct msg_ctx *ctx __arg_nonnull, struct dynamic_table_info *dt_info __arg_nonnull, struct parse_res *pres __arg_nonnull, struct h2_parse_state *ps __arg_nonnull) { + u32 v = ps->v & MAX_BYTES; + + bpf_trace("hdr: %d: kind %d, val %d", ps->i, ps->kind, v); + + // a name that is an index has to be read out of a table before it can be + // matched. Both representations that carry one are handled here, so that the + // walk over the entry is only built into the program once + if (ps->kind == H2A_INDEXED || ps->kind == H2A_IDX_NAME) { + ps->add_to_dt = (ps->flags & H2F_ADD_DT) != 0; + ps->cid = -1; + ps->key = (struct hdr_match) { + .idx = v, + .len = 0, + .in_msg = false, + .huff = false, + }; + + struct header_field *hf = NULL; + _get_table_entry(&ctx->conn, dt_info, v, &hf); + if (hf == NULL) return 0; + + int mid = _match_header_key(hf->key, hf->key_len & HEADER_FIELD_MASK); + if (mid < 0) return 0; + + if (ps->kind == H2A_IDX_NAME) { + ps->cid = mid; + return 0; + } + + // both halves of the field are in the table, so the value is reported + // as the index it is to be read back with + pres->ms[mid & MAX_MATCH_MASK] = (struct hdr_match) { + .idx = v, + .len = HEADER_FIELD_MASK, + .in_msg = false, + .huff = false, + }; + + return 0; + } + + if (ps->kind == H2A_LIT_NAME) { + ps->add_to_dt = (ps->flags & H2F_ADD_DT) != 0; + ps->cid = -1; + return 0; + } + + if (ps->kind == H2A_KEY_LEN) { + ps->key = (struct hdr_match) { + .idx = ps->i + 1, + .len = v, + .in_msg = true, + .huff = (ps->flags & H2F_HUFF) != 0, + }; + + ps->skip = v; + ps->is_key = true; + if (v == 0) ps->s = S_VAL_LEN; + + return 0; + } + + if (ps->kind == H2A_VAL_LEN) { + struct hdr_match val = (struct hdr_match) { + .idx = ps->i + 1, + .len = v, + .in_msg = true, + .huff = (ps->flags & H2F_HUFF) != 0, + }; + + if (ps->add_to_dt) { + _add_dynamic_table_entry(ctx, dt_info, &ps->key, &val); + } + + if (ps->cid >= 0) { + pres->ms[ps->cid & MAX_MATCH_MASK] = val; + ps->cid = -1; + } + + ps->skip = v; + ps->is_key = false; + + return 0; + } + + if (ps->kind == H2A_TABLE_SIZE) { + bpf_debug("hdr: table size update: %u", v); + dt_info->max_size = v; + return 0; + } + + if (ps->kind == H2A_ERR) { + bpf_debug("hdr: malformed representation at %d", ps->i); + return -1; + } + + return 0; +} + // Decodes the header block between the offsets `start` and `end` and records // the values of the fields whose name matches a pattern in `pres`. Fields the // peer adds to its dynamic table are added to the mirrored one, so that later // blocks can resolve the indices referring to them. `s` is the state the DFA -// walk over the field names starts in. +// walk starts in, which for the beginning of a block is `S_FIELD`. // // `null_prefix` is the length of the run of NUL bytes at the beginning of the // buffer that is to be skipped rather than parsed; it is updated as those bytes @@ -616,26 +768,30 @@ static __always_inline int _parse_hdr_from(const struct msg_ctx *ctx, u16 start, if (end < len) len = end & MAX_BYTES; if (data + 9 > data_end) return 0; - u8 type = data[3]; - u8 flags = data[4]; - u32 stream_id = data[5] << 24 | data[6] << 16 | data[7] << 8 | data[8]; - struct dynamic_table_info *dt_info = _get_dynamic_table(&ctx->conn); if (!dt_info) return 0; - u32 n = 0, m = 0, i = 0, k = 0; - u8 j = 0; - s8 cid = -1; - u8 add_to_dt = 0; - bool huff = false; - enum h2_parse_state ps = H2_IDX; - struct hdr_match key = { - .idx = 0, - .len = 0, - .in_msg = true, - .huff = false, + struct h2_parse_state ps = { + .s = *s, + .k = 0, + .m = 0, + .skip = 0, + .is_key = false, + .cid = -1, + .add_to_dt = 0, + .key = { + .idx = 0, + .len = 0, + .in_msg = true, + .huff = false, + }, + .i = 0, + .v = 0, + .kind = H2A_NONE, + .flags = 0, }; + u32 i = 0; bpf_for(i, start, len+1) { if (data + i + 1 > data_end) break; u8 c = data[i]; @@ -646,69 +802,60 @@ static __always_inline int _parse_hdr_from(const struct msg_ctx *ctx, u16 start, continue; } - _parse_hpack(c, &ps, &n, &m, &k, &j, &huff); - bpf_trace("hdr: hpack idx: %d, ps: %d, n: %d, k: %d, j: %d", i, ps, n, k, j); - - if (j != 0 && !PS_IS_STR(ps)) continue; - if (ps == H2_IDX) { - add_to_dt = (u8)(n == 6); - *s = s_any; - struct header_field *hf; - _get_table_entry(&ctx->conn, dt_info, k, &hf); - if (hf == NULL) { - cid = -1; - continue; - } + if (ps.skip > 0) { + if (ps.is_key) { + u16 a = 0; + _next(ps.s, c, &ps.s, &a); - cid = _match_header_key(hf->key, hf->key_len, s); - if (cid >= 0) { - // check if we are replacing the exisiting entry, or taking - // the one in the table - if (n == 7) { - pres->ms[cid & MAX_MATCH_MASK] = (struct hdr_match) { - .idx = k, - .len = HEADER_FIELD_MASK, - .in_msg = false, - .huff = false, - }; - } + struct h2_action act = _action(a); + if (act.kind == H2A_CAPTURE) ps.cid = act.val & MAX_MATCH_MASK; } - key.idx = k; - key.in_msg = false; - } - else if (ps == H2_KEY_LEN) { - key.idx = i + 1; - key.len = k; - key.in_msg = true; - key.huff = huff; + + ps.skip--; + // the name ended, so the length of the value comes next. A value + // ends on the transition that announced it, which already leads + // back to `S_FIELD` + if (ps.skip == 0 && ps.is_key) ps.s = S_VAL_LEN; + + continue; } - else if (ps == H2_KEY) { - u16 a = 0; - _next(*s, c, s, &a); - if ((a & a_start_capture) != 0) { - cid = a & a_id_mask & MAX_MATCH_MASK; - } + u16 a = 0; + _next(ps.s, c, &ps.s, &a); + struct h2_action act = _action(a); + + if (act.kind == H2A_INT_START) { + ps.k = act.val; + ps.m = 0; + continue; } - else if (ps == H2_VAL_LEN) { - struct hdr_match val = (struct hdr_match) { - .idx = i + 1, - .len = k, - .in_msg = true, - .huff = huff, - }; - - if (add_to_dt) { - _add_dynamic_table_entry(ctx, dt_info, &key, &val); + if (act.kind == H2A_INT_CONT) { + // an integer wider than the longest block the parser reads is of no + // use, and shifting by more than the width of the accumulator is + // not defined. Such an integer is left short, which makes the field + // it belongs to unresolvable rather than the block unparsable + if (ps.m <= 28) { + ps.k += (u32)(c & 0x7F) << ps.m; + ps.m += 7; } - if (cid >= 0) { - pres->ms[cid & MAX_MATCH_MASK] = val; - cid = -1; - } + continue; + } + + ps.i = i; + ps.v = act.val; + if ((act.flags & H2F_CONT) != 0) { + ps.v = ps.k; + if (ps.m <= 28) ps.v += (u32)(c & 0x7F) << ps.m; } + ps.kind = act.kind; + ps.flags = act.flags; + + if (_run_action(ctx, dt_info, pres, &ps) < 0) break; } + *s = ps.s; + return i; } @@ -756,7 +903,7 @@ int parse_msg(struct sk_msg_md *msg, struct parse_res *pres __arg_nonnull, struc return -(data_end - data); } - u16 s = s_any; + u16 s = S_FIELD; struct msg_ctx ctx = _new_msg_ctx(msg); // the entry is only ever updated below, never deleted, so the pointer @@ -806,7 +953,7 @@ int parse_skb(struct __sk_buff *skb, struct parse_res *pres __arg_nonnull, struc return -(data_end - data); } - u16 s = s_any; + u16 s = S_FIELD; int res = _parse_skb_from(skb, hdr_len, len+hdr_len, &s, pres, null_prefix); if (len + hdr_len > res) return -1; @@ -836,7 +983,7 @@ int parse_buf(const struct bpf_dynptr *buf_ptr, struct ip4_conn *conn, struct pa } u32 cidx[MAX_MATCHES] = { 0 }; - u16 s = s_any; + u16 s = S_FIELD; data = bpf_dynptr_data(buf_ptr, 0, len + hdr_len); if (data == NULL) return -1; diff --git a/beeper/src/h2/parser.rs b/beeper/src/h2/parser.rs index 23d1400..5b899d5 100644 --- a/beeper/src/h2/parser.rs +++ b/beeper/src/h2/parser.rs @@ -1,5 +1,8 @@ #![allow(unused_imports)] -use crate::{Action, Dfa, StateId, autoload_and_attach, h2::create_header_maps}; +use crate::{ + Action, Dfa, StateId, autoload_and_attach, + h2::{create_header_maps, hpack}, +}; use anyhow::{Result, bail}; use as_bytes::AsBytes; use httlib_huffman as huffman; @@ -36,29 +39,21 @@ pub struct Parser { xbpf::include_bpf!("h2/parser"); -/// Encodes a transition the way the BPF parser reads it out of its transition -/// table. +/// Translates the action of a pattern into the one the BPF parser runs. /// -/// An action is a bit field: the flag identifying the action occupies the high -/// bits, the capture id the low ones. -fn new_transition(state: StateId, action: Option, rodata: &rodata) -> trans { - let action = match action { - Some(Action::StartCapture(cid)) => { - rodata.a_start_capture | (cid.0 as u16) & rodata.a_id_mask - } - Some(Action::StartCaptureAndDone(cid)) => { - rodata.a_done | rodata.a_start_capture | (cid.0 as u16) & rodata.a_id_mask +/// A pattern only ever matches a field name, which HPACK announces the length +/// of, so the only action an HTTP/2 pattern carries is the one starting the +/// capture of the value that follows. +fn new_action(action: Option) -> hpack::Action { + match action { + None => hpack::Action::NONE, + Some(Action::StartCapture(cid)) => hpack::Action::capture(cid.0), + Some(Action::Done) | Some(Action::StartCaptureAndDone(..)) => { + unreachable!("an h2 pattern never terminates the parse, it captures and moves on") } - Some(Action::Done) => rodata.a_done, Some(Action::EndCapture(..)) | Some(Action::EndCaptureAndDone(..)) => { unreachable!("HPACK announces the value length, so an h2 pattern never ends a capture") } - None => 0, - }; - - trans { - state: state.0, - action, } } @@ -69,7 +64,7 @@ impl Parser { /// Additional configuration must be done through the builder methods before calling `attach`. pub fn new() -> Parser { Parser { - dfa: Dfa::new(), + dfa: Dfa::with_reserved_states(hpack::S_RESERVED), parse_msg_fn: None, parse_buf_fn: None, parse_skb_fn: None, @@ -169,7 +164,7 @@ impl Parser { huffman::encode(name.as_str().as_bytes(), &mut name_encoded)?; self.dfa - .start_pattern(false) + .start_pattern_at(StateId(hpack::S_NAME)) .push_bytes(&name_encoded) .capture(); @@ -311,15 +306,61 @@ impl Parser { }) } - /// Writes the transition table of the DFA into the read-only data of the - /// parser program. This has to happen before the program is loaded, as the - /// kernel freezes the section afterwards. + /// Writes the transition table of the DFA and the actions its transitions + /// carry into the read-only data of the parser program. This has to happen + /// before the program is loaded, as the kernel freezes the section + /// afterwards. + /// + /// # Errors + /// + /// Returns an error if the patterns do not fit into the tables the parser + /// program reserves for them. fn inject(&self, skel: &mut OpenParserSkel) -> Result<()> { + let mut table = hpack::Table::new(); for (from, to, input, action) in self.dfa.iter_transitions() { - let s = from.0 as usize; - let data = skel.maps.rodata_data.as_mut().unwrap(); - let t = new_transition(*to, action, data); - data.s2ts[s][*input as usize] = t; + table.push_edge(from.0, *input, to.0, new_action(action)); + } + + let Some(data) = skel.maps.rodata_data.as_mut() else { + bail!("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() { + bail!( + "the patterns take {num_states} states, the parser holds {}", + data.s2ts.len() + ); + } + + let num_actions = table.actions().len(); + if num_actions > data.a2as.len() { + bail!( + "the patterns take {num_actions} actions, the parser holds {}", + data.a2as.len() + ); + } + + for hpack::Edge { + from, + input, + to, + action, + } in table.edges() + { + data.s2ts[*from as usize][*input as usize] = trans { + state: *to, + action: *action, + }; + } + + for (i, action) in table.actions().iter().enumerate() { + let (kind, flags) = action.encode(); + data.a2as[i] = h2_action { + val: action.val, + kind, + flags, + }; } Ok(()) From e6183789288d22a185649759d737dae3afda305f Mon Sep 17 00:00:00 2001 From: Laurin Brandner Date: Tue, 1 Sep 2026 10:42:47 +0200 Subject: [PATCH 2/5] readme: update min kernel version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 63f84be..bec13b9 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Beeper (BEEline's ParsER) is an application-layer parser for eBPF. It allows you Protocol | Status | Minimal Kernel Version ------------- | ------- | ---------------------- HTTP/1.1 | ✅ | 6.8 -HTTP/2 | ✅ | 7.0 +HTTP/2 | ✅ | 6.8 gRPC | WIP | ## Build From 411640835624e227d953853e4c58600d2199aed1 Mon Sep 17 00:00:00 2001 From: Laurin Brandner Date: Tue, 1 Sep 2026 14:58:51 +0200 Subject: [PATCH 3/5] h2: support for priority frames --- beeper/src/h2/parser.bpf.c | 328 ++++++++++++++++++++++++------------ beeper/src/h2/parser.rs | 10 ++ beeper/tests/h2.rs | 335 +++++++++++++++++++++++++++++++++++-- 3 files changed, 553 insertions(+), 120 deletions(-) diff --git a/beeper/src/h2/parser.bpf.c b/beeper/src/h2/parser.bpf.c index 44f1e26..fc5f99a 100644 --- a/beeper/src/h2/parser.bpf.c +++ b/beeper/src/h2/parser.bpf.c @@ -24,6 +24,25 @@ // table. #define SETTINGS_HEADER_TABLE_SIZE 0x1 +// The length of a frame header, see section 4.1 of RFC 9113. +#define H2_FRAME_HDR_LEN 9 + +// The frame types the parser reads. Every other one is skipped. +#define H2_HEADERS_FRAME 0x01 +#define H2_SETTINGS_FRAME 0x04 +#define H2_CONTINUATION_FRAME 0x09 + +// The flags of a HEADERS frame that move the header block within it, and the +// one saying that the block ends with the frame rather than carrying on into a +// CONTINUATION frame. See sections 6.2 and 6.10 of RFC 9113. +#define H2_END_HEADERS_FLAG 0x04 +#define H2_PADDED_FLAG 0x08 +#define H2_PRIORITY_FLAG 0x20 + +// The number of bytes the priority of a HEADERS frame takes up, a stream +// dependency and a weight. +#define H2_PRIORITY_LEN 5 + // The HPACK static table. User space populates and freezes it when the parser // is attached. struct { @@ -72,6 +91,13 @@ struct dynamic_table_info { u32 size; u32 max_size; u32 deleted; + + // Whether the table has drifted from the peer's, which happens when a + // header block is split over frames in the middle of a field: the parser + // cannot address the half that is already gone, so the entry the peer adds + // is one it cannot mirror. A table that has drifted is neither added to nor + // resolved from, as its indices no longer mean what the peer means by them. + u32 dirty; }; struct { @@ -261,6 +287,34 @@ static __always_inline u32 hpack_huffman_decoded_len(const u8 *src, u16 src__sz) return n; } +// The offsets of the header block of the frame at `data`, whose payload is +// `len` bytes long. A HEADERS frame may put a pad length and a priority in +// front of its block and pad it at the end, see section 6.2 of RFC 9113. Every +// other frame is nothing but its payload. +// +// Returns 0, or -1 if the frame is too short to hold what its flags announce. +static __always_inline int _h2_block(const u8 *data, const u8 *data_end, u32 len, u8 type, u8 flags, u16 *start, u16 *end) { + u32 off = H2_FRAME_HDR_LEN; + u32 pad_len = 0; + + if (type == H2_HEADERS_FRAME) { + if ((flags & H2_PADDED_FLAG) != 0) { + if (data + off + 1 > data_end) return -1; + pad_len = data[off]; + off += 1; + } + + if ((flags & H2_PRIORITY_FLAG) != 0) off += H2_PRIORITY_LEN; + } + + if (off + pad_len > H2_FRAME_HDR_LEN + len) return -1; + + *start = off; + *end = H2_FRAME_HDR_LEN + len - pad_len; + + return 0; +} + // Everything the parser needs of the message it walks, no matter whether that // message came in as an sk_msg, an sk_buff or a dynptr: the bytes to parse and // the connection they belong to, which is what keys the dynamic table. @@ -393,20 +447,16 @@ static __always_inline void _next(u16 state, u8 input, u16 *next_state, u16 *act *action = t.action; } -// Looks up the oldest entry of the dynamic table, i.e. the one HPACK evicts -// first. `*entry` is NULL if the table is empty. -static __always_inline void _get_lru_dynamic_table_entry(const struct ip4_conn *conn __arg_nonnull, struct dynamic_table_info *dt_info __arg_nonnull, struct dynamic_table_entry **entry) { - u32 end_idx = STATIC_TABLE_SIZE + dt_info->deleted; - bpf_trace("dt: getting LRU entry at index %d", end_idx); - struct dynamic_table_key key = _new_dynamic_table_key(conn, end_idx); - *entry = bpf_map_lookup_elem(&dynamic_table, &key); -} - // Looks up the field the HPACK index `idx` refers to, in the static table if it // is one of the first `STATIC_TABLE_SIZE` indices and in the dynamic table of // `conn` otherwise. `*hf` is NULL if there is no such entry. static __always_inline void _get_table_entry(const struct ip4_conn *conn __arg_nonnull, const struct dynamic_table_info *dt_info __arg_nonnull, u32 idx, struct header_field **hf) { if (idx > STATIC_TABLE_SIZE) { + if (dt_info->dirty) { + *hf = NULL; + return; + } + u32 dt_idx = _get_dynamic_table_index(dt_info, idx); struct dynamic_table_key key = _new_dynamic_table_key(conn, dt_idx); @@ -453,38 +503,48 @@ static __always_inline struct dynamic_table_info* _get_dynamic_table(const struc .size = 0, .max_size = 4096, .deleted = 0, + .dirty = 0, }; bpf_map_update_elem(&dynamic_table_info, conn, &new_info, BPF_ANY); return bpf_map_lookup_elem(&dynamic_table_info, conn); } -// evicts the least recently used entries from the dynamic table to make room for the new entry of size `new_entry_size`. -// returns the number of bytes freed. +// Evicts the oldest entries of the dynamic table until an entry of +// `new_entry_size` fits into it, which is what section 4.4 of RFC 7541 has the +// peer do before adding one. Returns the number of bytes freed. +// +// An entry that does not fit into the empty table frees all of them, and is +// then dropped by the caller, which is what the peer does with it as well. __noinline __weak u32 _try_evict_dynamic_table_entries(const struct msg_ctx *ctx __arg_nonnull, struct dynamic_table_info *dt_info __arg_nonnull, u32 new_entry_size) { bpf_trace("dt: try evicting %dB (%d actual entries)", new_entry_size, dt_info->count); u32 freed = 0; bpf_repeat(dt_info->count) { - if (dt_info->size + new_entry_size < dt_info->max_size) break; + if (dt_info->size + new_entry_size <= dt_info->max_size) break; - struct dynamic_table_entry *last_entry; - _get_lru_dynamic_table_entry(&ctx->conn, dt_info, &last_entry); - if (!last_entry) { - bpf_error("dt: no entries"); + // entries are stored under the running count of the ones added so far, + // so the oldest one that is still live sits right above the evicted + u32 idx = STATIC_TABLE_SIZE + dt_info->deleted; + struct dynamic_table_key key = _new_dynamic_table_key(&ctx->conn, idx); + struct dynamic_table_entry *entry = bpf_map_lookup_elem(&dynamic_table, &key); + if (!entry) { + bpf_error("dt: no entry at index %d", idx); break; } - bpf_trace("dt: evicting LRU entry"); + bpf_trace("dt: evicting %dB entry at index %d", entry->size, idx); + + // the table has to shrink as the entries go, or the loop would not know + // when it has freed enough + dt_info->size -= entry->size; dt_info->count--; dt_info->deleted++; - freed += last_entry->size; + freed += entry->size; - struct dynamic_table_key key = _new_dynamic_table_key(&ctx->conn, dt_info->count - 1); bpf_map_delete_elem(&dynamic_table, &key); } bpf_trace("dt: evicted %dB", freed); - dt_info->size -= freed; return freed; } @@ -497,6 +557,8 @@ __noinline __weak u32 _try_evict_dynamic_table_entries(const struct msg_ctx *ctx // Returns 0 if the entry was added, -1 if it could not be resolved or does not // fit into the table even when emptied, in which case the peer drops it too. __noinline __weak int _add_dynamic_table_entry(const struct msg_ctx *ctx __arg_nonnull, struct dynamic_table_info *dt_info __arg_nonnull, const struct hdr_match *key __arg_nonnull, const struct hdr_match *val __arg_nonnull) { + if (dt_info->dirty) return -1; + u8 *key_ptr = NULL; u32 key_len = 0; bool key_huff = false; @@ -516,7 +578,7 @@ __noinline __weak int _add_dynamic_table_entry(const struct msg_ctx *ctx __arg_n struct dynamic_table_entry *dt_val = bpf_map_lookup_elem(&dynamic_table_entry, &per_cpu_key); if (!dt_val) return -1; - u32 idx = STATIC_TABLE_SIZE + dt_info->count; + u32 idx = STATIC_TABLE_SIZE + dt_info->count + dt_info->deleted; struct dynamic_table_key dt_key = _new_dynamic_table_key(&ctx->conn, idx); __builtin_memset(dt_val, 0, sizeof(*dt_val)); @@ -642,6 +704,38 @@ struct h2_parse_state { u8 flags; }; +// The state of a header block that carries on into a CONTINUATION frame, see +// section 6.10 of RFC 9113. +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 16384); + __type(key, struct ip4_conn); + __type(value, struct h2_parse_state); +} continued_blocks SEC(".maps"); + +// Returns the state a header block is read from its first byte with. +static __always_inline struct h2_parse_state _new_h2_parse_state(void) { + return (struct h2_parse_state) { + .s = S_FIELD, + .k = 0, + .m = 0, + .skip = 0, + .is_key = false, + .cid = -1, + .add_to_dt = 0, + .key = { + .idx = 0, + .len = 0, + .in_msg = true, + .huff = false, + }, + .i = 0, + .v = 0, + .kind = H2A_NONE, + .flags = 0, + }; +} + // Runs the action of the transition `ps` holds, which is what turns the parts // of a field the DFA picked out into a capture, into an entry of the mirrored // dynamic table, or into both. @@ -753,46 +847,25 @@ __noinline __weak int _run_action(const struct msg_ctx *ctx __arg_nonnull, struc // Decodes the header block between the offsets `start` and `end` and records // the values of the fields whose name matches a pattern in `pres`. Fields the // peer adds to its dynamic table are added to the mirrored one, so that later -// blocks can resolve the indices referring to them. `s` is the state the DFA -// walk starts in, which for the beginning of a block is `S_FIELD`. +// blocks can resolve the indices referring to them. `ps` is where the walk +// picks up, which for the beginning of a block is `_new_h2_parse_state`. // // `null_prefix` is the length of the run of NUL bytes at the beginning of the // buffer that is to be skipped rather than parsed; it is updated as those bytes // are consumed. It may be NULL if the data cannot carry such a prefix. // -// Returns the offset it stopped at. -static __always_inline int _parse_hdr_from(const struct msg_ctx *ctx, u16 start, u16 end, u16 *s, struct parse_res *pres, u16 *null_prefix) { +// Returns the offset it stopped at, which is `end` if the whole block was read. +static __always_inline int _parse_hdr_from(const struct msg_ctx *ctx, u16 start, u16 end, struct dynamic_table_info *dt_info, struct h2_parse_state *ps, struct parse_res *pres, u16 *null_prefix) { const u8 *data = ctx->data; const u8 *data_end = ctx->data_end; u32 len = (u32)(data_end - data) & MAX_BYTES; if (end < len) len = end & MAX_BYTES; - if (data + 9 > data_end) return 0; - - struct dynamic_table_info *dt_info = _get_dynamic_table(&ctx->conn); - if (!dt_info) return 0; - - struct h2_parse_state ps = { - .s = *s, - .k = 0, - .m = 0, - .skip = 0, - .is_key = false, - .cid = -1, - .add_to_dt = 0, - .key = { - .idx = 0, - .len = 0, - .in_msg = true, - .huff = false, - }, - .i = 0, - .v = 0, - .kind = H2A_NONE, - .flags = 0, - }; u32 i = 0; bpf_for(i, start, len+1) { + // the block ends before the message does when the message carries more + // than one frame, so the loop cannot lean on the bounds check alone + if (i >= len) break; if (data + i + 1 > data_end) break; u8 c = data[i]; @@ -802,31 +875,31 @@ static __always_inline int _parse_hdr_from(const struct msg_ctx *ctx, u16 start, continue; } - if (ps.skip > 0) { - if (ps.is_key) { + if (ps->skip > 0) { + if (ps->is_key) { u16 a = 0; - _next(ps.s, c, &ps.s, &a); + _next(ps->s, c, &ps->s, &a); struct h2_action act = _action(a); - if (act.kind == H2A_CAPTURE) ps.cid = act.val & MAX_MATCH_MASK; + if (act.kind == H2A_CAPTURE) ps->cid = act.val & MAX_MATCH_MASK; } - ps.skip--; + ps->skip--; // the name ended, so the length of the value comes next. A value // ends on the transition that announced it, which already leads // back to `S_FIELD` - if (ps.skip == 0 && ps.is_key) ps.s = S_VAL_LEN; + if (ps->skip == 0 && ps->is_key) ps->s = S_VAL_LEN; continue; } u16 a = 0; - _next(ps.s, c, &ps.s, &a); + _next(ps->s, c, &ps->s, &a); struct h2_action act = _action(a); if (act.kind == H2A_INT_START) { - ps.k = act.val; - ps.m = 0; + ps->k = act.val; + ps->m = 0; continue; } if (act.kind == H2A_INT_CONT) { @@ -834,35 +907,73 @@ static __always_inline int _parse_hdr_from(const struct msg_ctx *ctx, u16 start, // use, and shifting by more than the width of the accumulator is // not defined. Such an integer is left short, which makes the field // it belongs to unresolvable rather than the block unparsable - if (ps.m <= 28) { - ps.k += (u32)(c & 0x7F) << ps.m; - ps.m += 7; + if (ps->m <= 28) { + ps->k += (u32)(c & 0x7F) << ps->m; + ps->m += 7; } continue; } - ps.i = i; - ps.v = act.val; + ps->i = i; + ps->v = act.val; if ((act.flags & H2F_CONT) != 0) { - ps.v = ps.k; - if (ps.m <= 28) ps.v += (u32)(c & 0x7F) << ps.m; + ps->v = ps->k; + if (ps->m <= 28) ps->v += (u32)(c & 0x7F) << ps->m; } - ps.kind = act.kind; - ps.flags = act.flags; + ps->kind = act.kind; + ps->flags = act.flags; - if (_run_action(ctx, dt_info, pres, &ps) < 0) break; + if (_run_action(ctx, dt_info, pres, ps) < 0) break; } - *s = ps.s; - return i; } -// Decodes the header block carried by an sk_buff, see `_parse_hdr_from`. -static __always_inline int _parse_skb_from(const struct __sk_buff *skb, u16 start, u16 end, u16 *s, struct parse_res *pres, u16 *null_prefix) { - struct msg_ctx ctx = _new_skb_ctx(skb); - return _parse_hdr_from(&ctx, start, end, s, pres, null_prefix); +// Reads the header block of a HEADERS or a CONTINUATION frame, picking up where +// the frame before it left off if the block is split over several of them. +// +// A field whose bytes straddle two frames has half of itself in a frame the +// parser cannot address anymore. The block itself stays readable, as the parser +// only has to count those bytes, but the field can neither be captured nor +// mirrored, so the dynamic table is marked as drifted. +// +// Returns the offset it stopped at, see `_parse_hdr_from`. +static __always_inline int _parse_hdr_frame(const struct msg_ctx *ctx, u16 start, u16 end, u8 type, u8 flags, struct parse_res *pres, u16 *null_prefix) { + struct dynamic_table_info *dt_info = _get_dynamic_table(&ctx->conn); + if (!dt_info) return start; + + struct h2_parse_state ps = _new_h2_parse_state(); + if (type == H2_CONTINUATION_FRAME) { + struct h2_parse_state *resumed = bpf_map_lookup_elem(&continued_blocks, &ctx->conn); + if (resumed == NULL) { + bpf_debug("hdr: a continuation of a block that was not followed"); + return end; + } + + ps = *resumed; + } + + int res = _parse_hdr_from(ctx, start, end, dt_info, &ps, pres, null_prefix); + + if ((flags & H2_END_HEADERS_FLAG) != 0) { + bpf_map_delete_elem(&continued_blocks, &ctx->conn); + return res; + } + + if (ps.skip > 0 && !dt_info->dirty) { + bpf_debug("dt: a field split over two frames, the table has drifted"); + dt_info->dirty = 1; + } + + // the pending field belongs to the frame that is ending, so nothing of it + // survives into the next one + ps.cid = -1; + ps.add_to_dt = 0; + + bpf_map_update_elem(&continued_blocks, &ctx->conn, &ps, BPF_ANY); + + return res; } // Parses the frame the message starts with and describes it in `frame`, so that @@ -881,31 +992,32 @@ int parse_msg(struct sk_msg_md *msg, struct parse_res *pres __arg_nonnull, struc u8 *data = (u8 *)(long)msg->data; u8 *data_end = (u8 *)(long)msg->data_end; - if (data + 9 > data_end) return 0; + if (data + H2_FRAME_HDR_LEN > data_end) return 0; u32 len = data[0] << 16 | data[1] << 8 | data[2]; u8 type = data[3]; u8 flags = data[4]; - bool padded = flags & 0x08; - u8 hdr_len = (padded) ? 10 : 9; + u32 frame_len = H2_FRAME_HDR_LEN + len; *frame = _new_h2_frame(data, type, flags); bpf_debug("Parsing HTTP/2 message with length %d, type %d, flags %d", len, type, flags); - bool is_hdr = (type == 0x01); - bool is_stg = (type == 0x04); + bool is_hdr = (type == H2_HEADERS_FRAME || type == H2_CONTINUATION_FRAME); + bool is_stg = (type == H2_SETTINGS_FRAME); if (!is_hdr && !(is_stg && flags == 0)) { - return len + hdr_len; + return frame_len; } - if (bpf_msg_pull_data(msg, 0, len+hdr_len, 0) < 0) { + if (bpf_msg_pull_data(msg, 0, frame_len, 0) < 0) { return -(data_end - data); } - u16 s = S_FIELD; struct msg_ctx ctx = _new_msg_ctx(msg); + u16 start = 0, end = 0; + if (_h2_block(ctx.data, ctx.data_end, len, type, flags, &start, &end) < 0) return -1; + // the entry is only ever updated below, never deleted, so the pointer // stays good across the parse struct dynamic_table_info *dt_info = _get_dynamic_table(&ctx.conn); @@ -913,16 +1025,17 @@ int parse_msg(struct sk_msg_md *msg, struct parse_res *pres __arg_nonnull, struc int res; if (is_hdr) { - res = _parse_hdr_from(&ctx, hdr_len, len+hdr_len, &s, pres, NULL); + res = _parse_hdr_frame(&ctx, start, end, type, flags, pres, NULL); } else { - res = _parse_stg_from(&ctx, hdr_len, len+hdr_len, &s, pres, NULL); + u16 s = S_FIELD; + res = _parse_stg_from(&ctx, start, end, &s, pres, NULL); } frame->dt_count = dt_info ? dt_info->count : 0; - if (len > hdr_len + res) return -1; + if (res < end) return -1; - return res; + return frame_len; } // Parses the frame the packet starts with, pulling it into the linear part of @@ -933,31 +1046,34 @@ int parse_skb(struct __sk_buff *skb, struct parse_res *pres __arg_nonnull, struc u8 *data = (u8 *)(long)skb->data; u8 *data_end = (u8 *)(long)skb->data_end; - if (data + 9 > data_end) return 0; + if (data + H2_FRAME_HDR_LEN > data_end) return 0; u32 len = data[0] << 16 | data[1] << 8 | data[2]; u8 type = data[3]; u8 flags = data[4]; - bool padded = flags & 0x08; - u8 hdr_len = (padded) ? 10 : 9; + u32 frame_len = H2_FRAME_HDR_LEN + len; *frame = _new_h2_frame(data, type, flags); bpf_debug("Parsing HTTP/2 sk_buff with length %d, type %d, flags %d", len, type, flags); - if (type != 0x01) { - return len + hdr_len; + if (type != H2_HEADERS_FRAME && type != H2_CONTINUATION_FRAME) { + return frame_len; } - if (bpf_skb_pull_data(skb, len+hdr_len) < 0) { + if (bpf_skb_pull_data(skb, frame_len) < 0) { return -(data_end - data); } - u16 s = S_FIELD; - int res = _parse_skb_from(skb, hdr_len, len+hdr_len, &s, pres, null_prefix); - if (len + hdr_len > res) return -1; + struct msg_ctx ctx = _new_skb_ctx(skb); - return res; + u16 start = 0, end = 0; + if (_h2_block(ctx.data, ctx.data_end, len, type, flags, &start, &end) < 0) return -1; + + int res = _parse_hdr_frame(&ctx, start, end, type, flags, pres, null_prefix); + if (res < end) return -1; + + return frame_len; } // Parses the frame `buf_ptr` starts with. A buffer carries no connection of its @@ -971,33 +1087,29 @@ int parse_buf(const struct bpf_dynptr *buf_ptr, struct ip4_conn *conn, struct pa u32 len = data[0] << 16 | data[1] << 8 | data[2]; u8 type = data[3]; u8 flags = data[4]; - bool padded = flags & 0x08; - u8 hdr_len = (padded) ? 10 : 9; + u32 frame_len = H2_FRAME_HDR_LEN + len; *frame = _new_h2_frame(data, type, flags); bpf_debug("Parsing HTTP/2 buf with length %d, type %d, flags %d", len, type, flags); - if (type != 0x01) { - return len + hdr_len; + if (type != H2_HEADERS_FRAME && type != H2_CONTINUATION_FRAME) { + return frame_len; } - u32 cidx[MAX_MATCHES] = { 0 }; - u16 s = S_FIELD; - - data = bpf_dynptr_data(buf_ptr, 0, len + hdr_len); + data = bpf_dynptr_data(buf_ptr, 0, frame_len); if (data == NULL) return -1; - u8 *data_end = data + len + hdr_len; struct msg_ctx ctx = { .data = data, - .data_end = data_end, + .data_end = data + frame_len, .conn = *conn }; - int res = _parse_hdr_from(&ctx, hdr_len, len+hdr_len, &s, pres, null_prefix); + u16 start = 0, end = 0; + if (_h2_block(ctx.data, ctx.data_end, len, type, flags, &start, &end) < 0) return -1; - return res; + return _parse_hdr_frame(&ctx, start, end, type, flags, pres, null_prefix); } // Reads the `idx`th entry of `conn`'s dynamic table into `out`, `idx` counted diff --git a/beeper/src/h2/parser.rs b/beeper/src/h2/parser.rs index 5b899d5..590c9f4 100644 --- a/beeper/src/h2/parser.rs +++ b/beeper/src/h2/parser.rs @@ -400,6 +400,16 @@ pub struct DynamicTableInfo { /// The number of entries evicted so far. Together with `count` it turns an /// HPACK index into an index into the table. pub deleted: u32, + + /// Whether the table has drifted from the peer's and can no longer be + /// trusted. + /// + /// It drifts when a header block is split over a HEADERS frame and the + /// CONTINUATION frames following it in the middle of a field, see section + /// 6.10 of RFC 9113: the parser cannot address the half of the field that + /// is in the frame before, so the entry the peer adds is one it cannot + /// mirror. A table that has drifted is neither added to nor resolved from. + pub dirty: u32, } unsafe impl Plain for DynamicTableInfo {} diff --git a/beeper/tests/h2.rs b/beeper/tests/h2.rs index 168411f..26667ab 100644 --- a/beeper/tests/h2.rs +++ b/beeper/tests/h2.rs @@ -90,6 +90,25 @@ impl Client { &self, uri: String, headers: &[(header::HeaderName, HeaderValue)], + ) -> Response { + let response = self.send(uri, headers).await; + assert!( + response.status().is_success(), + "status: {}", + response.status() + ); + + response + } + + /// Same as [`Client::get`], but does not expect the server to have accepted + /// the request. A header list the server turns down still reaches the + /// parser, which is all some tests need of it. + #[allow(unused_results)] + async fn send( + &self, + uri: String, + headers: &[(header::HeaderName, HeaderValue)], ) -> Response { let mut req = Request::builder().method("GET").uri(uri); for (name, value) in headers { @@ -101,11 +120,7 @@ impl Client { let (response, _) = send_request .send_request(request, true) .expect("send_request"); - let response = response.await.expect("response"); - - assert!(response.status().is_success()); - - response + response.await.expect("response") } } @@ -208,19 +223,50 @@ impl RawClient { /// Sends a request carrying `block` and waits for its response, so that the /// parser has seen it by the time this returns. async fn request(&mut self, block: Vec) { + self.request_all(&[(0, block)]).await; + } + + /// Sends a request whose header block is split at `split`, the first half + /// going out in the HEADERS frame and the second in a CONTINUATION frame, + /// see section 6.10 of RFC 9113. + async fn request_continued(&mut self, block: Vec, split: usize) { let id = self.next_stream_id; self.next_stream_id += 2; - // END_STREAM | END_HEADERS - self.stream - .write_all(&frame(0x01, 0x05, id, &block)) - .await - .expect("request"); + let mut out = Vec::new(); + // END_STREAM, but the block carries on + out.extend_from_slice(&frame(0x01, 0x01, id, &block[..split])); + // CONTINUATION | END_HEADERS + out.extend_from_slice(&frame(0x09, 0x04, id, &block[split..])); + + self.stream.write_all(&out).await.expect("request"); self.stream.flush().await.expect("flush"); self.read_frame(0x01).await; } + /// Sends every request in `reqs` in a single write, so that the parser has + /// to find each frame by the length of the one before it. Each of them is + /// the flags its HEADERS frame carries on top of END_STREAM and + /// END_HEADERS, and the payload of that frame, which the caller lays out + /// itself rather than handing over a bare block. + async fn request_all(&mut self, reqs: &[(u8, Vec)]) { + let mut out = Vec::new(); + for (flags, payload) in reqs { + let id = self.next_stream_id; + self.next_stream_id += 2; + + out.extend_from_slice(&frame(0x01, 0x05 | flags, id, payload)); + } + + self.stream.write_all(&out).await.expect("request"); + self.stream.flush().await.expect("flush"); + + for _ in reqs { + self.read_frame(0x01).await; + } + } + /// Writes `bytes` as they are, without expecting an answer. /// /// A malformed frame is answered with a GOAWAY at best, so there is nothing @@ -826,13 +872,278 @@ async fn evict_header_field_from_dynamic_table() { ) .await; - // this should evict all entries, and add back the user-agent + // this should evict the authority, the oldest entry, and nothing more let info = h2 .dynamic_table_info(client.local_addr, client.remote_addr) .expect("dynamic_table_info"); - let expected_dt = &[(header::USER_AGENT, test_header_val.clone())]; + let expected_dt = &[ + (TEST_HEADER, test_header_val.clone()), + (header::USER_AGENT, user_agent_val.clone()), + (header::USER_AGENT, test_header_val.clone()), + ]; assert_eq!(info.max_size, 254); assert_eq!(info.count, expected_dt.len() as u32); assert_eq!(info.size, dynamic_table_size_for_headers(expected_dt)); + assert_eq!(info.deleted, 1); assert_match_eq(&prog, 1, Some(&test_header_val)); } + +/// The flag of a HEADERS frame saying that its block is padded, see section 6.2 +/// of RFC 9113. +const PADDED_FLAG: u8 = 0x08; + +/// The flag saying that a priority comes in front of its block. +const PRIORITY_FLAG: u8 = 0x20; + +/// Renders the payload of a HEADERS frame that pads `block` with `pad`, see +/// section 6.2 of RFC 9113: the length of the padding, the block, and the +/// padding itself. +fn padded(block: Vec, pad: &[u8]) -> Vec { + let mut payload = vec![pad.len() as u8]; + payload.extend_from_slice(&block); + payload.extend_from_slice(pad); + + payload +} + +/// Renders the payload of a HEADERS frame that puts a priority in front of +/// `block`, see section 6.3 of RFC 9113: a stream dependency and a weight. +fn prioritised(block: Vec) -> Vec { + let mut payload = vec![0x00, 0x00, 0x00, 0x00, 0x10]; + payload.extend_from_slice(&block); + + payload +} + +#[tokio::test] +async fn parse_padded_header_frame() { + let addr = server::launch().await.expect("launch server"); + + let mut open_obj = OpenObject::new(); + let prog = + TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program"); + + let _h1 = attach_preface_parser(prog.prog_fd()); + let h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]); + + let authority = addr.to_string(); + let padded_val = HeaderValue::from_static("padded"); + let next_val = HeaderValue::from_static("after-the-padding"); + + // the padding is a field that would be added to the dynamic table if it + // were read as one, which is how the test tells that it was skipped + let pad = [0x40, 0x00, 0x00]; + + // both requests go out in a single write, so the second is only found if + // the padded frame reported its own length correctly + let mut client = RawClient::connect(addr).await; + client + .request_all(&[ + ( + PADDED_FLAG, + padded( + raw_request_block(&authority, &[(Some(19), "accept", "padded")]), + &pad, + ), + ), + ( + 0, + raw_request_block(&authority, &[(Some(19), "accept", "after-the-padding")]), + ), + ]) + .await; + + assert_eq!( + prog.get_match(0).expect("get_match").as_deref(), + Some(next_val.as_bytes()), + "the frame after the padded one was not found" + ); + + let info = h2 + .dynamic_table_info(client.local_addr, client.remote_addr) + .expect("dynamic_table_info"); + + let expected_dt = &[ + (header::ACCEPT, padded_val.clone()), + (header::ACCEPT, next_val.clone()), + ]; + assert_eq!( + info.count, + expected_dt.len() as u32, + "the padding was read as a header field" + ); + assert_eq!(info.size, dynamic_table_size_for_headers(expected_dt)); +} + +#[tokio::test] +async fn parse_header_frame_that_carries_a_priority() { + let addr = server::launch().await.expect("launch server"); + + let mut open_obj = OpenObject::new(); + let prog = + TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program"); + + let _h1 = attach_preface_parser(prog.prog_fd()); + let h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]); + + let authority = addr.to_string(); + let accept_val = HeaderValue::from_static("after-the-priority"); + + let mut client = RawClient::connect(addr).await; + client + .request_all(&[( + PRIORITY_FLAG, + prioritised(raw_request_block( + &authority, + &[(Some(19), "accept", "after-the-priority")], + )), + )]) + .await; + + assert_eq!( + prog.get_match(0).expect("get_match").as_deref(), + Some(accept_val.as_bytes()), + "the block was not read from behind the priority" + ); + + let info = h2 + .dynamic_table_info(client.local_addr, client.remote_addr) + .expect("dynamic_table_info"); + + let expected_dt = &[(header::ACCEPT, accept_val.clone())]; + assert_eq!( + info.count, + expected_dt.len() as u32, + "the priority was read as a header field" + ); + assert_eq!(info.size, dynamic_table_size_for_headers(expected_dt)); +} + +#[tokio::test] +async fn resolve_index_of_entry_added_after_an_eviction() { + let addr = server::launch().await.expect("launch server"); + + let mut open_obj = OpenObject::new(); + let prog = + TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program"); + + let _h1 = attach_preface_parser(prog.prog_fd()); + let h2 = attach_h2_parser(prog.prog_fd(), &[TEST_HEADER, header::USER_AGENT]); + + let long_val = HeaderValue::from_static("asdfqwerasdfqwerasdfqwerasdfqwer"); + let agent_val = HeaderValue::from_static("test-agent"); + let other_agent_val = HeaderValue::from_static("other-agent"); + let url = format!("http://{}", addr); + + // a table this small fills up over the three requests below, the last of + // which evicts the authority the connection opened with + let client = Client::connect(addr, Some(254)).await; + client + .get(url.clone(), &[(TEST_HEADER, long_val.clone())]) + .await; + client + .get(url.clone(), &[(header::USER_AGENT, agent_val.clone())]) + .await; + client + .get(url.clone(), &[(header::USER_AGENT, long_val.clone())]) + .await; + + let info = h2 + .dynamic_table_info(client.local_addr, client.remote_addr) + .expect("dynamic_table_info"); + assert_eq!( + info.deleted, 1, + "nothing was evicted, so the entries below are stored where they would be anyway" + ); + + // this one is added to a table that has already evicted, which is what + // decides whether the entries added before it keep the index they were + // stored under + client + .get(url.clone(), &[(header::USER_AGENT, other_agent_val.clone())]) + .await; + assert_match_eq(&prog, 1, Some(&other_agent_val)); + + // the client still holds the long user agent, so it sends it as nothing but + // the index of the entry it was added under before the eviction + client + .get(url.clone(), &[(header::USER_AGENT, long_val.clone())]) + .await; + + assert_match_eq(&prog, 1, Some(&long_val)); +} +#[tokio::test] +async fn parse_header_block_split_over_a_continuation_frame() { + let addr = server::launch().await.expect("launch server"); + + let mut open_obj = OpenObject::new(); + let prog = + TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program"); + + let _h1 = attach_preface_parser(prog.prog_fd()); + let h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]); + + let authority = addr.to_string(); + let accept_val = HeaderValue::from_static("in-the-continuation"); + let block = raw_request_block(&authority, &[(Some(19), "accept", "in-the-continuation")]); + + // the block breaks right after the three indexed pseudo headers it opens + // with, so every field of it is whole in the frame that carries it + let mut client = RawClient::connect(addr).await; + client.request_continued(block, 3).await; + + assert_eq!( + prog.get_match(0).expect("get_match").as_deref(), + Some(accept_val.as_bytes()), + "the field in the continuation frame was not read" + ); + + let info = h2 + .dynamic_table_info(client.local_addr, client.remote_addr) + .expect("dynamic_table_info"); + + let expected_dt = &[(header::ACCEPT, accept_val.clone())]; + assert_eq!(info.count, expected_dt.len() as u32); + assert_eq!(info.size, dynamic_table_size_for_headers(expected_dt)); + assert_eq!( + info.dirty, 0, + "a block that breaks between fields left the table looking untrustworthy" + ); +} + +#[tokio::test] +async fn mark_the_table_as_drifted_when_a_continuation_frame_splits_a_field() { + let addr = server::launch().await.expect("launch server"); + + let mut open_obj = OpenObject::new(); + let prog = + TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program"); + + let _h1 = attach_preface_parser(prog.prog_fd()); + let h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]); + + let authority = addr.to_string(); + let accept_val = HeaderValue::from_static("across-the-break"); + let block = raw_request_block(&authority, &[(Some(19), "accept", "across-the-break")]); + + // the block breaks two bytes into the authority, whose first half is in a + // frame the parser cannot address once the second one arrives + let mut client = RawClient::connect(addr).await; + client.request_continued(block, 3 + 1 + 1 + 2).await; + + // the fields behind the break are still read, the parser only loses the one + // the break falls inside of + assert_eq!( + prog.get_match(0).expect("get_match").as_deref(), + Some(accept_val.as_bytes()), + "the field behind the break was not read" + ); + + let info = h2 + .dynamic_table_info(client.local_addr, client.remote_addr) + .expect("dynamic_table_info"); + assert_eq!( + info.dirty, 1, + "a block that breaks inside a field left the table looking trustworthy" + ); +} From bc80aa7e24a7e3a13a9bdf854994da3985c9f8d3 Mon Sep 17 00:00:00 2001 From: Laurin Brandner Date: Wed, 2 Sep 2026 14:26:22 +0200 Subject: [PATCH 4/5] h2: replace table with dfa --- beeper/src/dfa.rs | 315 ++++++++---------- beeper/src/h1/action.rs | 52 +++ beeper/src/h1/mod.rs | 1 + beeper/src/h1/parser.bpf.c | 92 +++--- beeper/src/h1/parser.rs | 142 +++++---- beeper/src/h2/action.rs | 140 ++++++++ beeper/src/h2/hpack.rs | 632 ++++++++++++++++--------------------- beeper/src/h2/mod.rs | 134 +------- beeper/src/h2/parser.rs | 97 +++--- beeper/src/lib.rs | 74 +---- 10 files changed, 782 insertions(+), 897 deletions(-) create mode 100644 beeper/src/h1/action.rs create mode 100644 beeper/src/h2/action.rs diff --git a/beeper/src/dfa.rs b/beeper/src/dfa.rs index 2dfb435..2a0d3f4 100644 --- a/beeper/src/dfa.rs +++ b/beeper/src/dfa.rs @@ -1,27 +1,38 @@ -use crate::{Action, CaptureId, MatchId, StateId}; -use std::{collections::HashMap, ops::RangeBounds}; +use crate::StateId; +use std::{collections::HashMap, fmt::Debug, ops::RangeBounds}; use tracing::trace; /// The state a message is parsed from. Only patterns that must appear at the /// very beginning of a message are anchored here. -const INIT_STATE: StateId = StateId(0); +pub const INIT_STATE: StateId = StateId(0); /// The state input that matches no pattern leads back to. Patterns that may /// appear anywhere in the header block are anchored here. -const ANY_STATE: StateId = StateId(1); +pub const ANY_STATE: StateId = StateId(1); /// The input a state matches any byte with. The parser only follows it if the /// state has no transition for the byte it read. const ANY_INPUT: u8 = '*' as u8; +/// A single transition of a [`Dfa`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct Edge { + /// The state the transition leads to. + to: StateId, + + /// The action it carries, if it carries one of its own rather than the one + /// of the state it leads to. + action: Option, +} + /// Builds a single pattern into a [`Dfa`]. /// /// The builder walks the DFA from the state the pattern is anchored at, /// inserting states and edges as it goes. Patterns share their states, so /// pushing an input another pattern already pushed reuses its state instead of /// creating a new one. -pub struct DfaBuilder<'a> { - dfa: &'a mut Dfa, +pub struct DfaBuilder<'a, A: Copy + Debug + PartialEq + Eq> { + dfa: &'a mut Dfa, /// The state the pattern has been built up to. state: StateId, @@ -31,49 +42,66 @@ pub struct DfaBuilder<'a> { /// the state it branched off of. optional_prefixes: Vec<(String, bool)>, - /// The capture the next input starts, if any. - start_capture: Option, - - /// The capture [`DfaBuilder::end_capturing`] closes, if a capture is open. - end_capture: Option, + /// All edges that lead into [`DfaBuilder::state`]. + last_edges: Vec<(StateId, u8, bool)>, } -impl DfaBuilder<'_> { - fn new(dfa: &mut Dfa, state: StateId) -> DfaBuilder<'_> { +impl DfaBuilder<'_, A> { + fn new(dfa: &mut Dfa, state: StateId) -> DfaBuilder<'_, A> { DfaBuilder { dfa, state, optional_prefixes: Vec::new(), - start_capture: None, - end_capture: None, + last_edges: Vec::new(), } } - /// Appends a single input to the pattern, first building the optional - /// prefixes that were pushed since the last input and starting a capture if - /// one is pending. - fn push_edge(&mut self, input: u8, to: Option, case_sensitive: bool) { + /// Attaches `action` to every transition that leads into the state the + /// pattern has been built up to. + pub fn with(&mut self, action: A) -> &mut Self { + trace!("with; state={:?}, action={:?}", self.state, action); + + // an optional prefix loops back into the current state, so it is one of + // the routes into it and has to carry the action too + self.push_optional_prefixes(); + + for (from, input, case_sensitive) in self.last_edges.clone() { + if case_sensitive { + self.dfa.add_action(from, input, action); + } else { + self.dfa + .add_action(from, input.to_ascii_lowercase(), action); + self.dfa + .add_action(from, input.to_ascii_uppercase(), action); + } + } + + self + } + + /// Builds the optional prefixes that were pushed since the last input, each + /// of them leading back into the state the pattern has been built up to. + fn push_optional_prefixes(&mut self) { let start = self.state; while let Some((optional, case_sensitive)) = self.optional_prefixes.pop() { let mut from = start; for (i, b) in optional.as_bytes().iter().enumerate() { let to = if i == optional.len() - 1 { + self.last_edges.push((from, *b, case_sensitive)); Some(start) } else { None }; + from = self.push_edge_from(from, *b, to, case_sensitive); } } + } - if let Some(id) = self.start_capture.take() { - trace!("start_capturing; state={:?}, cid={:?} ", self.state, id); - - if self.state != INIT_STATE { - self.dfa.add_action(self.state, Action::StartCapture(id)); - } - self.end_capture = Some(id); - } + /// Appends a single input to the pattern, first building the optional + /// prefixes that were pushed since the last input. + fn push_edge(&mut self, input: u8, to: Option, case_sensitive: bool) { + self.push_optional_prefixes(); trace!( "push_edge; state={:?}, input={}, to={:?}", @@ -82,12 +110,15 @@ impl DfaBuilder<'_> { to ); + let start = self.state; self.state = self.push_edge_from(start, input, to, case_sensitive); + self.last_edges = vec![(start, input, case_sensitive)]; } - /// Inserts an edge for both the lower and the upper case of `input` and - /// returns the state they lead to. If `to` is `None`, the edge leads to the - /// state the DFA already has for `input`, or to a new one. + /// Inserts an edge for both the lower and the upper case of `input`, + /// carrying `action`, and returns the state they lead to. If `to` is + /// `None`, the edge leads to the state the DFA already has for `input`, or + /// to a new one. fn push_edge_from( &mut self, from: StateId, @@ -97,7 +128,7 @@ impl DfaBuilder<'_> { ) -> StateId { let to = to.unwrap_or(self.dfa.next_state(&from, &input)); - self.dfa.insert_edge(from, input, to); + self.dfa.insert_edge(from, input, to, None); if !case_sensitive { let other_case = if input.is_ascii_lowercase() { input.to_ascii_uppercase() @@ -105,7 +136,7 @@ impl DfaBuilder<'_> { input.to_ascii_lowercase() }; if other_case != input { - self.dfa.insert_edge(from, other_case, to); + self.dfa.insert_edge(from, other_case, to, None); } } @@ -166,19 +197,16 @@ impl DfaBuilder<'_> { self.optional_prefixes.push((prefix, true)); } + // the loop leads back into the state the repetition ends in, so it is + // one of the routes into it if matches!(range.end_bound(), std::ops::Bound::Unbounded) { self.push_edge_from(self.state, ANY_INPUT, Some(self.state), true); + self.last_edges.push((self.state, ANY_INPUT, true)); } self } - /// Pushes one branch per input onto the [`Dfa`]. One of the branches - /// must be matched case sensitively for the [`Dfa`] to reach a final state. - // pub fn push_options(&mut self, inputs: &[&str]) -> &mut Self { - // self.push_options_inner(inputs, true) - // } - /// Same as [`push_options`], but case insensitive. pub fn push_options_ci(&mut self, inputs: &[&str]) -> &mut Self { self.push_options_inner(inputs, false) @@ -195,6 +223,10 @@ impl DfaBuilder<'_> { self.push_inner(longest.as_bytes(), case_sensitive); let final_state = self.state; + // every option ends in the same state, so every one of them is a route + // into it + let mut last_edges = std::mem::take(&mut self.last_edges); + trace!( "push_options; state={:?}, longest={}, final_state={:?}", start, @@ -214,8 +246,12 @@ impl DfaBuilder<'_> { }; self.push_edge(*b, to, case_sensitive); } + + last_edges.append(&mut self.last_edges); } + self.last_edges = last_edges; + self } @@ -225,65 +261,6 @@ impl DfaBuilder<'_> { self } - /// Same as [`push_optional`], but case-insensitive. - // pub fn push_optional_ci(&mut self, input: &str) -> &mut Self { - // self.optional_prefixes.push((input.to_string(), false)); - // self - // } - - /// Starts capturing at the next input pushed onto the pattern. - /// - /// # Panics - /// - /// Panics if a capture has already been started but not yet pushed. - pub fn start_capturing(&mut self) -> &mut Self { - assert!(self.start_capture.is_none()); - let cid = self.dfa.new_capture(); - - self.start_capture = Some(cid); - - self - } - - /// Starts capturing at the state the pattern has been built up to, rather - /// than at the next input pushed onto it. - /// - /// It is meant for a parser that does not have to match the end of the - /// range it captures, and therefore never calls - /// [`DfaBuilder::end_capturing`]: HPACK prefixes a field value with its - /// length, so the HTTP/2 pattern matching a field name ends where the - /// capture begins. - /// - /// # Panics - /// - /// Panics if a capture has already been started but not yet pushed. - pub fn capture(&mut self) -> &mut Self { - let cid = self.dfa.new_capture(); - self.dfa.add_action(self.state, Action::StartCapture(cid)); - - self - } - - /// Ends the open capture at the last input pushed onto the pattern and - /// turns the captured range into a match. - /// - /// # Panics - /// - /// Panics if no capture has been started. - pub fn end_capturing(&mut self) -> &mut Self { - let cid = self.end_capture.take().expect("No capture started"); - let mid = self.dfa.new_match(); - trace!( - "end_capturing; state={:?}, cid={:?} mid={:?}", - self.state, cid, mid - ); - - self.dfa - .add_action(self.state, Action::EndCapture(cid, mid)); - - self - } - /// Matches the given input string but sets the final state /// to the state the DFA would be in if it started from [`ANY_STATE`]. pub fn restart_with(&mut self, input: &str) { @@ -309,56 +286,36 @@ impl DfaBuilder<'_> { self.push_edge(*b, to, false); } } - - /// Terminates parsing once the pattern has been matched. - pub fn done(&mut self) { - self.dfa.add_action(self.state, Action::Done); - } } -type EdgeMap = HashMap>; -type ActionMap = HashMap; +type EdgeMap = HashMap>>; /// The DFA the patterns of a [`Parser`](super::Parser) are compiled into. /// /// It is injected into the BPF parser program as a table of transitions, /// indexed by state and input byte, which is why states are shared between -/// patterns wherever possible. -pub(crate) struct Dfa { - /// The number of captures handed out so far. - num_captures: u16, - - /// The number of matches handed out so far. - num_matches: u16, - +/// patterns wherever possible. A transition names the action it carries by the +/// index it is held under in [`Dfa::actions`], so that an action can say more +/// than the 16 bits of a transition have room for. +pub(crate) struct Dfa { /// The number of states, including [`INIT_STATE`] and [`ANY_STATE`]. num_states: u16, /// The transitions of the DFA, keyed by state and input. - edges: EdgeMap, - - /// The action a state runs when it is entered. - actions: ActionMap, + edges: EdgeMap, } -impl Dfa { +impl Dfa { /// Creates a DFA that holds nothing but [`INIT_STATE`] and [`ANY_STATE`]. - pub fn new() -> Dfa { + pub fn new() -> Dfa { Dfa::with_reserved_states(2) } /// Creates a DFA that leaves the first `reserved` state ids to the caller. - /// - /// A parser that anchors its patterns at states it lays out itself, as the - /// HTTP/2 one anchors them at the root of its field name trie, keeps those - /// ids and lets the patterns take the ones above them. - pub fn with_reserved_states(reserved: u16) -> Dfa { + pub fn with_reserved_states(reserved: u16) -> Dfa { Dfa { - num_captures: 0, - num_matches: 0, num_states: reserved.max(2), edges: HashMap::new(), - actions: HashMap::new(), } } @@ -367,42 +324,22 @@ impl Dfa { self.num_states } - /// Starts a new pattern. - /// - /// A `head` pattern is anchored at [`INIT_STATE`] and therefore only - /// matches at the very beginning of a message, any other pattern is - /// anchored at [`ANY_STATE`] and may match anywhere in the header block. - pub fn start_pattern<'a>(&'a mut self, head: bool) -> DfaBuilder<'a> { - trace!("start_pattern; head={:?}", head); - let state = if head { INIT_STATE } else { ANY_STATE }; - self.start_pattern_at(state) + /// Returns the number of edges the DFA has. + pub fn num_edges(&self) -> usize { + self.edges.len() } /// Starts a new pattern anchored at `state`, which the caller has to have /// reserved with [`Dfa::with_reserved_states`]. - pub fn start_pattern_at<'a>(&'a mut self, state: StateId) -> DfaBuilder<'a> { - trace!("start_pattern_at; state={:?}", state); + pub fn start_pattern<'a>(&'a mut self, state: StateId) -> DfaBuilder<'a, A> { + trace!("start_pattern; state={:?}", state); DfaBuilder::new(self, state) } /// Returns an unused state id. fn new_state(&mut self) -> StateId { let id = StateId(self.num_states); - self.num_states += 1; - id - } - - /// Returns an unused capture id. - fn new_capture(&mut self) -> CaptureId { - let id = CaptureId(self.num_captures); - self.num_captures += 1; - id - } - - /// Returns an unused match id. - fn new_match(&mut self) -> MatchId { - let id = MatchId(self.num_matches); - self.num_matches += 1; + self.num_states = self.num_states.strict_add(1); id } @@ -411,52 +348,72 @@ impl Dfa { fn next_state(&mut self, from: &StateId, input: &u8) -> StateId { self.edges .get(from) - .and_then(|es| es.get(input).map(|to| *to)) + .and_then(|es| es.get(input).map(|edge| edge.to)) .unwrap_or_else(|| self.new_state()) } /// Inserts an edge from `from` to `to`, matching `input`. /// + /// `action` is the action the edge carries itself, which a parser whose + /// transitions mean more than the state they lead to needs; an edge without + /// one runs the action of `to`. + /// /// # Panics /// /// Panics if `from` already has an edge for `input` that leads somewhere - /// else, as that would make the automaton non-deterministic. - fn insert_edge(&mut self, from: StateId, input: u8, to: StateId) { - if let Some(to_old) = self.edges.entry(from).or_default().insert(input, to) { - assert!( - to_old == to, - "Cannot create a transition from {from:?} to {to_old:?} and {to:?}" - ); + /// else, as that would make the automaton non-deterministic, or if it + /// carries an action `action` cannot be combined with. + pub fn insert_edge(&mut self, from: StateId, input: u8, to: StateId, action: Option) { + let edges = self.edges.entry(from).or_default(); + let Some(old) = edges.get_mut(&input) else { + let _ = edges.insert(input, Edge { to, action }); + return; + }; + + assert!( + old.to == to, + "Cannot create a transition from {from:?} to {:?} and {to:?}", + old.to + ); + + // patterns share their transitions wherever they run alongside each + // other, so one walking over a transition another already laid down + // leaves the action on it alone + match (old.action, action) { + (_, None) => {} + (None, Some(action)) => old.action = Some(action), + (Some(old_action), Some(action)) => assert!( + old_action == action, + "Cannot {action:?} and {old_action:?} on the same transition" + ), } } - /// Attaches `action` to `state`, combining it with the action the state - /// already carries. + /// Adds an action to an existing edge. /// /// # Panics /// - /// Panics if the two actions cannot be combined, see [`Action::push`]. - fn add_action(&mut self, state: StateId, action: Action) { - let action = match self.actions.get(&state) { - Some(action_old) => action_old - .push(action) - .unwrap_or_else(|err| panic!("Cannot add {action:?} to {state:?}: {err}")), - None => action, + /// Panics if the edge does not exist, or already has an action assigned. + fn add_action(&mut self, from: StateId, input: u8, action: A) { + let Some(edges) = self.edges.get_mut(&from) else { + panic!("State not found"); + }; + + let Some(edge) = edges.get_mut(&input) else { + panic!("Edge not found"); }; - self.actions.insert(state, action); + edge.action = Some(action); } - /// Returns an iterator over the edges of the DFA, each paired with the - /// action of the state it leads to. - pub fn iter_transitions<'a>( - &'a self, - ) -> impl Iterator)> { + /// Returns an iterator over the transitions of the DFA, each paired with + /// the id of the action it carries: its own if it has one, and the one of + /// the state it leads to otherwise. + pub fn iter_transitions(&self) -> impl Iterator)> + '_ { self.edges.iter().flat_map(move |(from, edges)| { - edges.iter().map(move |(input, to)| { - let action = self.actions.get(to).copied(); - (from, to, input, action) - }) + edges + .iter() + .map(move |(input, edge)| (*from, *input, edge.to, edge.action)) }) } } diff --git a/beeper/src/h1/action.rs b/beeper/src/h1/action.rs new file mode 100644 index 0000000..bfb67d1 --- /dev/null +++ b/beeper/src/h1/action.rs @@ -0,0 +1,52 @@ +//! What the HTTP/1.x parser does upon taking a transition. +//! +//! The kinds and flags below must stay in sync with the `H1A_*` and `H1F_*` +//! constants of h1/parser.bpf.c. + +use crate::{MatchId, h1::parser::types::h1_action}; + +/// The parser does nothing. +const H1A_NONE: u8 = 0; + +/// A capture starts at the byte behind the transition. +const H1A_START_CAPTURE: u8 = 1; + +/// The open capture ends at the byte the transition read. +const H1A_END_CAPTURE: u8 = 2; + +/// Parsing is complete, the rest of the message is not a header anymore. +const H1F_DONE: u8 = 1 << 0; + +/// The action a transition of the HTTP/1.x parser carries. +/// +/// A transition either opens or closes a capture, and may on top of that end +/// the parse. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Action { + /// Starts capturing a range, which begins at the byte behind the + /// transition and is identified by the capture id. + StartCapture(MatchId), + + /// Ends the capture the first id names at the byte the transition read, and + /// reports the range it covers under the match id the second one names. + EndCapture(MatchId), + + /// Terminates parsing. + Done, + + /// Ends capturing a range and terminates parsing. + EndCaptureAndDone(MatchId), +} + +impl From for h1_action { + fn from(value: Action) -> Self { + let (kind, flags, mid) = match value { + Action::Done => (H1A_NONE, H1F_DONE, 0), + Action::StartCapture(mid) => (H1A_START_CAPTURE, 0, mid.0 as u8), + Action::EndCapture(mid) => (H1A_END_CAPTURE, 0, mid.0 as u8), + Action::EndCaptureAndDone(mid) => (H1A_END_CAPTURE, H1F_DONE, mid.0 as u8), + }; + + h1_action { kind, flags, mid } + } +} diff --git a/beeper/src/h1/mod.rs b/beeper/src/h1/mod.rs index b606c8f..2a8b700 100644 --- a/beeper/src/h1/mod.rs +++ b/beeper/src/h1/mod.rs @@ -5,6 +5,7 @@ //! message byte by byte, follows the table and runs the action of every state it //! enters, which is what turns a pattern into a captured range. +mod action; mod parser; pub use parser::AttachedParser; diff --git a/beeper/src/h1/parser.bpf.c b/beeper/src/h1/parser.bpf.c index 787c60d..1ba68a2 100644 --- a/beeper/src/h1/parser.bpf.c +++ b/beeper/src/h1/parser.bpf.c @@ -4,29 +4,8 @@ #include // The parser for HTTP/1.x messages. It walks a message byte by byte, following -// the transitions user space injected into `s2ts`, and runs the action of every -// state it enters. The flags below are how those actions are encoded; they are -// read back by the Rust side, which is what assembles the table. - -// Parsing is complete, the rest of the message is not a header anymore. -const u16 a_done = 1 << 14; - -// The capture identified by the low bits starts at the next byte. -const u16 a_start_capture = 1 << 13; - -// The capture identified by the low bits ends at the current byte. -const u16 a_end_capture = 1 << 12; - -// Reserved for the HTTP/2 parser, unused here. -const u16 a_h2_read_st = 1 << 11; -const u16 a_h2_read_dt = 1 << 10; - -// if a_done -> then this is 0 -// if a_start_capture -> then this is the cid -// if a_end_capture -> then this is cid | mid -const u16 a_id_mask = 0x0FFF; -const u16 a_id_1_mask = 0x0FC0; -const u16 a_id_2_mask = 0x003F; +// the transitions user space injected into `s2ts`, and runs the action every +// one of them carries. // The state a message is parsed from. const u16 s_init = 0; @@ -34,13 +13,52 @@ const u16 s_init = 0; // The state input that matches no pattern leads back to. const u16 s_any = 1; +// What the parser does upon taking a transition. Must stay in sync with the +// action kinds of h1/action.rs. + +// Nothing. +#define H1A_NONE 0 + +// A capture starts at the byte behind the transition: `cid` names the one whose +// start index is to be written down. +#define H1A_START_CAPTURE 1 +// The open capture ends at the byte the transition read: `cid` names the one +// whose start index is to be read back, `mid` the match its range is reported +// under. +#define H1A_END_CAPTURE 2 + +// Parsing is complete, the rest of the message is not a header anymore. +#define H1F_DONE (1 << 0) + +// A single action of the DFA. +// +// Actions are kept in a table of their own so that a transition only has to +// name the index of the one it carries, which leaves room for saying more than +// the 16 bits of a transition would hold. +struct h1_action { + u8 kind; + u8 flags; + u8 mid; +}; + +// these restrictions are needed to make the verifier happy. All three are +// masked onto an index, so all three have to be powers of two. #define MAX_STATES 512 #define MAX_TRANS 128 +#define MAX_ACTIONS 256 -// The transition table of the DFA, indexed by state and input byte. User space -// fills it in before the program is loaded, after which it is read-only. +// The transition table of the DFA, indexed by state and input byte, and the +// actions its transitions carry. User space fills both in before the program is +// loaded, after which they are read-only. volatile const struct trans s2ts[MAX_STATES][MAX_TRANS]; +volatile const struct h1_action a2as[MAX_ACTIONS]; + +// Reads the action a transition carries. Transition 0 is the one a state +// without a transition for the byte it read falls back to, and carries none. +static __always_inline struct h1_action _action(u16 id) { + return a2as[id & (MAX_ACTIONS - 1)]; +} // Follows the transition `input` takes out of `state`. A state that has no // transition for `input` falls back to the one matching any byte, and if it has @@ -92,7 +110,6 @@ static __always_inline int _parse_from(u8 *data, u8 *data_end, u16 start, struct continue; } - u16 old_state = *s; u16 a = 0; _next(*s, c, s, &a); @@ -100,23 +117,24 @@ static __always_inline int _parse_from(u8 *data, u8 *data_end, u16 start, struct _next(s_any, c, s, &a); } - if ((a & a_start_capture) != 0) { - u16 cid = a & a_id_mask & MAX_MATCH_MASK; - bpf_debug("start capture range (%d, ?) in [%d, ...]", cid, i+1); - cidx[cid] = i + 1; + struct h1_action act = _action(a); + if (act.kind == H1A_START_CAPTURE) { + u16 mid = act.mid & MAX_MATCH_MASK; + bpf_debug("start capture range (%d) in [%d, ...]", mid, i+1); + cidx[mid] = i + 1; } - if ((a & a_end_capture) != 0) { - u16 cid = ((a & a_id_1_mask) >> 6) & MAX_MATCH_MASK; - u16 mid = a & a_id_2_mask & MAX_MATCH_MASK; - bpf_debug("end capture range (%d, %d) in [%d, %d]", cid, mid, cidx[cid], i - cidx[cid] + 1); + else if (act.kind == H1A_END_CAPTURE) { + u16 mid = act.mid & MAX_MATCH_MASK; + bpf_debug("end capture range (%d) in [%d, %d]", mid, cidx[mid], i - cidx[mid] + 1); ms[mid] = (struct hdr_match) { - .idx = cidx[cid], - .len = i - cidx[cid] + 1, + .idx = cidx[mid], + .len = i - cidx[mid] + 1, .in_msg = true }; } - if ((a & a_done) != 0) { + + if ((act.flags & H1F_DONE) != 0) { bpf_debug("done parsing at %d", i); return i+1; } diff --git a/beeper/src/h1/parser.rs b/beeper/src/h1/parser.rs index 80e769d..46f5adc 100644 --- a/beeper/src/h1/parser.rs +++ b/beeper/src/h1/parser.rs @@ -1,9 +1,11 @@ #![allow(unused_imports)] use crate::{ - Action, CaptureId, Dfa, MatchId, StateId, autoload_and_attach, + Dfa, MatchId, autoload_and_attach, + dfa::{ANY_STATE, INIT_STATE}, + h1::action::Action, header::{METHOD, PATH, STATUS}, }; -use anyhow::Result; +use anyhow::{Result, bail}; use http::HeaderName; use std::{collections::HashMap, mem::MaybeUninit}; use tracing::{Level, debug, trace, warn}; @@ -23,7 +25,10 @@ const CRLF: &str = "\r\n"; /// kernel until [`Parser::attach`] is called. pub struct Parser { /// The patterns configured so far, compiled into a DFA. - dfa: Dfa, + dfa: Dfa, + + /// The number of matches occuring in the patterns. + num_matches: u16, parse_msg_fn: Option, parse_buf_fn: Option, @@ -34,36 +39,6 @@ pub struct Parser { xbpf::include_bpf!("h1/parser"); -/// Encodes a transition the way the BPF parser reads it out of its transition -/// table. -/// -/// An action is a bit field: the flags identifying the action occupy the high -/// bits, the capture and match ids the low ones. [`Action::EndCapture`] needs -/// both ids, so it packs the capture id above the match id. -fn new_transition(state: StateId, action: Option, rodata: &rodata) -> trans { - fn start(cid: CaptureId, rodata: &rodata) -> u16 { - rodata.a_start_capture | (cid.0 as u16) & rodata.a_id_mask - } - fn end(cid: CaptureId, mid: MatchId, rodata: &rodata) -> u16 { - let id = (cid.0 as u16) << 6 | (mid.0 as u16); - rodata.a_end_capture | id & rodata.a_id_mask - } - - let action = match action { - Some(Action::StartCapture(cid)) => start(cid, rodata), - Some(Action::EndCapture(cid, mid)) => end(cid, mid, rodata), - Some(Action::Done) => rodata.a_done, - Some(Action::StartCaptureAndDone(cid)) => start(cid, rodata) | rodata.a_done, - Some(Action::EndCaptureAndDone(cid, mid)) => end(cid, mid, rodata) | rodata.a_done, - None => 0, - }; - - trans { - state: state.0, - action, - } -} - #[allow(dead_code)] impl Parser { /// Creates a new HTTP/1.1 parser. @@ -72,6 +47,7 @@ impl Parser { pub fn new() -> Parser { Parser { dfa: Dfa::new(), + num_matches: 0, parse_msg_fn: None, parse_buf_fn: None, parse_skb_fn: None, @@ -135,6 +111,13 @@ impl Parser { self } + /// Returns an unused match id. + fn new_match(&mut self) -> MatchId { + let id = MatchId(self.num_matches); + self.num_matches += 1; + id + } + /// Configures the parser to capture the value of a header field. /// /// The field is matched case insensitively and its value is captured up to @@ -152,8 +135,9 @@ impl Parser { return self.capture_status_code(); } + let mid = self.new_match(); self.dfa - .start_pattern(false) + .start_pattern(ANY_STATE) .push_ci(CRLF) .push_ci(name.as_str()) .push_optional("\t") @@ -161,9 +145,9 @@ impl Parser { .push_ci(":") .push_optional("\t") .push_optional(" ") - .start_capturing() + .with(Action::StartCapture(mid)) .push_any(1..) - .end_capturing() + .with(Action::EndCapture(mid)) .restart_with(CRLF); self @@ -177,12 +161,12 @@ impl Parser { /// The preface is captured as a match, so the target program can detect the /// upgrade and switch to an HTTP/2 parser for the rest of the connection. pub fn match_h2_preface(mut self) -> Parser { + let mid = self.new_match(); self.dfa - .start_pattern(true) - .start_capturing() + .start_pattern(INIT_STATE) + .with(Action::StartCapture(mid)) .push(&format!("PRI * HTTP/2.0{}{}SM{}{}", CRLF, CRLF, CRLF, CRLF)) - .end_capturing() - .done(); + .with(Action::EndCaptureAndDone(mid)); self } @@ -190,7 +174,11 @@ impl Parser { /// Configures the parser to stop at the empty line that ends the header /// block, so that it never walks into the body of a message. fn done_on_hdr_end(mut self) -> Parser { - self.dfa.start_pattern(false).push(CRLF).push(CRLF).done(); + self.dfa + .start_pattern(ANY_STATE) + .push(CRLF) + .push(CRLF) + .with(Action::Done); self } @@ -207,23 +195,25 @@ impl Parser { ]; if name == &METHOD { + let mid = self.new_match(); self.dfa - .start_pattern(true) - .start_capturing() + .start_pattern(INIT_STATE) + .with(Action::StartCapture(mid)) .push_options_ci(&methods) - .end_capturing() + .with(Action::EndCapture(mid)) .push(" ") .push_any(1..) .push_ci(" HTTP/1.1") .restart_with(CRLF); } else if name == &PATH { + let mid = self.new_match(); self.dfa - .start_pattern(true) + .start_pattern(INIT_STATE) .push_options_ci(&methods) .push(" ") - .start_capturing() + .with(Action::StartCapture(mid)) .push_any(1..) - .end_capturing() + .with(Action::EndCapture(mid)) .push_ci(" HTTP/1.1") .restart_with(CRLF); } else { @@ -239,13 +229,14 @@ impl Parser { /// Configures the parser to match the status line of a response and capture /// its status code. fn capture_status_code(mut self) -> Parser { + let mid = self.new_match(); + self.dfa - .start_pattern(true) + .start_pattern(INIT_STATE) .push_ci("HTTP/1.1 ") - .start_capturing() + .with(Action::StartCapture(mid)) .push_any(3..=3) - .end_capturing() - // the reason phrase is matched but not captured + .with(Action::EndCapture(mid)) .push_any(1..) .restart_with(CRLF); @@ -328,15 +319,50 @@ impl Parser { /// parser program. This has to happen before the program is loaded, as the /// kernel freezes the section afterwards. fn inject(&self, skel: &mut OpenParserSkel) -> Result<()> { - for (from, to, input, action) in self.dfa.iter_transitions() { - let s = from.0 as usize; - let data = skel.maps.rodata_data.as_mut().unwrap(); - let t = new_transition(*to, action, data); + let Some(data) = skel.maps.rodata_data.as_mut() else { + bail!("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() { + bail!( + "the patterns take {num_states} states, the parser holds {}", + data.s2ts.len() + ); + } + + let num_edges = self.dfa.num_edges(); + if num_edges > data.a2as.len() { + bail!( + "the patterns take {} edges, the parser holds {}", + num_edges, + data.a2as.len() + ); + } + + // action index 0 is reserved for the noop action + 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); + let action = *action as u16; + trace!( - "inject; from={} to={} input={} action={:?}", - from.0, to.0, *input as u8 as char, action + "inject; from={} to={} input={} action={}", + from.0, to.0, input as char, action ); - data.s2ts[s][*input as usize] = t; + + data.s2ts[from.0 as usize][input as usize] = trans { + state: to.0, + action, + }; + } + + for (action, i) in action_idx { + let Some(action) = action else { continue }; + data.a2as[i] = action.into(); } Ok(()) diff --git a/beeper/src/h2/action.rs b/beeper/src/h2/action.rs new file mode 100644 index 0000000..fe41f08 --- /dev/null +++ b/beeper/src/h2/action.rs @@ -0,0 +1,140 @@ +//! The shape of an HPACK header field representation, compiled into +//! transitions. +//! +//! A field is a sequence of integers and strings, and which one comes next is +//! decided by the bytes read so far, so it can be walked with the same +//! automaton as the field names themselves. Section 6 of RFC 7541 spells the +//! representations out. +//! +//! The value an integer carries lives on the transition rather than in the +//! state it leads to, which is what keeps the automaton small: every index a +//! representation can carry is a transition of its own, but all of them lead to +//! the same handful of states. +//! +//! The state ids and action kinds below must stay in sync with the `S_*` and +//! `H2A_*` constants of h2/parser.bpf.c. + +use crate::{StateId, h2::parser::types::h2_action}; + +/// A field name that matched no pattern. +pub const S_DEAD: StateId = StateId(2); + +/// At the first byte of a field representation. +pub const S_FIELD: StateId = StateId(3); + +/// At the first byte of the length of a field name. +pub const S_KEY_LEN: StateId = StateId(4); + +/// At the first byte of the length of a field value. +pub const S_VAL_LEN: StateId = StateId(5); + +/// The root of the trie of the field names to capture. +pub const S_NAME: StateId = StateId(6); + +/// The continuation of the index of an indexed field. +pub const S_IDX7_CONT: StateId = StateId(7); + +/// The continuation of the name index of a field that is added to the dynamic +/// table. +pub const S_IDX6_CONT: StateId = StateId(8); + +/// The continuation of the name index of a field that is not. +pub const S_IDX4_CONT: StateId = StateId(9); + +/// The continuation of a dynamic table size update. +pub const S_STG_CONT: StateId = StateId(10); + +/// The continuation of the length of a field name. +pub const S_KEY_LEN_CONT: StateId = StateId(11); + +/// The continuation of the length of a Huffman coded field name. +pub const S_KEY_LEN_CONT_HUFF: StateId = StateId(12); + +/// The continuation of the length of a field value. +pub const S_VAL_LEN_CONT: StateId = StateId(13); + +/// The continuation of the length of a Huffman coded field value. +pub const S_VAL_LEN_CONT_HUFF: StateId = StateId(14); + +/// The number of state ids the ones above reserve. +pub const S_RESERVED: u16 = 15; + +/// The string the action describes is Huffman coded. +pub const F_HUFF: u8 = 1 << 0; + +/// The field the action describes is added to the dynamic table. +pub const F_ADD_DT: u8 = 1 << 1; + +/// The integer the action describes is spread over several bytes, so the parser +/// takes it from its accumulator rather than from [`Action::val`]. +pub const F_CONT: u8 = 1 << 2; + +/// What the parser does upon taking a transition. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum Kind { + /// A field spelled out by nothing but an index, which addresses the entry + /// both its name and its value are read from. + Indexed = 1, + + /// A field whose name is an index and whose value is spelled out. + IdxName, + + /// A field whose name is spelled out as well. + LitName, + + /// The length of a field name. + KeyLen, + + /// The length of a field value. + ValLen, + + /// A dynamic table size update. + TableSize, + + /// The first byte of an integer that does not fit into the prefix of that + /// byte, carrying the prefix maximum the integer is counted from. + IntStart, + + /// A byte of such an integer that is not its last one either. + IntCont, + + /// The name of the field being read just matched a pattern. + Capture, + + /// The representation is malformed. + Err, +} + +/// A single action of the automaton, as the BPF parser reads it. +/// +/// `val` is an index, a length or a table size, depending on `kind`. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Action { + pub kind: Kind, + pub val: u16, + pub flags: u8, +} + +impl Action { + /// Returns the action of a transition of `kind` carrying `val`. + pub const fn new(kind: Kind, val: u16, flags: u8) -> Action { + Action { kind, val, flags } + } + + /// Returns the action capturing the value of the field whose name the + /// automaton just matched, under the id `cid`. + pub const fn capture(cid: u16) -> Action { + Action::new(Kind::Capture, cid, 0) + } +} + +impl From for h2_action { + fn from(value: Action) -> Self { + h2_action { + val: value.val, + kind: value.kind as u8, + flags: value.flags, + } + } +} diff --git a/beeper/src/h2/hpack.rs b/beeper/src/h2/hpack.rs index 2441fd6..97afafb 100644 --- a/beeper/src/h2/hpack.rs +++ b/beeper/src/h2/hpack.rs @@ -1,416 +1,322 @@ -//! The shape of an HPACK header field representation, compiled into -//! transitions. -//! -//! A field is a sequence of integers and strings, and which one comes next is -//! decided by the bytes read so far, so it can be walked with the same -//! automaton as the field names themselves. Section 6 of RFC 7541 spells the -//! representations out. -//! -//! The value an integer carries lives on the transition rather than in the -//! state it leads to, which is what keeps the automaton small: every index a -//! representation can carry is a transition of its own, but all of them lead to -//! the same handful of states. -//! -//! The state ids and action kinds below must stay in sync with the `S_*` and -//! `H2A_*` constants of h2/parser.bpf.c. - +use crate::{Dfa, h2::action::*}; use std::collections::HashMap; -/// A field name that matched no pattern. -const S_DEAD: u16 = 2; - -/// At the first byte of a field representation. -const S_FIELD: u16 = 3; - -/// At the first byte of the length of a field name. -const S_KEY_LEN: u16 = 4; - -/// At the first byte of the length of a field value. -const S_VAL_LEN: u16 = 5; - -/// The root of the trie of the field names to capture. -pub(super) const S_NAME: u16 = 6; - -/// The continuation of the index of an indexed field. -const S_IDX7_CONT: u16 = 7; - -/// The continuation of the name index of a field that is added to the dynamic -/// table. -const S_IDX6_CONT: u16 = 8; - -/// The continuation of the name index of a field that is not. -const S_IDX4_CONT: u16 = 9; - -/// The continuation of a dynamic table size update. -const S_STG_CONT: u16 = 10; - -/// The continuation of the length of a field name. -const S_KEY_LEN_CONT: u16 = 11; - -/// The continuation of the length of a Huffman coded field name. -const S_KEY_LEN_CONT_HUFF: u16 = 12; - -/// The continuation of the length of a field value. -const S_VAL_LEN_CONT: u16 = 13; - -/// The continuation of the length of a Huffman coded field value. -const S_VAL_LEN_CONT_HUFF: u16 = 14; - -/// The number of state ids the ones above reserve. -pub(super) const S_RESERVED: u16 = 15; - -/// The string the action describes is Huffman coded. -const F_HUFF: u8 = 1 << 0; - -/// The field the action describes is added to the dynamic table. -const F_ADD_DT: u8 = 1 << 1; - -/// The integer the action describes is spread over several bytes, so the parser -/// takes it from its accumulator rather than from [`Action::val`]. -const F_CONT: u8 = 1 << 2; - -/// What the parser does upon taking a transition. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub(super) enum Kind { - /// Nothing. - None, - - /// A field spelled out by nothing but an index, which addresses the entry - /// both its name and its value are read from. - Indexed, - - /// A field whose name is an index and whose value is spelled out. - IdxName, - - /// A field whose name is spelled out as well. - LitName, - - /// The length of a field name. - KeyLen, - - /// The length of a field value. - ValLen, - - /// A dynamic table size update. - TableSize, - - /// The first byte of an integer that does not fit into the prefix of that - /// byte, carrying the prefix maximum the integer is counted from. - IntStart, - - /// A byte of such an integer that is not its last one either. - IntCont, - - /// The name of the field being read just matched a pattern. - Capture, - - /// The representation is malformed. - Err, -} - -impl Kind { - /// Returns the number the BPF parser identifies the kind by. - fn id(self) -> u8 { - match self { - Kind::None => 0, - Kind::Indexed => 1, - Kind::IdxName => 2, - Kind::LitName => 3, - Kind::KeyLen => 4, - Kind::ValLen => 5, - Kind::TableSize => 6, - Kind::IntStart => 7, - Kind::IntCont => 8, - Kind::Capture => 9, - Kind::Err => 10, - } - } -} - -/// A single action of the automaton, as the BPF parser reads it. -/// -/// `val` is an index, a length or a table size, depending on `kind`. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub(super) struct Action { - pub kind: Kind, - pub val: u16, - pub flags: u8, +/// Builds the transitions of every field representation of RFC 7541 into +/// `dfa`, which has to have been created with [`S_RESERVED`] states reserved +/// for them. +pub(super) fn insert_representations(dfa: &mut Dfa) { + insert_field_row(dfa); + insert_length_rows(dfa); + insert_continuation_rows(dfa); } -impl Action { - /// The action of a transition that does nothing. - pub const NONE: Action = Action { - kind: Kind::None, - val: 0, - flags: 0, - }; - - /// Returns the action of a transition of `kind` carrying `val`. - const fn new(kind: Kind, val: u16, flags: u8) -> Action { - Action { kind, val, flags } - } - - /// Returns the action capturing the value of the field whose name the - /// automaton just matched, under the id `cid`. - pub const fn capture(cid: u16) -> Action { - Action::new(Kind::Capture, cid, 0) - } - - /// Returns the kind and flags the BPF parser reads the action with. - pub fn encode(&self) -> (u8, u8) { - (self.kind.id(), self.flags) - } -} - -/// A single transition, as the BPF parser reads it out of its table. -#[derive(Clone, Copy, Debug)] -pub(super) struct Edge { - pub from: u16, - pub input: u8, - pub to: u16, - - /// The index of the entry of the action table the transition carries. - pub action: u16, -} - -/// The transitions and actions the BPF parser is injected with. -/// -/// Actions are interned, so the transitions of an index and those of a length -/// only take up as many entries as there are distinct values they can carry. -pub(super) struct Table { - edges: Vec, - actions: Vec, - interned: HashMap, -} +/// Inserts the transitions of the first byte of a representation, see section 6 +/// of RFC 7541. +fn insert_field_row(dfa: &mut Dfa) { + let mut edge = |input: u8, to, action| dfa.insert_edge(S_FIELD, input, to, Some(action)); -impl Table { - /// Creates a table holding the transitions of every representation of RFC - /// 7541, ready to take the field name patterns. - pub fn new() -> Table { - let mut table = Table { - edges: Vec::new(), - actions: Vec::new(), - interned: HashMap::new(), - }; - - let none = table.intern(Action::NONE); - assert_eq!(none, 0, "the action of a transition that has none is 0"); - - table.push_field_row(); - table.push_length_rows(); - table.push_continuation_rows(); - - table + // an indexed field, 7 bit prefix. The index 0 is not used + edge(0x80, S_DEAD, Action::new(Kind::Err, 0, 0)); + for idx in 1..0x7F { + edge( + 0x80 | idx as u8, + S_FIELD, + Action::new(Kind::Indexed, idx, 0), + ); } + edge(0xFF, S_IDX7_CONT, Action::new(Kind::IntStart, 0x7F, 0)); - /// Returns the transitions of the automaton. - pub fn edges(&self) -> &[Edge] { - &self.edges + // a literal field that is added to the dynamic table, 6 bit prefix + edge(0x40, S_KEY_LEN, Action::new(Kind::LitName, 0, F_ADD_DT)); + for idx in 1..0x3F { + let action = Action::new(Kind::IdxName, idx, F_ADD_DT); + edge(0x40 | idx as u8, S_VAL_LEN, action); } + edge(0x7F, S_IDX6_CONT, Action::new(Kind::IntStart, 0x3F, 0)); - /// Returns the actions the transitions carry, indexed by [`Edge::action`]. - pub fn actions(&self) -> &[Action] { - &self.actions + // a dynamic table size update, 5 bit prefix + for size in 0..0x1F { + edge( + 0x20 | size as u8, + S_FIELD, + Action::new(Kind::TableSize, size, 0), + ); } - - /// Returns the index of the entry `action` is held under, adding it if the - /// table does not carry it yet. - fn intern(&mut self, action: Action) -> u16 { - if let Some(idx) = self.interned.get(&action) { - return *idx; + edge(0x3F, S_STG_CONT, Action::new(Kind::IntStart, 0x1F, 0)); + + // a literal field that is not, either because it is never to be indexed or + // because it is only not indexed here, 4 bit prefix. Beeper reads both the + // same way + for base in [0x00u8, 0x10] { + edge(base, S_KEY_LEN, Action::new(Kind::LitName, 0, 0)); + for idx in 1..0x0F { + edge( + base | idx as u8, + S_VAL_LEN, + Action::new(Kind::IdxName, idx, 0), + ); } - - let idx = self.actions.len() as u16; - self.actions.push(action); - let _ = self.interned.insert(action, idx); - - idx - } - - /// Appends the transition `input` takes from `from` to `to`. - pub fn push_edge(&mut self, from: u16, input: u8, to: u16, action: Action) { - let action = self.intern(action); - self.edges.push(Edge { - from, - input, - to, - action, - }); + edge( + base | 0x0F, + S_IDX4_CONT, + Action::new(Kind::IntStart, 0x0F, 0), + ); } +} - /// Appends the transitions of the first byte of a representation, see - /// section 6 of RFC 7541. - fn push_field_row(&mut self) { - // an indexed field, 7 bit prefix. The index 0 is not used - self.push_edge(S_FIELD, 0x80, S_DEAD, Action::new(Kind::Err, 0, 0)); - for idx in 1..0x7F { - let input = 0x80 | idx as u8; - self.push_edge(S_FIELD, input, S_FIELD, Action::new(Kind::Indexed, idx, 0)); - } - self.push_edge(S_FIELD, 0xFF, S_IDX7_CONT, Action::new(Kind::IntStart, 0x7F, 0)); - - // a literal field that is added to the dynamic table, 6 bit prefix - self.push_edge( - S_FIELD, - 0x40, +/// Inserts the transitions of the byte announcing the length of a name and of +/// the one announcing the length of a value, see section 5.2 of RFC 7541. Both +/// carry the Huffman bit in their top bit and a 7 bit prefix. +fn insert_length_rows(dfa: &mut Dfa) { + let rows = [ + ( S_KEY_LEN, - Action::new(Kind::LitName, 0, F_ADD_DT), - ); - for idx in 1..0x3F { - let input = 0x40 | idx as u8; - let action = Action::new(Kind::IdxName, idx, F_ADD_DT); - self.push_edge(S_FIELD, input, S_VAL_LEN, action); - } - self.push_edge(S_FIELD, 0x7F, S_IDX6_CONT, Action::new(Kind::IntStart, 0x3F, 0)); - - // a dynamic table size update, 5 bit prefix - for size in 0..0x1F { - let input = 0x20 | size as u8; - self.push_edge(S_FIELD, input, S_FIELD, Action::new(Kind::TableSize, size, 0)); - } - self.push_edge(S_FIELD, 0x3F, S_STG_CONT, Action::new(Kind::IntStart, 0x1F, 0)); - - // a literal field that is not, either because it is never to be indexed - // or because it is only not indexed here, 4 bit prefix. Beeper reads - // both the same way - for base in [0x00u8, 0x10] { - self.push_edge(S_FIELD, base, S_KEY_LEN, Action::new(Kind::LitName, 0, 0)); - for idx in 1..0x0F { - let input = base | idx as u8; - let action = Action::new(Kind::IdxName, idx, 0); - self.push_edge(S_FIELD, input, S_VAL_LEN, action); + Kind::KeyLen, + S_NAME, + S_KEY_LEN_CONT, + S_KEY_LEN_CONT_HUFF, + ), + ( + S_VAL_LEN, + Kind::ValLen, + S_FIELD, + S_VAL_LEN_CONT, + S_VAL_LEN_CONT_HUFF, + ), + ]; + + for (from, kind, to, cont, cont_huff) in rows { + for (base, flags, cont) in [(0x00u8, 0, cont), (0x80u8, F_HUFF, cont_huff)] { + for len in 0..0x7F { + let action = Action::new(kind, len, flags); + dfa.insert_edge(from, base | len as u8, to, Some(action)); } - let input = base | 0x0F; - self.push_edge(S_FIELD, input, S_IDX4_CONT, Action::new(Kind::IntStart, 0x0F, 0)); - } - } - /// Appends the transitions of the byte announcing the length of a name and - /// of the one announcing the length of a value, see section 5.2 of RFC - /// 7541. Both carry the Huffman bit in their top bit and a 7 bit prefix. - fn push_length_rows(&mut self) { - let rows = [ - ( - S_KEY_LEN, - Kind::KeyLen, - S_NAME, - S_KEY_LEN_CONT, - S_KEY_LEN_CONT_HUFF, - ), - ( - S_VAL_LEN, - Kind::ValLen, - S_FIELD, - S_VAL_LEN_CONT, - S_VAL_LEN_CONT_HUFF, - ), - ]; - - for (from, kind, to, cont, cont_huff) in rows { - for (base, flags, cont) in [(0x00u8, 0, cont), (0x80u8, F_HUFF, cont_huff)] { - for len in 0..0x7F { - let input = base | len as u8; - self.push_edge(from, input, to, Action::new(kind, len, flags)); - } - - let input = base | 0x7F; - self.push_edge(from, input, cont, Action::new(Kind::IntStart, 0x7F, 0)); - } + let action = Action::new(Kind::IntStart, 0x7F, 0); + dfa.insert_edge(from, base | 0x7F, cont, Some(action)); } } +} - /// Appends the transitions of the bytes an integer that did not fit into - /// the prefix of its first byte is spread over, see section 5.1 of RFC - /// 7541. The top bit of every one of them says whether another follows. - fn push_continuation_rows(&mut self) { - let rows = [ - (S_IDX7_CONT, Kind::Indexed, S_FIELD, 0), - (S_IDX6_CONT, Kind::IdxName, S_VAL_LEN, F_ADD_DT), - (S_IDX4_CONT, Kind::IdxName, S_VAL_LEN, 0), - (S_STG_CONT, Kind::TableSize, S_FIELD, 0), - (S_KEY_LEN_CONT, Kind::KeyLen, S_NAME, 0), - (S_KEY_LEN_CONT_HUFF, Kind::KeyLen, S_NAME, F_HUFF), - (S_VAL_LEN_CONT, Kind::ValLen, S_FIELD, 0), - (S_VAL_LEN_CONT_HUFF, Kind::ValLen, S_FIELD, F_HUFF), - ]; - - for (from, kind, to, flags) in rows { - for input in 0..0x80u8 { - let action = Action::new(kind, 0, flags | F_CONT); - self.push_edge(from, input, to, action); - self.push_edge(from, 0x80 | input, from, Action::new(Kind::IntCont, 0, 0)); - } +/// Inserts the transitions of the bytes an integer that did not fit into the +/// prefix of its first byte is spread over, see section 5.1 of RFC 7541. The +/// top bit of every one of them says whether another follows. +fn insert_continuation_rows(dfa: &mut Dfa) { + let rows = [ + (S_IDX7_CONT, Kind::Indexed, S_FIELD, 0), + (S_IDX6_CONT, Kind::IdxName, S_VAL_LEN, F_ADD_DT), + (S_IDX4_CONT, Kind::IdxName, S_VAL_LEN, 0), + (S_STG_CONT, Kind::TableSize, S_FIELD, 0), + (S_KEY_LEN_CONT, Kind::KeyLen, S_NAME, 0), + (S_KEY_LEN_CONT_HUFF, Kind::KeyLen, S_NAME, F_HUFF), + (S_VAL_LEN_CONT, Kind::ValLen, S_FIELD, 0), + (S_VAL_LEN_CONT_HUFF, Kind::ValLen, S_FIELD, F_HUFF), + ]; + + for (from, kind, to, flags) in rows { + for input in 0..0x80u8 { + let action = Action::new(kind, 0, flags | F_CONT); + dfa.insert_edge(from, input, to, Some(action)); + + let action = Action::new(Kind::IntCont, 0, 0); + dfa.insert_edge(from, 0x80 | input, from, Some(action)); } } } +/// Returns the HPACK static table, split by whether an entry predefines a +/// value or not. +/// +/// The first map goes from a field name to its index, the second from a field +/// name to the index of each of the values that are predefined for it. See +/// appendix A of RFC 7541. +pub fn create_header_maps() -> ( + HashMap, + HashMap>, +) { + // HashMap for headers without values (header_name -> index) + let mut headers_without_values = HashMap::new(); + + // HashMap for headers with values (header_name -> (header_value -> index)) + let mut headers_with_values: HashMap> = HashMap::new(); + + // Headers without values + headers_without_values.insert("authority".to_string(), 1); + headers_without_values.insert("accept-charset".to_string(), 15); + headers_without_values.insert("accept-language".to_string(), 17); + headers_without_values.insert("accept-ranges".to_string(), 18); + headers_without_values.insert("accept".to_string(), 19); + headers_without_values.insert("access-control-allow-origin".to_string(), 20); + headers_without_values.insert("age".to_string(), 21); + headers_without_values.insert("allow".to_string(), 22); + headers_without_values.insert("authorization".to_string(), 23); + headers_without_values.insert("cache-control".to_string(), 24); + headers_without_values.insert("content-disposition".to_string(), 25); + headers_without_values.insert("content-encoding".to_string(), 26); + headers_without_values.insert("content-language".to_string(), 27); + headers_without_values.insert("content-length".to_string(), 28); + headers_without_values.insert("content-location".to_string(), 29); + headers_without_values.insert("content-range".to_string(), 30); + headers_without_values.insert("content-type".to_string(), 31); + headers_without_values.insert("cookie".to_string(), 32); + headers_without_values.insert("date".to_string(), 33); + headers_without_values.insert("etag".to_string(), 34); + headers_without_values.insert("expect".to_string(), 35); + headers_without_values.insert("expires".to_string(), 36); + headers_without_values.insert("from".to_string(), 37); + headers_without_values.insert("host".to_string(), 38); + headers_without_values.insert("if-match".to_string(), 39); + headers_without_values.insert("if-modified-since".to_string(), 40); + headers_without_values.insert("if-none-match".to_string(), 41); + headers_without_values.insert("if-range".to_string(), 42); + headers_without_values.insert("if-unmodified-since".to_string(), 43); + headers_without_values.insert("last-modified".to_string(), 44); + headers_without_values.insert("link".to_string(), 45); + headers_without_values.insert("location".to_string(), 46); + headers_without_values.insert("max-forwards".to_string(), 47); + headers_without_values.insert("proxy-authenticate".to_string(), 48); + headers_without_values.insert("proxy-authorization".to_string(), 49); + headers_without_values.insert("range".to_string(), 50); + headers_without_values.insert("referer".to_string(), 51); + headers_without_values.insert("refresh".to_string(), 52); + headers_without_values.insert("retry-after".to_string(), 53); + headers_without_values.insert("server".to_string(), 54); + headers_without_values.insert("set-cookie".to_string(), 55); + headers_without_values.insert("strict-transport-security".to_string(), 56); + headers_without_values.insert("transfer-encoding".to_string(), 57); + headers_without_values.insert("user-agent".to_string(), 58); + headers_without_values.insert("vary".to_string(), 59); + headers_without_values.insert("via".to_string(), 60); + headers_without_values.insert("www-authenticate".to_string(), 61); + + // Headers with values + // :method + let mut method_map = HashMap::new(); + method_map.insert("GET".to_string(), 2); + method_map.insert("POST".to_string(), 3); + headers_with_values.insert("method".to_string(), method_map); + + // :path + let mut path_map = HashMap::new(); + path_map.insert("/".to_string(), 4); + path_map.insert("/index.html".to_string(), 5); + headers_with_values.insert("path".to_string(), path_map); + + // :scheme + let mut scheme_map = HashMap::new(); + scheme_map.insert("http".to_string(), 6); + scheme_map.insert("https".to_string(), 7); + headers_with_values.insert("scheme".to_string(), scheme_map); + + // :status + let mut status_map = HashMap::new(); + status_map.insert("200".to_string(), 8); + status_map.insert("204".to_string(), 9); + status_map.insert("206".to_string(), 10); + status_map.insert("304".to_string(), 11); + status_map.insert("400".to_string(), 12); + status_map.insert("404".to_string(), 13); + status_map.insert("500".to_string(), 14); + headers_with_values.insert("status".to_string(), status_map); + + // accept-encoding + let mut accept_encoding_map = HashMap::new(); + accept_encoding_map.insert("gzip, deflate".to_string(), 16); + headers_with_values.insert("accept-encoding".to_string(), accept_encoding_map); + + (headers_without_values, headers_with_values) +} + #[cfg(test)] mod tests { use super::*; + use crate::StateId; use std::collections::HashSet; - /// Returns the states the structure is walked with. `S_DEAD` and `S_NAME` - /// are left out, as the patterns are what gives those their transitions. - fn structure_states() -> Vec { - (S_FIELD..S_RESERVED).filter(|s| *s != S_NAME).collect() + /// Returns a DFA holding nothing but the representations. + fn representations() -> Dfa { + let mut dfa = Dfa::with_reserved_states(S_RESERVED); + insert_representations(&mut dfa); + + dfa + } + + /// Returns the states a representation is walked with. `S_DEAD` and + /// `S_NAME` are left out, as it is the patterns that give those their + /// transitions. + fn representation_states() -> Vec { + (S_FIELD.0..S_RESERVED) + .map(StateId) + .filter(|state| *state != S_NAME) + .collect() } #[test] - fn every_structure_state_has_a_transition_for_every_byte() { - let table = Table::new(); - - for state in structure_states() { - let inputs: HashSet = table - .edges() - .iter() - .filter(|edge| edge.from == state) - .map(|edge| edge.input) + fn every_representation_state_reads_every_byte() { + let dfa = representations(); + + for state in representation_states() { + let inputs: HashSet = dfa + .iter_transitions() + .filter(|(from, ..)| *from == state) + .map(|(_, input, _, _)| input) .collect(); - assert_eq!(inputs.len(), 256, "state {state} does not read every byte"); + assert_eq!( + inputs.len(), + 256, + "state {state:?} does not read every byte" + ); } } #[test] - fn no_state_reads_a_byte_twice() { - let table = Table::new(); - let mut seen = HashSet::new(); + fn no_transition_leads_to_a_state_without_transitions() { + let dfa = representations(); + let from: HashSet = dfa.iter_transitions().map(|(from, ..)| from).collect(); - for edge in table.edges() { + for (_, _, to, _) in dfa.iter_transitions() { assert!( - seen.insert((edge.from, edge.input)), - "state {} reads {:#04x} twice", - edge.from, - edge.input + to == S_DEAD || to == S_NAME || from.contains(&to), + "state {to:?} leads nowhere" ); } } #[test] - fn no_transition_leads_to_a_state_without_transitions() { - let table = Table::new(); - let from: HashSet = table.edges().iter().map(|edge| edge.from).collect(); + fn every_representation_transition_carries_an_action_of_its_own() { + let dfa = representations(); + let states = representation_states(); + + for (from, input, _, action) in dfa.iter_transitions() { + if !states.contains(&from) { + continue; + } - for edge in table.edges() { assert!( - edge.to == S_DEAD || edge.to == S_NAME || from.contains(&edge.to), - "state {} leads nowhere", - edge.to + action.is_some(), + "the transition {input:#04x} takes out of {from:?} carries no action" ); } } #[test] - fn the_action_of_a_transition_without_one_is_zero() { - let table = Table::new(); - assert_eq!(table.actions()[0], Action::NONE); - } - - #[test] - fn actions_are_interned() { - let table = Table::new(); - let unique: HashSet = table.actions().iter().copied().collect(); - - assert_eq!(unique.len(), table.actions().len()); + fn a_pattern_captures_where_it_ends() { + let mut dfa = representations(); + dfa.start_pattern(S_NAME) + .push_bytes(b"beep") + .with(Action::capture(0)); + + let captures: Vec = dfa + .iter_transitions() + .filter(|(_, _, _, action)| action.is_some_and(|action| action.kind == Kind::Capture)) + .map(|(from, ..)| from) + .collect(); + + assert_eq!( + captures.len(), + 1, + "the capture is not on the last transition alone" + ); } } diff --git a/beeper/src/h2/mod.rs b/beeper/src/h2/mod.rs index f2f75c4..c3d1efb 100644 --- a/beeper/src/h2/mod.rs +++ b/beeper/src/h2/mod.rs @@ -6,9 +6,9 @@ //! name against its DFA and mirrors the peer's dynamic table in a BPF map, so //! that indexed fields can be resolved in the kernel as well. -use std::collections::HashMap; use std::net::SocketAddr; +mod action; mod hpack; mod parser; pub use parser::{AttachedParser, Parser, ip4_addr, ip4_conn}; @@ -30,135 +30,3 @@ impl From for ip4_addr { } } } - -/// Returns the HPACK static table, split by whether an entry predefines a -/// value or not. -/// -/// The first map goes from a field name to its index, the second from a field -/// name to the index of each of the values that are predefined for it. See -/// appendix A of RFC 7541. -fn create_header_maps() -> ( - HashMap, - HashMap>, -) { - // HashMap for headers without values (header_name -> index) - let mut headers_without_values = HashMap::new(); - - // HashMap for headers with values (header_name -> (header_value -> index)) - let mut headers_with_values: HashMap> = HashMap::new(); - - // Headers without values - headers_without_values.insert("authority".to_string(), 1); - headers_without_values.insert("accept-charset".to_string(), 15); - headers_without_values.insert("accept-language".to_string(), 17); - headers_without_values.insert("accept-ranges".to_string(), 18); - headers_without_values.insert("accept".to_string(), 19); - headers_without_values.insert("access-control-allow-origin".to_string(), 20); - headers_without_values.insert("age".to_string(), 21); - headers_without_values.insert("allow".to_string(), 22); - headers_without_values.insert("authorization".to_string(), 23); - headers_without_values.insert("cache-control".to_string(), 24); - headers_without_values.insert("content-disposition".to_string(), 25); - headers_without_values.insert("content-encoding".to_string(), 26); - headers_without_values.insert("content-language".to_string(), 27); - headers_without_values.insert("content-length".to_string(), 28); - headers_without_values.insert("content-location".to_string(), 29); - headers_without_values.insert("content-range".to_string(), 30); - headers_without_values.insert("content-type".to_string(), 31); - headers_without_values.insert("cookie".to_string(), 32); - headers_without_values.insert("date".to_string(), 33); - headers_without_values.insert("etag".to_string(), 34); - headers_without_values.insert("expect".to_string(), 35); - headers_without_values.insert("expires".to_string(), 36); - headers_without_values.insert("from".to_string(), 37); - headers_without_values.insert("host".to_string(), 38); - headers_without_values.insert("if-match".to_string(), 39); - headers_without_values.insert("if-modified-since".to_string(), 40); - headers_without_values.insert("if-none-match".to_string(), 41); - headers_without_values.insert("if-range".to_string(), 42); - headers_without_values.insert("if-unmodified-since".to_string(), 43); - headers_without_values.insert("last-modified".to_string(), 44); - headers_without_values.insert("link".to_string(), 45); - headers_without_values.insert("location".to_string(), 46); - headers_without_values.insert("max-forwards".to_string(), 47); - headers_without_values.insert("proxy-authenticate".to_string(), 48); - headers_without_values.insert("proxy-authorization".to_string(), 49); - headers_without_values.insert("range".to_string(), 50); - headers_without_values.insert("referer".to_string(), 51); - headers_without_values.insert("refresh".to_string(), 52); - headers_without_values.insert("retry-after".to_string(), 53); - headers_without_values.insert("server".to_string(), 54); - headers_without_values.insert("set-cookie".to_string(), 55); - headers_without_values.insert("strict-transport-security".to_string(), 56); - headers_without_values.insert("transfer-encoding".to_string(), 57); - headers_without_values.insert("user-agent".to_string(), 58); - headers_without_values.insert("vary".to_string(), 59); - headers_without_values.insert("via".to_string(), 60); - headers_without_values.insert("www-authenticate".to_string(), 61); - - // Headers with values - // :method - let mut method_map = HashMap::new(); - method_map.insert("GET".to_string(), 2); - method_map.insert("POST".to_string(), 3); - headers_with_values.insert("method".to_string(), method_map); - - // :path - let mut path_map = HashMap::new(); - path_map.insert("/".to_string(), 4); - path_map.insert("/index.html".to_string(), 5); - headers_with_values.insert("path".to_string(), path_map); - - // :scheme - let mut scheme_map = HashMap::new(); - scheme_map.insert("http".to_string(), 6); - scheme_map.insert("https".to_string(), 7); - headers_with_values.insert("scheme".to_string(), scheme_map); - - // :status - let mut status_map = HashMap::new(); - status_map.insert("200".to_string(), 8); - status_map.insert("204".to_string(), 9); - status_map.insert("206".to_string(), 10); - status_map.insert("304".to_string(), 11); - status_map.insert("400".to_string(), 12); - status_map.insert("404".to_string(), 13); - status_map.insert("500".to_string(), 14); - headers_with_values.insert("status".to_string(), status_map); - - // accept-encoding - let mut accept_encoding_map = HashMap::new(); - accept_encoding_map.insert("gzip, deflate".to_string(), 16); - headers_with_values.insert("accept-encoding".to_string(), accept_encoding_map); - - (headers_without_values, headers_with_values) -} - -/// The action a transition of the DFA carries. -/// -/// Unlike HTTP/1.x, a field value does not have to be delimited by matching its -/// end: HPACK prefixes it with its length. A single action per matched field -/// name is therefore enough. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Action { - /// Capture a field value identified by the capture id - CaptureFieldValue(u8), - - /// Terminates parsing - Done, - - /// No action - None, -} - -impl Action { - /// Returns `true` if the transition carries an action. - pub fn is_some(&self) -> bool { - !self.is_none() - } - - /// Returns `true` if the transition carries no action. - pub fn is_none(&self) -> bool { - matches!(self, Action::None) - } -} diff --git a/beeper/src/h2/parser.rs b/beeper/src/h2/parser.rs index 590c9f4..b1f5276 100644 --- a/beeper/src/h2/parser.rs +++ b/beeper/src/h2/parser.rs @@ -1,13 +1,14 @@ #![allow(unused_imports)] use crate::{ - Action, Dfa, StateId, autoload_and_attach, - h2::{create_header_maps, hpack}, + Dfa, MatchId, autoload_and_attach, + h2::{action::*, hpack}, }; use anyhow::{Result, bail}; 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}; @@ -27,7 +28,10 @@ extern crate plain; /// kernel until [`Parser::attach`] is called. pub struct Parser { /// The patterns configured so far, compiled into a DFA. - dfa: Dfa, + dfa: Dfa, + + /// The number of matches occuring in the patterns. + num_matches: u16, parse_msg_fn: Option, parse_buf_fn: Option, @@ -39,32 +43,18 @@ pub struct Parser { xbpf::include_bpf!("h2/parser"); -/// Translates the action of a pattern into the one the BPF parser runs. -/// -/// A pattern only ever matches a field name, which HPACK announces the length -/// of, so the only action an HTTP/2 pattern carries is the one starting the -/// capture of the value that follows. -fn new_action(action: Option) -> hpack::Action { - match action { - None => hpack::Action::NONE, - Some(Action::StartCapture(cid)) => hpack::Action::capture(cid.0), - Some(Action::Done) | Some(Action::StartCaptureAndDone(..)) => { - unreachable!("an h2 pattern never terminates the parse, it captures and moves on") - } - Some(Action::EndCapture(..)) | Some(Action::EndCaptureAndDone(..)) => { - unreachable!("HPACK announces the value length, so an h2 pattern never ends a capture") - } - } -} - #[allow(dead_code)] impl Parser { /// Creates a new HTTP/2 parser. /// /// Additional configuration must be done through the builder methods before calling `attach`. pub fn new() -> Parser { + let mut dfa = Dfa::with_reserved_states(S_RESERVED); + hpack::insert_representations(&mut dfa); + Parser { - dfa: Dfa::with_reserved_states(hpack::S_RESERVED), + dfa, + num_matches: 0, parse_msg_fn: None, parse_buf_fn: None, parse_skb_fn: None, @@ -163,14 +153,22 @@ impl Parser { let mut name_encoded = Vec::new(); huffman::encode(name.as_str().as_bytes(), &mut name_encoded)?; + let mid = self.new_match(); self.dfa - .start_pattern_at(StateId(hpack::S_NAME)) + .start_pattern(S_NAME) .push_bytes(&name_encoded) - .capture(); + .with(Action::capture(mid.0)); Ok(self) } + /// Returns an unused match id. + fn new_match(&mut self) -> MatchId { + let id = MatchId(self.num_matches); + self.num_matches += 1; + id + } + /// Fills `static_table` with the Huffman encoded entries of the HPACK /// static table and freezes it, so that the parser can resolve the fields a /// peer refers to by index. @@ -211,7 +209,7 @@ impl Parser { anyhow::Ok(()) }; - let (st_keys, st_hfs) = create_header_maps(); + 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))?; @@ -316,11 +314,6 @@ impl Parser { /// Returns an error if the patterns do not fit into the tables the parser /// program reserves for them. fn inject(&self, skel: &mut OpenParserSkel) -> Result<()> { - let mut table = hpack::Table::new(); - for (from, to, input, action) in self.dfa.iter_transitions() { - table.push_edge(from.0, *input, to.0, new_action(action)); - } - let Some(data) = skel.maps.rodata_data.as_mut() else { bail!("the parser program has no read-only data to inject into"); }; @@ -333,34 +326,30 @@ impl Parser { ); } - let num_actions = table.actions().len(); - if num_actions > data.a2as.len() { - bail!( - "the patterns take {num_actions} actions, the parser holds {}", - data.a2as.len() - ); - } + // action index 0 is reserved for the noop action + 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() { + bail!( + "the patterns take more actions than the {} the parser holds", + data.a2as.len() + ); + } - for hpack::Edge { - from, - input, - to, - action, - } in table.edges() - { - data.s2ts[*from as usize][*input as usize] = trans { - state: *to, - action: *action, + data.s2ts[from.0 as usize][input as usize] = trans { + state: to.0, + action: action as u16, }; } - for (i, action) in table.actions().iter().enumerate() { - let (kind, flags) = action.encode(); - data.a2as[i] = h2_action { - val: action.val, - kind, - flags, - }; + for (action, i) in action_idx { + let Some(action) = action else { continue }; + + data.a2as[i] = action.into(); } Ok(()) diff --git a/beeper/src/lib.rs b/beeper/src/lib.rs index 85f3334..58e7719 100644 --- a/beeper/src/lib.rs +++ b/beeper/src/lib.rs @@ -27,7 +27,7 @@ //! The value returned by `attach` owns the links to the attached programs, so //! the parser stays in place until it is dropped. -use anyhow::{Result, bail}; +use anyhow::Result; pub(crate) use dfa::Dfa; use xbpf::libbpf::{Mut, OpenProgramImpl}; @@ -79,13 +79,6 @@ fn autoload_and_attach<'obj>( #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub(crate) struct StateId(u16); -/// Identifies a range that is being captured. -/// -/// The parser keeps one start index per capture id while it walks a message. -/// [`Action::EndCapture`] turns that index into a match. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub(crate) struct CaptureId(u16); - /// Identifies a captured range in the parse result. /// /// It is the index the target program passes to the functions replaced with @@ -93,68 +86,3 @@ pub(crate) struct CaptureId(u16); /// numbered in the order in which they are configured. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub(crate) struct MatchId(u16); - -/// The action a single state carries. A state either starts or ends a capture, -/// and optionally terminates parsing. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum Action { - /// Starts capturing a range - /// The start index is identified by the cid - StartCapture(CaptureId), - - /// Ends capturing a range with a given cid (1st argument) - /// The range is identified by the rid (2nd argument) - EndCapture(CaptureId, MatchId), - - /// Terminates parsing - Done, - - /// Starts capturing a range and terminates parsing - StartCaptureAndDone(CaptureId), - - /// Ends capturing a range and terminates parsing - EndCaptureAndDone(CaptureId, MatchId), -} - -impl Action { - /// Combines `self` with `action`. Since a state carries a single capture, - /// this fails if the two capture different ranges. Pushing the very same - /// action twice is a no-op, states are shared between patterns after all. - pub(crate) fn push(self, action: Action) -> Result { - let action = match (self, action) { - (action, other) if action == other => action, - - // [`Action::Done`] combines with any capture - (Action::Done, Action::StartCapture(cid)) - | (Action::StartCapture(cid), Action::Done) - | (Action::Done, Action::StartCaptureAndDone(cid)) - | (Action::StartCaptureAndDone(cid), Action::Done) => Action::StartCaptureAndDone(cid), - - (Action::Done, Action::EndCapture(cid, mid)) - | (Action::EndCapture(cid, mid), Action::Done) - | (Action::Done, Action::EndCaptureAndDone(cid, mid)) - | (Action::EndCaptureAndDone(cid, mid), Action::Done) => { - Action::EndCaptureAndDone(cid, mid) - } - - // a capture combines with the very same capture that is also done - (Action::StartCapture(cid), Action::StartCaptureAndDone(other)) - | (Action::StartCaptureAndDone(other), Action::StartCapture(cid)) - if cid == other => - { - Action::StartCaptureAndDone(cid) - } - - (Action::EndCapture(cid, mid), Action::EndCaptureAndDone(other_cid, other_mid)) - | (Action::EndCaptureAndDone(other_cid, other_mid), Action::EndCapture(cid, mid)) - if (cid, mid) == (other_cid, other_mid) => - { - Action::EndCaptureAndDone(cid, mid) - } - - (action, other) => bail!("Cannot {action:?} and {other:?} with the same state"), - }; - - Ok(action) - } -} From 4a9453dcb4d2f01bb17eb950096e6e3f7c2aeb9b Mon Sep 17 00:00:00 2001 From: Laurin Brandner Date: Wed, 2 Sep 2026 19:27:13 +0200 Subject: [PATCH 5/5] h2: clean up hpackg --- beeper/src/h2/action.rs | 6 +++--- beeper/src/h2/hpack.rs | 31 +++++++++++++------------------ beeper/src/h2/parser.rs | 5 ++--- 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/beeper/src/h2/action.rs b/beeper/src/h2/action.rs index fe41f08..25569dd 100644 --- a/beeper/src/h2/action.rs +++ b/beeper/src/h2/action.rs @@ -14,7 +14,7 @@ //! The state ids and action kinds below must stay in sync with the `S_*` and //! `H2A_*` constants of h2/parser.bpf.c. -use crate::{StateId, h2::parser::types::h2_action}; +use crate::{MatchId, StateId, h2::parser::types::h2_action}; /// A field name that matched no pattern. pub const S_DEAD: StateId = StateId(2); @@ -124,8 +124,8 @@ impl Action { /// Returns the action capturing the value of the field whose name the /// automaton just matched, under the id `cid`. - pub const fn capture(cid: u16) -> Action { - Action::new(Kind::Capture, cid, 0) + pub const fn capture(mid: MatchId) -> Action { + Action::new(Kind::Capture, mid.0, 0) } } diff --git a/beeper/src/h2/hpack.rs b/beeper/src/h2/hpack.rs index 97afafb..ef63423 100644 --- a/beeper/src/h2/hpack.rs +++ b/beeper/src/h2/hpack.rs @@ -4,10 +4,12 @@ use std::collections::HashMap; /// Builds the transitions of every field representation of RFC 7541 into /// `dfa`, which has to have been created with [`S_RESERVED`] states reserved /// for them. -pub(super) fn insert_representations(dfa: &mut Dfa) { - insert_field_row(dfa); - insert_length_rows(dfa); - insert_continuation_rows(dfa); +pub fn dfa() -> Dfa { + let mut dfa = Dfa::with_reserved_states(S_RESERVED); + insert_field_row(&mut dfa); + insert_length_rows(&mut dfa); + insert_continuation_rows(&mut dfa); + dfa } /// Inserts the transitions of the first byte of a representation, see section 6 @@ -230,17 +232,9 @@ pub fn create_header_maps() -> ( #[cfg(test)] mod tests { use super::*; - use crate::StateId; + use crate::{MatchId, StateId}; use std::collections::HashSet; - /// Returns a DFA holding nothing but the representations. - fn representations() -> Dfa { - let mut dfa = Dfa::with_reserved_states(S_RESERVED); - insert_representations(&mut dfa); - - dfa - } - /// Returns the states a representation is walked with. `S_DEAD` and /// `S_NAME` are left out, as it is the patterns that give those their /// transitions. @@ -253,7 +247,7 @@ mod tests { #[test] fn every_representation_state_reads_every_byte() { - let dfa = representations(); + let dfa = dfa(); for state in representation_states() { let inputs: HashSet = dfa @@ -272,7 +266,7 @@ mod tests { #[test] fn no_transition_leads_to_a_state_without_transitions() { - let dfa = representations(); + let dfa = dfa(); let from: HashSet = dfa.iter_transitions().map(|(from, ..)| from).collect(); for (_, _, to, _) in dfa.iter_transitions() { @@ -285,7 +279,7 @@ mod tests { #[test] fn every_representation_transition_carries_an_action_of_its_own() { - let dfa = representations(); + let dfa = dfa(); let states = representation_states(); for (from, input, _, action) in dfa.iter_transitions() { @@ -302,10 +296,11 @@ mod tests { #[test] fn a_pattern_captures_where_it_ends() { - let mut dfa = representations(); + let mid = MatchId(0); + let mut dfa = dfa(); dfa.start_pattern(S_NAME) .push_bytes(b"beep") - .with(Action::capture(0)); + .with(Action::capture(mid)); let captures: Vec = dfa .iter_transitions() diff --git a/beeper/src/h2/parser.rs b/beeper/src/h2/parser.rs index b1f5276..e30b882 100644 --- a/beeper/src/h2/parser.rs +++ b/beeper/src/h2/parser.rs @@ -49,8 +49,7 @@ impl Parser { /// /// Additional configuration must be done through the builder methods before calling `attach`. pub fn new() -> Parser { - let mut dfa = Dfa::with_reserved_states(S_RESERVED); - hpack::insert_representations(&mut dfa); + let dfa = hpack::dfa(); Parser { dfa, @@ -157,7 +156,7 @@ impl Parser { self.dfa .start_pattern(S_NAME) .push_bytes(&name_encoded) - .with(Action::capture(mid.0)); + .with(Action::capture(mid)); Ok(self) }