Compare commits

...
1 Commits
Author SHA1 Message Date
Ian 582c260d44 Add a rustfmt config matching the code's style, and format to it 2026-10-23 12:15:06 -04:00
20 changed files with 87 additions and 106 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
use_small_heuristics = "Max"
+4 -1
View File
@@ -83,7 +83,10 @@ fn record(
let holds = filter == "age" && watched;
// A repeat fetch of a held package waits on the same answer.
let waiting = blocks.held.iter().find(|(held, wake)| {
wake.receiver_count() > 0 && held.eco == eco && held.name == pkg.name && held.version == pkg.version
wake.receiver_count() > 0
&& held.eco == eco
&& held.name == pkg.name
&& held.version == pkg.version
});
if let (true, Some((_, wake))) = (holds, waiting) {
return Some(wake.subscribe());
+7 -3
View File
@@ -86,10 +86,13 @@ impl FirewallConfig {
Err(e) if e.kind() == ErrorKind::NotFound => continue,
Err(e) => return Err(Error::new(e.kind(), format!("{}: {e}", path.display()))),
};
let invalid = |e| Error::new(ErrorKind::InvalidData, format!("{}: {e}", path.display()));
let invalid =
|e| Error::new(ErrorKind::InvalidData, format!("{}: {e}", path.display()));
let layer: Value = serde_json::from_str(&text).map_err(invalid)?;
let config = FirewallConfig::deserialize(&layer).map_err(invalid)?;
check(&config).map_err(|e| Error::new(ErrorKind::InvalidData, format!("{}: {e}", path.display())))?;
check(&config).map_err(|e| {
Error::new(ErrorKind::InvalidData, format!("{}: {e}", path.display()))
})?;
let Value::Object(layer) = layer else {
unreachable!("a config parses only from an object");
};
@@ -122,7 +125,8 @@ fn merge(base: &mut Map<String, Value>, top: Map<String, Value>) {
/// Adds an `allow` entry to the file, leaving the rest of it as written.
pub fn allow(path: &Path, eco: &str, name: &str, version: &str) -> std::io::Result<()> {
let invalid = |e: String| Error::new(ErrorKind::InvalidData, format!("{}: {e}", path.display()));
let invalid =
|e: String| Error::new(ErrorKind::InvalidData, format!("{}: {e}", path.display()));
let mut config = match std::fs::read_to_string(path) {
Ok(text) => serde_json::from_str(&text).map_err(|e| invalid(e.to_string()))?,
Err(e) if e.kind() == ErrorKind::NotFound => json!({}),
+5 -1
View File
@@ -25,7 +25,11 @@ pub fn local_url(port: u16, path: &str) -> String {
}
pub fn init_logging(default_level: &str) {
fmt().with_env_filter(log_filter(default_level)).with_target(false).with_writer(std::io::stderr).init();
fmt()
.with_env_filter(log_filter(default_level))
.with_target(false)
.with_writer(std::io::stderr)
.init();
}
pub(crate) fn log_filter(default_level: &str) -> EnvFilter {
+1 -4
View File
@@ -58,10 +58,7 @@ fn serve() -> ! {
Ok(()) => exit(0),
Err(e) if e.kind() == ErrorKind::AddrInUse => {
let location = config::path().map_or("the config".into(), |p| p.display().to_string());
fail(
"port",
&format!("port {port} is in use, set another in {location}"),
)
fail("port", &format!("port {port} is in use, set another in {location}"))
}
Err(e) if e.kind() == ErrorKind::PermissionDenied => {
fail("port", &format!("not allowed to listen on port {port}"))
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::http::{blocked, https_url};
use crate::{policies, telemetry};
use crate::Firewall;
use crate::{policies, telemetry};
use rama::error::BoxError;
use rama::http::{Request, Response};
use rama::{Layer, Service};
+6 -3
View File
@@ -75,8 +75,8 @@ fn create() -> Result<SecKey, BoxError> {
.set_key_type(KeyType::ec_sec_prime_random())
.set_token(Token::SecureEnclave)
.set_access_control(access);
let key = SecKey::new(&options)
.map_err(|e| format!("secure enclave key generation failed: {e}"))?;
let key =
SecKey::new(&options).map_err(|e| format!("secure enclave key generation failed: {e}"))?;
let blob = key
.attributes()
@@ -102,7 +102,10 @@ fn load(blob: &[u8]) -> Result<SecKey, BoxError> {
CFString::wrap_under_get_rule(kSecAttrKeyClass),
CFString::wrap_under_get_rule(kSecAttrKeyClassPrivate).into_CFType(),
),
(CFString::from_static_string(TOKEN_OBJECT_ID), CFData::from_buffer(blob).into_CFType()),
(
CFString::from_static_string(TOKEN_OBJECT_ID),
CFData::from_buffer(blob).into_CFType(),
),
])
};
let mut error = ptr::null_mut();
+5 -3
View File
@@ -5,8 +5,8 @@ mod uninstall;
pub use uninstall::uninstall;
use crate::config::FirewallConfig;
use crate::{blocks, mitm, telemetry};
use crate::Firewall;
use crate::{blocks, mitm, telemetry};
use rama::bytes::Bytes;
use rama::error::BoxError;
use rama::io::BridgeIo;
@@ -107,7 +107,8 @@ pub fn system_config() -> Option<PathBuf> {
fn handler(exec: Executor) -> Result<Handler, BoxError> {
telemetry::set_mode("extension");
telemetry::heartbeat();
let config = FirewallConfig::load_from(&[CONFIG_FILE.into()]).inspect_err(|_| telemetry::error("config"))?;
let config = FirewallConfig::load_from(&[CONFIG_FILE.into()])
.inspect_err(|_| telemetry::error("config"))?;
let firewall = Arc::new(Firewall::new(config));
let hosts = firewall.hosts();
let key = enclave::EnclaveKey::load_or_create().inspect_err(|_| telemetry::error("enclave"))?;
@@ -125,7 +126,8 @@ fn init(_config: Option<&TransparentProxyInitConfig>) -> bool {
std::panic::set_hook(Box::new(|panic| tracing::error!("{panic}")));
if let Ok(oslog) = OsLogLayer::new(BUNDLE_ID, "extension") {
let oslog = oslog.with_privacy(Privacy::Public);
let _ = tracing_subscriber::registry().with(crate::log_filter("info")).with(oslog).try_init();
let _ =
tracing_subscriber::registry().with(crate::log_filter("info")).with(oslog).try_init();
}
true
}
+2 -6
View File
@@ -37,9 +37,7 @@ pub fn load_or_create_ca(
None => {
warn!(?hosts, "minting a new CA");
let ca = CertificateAuthority::generate(hosts, signer)?;
LOCAL_MACHINE
.create(REGISTRY_KEY)?
.set_string("ca", ca.to_blob(hosts))?;
LOCAL_MACHINE.create(REGISTRY_KEY)?.set_string("ca", ca.to_blob(hosts))?;
// Windows discards CRLs it cached before now, in case an old CA's is cached at our URL.
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos() / 100
+ FILETIME_UNIX_EPOCH;
@@ -119,9 +117,7 @@ fn owned_by_admins(path: &Path) -> windows::core::Result<bool> {
.ok()?
};
let admins = [WinBuiltinAdministratorsSid, WinLocalSystemSid];
let owned = admins
.into_iter()
.any(|sid| unsafe { IsWellKnownSid(owner, sid) }.as_bool());
let owned = admins.into_iter().any(|sid| unsafe { IsWellKnownSid(owner, sid) }.as_bool());
// `owner` points into the descriptor, so it is freed last.
unsafe { LocalFree(Some(HLOCAL(descriptor.0))) };
Ok(owned)
+5 -20
View File
@@ -48,9 +48,7 @@ pub fn system_config() -> Option<PathBuf> {
/// In the folder Windows reads it from, normally %SystemRoot%\System32\drivers\etc.
fn hosts_file() -> PathBuf {
let key = r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters";
let configured = LOCAL_MACHINE
.open(key)
.and_then(|key| key.get_hstring("DataBasePath"));
let configured = LOCAL_MACHINE.open(key).and_then(|key| key.get_hstring("DataBasePath"));
let dir = match configured {
Ok(dir) => expand_environment(&dir),
Err(_) => known_folder(&FOLDERID_System).join(r"drivers\etc"),
@@ -71,17 +69,12 @@ fn expand_environment(path: &HSTRING) -> PathBuf {
let mut expanded = vec![0; len as usize];
let len = unsafe { ExpandEnvironmentStringsW(path, Some(&mut expanded)) };
// Less the terminating null.
PathBuf::from(String::from_utf16_lossy(
&expanded[..(len as usize).saturating_sub(1)],
))
PathBuf::from(String::from_utf16_lossy(&expanded[..(len as usize).saturating_sub(1)]))
}
/// From 127.0.0.3, leaving 127.0.0.1 to whatever else serves locally.
fn loopback_addresses(hosts: &[&'static str]) -> Vec<(Ipv4Addr, &'static str)> {
(3..)
.zip(hosts)
.map(|(i, host)| (Ipv4Addr::new(127, 0, 0, i), *host))
.collect()
(3..).zip(hosts).map(|(i, host)| (Ipv4Addr::new(127, 0, 0, i), *host)).collect()
}
/// Points each host at its listener, replacing the lines we wrote before; with none, only removes
@@ -91,11 +84,7 @@ fn write_hosts_entries(hosts: &[&'static str]) -> std::io::Result<()> {
let old = std::fs::read(hosts_file())?;
let ours = |line: &&[u8]| line.trim_ascii_end().ends_with(TAG.as_bytes());
let lines = old.split_inclusive(|&byte| byte == b'\n');
let mut new: Vec<u8> = lines
.filter(|line| !ours(line))
.flatten()
.copied()
.collect();
let mut new: Vec<u8> = lines.filter(|line| !ours(line)).flatten().copied().collect();
if !new.is_empty() && !new.ends_with(b"\n") {
new.extend_from_slice(b"\r\n");
}
@@ -120,11 +109,7 @@ fn security_descriptor(sddl: &str) -> windows::core::Result<Owned<HLOCAL>> {
}
fn read_registry_string(key: &str, value: &str) -> Result<Option<String>, BoxError> {
none_if_missing(
LOCAL_MACHINE
.open(key)
.and_then(|key| key.get_string(value)),
)
none_if_missing(LOCAL_MACHINE.open(key).and_then(|key| key.get_string(value)))
}
/// `None` for a missing key or value.
+1 -4
View File
@@ -51,10 +51,7 @@ fn create(first: bool) -> Result<NamedPipeServer, BoxError> {
async fn answer(client: NamedPipeServer, firewall: &Firewall) -> Result<(), BoxError> {
let mut client = BufReader::new(client);
let mut request = String::new();
(&mut client)
.take(64 * 1024)
.read_line(&mut request)
.await?;
(&mut client).take(64 * 1024).read_line(&mut request).await?;
let request = request.trim_end();
// Only the app can message the macOS extension; here any program could connect.
let reply = if !client_is_tray(client.get_ref())? {
+4 -12
View File
@@ -79,9 +79,7 @@ fn run_until_stopped() -> Result<(), BoxError> {
// Answered once handled, so a failed pause fails the caller's ControlService.
let (reply, handled) = tokio::sync::oneshot::channel();
let _ = control_sender.send((control, reply));
handled
.blocking_recv()
.unwrap_or(ServiceControlHandlerResult::NoError)
handled.blocking_recv().unwrap_or(ServiceControlHandlerResult::NoError)
}
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
_ => ServiceControlHandlerResult::NotImplemented,
@@ -109,9 +107,7 @@ fn run_until_stopped() -> Result<(), BoxError> {
}
Err(error) => {
tracing::error!(%error, "could not rewrite the hosts file");
let code = error
.raw_os_error()
.map_or(ERROR_GEN_FAILURE.0, |code| code as u32);
let code = error.raw_os_error().map_or(ERROR_GEN_FAILURE.0, |code| code as u32);
ServiceControlHandlerResult::Other(code)
}
};
@@ -165,12 +161,8 @@ async fn start_screening(status: &ServiceStatusHandle) -> Result<Vec<&'static st
let service = mitm::mitm_service(firewall, &ca, signer, Some(crl_url), exec.clone());
for (address, host) in loopback_addresses(&hosts) {
for port in [80, 443] {
let upstream = RedirectedHost {
host,
port,
resolver: resolver.clone(),
service: service.clone(),
};
let upstream =
RedirectedHost { host, port, resolver: resolver.clone(), service: service.clone() };
let upstream = ConsumeErrLayer::trace_as_warning().into_layer(upstream);
tokio::spawn(listen_on(address, port, &exec)?.serve(upstream));
}
+8 -27
View File
@@ -50,9 +50,7 @@ pub fn run() -> Result<(), BoxError> {
&MenuItem::with_id(MENU_QUIT, "Quit", true, None),
])?;
let (logo, logo_size) = logo_mask()?;
let icon = TrayIconBuilder::new()
.with_menu(Box::new(menu.clone()))
.build()?;
let icon = TrayIconBuilder::new().with_menu(Box::new(menu.clone())).build()?;
let window = HWND(icon.window_handle());
let mut tray = Tray {
icon,
@@ -84,11 +82,7 @@ pub fn run() -> Result<(), BoxError> {
}
while let Ok(event) = TrayIconEvent::receiver().try_recv() {
// The menu opens on release, so this keeps it current.
if let TrayIconEvent::Click {
button_state: MouseButtonState::Down,
..
} = event
{
if let TrayIconEvent::Click { button_state: MouseButtonState::Down, .. } = event {
tray.update();
}
}
@@ -218,11 +212,7 @@ impl Tray {
}
let _ = self.menu.insert(&self.blocked_separator, BLOCKED_AT);
for (i, package) in self.blocked.iter().enumerate() {
let verb = if package.malicious() {
"Blocked"
} else {
"Held back"
};
let verb = if package.malicious() { "Blocked" } else { "Held back" };
let reason = if package.malicious() {
format!("Malicious: {}", package.reason)
} else {
@@ -230,11 +220,8 @@ impl Tray {
};
let reason = MenuItem::new(reason, false, None);
let once = MenuItem::with_id(format!("{ALLOW_ONCE}:{i}"), "Allow Once", true, None);
let items: &[&dyn IsMenuItem] = if package.trustable() {
&[&reason, &once]
} else {
&[&reason]
};
let items: &[&dyn IsMenuItem] =
if package.trustable() { &[&reason, &once] } else { &[&reason] };
let label = format!("{verb} {}", package.label());
if let Ok(item) = Submenu::with_items(label, true, items) {
let _ = self.menu.insert(&item, BLOCKED_AT + 1 + i);
@@ -316,10 +303,7 @@ fn allow_once(package: BlockedPackage, allowed: &Sender<BlockedPackage>) {
/// One request per connection, as the service answers.
fn ask_service(request: &str) -> Result<String, BoxError> {
let mut pipe = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(PIPE)?;
let mut pipe = std::fs::OpenOptions::new().read(true).write(true).open(PIPE)?;
// Any program can create the pipe if it gets there before the service.
let mut server = 0;
unsafe { GetNamedPipeServerProcessId(HANDLE(pipe.as_raw_handle()), &mut server)? };
@@ -345,9 +329,7 @@ fn show_error(error: BoxError) {
fn service_status() -> Option<ServiceStatus> {
let manager =
ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT).ok()?;
let service = manager
.open_service(SERVICE_NAME, ServiceAccess::QUERY_STATUS)
.ok()?;
let service = manager.open_service(SERVICE_NAME, ServiceAccess::QUERY_STATUS).ok()?;
service.query_status().ok()
}
@@ -356,8 +338,7 @@ fn light_taskbar() -> bool {
let Ok(key) = CURRENT_USER.open(personalize) else {
return false;
};
key.get_u32("SystemUsesLightTheme")
.is_ok_and(|light| light == 1)
key.get_u32("SystemUsesLightTheme").is_ok_and(|light| light == 1)
}
fn status_text(state: Option<ServiceState>) -> &'static str {
+1 -3
View File
@@ -63,9 +63,7 @@ impl Filter for Denylist {
fn check<'a>(&'a self, registry: &'static Registry, pkg: &'a Package) -> VerdictFuture<'a> {
Box::pin(async move {
let eco = registry.osv_name;
let load = || async {
RwLock::new(Arc::new(load(&self.source, eco).await))
};
let load = || async { RwLock::new(Arc::new(load(&self.source, eco).await)) };
let list = self.lists[eco].get_or_init(load).await;
let list = Arc::clone(&list.read().expect("denylist lock"));
match &*list {
+4 -2
View File
@@ -41,8 +41,10 @@ impl Filter for Osv {
if hits.is_empty() {
Verdict::Allow
} else {
let links: Vec<String> =
hits.iter().map(|id| format!("{id} https://osv.dev/vulnerability/{id}")).collect();
let links: Vec<String> = hits
.iter()
.map(|id| format!("{id} https://osv.dev/vulnerability/{id}"))
.collect();
Verdict::Deny(format!("is flagged malicious by OSV: {}", links.join(", ")))
}
})
+1 -5
View File
@@ -23,11 +23,7 @@ static TELEMETRY: LazyLock<Option<Telemetry>> = LazyLock::new(|| {
return None;
}
let session = format!("{:016x}", rand::random::<u64>());
Some(Telemetry {
key,
session,
events: Mutex::default(),
})
Some(Telemetry { key, session, events: Mutex::default() })
});
/// How the firewall is running: "extension", "service", "wrapper" or "proxy".
+10 -3
View File
@@ -14,7 +14,8 @@ async fn allowing_once_releases_the_held_download() {
let held = tokio::spawn(async move { block("npm", &pkg, "age", "is new").await });
let id = loop {
let blocks: serde_json::Value = serde_json::from_str(&answer(&firewall, config, "blocks")).unwrap();
let blocks: serde_json::Value =
serde_json::from_str(&answer(&firewall, config, "blocks")).unwrap();
if let Some(id) = blocks[0]["id"].as_u64() {
break id;
}
@@ -22,6 +23,12 @@ async fn allowing_once_releases_the_held_download() {
};
assert_eq!(answer(&firewall, config, &format!("allow-once {id}")), "ok");
assert!(held.await.unwrap(), "the waiting download goes through");
assert!(firewall.age_check_lifted("npm", |p| p.name == "left-pad"), "and the rest of the install");
assert!(!block("npm", &Package { name: "evil".into(), version: None }, "denylist", "is malware").await);
assert!(
firewall.age_check_lifted("npm", |p| p.name == "left-pad"),
"and the rest of the install"
);
assert!(
!block("npm", &Package { name: "evil".into(), version: None }, "denylist", "is malware")
.await
);
}
+17 -6
View File
@@ -49,8 +49,13 @@ fn a_later_file_wins_and_allow_lists_add_up() {
std::fs::create_dir_all(&dir).expect("temp dir");
let user = dir.join("user.json");
let system = dir.join("system.json");
std::fs::write(&user, r#"{"min_age_hours": 1, "fail_open": false, "allow": {"npm": [{"name": "a"}]}}"#).expect("write");
std::fs::write(&system, r#"{"min_age_hours": 24, "allow": {"npm": [{"name": "b"}]}}"#).expect("write");
std::fs::write(
&user,
r#"{"min_age_hours": 1, "fail_open": false, "allow": {"npm": [{"name": "a"}]}}"#,
)
.expect("write");
std::fs::write(&system, r#"{"min_age_hours": 24, "allow": {"npm": [{"name": "b"}]}}"#)
.expect("write");
let missing = dir.join("missing.json");
let config = FirewallConfig::load_from(&[user, system, missing]).expect("loads");
std::fs::remove_dir_all(&dir).ok();
@@ -67,9 +72,13 @@ async fn a_mirror_is_screened_like_the_registry_it_copies() {
assert!(firewall.hosts().contains(&"npm.example.com"));
let url = "https://npm.example.com/left-pad/-/left-pad-1.3.0.tgz";
let (registry, rest) = firewall.match_on(url, |r| r.upstream).expect("the mirror");
assert_eq!(firewall.resolve(registry, rest).pkg.map(|p| p.label()), Some("[email protected]".into()));
assert_eq!(
firewall.resolve(registry, rest).pkg.map(|p| p.label()),
Some("[email protected]".into())
);
let unknown = std::env::temp_dir().join(format!("firewall-mirror-{}.json", std::process::id()));
std::fs::write(&unknown, r#"{"registries": {"nmp": ["https://npm.example.com/"]}}"#).expect("write");
std::fs::write(&unknown, r#"{"registries": {"nmp": ["https://npm.example.com/"]}}"#)
.expect("write");
let loaded = FirewallConfig::load_from(std::slice::from_ref(&unknown));
std::fs::remove_file(&unknown).ok();
assert!(loaded.is_err());
@@ -80,7 +89,8 @@ async fn an_allowed_version_is_still_checked_for_malware() {
// The unreachable denylist shows that it still runs.
let config = r#"{"allow": {"go": [{"name": "golang.org/x/net", "version": "v0.20.0"}]}, "denylist": {"base": "http://127.0.0.1:1"}}"#;
let firewall = Firewall::new(serde_json::from_str(config).expect("parses"));
let (registry, _) = firewall.match_on("/go/golang.org/x/net/@v/list", |r| r.prefix).expect("go");
let (registry, _) =
firewall.match_on("/go/golang.org/x/net/@v/list", |r| r.prefix).expect("go");
let pkg = Package { name: "golang.org/x/net".into(), version: Some("0.20.0".into()) };
let verdict = consult(&firewall, registry, &pkg).await;
assert_eq!(verdict.map(|(filter, _)| filter), Some("denylist"));
@@ -99,7 +109,8 @@ async fn failing_closed_blocks_what_could_not_be_checked() {
async fn lifting_the_age_check_still_checks_for_malware() {
let config = r#"{"denylist": {"base": "http://127.0.0.1:1"}}"#;
let firewall = Firewall::new(serde_json::from_str(config).expect("parses"));
let (registry, _) = firewall.match_on("/go/golang.org/x/net/@v/list", |r| r.prefix).expect("go");
let (registry, _) =
firewall.match_on("/go/golang.org/x/net/@v/list", |r| r.prefix).expect("go");
let pkg = Package { name: "golang.org/x/net".into(), version: Some("0.20.0".into()) };
firewall.lift_age_check("go", AllowedPackage { name: pkg.name.clone(), version: None }, None);
let (filter, _) = consult(&firewall, registry, &pkg).await.expect("the denylist still runs");
+2 -1
View File
@@ -49,7 +49,8 @@ fn a_plain_http_upstream_is_not_upgraded() {
let port = aliasing::spawn(0, FirewallConfig::default()).expect("spawn");
let mut proxy = TcpStream::connect(("127.0.0.1", port)).expect("connect");
let request = format!("GET http://{addr}/x HTTP/1.1\r\nHost: {addr}\r\nConnection: close\r\n\r\n");
let request =
format!("GET http://{addr}/x HTTP/1.1\r\nHost: {addr}\r\nConnection: close\r\n\r\n");
proxy.write_all(request.as_bytes()).expect("write");
let mut response = String::new();
proxy.read_to_string(&mut response).expect("read");
+2 -1
View File
@@ -36,7 +36,8 @@ pub fn all(port: u16) -> Vec<(&'static str, Outcome)> {
fn run(probe: &Probe, port: u16) -> Outcome {
let args = probe.args.iter().map(|arg| arg.to_string()).collect();
let dir = probe.setup.and_then(|prepare| prepare());
match aliasing::run(port, &Invocation::new(probe.manager.command.into(), args), dir.as_deref()) {
match aliasing::run(port, &Invocation::new(probe.manager.command.into(), args), dir.as_deref())
{
Ok(0) => Outcome::Ok,
Ok(_) => Outcome::Failed,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Outcome::Absent,