Skip to content

Commit

Permalink
feat(process-monitor): Detect new containers
Browse files Browse the repository at this point in the history
Enhance the process monitor with an ability to detect when a
container runtime creates a new PID namespace, which we can consider
as a creation of a new container.

Achieve that by:

* Registering the inodes of container runtime binaries we want to
  track in the user-space, saving them in a BPF map.
* In BPF, every time a process is being executed using the runtime
  binary, checking whether the PID namespace was changed.
  • Loading branch information
vadorovsky committed Oct 29, 2023
1 parent 57fb058 commit b43c665
Show file tree
Hide file tree
Showing 13 changed files with 452 additions and 43 deletions.
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/bpf-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,15 @@ procfs = { workspace = true }
libc = { workspace = true }
glob = { workspace = true }
hex = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }

# Test deps
which = { workspace = true, optional = true }
cgroups-rs = { workspace = true, optional = true }
rand = { workspace = true, optional = true }

validatron = { workspace = true }

[build-dependencies]
bpf-builder = { workspace = true }
148 changes: 148 additions & 0 deletions crates/bpf-common/src/parsing/containers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
use std::{
fs::File,
io::{self, BufReader},
process::Command,
};

use serde::{Deserialize, Serialize};
use thiserror::Error;
use validatron::Validatron;

#[derive(Error, Debug)]
pub enum ContainerError {
#[error("reading file {path} failed")]
ReadFile {
#[source]
source: io::Error,
path: String,
},
#[error("parsing config from `{path}` failed")]
ParseConfig {
#[source]
source: serde_json::error::Error,
path: String,
},
#[error("executing {command} failed")]
Exec {
#[source]
source: io::Error,
command: String,
},
#[error("executing {command} failed with status {code:?}")]
ExecStatus { command: String, code: Option<i32> },
#[error("parsing image digest {digest} failed")]
ParseDigest { digest: String },
#[error("invalid hash function {hash_fn}")]
InvalidHashFunction { hash_fn: String },
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ContainerId {
Docker(String),
Libpod(String),
}

#[derive(Debug, Deserialize)]
struct DockerConfig {
#[serde(rename = "Config")]
config: DockerContainerConfig,
#[serde(rename = "Image")]
image_digest: String,
#[serde(rename = "Name")]
name: String,
}

#[derive(Debug, Deserialize)]
struct DockerContainerConfig {
#[serde(rename = "Image")]
image: String,
}

#[derive(Debug, Deserialize)]
struct LibpodConfig {
#[serde(rename = "Name")]
name: String,
#[serde(rename = "Image")]
image: String,
#[serde(rename = "ImageDigest")]
image_digest: String,
}

#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Validatron)]
pub struct ContainerInfo {
pub id: String,
pub name: String,
pub image: String,
pub image_digest: String,
}

pub fn get_container_info(id: ContainerId) -> Result<ContainerInfo, ContainerError> {
match id {
ContainerId::Docker(id) => {
let path = format!("/var/lib/docker/containers/{}/config.v2.json", id);
let file = File::open(&path).map_err(|source| ContainerError::ReadFile {
source,
path: path.clone(),
})?;

let reader = BufReader::new(file);
let config: DockerConfig = serde_json::from_reader(reader)
.map_err(|source| ContainerError::ParseConfig { source, path })?;

let name = config.name;
let name = if name.starts_with('/') {
name[1..].to_owned()
} else {
name
};
let image = config.config.image;
let image_digest = config.image_digest;

Ok(ContainerInfo {
id,
name,
image,
image_digest,
})
}
ContainerId::Libpod(id) => {
// TODO(vadorovsky): Find a file from which that information
// could be retrieved.
let output = Command::new("podman")
.arg("inspect")
.arg("--type=container")
.arg(&id)
.output()
.map_err(|source| ContainerError::Exec {
source,
command: "podman".to_owned(),
})?;

if !output.status.success() {
return Err(ContainerError::ExecStatus {
command: "podman".to_owned(),
code: output.status.code(),
});
}

let config: LibpodConfig =
serde_json::from_slice(&output.stdout).map_err(|source| {
ContainerError::ParseConfig {
source,
path: format!("podman inspect --type=container {id}"),
}
})?;

let name = config.name;
let image = config.image;
let image_digest = config.image_digest;

Ok(ContainerInfo {
id,
name,
image,
image_digest,
})
}
}
}
1 change: 1 addition & 0 deletions crates/bpf-common/src/parsing/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod containers;
pub mod procfs;

mod buffer_index;
Expand Down
29 changes: 29 additions & 0 deletions crates/bpf-common/src/parsing/procfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@ use std::{
};
use thiserror::Error;

use super::containers::ContainerId;

// Special value used to indicate openat should use the current working directory.
const AT_FDCWD: i32 = -100;

/// Prefix of Docker cgroups.
const CGROUP_PREFIX_DOCKER: &str = "/docker/";
/// Prefix of podman/libpod cgroups.
const CGROUP_PREFIX_LIBPOD: &str = "/libpod_parent/libpod-";

#[derive(Error, Debug)]
pub enum ProcfsError {
#[error("reading link failed {path}")]
Expand Down Expand Up @@ -153,3 +160,25 @@ pub fn get_running_processes() -> Result<Vec<Pid>, ProcfsError> {
})
.collect()
}

