mirror of
https://github.com/usestrix/firewall.git
synced 2026-11-09 05:19:35 -10:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9b8176b7e | ||
|
|
e0b723a732 | ||
|
|
fc02226cd6 | ||
|
|
5068b3578d | ||
|
|
8d781f3f98 | ||
|
|
e521cbdefa | ||
|
|
674f3bdcd3 | ||
|
|
2df02bf23f |
No files matched your search
+8
-1
@@ -38,14 +38,17 @@ path = "src/main.rs"
|
||||
[[bin]]
|
||||
name = "firewall-service"
|
||||
path = "src/bin/firewall-service.rs"
|
||||
required-features = ["machine-wide"]
|
||||
|
||||
[[bin]]
|
||||
name = "firewall-tray"
|
||||
path = "src/bin/firewall-tray.rs"
|
||||
required-features = ["machine-wide"]
|
||||
|
||||
[[bin]]
|
||||
name = "firewall-uninstall"
|
||||
path = "src/bin/firewall-uninstall.rs"
|
||||
required-features = ["machine-wide"]
|
||||
|
||||
[[example]]
|
||||
name = "proxy-test"
|
||||
@@ -87,6 +90,10 @@ path = "tests/tpm.rs"
|
||||
name = "managers"
|
||||
path = "tests/managers.rs"
|
||||
|
||||
[features]
|
||||
# Builds the machine-wide binaries: the Windows service and tray, and the macOS uninstaller.
|
||||
machine-wide = []
|
||||
|
||||
[dependencies]
|
||||
clap = "4.6.6"
|
||||
directories = "6.0.0"
|
||||
@@ -118,7 +125,7 @@ schannel = "0.1.29"
|
||||
tokio = { version = "1.53.1", features = ["net"] }
|
||||
tracing-appender = "0.2.5"
|
||||
tray-icon = { version = "0.26.1", default-features = false }
|
||||
windows = { version = "0.62", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Security_Cryptography", "Win32_System_Com", "Win32_System_Environment", "Win32_System_Threading", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
|
||||
windows = { version = "0.62", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Security_Cryptography", "Win32_System_Com", "Win32_System_Console", "Win32_System_Environment", "Win32_System_JobObjects", "Win32_System_Threading", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
|
||||
windows-registry = "0.6.1"
|
||||
windows-service = "0.8.0"
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
pub mod forward;
|
||||
pub mod managers;
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
|
||||
pub use forward::spawn;
|
||||
pub use managers::*;
|
||||
@@ -34,7 +36,8 @@ impl Invocation {
|
||||
|
||||
pub fn run(port: u16, invocation: &Invocation, dir: Option<&Path>) -> std::io::Result<i32> {
|
||||
let proxy = crate::local_url(port, "");
|
||||
let mut command = Command::new(resolve(&invocation.program)?);
|
||||
let program = resolve(&invocation.program)?;
|
||||
let mut command = Command::new(&program);
|
||||
command.args(invocation.arguments(&proxy));
|
||||
if let Some(dir) = dir {
|
||||
command.current_dir(dir);
|
||||
@@ -42,7 +45,12 @@ pub fn run(port: u16, invocation: &Invocation, dir: Option<&Path>) -> std::io::R
|
||||
for (key, value) in managers::variables(&proxy) {
|
||||
command.env(key, value);
|
||||
}
|
||||
Ok(command.status()?.code().unwrap_or(1))
|
||||
#[cfg(windows)]
|
||||
windows::prepare();
|
||||
let status = command.status();
|
||||
#[cfg(windows)]
|
||||
let status = status.map_err(|error| windows::explain(error, &program));
|
||||
Ok(status?.code().unwrap_or(1))
|
||||
}
|
||||
|
||||
// Command::new only finds .exe on Windows; npm, pnpm and gem are .cmd shims found via PATHEXT.
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
use std::io::{Error, ErrorKind};
|
||||
use std::path::Path;
|
||||
use std::sync::Once;
|
||||
use tracing::debug;
|
||||
use windows::core::{BOOL, PCWSTR};
|
||||
use windows::Win32::System::Console::{SetConsoleCtrlHandler, CTRL_BREAK_EVENT, CTRL_C_EVENT};
|
||||
use windows::Win32::System::JobObjects::{
|
||||
AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation,
|
||||
SetInformationJobObject, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
|
||||
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
||||
};
|
||||
use windows::Win32::System::Threading::GetCurrentProcess;
|
||||
|
||||
/// Called before spawning, so the manager behaves as it would run from the shell directly.
|
||||
/// Once per process: a caller may run several managers.
|
||||
pub fn prepare() {
|
||||
static PREPARED: Once = Once::new();
|
||||
PREPARED.call_once(|| {
|
||||
// The console sends Ctrl+C to the manager too. Exiting first would hand the prompt back
|
||||
// while a .cmd shim still asks "Terminate batch job (Y/N)?", so wait for its exit instead.
|
||||
// A handler, unlike ignoring the signal outright, is not inherited by the manager.
|
||||
let _ = unsafe { SetConsoleCtrlHandler(Some(ignore_interrupt), true) };
|
||||
if let Err(error) = kill_children_on_exit() {
|
||||
debug!(%error, "the manager may outlive the firewall");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
unsafe extern "system" fn ignore_interrupt(event: u32) -> BOOL {
|
||||
(event == CTRL_C_EVENT || event == CTRL_BREAK_EVENT).into()
|
||||
}
|
||||
|
||||
/// Joins a job that kills what is left in it once the firewall exits, even if killed itself.
|
||||
/// The manager inherits the job when spawned, as do its own children.
|
||||
fn kill_children_on_exit() -> windows::core::Result<()> {
|
||||
let mut limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
|
||||
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
// Never closed: the handle closing as the firewall exits is what kills the rest.
|
||||
let job = unsafe { CreateJobObjectW(None, PCWSTR::null())? };
|
||||
unsafe {
|
||||
SetInformationJobObject(
|
||||
job,
|
||||
JobObjectExtendedLimitInformation,
|
||||
(&raw const limits).cast(),
|
||||
size_of_val(&limits) as u32,
|
||||
)?
|
||||
};
|
||||
unsafe { AssignProcessToJobObject(job, GetCurrentProcess()) }
|
||||
}
|
||||
|
||||
/// Rust refuses to pass a line break to a batch file, where it would cut the command short.
|
||||
pub fn explain(error: Error, program: &Path) -> Error {
|
||||
let batch = program
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("cmd") || ext.eq_ignore_ascii_case("bat"));
|
||||
if !batch || error.kind() != ErrorKind::InvalidInput {
|
||||
return error;
|
||||
}
|
||||
let name = program.file_name().unwrap_or(program.as_os_str()).to_string_lossy();
|
||||
Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("{name} is a batch file, which cannot be passed an argument with a line break"),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use crate::policies::Package;
|
||||
use serde::Serialize;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Bounded, so blocks pile up harmlessly while nothing collects them.
|
||||
const KEPT: usize = 50;
|
||||
|
||||
static RECENT: Mutex<Vec<BlockedPackage>> = Mutex::new(Vec::new());
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct BlockedPackage {
|
||||
pub eco: &'static str,
|
||||
pub name: String,
|
||||
pub version: Option<String>,
|
||||
pub filter: &'static str,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
pub fn record(eco: &'static str, pkg: &Package, filter: &'static str, reason: &str) {
|
||||
let mut recent = RECENT.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if recent.len() == KEPT {
|
||||
recent.remove(0);
|
||||
}
|
||||
recent.push(BlockedPackage {
|
||||
eco,
|
||||
name: pkg.name.clone(),
|
||||
version: pkg.version.clone(),
|
||||
filter,
|
||||
reason: reason.to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Hands each block over once.
|
||||
pub fn take() -> Vec<BlockedPackage> {
|
||||
std::mem::take(&mut *RECENT.lock().unwrap_or_else(|e| e.into_inner()))
|
||||
}
|
||||
@@ -17,7 +17,7 @@ pub struct FirewallConfig {
|
||||
/// 0 turns the age check off.
|
||||
pub min_age_hours: u64,
|
||||
pub denylist: DenylistSource,
|
||||
/// Packages never screened, by ecosystem: `{"npm": [{"name": "left-pad"}]}`.
|
||||
/// Packages exempt from the age check, by ecosystem: `{"npm": [{"name": "left-pad"}]}`.
|
||||
pub allow: HashMap<String, Vec<AllowedPackage>>,
|
||||
pub telemetry: bool,
|
||||
/// Not describable in the file yet, so this is always the built-in table.
|
||||
@@ -48,7 +48,7 @@ impl Default for FirewallConfig {
|
||||
FirewallConfig {
|
||||
port: DEFAULT_PORT,
|
||||
fail_open: true,
|
||||
min_age_hours: 0,
|
||||
min_age_hours: 24,
|
||||
denylist: DenylistSource::default(),
|
||||
allow: HashMap::new(),
|
||||
telemetry: true,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod aliasing;
|
||||
pub mod blocks;
|
||||
pub mod config;
|
||||
pub mod firewall;
|
||||
pub mod http;
|
||||
|
||||
@@ -5,7 +5,7 @@ mod uninstall;
|
||||
pub use uninstall::uninstall;
|
||||
|
||||
use crate::config::FirewallConfig;
|
||||
use crate::{mitm, telemetry};
|
||||
use crate::{blocks, mitm, telemetry};
|
||||
use crate::Firewall;
|
||||
use rama::bytes::Bytes;
|
||||
use rama::error::BoxError;
|
||||
@@ -58,6 +58,7 @@ impl TransparentProxyHandler for Handler {
|
||||
let message = String::from_utf8_lossy(&message).into_owned();
|
||||
Some(Bytes::from(match message.as_str() {
|
||||
"ca" => self.ca.pem.clone(),
|
||||
"blocks" => serde_json::to_string(&blocks::take()).expect("blocks serialize"),
|
||||
other => format!("error: unknown message {other}"),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||
use windows::Win32::Foundation::ERROR_GEN_FAILURE;
|
||||
use windows_service::service::*;
|
||||
use windows_service::service_control_handler::{
|
||||
self, ServiceControlHandlerResult, ServiceStatusHandle,
|
||||
@@ -73,8 +74,12 @@ fn run_until_stopped() -> Result<(), BoxError> {
|
||||
| ServiceControl::Shutdown
|
||||
| ServiceControl::Pause
|
||||
| ServiceControl::Continue => {
|
||||
let _ = control_sender.send(control);
|
||||
ServiceControlHandlerResult::NoError
|
||||
// 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)
|
||||
}
|
||||
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
|
||||
_ => ServiceControlHandlerResult::NotImplemented,
|
||||
@@ -89,16 +94,26 @@ fn run_until_stopped() -> Result<(), BoxError> {
|
||||
}
|
||||
};
|
||||
// Only pausing removes our hosts entries; a stop or crash keeps them, failing closed.
|
||||
while let Some(control) = control_receiver.recv().await {
|
||||
while let Some((control, reply)) = control_receiver.recv().await {
|
||||
let (hosts, state) = match control {
|
||||
ServiceControl::Pause => (&[][..], ServiceState::Paused),
|
||||
ServiceControl::Continue => (&hosts[..], ServiceState::Running),
|
||||
_ => break,
|
||||
};
|
||||
match write_hosts_entries(hosts) {
|
||||
Ok(()) => set_status(&status, state, false)?,
|
||||
Err(error) => tracing::error!(%error, "could not rewrite the hosts file"),
|
||||
}
|
||||
let result = match write_hosts_entries(hosts) {
|
||||
Ok(()) => {
|
||||
set_status(&status, state, false)?;
|
||||
ServiceControlHandlerResult::NoError
|
||||
}
|
||||
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);
|
||||
ServiceControlHandlerResult::Other(code)
|
||||
}
|
||||
};
|
||||
let _ = reply.send(result);
|
||||
}
|
||||
Ok::<_, BoxError>(())
|
||||
});
|
||||
@@ -147,7 +162,7 @@ async fn start_screening(status: &ServiceStatusHandle) -> Result<Vec<&'static st
|
||||
resolver: resolver.clone(),
|
||||
service: service.clone(),
|
||||
};
|
||||
let upstream = ConsumeErrLayer::trace_as_debug().into_layer(upstream);
|
||||
let upstream = ConsumeErrLayer::trace_as_warning().into_layer(upstream);
|
||||
tokio::spawn(listen_on(address, port, &exec)?.serve(upstream));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use super::*;
|
||||
use muda::{Menu, MenuEvent, MenuItem, PredefinedMenuItem};
|
||||
use std::io::Cursor;
|
||||
use std::time::Duration;
|
||||
use tray_icon::{Icon, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent};
|
||||
use windows::core::{w, PCWSTR};
|
||||
use windows::Win32::Foundation::*;
|
||||
@@ -14,6 +15,7 @@ use windows_service::service::{Service, ServiceAccess, ServiceState};
|
||||
use windows_service::service_manager::{ServiceManager, ServiceManagerAccess};
|
||||
|
||||
const MENU_PAUSE: &str = "pause";
|
||||
const MENU_RESTART: &str = "restart";
|
||||
const MENU_LOG: &str = "log";
|
||||
const MENU_QUIT: &str = "quit";
|
||||
const LOGO: &[u8] = include_bytes!("tray.png");
|
||||
@@ -31,6 +33,7 @@ pub fn run() -> Result<(), BoxError> {
|
||||
&status,
|
||||
&PredefinedMenuItem::separator(),
|
||||
&pause,
|
||||
&MenuItem::with_id(MENU_RESTART, "Restart", true, None),
|
||||
&MenuItem::with_id(MENU_LOG, "Open logs", true, None),
|
||||
&PredefinedMenuItem::separator(),
|
||||
&MenuItem::with_id(MENU_QUIT, "Quit", true, None),
|
||||
@@ -123,6 +126,11 @@ impl Tray {
|
||||
|| Ok(open_service()?.resume().map(drop)?)
|
||||
}
|
||||
MENU_PAUSE => || Ok(open_service()?.start::<&str>(&[])?),
|
||||
MENU_RESTART => {
|
||||
// Off the UI thread, since stopping takes a while.
|
||||
std::thread::spawn(|| restart().unwrap_or_else(show_error));
|
||||
return;
|
||||
}
|
||||
MENU_LOG => {
|
||||
let log = HSTRING::from(data_dir().as_path());
|
||||
let none = PCWSTR::null();
|
||||
@@ -131,13 +139,19 @@ impl Tray {
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
if let Err(error) = action() {
|
||||
let text = HSTRING::from(error.to_string());
|
||||
unsafe { MessageBoxW(None, &text, w!("Strix Firewall"), MB_ICONERROR) };
|
||||
}
|
||||
action().unwrap_or_else(show_error);
|
||||
}
|
||||
}
|
||||
|
||||
fn show_error(error: BoxError) {
|
||||
let text = match error.source() {
|
||||
Some(source) => format!("{error}: {source}"),
|
||||
None => error.to_string(),
|
||||
};
|
||||
let text = HSTRING::from(text);
|
||||
unsafe { MessageBoxW(None, &text, w!("Strix Firewall"), MB_ICONERROR) };
|
||||
}
|
||||
|
||||
/// `None` when the service is not installed.
|
||||
fn service_state() -> Option<ServiceState> {
|
||||
let manager =
|
||||
@@ -166,10 +180,28 @@ fn status_text(state: Option<ServiceState>) -> &'static str {
|
||||
|
||||
fn open_service() -> Result<Service, BoxError> {
|
||||
let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)?;
|
||||
let access = ServiceAccess::START | ServiceAccess::PAUSE_CONTINUE;
|
||||
let access = ServiceAccess::QUERY_STATUS
|
||||
| ServiceAccess::START
|
||||
| ServiceAccess::STOP
|
||||
| ServiceAccess::PAUSE_CONTINUE;
|
||||
Ok(manager.open_service(SERVICE_NAME, access)?)
|
||||
}
|
||||
|
||||
/// Starting again re-reads the config.
|
||||
fn restart() -> Result<(), BoxError> {
|
||||
let service = open_service()?;
|
||||
if service.query_status()?.current_state != ServiceState::Stopped {
|
||||
service.stop()?;
|
||||
}
|
||||
for _ in 0..60 {
|
||||
if service.query_status()?.current_state == ServiceState::Stopped {
|
||||
return Ok(service.start::<&str>(&[])?);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
Err("The service did not stop.".into())
|
||||
}
|
||||
|
||||
/// The alpha channel of the RGBA logo, and its width.
|
||||
fn logo_mask() -> Result<(Vec<u8>, u32), BoxError> {
|
||||
let mut reader = png::Decoder::new(Cursor::new(LOGO)).read_info()?;
|
||||
|
||||
@@ -25,16 +25,18 @@ pub trait Filter: Send + Sync {
|
||||
}
|
||||
|
||||
/// Asks every filter. A `Deny` wins; failing that, the first filter that could not tell.
|
||||
/// An allowed package skips only the age check, so malware is never let through.
|
||||
pub async fn consult(
|
||||
firewall: &Firewall,
|
||||
registry: &'static Registry,
|
||||
pkg: &Package,
|
||||
) -> Option<(&'static str, Verdict)> {
|
||||
if allowed(firewall, registry, pkg) {
|
||||
return None;
|
||||
}
|
||||
let allowed = allowed(firewall, registry, pkg);
|
||||
let mut unsure = None;
|
||||
for filter in firewall.filters() {
|
||||
if allowed && filter.name() == "age" {
|
||||
continue;
|
||||
}
|
||||
match filter.check(registry, pkg).await {
|
||||
Verdict::Allow => continue,
|
||||
Verdict::Deny(reason) => return Some((filter.name(), Verdict::Deny(reason))),
|
||||
|
||||
@@ -69,7 +69,8 @@ pub async fn screen(url: &str, firewall: &Firewall) -> Option<String> {
|
||||
};
|
||||
info!(eco = registry.eco, pkg = %pkg.label(), %url, "screening");
|
||||
match consult(firewall, registry, &pkg).await {
|
||||
Some((_, Verdict::Deny(reason))) => {
|
||||
Some((filter, Verdict::Deny(reason))) => {
|
||||
crate::blocks::record(registry.eco, &pkg, filter, &reason);
|
||||
Some(format!("{} {reason}", pkg.label()))
|
||||
}
|
||||
Some((filter, Verdict::Unknown(e))) if !firewall.config.fail_open => {
|
||||
|
||||
@@ -39,14 +39,14 @@ fn a_later_file_wins_and_allow_lists_add_up() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_allowed_version_skips_the_filters() {
|
||||
// An unreachable denylist would otherwise leave the package unchecked.
|
||||
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 pkg = |version: &str| Package { name: "golang.org/x/net".into(), version: Some(version.into()) };
|
||||
assert!(consult(&firewall, registry, &pkg("0.20.0")).await.is_none());
|
||||
assert!(consult(&firewall, registry, &pkg("0.21.0")).await.is_some());
|
||||
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"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -7,6 +7,7 @@ ci = "github"
|
||||
installers = ["shell", "powershell", "homebrew", "msi"]
|
||||
# core/wix/main.wxs is edited to install the service.
|
||||
allow-dirty = ["msi"]
|
||||
features = ["machine-wide"]
|
||||
tap = "usestrix/homebrew-tap"
|
||||
publish-jobs = ["homebrew", "./publish-crates"]
|
||||
targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"]
|
||||
@@ -0,0 +1,32 @@
|
||||
import AppKit
|
||||
|
||||
struct BlockedPackage: Codable, Hashable, Identifiable {
|
||||
let eco: String
|
||||
let name: String
|
||||
let version: String?
|
||||
let filter: String
|
||||
let reason: String
|
||||
|
||||
var id: String { "\(eco)/\(name)@\(version ?? "")" }
|
||||
var label: String { version.map { "\(name) \($0)" } ?? name }
|
||||
/// The denylist and OSV only flag packages known to be malicious; the age check only holds back new ones.
|
||||
var malicious: Bool { filter != "age" }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
enum BlockAlert {
|
||||
static func show(_ package: BlockedPackage) {
|
||||
let alert = NSAlert()
|
||||
alert.addButton(withTitle: "OK")
|
||||
if package.malicious {
|
||||
alert.alertStyle = .critical
|
||||
alert.messageText = "Blocked a malicious package"
|
||||
alert.informativeText = "\(package.label) (\(package.eco)) \(package.reason). Do not install it."
|
||||
} else {
|
||||
alert.messageText = "Held back a new package"
|
||||
alert.informativeText = "\(package.label) (\(package.eco)) \(package.reason)."
|
||||
}
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
alert.runModal()
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ final class FirewallModel: NSObject, ObservableObject {
|
||||
@Published private(set) var completed = false
|
||||
@Published private(set) var removed = false
|
||||
@Published private(set) var screening = false
|
||||
@Published private(set) var blocked: [BlockedPackage] = []
|
||||
|
||||
private let logger = Logger(subsystem: FirewallIDs.appBundleID, category: "installer")
|
||||
private var pendingRequest: (request: OSSystemExtensionRequest, continuation: CheckedContinuation<Void, Error>)?
|
||||
@@ -72,6 +73,7 @@ final class FirewallModel: NSObject, ObservableObject {
|
||||
await refresh()
|
||||
if CommandLine.arguments.contains("--install") { await install() }
|
||||
}
|
||||
Task { await watchBlocks() }
|
||||
}
|
||||
|
||||
// MARK: - Flows
|
||||
@@ -199,6 +201,21 @@ final class FirewallModel: NSObject, ObservableObject {
|
||||
if let url = URL(string: pane) { NSWorkspace.shared.open(url) }
|
||||
}
|
||||
|
||||
/// The extension cannot message the app, so the app asks it for blocks.
|
||||
private func watchBlocks() async {
|
||||
while true {
|
||||
if screening, let reply = try? await providerMessage("blocks"),
|
||||
let packages = try? JSONDecoder().decode([BlockedPackage].self, from: Data(reply.utf8)) {
|
||||
for package in packages where !blocked.contains(package) {
|
||||
blocked.append(package)
|
||||
BlockAlert.show(package)
|
||||
}
|
||||
blocked = Array(blocked.suffix(10))
|
||||
}
|
||||
try? await Task.sleep(nanoseconds: 2_000_000_000)
|
||||
}
|
||||
}
|
||||
|
||||
private func stage(_ step: ReferenceWritableKeyPath<FirewallModel, StepStatus>,
|
||||
running: String,
|
||||
work: @MainActor () async throws -> String) async -> Bool {
|
||||
|
||||
@@ -22,5 +22,20 @@ struct MenuBarView: View {
|
||||
Task { await model.restart() }
|
||||
}
|
||||
.disabled(model.busy)
|
||||
|
||||
if !model.blocked.isEmpty {
|
||||
Divider()
|
||||
ForEach(model.blocked) { package in
|
||||
if !package.malicious {
|
||||
Menu("Held back \(package.label) (\(package.eco))") {
|
||||
Text(package.reason)
|
||||
}
|
||||
} else {
|
||||
Menu("Blocked \(package.label) (\(package.eco))") {
|
||||
Text("Malicious: \(package.reason)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -133,7 +133,7 @@ targets:
|
||||
arm64|arm64e) triple=aarch64-apple-darwin ;;
|
||||
x86_64) triple=x86_64-apple-darwin ;;
|
||||
esac
|
||||
cargo build --bin firewall-uninstall --target $triple $([ "$CONFIGURATION" = Debug ] || echo --release)
|
||||
cargo build --bin firewall-uninstall --features machine-wide --target $triple $([ "$CONFIGURATION" = Debug ] || echo --release)
|
||||
bins="$bins target/$triple/$CARGO_PROFILE/firewall-uninstall"
|
||||
done
|
||||
lipo -create $bins -output "$TARGET_BUILD_DIR/$EXECUTABLE_FOLDER_PATH/firewall-uninstall"
|
||||
Reference in new issue
Block a user