beeper 0.1.0

Application-Layer Parsing in eBPF
#![allow(unused_imports)]
use crate::{
    autoload_and_attach,
    h2::{create_header_maps, dfa::Dfa, Action},
};
use anyhow::{bail, Result};
use as_bytes::AsBytes;
use httlib_huffman as huffman;
use http::HeaderName;
use plain::Plain;
use std::mem::MaybeUninit;
use std::net::SocketAddr;
use tracing::{debug, warn, Level};
use types::*;
pub use types::{ip4_addr, ip4_conn};
use xbpf::libbpf::{
    self as libbpf_rs,
    skel::{OpenSkel, Skel, SkelBuilder},
    Link, MapCore, MapFlags, MapHandle, OpenObject,
};

extern crate plain;

/// A parser for HTTP/2 messages.
///
/// The builder methods configure which fields the parser captures and which
/// functions of the target program it replaces. Nothing is loaded into the
/// kernel until [`Parser::attach`] is called.
pub struct Parser {
    /// The state every pattern is anchored at. A field name may appear in any
    /// header block, so there is no equivalent to the request line of HTTP/1.x
    /// to anchor a pattern at.
    s_any: u16,

    /// The patterns configured so far, compiled into a DFA.
    dfa: Dfa,

    parse_msg_fn: Option<String>,
    parse_buf_fn: Option<String>,
    parse_skb_fn: Option<String>,
    extract_fn: Option<String>,
    matched_fn: Option<String>,
    get_dynamic_table_entry_fn: Option<String>,
}

xbpf::include_bpf!("h2/parser");

/// Encodes a transition the way the BPF parser reads it out of its transition
/// table.
///
/// 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: u16, action: Action, rodata: &rodata) -> trans {
    let action = match action {
        Action::CaptureFieldValue(cid) => rodata.a_start_capture | (cid as u16) & rodata.a_id_mask,
        // Action::EndCapturing(rid) => rodata.a_end_capture | (rid as u16) & rodata.a_id_mask,
        Action::Done => rodata.a_done,
        Action::None => 0,
    };

    trans { state, action }
}

