Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions bottlecap/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,17 @@ rustls-pki-types = { version = "1.0", default-features = false }
hyper-rustls = { version = "0.27.7", default-features = false }
rand = { version = "0.8", default-features = false }
prost = { version = "0.14", default-features = false }
tonic = { version = "0.14", features = ["transport", "codegen", "server", "channel", "router"], default-features = false }
tonic-types = { version = "0.14", default-features = false }
# otlp-only (also pulled transitively via datadog-protos, but the OTLP gRPC server
# needs them as direct deps). Gated behind the `otlp` feature.
tonic = { version = "0.14", features = ["transport", "codegen", "server", "channel", "router"], default-features = false, optional = true }
tonic-types = { version = "0.14", default-features = false, optional = true }
zstd = { version = "0.13.3", default-features = false }
futures = { version = "0.3.31", default-features = false }
serde-aux = { version = "4.7", default-features = false }
serde_html_form = { version = "0.2", default-features = false }
opentelemetry-proto = { version = "0.31.0", features = ["trace", "with-serde", "gen-tonic"] }
opentelemetry-semantic-conventions = { version = "0.30", features = ["semconv_experimental"] }
# otlp-only. Gated behind the `otlp` feature.
opentelemetry-proto = { version = "0.31.0", features = ["trace", "with-serde", "gen-tonic"], optional = true }
opentelemetry-semantic-conventions = { version = "0.30", features = ["semconv_experimental"], optional = true }
# Pinned to <0.8.3: version 0.8.3 upgraded to openssl-probe 0.2.x which scans all cert
# directories and parses ~200 individual cert files on Lambda instead of loading a single
# bundle file, adding ~45ms to each reqwest::Client::build() call.
Expand Down Expand Up @@ -85,7 +88,8 @@ datadog-opentelemetry = { git = "https://github.com/DataDog/dd-trace-rs", rev =
dogstatsd = { git = "https://github.com/DataDog/serverless-components", rev = "8ce37eb029410b7cf30847376772e3af6baa5f5c", default-features = false }
datadog-fips = { git = "https://github.com/DataDog/serverless-components", rev = "8ce37eb029410b7cf30847376772e3af6baa5f5c", default-features = false }
datadog-agent-config = { git = "https://github.com/DataDog/serverless-components", rev = "8ce37eb029410b7cf30847376772e3af6baa5f5c", default-features = false }
libddwaf = { version = "1.28.1", git = "https://github.com/DataDog/libddwaf-rust", rev = "d1534a158d976bd4f747bf9fcc58e0712d2d17fc", default-features = false, features = ["serde"] }
# appsec-only. Gated behind the `appsec` feature.
libddwaf = { version = "1.28.1", git = "https://github.com/DataDog/libddwaf-rust", rev = "d1534a158d976bd4f747bf9fcc58e0712d2d17fc", default-features = false, features = ["serde"], optional = true }

[dev-dependencies]
figment = { version = "0.10", default-features = false, features = ["yaml", "env", "test"] }
Expand Down Expand Up @@ -129,6 +133,8 @@ tikv-jemallocator = "0.5"

[features]
default = [
"otlp",
"appsec",
"reqwest/rustls-tls-native-roots",
"datadog-fips/default",
"datadog-agent-config/https",
Expand All @@ -137,13 +143,29 @@ default = [
"libdd-trace-obfuscation/https",
"libdd-trace-stats/https",
]
# OpenTelemetry Protocol (OTLP) ingest. Pulls in the heavy `tonic` /
# `opentelemetry-proto` stack. On by default so the shipped binary is unchanged.
otlp = [
"dep:opentelemetry-proto",
"dep:opentelemetry-semantic-conventions",
"dep:tonic",
"dep:tonic-types",
]
# App & API Protection (AAP / AppSec). Pulls in the `libddwaf` WAF engine. On by
# default so the shipped binary is unchanged.
appsec = ["dep:libddwaf"]
fips = [
"datadog-agent-config/fips",
"libdd-common/fips",
"libdd-trace-utils/fips",
"libdd-trace-obfuscation/fips",
"libdd-trace-stats/fips",
"datadog-fips/fips",
# OTLP is unconditional in the shipped binary; keep it in the FIPS feature set
# too since fips builds use --no-default-features.
"otlp",
# FIPS-mode WAF only makes sense when AppSec is compiled in.
"appsec",
"libddwaf/fips",
"reqwest/rustls-tls-native-roots-no-provider",
"rustls/fips",
Expand Down
32 changes: 28 additions & 4 deletions bottlecap/src/bin/bottlecap/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@ use tikv_jemallocator::Jemalloc;
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;

#[cfg(not(feature = "appsec"))]
use bottlecap::AppSecProcessorStub as AppSecProcessor;
#[cfg(feature = "appsec")]
use bottlecap::appsec::processor::{
Error::FeatureDisabled as AppSecFeatureDisabled, Processor as AppSecProcessor,
};
#[cfg(feature = "otlp")]
use bottlecap::otlp::{agent::Agent as OtlpAgent, should_enable_otlp_agent};
use bottlecap::{
DOGSTATSD_PORT, LAMBDA_RUNTIME_SLUG,
appsec::processor::{
Error::FeatureDisabled as AppSecFeatureDisabled, Processor as AppSecProcessor,
},
config::{
self, Config,
aws::{AwsConfig, build_lambda_function_arn},
Expand Down Expand Up @@ -53,7 +58,6 @@ use bottlecap::{
flusher::LogsFlusher,
lambda::DurableContextUpdate,
},
otlp::{agent::Agent as OtlpAgent, should_enable_otlp_agent},
proxy::{interceptor, should_start_proxy},
secrets::decrypt,
tags::{
Expand Down Expand Up @@ -388,6 +392,7 @@ async fn extension_loop_active(
});

// AppSec processor (if enabled)
#[cfg(feature = "appsec")]
let appsec_processor = match AppSecProcessor::new(config) {
Ok(p) => Some(Arc::new(TokioMutex::new(p))),
Err(AppSecFeatureDisabled) => None,
Expand All @@ -398,6 +403,10 @@ async fn extension_loop_active(
None
}
};
// AppSec compiled out: the processor is always absent, but the value is still
// threaded through trace/proxy wiring (as `None`) to keep call sites uniform.
#[cfg(not(feature = "appsec"))]
let appsec_processor: Option<Arc<TokioMutex<AppSecProcessor>>> = None;

let (
trace_agent_channel,
Expand Down Expand Up @@ -1441,6 +1450,7 @@ async fn setup_telemetry_client(
Ok(cancel_token)
}

#[cfg(feature = "otlp")]
fn start_otlp_agent(
config: &Arc<Config>,
tags_provider: Arc<TagProvider>,
Expand Down Expand Up @@ -1473,6 +1483,20 @@ fn start_otlp_agent(
Some(cancel_token)
}

/// No-op fallback when the `otlp` feature is disabled: the OTLP agent is not
/// compiled in, so there is never a cancellation token to return.
#[cfg(not(feature = "otlp"))]
#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
fn start_otlp_agent(
_config: &Arc<Config>,
_tags_provider: Arc<TagProvider>,
_trace_processor: Arc<dyn trace_processor::TraceProcessor + Send + Sync>,
_trace_tx: Sender<SendDataBuilderInfo>,
_stats_concentrator: StatsConcentratorHandle,
) -> Option<CancellationToken> {
None
}

fn start_api_runtime_proxy(
config: &Arc<Config>,
aws_config: Arc<AwsConfig>,
Expand Down
12 changes: 12 additions & 0 deletions bottlecap/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
// Allow use of the `coverage_nightly` attribute
#![cfg_attr(coverage_nightly, feature(coverage_attribute))]

#[cfg(feature = "appsec")]
pub mod appsec;
pub mod config;
pub mod event_bus;
Expand All @@ -31,13 +32,24 @@ pub mod logger;
pub mod logs;
pub mod lwa;
pub mod metrics;
#[cfg(feature = "otlp")]
pub mod otlp;
pub mod proc;
pub mod proxy;
pub mod secrets;
pub mod tags;
pub mod traces;

/// Placeholder standing in for [`appsec::processor::Processor`] when the `appsec`
/// feature is disabled. It keeps `Option<Arc<Mutex<…>>>` fields and the proxy
/// state tuple identically shaped across both builds, so call sites that merely
/// thread the value through (always as `None`) compile unchanged. It is an
/// uninhabited enum and can never be constructed; every code path that would
/// actually invoke the WAF is itself `#[cfg(feature = "appsec")]`-gated.
#[cfg(not(feature = "appsec"))]
#[derive(Debug, Clone, Copy)]
pub enum AppSecProcessorStub {}

pub const LAMBDA_RUNTIME_SLUG: &str = "lambda";

// todo: consider making this configurable
Expand Down
4 changes: 4 additions & 0 deletions bottlecap/src/lifecycle/invocation/triggers/body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ pub struct Body {
impl Body {
/// Obtains a reader to the data contained in this [`Body`], decoded from
/// Base64 if [`Body::is_base64_encoded`] is `true`.
///
/// Only consumed by the App and API Protection request/response inspection
/// code, so it is dead code when the `appsec` feature is disabled.
#[cfg_attr(not(feature = "appsec"), allow(dead_code))]
pub(crate) fn reader<'a>(&'a self) -> Result<Option<Box<dyn Read + 'a>>, base64::DecodeError> {
let Some(body) = &self.body else {
return Ok(None);
Expand Down
45 changes: 33 additions & 12 deletions bottlecap/src/proxy/interceptor.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
#[cfg(not(feature = "appsec"))]
use crate::AppSecProcessorStub as AppSecProcessor;
#[cfg(feature = "appsec")]
use crate::appsec::processor::Processor as AppSecProcessor;
use crate::lifecycle::invocation::processor_service::InvocationProcessorHandle;
#[cfg(feature = "appsec")]
use crate::lifecycle::invocation::triggers::IdentifiedTrigger;
use crate::traces::propagation::DatadogCompositePropagator;
use crate::{
appsec::processor::Processor as AppSecProcessor, config::aws::AwsConfig,
extension::EXTENSION_HOST, lwa, proxy::tee_body::TeeBodyWithCompletion,
config::aws::AwsConfig, extension::EXTENSION_HOST, lwa, proxy::tee_body::TeeBodyWithCompletion,
};
use axum::{
Router,
Expand Down Expand Up @@ -179,6 +183,7 @@ async fn invocation_next_proxy(
if let Ok(body) = intercepted_completion_receiver.await {
debug!("PROXY | invocation_next_proxy | intercepted body completed");

#[cfg(feature = "appsec")]
if let Some(appsec_processor) = appsec_processor
&& let Some(request_id) = intercepted_parts_clone
.headers
Expand All @@ -195,6 +200,10 @@ async fn invocation_next_proxy(
}
}
}
// AppSec compiled out: the processor is always `None`; drop it to keep
// the State tuple shape uniform without an unused-variable warning.
#[cfg(not(feature = "appsec"))]
let _ = appsec_processor;

if aws_config.aws_lwa_proxy_lambda_runtime_api.is_some() {
lwa::process_invocation_next(
Expand Down Expand Up @@ -241,13 +250,16 @@ async fn invocation_response_proxy(
join_set.spawn(async move {
if let Ok(body) = outgoing_completion_receiver.await {
debug!("PROXY | invocation_response_proxy | intercepted outgoing body completed");
#[cfg(feature = "appsec")]
if let Some(appsec_processor) = appsec_processor {
appsec_processor
.lock()
.await
.process_invocation_result(&request_id, &body)
.await;
}
#[cfg(not(feature = "appsec"))]
let _ = appsec_processor;

if aws_config_clone.aws_lwa_proxy_lambda_runtime_api.is_some() {
lwa::process_invocation_response(&invocation_processor, &body).await;
Expand Down Expand Up @@ -304,14 +316,17 @@ async fn invocation_error_proxy(
request: Request,
) -> Response {
debug!("PROXY | invocation_error_proxy | api_version: {api_version}, request_id: {request_id}");
let State((_, _, _, appsec_processor, _, _)) = &state;
if let Some(appsec_processor) = appsec_processor {
// Marking any outstanding security context as finalized by sending a blank response.
appsec_processor
.lock()
.await
.process_invocation_result(&request_id, &Bytes::from("{}"))
.await;
#[cfg(feature = "appsec")]
{
let State((_, _, _, appsec_processor, _, _)) = &state;
if let Some(appsec_processor) = appsec_processor {
// Marking any outstanding security context as finalized by sending a blank response.
appsec_processor
.lock()
.await
.process_invocation_result(&request_id, &Bytes::from("{}"))
.await;
}
}

passthrough_proxy(state, request).await
Expand Down Expand Up @@ -442,13 +457,16 @@ mod tests {
use hyper_util::rt::TokioIo;

use super::*;
#[cfg(feature = "appsec")]
use crate::appsec::processor::Error::FeatureDisabled as AppSecFeatureDisabled;
use crate::lifecycle::invocation::processor_service::InvocationProcessorService;
use crate::{
LAMBDA_RUNTIME_SLUG, appsec::processor::Error::FeatureDisabled as AppSecFeatureDisabled,
config::Config, tags::provider::Provider, traces::propagation::DatadogCompositePropagator,
LAMBDA_RUNTIME_SLUG, config::Config, tags::provider::Provider,
traces::propagation::DatadogCompositePropagator,
};

#[tokio::test]
#[allow(clippy::too_many_lines)]
async fn test_noop_proxy() {
let aws_lwa_lambda_runtime_api = "127.0.0.1:12345";
let aws_lambda_runtime_api = "127.0.0.1:12344";
Expand Down Expand Up @@ -513,6 +531,7 @@ mod tests {
invocation_processor_service.run().await;
});

#[cfg(feature = "appsec")]
let appsec_processor = match AppSecProcessor::new(&config) {
Ok(p) => Some(Arc::new(TokioMutex::new(p))),
Err(AppSecFeatureDisabled) => None,
Expand All @@ -523,6 +542,8 @@ mod tests {
None
}
};
#[cfg(not(feature = "appsec"))]
let appsec_processor: Option<Arc<TokioMutex<AppSecProcessor>>> = None;

let proxy_handle = start(
aws_config,
Expand Down
5 changes: 4 additions & 1 deletion bottlecap/src/traces/trace_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ use tokio_util::sync::CancellationToken;
use tower_http::limit::RequestBodyLimitLayer;
use tracing::{debug, error, warn};

#[cfg(not(feature = "appsec"))]
use crate::AppSecProcessorStub as AppSecProcessor;
#[cfg(feature = "appsec")]
use crate::appsec::processor::Processor as AppSecProcessor;
use crate::traces::trace_processor::SendingTraceProcessor;
use crate::{
appsec::processor::Processor as AppSecProcessor,
config,
http::{extract_request_body, handler_not_found},
lifecycle::invocation::{
Expand Down
Loading
Loading