Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c50344062 | ||
|
|
9c85bd84e5 | ||
|
|
78172519f0 | ||
|
|
902afd031d | ||
|
|
19d29f1ea6 | ||
|
|
7cafa16cc4 | ||
|
|
38d9a4500e | ||
|
|
d03c4cb6e3 | ||
|
|
8f20758ba0 | ||
|
|
577e0b7eb0 | ||
|
|
f94d3bf211 | ||
|
|
753e21dd19 |
No files matched your search
@@ -172,6 +172,17 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asynk-strim"
|
||||
version = "0.1.5"
|
||||
@@ -456,6 +467,16 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
@@ -496,6 +517,12 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "critical-section"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.17"
|
||||
@@ -997,6 +1024,78 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hickory-net"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c480823ed7c2c5d0f09c41020cb6b7c28029ce60ec42dc942158dcf22f8e0a4d"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"cfg-if",
|
||||
"data-encoding",
|
||||
"futures-channel",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"hickory-proto",
|
||||
"idna",
|
||||
"ipnet",
|
||||
"jni",
|
||||
"rand",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hickory-proto"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12b92608f679a6fa515dd1d15c1ff89443026e391200a2c840c7afcba482893d"
|
||||
dependencies = [
|
||||
"data-encoding",
|
||||
"idna",
|
||||
"ipnet",
|
||||
"jni",
|
||||
"once_cell",
|
||||
"prefix-trie",
|
||||
"rand",
|
||||
"ring",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hickory-resolver"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f3da5255c95d5a716857d54b5b8f4e8d67c3484d3beaaaae2ce25063b3ba981"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"hickory-net",
|
||||
"hickory-proto",
|
||||
"ipconfig",
|
||||
"ipnet",
|
||||
"jni",
|
||||
"moka",
|
||||
"ndk-context",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"rand",
|
||||
"resolv-conf",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"system-configuration",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.5.0"
|
||||
@@ -1158,6 +1257,19 @@ dependencies = [
|
||||
"hashbrown 0.17.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipconfig"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222"
|
||||
dependencies = [
|
||||
"socket2",
|
||||
"widestring",
|
||||
"windows-registry",
|
||||
"windows-result",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.12.2"
|
||||
@@ -1644,6 +1756,10 @@ name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
dependencies = [
|
||||
"critical-section",
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
@@ -1778,6 +1894,17 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "prefix-trie"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7"
|
||||
dependencies = [
|
||||
"either",
|
||||
"ipnet",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
@@ -2121,6 +2248,7 @@ dependencies = [
|
||||
"arc-swap",
|
||||
"base64",
|
||||
"bindgen",
|
||||
"hickory-resolver",
|
||||
"libc",
|
||||
"moka",
|
||||
"parking_lot",
|
||||
@@ -2419,7 +2547,7 @@ dependencies = [
|
||||
"base64",
|
||||
"bitflags 2.13.2",
|
||||
"const_format",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"csv",
|
||||
"idna",
|
||||
"ipnet",
|
||||
@@ -2751,6 +2879,12 @@ version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "resolv-conf"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -2886,7 +3020,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
@@ -3094,13 +3228,14 @@ name = "strix-firewall"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"directories",
|
||||
"percent-encoding",
|
||||
"rama",
|
||||
"rand",
|
||||
"rcgen",
|
||||
"regex",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"semver",
|
||||
@@ -3109,8 +3244,12 @@ dependencies = [
|
||||
"time",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"which",
|
||||
"windows",
|
||||
"windows-registry",
|
||||
"windows-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3181,6 +3320,27 @@ dependencies = [
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tagptr"
|
||||
version = "0.2.0"
|
||||
@@ -3269,6 +3429,12 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.1"
|
||||
@@ -3522,6 +3688,19 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
@@ -3669,6 +3848,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "widestring"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
|
||||
|
||||
[[package]]
|
||||
name = "wildcard"
|
||||
version = "0.3.0"
|
||||
@@ -3709,12 +3894,100 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
@@ -3724,6 +3997,26 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-service"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "857224b3b211c6f3616921f081ee54721ee3ad2ace2fac6a6337e032f7b4dcf2"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"widestring",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
@@ -3758,6 +4051,15 @@ dependencies = [
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
|
||||
@@ -10,6 +10,14 @@ 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.
|
||||
[package.metadata.dist.binaries]
|
||||
x86_64-pc-windows-msvc = ["firewall", "firewall-service"]
|
||||
aarch64-apple-darwin = ["firewall"]
|
||||
x86_64-apple-darwin = ["firewall"]
|
||||
aarch64-unknown-linux-gnu = ["firewall"]
|
||||
x86_64-unknown-linux-gnu = ["firewall"]
|
||||
|
||||
[lib]
|
||||
name = "firewall"
|
||||
# The macOS extension links a staticlib, built on demand:
|
||||
@@ -20,6 +28,10 @@ crate-type = ["lib"]
|
||||
name = "firewall"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "firewall-service"
|
||||
path = "src/bin/firewall-service.rs"
|
||||
|
||||
[[example]]
|
||||
name = "proxy-test"
|
||||
path = "tests/proxy-test/main.rs"
|
||||
@@ -52,6 +64,10 @@ path = "tests/age.rs"
|
||||
name = "mitm"
|
||||
path = "tests/mitm.rs"
|
||||
|
||||
[[test]]
|
||||
name = "tpm"
|
||||
path = "tests/tpm.rs"
|
||||
|
||||
[[test]]
|
||||
name = "managers"
|
||||
path = "tests/managers.rs"
|
||||
@@ -79,6 +95,23 @@ rama = { version = "0.4.0", features = ["net-apple-networkextension"] }
|
||||
security-framework = "3.7.0"
|
||||
security-framework-sys = "2.17.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
rama = { version = "0.4.0", features = ["dns-hickory"] }
|
||||
schannel = "0.1.29"
|
||||
tokio = { version = "1.53.1", features = ["net"] }
|
||||
tracing-appender = "0.2.5"
|
||||
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"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.53.1", features = ["macros", "rt-multi-thread"] }
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ async fn handle(req: Request, firewall: &Firewall) -> Result<Response, Infallibl
|
||||
};
|
||||
|
||||
let reason = policies::screen(&url, firewall).await;
|
||||
telemetry::screened(firewall, &url, reason.is_some());
|
||||
telemetry::screened(firewall, &url, reason.as_deref());
|
||||
if let Some(reason) = reason {
|
||||
warn!(method = %req.method(), %url, %reason, "blocked");
|
||||
return Ok(blocked(&reason));
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
//! Started by the Windows service manager. It logs to a file, having no console.
|
||||
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
|
||||
/// The service is Windows-only.
|
||||
#[cfg(not(windows))]
|
||||
fn main() {}
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::policies::registries::{Registry, BUILT_IN};
|
||||
use directories::ProjectDirs;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Map, Value};
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Error, ErrorKind};
|
||||
use std::path::PathBuf;
|
||||
@@ -68,18 +69,31 @@ impl Default for DenylistSource {
|
||||
}
|
||||
|
||||
impl FirewallConfig {
|
||||
/// A missing file means defaults; a bad one is an error, so the policy is never quietly relaxed.
|
||||
pub fn load() -> std::io::Result<Self> {
|
||||
let Some(path) = path() else {
|
||||
return Ok(Self::default());
|
||||
};
|
||||
let text = match std::fs::read_to_string(&path) {
|
||||
Ok(text) => text,
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(Self::default()),
|
||||
Err(e) => return Err(Error::new(e.kind(), format!("{}: {e}", path.display()))),
|
||||
};
|
||||
serde_json::from_str(&text)
|
||||
.map_err(|e| Error::new(ErrorKind::InvalidData, format!("{}: {e}", path.display())))
|
||||
let mut paths: Vec<PathBuf> = path().into_iter().collect();
|
||||
paths.extend(crate::os::system_config());
|
||||
Self::load_from(&paths)
|
||||
}
|
||||
|
||||
/// Later files win key by key, but allow lists add up.
|
||||
/// A bad file is an error, so the policy is never quietly relaxed.
|
||||
pub fn load_from(paths: &[PathBuf]) -> std::io::Result<Self> {
|
||||
let mut merged = Map::new();
|
||||
for path in paths {
|
||||
let text = match std::fs::read_to_string(path) {
|
||||
Ok(text) => text,
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => continue,
|
||||
Err(e) => return Err(Error::new(e.kind(), format!("{}: {e}", path.display()))),
|
||||
};
|
||||
let invalid = |e| Error::new(ErrorKind::InvalidData, format!("{}: {e}", path.display()));
|
||||
let layer: Value = serde_json::from_str(&text).map_err(invalid)?;
|
||||
FirewallConfig::deserialize(&layer).map_err(invalid)?;
|
||||
let Value::Object(layer) = layer else {
|
||||
unreachable!("a config parses only from an object");
|
||||
};
|
||||
merge(&mut merged, layer);
|
||||
}
|
||||
Ok(serde_json::from_value(Value::Object(merged)).expect("each layer parsed on its own"))
|
||||
}
|
||||
|
||||
pub fn local_url(&self, path: &str) -> String {
|
||||
@@ -103,3 +117,15 @@ pub fn path() -> Option<PathBuf> {
|
||||
let dirs = ProjectDirs::from("com", "usestrix", "firewall")?;
|
||||
Some(dirs.config_dir().join("config.json"))
|
||||
}
|
||||
|
||||
fn merge(base: &mut Map<String, Value>, top: Map<String, Value>) {
|
||||
for (key, value) in top {
|
||||
match (base.get_mut(&key), value) {
|
||||
(Some(Value::Object(base)), Value::Object(top)) => merge(base, top),
|
||||
(Some(Value::Array(base)), Value::Array(top)) => base.extend(top),
|
||||
(_, value) => {
|
||||
base.insert(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,7 @@ fn options() -> Command {
|
||||
}
|
||||
|
||||
fn serve() -> ! {
|
||||
telemetry::set_mode("proxy");
|
||||
let config = load();
|
||||
let port = config.port;
|
||||
print_setup(&config);
|
||||
@@ -83,6 +84,7 @@ fn uninstall() -> ! {
|
||||
}
|
||||
|
||||
fn run(invocation: Invocation) -> ! {
|
||||
telemetry::set_mode("wrapper");
|
||||
let command = &invocation.program;
|
||||
let port = match aliasing::spawn(0, load()) {
|
||||
Ok(port) => port,
|
||||
|
||||
@@ -8,7 +8,7 @@ use rcgen::{
|
||||
use std::sync::Arc;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
|
||||
/// Held in the Secure Enclave on macOS, in memory elsewhere.
|
||||
/// Held in the Secure Enclave on macOS, the TPM on Windows, in memory elsewhere.
|
||||
pub type CaSigner = Arc<dyn SigningKey + Send + Sync>;
|
||||
|
||||
pub struct CertificateAuthority {
|
||||
@@ -61,12 +61,14 @@ pub fn crl(hosts: &[&str], signer: &dyn SigningKey) -> Result<Vec<u8>, BoxError>
|
||||
}
|
||||
|
||||
pub const COMMON_NAME: &str = "Strix Firewall";
|
||||
/// With `COMMON_NAME`, how a trust store entry is recognized as ours.
|
||||
pub const ORGANIZATION: &str = "Strix";
|
||||
|
||||
/// Also used by the issuer, whose issuer name must match the CA's subject.
|
||||
pub fn params(hosts: &[&str]) -> CertificateParams {
|
||||
let mut name = DistinguishedName::new();
|
||||
name.push(DnType::CommonName, COMMON_NAME);
|
||||
name.push(DnType::OrganizationName, "Strix");
|
||||
name.push(DnType::OrganizationName, ORGANIZATION);
|
||||
|
||||
let mut params = CertificateParams::default();
|
||||
params.distinguished_name = name;
|
||||
|
||||
@@ -3,7 +3,7 @@ mod issuer;
|
||||
mod route;
|
||||
mod screen;
|
||||
|
||||
pub use ca::{crl, CaSigner, CertificateAuthority, COMMON_NAME};
|
||||
pub use ca::{crl, CaSigner, CertificateAuthority, COMMON_NAME, ORGANIZATION};
|
||||
|
||||
use crate::Firewall;
|
||||
use issuer::ConstrainedCertIssuer;
|
||||
|
||||
@@ -37,7 +37,7 @@ where
|
||||
async fn serve(&self, req: Request) -> Result<Response, Self::Error> {
|
||||
if let Some(url) = https_url(&req) {
|
||||
let reason = policies::screen(&url, &self.firewall).await;
|
||||
telemetry::screened(&self.firewall, &url, reason.is_some());
|
||||
telemetry::screened(&self.firewall, &url, reason.as_deref());
|
||||
if let Some(reason) = reason {
|
||||
warn!(%url, "blocked by firewall: {reason}");
|
||||
return Ok(blocked(&reason));
|
||||
|
||||
@@ -23,6 +23,7 @@ use rama::rt::Executor;
|
||||
use rama::telemetry::tracing::apple::oslog::{OsLogLayer, Privacy};
|
||||
use rama::{Layer, Service};
|
||||
use std::convert::Infallible;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
@@ -32,6 +33,7 @@ const KEYCHAIN_SERVICE: &str = BUNDLE_ID;
|
||||
const KEYCHAIN_ACCOUNT: &str = "ca";
|
||||
/// Must match `macos/LaunchAgents/com.usestrix.firewall.env.plist`, which points npm at it.
|
||||
const CA_FILE: &str = "/Library/Application Support/Firewall/ca.pem";
|
||||
const CONFIG_FILE: &str = "/Library/Application Support/Firewall/config.json";
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Handler {
|
||||
@@ -97,9 +99,14 @@ impl TransparentProxyHandlerFactory for Factory {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn system_config() -> Option<PathBuf> {
|
||||
Some(CONFIG_FILE.into())
|
||||
}
|
||||
|
||||
fn handler(exec: Executor) -> Result<Handler, BoxError> {
|
||||
telemetry::set_mode("extension");
|
||||
telemetry::heartbeat();
|
||||
let config = FirewallConfig::load().inspect_err(|_| telemetry::error("config"))?;
|
||||
let config = FirewallConfig::load_from(&[CONFIG_FILE.into()]).inspect_err(|_| telemetry::error("config"))?;
|
||||
let firewall = Arc::new(Firewall::new(config));
|
||||
let hosts = firewall.hosts();
|
||||
let key = enclave::EnclaveKey::load_or_create().inspect_err(|_| telemetry::error("enclave"))?;
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
#[cfg(windows)]
|
||||
pub mod windows;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use macos::uninstall;
|
||||
pub use macos::{system_config, uninstall};
|
||||
#[cfg(windows)]
|
||||
pub use windows::system_config;
|
||||
|
||||
/// Elsewhere nothing is installed system-wide.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn uninstall() -> Result<(), rama::error::BoxError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", windows)))]
|
||||
pub fn system_config() -> Option<std::path::PathBuf> {
|
||||
None
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
use super::*;
|
||||
use crate::mitm::{CertificateAuthority, COMMON_NAME, ORGANIZATION};
|
||||
use rama::tls::boring::core::nid::Nid;
|
||||
use rama::tls::boring::core::x509::X509;
|
||||
use rcgen::SigningKey;
|
||||
use schannel::cert_context::CertContext;
|
||||
use schannel::cert_store::{CertAdd, CertStore};
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use tracing::warn;
|
||||
use windows::Win32::Security::Authorization::{GetNamedSecurityInfoW, SE_FILE_OBJECT};
|
||||
use windows::Win32::Security::*;
|
||||
|
||||
/// SYSTEM and admins write, users only read, so no one else can plant a CA for node.
|
||||
const DATA_DIR_ACL: &str = concat!(
|
||||
"O:BA", // owner: Administrators
|
||||
"D:P", // protected: ignore ProgramData's rules, which let any user create files
|
||||
"(A;OICI;FA;;;SY)", // allow SYSTEM full control, inherited by files (OI) and subfolders (CI)
|
||||
"(A;OICI;FA;;;BA)", // allow Administrators full control
|
||||
"(A;OICI;FRFX;;;BU)", // allow Users read and execute only
|
||||
);
|
||||
|
||||
/// Trusted machine-wide so restarts keep it; replaced if the hosts change.
|
||||
pub fn load_or_create_ca(
|
||||
hosts: &[&str],
|
||||
signer: &dyn SigningKey,
|
||||
) -> Result<CertificateAuthority, BoxError> {
|
||||
let stored = read_registry_string(REGISTRY_KEY, "ca")?;
|
||||
let ca = match stored.and_then(|blob| CertificateAuthority::from_blob(&blob, hosts, signer)) {
|
||||
Some(ca) => ca,
|
||||
None => {
|
||||
warn!(?hosts, "minting a new CA");
|
||||
let ca = CertificateAuthority::generate(hosts, signer)?;
|
||||
LOCAL_MACHINE
|
||||
.create(REGISTRY_KEY)?
|
||||
.set_string("ca", ca.to_blob(hosts))?;
|
||||
ca
|
||||
}
|
||||
};
|
||||
// Idempotent, and the registry value can outlive the certificate.
|
||||
let der = ca.crt.to_der()?;
|
||||
let mut store = CertStore::open_local_machine("Root")?;
|
||||
store.add_cert(&CertContext::new(&der)?, CertAdd::ReplaceExisting)?;
|
||||
remove_our_cas_except(Some(&der))?;
|
||||
// For clients that read no trust store; screening still works for the rest if this fails.
|
||||
let exported = secure_data_dir().and_then(|dir| std::fs::write(dir.join("ca.pem"), &ca.pem));
|
||||
if let Err(error) = exported {
|
||||
warn!(%error, "could not export the CA");
|
||||
}
|
||||
Ok(ca)
|
||||
}
|
||||
|
||||
/// Created on first use, its owner and ACL 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)?,
|
||||
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::create_dir_all(&dir)?;
|
||||
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()? };
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
/// By SYSTEM or the Administrators group, as what an elevated process creates is.
|
||||
fn owned_by_admins(path: &Path) -> windows::core::Result<bool> {
|
||||
let (mut owner, mut descriptor) = (PSID::default(), PSECURITY_DESCRIPTOR::default());
|
||||
unsafe {
|
||||
GetNamedSecurityInfoW(
|
||||
&HSTRING::from(path),
|
||||
SE_FILE_OBJECT,
|
||||
OWNER_SECURITY_INFORMATION,
|
||||
Some(&mut owner),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
&mut descriptor,
|
||||
)
|
||||
.ok()?
|
||||
};
|
||||
let admins = [WinBuiltinAdministratorsSid, WinLocalSystemSid];
|
||||
let owned = admins
|
||||
.into_iter()
|
||||
.any(|sid| unsafe { IsWellKnownSid(owner, sid) }.as_bool());
|
||||
// `owner` points into the descriptor, so it is freed last.
|
||||
unsafe { LocalFree(Some(HLOCAL(descriptor.0))) };
|
||||
Ok(owned)
|
||||
}
|
||||
|
||||
/// Drops every CA of ours from LocalMachine\Root but `keep`.
|
||||
pub(super) fn remove_our_cas_except(keep: Option<&[u8]>) -> Result<(), BoxError> {
|
||||
for cert in CertStore::open_local_machine("Root")?.certs() {
|
||||
if keep != Some(cert.to_der()) && is_our_ca(cert.to_der()) {
|
||||
cert.delete()?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_our_ca(der: &[u8]) -> bool {
|
||||
let Ok(crt) = X509::from_der(der) else {
|
||||
return false;
|
||||
};
|
||||
let name = |nid| {
|
||||
let entry = crt.subject_name().entries_by_nid(nid).next()?;
|
||||
Some(entry.data().as_utf8().ok()?.to_string())
|
||||
};
|
||||
name(Nid::COMMONNAME).as_deref() == Some(COMMON_NAME)
|
||||
&& name(Nid::ORGANIZATIONNAME).as_deref() == Some(ORGANIZATION)
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
//! The hosts file sends each registry host to its own loopback address, where a
|
||||
//! LocalSystem service screens it as the macOS extension does.
|
||||
|
||||
mod ca;
|
||||
mod service;
|
||||
mod tpm;
|
||||
|
||||
pub use service::run as run_service;
|
||||
pub use tpm::CaKey;
|
||||
|
||||
use rama::error::BoxError;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::path::PathBuf;
|
||||
use windows::core::{Owned, GUID, HSTRING};
|
||||
use windows::Win32::Foundation::{LocalFree, ERROR_FILE_NOT_FOUND, HLOCAL};
|
||||
use windows::Win32::Security::Authorization::*;
|
||||
use windows::Win32::Security::PSECURITY_DESCRIPTOR;
|
||||
use windows::Win32::System::Com::CoTaskMemFree;
|
||||
use windows::Win32::System::Environment::ExpandEnvironmentStringsW;
|
||||
use windows::Win32::UI::Shell::*;
|
||||
use windows_registry::LOCAL_MACHINE;
|
||||
|
||||
const SERVICE_NAME: &str = "StrixFirewall";
|
||||
/// Admin-only, so it holds the CA as the System keychain does on macOS.
|
||||
const REGISTRY_KEY: &str = r"SOFTWARE\Strix Firewall";
|
||||
/// Serves the CRL our leaves point at; schannel fails closed without one.
|
||||
const CRL_ADDRESS: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 2);
|
||||
|
||||
/// Admin-only; holds the exported CA, the service log and the config.
|
||||
fn data_dir() -> PathBuf {
|
||||
known_folder(&FOLDERID_ProgramData).join("Strix Firewall")
|
||||
}
|
||||
|
||||
/// Admin-only, so users cannot relax what the service screens.
|
||||
fn config_file() -> PathBuf {
|
||||
data_dir().join("config.json")
|
||||
}
|
||||
|
||||
pub fn system_config() -> Option<PathBuf> {
|
||||
Some(config_file())
|
||||
}
|
||||
|
||||
/// In the folder Windows reads it from, normally %SystemRoot%\System32\drivers\etc.
|
||||
fn hosts_file() -> PathBuf {
|
||||
let key = r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters";
|
||||
let configured = LOCAL_MACHINE
|
||||
.open(key)
|
||||
.and_then(|key| key.get_hstring("DataBasePath"));
|
||||
let dir = match configured {
|
||||
Ok(dir) => expand_environment(&dir),
|
||||
Err(_) => known_folder(&FOLDERID_System).join(r"drivers\etc"),
|
||||
};
|
||||
dir.join("hosts")
|
||||
}
|
||||
|
||||
/// Windows always has its known folders.
|
||||
fn known_folder(id: &GUID) -> PathBuf {
|
||||
let path = unsafe { SHGetKnownFolderPath(id, KF_FLAG_DEFAULT, None) }.expect("a known folder");
|
||||
let dir = PathBuf::from(unsafe { path.to_hstring() }.to_os_string());
|
||||
unsafe { CoTaskMemFree(Some(path.0 as *const _)) };
|
||||
dir
|
||||
}
|
||||
|
||||
fn expand_environment(path: &HSTRING) -> PathBuf {
|
||||
let len = unsafe { ExpandEnvironmentStringsW(path, None) };
|
||||
let mut expanded = vec![0; len as usize];
|
||||
let len = unsafe { ExpandEnvironmentStringsW(path, Some(&mut expanded)) };
|
||||
// Less the terminating null.
|
||||
PathBuf::from(String::from_utf16_lossy(
|
||||
&expanded[..(len as usize).saturating_sub(1)],
|
||||
))
|
||||
}
|
||||
|
||||
/// From 127.0.0.3, leaving 127.0.0.1 to whatever else serves locally.
|
||||
fn loopback_addresses(hosts: &[&'static str]) -> Vec<(Ipv4Addr, &'static str)> {
|
||||
(3..)
|
||||
.zip(hosts)
|
||||
.map(|(i, host)| (Ipv4Addr::new(127, 0, 0, i), *host))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Points each host at its listener, replacing the lines we wrote before; with none, only removes
|
||||
/// them. Bytes, as the file needn't be UTF-8, and every other line is kept as it was.
|
||||
fn write_hosts_entries(hosts: &[&'static str]) -> std::io::Result<()> {
|
||||
const TAG: &str = " # Strix Firewall";
|
||||
let old = std::fs::read(hosts_file())?;
|
||||
let ours = |line: &&[u8]| line.trim_ascii_end().ends_with(TAG.as_bytes());
|
||||
let lines = old.split_inclusive(|&byte| byte == b'\n');
|
||||
let mut new: Vec<u8> = lines
|
||||
.filter(|line| !ours(line))
|
||||
.flatten()
|
||||
.copied()
|
||||
.collect();
|
||||
if !new.is_empty() && !new.ends_with(b"\n") {
|
||||
new.extend_from_slice(b"\r\n");
|
||||
}
|
||||
for (address, host) in loopback_addresses(hosts) {
|
||||
new.extend_from_slice(format!("{address} {host}{TAG}\r\n").as_bytes());
|
||||
}
|
||||
std::fs::write(hosts_file(), new)
|
||||
}
|
||||
|
||||
/// Parses `sddl`; freed on drop.
|
||||
fn security_descriptor(sddl: &str) -> windows::core::Result<Owned<HLOCAL>> {
|
||||
let mut descriptor = PSECURITY_DESCRIPTOR::default();
|
||||
unsafe {
|
||||
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
&HSTRING::from(sddl),
|
||||
SDDL_REVISION_1,
|
||||
&mut descriptor,
|
||||
None,
|
||||
)?
|
||||
};
|
||||
Ok(unsafe { Owned::new(HLOCAL(descriptor.0)) })
|
||||
}
|
||||
|
||||
fn read_registry_string(key: &str, value: &str) -> Result<Option<String>, BoxError> {
|
||||
none_if_missing(
|
||||
LOCAL_MACHINE
|
||||
.open(key)
|
||||
.and_then(|key| key.get_string(value)),
|
||||
)
|
||||
}
|
||||
|
||||
/// `None` for a missing key or value.
|
||||
fn none_if_missing<T>(result: windows_registry::Result<T>) -> Result<Option<T>, BoxError> {
|
||||
match result {
|
||||
Err(error) if error.code() == ERROR_FILE_NOT_FOUND.to_hresult() => Ok(None),
|
||||
result => Ok(Some(result?)),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
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 crate::config::FirewallConfig;
|
||||
use crate::http::{reply, text};
|
||||
use crate::{mitm, telemetry, Firewall};
|
||||
use rama::dns::client::hickory::resolver::config::ResolveHosts;
|
||||
use rama::dns::client::hickory::{default_resolver_opts, HickoryDnsResolver};
|
||||
use rama::dns::client::resolver::DnsAddressResolver;
|
||||
use rama::dns::client::try_init_global_dns_resolver;
|
||||
use rama::error::BoxError;
|
||||
use rama::http::server::HttpServer;
|
||||
use rama::http::{Request, Response};
|
||||
use rama::io::BridgeIo;
|
||||
use rama::layer::ConsumeErrLayer;
|
||||
use rama::net::address::Domain;
|
||||
use rama::rt::Executor;
|
||||
use rama::service::service_fn;
|
||||
use rama::tcp::server::TcpListener;
|
||||
use rama::tcp::TcpStream;
|
||||
use rama::{Layer, Service};
|
||||
use rcgen::SigningKey;
|
||||
use std::convert::Infallible;
|
||||
use std::ffi::OsString;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||
use windows_service::service::*;
|
||||
use windows_service::service_control_handler::{
|
||||
self, ServiceControlHandlerResult, ServiceStatusHandle,
|
||||
};
|
||||
use windows_service::{define_windows_service, service_dispatcher};
|
||||
|
||||
const CRL_PATH: &str = "/ca.crl";
|
||||
|
||||
define_windows_service!(ffi_service_main, service_main);
|
||||
|
||||
/// Called by the service control manager, never by hand.
|
||||
pub fn run() -> windows_service::Result<()> {
|
||||
service_dispatcher::start(SERVICE_NAME, ffi_service_main)
|
||||
}
|
||||
|
||||
/// stderr goes nowhere in a service, so events go to a file beside the CA.
|
||||
fn service_main(_arguments: Vec<OsString>) {
|
||||
let log = secure_data_dir().ok().and_then(|dir| {
|
||||
RollingFileAppender::builder()
|
||||
.rotation(Rotation::DAILY)
|
||||
.filename_prefix("firewall")
|
||||
.filename_suffix("log")
|
||||
.max_log_files(7)
|
||||
.build(dir)
|
||||
.ok()
|
||||
});
|
||||
if let Some(appender) = log {
|
||||
let filter = crate::log_filter("info");
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_env_filter(filter)
|
||||
.with_ansi(false)
|
||||
.with_writer(appender)
|
||||
.try_init();
|
||||
}
|
||||
std::panic::set_hook(Box::new(|panic| tracing::error!("{panic}")));
|
||||
if let Err(error) = run_until_stopped() {
|
||||
tracing::error!(%error, "the service stopped");
|
||||
}
|
||||
}
|
||||
|
||||
fn run_until_stopped() -> Result<(), BoxError> {
|
||||
let (control_sender, mut control_receiver) = tokio::sync::mpsc::unbounded_channel();
|
||||
let status = service_control_handler::register(SERVICE_NAME, move |control| match control {
|
||||
ServiceControl::Stop
|
||||
| ServiceControl::Shutdown
|
||||
| ServiceControl::Pause
|
||||
| ServiceControl::Continue => {
|
||||
let _ = control_sender.send(control);
|
||||
ServiceControlHandlerResult::NoError
|
||||
}
|
||||
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
|
||||
_ => ServiceControlHandlerResult::NotImplemented,
|
||||
})?;
|
||||
set_status(&status, ServiceState::StartPending, false)?;
|
||||
let served = crate::runtime()?.block_on(async {
|
||||
let hosts = match start_screening(&status).await {
|
||||
Ok(hosts) => hosts,
|
||||
Err(error) => {
|
||||
telemetry::send().await;
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
// Only pausing removes our hosts entries; a stop or crash keeps them, failing closed.
|
||||
while let Some(control) = control_receiver.recv().await {
|
||||
let (hosts, state) = match control {
|
||||
ServiceControl::Pause => (&[][..], ServiceState::Paused),
|
||||
ServiceControl::Continue => (&hosts[..], ServiceState::Running),
|
||||
_ => break,
|
||||
};
|
||||
match write_hosts_entries(hosts) {
|
||||
Ok(()) => set_status(&status, state, false)?,
|
||||
Err(error) => tracing::error!(%error, "could not rewrite the hosts file"),
|
||||
}
|
||||
}
|
||||
Ok::<_, BoxError>(())
|
||||
});
|
||||
// A failed stop is what makes the service manager restart it.
|
||||
set_status(&status, ServiceState::Stopped, served.is_err())?;
|
||||
served
|
||||
}
|
||||
|
||||
async fn start_screening(status: &ServiceStatusHandle) -> Result<Vec<&'static str>, BoxError> {
|
||||
telemetry::set_mode("service");
|
||||
telemetry::heartbeat();
|
||||
// Never the hosts file, which points back at us; the Firewall's lookups included.
|
||||
let mut options = default_resolver_opts();
|
||||
options.use_hosts_file = ResolveHosts::Never;
|
||||
let resolver = HickoryDnsResolver::try_new_system_with_options(options)?;
|
||||
let _ = try_init_global_dns_resolver(resolver.clone());
|
||||
let config =
|
||||
FirewallConfig::load_from(&[config_file()]).inspect_err(|_| telemetry::error("config"))?;
|
||||
let firewall = Arc::new(Firewall::new(config));
|
||||
let hosts = firewall.hosts();
|
||||
let key = CaKey::load_or_create().inspect_err(|_| telemetry::error("tpm"))?;
|
||||
let signer: mitm::CaSigner = Arc::new(key);
|
||||
let ca = load_or_create_ca(&hosts, &*signer).inspect_err(|_| telemetry::error("ca"))?;
|
||||
let exec = Executor::default();
|
||||
|
||||
// An empty CRL for the leaves to point at, signed fresh for each fetch.
|
||||
let crl_url = format!("http://{CRL_ADDRESS}{CRL_PATH}");
|
||||
let (crl_hosts, crl_signer) = (hosts.clone(), Arc::clone(&signer));
|
||||
let crl = service_fn(move |req: Request| {
|
||||
let response = if req.uri().request_target() == CRL_PATH {
|
||||
crl_response(&crl_hosts, &*crl_signer)
|
||||
} else {
|
||||
text(404, "No route.\n")
|
||||
};
|
||||
async move { Ok::<_, Infallible>(response) }
|
||||
});
|
||||
let crl = HttpServer::auto(exec.clone()).service(crl);
|
||||
tokio::spawn(listen_on(CRL_ADDRESS, 80, &exec)?.serve(crl));
|
||||
|
||||
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] {
|
||||
let upstream = RedirectedHost {
|
||||
host,
|
||||
port,
|
||||
resolver: resolver.clone(),
|
||||
service: service.clone(),
|
||||
};
|
||||
let upstream = ConsumeErrLayer::trace_as_debug().into_layer(upstream);
|
||||
tokio::spawn(listen_on(address, port, &exec)?.serve(upstream));
|
||||
}
|
||||
}
|
||||
// Rewritten on every start, so a restart picks up a changed host list.
|
||||
write_hosts_entries(&hosts)?;
|
||||
tracing::info!(?hosts, "screening redirected registry traffic");
|
||||
set_status(status, ServiceState::Running, false)?;
|
||||
Ok(hosts)
|
||||
}
|
||||
|
||||
fn crl_response(hosts: &[&str], signer: &dyn SigningKey) -> Response {
|
||||
match mitm::crl(hosts, signer) {
|
||||
Ok(der) => reply(200, "application/pkix-crl", der),
|
||||
Err(error) => text(503, &format!("{error}\n")),
|
||||
}
|
||||
}
|
||||
|
||||
fn listen_on(address: Ipv4Addr, port: u16, exec: &Executor) -> std::io::Result<TcpListener> {
|
||||
let bound = std::net::TcpListener::bind((address, port))?;
|
||||
bound.set_nonblocking(true)?;
|
||||
TcpListener::try_from_std_tcp_listener(bound, exec.clone())
|
||||
}
|
||||
|
||||
/// Each listener is bound to one host's address, which is how it knows where to connect.
|
||||
#[derive(Clone)]
|
||||
struct RedirectedHost {
|
||||
host: &'static str,
|
||||
port: u16,
|
||||
resolver: HickoryDnsResolver,
|
||||
service: mitm::MitmService,
|
||||
}
|
||||
|
||||
impl Service<TcpStream> for RedirectedHost {
|
||||
type Output = ();
|
||||
type Error = BoxError;
|
||||
|
||||
async fn serve(&self, ingress: TcpStream) -> Result<(), BoxError> {
|
||||
let address = self.resolve().await?;
|
||||
let egress = tokio::net::TcpStream::connect((address, self.port)).await?;
|
||||
let streams = BridgeIo(ingress, TcpStream::from(egress));
|
||||
self.service.serve(streams).await
|
||||
}
|
||||
}
|
||||
|
||||
impl RedirectedHost {
|
||||
/// IPv4 if it has one, else IPv6.
|
||||
async fn resolve(&self) -> Result<IpAddr, BoxError> {
|
||||
let domain = Domain::from_static(self.host);
|
||||
if let Some(Ok(v4)) = self.resolver.lookup_ipv4_first(domain.clone()).await {
|
||||
return Ok(v4.into());
|
||||
}
|
||||
match self.resolver.lookup_ipv6_first(domain).await {
|
||||
Some(v6) => Ok(v6?.into()),
|
||||
None => Err(format!("{} has no address", self.host).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_status(
|
||||
status: &ServiceStatusHandle,
|
||||
state: ServiceState,
|
||||
failed: bool,
|
||||
) -> windows_service::Result<()> {
|
||||
status.set_service_status(ServiceStatus {
|
||||
service_type: ServiceType::OWN_PROCESS,
|
||||
current_state: state,
|
||||
controls_accepted: match state {
|
||||
ServiceState::Running | ServiceState::Paused => {
|
||||
ServiceControlAccept::STOP
|
||||
| ServiceControlAccept::SHUTDOWN
|
||||
| ServiceControlAccept::PAUSE_CONTINUE
|
||||
}
|
||||
_ => ServiceControlAccept::empty(),
|
||||
},
|
||||
exit_code: if failed {
|
||||
ServiceExitCode::ServiceSpecific(1)
|
||||
} else {
|
||||
ServiceExitCode::Win32(0)
|
||||
},
|
||||
checkpoint: 0,
|
||||
wait_hint: Duration::from_secs(30),
|
||||
process_id: None,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
use rama::error::BoxError;
|
||||
use rama::tls::boring::core::bn::BigNum;
|
||||
use rama::tls::boring::core::ecdsa::EcdsaSig;
|
||||
use rama::tls::boring::core::sha::sha256;
|
||||
use rcgen::{Error, PublicKeyData, SignatureAlgorithm, SigningKey, PKCS_ECDSA_P256_SHA256};
|
||||
use windows::core::{Owned, Result as WinResult, HSTRING};
|
||||
use windows::Win32::Foundation::{NTE_BAD_KEY, NTE_BAD_KEYSET};
|
||||
use windows::Win32::Security::Cryptography::*;
|
||||
|
||||
const KEY_NAME: &str = "Strix Firewall CA";
|
||||
|
||||
/// A P-256 key held by the TPM. Signing asks the TPM to sign, so the private key never leaves it.
|
||||
pub struct CaKey {
|
||||
// Fields drop in order, so the key handle closes before its provider's.
|
||||
key_handle: Owned<NCRYPT_KEY_HANDLE>,
|
||||
_provider_handle: Owned<NCRYPT_PROV_HANDLE>,
|
||||
/// 0x04 || X || Y, the form rcgen writes into a certificate's SubjectPublicKeyInfo.
|
||||
public_point: Vec<u8>,
|
||||
}
|
||||
|
||||
impl CaKey {
|
||||
/// A machine key, so the service and an elevated installer share it.
|
||||
pub fn load_or_create() -> Result<Self, BoxError> {
|
||||
match open_key(KEY_NAME, NCRYPT_MACHINE_KEY_FLAG)? {
|
||||
Some(key) => Ok(key),
|
||||
None => Ok(create_key(KEY_NAME, NCRYPT_MACHINE_KEY_FLAG)?),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>());
|
||||
create_key(&name, NCRYPT_FLAGS(0))
|
||||
}
|
||||
|
||||
/// Deletes the key from the TPM, not just this handle to it.
|
||||
pub fn delete_from_tpm(mut self) -> WinResult<()> {
|
||||
// NCryptDeleteKey also frees the handle, so the Owned mustn't free it again.
|
||||
let key_handle = std::mem::take(&mut *self.key_handle);
|
||||
unsafe { NCryptDeleteKey(key_handle, 0) }
|
||||
}
|
||||
|
||||
/// Takes both handles and reads the key's public point once, for rcgen.
|
||||
fn from_handles(
|
||||
provider_handle: Owned<NCRYPT_PROV_HANDLE>,
|
||||
key_handle: Owned<NCRYPT_KEY_HANDLE>,
|
||||
) -> WinResult<Self> {
|
||||
// A BCRYPT_ECCKEY_BLOB header, then X and Y.
|
||||
const HEADER_LEN: usize = size_of::<BCRYPT_ECCKEY_BLOB>();
|
||||
let mut blob = [0u8; HEADER_LEN + 64];
|
||||
let mut blob_len = 0;
|
||||
unsafe {
|
||||
NCryptExportKey(
|
||||
*key_handle,
|
||||
None,
|
||||
BCRYPT_ECCPUBLIC_BLOB,
|
||||
None,
|
||||
Some(&mut blob),
|
||||
&mut blob_len,
|
||||
NCRYPT_FLAGS(0),
|
||||
)?
|
||||
};
|
||||
let (header, x_and_y) = blob.split_at(HEADER_LEN);
|
||||
let is_p256 = header[..4] == BCRYPT_ECDSA_PUBLIC_P256_MAGIC.to_le_bytes();
|
||||
if !is_p256 || blob_len as usize != blob.len() {
|
||||
return Err(windows::core::Error::new(NTE_BAD_KEY, "key is not an ECDSA P-256 key"));
|
||||
}
|
||||
let public_point = [&[0x04], x_and_y].concat();
|
||||
Ok(CaKey { key_handle, _provider_handle: provider_handle, public_point })
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicKeyData for CaKey {
|
||||
fn der_bytes(&self) -> &[u8] {
|
||||
&self.public_point
|
||||
}
|
||||
|
||||
fn algorithm(&self) -> &'static SignatureAlgorithm {
|
||||
&PKCS_ECDSA_P256_SHA256
|
||||
}
|
||||
}
|
||||
|
||||
impl SigningKey for CaKey {
|
||||
fn sign(&self, msg: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
let mut r_and_s = [0u8; 64];
|
||||
let mut signature_len = 0;
|
||||
let signed = unsafe {
|
||||
NCryptSignHash(
|
||||
*self.key_handle,
|
||||
None,
|
||||
&sha256(msg),
|
||||
Some(&mut r_and_s),
|
||||
&mut signature_len,
|
||||
NCRYPT_SILENT_FLAG,
|
||||
)
|
||||
};
|
||||
signed.map_err(|_| Error::RemoteKeyError)?;
|
||||
// NCrypt returns r || s, each half the length; X.509 wants DER SEQUENCE { r, s }.
|
||||
let (r, s) = r_and_s[..signature_len as usize].split_at(signature_len as usize / 2);
|
||||
let der = BigNum::from_slice(r)
|
||||
.and_then(|r| EcdsaSig::from_private_components(r, BigNum::from_slice(s)?))
|
||||
.and_then(|signature| signature.to_der());
|
||||
der.map_err(|_| Error::RemoteKeyError)
|
||||
}
|
||||
}
|
||||
|
||||
/// `None` if the TPM holds no key by that name.
|
||||
/// `scope` is `NCRYPT_MACHINE_KEY_FLAG`, or empty for the current user's keys.
|
||||
fn open_key(name: &str, scope: NCRYPT_FLAGS) -> WinResult<Option<CaKey>> {
|
||||
let provider_handle = open_tpm_provider()?;
|
||||
let mut key_handle = NCRYPT_KEY_HANDLE::default();
|
||||
let opened = unsafe {
|
||||
NCryptOpenKey(
|
||||
*provider_handle,
|
||||
&mut key_handle,
|
||||
&HSTRING::from(name),
|
||||
CERT_KEY_SPEC::default(), // legacy, unused by the TPM provider
|
||||
scope | NCRYPT_SILENT_FLAG,
|
||||
)
|
||||
};
|
||||
match opened {
|
||||
Err(error) if error.code() == NTE_BAD_KEYSET => Ok(None),
|
||||
Err(error) => Err(error),
|
||||
Ok(()) => CaKey::from_handles(provider_handle, unsafe { Owned::new(key_handle) }).map(Some),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates the key inside the TPM, which never lets it out.
|
||||
fn create_key(name: &str, scope: NCRYPT_FLAGS) -> WinResult<CaKey> {
|
||||
let provider_handle = open_tpm_provider()?;
|
||||
let mut key_handle = NCRYPT_KEY_HANDLE::default();
|
||||
unsafe {
|
||||
NCryptCreatePersistedKey(
|
||||
*provider_handle,
|
||||
&mut key_handle,
|
||||
NCRYPT_ECDSA_P256_ALGORITHM,
|
||||
&HSTRING::from(name),
|
||||
CERT_KEY_SPEC::default(), // legacy, unused by the TPM provider
|
||||
scope,
|
||||
)?
|
||||
};
|
||||
let key_handle = unsafe { Owned::new(key_handle) };
|
||||
// Until finalized the key is only a handle; this generates and stores it.
|
||||
unsafe { NCryptFinalizeKey(*key_handle, NCRYPT_SILENT_FLAG)? };
|
||||
CaKey::from_handles(provider_handle, key_handle)
|
||||
}
|
||||
|
||||
/// The Microsoft Platform Crypto Provider, which keeps its keys in the TPM.
|
||||
fn open_tpm_provider() -> WinResult<Owned<NCRYPT_PROV_HANDLE>> {
|
||||
let mut provider_handle = NCRYPT_PROV_HANDLE::default();
|
||||
unsafe { NCryptOpenStorageProvider(&mut provider_handle, MS_PLATFORM_CRYPTO_PROVIDER, 0)? };
|
||||
Ok(unsafe { Owned::new(provider_handle) })
|
||||
}
|
||||
@@ -72,6 +72,9 @@ pub async fn screen(url: &str, firewall: &Firewall) -> Option<String> {
|
||||
Some((_, Verdict::Deny(reason))) => {
|
||||
Some(format!("{} {reason}", pkg.label()))
|
||||
}
|
||||
Some((filter, Verdict::Unknown(e))) if !firewall.config.fail_open => {
|
||||
Some(format!("{} could not be checked by {filter} ({e})", pkg.label()))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::http::{blocked, reply, text};
|
||||
use crate::policies::{self, Check, Decision};
|
||||
use crate::{Firewall, FirewallConfig};
|
||||
use crate::{telemetry, Firewall, FirewallConfig};
|
||||
use rama::http::body::util::BodyExt;
|
||||
use rama::http::client::EasyHttpWebClient;
|
||||
use rama::http::header::CONTENT_TYPE;
|
||||
@@ -19,6 +19,7 @@ pub fn serve(config: FirewallConfig) -> std::io::Result<()> {
|
||||
bound.set_nonblocking(true)?;
|
||||
crate::runtime()?.block_on(async move {
|
||||
let firewall = Arc::new(Firewall::new(config));
|
||||
telemetry::heartbeat();
|
||||
let exec = Executor::default();
|
||||
let listener = TcpListener::try_from_std_tcp_listener(bound, exec.clone())?;
|
||||
let server = HttpServer::auto(exec).service(service_fn(move |req: Request| {
|
||||
@@ -44,6 +45,7 @@ async fn screen(target: String, firewall: &Firewall) -> Response {
|
||||
}
|
||||
Decision::Block { eco, pkg, filter, reason } => {
|
||||
let reason = format!("{} {reason}", pkg.label());
|
||||
telemetry::screened_package(eco, &pkg, Some(&reason));
|
||||
warn!(%eco, %filter, "blocked by firewall: {reason}");
|
||||
blocked(&reason)
|
||||
}
|
||||
@@ -51,9 +53,11 @@ async fn screen(target: String, firewall: &Firewall) -> Response {
|
||||
match &check {
|
||||
Check::NoPackage => {}
|
||||
Check::Allowed(pkg) => {
|
||||
telemetry::screened_package(target.eco, pkg, None);
|
||||
info!(eco = %target.eco, pkg = %pkg.label(), "cleared by every filter")
|
||||
}
|
||||
Check::Unverified { pkg, filter, error } => {
|
||||
telemetry::screened_package(target.eco, pkg, None);
|
||||
info!(pkg = %pkg.label(), %filter, %error, "filter could not check, allowing")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::policies::Package;
|
||||
use crate::{Firewall, FirewallConfig};
|
||||
use rama::http::client::EasyHttpWebClient;
|
||||
use rama::http::header::CONTENT_TYPE;
|
||||
use rama::http::service::client::HttpClientExt;
|
||||
use rama::rt::blocking::Runtime;
|
||||
use serde_json::{json, Value};
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
use std::sync::{LazyLock, Mutex, OnceLock};
|
||||
use std::time::Duration;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use time::OffsetDateTime;
|
||||
@@ -29,29 +30,40 @@ static TELEMETRY: LazyLock<Option<Telemetry>> = LazyLock::new(|| {
|
||||
})
|
||||
});
|
||||
|
||||
/// How the firewall is running: "extension", "service", "wrapper" or "proxy".
|
||||
static MODE: OnceLock<&'static str> = OnceLock::new();
|
||||
|
||||
struct Telemetry {
|
||||
key: &'static str,
|
||||
session: String,
|
||||
events: Mutex<Vec<Value>>,
|
||||
}
|
||||
|
||||
pub fn screened(firewall: &Firewall, url: &str, blocked: bool) {
|
||||
pub fn screened(firewall: &Firewall, url: &str, reason: Option<&str>) {
|
||||
if TELEMETRY.is_none() {
|
||||
return;
|
||||
}
|
||||
let Some((registry, rest)) = firewall.match_on(url, |r| r.upstream) else {
|
||||
return;
|
||||
};
|
||||
let Some(pkg) = firewall.resolve(registry, rest).pkg else {
|
||||
return;
|
||||
};
|
||||
let mut properties = json!({ "eco": registry.eco, "blocked": blocked });
|
||||
if blocked {
|
||||
if let Some(pkg) = firewall.resolve(registry, rest).pkg {
|
||||
screened_package(registry.eco, &pkg, reason);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn screened_package(eco: &str, pkg: &Package, reason: Option<&str>) {
|
||||
let mut properties = json!({ "eco": eco, "blocked": reason.is_some() });
|
||||
if let Some(reason) = reason {
|
||||
properties["package"] = pkg.label().into();
|
||||
properties["reason"] = reason.into();
|
||||
}
|
||||
record("package_screened", properties);
|
||||
}
|
||||
|
||||
pub fn set_mode(mode: &'static str) {
|
||||
MODE.set(mode).ok();
|
||||
}
|
||||
|
||||
pub fn error(kind: &'static str) {
|
||||
record("error", json!({ "kind": kind }));
|
||||
}
|
||||
@@ -81,6 +93,7 @@ pub fn flush() {
|
||||
fn record(event: &str, mut properties: Value) {
|
||||
let Some(telemetry) = &*TELEMETRY else { return };
|
||||
properties["distinct_id"] = telemetry.session.clone().into();
|
||||
properties["mode"] = MODE.get().copied().into();
|
||||
properties["os"] = std::env::consts::OS.into();
|
||||
properties["arch"] = std::env::consts::ARCH.into();
|
||||
properties["$process_person_profile"] = false.into();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use firewall::config::path;
|
||||
use firewall::policies::filters::consult;
|
||||
use firewall::policies::Package;
|
||||
use firewall::policies::{screen, Package};
|
||||
use firewall::{Firewall, FirewallConfig};
|
||||
|
||||
#[test]
|
||||
@@ -21,6 +21,23 @@ fn the_config_lives_under_the_config_directory() {
|
||||
assert!(path.ends_with("config.json"), "{}", path.display());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_later_file_wins_and_allow_lists_add_up() {
|
||||
let dir = std::env::temp_dir().join(format!("firewall-layers-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).expect("temp dir");
|
||||
let user = dir.join("user.json");
|
||||
let system = dir.join("system.json");
|
||||
std::fs::write(&user, r#"{"min_age_hours": 1, "fail_open": false, "allow": {"npm": [{"name": "a"}]}}"#).expect("write");
|
||||
std::fs::write(&system, r#"{"min_age_hours": 24, "allow": {"npm": [{"name": "b"}]}}"#).expect("write");
|
||||
let missing = dir.join("missing.json");
|
||||
let config = FirewallConfig::load_from(&[user, system, missing]).expect("loads");
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
assert_eq!(config.min_age_hours, 24);
|
||||
assert!(!config.fail_open);
|
||||
let names: Vec<&str> = config.allow["npm"].iter().map(|p| p.name.as_str()).collect();
|
||||
assert_eq!(names, ["a", "b"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_allowed_version_skips_the_filters() {
|
||||
// An unreachable denylist would otherwise leave the package unchecked.
|
||||
@@ -31,3 +48,12 @@ async fn an_allowed_version_skips_the_filters() {
|
||||
assert!(consult(&firewall, registry, &pkg("0.20.0")).await.is_none());
|
||||
assert!(consult(&firewall, registry, &pkg("0.21.0")).await.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn failing_closed_blocks_what_could_not_be_checked() {
|
||||
let config = r#"{"fail_open": false, "denylist": {"base": "http://127.0.0.1:1"}}"#;
|
||||
let firewall = Firewall::new(serde_json::from_str(config).expect("parses"));
|
||||
let url = "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz";
|
||||
let blocked = screen(url, &firewall).await.unwrap_or_default();
|
||||
assert!(blocked.contains("could not be checked"), "{blocked:?}");
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#![cfg(windows)]
|
||||
|
||||
use firewall::mitm::CertificateAuthority;
|
||||
use firewall::os::windows::CaKey;
|
||||
use rcgen::PublicKeyData;
|
||||
|
||||
/// Skipped on machines without a TPM.
|
||||
#[test]
|
||||
fn key_signs_a_verifiable_ca() {
|
||||
let key = match CaKey::create_test_key() {
|
||||
Ok(key) => key,
|
||||
Err(error) => return eprintln!("skipping, no TPM key: {error}"),
|
||||
};
|
||||
let ca = CertificateAuthority::generate(&["registry.npmjs.org"], &key);
|
||||
let spki = key.subject_public_key_info();
|
||||
key.delete_from_tpm().expect("delete the test key");
|
||||
let ca = ca.expect("mint a ca");
|
||||
let public = ca.crt.public_key().expect("ca public key");
|
||||
assert!(ca.crt.verify(&public).expect("verify the ca"));
|
||||
assert_eq!(public.public_key_to_der().unwrap(), spki);
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"fill" : {
|
||||
"solid" : "srgb:0.07000,0.07000,0.07000,1.00000"
|
||||
},
|
||||
"groups" : [
|
||||
{
|
||||
"layers" : [
|
||||
{
|
||||
"image-name" : "logo.png",
|
||||
"name" : "logo",
|
||||
"position" : {
|
||||
"scale" : 1.8,
|
||||
"translation-in-points" : [0, 0]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"supported-platforms" : {
|
||||
"squares" : [
|
||||
"macOS"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"idiom": "mac",
|
||||
"size": "16x16",
|
||||
"scale": "1x",
|
||||
"filename": "icon_16.png"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"size": "16x16",
|
||||
"scale": "2x",
|
||||
"filename": "icon_32.png"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"size": "32x32",
|
||||
"scale": "1x",
|
||||
"filename": "icon_32.png"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"size": "32x32",
|
||||
"scale": "2x",
|
||||
"filename": "icon_64.png"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"size": "128x128",
|
||||
"scale": "1x",
|
||||
"filename": "icon_128.png"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"size": "128x128",
|
||||
"scale": "2x",
|
||||
"filename": "icon_256.png"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"size": "256x256",
|
||||
"scale": "1x",
|
||||
"filename": "icon_256.png"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"size": "256x256",
|
||||
"scale": "2x",
|
||||
"filename": "icon_512.png"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"size": "512x512",
|
||||
"scale": "1x",
|
||||
"filename": "icon_512.png"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"size": "512x512",
|
||||
"scale": "2x",
|
||||
"filename": "icon_1024.png"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -114,6 +114,22 @@ final class FirewallModel: NSObject, ObservableObject {
|
||||
|
||||
completed = true
|
||||
log("Setup complete")
|
||||
offerRestart()
|
||||
}
|
||||
|
||||
/// Apps opened before setup, terminals included, only get the environment the env agent sets once reopened.
|
||||
private func offerRestart() {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Restart to finish setting up"
|
||||
alert.informativeText = "Apps that were already open, such as terminals, can't install packages until they're reopened. Restarting reopens them all."
|
||||
alert.addButton(withTitle: "Restart")
|
||||
alert.addButton(withTitle: "Later")
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
guard alert.runModal() == .alertFirstButtonReturn else { return }
|
||||
let osascript = Process()
|
||||
osascript.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
||||
osascript.arguments = ["-e", "tell application \"loginwindow\" to «event aevtrest»"]
|
||||
try? osascript.run()
|
||||
}
|
||||
|
||||
/// After the admin gate, every stage runs even if an earlier one fails, so a partial uninstall never strands the rest.
|
||||
|
||||