Compare commits

..
Author SHA1 Message Date
Ian ac071c7cab Show the Windows service's blocks from the tray
The service answers the tray's requests on a named pipe, as the macOS extension answers the app's messages. The tray asks for blocks every two seconds, pops each new one up and lists the last ten in its menu.
2026-10-23 10:46:10 -04:00
ian-at-strix 6c69036db0 Screen mirrors listed in the config file, and reject unusable config values (#73)
* Screen mirrors listed in the config file

* Reject config values the firewall cannot use
2026-10-22 18:26:59 -04:00
ian-at-strix 4fd3021760 Create the Windows data folder with its ACL instead of setting it after (#55) 2026-10-22 18:25:28 -04:00
21 changed files with 272 additions and 102 deletions

No files matched your search

+1 -1
View File
@@ -125,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_Console", "Win32_System_Environment", "Win32_System_JobObjects", "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_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Environment", "Win32_System_JobObjects", "Win32_System_Pipes", "Win32_System_Threading", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
windows-registry = "0.6.1"
windows-service = "0.8.0"
+1 -1
View File
@@ -8,7 +8,7 @@ pub static BUN: Manager = Manager {
("https_proxy", PROXY),
("HTTP_PROXY", PROXY),
("HTTPS_PROXY", PROXY),
("npm_config_registry", "http://{npm}/"),
("npm_config_registry", "http://registry.npmjs.org/"),
],
args: &[],
};
+1 -1
View File
@@ -14,7 +14,7 @@ pub static CARGO: Manager = Manager {
"--config",
"source.crates-io.replace-with=\"firewall\"",
"--config",
"source.firewall.registry=\"sparse+http://{cargo/index}/\"",
"source.firewall.registry=\"sparse+http://index.crates.io/\"",
],
}],
};
+1 -1
View File
@@ -6,6 +6,6 @@ pub static GEM: Manager = Manager {
// gem has no variable for the source list, and gem env and gem sources reject the flags.
args: &[Args {
when: &["install", "update", "fetch"],
flags: &["--clear-sources", "--source", "http://{gem}/"],
flags: &["--clear-sources", "--source", "http://rubygems.org/"],
}],
};
+1 -1
View File
@@ -6,7 +6,7 @@ pub static GO: Manager = Manager {
env: &[
("HTTP_PROXY", PROXY),
("http_proxy", PROXY),
("GOPROXY", "http://{go}"),
("GOPROXY", "http://proxy.golang.org"),
("GOSUMDB", "off"),
("GOFLAGS", "-mod=mod"),
],
+8 -26
View File
@@ -9,9 +9,6 @@ mod pipenv;
mod pnpm;
mod uv;
use crate::policies::registries::{host_of, BUILT_IN};
use crate::FirewallConfig;
pub use bun::BUN;
pub use cargo::CARGO;
pub use gem::GEM;
@@ -23,7 +20,7 @@ pub use pipenv::PIPENV;
pub use pnpm::PNPM;
pub use uv::UV;
pub const PYPI_SIMPLE: &str = "http://{pypi}/simple";
pub const PYPI_SIMPLE: &str = "http://pypi.org/simple";
pub const PROXY: &str = "{proxy}";
@@ -34,9 +31,7 @@ pub struct Args {
}
/// An https:// registry arrives as a CONNECT tunnel naming only the host, so each
/// manager also overrides its registry URL to http://. `{npm}` and the like name a
/// built-in registry, and expand to its first mirror, if any, without the scheme;
/// `{host:npm}` to just its host.
/// manager also overrides its registry URL to http://.
pub struct Manager {
pub command: &'static str,
pub env: &'static [(&'static str, &'static str)],
@@ -47,36 +42,23 @@ pub static MANAGERS: &[&Manager] =
&[&NPM, &PIP, &PIP3, &CARGO, &GO, &GEM, &UV, &PDM, &PIPENV, &PNPM, &BUN];
/// Every manager's variables, so whichever the command starts is pointed at the firewall.
pub fn variables(proxy: &str, config: &FirewallConfig) -> Vec<(String, String)> {
pub fn variables(proxy: &str) -> Vec<(String, String)> {
let env = MANAGERS.iter().flat_map(|manager| manager.env);
env.map(|(key, value)| (key.to_string(), expand(value, proxy, config))).collect()
env.map(|(key, value)| (key.to_string(), expand(value, proxy))).collect()
}
pub fn arguments(
proxy: &str,
config: &FirewallConfig,
manager: &Manager,
typed: &[String],
) -> Vec<String> {
pub fn arguments(proxy: &str, manager: &Manager, typed: &[String]) -> Vec<String> {
let mut out = Vec::new();
for args in manager.args {
let wanted =
args.when.is_empty() || typed.iter().any(|arg| args.when.contains(&arg.as_str()));
if wanted {
out.extend(args.flags.iter().map(|flag| expand(flag, proxy, config)));
out.extend(args.flags.iter().map(|flag| expand(flag, proxy)));
}
}
out
}
fn expand(value: &str, proxy: &str, config: &FirewallConfig) -> String {
let mut value = value.replace(PROXY, proxy);
for registry in BUILT_IN {
let mirror = config.registries.get(registry.name()).and_then(|mirrors| mirrors.first());
let upstream = mirror.map_or(registry.upstream, String::as_str);
let address = upstream.trim_start_matches("https://").trim_end_matches('/');
value = value.replace(&format!("{{{}}}", registry.name()), address);
value = value.replace(&format!("{{host:{}}}", registry.name()), host_of(upstream));
}
value
fn expand(value: &str, proxy: &str) -> String {
value.replace(PROXY, proxy)
}
+1 -1
View File
@@ -5,7 +5,7 @@ pub static NPM: Manager = Manager {
env: &[
("npm_config_proxy", PROXY),
("npm_config_https_proxy", PROXY),
("npm_config_registry", "http://{npm}/"),
("npm_config_registry", "http://registry.npmjs.org/"),
],
args: &[],
};
+1 -1
View File
@@ -4,7 +4,7 @@ use super::{Manager, PROXY, PYPI_SIMPLE};
const PIP_ENV: &[(&str, &str)] = &[
("PIP_PROXY", PROXY),
("PIP_INDEX_URL", PYPI_SIMPLE),
("PIP_TRUSTED_HOST", "{host:pypi} {host:pypi-files}"),
("PIP_TRUSTED_HOST", "pypi.org files.pythonhosted.org"),
("PIP_DISABLE_PIP_VERSION_CHECK", "1"),
];
+1 -1
View File
@@ -10,7 +10,7 @@ pub static PIPENV: Manager = Manager {
("https_proxy", PROXY),
("PIPENV_PYPI_MIRROR", PYPI_SIMPLE),
// pipenv installs through pip, which skips a plaintext index on a host it doesn't trust.
("PIP_TRUSTED_HOST", "{host:pypi} {host:pypi-files}"),
("PIP_TRUSTED_HOST", "pypi.org files.pythonhosted.org"),
],
args: &[],
};
+1 -1
View File
@@ -6,7 +6,7 @@ pub static PNPM: Manager = Manager {
env: &[
("pnpm_config_proxy", PROXY),
("pnpm_config_https_proxy", PROXY),
("pnpm_config_registry", "http://{npm}/"),
("pnpm_config_registry", "http://registry.npmjs.org/"),
],
args: &[],
};
+5 -11
View File
@@ -6,7 +6,6 @@ mod windows;
pub use forward::spawn;
pub use managers::*;
use crate::FirewallConfig;
use std::io::{Error, ErrorKind};
use std::path::{Path, PathBuf};
use std::process::Command;
@@ -22,12 +21,12 @@ impl Invocation {
}
/// What the program is run with: the typed arguments plus any its manager needs.
pub fn arguments(&self, proxy: &str, config: &FirewallConfig) -> Vec<String> {
pub fn arguments(&self, proxy: &str) -> Vec<String> {
let mut args = self.args.clone();
let Some(manager) = MANAGERS.iter().find(|manager| manager.command == self.program) else {
return args;
};
let ours = managers::arguments(proxy, config, manager, &args);
let ours = managers::arguments(proxy, manager, &args);
// Anything after `--` belongs to another program, so ours go before it.
let at = args.iter().position(|arg| arg == "--").unwrap_or(args.len());
args.splice(at..at, ours);
@@ -35,20 +34,15 @@ impl Invocation {
}
}
pub fn run(
port: u16,
config: &FirewallConfig,
invocation: &Invocation,
dir: Option<&Path>,
) -> std::io::Result<i32> {
pub fn run(port: u16, invocation: &Invocation, dir: Option<&Path>) -> std::io::Result<i32> {
let proxy = crate::local_url(port, "");
let program = resolve(&invocation.program)?;
let mut command = Command::new(&program);
command.args(invocation.arguments(&proxy, config));
command.args(invocation.arguments(&proxy));
if let Some(dir) = dir {
command.current_dir(dir);
}
for (key, value) in managers::variables(&proxy, config) {
for (key, value) in managers::variables(&proxy) {
command.env(key, value);
}
#[cfg(windows)]
+2 -2
View File
@@ -10,7 +10,7 @@ use std::path::{Path, PathBuf};
pub const DEFAULT_PORT: u16 = 8080;
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct FirewallConfig {
pub port: NonZeroU16,
@@ -26,7 +26,7 @@ pub struct FirewallConfig {
pub registries: HashMap<String, Vec<String>>,
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AllowedPackage {
pub name: String,
+2 -3
View File
@@ -73,14 +73,13 @@ fn serve() -> ! {
fn run(invocation: Invocation) -> ! {
telemetry::set_mode("wrapper");
let command = &invocation.program;
let config = load();
let port = match aliasing::spawn(0, config.clone()) {
let port = match aliasing::spawn(0, load()) {
Ok(port) => port,
Err(e) => fail("spawn", &format!("could not start the firewall: {e}")),
};
info!(url = %firewall::local_url(port, ""), "firewall listening");
let result = aliasing::run(port, &config, &invocation, None);
let result = aliasing::run(port, &invocation, None);
telemetry::flush();
match result {
Ok(code) => exit(code),
+23 -8
View File
@@ -11,6 +11,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
use tracing::warn;
use windows::Win32::Security::Authorization::{GetNamedSecurityInfoW, SE_FILE_OBJECT};
use windows::Win32::Security::*;
use windows::Win32::Storage::FileSystem::CreateDirectoryW;
use windows_registry::Type;
/// SYSTEM and admins write, users only read, so no one else can plant a CA for node.
@@ -65,25 +66,39 @@ pub fn load_or_create_ca(
Ok(ca)
}
/// Created on first use, its owner and ACL reset every time. One a user made first, or a link,
/// Created on first use with its ACL, which is reset every time. One a user made first, or a link,
/// is replaced: its owner could grant itself access again, and a link would redirect our writes.
pub(super) fn secure_data_dir() -> io::Result<PathBuf> {
let dir = data_dir();
match std::fs::symlink_metadata(&dir) {
Ok(meta) if meta.file_type().is_symlink() => std::fs::remove_dir(&dir)?,
let exists = match std::fs::symlink_metadata(&dir) {
Ok(meta) if meta.file_type().is_symlink() => {
std::fs::remove_dir(&dir)?;
false
}
Ok(_) if !owned_by_admins(&dir)? => {
warn!(dir = %dir.display(), "replacing a data folder not created by an admin");
std::fs::remove_dir_all(&dir)?
std::fs::remove_dir_all(&dir)?;
false
}
_ => {}
}
std::fs::create_dir_all(&dir)?;
Ok(_) => true,
Err(_) => false,
};
let info = OWNER_SECURITY_INFORMATION
| DACL_SECURITY_INFORMATION
| PROTECTED_DACL_SECURITY_INFORMATION;
let path = HSTRING::from(dir.as_path());
let acl = security_descriptor(DATA_DIR_ACL)?;
unsafe { SetFileSecurityW(&path, info, PSECURITY_DESCRIPTOR(acl.0)).ok()? };
if exists {
unsafe { SetFileSecurityW(&path, info, PSECURITY_DESCRIPTOR(acl.0)).ok()? };
} else {
let attributes = SECURITY_ATTRIBUTES {
nLength: size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: acl.0,
bInheritHandle: false.into(),
};
// Never inherits ProgramData's ACL, which lets users add files; fails if it was recreated.
unsafe { CreateDirectoryW(&path, Some(&attributes))? };
}
Ok(dir)
}
+1
View File
@@ -2,6 +2,7 @@
//! LocalSystem service screens it as the macOS extension does.
mod ca;
mod pipe;
mod service;
mod tpm;
mod tray;
+58
View File
@@ -0,0 +1,58 @@
//! How the tray asks the service what it blocked, as the macOS app messages its extension.
use super::security_descriptor;
use crate::blocks;
use rama::error::BoxError;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::windows::named_pipe::{NamedPipeServer, ServerOptions};
use windows::Win32::Security::SECURITY_ATTRIBUTES;
pub(super) const PIPE: &str = r"\\.\pipe\StrixFirewall";
/// Signed-in users can connect, as the tray runs as them.
const PIPE_ACL: &str = "D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;IU)";
pub(super) async fn serve() -> Result<(), BoxError> {
let mut server = create(true)?;
loop {
server.connect().await?;
let client = std::mem::replace(&mut server, create(false)?);
tokio::spawn(async move {
if let Err(error) = answer(client).await {
tracing::debug!(%error, "could not answer the tray");
}
});
}
}
fn create(first: bool) -> Result<NamedPipeServer, BoxError> {
let acl = security_descriptor(PIPE_ACL)?;
let mut attributes = SECURITY_ATTRIBUTES {
nLength: size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: acl.0,
bInheritHandle: false.into(),
};
let attributes = (&raw mut attributes).cast();
let mut options = ServerOptions::new();
// Fails if anyone else made the pipe first.
options.first_pipe_instance(first);
Ok(unsafe { options.create_with_security_attributes_raw(PIPE, attributes)? })
}
/// A line each way.
async fn answer(client: NamedPipeServer) -> Result<(), BoxError> {
let mut client = BufReader::new(client);
let mut request = String::new();
(&mut client)
.take(64 * 1024)
.read_line(&mut request)
.await?;
let reply = match request.trim_end() {
"blocks" => serde_json::to_string(&blocks::take())?,
other => format!("error: unknown request {other}"),
};
client.write_all(format!("{reply}\n").as_bytes()).await?;
// Closing before the tray hangs up could discard the reply.
let _ = tokio::time::timeout(Duration::from_secs(5), client.read(&mut [0])).await;
Ok(())
}
+9 -1
View File
@@ -1,6 +1,8 @@
use super::ca::{load_or_create_ca, secure_data_dir};
use super::tpm::CaKey;
use super::{config_file, loopback_addresses, write_hosts_entries, CRL_ADDRESS, SERVICE_NAME};
use super::{
config_file, loopback_addresses, pipe, write_hosts_entries, CRL_ADDRESS, SERVICE_NAME,
};
use crate::config::FirewallConfig;
use crate::http::{reply, text};
use crate::{mitm, telemetry, Firewall};
@@ -153,6 +155,12 @@ async fn start_screening(status: &ServiceStatusHandle) -> Result<Vec<&'static st
let crl = HttpServer::auto(exec.clone()).service(crl);
tokio::spawn(listen_on(CRL_ADDRESS, 80, &exec)?.serve(crl));
tokio::spawn(async {
if let Err(error) = pipe::serve().await {
tracing::error!(%error, "the tray cannot reach the service");
}
});
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] {
+136 -8
View File
@@ -1,17 +1,21 @@
//! The notification-area icon, started at sign-in as the menu bar app is on macOS.
use super::pipe::PIPE;
use super::*;
use muda::{Menu, MenuEvent, MenuItem, PredefinedMenuItem};
use std::io::Cursor;
use muda::{Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu};
use serde::Deserialize;
use std::io::{BufRead, BufReader, Cursor, Write};
use std::os::windows::io::AsRawHandle;
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::Pipes::GetNamedPipeServerProcessId;
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::{Service, ServiceAccess, ServiceState, ServiceStatus};
use windows_service::service_manager::{ServiceManager, ServiceManagerAccess};
const MENU_PAUSE: &str = "pause";
@@ -19,6 +23,9 @@ const MENU_RESTART: &str = "restart";
const MENU_LOG: &str = "log";
const MENU_QUIT: &str = "quit";
const LOGO: &[u8] = include_bytes!("tray.png");
/// Where blocked packages are listed, below Open logs.
const BLOCKED_AT: usize = 5;
const BLOCKED_KEPT: usize = 10;
/// Runs until Quit, once per session.
pub fn run() -> Result<(), BoxError> {
@@ -29,6 +36,7 @@ pub fn run() -> Result<(), BoxError> {
}
let status = MenuItem::new("", false, None);
let pause = MenuItem::with_id(MENU_PAUSE, "", false, None);
let blocked_separator = PredefinedMenuItem::separator();
let menu = Menu::with_items(&[
&status,
&PredefinedMenuItem::separator(),
@@ -39,7 +47,9 @@ 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)).build()?;
let icon = TrayIconBuilder::new()
.with_menu(Box::new(menu.clone()))
.build()?;
let window = HWND(icon.window_handle());
let mut tray = Tray {
icon,
@@ -49,6 +59,9 @@ pub fn run() -> Result<(), BoxError> {
status,
pause,
state: None,
menu,
blocked_separator,
blocked: Vec::new(),
};
tray.update();
unsafe { SetTimer(None, 0, 2000, None) };
@@ -62,6 +75,7 @@ pub fn run() -> Result<(), BoxError> {
// Ours, not the icon window's own hover timer.
if message.message == WM_TIMER && message.hwnd.is_invalid() {
tray.update();
tray.collect_blocks();
}
while let Ok(event) = TrayIconEvent::receiver().try_recv() {
// The menu opens on release, so this keeps it current.
@@ -94,11 +108,39 @@ struct Tray {
pause: MenuItem,
/// As the menu last showed it.
state: Option<ServiceState>,
menu: Menu,
blocked_separator: PredefinedMenuItem,
/// The most recent, as the menu lists them.
blocked: Vec<BlockedPackage>,
}
#[derive(Clone, PartialEq, Deserialize)]
struct BlockedPackage {
eco: String,
name: String,
version: Option<String>,
filter: String,
reason: String,
}
impl BlockedPackage {
fn label(&self) -> String {
match &self.version {
Some(version) => format!("{} {version} ({})", self.name, self.eco),
None => format!("{} ({})", self.name, self.eco),
}
}
/// The denylist and OSV only flag packages known to be malicious; the age check only holds
/// back new ones.
fn malicious(&self) -> bool {
self.filter != "age"
}
}
impl Tray {
fn update(&mut self) {
self.state = service_state();
self.state = service_status().map(|status| status.current_state);
let screening = self.state == Some(ServiceState::Running);
let light = light_taskbar();
if self.drawn != Some((light, screening)) {
@@ -117,6 +159,58 @@ impl Tray {
self.pause.set_enabled(self.state.is_some());
}
/// The service hands each block over once, so whichever tray asks first shows it.
fn collect_blocks(&mut self) {
if self.state != Some(ServiceState::Running) {
return;
}
let Ok(reply) = ask_service("blocks") else {
return;
};
let Ok(packages) = serde_json::from_str::<Vec<BlockedPackage>>(&reply) else {
return;
};
let mut new = Vec::new();
for package in packages {
if !self.blocked.contains(&package) {
self.blocked.push(package.clone());
new.push(package);
}
}
if new.is_empty() {
return;
}
let excess = self.blocked.len().saturating_sub(BLOCKED_KEPT);
self.blocked.drain(..excess);
self.list_blocked();
// Off the UI thread, so the menu keeps working while they are open.
std::thread::spawn(move || new.iter().for_each(alert));
}
fn list_blocked(&self) {
while self.menu.items().len() > BLOCKED_AT + 2 {
self.menu.remove_at(BLOCKED_AT);
}
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 reason = if package.malicious() {
format!("Malicious: {}", package.reason)
} else {
package.reason.clone()
};
let reason = MenuItem::new(reason, false, None);
let label = format!("{verb} {}", package.label());
if let Ok(item) = Submenu::with_items(label, true, &[&reason]) {
let _ = self.menu.insert(&item, BLOCKED_AT + 1 + i);
}
}
}
fn on_menu(&self, id: &str) {
let state = self.state;
let screening = state == Some(ServiceState::Running);
@@ -143,6 +237,39 @@ impl Tray {
}
}
fn alert(package: &BlockedPackage) {
let text = format!("{} {}.", package.label(), package.reason);
let (title, text, style) = if package.malicious() {
let title = format!("Blocked malware: {}", package.label());
let text = text
+ "\n\nIt was not installed. Don't install it from another source. If you didn't expect this "
+ "package, a dependency may be compromised; tell your security team.";
(title, text, MB_ICONERROR)
} else {
("Held back a new package".to_owned(), text, MB_ICONINFORMATION)
};
let (title, text) = (HSTRING::from(title), HSTRING::from(text));
unsafe { MessageBoxW(None, &text, &title, style | MB_SETFOREGROUND) };
}
/// 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)?;
// 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)? };
if service_status().and_then(|status| status.process_id) != Some(server) {
return Err("Another program is posing as the firewall service.".into());
}
pipe.write_all(format!("{request}\n").as_bytes())?;
let mut reply = String::new();
BufReader::new(pipe).read_line(&mut reply)?;
Ok(reply.trim_end().to_owned())
}
fn show_error(error: BoxError) {
let text = match error.source() {
Some(source) => format!("{error}: {source}"),
@@ -153,13 +280,13 @@ fn show_error(error: BoxError) {
}
/// `None` when the service is not installed.
fn service_state() -> Option<ServiceState> {
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()?;
Some(service.query_status().ok()?.current_state)
service.query_status().ok()
}
fn light_taskbar() -> bool {
@@ -167,7 +294,8 @@ 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 {
+13 -26
View File
@@ -1,11 +1,11 @@
use firewall::aliasing::managers::*;
use firewall::aliasing::Invocation;
use firewall::{Firewall, FirewallConfig};
use firewall::Firewall;
#[test]
fn arguments_after_a_double_dash_reach_the_program() {
let typed = ["run", "--", "--release"].map(String::from).to_vec();
let args = Invocation::new("cargo".into(), typed).arguments("http://127.0.0.1:9000", &FirewallConfig::default());
let args = Invocation::new("cargo".into(), typed).arguments("http://127.0.0.1:9000");
assert_eq!(args.first().map(String::as_str), Some("run"));
assert_eq!(args[args.len() - 2..], ["--", "--release"]);
}
@@ -13,23 +13,12 @@ fn arguments_after_a_double_dash_reach_the_program() {
#[test]
fn recipes_expand_into_the_environment() {
let var = |name: &str, value: &str| (name.to_string(), value.to_string());
let config = FirewallConfig::default();
let npm = variables("http://127.0.0.1:9000", &config);
let npm = variables("http://127.0.0.1:9000");
assert!(npm.contains(&var("npm_config_proxy", "http://127.0.0.1:9000")));
assert!(npm.contains(&var("npm_config_registry", "http://registry.npmjs.org/")));
let gem = arguments("http://127.0.0.1:9000", &config, &GEM, &["install".to_string()]);
let gem = arguments("http://127.0.0.1:9000", &GEM, &["install".to_string()]);
assert_eq!(gem, ["--clear-sources", "--source", "http://rubygems.org/"]);
assert!(arguments("http://127.0.0.1:9000", &config, &GEM, &["sources".to_string()]).is_empty());
}
#[test]
fn a_mirror_replaces_the_registry_it_copies() {
let var = |name: &str, value: &str| (name.to_string(), value.to_string());
let config = r#"{"registries": {"npm": ["https://npm.example.com/"], "pypi": ["https://art.example.com/pypi/"]}}"#;
let env = variables("http://127.0.0.1:9000", &serde_json::from_str(config).expect("parses"));
assert!(env.contains(&var("npm_config_registry", "http://npm.example.com/")));
assert!(env.contains(&var("PIP_INDEX_URL", "http://art.example.com/pypi/simple")));
assert!(env.contains(&var("PIP_TRUSTED_HOST", "art.example.com files.pythonhosted.org")));
assert!(arguments("http://127.0.0.1:9000", &GEM, &["sources".to_string()]).is_empty());
}
#[test]
@@ -45,17 +34,15 @@ fn no_two_managers_set_a_variable_differently() {
#[tokio::test]
async fn every_overridden_registry_is_known() {
let config = FirewallConfig::default();
let hosts = Firewall::default().hosts();
let values = variables("", &config).into_iter().map(|(_, value)| value);
let flags = MANAGERS.iter().flat_map(|manager| {
let typed: Vec<String> = manager.args.iter().flat_map(|args| args.when).map(|arg| arg.to_string()).collect();
arguments("", &config, manager, &typed)
});
for value in values.chain(flags) {
for (_, after) in value.match_indices("http://").map(|(i, _)| value.split_at(i + 7)) {
let host = after.split(['/', '"']).next().unwrap_or(after);
assert!(hosts.contains(&host), "{value} points at {host}");
for manager in MANAGERS {
let values = manager.env.iter().map(|(_, value)| *value);
let flags = manager.args.iter().flat_map(|args| args.flags.iter().copied());
for value in values.chain(flags) {
for (_, after) in value.match_indices("http://").map(|(i, _)| value.split_at(i + 7)) {
let host = after.split(['/', '"']).next().unwrap_or(after);
assert!(hosts.contains(&host), "{} points at {host}", manager.command);
}
}
}
}
+2 -3
View File
@@ -6,11 +6,10 @@ use probe::Outcome;
fn main() -> std::io::Result<()> {
firewall::init_logging("info");
let config = FirewallConfig::load()?;
let port = aliasing::spawn(0, config.clone())?;
let port = aliasing::spawn(0, FirewallConfig::load()?)?;
println!("firewall proxy on {}\n", firewall::local_url(port, ""));
let results = probe::all(port, &config);
let results = probe::all(port);
println!();
let mut failed = 0;
for (name, outcome) in &results {
+4 -5
View File
@@ -1,5 +1,4 @@
use firewall::aliasing::{self, Invocation, Manager, CARGO, GEM, GO, NPM, PIP, PIP3, UV};
use firewall::FirewallConfig;
use std::path::PathBuf;
struct Probe {
@@ -30,14 +29,14 @@ static PROBES: &[Probe] = &[
},
];
pub fn all(port: u16, config: &FirewallConfig) -> Vec<(&'static str, Outcome)> {
PROBES.iter().map(|probe| (probe.manager.command, run(probe, port, config))).collect()
pub fn all(port: u16) -> Vec<(&'static str, Outcome)> {
PROBES.iter().map(|probe| (probe.manager.command, run(probe, port))).collect()
}
fn run(probe: &Probe, port: u16, config: &FirewallConfig) -> 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, config, &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,