Compare commits

...
Author SHA1 Message Date
Ian 10fdd0f272 Install machine-wide screening on Windows with an MSI
The MSI installs both binaries, registers and starts the service with a
DACL that lets interactive users start, stop and pause it, restarts it on
failure, and sets NODE_EXTRA_CA_CERTS and UV_SYSTEM_CERTS. Removing it runs
`firewall-service.exe uninstall` once the service is deleted, which clears
the hosts entries and removes the CA, its TPM key and the data folder but
the config.
2026-10-22 15:23:40 -04:00
8 changed files with 258 additions and 6 deletions

No files matched your search

+7
View File
@@ -3,6 +3,7 @@ name = "strix-firewall"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
authors = ["Strix"]
description = "Screens package downloads for known malware before they reach your machine"
repository = "https://github.com/usestrix/firewall"
homepage = "https://github.com/usestrix/firewall"
@@ -18,6 +19,12 @@ x86_64-apple-darwin = ["firewall"]
aarch64-unknown-linux-gnu = ["firewall"]
x86_64-unknown-linux-gnu = ["firewall"]
[package.metadata.wix]
upgrade-guid = "D944C2F6-BF55-4B21-A60E-1BBDB1A66293"
path-guid = "033B1050-23D0-4F5A-A0D3-F4757B4B67E0"
license = false
eula = false
[lib]
name = "firewall"
# The macOS extension links a staticlib, built on demand:
+16 -5
View File
@@ -2,14 +2,25 @@
#[cfg(windows)]
fn main() {
if let Err(e) = firewall::os::windows::run_service() {
tracing::error!("could not run the service: {e}");
firewall::telemetry::error("service");
firewall::telemetry::flush();
std::process::exit(1)
// Run by the MSI as it removes the firewall.
if std::env::args().nth(1).as_deref() == Some("uninstall") {
firewall::init_logging("info");
if let Err(e) = firewall::os::windows::uninstall() {
fail("uninstall", &format!("could not uninstall: {e}"))
}
} else if let Err(e) = firewall::os::windows::run_service() {
fail("service", &format!("could not run the service: {e}"))
}
}
#[cfg(windows)]
fn fail(kind: &'static str, message: &str) -> ! {
tracing::error!("{message}");
firewall::telemetry::error(kind);
firewall::telemetry::flush();
std::process::exit(1)
}
/// The service is Windows-only.
#[cfg(not(windows))]
fn main() {}
+15
View File
@@ -7,9 +7,11 @@ use schannel::cert_context::CertContext;
use schannel::cert_store::{CertAdd, CertStore};
use std::io;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use tracing::warn;
use windows::Win32::Security::Authorization::{GetNamedSecurityInfoW, SE_FILE_OBJECT};
use windows::Win32::Security::*;
use windows_registry::Type;
/// SYSTEM and admins write, users only read, so no one else can plant a CA for node.
const DATA_DIR_ACL: &str = concat!(
@@ -20,6 +22,9 @@ const DATA_DIR_ACL: &str = concat!(
"(A;OICI;FRFX;;;BU)", // allow Users read and execute only
);
/// 1970 in FILETIME, which counts 100 ns from 1601.
const FILETIME_UNIX_EPOCH: u128 = 116_444_736_000_000_000;
/// Trusted machine-wide so restarts keep it; replaced if the hosts change.
pub fn load_or_create_ca(
hosts: &[&str],
@@ -34,6 +39,16 @@ pub fn load_or_create_ca(
LOCAL_MACHINE
.create(REGISTRY_KEY)?
.set_string("ca", ca.to_blob(hosts))?;
// Windows discards CRLs it cached before now, in case an old CA's is cached at our URL.
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos() / 100
+ FILETIME_UNIX_EPOCH;
let chain = r"SOFTWARE\Microsoft\Cryptography\OID\EncodingType 0\CertDllCreateCertificateChainEngine\Config";
let resync = (now as u64).to_le_bytes();
LOCAL_MACHINE.create(chain)?.set_bytes(
"ChainCacheResyncFiletime",
Type::Bytes,
&resync,
)?;
ca
}
};
+2
View File
@@ -4,9 +4,11 @@
mod ca;
mod service;
mod tpm;
mod uninstall;
pub use service::run as run_service;
pub use tpm::CaKey;
pub use uninstall::uninstall;
use rama::error::BoxError;
use std::net::Ipv4Addr;
+7
View File
@@ -27,6 +27,13 @@ impl CaKey {
}
}
pub(super) fn delete_machine_key() -> Result<(), BoxError> {
if let Some(key) = open_key(KEY_NAME, NCRYPT_MACHINE_KEY_FLAG)? {
key.delete_from_tpm()?;
}
Ok(())
}
/// A per-user key under a random name, which needs no elevation; `delete_from_tpm` it after.
pub fn create_test_key() -> WinResult<Self> {
let name = format!("Strix Firewall test {:016x}", rand::random::<u64>());
+41
View File
@@ -0,0 +1,41 @@
use super::ca::remove_our_cas_except;
use super::tpm::CaKey;
use super::*;
use std::io::ErrorKind;
use std::path::Path;
use tracing::info;
/// Run by the MSI after it deletes the service, which would restore the hosts entries. Does
/// nothing for what is already gone.
pub fn uninstall() -> Result<(), BoxError> {
write_hosts_entries(&[])?;
remove_our_cas_except(None)?;
none_if_missing(LOCAL_MACHINE.remove_tree(REGISTRY_KEY))?;
CaKey::delete_machine_key()?;
let entries = match std::fs::read_dir(data_dir()) {
Err(error) if error.kind() == ErrorKind::NotFound => Vec::new(),
entries => entries?.collect::<Result<Vec<_>, _>>()?,
};
for path in entries.into_iter().map(|entry| entry.path()) {
// The admin's config stays for a reinstall, as on macOS.
if path != config_file() {
remove_path(&path)?;
}
}
// Fails while the config is in it.
let _ = std::fs::remove_dir(data_dir());
info!("removed the CA, its key and the data folder");
Ok(())
}
/// A file or a folder, if it is there.
fn remove_path(path: &Path) -> std::io::Result<()> {
let removed = match path.is_dir() {
true => std::fs::remove_dir_all(path),
false => std::fs::remove_file(path),
};
match removed {
Err(error) if error.kind() == ErrorKind::NotFound => Ok(()),
removed => removed,
}
}
+167
View File
@@ -0,0 +1,167 @@
<?xml version='1.0' encoding='windows-1252'?>
<!--
Copyright (C) 2017 Christopher R. Field.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<?if $(sys.BUILDARCH) = x64 or $(sys.BUILDARCH) = arm64 ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
<?else ?>
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
<?endif ?>
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
<Product
Id='*'
Name='strix-firewall'
UpgradeCode='D944C2F6-BF55-4B21-A60E-1BBDB1A66293'
Manufacturer='Strix'
Language='1033'
Codepage='1252'
Version='$(var.Version)'>
<Package Id='*'
Keywords='Installer'
Description='Screens package downloads for known malware before they reach your machine'
Manufacturer='Strix'
InstallerVersion='500'
Languages='1033'
Compressed='yes'
InstallScope='perMachine'
SummaryCodepage='1252'
/>
<MajorUpgrade
Schedule='afterInstallInitialize'
DowngradeErrorMessage='A newer version of [ProductName] is already installed. Setup will now exit.'/>
<Media Id='1' Cabinet='media1.cab' EmbedCab='yes' DiskPrompt='CD-ROM #1'/>
<Property Id='DiskPrompt' Value='strix-firewall Installation'/>
<Directory Id='TARGETDIR' Name='SourceDir'>
<Directory Id='$(var.PlatformProgramFilesFolder)' Name='PFiles'>
<Directory Id='APPLICATIONFOLDER' Name='Strix Firewall'>
<Component Id='Path' Guid='033B1050-23D0-4F5A-A0D3-F4757B4B67E0' KeyPath='yes'>
<Environment
Id='PATH'
Name='PATH'
Value='[APPLICATIONFOLDER]'
Permanent='no'
Part='last'
Action='set'
System='yes'/>
</Component>
<Component Id='binary0' Guid='*'>
<File
Id='exe0'
Name='firewall.exe'
DiskId='1'
Source='$(var.CargoTargetBinDir)\firewall.exe'
KeyPath='yes'/>
</Component>
<Component Id='binary1' Guid='*'>
<File
Id='exe1'
Name='firewall-service.exe'
DiskId='1'
Source='$(var.CargoTargetBinDir)\firewall-service.exe'
KeyPath='yes'/>
<ServiceInstall
Name='StrixFirewall'
DisplayName='Strix Firewall'
Description='Screens package downloads for known malware'
Type='ownProcess'
Start='auto'
ErrorControl='normal'
Vital='yes'>
<!-- The default, except that interactive users (IU) may also start (RP), stop (WP) and pause (DT) it, as the tray does. -->
<PermissionEx Sddl='D:(A;;CCLCSWRPWPDTLOCRRC;;;SY)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;BA)(A;;CCLCSWRPWPDTLOCRRC;;;IU)(A;;CCLCSWLOCRRC;;;SU)'/>
</ServiceInstall>
<!-- Waits, so a service that cannot start fails the install. -->
<ServiceControl Id='StrixFirewall' Name='StrixFirewall' Start='install' Stop='both' Remove='uninstall' Wait='yes'/>
<!-- For clients that read no trust store. SSL_CERT_FILE would replace OpenSSL's bundle. -->
<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>
</Directory>
</Directory>
</Directory>
<Feature
Id='Binaries'
Title='Application'
Description='Installs the firewall and its service.'
Level='1'
ConfigurableDirectory='APPLICATIONFOLDER'
AllowAdvertise='no'
Display='expand'
Absent='disallow'>
<ComponentRef Id='binary0'/>
<ComponentRef Id='binary1'/>
<Feature
Id='Environment'
Title='PATH Environment Variable'
Description='Add the install location of the [ProductName] executable to the PATH system environment variable. This allows the [ProductName] executable to be called from any location.'
Level='1'
Absent='allow'>
<ComponentRef Id='Path'/>
</Feature>
</Feature>
<!-- Removing the app must not leave the hosts file pointing at a service that is gone. -->
<CustomAction
Id='UninstallFirewall'
FileKey='exe1'
ExeCommand='uninstall'
Execute='deferred'
Impersonate='no'
Return='check'/>
<!--
Registry hosts lead nowhere while the service is down, so bring it back, also when it fails to
start. Through sc.exe, as MsiServiceConfigFailureActions cannot restart a service.
-->
<CustomAction
Id='SetFailureActions'
Directory='APPLICATIONFOLDER'
ExeCommand='"[System64Folder]sc.exe" failure StrixFirewall reset= 3600 actions= restart/5000/restart/5000/restart/5000'
Execute='deferred'
Impersonate='no'
Return='check'/>
<CustomAction
Id='SetFailureFlag'
Directory='APPLICATIONFOLDER'
ExeCommand='"[System64Folder]sc.exe" failureflag StrixFirewall 1'
Execute='deferred'
Impersonate='no'
Return='check'/>
<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>
</InstallExecuteSequence>
<SetProperty Id='ARPINSTALLLOCATION' Value='[APPLICATIONFOLDER]' After='CostFinalize'/>
<Property Id='ARPHELPLINK' Value='https://github.com/usestrix/firewall'/>
<UI>
<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>
</UI>
</Product>
</Wix>
+3 -1
View File
@@ -4,7 +4,9 @@ members = ["cargo:core"]
[dist]
cargo-dist-version = "0.32.0"
ci = "github"
installers = ["shell", "powershell", "homebrew"]
installers = ["shell", "powershell", "homebrew", "msi"]
# core/wix/main.wxs is edited to install the service.
allow-dirty = ["msi"]
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"]