#[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 states = vec![0, 1];

        Parser {
            s_any: 0,
            dfa: Dfa::new(states.into_iter()),
            parse_msg_fn: None,
            parse_buf_fn: None,
            parse_skb_fn: None,
            extract_fn: None,
            matched_fn: None,
            get_dynamic_table_entry_fn: None,
        }
    }

    /// Specifies the function template in the target program to be replaced with an HTTP/2
    /// parser. The function will not be replaced until `attach` is called.
    ///
    /// # Arguments
    ///
    /// * `parse_fn` - The name of the function to replace in the target program
    pub fn replace_parse_msg<S: ToString>(mut self, parse_fn: S) -> Parser {
        self.parse_msg_fn = Some(parse_fn.to_string());
        self
    }

    /// Specifies the function template in the target program to be replaced with a parser
    /// reading from a `sk_buff`. The function will not be replaced until `attach` is called.
    ///
    /// # Arguments
    ///
    /// * `parse_fn` - The name of the function to replace in the target program
    pub fn replace_parse_skb<S: ToString>(mut self, parse_fn: S) -> Parser {
        self.parse_skb_fn = Some(parse_fn.to_string());
        self
    }

    /// Specifies the function template in the target program to be replaced with a parser
    /// reading from a dynptr. The function will not be replaced until `attach` is called.
    ///
    /// # Arguments
    ///
    /// * `parse_fn` - The name of the function to replace in the target program
    pub fn replace_parse_buf<S: ToString>(mut self, parse_fn: S) -> Parser {
        self.parse_buf_fn = Some(parse_fn.to_string());
        self
    }

    /// Specifies the function template in the target program to be called when a pattern match
    /// is completed. The function will not be replaced until `attach` is called.
    ///
    /// # Arguments
    ///
    /// * `matched_fn` - The name of the matched callback function in the target program
    pub fn replace_matched<S: ToString>(mut self, matched_fn: S) -> Parser {
        self.matched_fn = Some(matched_fn.to_string());
        self
    }

    /// Specifies the function template in the target program to be called when extracting
    /// matched content. The function will not be replaced until `attach` is called.
    ///
    /// # Arguments
    ///
    /// * `extract_fn` - The name of the extract callback function in the target program
    pub fn replace_extract<S: ToString>(mut self, extract_fn: S) -> Parser {
        self.extract_fn = Some(extract_fn.to_string());
        self
    }

    /// Specifies the function template in the target program to be replaced with a reader of the
    /// connection's dynamic table (`BEEPER_H2_GET_DT_ENTRY`). The function will not be replaced
    /// until `attach` is called.
    ///
    /// # Arguments
    ///
    /// * `get_dynamic_table_entry_fn` - The name of the dynamic table entry reader function in the target program
    pub fn replace_get_dynamic_table_entry<S: ToString>(
        mut self,
        get_dynamic_table_entry_fn: S,
    ) -> Parser {
        self.get_dynamic_table_entry_fn = Some(get_dynamic_table_entry_fn.to_string());
        self
    }

    /// Configures the parser to capture the value of a header field.
    ///
    /// The field name is matched in its Huffman encoded form, which is how
    /// HPACK puts it on the wire. Fields the peer replaced with an index into
    /// the static or the dynamic table are matched against the entry the index
    /// resolves to. Pseudo-headers are addressed without their leading colon,
    /// see [`crate::header`].
    ///
    /// # Arguments
    ///
    /// * `name` - The header name whose value to capture
    ///
    /// # Errors
    ///
    /// Returns an error if `name` cannot be Huffman encoded.
    pub fn capture_hdr(mut self, name: &HeaderName) -> Result<Parser> {
        let mut name_encoded = Vec::new();
        huffman::encode(name.as_str().as_bytes(), &mut name_encoded)?;

        self.dfa
            .start_pattern(self.s_any)
            .push(&name_encoded)
            .capture_field_value();

        Ok(self)
    }

    /// 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.
    ///
    /// # Errors
    ///
    /// Returns an error if an entry cannot be encoded or written to the map.
    fn populate_static_table(&self, static_table: &MapHandle) -> Result<()> {
        let insert = |idx: u32, key: &str, val: Option<&str>| {
            let mut hf_key = Vec::new();
            huffman::encode(key.as_bytes(), &mut hf_key)?;

            let mut hf_val = Vec::new();
            if let Some(val) = val {
                huffman::encode(val.as_bytes(), &mut hf_val)?;
            }

            let key_len = hf_key.len() as u8;
            let val_len = hf_val.len() as u8;
            hf_key.resize(128, 0);
            hf_val.resize(128, 0);

            let hf = header_field {
                key: hf_key.try_into().unwrap(),
                key_len,
                val: hf_val.try_into().unwrap(),
                val_len,
                // the static table is written out Huffman coded above
                key_huff: 1,
                val_huff: 1,
            };

            let idx = unsafe { idx.as_bytes() };
            let hf = unsafe { hf.as_bytes() };

            static_table.update(&idx, &hf, MapFlags::ANY)?;

            anyhow::Ok(())
        };

        let (st_keys, st_hfs) = create_header_maps();
        for (key, vals) in st_hfs.iter() {
            for (val, idx) in vals.iter() {
                insert(*idx as u32, key, Some(val))?;
            }
        }

        for (key, idx) in st_keys.iter() {
            insert(*idx as u32, key, None)?;
        }

        static_table.freeze()?;

        Ok(())
    }

    /// Loads the configured parser and attaches it to the target program.
    ///
    /// Every function configured with one of the `replace_*` methods is
    /// replaced in the target program, the remaining parser programs are left
    /// unloaded. Loading the parser also populates the HPACK static table.
    ///
    /// # Arguments
    ///
    /// * `target` - The file descriptor of the target program to attach to
    ///
    /// # Errors
    ///
    /// Returns an error if the parser cannot be loaded, or if one of the
    /// functions it should replace does not exist in the target program with a
    /// matching signature.
    pub fn attach<'obj>(self, target: i32) -> Result<AttachedParser> {
        let skel_builder = ParserSkelBuilder::default();
        let mut open_obj: MaybeUninit<OpenObject> = MaybeUninit::uninit();
        let mut open_skel = skel_builder.open(&mut open_obj)?;
        if tracing::event_enabled!(Level::TRACE) {
            open_skel.progs.parse_msg.set_log_level(1);
            open_skel.progs.parse_skb.set_log_level(1);
            open_skel.progs.parse_buf.set_log_level(1);
        }

        let progs = vec![
            (&mut open_skel.progs.parse_msg, self.parse_msg_fn.clone()),
            (&mut open_skel.progs.parse_skb, self.parse_skb_fn.clone()),
            (&mut open_skel.progs.parse_buf, self.parse_buf_fn.clone()),
            (&mut open_skel.progs.matched, self.matched_fn.clone()),
            (&mut open_skel.progs.extract_match, self.extract_fn.clone()),
            (
                &mut open_skel.progs.get_dt_entry,
                self.get_dynamic_table_entry_fn.clone(),
            ),
        ];

        for (prog, func) in progs {
            autoload_and_attach(prog, target, func)?;
        }

        self.inject(&mut open_skel)?;

        let skel = open_skel.load()?;
        xbpf::tracing::try_init(skel.object())?;

        let mut links = Vec::new();
        if self.parse_msg_fn.is_some() {
            links.push(skel.progs.parse_msg.attach()?);
        }
        if self.parse_skb_fn.is_some() {
            links.push(skel.progs.parse_skb.attach()?);
        }
        if self.parse_buf_fn.is_some() {
            links.push(skel.progs.parse_buf.attach()?);
        }
        if self.matched_fn.is_some() {
            links.push(skel.progs.matched.attach()?);
        }
        if self.extract_fn.is_some() {
            links.push(skel.progs.extract_match.attach()?);
        }
        if self.get_dynamic_table_entry_fn.is_some() {
            links.push(skel.progs.get_dt_entry.attach()?);
        }

        let id = skel.maps.static_table.info()?.info.id;
        let static_table = MapHandle::from_map_id(id)?;
        self.populate_static_table(&static_table)?;

        debug!("Beeper http/2 attached");

        let id = skel.maps.dynamic_table_info.info()?.info.id;
        Ok(AttachedParser {
            dynamic_table_info: MapHandle::from_map_id(id)?,
            links,
        })
    }

    /// 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.
    fn inject(&self, skel: &mut OpenParserSkel) -> Result<()> {
        for (from, to, input, action) in self.dfa.iter_transitions() {
            let s = *from 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;
        }

        Ok(())
    }
}

