Compare commits

...
8 changed files with 43 additions and 11 deletions

No files matched your search

+1 -1
View File
@@ -82,7 +82,7 @@ async fn handle(req: Request, firewall: &Firewall) -> Result<Response, Infallibl
};
let reason = policies::screen(&url, firewall).await;
telemetry::screened(firewall, &url, reason.is_some());
telemetry::screened(firewall, &url, reason.as_deref());
if let Some(reason) = reason {
warn!(method = %req.method(), %url, %reason, "blocked");
return Ok(blocked(&reason));
+2
View File
@@ -56,6 +56,7 @@ fn options() -> Command {
}
fn serve() -> ! {
telemetry::set_mode("proxy");
let config = load();
let port = config.port;
print_setup(&config);
@@ -83,6 +84,7 @@ fn uninstall() -> ! {
}
fn run(invocation: Invocation) -> ! {
telemetry::set_mode("wrapper");
let command = &invocation.program;
let port = match aliasing::spawn(0, load()) {
Ok(port) => port,
+1 -1
View File
@@ -37,7 +37,7 @@ where
async fn serve(&self, req: Request) -> Result<Response, Self::Error> {
if let Some(url) = https_url(&req) {
let reason = policies::screen(&url, &self.firewall).await;
telemetry::screened(&self.firewall, &url, reason.is_some());
telemetry::screened(&self.firewall, &url, reason.as_deref());
if let Some(reason) = reason {
warn!(%url, "blocked by firewall: {reason}");
return Ok(blocked(&reason));
+1
View File
@@ -98,6 +98,7 @@ impl TransparentProxyHandlerFactory for Factory {
}
fn handler(exec: Executor) -> Result<Handler, BoxError> {
telemetry::set_mode("extension");
telemetry::heartbeat();
let config = FirewallConfig::load().inspect_err(|_| telemetry::error("config"))?;
let firewall = Arc::new(Firewall::new(config));
+3
View File
@@ -72,6 +72,9 @@ pub async fn screen(url: &str, firewall: &Firewall) -> Option<String> {
Some((_, Verdict::Deny(reason))) => {
Some(format!("{} {reason}", pkg.label()))
}
Some((filter, Verdict::Unknown(e))) if !firewall.config.fail_open => {
Some(format!("{} could not be checked by {filter} ({e})", pkg.label()))
}
_ => None,
}
}
+5 -1
View File
@@ -1,6 +1,6 @@
use crate::http::{blocked, reply, text};
use crate::policies::{self, Check, Decision};
use crate::{Firewall, FirewallConfig};
use crate::{telemetry, Firewall, FirewallConfig};
use rama::http::body::util::BodyExt;
use rama::http::client::EasyHttpWebClient;
use rama::http::header::CONTENT_TYPE;
@@ -19,6 +19,7 @@ pub fn serve(config: FirewallConfig) -> std::io::Result<()> {
bound.set_nonblocking(true)?;
crate::runtime()?.block_on(async move {
let firewall = Arc::new(Firewall::new(config));
telemetry::heartbeat();
let exec = Executor::default();
let listener = TcpListener::try_from_std_tcp_listener(bound, exec.clone())?;
let server = HttpServer::auto(exec).service(service_fn(move |req: Request| {
@@ -44,6 +45,7 @@ async fn screen(target: String, firewall: &Firewall) -> Response {
}
Decision::Block { eco, pkg, filter, reason } => {
let reason = format!("{} {reason}", pkg.label());
telemetry::screened_package(eco, &pkg, Some(&reason));
warn!(%eco, %filter, "blocked by firewall: {reason}");
blocked(&reason)
}
@@ -51,9 +53,11 @@ async fn screen(target: String, firewall: &Firewall) -> Response {
match &check {
Check::NoPackage => {}
Check::Allowed(pkg) => {
telemetry::screened_package(target.eco, pkg, None);
info!(eco = %target.eco, pkg = %pkg.label(), "cleared by every filter")
}
Check::Unverified { pkg, filter, error } => {
telemetry::screened_package(target.eco, pkg, None);
info!(pkg = %pkg.label(), %filter, %error, "filter could not check, allowing")
}
}
+20 -7
View File
@@ -1,10 +1,11 @@
use crate::policies::Package;
use crate::{Firewall, FirewallConfig};
use rama::http::client::EasyHttpWebClient;
use rama::http::header::CONTENT_TYPE;
use rama::http::service::client::HttpClientExt;
use rama::rt::blocking::Runtime;
use serde_json::{json, Value};
use std::sync::{LazyLock, Mutex};
use std::sync::{LazyLock, Mutex, OnceLock};
use std::time::Duration;
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;
@@ -29,29 +30,40 @@ static TELEMETRY: LazyLock<Option<Telemetry>> = LazyLock::new(|| {
})
});
/// How the firewall is running: "extension", "wrapper" or "proxy".
static MODE: OnceLock<&'static str> = OnceLock::new();
struct Telemetry {
key: &'static str,
session: String,
events: Mutex<Vec<Value>>,
}
pub fn screened(firewall: &Firewall, url: &str, blocked: bool) {
pub fn screened(firewall: &Firewall, url: &str, reason: Option<&str>) {
if TELEMETRY.is_none() {
return;
}
let Some((registry, rest)) = firewall.match_on(url, |r| r.upstream) else {
return;
};
let Some(pkg) = firewall.resolve(registry, rest).pkg else {
return;
};
let mut properties = json!({ "eco": registry.eco, "blocked": blocked });
if blocked {
if let Some(pkg) = firewall.resolve(registry, rest).pkg {
screened_package(registry.eco, &pkg, reason);
}
}
pub fn screened_package(eco: &str, pkg: &Package, reason: Option<&str>) {
let mut properties = json!({ "eco": eco, "blocked": reason.is_some() });
if let Some(reason) = reason {
properties["package"] = pkg.label().into();
properties["reason"] = reason.into();
}
record("package_screened", properties);
}
pub fn set_mode(mode: &'static str) {
MODE.set(mode).ok();
}
pub fn error(kind: &'static str) {
record("error", json!({ "kind": kind }));
}
@@ -81,6 +93,7 @@ pub fn flush() {
fn record(event: &str, mut properties: Value) {
let Some(telemetry) = &*TELEMETRY else { return };
properties["distinct_id"] = telemetry.session.clone().into();
properties["mode"] = MODE.get().copied().into();
properties["os"] = std::env::consts::OS.into();
properties["arch"] = std::env::consts::ARCH.into();
properties["$process_person_profile"] = false.into();
+10 -1
View File
@@ -1,6 +1,6 @@
use firewall::config::path;
use firewall::policies::filters::consult;
use firewall::policies::Package;
use firewall::policies::{screen, Package};
use firewall::{Firewall, FirewallConfig};
#[test]
@@ -31,3 +31,12 @@ async fn an_allowed_version_skips_the_filters() {
assert!(consult(&firewall, registry, &pkg("0.20.0")).await.is_none());
assert!(consult(&firewall, registry, &pkg("0.21.0")).await.is_some());
}
#[tokio::test]
async fn failing_closed_blocks_what_could_not_be_checked() {
let config = r#"{"fail_open": false, "denylist": {"base": "http://127.0.0.1:1"}}"#;
let firewall = Firewall::new(serde_json::from_str(config).expect("parses"));
let url = "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz";
let blocked = screen(url, &firewall).await.unwrap_or_default();
assert!(blocked.contains("could not be checked"), "{blocked:?}");
}