mirror of
https://github.com/usestrix/firewall.git
synced 2026-11-09 18:08:49 -10:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e73888e2b | ||
|
|
1f0e3f8e31 | ||
|
|
de77b3716d | ||
|
|
213c48ab01 | ||
|
|
314635bf2c |
No files matched your search
Generated
+80
-5
@@ -711,6 +711,20 @@ version = "1.18.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
|
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "embed-resource"
|
||||||
|
version = "3.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6168487b5e7eb2673e6efe1200b1848016e436a6ca0b8031bf04bd9d493a249f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"memchr",
|
||||||
|
"rustc_version",
|
||||||
|
"toml",
|
||||||
|
"vswhom",
|
||||||
|
"winreg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.42"
|
version = "0.8.42"
|
||||||
@@ -3243,6 +3257,15 @@ dependencies = [
|
|||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -3375,6 +3398,7 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"core-foundation 0.10.1",
|
"core-foundation 0.10.1",
|
||||||
"directories",
|
"directories",
|
||||||
|
"embed-resource",
|
||||||
"muda",
|
"muda",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"png",
|
"png",
|
||||||
@@ -3671,10 +3695,25 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml"
|
||||||
version = "1.1.1+spec-1.1.0"
|
version = "1.1.8+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde_core",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_parser",
|
||||||
|
"toml_writer",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "1.1.2+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
@@ -3693,13 +3732,19 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_parser"
|
name = "toml_parser"
|
||||||
version = "1.1.3+spec-1.1.0"
|
version = "1.1.5+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
|
checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_writer"
|
||||||
|
version = "1.1.3+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-layer"
|
name = "tower-layer"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@@ -3909,6 +3954,26 @@ version = "0.9.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vswhom"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"vswhom-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vswhom-sys"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "walkdir"
|
name = "walkdir"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -4284,6 +4349,16 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winreg"
|
||||||
|
version = "0.56.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen"
|
name = "wit-bindgen"
|
||||||
version = "0.57.1"
|
version = "0.57.1"
|
||||||
|
|||||||
+5
-1
@@ -129,7 +129,7 @@ schannel = "0.1.29"
|
|||||||
tokio = { version = "1.53.1", features = ["net"] }
|
tokio = { version = "1.53.1", features = ["net"] }
|
||||||
tracing-appender = "0.2.5"
|
tracing-appender = "0.2.5"
|
||||||
tray-icon = { version = "0.26.1", default-features = false }
|
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_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 = { 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_LibraryLoader", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_UI_Controls", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
|
||||||
windows-registry = "0.6.1"
|
windows-registry = "0.6.1"
|
||||||
windows-service = "0.8.0"
|
windows-service = "0.8.0"
|
||||||
|
|
||||||
@@ -139,3 +139,7 @@ tokio = { version = "1.53.1", features = ["macros", "rt-multi-thread"] }
|
|||||||
[profile.dist]
|
[profile.dist]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
lto = "thin"
|
lto = "thin"
|
||||||
|
|
||||||
|
# Only on Windows hosts, where the tray and service are built.
|
||||||
|
[target.'cfg(windows)'.build-dependencies]
|
||||||
|
embed-resource = "3.0.12"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
//! The tray's task dialogs exist only in version 6 of the common controls, which takes a manifest.
|
||||||
|
//! Both machine-wide binaries also get the Strix icon.
|
||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=src/bin/firewall-tray.manifest");
|
||||||
|
let msvc = std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc");
|
||||||
|
if msvc && std::env::var_os("CARGO_FEATURE_MACHINE_WIDE").is_some() {
|
||||||
|
let manifest = concat!(env!("CARGO_MANIFEST_DIR"), "/src/bin/firewall-tray.manifest");
|
||||||
|
println!("cargo:rustc-link-arg-bin=firewall-tray=/MANIFEST:EMBED");
|
||||||
|
println!("cargo:rustc-link-arg-bin=firewall-tray=/MANIFESTINPUT:{manifest}");
|
||||||
|
#[cfg(windows)]
|
||||||
|
embed_resource::compile_for("src/bin/firewall.rc", ["firewall-tray", "firewall-service"], embed_resource::NONE)
|
||||||
|
// Fails the build, rather than ship without the icon.
|
||||||
|
.manifest_required()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0"
|
||||||
|
processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
</assembly>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// The icon Explorer, UAC and the tray's popups show.
|
||||||
|
1 ICON "strix.ico"
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
+54
-10
@@ -1,24 +1,32 @@
|
|||||||
//! How the tray asks the service what it blocked, as the macOS app messages its extension.
|
//! How the tray asks the service what it blocked, and allows a new package, as the macOS app
|
||||||
|
//! messages its extension.
|
||||||
|
|
||||||
use super::security_descriptor;
|
use super::{config_file, security_descriptor};
|
||||||
use crate::blocks;
|
use crate::{blocks, Firewall};
|
||||||
use rama::error::BoxError;
|
use rama::error::BoxError;
|
||||||
|
use std::os::windows::io::AsRawHandle;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
|
||||||
use tokio::net::windows::named_pipe::{NamedPipeServer, ServerOptions};
|
use tokio::net::windows::named_pipe::{NamedPipeServer, ServerOptions};
|
||||||
use windows::Win32::Security::SECURITY_ATTRIBUTES;
|
use windows::core::{Owned, PWSTR};
|
||||||
|
use windows::Win32::Foundation::HANDLE;
|
||||||
|
use windows::Win32::Security::*;
|
||||||
|
use windows::Win32::System::Pipes::{GetNamedPipeClientProcessId, ImpersonateNamedPipeClient};
|
||||||
|
use windows::Win32::System::Threading::*;
|
||||||
|
|
||||||
pub(super) const PIPE: &str = r"\\.\pipe\StrixFirewall";
|
pub(super) const PIPE: &str = r"\\.\pipe\StrixFirewall";
|
||||||
/// Signed-in users can connect, as the tray runs as them.
|
/// 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)";
|
const PIPE_ACL: &str = "D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;IU)";
|
||||||
|
|
||||||
pub(super) async fn serve() -> Result<(), BoxError> {
|
pub(super) async fn serve(firewall: Arc<Firewall>) -> Result<(), BoxError> {
|
||||||
let mut server = create(true)?;
|
let mut server = create(true)?;
|
||||||
loop {
|
loop {
|
||||||
server.connect().await?;
|
server.connect().await?;
|
||||||
let client = std::mem::replace(&mut server, create(false)?);
|
let client = std::mem::replace(&mut server, create(false)?);
|
||||||
|
let firewall = Arc::clone(&firewall);
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(error) = answer(client).await {
|
if let Err(error) = answer(client, &firewall).await {
|
||||||
tracing::debug!(%error, "could not answer the tray");
|
tracing::debug!(%error, "could not answer the tray");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -40,19 +48,55 @@ fn create(first: bool) -> Result<NamedPipeServer, BoxError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A line each way.
|
/// A line each way.
|
||||||
async fn answer(client: NamedPipeServer) -> Result<(), BoxError> {
|
async fn answer(client: NamedPipeServer, firewall: &Firewall) -> Result<(), BoxError> {
|
||||||
let mut client = BufReader::new(client);
|
let mut client = BufReader::new(client);
|
||||||
let mut request = String::new();
|
let mut request = String::new();
|
||||||
(&mut client)
|
(&mut client)
|
||||||
.take(64 * 1024)
|
.take(64 * 1024)
|
||||||
.read_line(&mut request)
|
.read_line(&mut request)
|
||||||
.await?;
|
.await?;
|
||||||
let reply = match request.trim_end() {
|
let request = request.trim_end();
|
||||||
"blocks" => serde_json::to_string(&blocks::take())?,
|
// Only the app can message the macOS extension; here any program could connect.
|
||||||
other => format!("error: unknown request {other}"),
|
let reply = if !client_is_tray(client.get_ref())? {
|
||||||
|
"error: only the tray can ask".to_owned()
|
||||||
|
} else if request.starts_with("allow ") && !client_is_admin(client.get_ref())? {
|
||||||
|
"error: trusting a package takes an administrator".to_owned()
|
||||||
|
} else {
|
||||||
|
blocks::answer(firewall, &config_file(), request)
|
||||||
};
|
};
|
||||||
client.write_all(format!("{reply}\n").as_bytes()).await?;
|
client.write_all(format!("{reply}\n").as_bytes()).await?;
|
||||||
// Closing before the tray hangs up could discard the reply.
|
// Closing before the tray hangs up could discard the reply.
|
||||||
let _ = tokio::time::timeout(Duration::from_secs(5), client.read(&mut [0])).await;
|
let _ = tokio::time::timeout(Duration::from_secs(5), client.read(&mut [0])).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn client_is_tray(client: &NamedPipeServer) -> Result<bool, BoxError> {
|
||||||
|
let mut id = 0;
|
||||||
|
unsafe { GetNamedPipeClientProcessId(HANDLE(client.as_raw_handle()), &mut id)? };
|
||||||
|
let process = unsafe { Owned::new(OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, id)?) };
|
||||||
|
let mut path = [0u16; 1024];
|
||||||
|
let mut len = path.len() as u32;
|
||||||
|
let flags = PROCESS_NAME_WIN32;
|
||||||
|
unsafe { QueryFullProcessImageNameW(*process, flags, PWSTR(path.as_mut_ptr()), &mut len)? };
|
||||||
|
let path = String::from_utf16(&path[..len as usize])?;
|
||||||
|
let tray = std::env::current_exe()?.with_file_name("firewall-tray.exe");
|
||||||
|
// Windows paths ignore case.
|
||||||
|
Ok(path.to_lowercase() == tray.to_string_lossy().to_lowercase())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Elevated, as only an elevated token has the Administrators group enabled.
|
||||||
|
fn client_is_admin(client: &NamedPipeServer) -> windows::core::Result<bool> {
|
||||||
|
let mut sid = [0u32; SECURITY_MAX_SID_SIZE as usize / 4];
|
||||||
|
let admins = PSID(sid.as_mut_ptr().cast());
|
||||||
|
let mut size = SECURITY_MAX_SID_SIZE;
|
||||||
|
unsafe { CreateWellKnownSid(WinBuiltinAdministratorsSid, None, Some(admins), &mut size)? };
|
||||||
|
unsafe { ImpersonateNamedPipeClient(HANDLE(client.as_raw_handle()))? };
|
||||||
|
let mut member = false.into();
|
||||||
|
let checked = unsafe { CheckTokenMembership(None, admins, &mut member) };
|
||||||
|
// Carrying on as the client would be worse than stopping.
|
||||||
|
if unsafe { RevertToSelf() }.is_err() {
|
||||||
|
std::process::abort();
|
||||||
|
}
|
||||||
|
checked?;
|
||||||
|
Ok(member.as_bool())
|
||||||
|
}
|
||||||
@@ -155,8 +155,9 @@ async fn start_screening(status: &ServiceStatusHandle) -> Result<Vec<&'static st
|
|||||||
let crl = HttpServer::auto(exec.clone()).service(crl);
|
let crl = HttpServer::auto(exec.clone()).service(crl);
|
||||||
tokio::spawn(listen_on(CRL_ADDRESS, 80, &exec)?.serve(crl));
|
tokio::spawn(listen_on(CRL_ADDRESS, 80, &exec)?.serve(crl));
|
||||||
|
|
||||||
tokio::spawn(async {
|
let tray_firewall = Arc::clone(&firewall);
|
||||||
if let Err(error) = pipe::serve().await {
|
tokio::spawn(async move {
|
||||||
|
if let Err(error) = pipe::serve(tray_firewall).await {
|
||||||
tracing::error!(%error, "the tray cannot reach the service");
|
tracing::error!(%error, "the tray cannot reach the service");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+200
-12
@@ -2,16 +2,19 @@
|
|||||||
|
|
||||||
use super::pipe::PIPE;
|
use super::pipe::PIPE;
|
||||||
use super::*;
|
use super::*;
|
||||||
use muda::{Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu};
|
use muda::{IsMenuItem, Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::io::{BufRead, BufReader, Cursor, Write};
|
use std::io::{BufRead, BufReader, Cursor, Write};
|
||||||
use std::os::windows::io::AsRawHandle;
|
use std::os::windows::io::AsRawHandle;
|
||||||
|
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tray_icon::{Icon, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent};
|
use tray_icon::{Icon, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent};
|
||||||
use windows::core::{w, PCWSTR};
|
use windows::core::{w, HRESULT, PCWSTR};
|
||||||
use windows::Win32::Foundation::*;
|
use windows::Win32::Foundation::*;
|
||||||
|
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||||
use windows::Win32::System::Pipes::GetNamedPipeServerProcessId;
|
use windows::Win32::System::Pipes::GetNamedPipeServerProcessId;
|
||||||
use windows::Win32::System::Threading::CreateMutexW;
|
use windows::Win32::System::Threading::*;
|
||||||
|
use windows::Win32::UI::Controls::*;
|
||||||
use windows::Win32::UI::Shell::*;
|
use windows::Win32::UI::Shell::*;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::*;
|
use windows::Win32::UI::WindowsAndMessaging::*;
|
||||||
use windows_registry::CURRENT_USER;
|
use windows_registry::CURRENT_USER;
|
||||||
@@ -22,6 +25,9 @@ const MENU_PAUSE: &str = "pause";
|
|||||||
const MENU_RESTART: &str = "restart";
|
const MENU_RESTART: &str = "restart";
|
||||||
const MENU_LOG: &str = "log";
|
const MENU_LOG: &str = "log";
|
||||||
const MENU_QUIT: &str = "quit";
|
const MENU_QUIT: &str = "quit";
|
||||||
|
/// Also the service's requests, and `allow` the elevated `firewall-tray` command.
|
||||||
|
const ALLOW: &str = "allow";
|
||||||
|
const ALLOW_ONCE: &str = "allow-once";
|
||||||
const LOGO: &[u8] = include_bytes!("tray.png");
|
const LOGO: &[u8] = include_bytes!("tray.png");
|
||||||
/// Where blocked packages are listed, below Open logs.
|
/// Where blocked packages are listed, below Open logs.
|
||||||
const BLOCKED_AT: usize = 5;
|
const BLOCKED_AT: usize = 5;
|
||||||
@@ -29,6 +35,19 @@ const BLOCKED_KEPT: usize = 10;
|
|||||||
|
|
||||||
/// Runs until Quit, once per session.
|
/// Runs until Quit, once per session.
|
||||||
pub fn run() -> Result<(), BoxError> {
|
pub fn run() -> Result<(), BoxError> {
|
||||||
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
|
if let [verb, id, label @ ..] = &args[..] {
|
||||||
|
if verb == ALLOW {
|
||||||
|
// Run by `run_elevated`, so the service can tell an admin asked.
|
||||||
|
let trusted = ask_service(&format!("{ALLOW} {id}"))
|
||||||
|
.and_then(|reply| answered(&format!("Trusted {}", label.join(" ")), &reply));
|
||||||
|
if let Err(error) = trusted {
|
||||||
|
show_error(error);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
// Held until we exit.
|
// Held until we exit.
|
||||||
let _mutex = unsafe { CreateMutexW(None, false, w!(r"Local\StrixFirewallTray"))? };
|
let _mutex = unsafe { CreateMutexW(None, false, w!(r"Local\StrixFirewallTray"))? };
|
||||||
if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
|
if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
|
||||||
@@ -62,9 +81,11 @@ pub fn run() -> Result<(), BoxError> {
|
|||||||
menu,
|
menu,
|
||||||
blocked_separator,
|
blocked_separator,
|
||||||
blocked: Vec::new(),
|
blocked: Vec::new(),
|
||||||
|
allowed: channel(),
|
||||||
};
|
};
|
||||||
tray.update();
|
tray.update();
|
||||||
unsafe { SetTimer(None, 0, 2000, None) };
|
// The service holds a new package's download only while it is asked this often.
|
||||||
|
unsafe { SetTimer(None, 0, 500, None) };
|
||||||
let mut message = MSG::default();
|
let mut message = MSG::default();
|
||||||
while unsafe { GetMessageW(&mut message, None, 0, 0) }.0 > 0 {
|
while unsafe { GetMessageW(&mut message, None, 0, 0) }.0 > 0 {
|
||||||
unsafe { DispatchMessageW(&message) };
|
unsafe { DispatchMessageW(&message) };
|
||||||
@@ -112,10 +133,13 @@ struct Tray {
|
|||||||
blocked_separator: PredefinedMenuItem,
|
blocked_separator: PredefinedMenuItem,
|
||||||
/// The most recent, as the menu lists them.
|
/// The most recent, as the menu lists them.
|
||||||
blocked: Vec<BlockedPackage>,
|
blocked: Vec<BlockedPackage>,
|
||||||
|
/// Allowed from another thread, so no longer listed.
|
||||||
|
allowed: (Sender<BlockedPackage>, Receiver<BlockedPackage>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Deserialize)]
|
#[derive(Clone, PartialEq, Deserialize)]
|
||||||
struct BlockedPackage {
|
struct BlockedPackage {
|
||||||
|
id: u64,
|
||||||
eco: String,
|
eco: String,
|
||||||
name: String,
|
name: String,
|
||||||
version: Option<String>,
|
version: Option<String>,
|
||||||
@@ -136,6 +160,11 @@ impl BlockedPackage {
|
|||||||
fn malicious(&self) -> bool {
|
fn malicious(&self) -> bool {
|
||||||
self.filter != "age"
|
self.filter != "age"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Malware is never allowed, and a new package only by version.
|
||||||
|
fn trustable(&self) -> bool {
|
||||||
|
!self.malicious() && self.version.is_some()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tray {
|
impl Tray {
|
||||||
@@ -161,6 +190,11 @@ impl Tray {
|
|||||||
|
|
||||||
/// The service hands each block over once, so whichever tray asks first shows it.
|
/// The service hands each block over once, so whichever tray asks first shows it.
|
||||||
fn collect_blocks(&mut self) {
|
fn collect_blocks(&mut self) {
|
||||||
|
let allowed: Vec<_> = self.allowed.1.try_iter().collect();
|
||||||
|
if !allowed.is_empty() {
|
||||||
|
self.blocked.retain(|blocked| !allowed.contains(blocked));
|
||||||
|
self.list_blocked();
|
||||||
|
}
|
||||||
if self.state != Some(ServiceState::Running) {
|
if self.state != Some(ServiceState::Running) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -184,7 +218,14 @@ impl Tray {
|
|||||||
self.blocked.drain(..excess);
|
self.blocked.drain(..excess);
|
||||||
self.list_blocked();
|
self.list_blocked();
|
||||||
// Off the UI thread, so the menu keeps working while they are open.
|
// Off the UI thread, so the menu keeps working while they are open.
|
||||||
std::thread::spawn(move || new.iter().for_each(alert));
|
let allowed = self.allowed.0.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
for package in new {
|
||||||
|
if let Some(trust) = alert(&package) {
|
||||||
|
allow(package, trust, &allowed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_blocked(&self) {
|
fn list_blocked(&self) {
|
||||||
@@ -204,14 +245,31 @@ impl Tray {
|
|||||||
package.reason.clone()
|
package.reason.clone()
|
||||||
};
|
};
|
||||||
let reason = MenuItem::new(reason, false, None);
|
let reason = MenuItem::new(reason, false, None);
|
||||||
|
let trustable = package.trustable();
|
||||||
|
let once = MenuItem::with_id(format!("{ALLOW_ONCE}:{i}"), "Allow Once", trustable, None);
|
||||||
|
let always = MenuItem::with_id(format!("{ALLOW}:{i}"), "Trust this version", trustable, None);
|
||||||
|
let items: &[&dyn IsMenuItem] = if package.malicious() {
|
||||||
|
&[&reason]
|
||||||
|
} else {
|
||||||
|
&[&reason, &once, &always]
|
||||||
|
};
|
||||||
let label = format!("{verb} {}", package.label());
|
let label = format!("{verb} {}", package.label());
|
||||||
if let Ok(item) = Submenu::with_items(label, true, &[&reason]) {
|
if let Ok(item) = Submenu::with_items(label, true, items) {
|
||||||
let _ = self.menu.insert(&item, BLOCKED_AT + 1 + i);
|
let _ = self.menu.insert(&item, BLOCKED_AT + 1 + i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_menu(&self, id: &str) {
|
fn on_menu(&self, id: &str) {
|
||||||
|
if let Some((verb @ (ALLOW | ALLOW_ONCE), i)) = id.split_once(':') {
|
||||||
|
let trust = verb == ALLOW;
|
||||||
|
let package = i.parse().ok().and_then(|i: usize| self.blocked.get(i));
|
||||||
|
if let Some(package) = package.cloned() {
|
||||||
|
let allowed = self.allowed.0.clone();
|
||||||
|
std::thread::spawn(move || allow(package, trust, &allowed));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
let state = self.state;
|
let state = self.state;
|
||||||
let screening = state == Some(ServiceState::Running);
|
let screening = state == Some(ServiceState::Running);
|
||||||
let action: fn() -> Result<(), BoxError> = match id {
|
let action: fn() -> Result<(), BoxError> = match id {
|
||||||
@@ -237,19 +295,149 @@ impl Tray {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn alert(package: &BlockedPackage) {
|
const ALLOW_ONCE_BUTTON: i32 = 100;
|
||||||
|
const TRUST_BUTTON: i32 = 101;
|
||||||
|
/// `firewall.rc`'s icon, embedded by `build.rs`.
|
||||||
|
const STRIX_ICON: PCWSTR = PCWSTR(1 as _);
|
||||||
|
|
||||||
|
/// `Some(true)` if the user chose to trust it, `Some(false)` to allow it once.
|
||||||
|
/// A task dialog, as a message box can't have both buttons.
|
||||||
|
fn alert(package: &BlockedPackage) -> Option<bool> {
|
||||||
let text = format!("{} {}.", package.label(), package.reason);
|
let text = format!("{} {}.", package.label(), package.reason);
|
||||||
let (title, text, style) = if package.malicious() {
|
let (title, text, icon) = if package.malicious() {
|
||||||
let title = format!("Blocked malware: {}", package.label());
|
let title = format!("Blocked malware: {}", package.label());
|
||||||
let text = text
|
let text = text
|
||||||
+ "\n\nIt was not installed. Don't install it from another source. If you didn't expect this "
|
+ "\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.";
|
+ "package, a dependency may be compromised; tell your security team.";
|
||||||
(title, text, MB_ICONERROR)
|
(title, text, TD_ERROR_ICON)
|
||||||
} else {
|
} else {
|
||||||
("Held back a new package".to_owned(), text, MB_ICONINFORMATION)
|
("Held back a new package".to_owned(), text, STRIX_ICON)
|
||||||
};
|
};
|
||||||
let (title, text) = (HSTRING::from(title), HSTRING::from(text));
|
let button = |id, text| TASKDIALOG_BUTTON { nButtonID: id, pszButtonText: text };
|
||||||
unsafe { MessageBoxW(None, &text, &title, style | MB_SETFOREGROUND) };
|
let allow = [button(ALLOW_ONCE_BUTTON, w!("Allow Once")), button(TRUST_BUTTON, w!("Trust this version"))];
|
||||||
|
let buttons: &[_] = if package.trustable() { &allow } else { &[] };
|
||||||
|
let (title, text) = (HSTRING::from(title), HSTRING::from(linked(&text)));
|
||||||
|
let dialog = TASKDIALOGCONFIG {
|
||||||
|
cbSize: size_of::<TASKDIALOGCONFIG>() as u32,
|
||||||
|
dwFlags: TDF_ENABLE_HYPERLINKS,
|
||||||
|
// Where `STRIX_ICON` is loaded from; the `TD_` icons are Windows'.
|
||||||
|
hInstance: unsafe { GetModuleHandleW(None) }.ok()?.into(),
|
||||||
|
dwCommonButtons: TDCBF_CLOSE_BUTTON,
|
||||||
|
pszWindowTitle: w!("Strix Firewall"),
|
||||||
|
Anonymous1: TASKDIALOGCONFIG_0 { pszMainIcon: icon },
|
||||||
|
pszMainInstruction: PCWSTR(title.as_ptr()),
|
||||||
|
pszContent: PCWSTR(text.as_ptr()),
|
||||||
|
cButtons: buttons.len() as u32,
|
||||||
|
pButtons: buttons.as_ptr(),
|
||||||
|
pfCallback: Some(on_dialog),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut chosen = 0;
|
||||||
|
unsafe { TaskDialogIndirect(&dialog, Some(&raw mut chosen), None, None) }.ok()?;
|
||||||
|
match chosen {
|
||||||
|
ALLOW_ONCE_BUTTON => Some(false),
|
||||||
|
TRUST_BUTTON => Some(true),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asks for the foreground, as `MB_SETFOREGROUND` did; Windows may still leave it behind.
|
||||||
|
/// Opens the links `linked` made, in the browser.
|
||||||
|
unsafe extern "system" fn on_dialog(
|
||||||
|
window: HWND,
|
||||||
|
event: TASKDIALOG_NOTIFICATIONS,
|
||||||
|
_: WPARAM,
|
||||||
|
link: LPARAM,
|
||||||
|
_: isize,
|
||||||
|
) -> HRESULT {
|
||||||
|
if event == TDN_CREATED {
|
||||||
|
let _ = unsafe { SetForegroundWindow(window) };
|
||||||
|
} else if event == TDN_HYPERLINK_CLICKED {
|
||||||
|
let link = PCWSTR(link.0 as *const u16);
|
||||||
|
// Only web pages, whatever a block's reason contains.
|
||||||
|
let web = unsafe { link.to_string() }
|
||||||
|
.is_ok_and(|link| link.starts_with("https://") || link.starts_with("http://"));
|
||||||
|
if web {
|
||||||
|
let none = PCWSTR::null();
|
||||||
|
unsafe { ShellExecuteW(None, w!("open"), link, none, none, SW_SHOWNORMAL) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
S_OK
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A task dialog's markup for the text, with each URL a link.
|
||||||
|
fn linked(text: &str) -> String {
|
||||||
|
let mut marked = String::new();
|
||||||
|
let mut rest = text;
|
||||||
|
while let Some(start) = ["https://", "http://"].iter().filter_map(|scheme| rest.find(scheme)).min() {
|
||||||
|
let end = rest[start..].find(|c: char| c.is_whitespace() || c == ',' || c == ')');
|
||||||
|
let url = rest[start..end.map_or(rest.len(), |end| start + end)].trim_end_matches('.');
|
||||||
|
marked += &rest[..start];
|
||||||
|
marked += &format!("<a href=\"{url}\">{url}</a>");
|
||||||
|
rest = &rest[start + url.len()..];
|
||||||
|
}
|
||||||
|
marked + rest
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trusting writes the config for every user, so it goes through a UAC prompt.
|
||||||
|
/// The elevated process shows its own errors and late replies.
|
||||||
|
fn allow(package: BlockedPackage, trust: bool, allowed: &Sender<BlockedPackage>) {
|
||||||
|
let allowed_it = if trust {
|
||||||
|
run_elevated(&[ALLOW, &package.id.to_string(), &package.label()])
|
||||||
|
.map(|code| code == Some(0))
|
||||||
|
} else {
|
||||||
|
let title = format!("Allowed {} for the next minute", package.label());
|
||||||
|
ask_service(&format!("{ALLOW_ONCE} {}", package.id))
|
||||||
|
.and_then(|reply| answered(&title, &reply))
|
||||||
|
.map(|()| true)
|
||||||
|
};
|
||||||
|
match allowed_it {
|
||||||
|
Ok(true) => {
|
||||||
|
let _ = allowed.send(package);
|
||||||
|
}
|
||||||
|
Ok(false) => {}
|
||||||
|
Err(error) => show_error(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tells the user, under `title`, when the install had already stopped waiting.
|
||||||
|
fn answered(title: &str, reply: &str) -> Result<(), BoxError> {
|
||||||
|
match reply {
|
||||||
|
"ok" => Ok(()),
|
||||||
|
"late" => {
|
||||||
|
let text = "The install had already stopped waiting for it. Run it again to finish.";
|
||||||
|
let (title, text) = (HSTRING::from(title), HSTRING::from(text));
|
||||||
|
unsafe { MessageBoxW(None, &text, &title, MB_ICONINFORMATION | MB_SETFOREGROUND) };
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
other => Err(other.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This program as an admin, waiting for its exit code; `None` if the user said no.
|
||||||
|
fn run_elevated(args: &[&str]) -> Result<Option<u32>, BoxError> {
|
||||||
|
let exe = HSTRING::from(std::env::current_exe()?.as_path());
|
||||||
|
let parameters = HSTRING::from(args.join(" "));
|
||||||
|
let mut info = SHELLEXECUTEINFOW {
|
||||||
|
cbSize: size_of::<SHELLEXECUTEINFOW>() as u32,
|
||||||
|
fMask: SEE_MASK_NOCLOSEPROCESS,
|
||||||
|
lpVerb: w!("runas"),
|
||||||
|
lpFile: PCWSTR(exe.as_ptr()),
|
||||||
|
lpParameters: PCWSTR(parameters.as_ptr()),
|
||||||
|
nShow: SW_HIDE.0,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
if let Err(error) = unsafe { ShellExecuteExW(&mut info) } {
|
||||||
|
if error.code() == ERROR_CANCELLED.to_hresult() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
return Err(error.into());
|
||||||
|
}
|
||||||
|
let process = unsafe { Owned::new(info.hProcess) };
|
||||||
|
unsafe { WaitForSingleObject(*process, INFINITE) };
|
||||||
|
let mut code = 1;
|
||||||
|
unsafe { GetExitCodeProcess(*process, &mut code)? };
|
||||||
|
Ok(Some(code))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One request per connection, as the service answers.
|
/// One request per connection, as the service answers.
|
||||||
|
|||||||
Reference in new issue
Block a user