/// A [`Parser`] attached to a target program.
///
/// It owns the links of the attached programs, so the target program keeps its
/// parser for as long as this value is alive.
pub struct AttachedParser {
    /// The map holding the state of the dynamic table of every connection the
    /// parser has seen a header block on.
    dynamic_table_info: MapHandle,

    #[allow(dead_code)]
    links: Vec<Link>,
}

/// The state of the dynamic table the parser mirrors for a connection.
///
/// This is mostly useful to assert that the kernel side stayed in sync with the
/// peer's own table.
#[repr(C)]
#[derive(Default, Clone)]
pub struct DynamicTableInfo {
    /// The number of entries currently in the table.
    pub count: u32,

    /// The size of those entries, as defined by section 4.1 of RFC 7541.
    pub size: u32,

    /// The maximum size the peer announced, either as the initial value or with
    /// a `SETTINGS_HEADER_TABLE_SIZE` setting.
    pub max_size: u32,

    /// The number of entries evicted so far. Together with `count` it turns an
    /// HPACK index into an index into the table.
    pub deleted: u32,
}

unsafe impl Plain for DynamicTableInfo {}

impl AttachedParser {
    /// Returns the state of the dynamic table the parser keeps for the
    /// connection between `local` and `remote`.
    ///
    /// # Errors
    ///
    /// Returns an error if the parser has not seen a header block on that
    /// connection yet, or if the map cannot be read.
    ///
    /// # Panics
    ///
    /// Panics if either address is an IPv6 address.
    pub fn dynamic_table_info(
        &self,
        local: SocketAddr,
        remote: SocketAddr,
    ) -> Result<DynamicTableInfo> {
        let conn = ip4_conn {
            local: local.into(),
            remote: remote.into(),
        };

        let key = unsafe { conn.as_bytes() };
        let val = self.dynamic_table_info.lookup(key, MapFlags::empty())?;
        let Some(val) = val else {
            bail!("no dynamic table info for connection");
        };

        let info: Result<&DynamicTableInfo, _> = plain::from_bytes(&val);
        match info {
            Ok(info) => Ok(info.clone()),
            Err(e) => bail!("failed to parse dynamic table info: {:?}", e),
        }
    }
}