mirror of
https://github.com/usestrix/firewall.git
synced 2026-11-09 13:31:32 -10:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
582c260d44 | ||
|
|
677884aa36 | ||
|
|
e6691b252c | ||
|
|
a2f631866e | ||
|
|
b42185ee6f | ||
|
|
97a5a148e0 |
No files matched your search
Generated
+1
-1
@@ -3370,7 +3370,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "strix-firewall"
|
||||
version = "0.1.0"
|
||||
version = "0.1.0-rc.1"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"core-foundation 0.10.1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "strix-firewall"
|
||||
version = "0.1.0"
|
||||
version = "0.1.0-rc.1"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
authors = ["Strix"]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
use_small_heuristics = "Max"
|
||||
@@ -11,4 +11,5 @@ pub static BUN: Manager = Manager {
|
||||
("npm_config_registry", "http://registry.npmjs.org/"),
|
||||
],
|
||||
args: &[],
|
||||
lockfile: None,
|
||||
};
|
||||
@@ -17,4 +17,5 @@ pub static CARGO: Manager = Manager {
|
||||
"source.firewall.registry=\"sparse+http://index.crates.io/\"",
|
||||
],
|
||||
}],
|
||||
lockfile: None,
|
||||
};
|
||||
@@ -8,4 +8,5 @@ pub static GEM: Manager = Manager {
|
||||
when: &["install", "update", "fetch"],
|
||||
flags: &["--clear-sources", "--source", "http://rubygems.org/"],
|
||||
}],
|
||||
lockfile: None,
|
||||
};
|
||||
@@ -11,4 +11,5 @@ pub static GO: Manager = Manager {
|
||||
("GOFLAGS", "-mod=mod"),
|
||||
],
|
||||
args: &[],
|
||||
lockfile: None,
|
||||
};
|
||||
@@ -36,6 +36,14 @@ pub struct Manager {
|
||||
pub command: &'static str,
|
||||
pub env: &'static [(&'static str, &'static str)],
|
||||
pub args: &'static [Args],
|
||||
/// Where the manager records registry URLs, which then carry our http:// override.
|
||||
pub lockfile: Option<Lockfile>,
|
||||
}
|
||||
|
||||
/// What to do after a run about http:// registry URLs left in the lockfile at this path.
|
||||
pub enum Lockfile {
|
||||
Restore(&'static str),
|
||||
Warn(&'static str),
|
||||
}
|
||||
|
||||
pub static MANAGERS: &[&Manager] =
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{Manager, PROXY};
|
||||
use super::{Lockfile, Manager, PROXY};
|
||||
|
||||
pub static NPM: Manager = Manager {
|
||||
command: "npm",
|
||||
@@ -8,4 +8,5 @@ pub static NPM: Manager = Manager {
|
||||
("npm_config_registry", "http://registry.npmjs.org/"),
|
||||
],
|
||||
args: &[],
|
||||
lockfile: Some(Lockfile::Restore("package-lock.json")),
|
||||
};
|
||||
@@ -13,4 +13,5 @@ pub static PDM: Manager = Manager {
|
||||
("PDM_PYPI_VERIFY_SSL", "false"),
|
||||
],
|
||||
args: &[],
|
||||
lockfile: None,
|
||||
};
|
||||
@@ -12,5 +12,5 @@ pub static PIP: Manager = recipe("pip");
|
||||
pub static PIP3: Manager = recipe("pip3");
|
||||
|
||||
const fn recipe(name: &'static str) -> Manager {
|
||||
Manager { command: name, env: PIP_ENV, args: &[] }
|
||||
Manager { command: name, env: PIP_ENV, args: &[], lockfile: None }
|
||||
}
|
||||
@@ -13,4 +13,5 @@ pub static PIPENV: Manager = Manager {
|
||||
("PIP_TRUSTED_HOST", "pypi.org files.pythonhosted.org"),
|
||||
],
|
||||
args: &[],
|
||||
lockfile: None,
|
||||
};
|
||||
@@ -9,4 +9,5 @@ pub static PNPM: Manager = Manager {
|
||||
("pnpm_config_registry", "http://registry.npmjs.org/"),
|
||||
],
|
||||
args: &[],
|
||||
lockfile: None,
|
||||
};
|
||||
@@ -1,7 +1,8 @@
|
||||
use super::{Manager, PROXY, PYPI_SIMPLE};
|
||||
use super::{Lockfile, Manager, PROXY, PYPI_SIMPLE};
|
||||
|
||||
pub static UV: Manager = Manager {
|
||||
command: "uv",
|
||||
env: &[("HTTP_PROXY", PROXY), ("http_proxy", PROXY), ("UV_DEFAULT_INDEX", PYPI_SIMPLE)],
|
||||
args: &[],
|
||||
lockfile: Some(Lockfile::Warn("uv.lock")),
|
||||
};
|
||||
@@ -6,9 +6,11 @@ mod windows;
|
||||
pub use forward::spawn;
|
||||
pub use managers::*;
|
||||
|
||||
use crate::policies::registries::BUILT_IN;
|
||||
use std::io::{Error, ErrorKind};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use tracing::{info, warn};
|
||||
|
||||
pub struct Invocation {
|
||||
pub program: String,
|
||||
@@ -50,10 +52,42 @@ pub fn run(port: u16, invocation: &Invocation, dir: Option<&Path>) -> std::io::R
|
||||
let status = command.status();
|
||||
#[cfg(windows)]
|
||||
let status = status.map_err(|error| windows::explain(error, &program));
|
||||
Ok(status?.code().unwrap_or(1))
|
||||
let code = status?.code().unwrap_or(1);
|
||||
let manager = MANAGERS.iter().find(|manager| manager.command == invocation.program);
|
||||
match manager.and_then(|manager| manager.lockfile.as_ref()) {
|
||||
Some(Lockfile::Restore(lockfile)) => restore_https(lockfile, dir),
|
||||
Some(Lockfile::Warn(lockfile)) => warn_if_plaintext(lockfile, dir),
|
||||
None => {}
|
||||
}
|
||||
Ok(code)
|
||||
}
|
||||
|
||||
// Command::new only finds .exe on Windows; npm, pnpm and gem are .cmd shims found via PATHEXT.
|
||||
fn resolve(program: &str) -> std::io::Result<PathBuf> {
|
||||
which::which(program).map_err(|e| Error::new(ErrorKind::NotFound, e))
|
||||
}
|
||||
|
||||
fn warn_if_plaintext(lockfile: &str, dir: Option<&Path>) {
|
||||
let path = dir.unwrap_or(Path::new(".")).join(lockfile);
|
||||
let Ok(text) = std::fs::read_to_string(path) else { return };
|
||||
if BUILT_IN.iter().any(|registry| text.contains(&format!("http://{}/", registry.host()))) {
|
||||
warn!("{lockfile} has http:// registry URLs; change them to https:// before committing");
|
||||
}
|
||||
}
|
||||
|
||||
fn restore_https(lockfile: &str, dir: Option<&Path>) {
|
||||
let path = dir.unwrap_or(Path::new(".")).join(lockfile);
|
||||
let Ok(text) = std::fs::read_to_string(&path) else { return };
|
||||
let mut fixed = text.clone();
|
||||
for registry in BUILT_IN {
|
||||
let host = registry.host();
|
||||
fixed = fixed.replace(&format!("http://{host}/"), &format!("https://{host}/"));
|
||||
}
|
||||
if fixed == text {
|
||||
return;
|
||||
}
|
||||
match std::fs::write(&path, fixed) {
|
||||
Ok(()) => info!("changed {lockfile}'s registry URLs back to https://"),
|
||||
Err(e) => warn!("could not change {lockfile}'s registry URLs back to https://: {e}"),
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -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());
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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};
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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())? {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(", ")))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
@@ -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
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ build() {
|
||||
xcodegen generate >/dev/null
|
||||
xcodebuild -project Firewall.xcodeproj -scheme StrixFirewall -configuration $CONFIGURATION \
|
||||
-destination generic/platform=macOS \
|
||||
-derivedDataPath build CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO \
|
||||
-derivedDataPath build CURRENT_PROJECT_VERSION="$(date +%s)" CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGN_IDENTITY="" CODE_SIGN_ENTITLEMENTS="" build >/dev/null
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ settings:
|
||||
ENABLE_DEBUG_DYLIB: "NO"
|
||||
CODE_SIGN_STYLE: Automatic
|
||||
CODE_SIGN_IDENTITY: Apple Development
|
||||
# build.sh sets a fresh one per build; macOS only replaces an installed extension whose version differs.
|
||||
CURRENT_PROJECT_VERSION: "1"
|
||||
|
||||
packages:
|
||||
Rama:
|
||||
@@ -42,6 +44,7 @@ targets:
|
||||
properties:
|
||||
CFBundlePackageType: SYSX
|
||||
CFBundleShortVersionString: "0.1"
|
||||
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
|
||||
NSSystemExtensionUsageDescription: Firewall uses a network extension to screen package downloads before they install.
|
||||
NetworkExtension:
|
||||
NEMachServiceName: ${TEAM}.com.usestrix.firewall.extension
|
||||
@@ -102,6 +105,7 @@ targets:
|
||||
path: App/Info.plist
|
||||
properties:
|
||||
CFBundleShortVersionString: "0.1"
|
||||
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
|
||||
LSMinimumSystemVersion: $(MACOSX_DEPLOYMENT_TARGET)
|
||||
NSSystemExtensionUsageDescription: Firewall uses a network extension to screen package downloads before they install.
|
||||
LSUIElement: true
|
||||
|
||||
Reference in new issue
Block a user