Back to writing

Creating a gRPC client for Tetragon in Rust using Tonic

Archived from the SubCom engineering blog. Subconscious Compute closed its doors; the guide survives here. Original page.
Tonic crane logo with the label Tetra gRPC Client on a dark canvas

Post card from the SubCom engineering blog.

The code for this project can be found here https://github.com/SubconsciousCompute/tetragon-grpc-client

Tetragon is a powerful tool that offers eBPF-based Security Observability and Runtime Enforcement, providing real-time security event detection and reaction capabilities, especially in Kubernetes environments.

Tetragon has a gRPC API and we’re interested in the getevents function. This function returns a stream which will send system process events. We’ll begin by setting up Tetragon and reading this stream through the tetra cli application:

  1. Download the Tetragon package
  2. Extract the tarball and run the install.sh script
  3. Check its status:
sudo systemctl status tetragon
  1. Run the tetra cli to check if you’re getting events
sudo tetra --server-address unix:///var/run/tetragon/tetragon.sock getevents
  1. You should get some JSON output

The API will be running on a Unix Domain Socket /var/run/tetragon/tetragon.sock. Our gRPC client will connect to this socket & get the events.

First we’ll need to get the proto files used for the Tetragon API. Luckily it’s mentioned in the documentation. We’ll add it as a submodule to our repository & symlink the relevant folder to a proto directory.

cargo new tetragon-grpc-client
cd tetragon-grpc-client
git submodule add https://github.com/cilium/tetragon.git tetragon/
ln -s ./tetragon/api/v1/ proto

Then our dependencies:

cargo add tonic prost prost-types

and dev dependencies. You may also need protoc (a proto compiler) if it is not installed.

# sudo apt install -y protobuf-compiler
cargo add --dev tonic-build

We’ll create a bulid.rs file to compile our proto files, but before we do that, make sure to install the the protoc Protocol Buffers compiler. The tonic examples readme might be of some help on setting this up. Once we have that create the build.rs at the root of your directory:

build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
    match tonic_build::configure().build_server(false).compile(
        &[
            "proto/tetragon/capabilities.proto",
            "proto/tetragon/tetragon.proto",
            "proto/tetragon/stack.proto",
            "proto/tetragon/events.proto",
            "proto/tetragon/sensors.proto",
        ],
        &["proto"],
    ) {
        Ok(_) => Ok(()),
        Err(e) => {
            // NOTE: print errors in a readable format
            eprintln!("{}", e);
            Err("Failed".into())
        }
    }
}

In build.rs file, we use tonic-build to compile the proto files and generate Rust code we can include in our program. Additionally we extract the error to print it with new lines.

Then in our main.rs we can add the following:

main.rs
pub mod tetragon {
    // NOTE: Since this is generated by tonic theres nothing else we can do.
    #![allow(clippy::large_enum_variant)]
    tonic::include_proto!("tetragon");
}

You’ll want to allow some clippy warnings as the generated code cannot be changed from our side. The include_proto! macro gives us access to the generated rust code inside our program. We can just import the types and create a program like so:

getevents.rs
use tokio::net::UnixStream;
use tonic::transport::{Channel, Endpoint, Uri};
use tower::service_fn;

use crate::tetragon::fine_guidance_sensors_client::FineGuidanceSensorsClient;
use crate::tetragon::{Filter, GetEventsRequest};

pub async fn socket_client(
    path: &'static str,
) -> Result<FineGuidanceSensorsClient<Channel>, Box<dyn std::error::Error>> {
    // NOTE: We will ignore this uri because uds do not use it
    // if your connector does use the uri it will be provided
    // as the request to the `MakeConnection`.
    // The url is the default address used for tetragon gRPC.
    let channel = Endpoint::try_from("http://0.0.0.0:54321")?
        .connect_with_connector(service_fn(move |_: Uri| {
            // Connect to a Uds socket
            UnixStream::connect(path)
        }))
        .await?;

    let client = FineGuidanceSensorsClient::new(channel);
    Ok(client)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // NOTE: get root
    sudo::escalate_if_needed()?;

    let mut client = socket_client("/var/run/tetragon/tetragon.sock").await?;

    let request = tonic::Request::new(GetEventsRequest {
        allow_list: vec![Filter::default()],
        ..Default::default()
    });

    let mut response = client.get_events(request).await?;

    while let Ok(Some(t)) = response.get_mut().message().await {
        tracing::info!("{t:?}");
    }

    Ok(())
}

Once you run this program, it’ll ask for a sudo access if it isn’t root and start returning events. It’ll look like this:

getevents via tetragon-grpc-client — asciinema.org/a/646726 Running the getevents function via the tetragon-grpc-client written in rust

Interested in work like this, apply at our Job Board.

Corrected a missing brace in the getevents.rs listing from the original post.