mirror of
https://github.com/usestrix/firewall.git
synced 2026-11-09 05:19:35 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10fdd0f272 |
No files matched your search
Generated
+1
-168
@@ -261,15 +261,6 @@ dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borsh"
|
||||
version = "1.8.1"
|
||||
@@ -678,16 +669,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.7"
|
||||
@@ -699,12 +680,6 @@ dependencies = [
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dpi"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.18.0"
|
||||
@@ -776,15 +751,6 @@ dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.13"
|
||||
@@ -805,7 +771,7 @@ checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"libz-sys",
|
||||
"miniz_oxide 0.9.1",
|
||||
"miniz_oxide",
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
@@ -1486,16 +1452,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyboard-types"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "konst"
|
||||
version = "0.2.20"
|
||||
@@ -1639,16 +1595,6 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.9.1"
|
||||
@@ -1690,24 +1636,6 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "muda"
|
||||
version = "0.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4340e89c178916e62879c5c36b7f699019369be413e496178a6983ce91b8bc03"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"dpi",
|
||||
"keyboard-types",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"png",
|
||||
"thiserror",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multer"
|
||||
version = "3.1.0"
|
||||
@@ -1814,66 +1742,6 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8"
|
||||
dependencies = [
|
||||
"objc2-encode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-app-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-graphics"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-encode"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||
|
||||
[[package]]
|
||||
name = "objc2-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"block2",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oid-registry"
|
||||
version = "0.8.1"
|
||||
@@ -1996,19 +1864,6 @@ version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.15.0"
|
||||
@@ -3375,9 +3230,7 @@ dependencies = [
|
||||
"clap",
|
||||
"core-foundation 0.10.1",
|
||||
"directories",
|
||||
"muda",
|
||||
"percent-encoding",
|
||||
"png",
|
||||
"rama",
|
||||
"rand",
|
||||
"rcgen",
|
||||
@@ -3393,7 +3246,6 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"tray-icon",
|
||||
"which",
|
||||
"windows",
|
||||
"windows-registry",
|
||||
@@ -3800,25 +3652,6 @@ dependencies = [
|
||||
"tracing-serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tray-icon"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f5cc5858ecf9574ddd8f0032a2ff7ffd6f7724d2b81e48b9374984b3fd6ddc9"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"muda",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-foundation",
|
||||
"once_cell",
|
||||
"png",
|
||||
"thiserror",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
|
||||
+11
-17
@@ -11,9 +11,9 @@ homepage = "https://github.com/usestrix/firewall"
|
||||
# as an integration test; new tests/*.rs need an explicit [[test]] entry.
|
||||
autotests = false
|
||||
|
||||
# Only Windows ships the service and tray.
|
||||
# Only Windows ships the service.
|
||||
[package.metadata.dist.binaries]
|
||||
x86_64-pc-windows-msvc = ["firewall", "firewall-service", "firewall-tray"]
|
||||
x86_64-pc-windows-msvc = ["firewall", "firewall-service"]
|
||||
aarch64-apple-darwin = ["firewall"]
|
||||
x86_64-apple-darwin = ["firewall"]
|
||||
aarch64-unknown-linux-gnu = ["firewall"]
|
||||
@@ -38,17 +38,10 @@ 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"
|
||||
@@ -90,10 +83,6 @@ 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,14 +107,19 @@ security-framework = "3.7.0"
|
||||
security-framework-sys = "2.17.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
muda = { version = "0.21.2", default-features = false }
|
||||
png = "0.18.1"
|
||||
rama = { version = "0.4.0", features = ["dns-hickory"] }
|
||||
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_Environment",
|
||||
"Win32_UI_Shell",
|
||||
] }
|
||||
windows-registry = "0.6.1"
|
||||
windows-service = "0.8.0"
|
||||
|
||||
|
||||
@@ -89,16 +89,12 @@ async fn handle(req: Request, firewall: &Firewall) -> Result<Response, Infallibl
|
||||
}
|
||||
|
||||
info!(method = %req.method(), %url, "forwarding");
|
||||
// Only an HTTPS registry was downgraded to reach us, so only it is upgraded back and rewritten.
|
||||
let tls = firewall.is_https_url(&url);
|
||||
let index = tls && firewall.is_index_url(&url);
|
||||
let index = firewall.is_index_url(&url);
|
||||
let (mut parts, body) = req.into_parts();
|
||||
if tls {
|
||||
let Ok(uri) = url.parse() else {
|
||||
return Ok(text(400, "firewall: expected an absolute URL\n"));
|
||||
};
|
||||
parts.uri = uri;
|
||||
}
|
||||
let Ok(uri) = url.parse() else {
|
||||
return Ok(text(400, "firewall: expected an absolute URL\n"));
|
||||
};
|
||||
parts.uri = uri;
|
||||
|
||||
let client = DecompressionLayer::new().into_layer(EasyHttpWebClient::default());
|
||||
let response = match client.serve(Request::from_parts(parts, body)).await {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
//! The notification-area icon, started at sign-in. A GUI app, so no console opens with it.
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
#[cfg(windows)]
|
||||
fn main() -> Result<(), rama::error::BoxError> {
|
||||
firewall::os::windows::run_tray()
|
||||
}
|
||||
|
||||
/// The tray is Windows-only.
|
||||
#[cfg(not(windows))]
|
||||
fn main() {}
|
||||
@@ -1,36 +0,0 @@
|
||||
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()))
|
||||
}
|
||||
@@ -48,7 +48,7 @@ impl Default for FirewallConfig {
|
||||
FirewallConfig {
|
||||
port: DEFAULT_PORT,
|
||||
fail_open: true,
|
||||
min_age_hours: 24,
|
||||
min_age_hours: 0,
|
||||
denylist: DenylistSource::default(),
|
||||
allow: HashMap::new(),
|
||||
telemetry: true,
|
||||
|
||||
@@ -47,11 +47,6 @@ impl Firewall {
|
||||
self.config.hosts()
|
||||
}
|
||||
|
||||
pub fn is_https_url(&self, url: &str) -> bool {
|
||||
let host = host_of(url);
|
||||
self.config.registries.iter().any(|r| r.upstream.starts_with("https://") && r.host() == host)
|
||||
}
|
||||
|
||||
pub fn is_index_url(&self, url: &str) -> bool {
|
||||
self.config.registries.iter().any(|r| r.index && r.host() == host_of(url))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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::{blocks, mitm, telemetry};
|
||||
use crate::{mitm, telemetry};
|
||||
use crate::Firewall;
|
||||
use rama::bytes::Bytes;
|
||||
use rama::error::BoxError;
|
||||
@@ -58,7 +58,6 @@ 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}"),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -4,12 +4,10 @@
|
||||
mod ca;
|
||||
mod service;
|
||||
mod tpm;
|
||||
mod tray;
|
||||
mod uninstall;
|
||||
|
||||
pub use service::run as run_service;
|
||||
pub use tpm::CaKey;
|
||||
pub use tray::run as run_tray;
|
||||
pub use uninstall::uninstall;
|
||||
|
||||
use rama::error::BoxError;
|
||||
|
||||
@@ -26,7 +26,6 @@ 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,
|
||||
@@ -74,12 +73,8 @@ fn run_until_stopped() -> Result<(), BoxError> {
|
||||
| ServiceControl::Shutdown
|
||||
| ServiceControl::Pause
|
||||
| ServiceControl::Continue => {
|
||||
// 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)
|
||||
let _ = control_sender.send(control);
|
||||
ServiceControlHandlerResult::NoError
|
||||
}
|
||||
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
|
||||
_ => ServiceControlHandlerResult::NotImplemented,
|
||||
@@ -94,26 +89,16 @@ fn run_until_stopped() -> Result<(), BoxError> {
|
||||
}
|
||||
};
|
||||
// Only pausing removes our hosts entries; a stop or crash keeps them, failing closed.
|
||||
while let Some((control, reply)) = control_receiver.recv().await {
|
||||
while let Some(control) = control_receiver.recv().await {
|
||||
let (hosts, state) = match control {
|
||||
ServiceControl::Pause => (&[][..], ServiceState::Paused),
|
||||
ServiceControl::Continue => (&hosts[..], ServiceState::Running),
|
||||
_ => break,
|
||||
};
|
||||
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);
|
||||
match write_hosts_entries(hosts) {
|
||||
Ok(()) => set_status(&status, state, false)?,
|
||||
Err(error) => tracing::error!(%error, "could not rewrite the hosts file"),
|
||||
}
|
||||
}
|
||||
Ok::<_, BoxError>(())
|
||||
});
|
||||
@@ -162,7 +147,7 @@ async fn start_screening(status: &ServiceStatusHandle) -> Result<Vec<&'static st
|
||||
resolver: resolver.clone(),
|
||||
service: service.clone(),
|
||||
};
|
||||
let upstream = ConsumeErrLayer::trace_as_warning().into_layer(upstream);
|
||||
let upstream = ConsumeErrLayer::trace_as_debug().into_layer(upstream);
|
||||
tokio::spawn(listen_on(address, port, &exec)?.serve(upstream));
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,227 +0,0 @@
|
||||
//! The notification-area icon, started at sign-in as the menu bar app is on macOS.
|
||||
|
||||
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::*;
|
||||
use windows::Win32::System::Threading::CreateMutexW;
|
||||
use windows::Win32::UI::Shell::*;
|
||||
use windows::Win32::UI::WindowsAndMessaging::*;
|
||||
use windows_registry::CURRENT_USER;
|
||||
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");
|
||||
|
||||
/// Runs until Quit, once per session.
|
||||
pub fn run() -> Result<(), BoxError> {
|
||||
// Held until we exit.
|
||||
let _mutex = unsafe { CreateMutexW(None, false, w!(r"Local\StrixFirewallTray"))? };
|
||||
if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
|
||||
return Ok(());
|
||||
}
|
||||
let status = MenuItem::new("", false, None);
|
||||
let pause = MenuItem::with_id(MENU_PAUSE, "", false, None);
|
||||
let menu = Menu::with_items(&[
|
||||
&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),
|
||||
])?;
|
||||
let (logo, logo_size) = logo_mask()?;
|
||||
let icon = TrayIconBuilder::new().with_menu(Box::new(menu)).build()?;
|
||||
let window = HWND(icon.window_handle());
|
||||
let mut tray = Tray {
|
||||
icon,
|
||||
logo,
|
||||
logo_size,
|
||||
drawn: None,
|
||||
status,
|
||||
pause,
|
||||
state: None,
|
||||
};
|
||||
tray.update();
|
||||
unsafe { SetTimer(None, 0, 2000, None) };
|
||||
let mut message = MSG::default();
|
||||
while unsafe { GetMessageW(&mut message, None, 0, 0) }.0 > 0 {
|
||||
unsafe { DispatchMessageW(&message) };
|
||||
// WM_CLOSE, as the MSI sends, destroys the icon's window.
|
||||
if !unsafe { IsWindow(Some(window)) }.as_bool() {
|
||||
break;
|
||||
}
|
||||
// Ours, not the icon window's own hover timer.
|
||||
if message.message == WM_TIMER && message.hwnd.is_invalid() {
|
||||
tray.update();
|
||||
}
|
||||
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
|
||||
{
|
||||
tray.update();
|
||||
}
|
||||
}
|
||||
while let Ok(event) = MenuEvent::receiver().try_recv() {
|
||||
if event.id == MENU_QUIT {
|
||||
return Ok(());
|
||||
}
|
||||
tray.on_menu(event.id.as_ref());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct Tray {
|
||||
icon: TrayIcon,
|
||||
/// The logo's alpha channel; it is square.
|
||||
logo: Vec<u8>,
|
||||
logo_size: u32,
|
||||
/// Whether the taskbar was light and the service screening when the icon was last set.
|
||||
drawn: Option<(bool, bool)>,
|
||||
status: MenuItem,
|
||||
pause: MenuItem,
|
||||
/// As the menu last showed it.
|
||||
state: Option<ServiceState>,
|
||||
}
|
||||
|
||||
impl Tray {
|
||||
fn update(&mut self) {
|
||||
self.state = service_state();
|
||||
let screening = self.state == Some(ServiceState::Running);
|
||||
let light = light_taskbar();
|
||||
if self.drawn != Some((light, screening)) {
|
||||
let rgba = logo_rgba(&self.logo, light, screening);
|
||||
if let Ok(icon) = Icon::from_rgba(rgba, self.logo_size, self.logo_size) {
|
||||
let _ = self.icon.set_icon(Some(icon));
|
||||
}
|
||||
self.drawn = Some((light, screening));
|
||||
}
|
||||
let text = status_text(self.state);
|
||||
let tip = format!("Strix Firewall: {text}");
|
||||
let _ = self.icon.set_tooltip(Some(tip));
|
||||
self.status.set_text(text);
|
||||
let pause = if screening { "Pause" } else { "Resume" };
|
||||
self.pause.set_text(pause);
|
||||
self.pause.set_enabled(self.state.is_some());
|
||||
}
|
||||
|
||||
fn on_menu(&self, id: &str) {
|
||||
let state = self.state;
|
||||
let screening = state == Some(ServiceState::Running);
|
||||
let action: fn() -> Result<(), BoxError> = match id {
|
||||
MENU_PAUSE if screening => || Ok(open_service()?.pause().map(drop)?),
|
||||
MENU_PAUSE if state == Some(ServiceState::Paused) => {
|
||||
|| 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();
|
||||
unsafe { ShellExecuteW(None, w!("open"), &log, none, none, SW_SHOWNORMAL) };
|
||||
return;
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
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 =
|
||||
ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT).ok()?;
|
||||
let service = manager
|
||||
.open_service(SERVICE_NAME, ServiceAccess::QUERY_STATUS)
|
||||
.ok()?;
|
||||
Some(service.query_status().ok()?.current_state)
|
||||
}
|
||||
|
||||
fn light_taskbar() -> bool {
|
||||
let personalize = r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
|
||||
let Ok(key) = CURRENT_USER.open(personalize) else {
|
||||
return false;
|
||||
};
|
||||
key.get_u32("SystemUsesLightTheme").is_ok_and(|light| light == 1)
|
||||
}
|
||||
|
||||
fn status_text(state: Option<ServiceState>) -> &'static str {
|
||||
match state {
|
||||
Some(ServiceState::Running) => "Screening downloads",
|
||||
Some(ServiceState::Paused) => "Paused",
|
||||
_ => "Not running",
|
||||
}
|
||||
}
|
||||
|
||||
fn open_service() -> Result<Service, BoxError> {
|
||||
let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)?;
|
||||
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()?;
|
||||
let mut rgba = vec![0; reader.output_buffer_size().ok_or("logo too large")?];
|
||||
let info = reader.next_frame(&mut rgba)?;
|
||||
let mut alpha = Vec::with_capacity(rgba.len() / 4);
|
||||
for pixel in rgba.chunks(4) {
|
||||
alpha.push(pixel[3]);
|
||||
}
|
||||
Ok((alpha, info.width))
|
||||
}
|
||||
|
||||
/// White on a dark taskbar and black on a light one, as macOS draws a template image; dimmed
|
||||
/// when not screening.
|
||||
fn logo_rgba(mask: &[u8], light: bool, screening: bool) -> Vec<u8> {
|
||||
let shade = if light { 0 } else { 255 };
|
||||
let mut rgba = Vec::with_capacity(mask.len() * 4);
|
||||
for &alpha in mask {
|
||||
let alpha = if screening { alpha } else { alpha / 2 };
|
||||
rgba.extend([shade, shade, shade, alpha]);
|
||||
}
|
||||
rgba
|
||||
}
|
||||
@@ -69,8 +69,7 @@ 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((filter, Verdict::Deny(reason))) => {
|
||||
crate::blocks::record(registry.eco, &pkg, filter, &reason);
|
||||
Some((_, Verdict::Deny(reason))) => {
|
||||
Some(format!("{} {reason}", pkg.label()))
|
||||
}
|
||||
Some((filter, Verdict::Unknown(e))) if !firewall.config.fail_open => {
|
||||
|
||||
+1
-22
@@ -1,9 +1,7 @@
|
||||
use firewall::aliasing::forward::downgrade;
|
||||
use firewall::http::https_url;
|
||||
use firewall::{aliasing, Firewall, FirewallConfig};
|
||||
use firewall::Firewall;
|
||||
use rama::http::{Body, Request};
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
|
||||
fn request(target: &str, host: Option<&str>) -> Request {
|
||||
let mut builder = Request::builder().method("GET").uri(target);
|
||||
@@ -37,25 +35,6 @@ fn requests_always_go_out_over_tls() {
|
||||
assert_eq!(https_url(&request("/six", None)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_plain_http_upstream_is_not_upgraded() {
|
||||
let upstream = TcpListener::bind("127.0.0.1:0").expect("bind");
|
||||
let addr = upstream.local_addr().expect("address");
|
||||
std::thread::spawn(move || {
|
||||
let (mut stream, _) = upstream.accept().expect("accept");
|
||||
let _ = stream.read(&mut [0; 4096]);
|
||||
let _ = stream.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok");
|
||||
});
|
||||
|
||||
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");
|
||||
proxy.write_all(request.as_bytes()).expect("write");
|
||||
let mut response = String::new();
|
||||
proxy.read_to_string(&mut response).expect("read");
|
||||
assert!(response.starts_with("HTTP/1.1 200") && response.ends_with("ok"), "{response}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn downgrade_touches_known_hosts_in_text_only() {
|
||||
let text = b"https://pypi.org/x https://example.com/y".to_vec();
|
||||
|
||||
+1
-27
@@ -21,7 +21,7 @@
|
||||
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
|
||||
<?endif ?>
|
||||
|
||||
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi' xmlns:util='http://schemas.microsoft.com/wix/UtilExtension'>
|
||||
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
|
||||
|
||||
<Product
|
||||
Id='*'
|
||||
@@ -95,20 +95,6 @@
|
||||
<Environment Id='NODE_EXTRA_CA_CERTS' Name='NODE_EXTRA_CA_CERTS' Value='[CommonAppDataFolder]Strix Firewall\ca.pem' Action='set' System='yes'/>
|
||||
<Environment Id='UV_SYSTEM_CERTS' Name='UV_SYSTEM_CERTS' Value='1' Action='set' System='yes'/>
|
||||
</Component>
|
||||
<Component Id='binary2' Guid='*'>
|
||||
<File
|
||||
Id='exe2'
|
||||
Name='firewall-tray.exe'
|
||||
DiskId='1'
|
||||
Source='$(var.CargoTargetBinDir)\firewall-tray.exe'
|
||||
KeyPath='yes'/>
|
||||
<RegistryValue
|
||||
Root='HKLM'
|
||||
Key='SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
|
||||
Name='Strix Firewall'
|
||||
Type='string'
|
||||
Value='"[#exe2]"'/>
|
||||
</Component>
|
||||
</Directory>
|
||||
</Directory>
|
||||
</Directory>
|
||||
@@ -124,7 +110,6 @@
|
||||
Absent='disallow'>
|
||||
<ComponentRef Id='binary0'/>
|
||||
<ComponentRef Id='binary1'/>
|
||||
<ComponentRef Id='binary2'/>
|
||||
|
||||
<Feature
|
||||
Id='Environment'
|
||||
@@ -162,21 +147,11 @@
|
||||
Execute='deferred'
|
||||
Impersonate='no'
|
||||
Return='check'/>
|
||||
<!-- From the exit dialog, which runs as the signed-in user rather than elevated. -->
|
||||
<CustomAction
|
||||
Id='StartTray'
|
||||
Directory='APPLICATIONFOLDER'
|
||||
ExeCommand='"[#exe2]"'
|
||||
Return='asyncNoWait'/>
|
||||
<!-- Before its file is replaced or removed. Trays WM_CLOSE does not reach, as in other sessions, are terminated. -->
|
||||
<util:CloseApplication Id='CloseTray' Target='firewall-tray.exe' CloseMessage='yes' RebootPrompt='no' TerminateProcess='0'/>
|
||||
<InstallExecuteSequence>
|
||||
<!-- An upgrade removes the old version too, but should keep screening. -->
|
||||
<Custom Action='UninstallFirewall' After='DeleteServices'>REMOVE~="ALL" AND NOT UPGRADINGPRODUCTCODE</Custom>
|
||||
<Custom Action='SetFailureActions' After='InstallServices'>NOT REMOVE~="ALL"</Custom>
|
||||
<Custom Action='SetFailureFlag' After='SetFailureActions'>NOT REMOVE~="ALL"</Custom>
|
||||
<!-- The default, before InstallFiles, is after RemoveFiles, which would find the tray still running. -->
|
||||
<Custom Action='WixCloseApplications' Before='RemoveFiles'/>
|
||||
</InstallExecuteSequence>
|
||||
|
||||
<SetProperty Id='ARPINSTALLLOCATION' Value='[APPLICATIONFOLDER]' After='CostFinalize'/>
|
||||
@@ -186,7 +161,6 @@
|
||||
<UIRef Id='WixUI_FeatureTree'/>
|
||||
<Publish Dialog='WelcomeDlg' Control='Next' Event='NewDialog' Value='CustomizeDlg' Order='99'>1</Publish>
|
||||
<Publish Dialog='CustomizeDlg' Control='Back' Event='NewDialog' Value='WelcomeDlg' Order='99'>1</Publish>
|
||||
<Publish Dialog='ExitDialog' Control='Finish' Event='DoAction' Value='StartTray' Order='1'>NOT Installed</Publish>
|
||||
</UI>
|
||||
</Product>
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ 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"]
|
||||
@@ -1,32 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import AppKit
|
||||
import NetworkExtension
|
||||
import OSLog
|
||||
import Security
|
||||
import ServiceManagement
|
||||
import SystemExtensions
|
||||
|
||||
enum StepStatus: Equatable {
|
||||
@@ -52,7 +51,6 @@ 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>)?
|
||||
@@ -74,7 +72,6 @@ final class FirewallModel: NSObject, ObservableObject {
|
||||
await refresh()
|
||||
if CommandLine.arguments.contains("--install") { await install() }
|
||||
}
|
||||
Task { await watchBlocks() }
|
||||
}
|
||||
|
||||
// MARK: - Flows
|
||||
@@ -115,11 +112,6 @@ final class FirewallModel: NSObject, ObservableObject {
|
||||
work: trustCertificateAuthority)
|
||||
else { return }
|
||||
|
||||
do {
|
||||
try SMAppService.mainApp.register()
|
||||
} catch {
|
||||
log("Could not open the app at login: \(error.localizedDescription)")
|
||||
}
|
||||
completed = true
|
||||
log("Setup complete")
|
||||
offerRestart()
|
||||
@@ -160,7 +152,6 @@ final class FirewallModel: NSObject, ObservableObject {
|
||||
await removeConfiguration()
|
||||
let strandedKey = removeStoredCA()
|
||||
await deactivate()
|
||||
try? await SMAppService.mainApp.unregister()
|
||||
if let strandedKey { trustStep = .failed(strandedKey) }
|
||||
|
||||
let failed = [extensionStep, screeningStep, trustStep].contains(where: \.isFailed)
|
||||
@@ -208,21 +199,6 @@ 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,20 +22,5 @@ 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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,6 @@ build() {
|
||||
echo "==> Building"
|
||||
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 \
|
||||
CODE_SIGN_IDENTITY="" CODE_SIGN_ENTITLEMENTS="" build >/dev/null
|
||||
}
|
||||
|
||||
+3
-11
@@ -10,7 +10,7 @@ settings:
|
||||
"CARGO_PROFILE[config=Debug]": debug
|
||||
"LIBRARY_SEARCH_PATHS[arch=arm64]": $(SRCROOT)/../core/target/aarch64-apple-darwin/$(CARGO_PROFILE)
|
||||
"LIBRARY_SEARCH_PATHS[arch=x86_64]": $(SRCROOT)/../core/target/x86_64-apple-darwin/$(CARGO_PROFILE)
|
||||
ARCHS: arm64 x86_64
|
||||
ARCHS: arm64
|
||||
"ONLY_ACTIVE_ARCH[config=Debug]": "YES"
|
||||
SWIFT_VERSION: "5.0"
|
||||
DEVELOPMENT_TEAM: ${TEAM}
|
||||
@@ -127,13 +127,5 @@ targets:
|
||||
unset CC CXX CFLAGS CXXFLAGS
|
||||
export SDKROOT="$(xcrun --sdk macosx --show-sdk-path)"
|
||||
cd "$SRCROOT/../core"
|
||||
bins=
|
||||
for arch in $ARCHS; do
|
||||
case $arch in
|
||||
arm64|arm64e) triple=aarch64-apple-darwin ;;
|
||||
x86_64) triple=x86_64-apple-darwin ;;
|
||||
esac
|
||||
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"
|
||||
cargo build --bin firewall-uninstall --target aarch64-apple-darwin $([ "$CONFIGURATION" = Debug ] || echo --release)
|
||||
cp "target/aarch64-apple-darwin/$CARGO_PROFILE/firewall-uninstall" "$TARGET_BUILD_DIR/$EXECUTABLE_FOLDER_PATH/"
|
||||
Reference in new issue
Block a user