pub fn get_process_container_id(pid: Pid) -> Result<Option<ContainerId>, ProcfsError> {
let path = format!("/proc/{pid}/cgroup");
let file = File::open(&path).map_err(|source| ProcfsError::ReadFile { source, path })?;

let reader = BufReader::new(file);
for line in reader.lines().flatten() {
log::warn!("cgroup: {line}");
if let Some(start) = line.rfind(CGROUP_PREFIX_DOCKER) {
return Ok(Some(ContainerId::Docker(
line[start + CGROUP_PREFIX_DOCKER.len()..].to_string(),
)));
}
if let Some(start) = line.rfind(CGROUP_PREFIX_LIBPOD) {
return Ok(Some(ContainerId::Libpod(
line[start + CGROUP_PREFIX_LIBPOD.len()..].to_string(),
)));
}
}

Ok(None)
}
8 changes: 7 additions & 1 deletion crates/bpf-common/src/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
//!
use core::fmt;
use std::{
collections::HashSet, convert::TryFrom, fmt::Display, mem::size_of, sync::Arc, time::Duration,
collections::HashSet, convert::TryFrom, fmt::Display, mem::size_of, path::PathBuf, sync::Arc,
time::Duration,
};

use aya::{
Expand Down Expand Up @@ -178,6 +179,11 @@ pub enum ProgramError {
BtfError(#[from] BtfError),
#[error("running background aya task {0}")]
JoinError(#[from] JoinError),
#[error("could not find the inode of {path}")]
InodeError {
path: PathBuf,
io_error: Box<std::io::Error>,
},
}

pub struct ProgramBuilder {
Expand Down
6 changes: 6 additions & 0 deletions crates/bpf-filtering/src/initializer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,24 @@ pub async fn setup_events_filter(
}
for process in &process_tree {
initializer.update(process)?;

// TODO: Check if parent is runc.
// let is_new_container = process.namespaces.pid != process.namespaces.pid_for_children;

process_tracker.update(TrackerUpdate::Fork {
ppid: process.parent,
pid: process.pid,
timestamp: Timestamp::from(0),
namespaces: process.namespaces,
is_new_container: false,
});
process_tracker.update(TrackerUpdate::Exec {
pid: process.pid,
image: process.image.to_string(),
timestamp: Timestamp::from(0),
argv: Vec::new(),
namespaces: process.namespaces,
is_new_container: false,
});
}

Expand Down
1 change: 1 addition & 0 deletions crates/modules/process-monitor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ nix = { workspace = true }
log = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }
which = { workspace = true }

[build-dependencies]
bpf-builder = { workspace = true }
Loading

0 comments on commit b43c665

Please sign in to comment.