Compare commits

...
31 changed files with 196 additions and 169 deletions

No files matched your search

+54
View File
@@ -0,0 +1,54 @@
name: Build Firewall.pkg
on:
workflow_call:
inputs:
plan:
required: true
type: string
jobs:
pkg:
runs-on: macos-26
env:
TEAM: ${{ vars.APPLE_TEAM }}
NOTARY_PROFILE: firewall
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- run: brew install xcodegen
- name: Install signing identities, profiles and notary key
env:
CERTIFICATES: ${{ secrets.MACOS_CERTIFICATES }}
CERTIFICATES_PASSWORD: ${{ secrets.MACOS_CERTIFICATES_PASSWORD }}
APP_PROFILE: ${{ secrets.MACOS_APP_PROFILE }}
EXTENSION_PROFILE: ${{ secrets.MACOS_EXTENSION_PROFILE }}
NOTARY_KEY: ${{ secrets.NOTARY_KEY }}
NOTARY_KEY_ID: ${{ secrets.NOTARY_KEY_ID }}
NOTARY_ISSUER: ${{ secrets.NOTARY_ISSUER }}
run: |
keychain="$RUNNER_TEMP/signing.keychain-db"
security create-keychain -p "" "$keychain"
security set-keychain-settings "$keychain"
security unlock-keychain -p "" "$keychain"
security default-keychain -s "$keychain"
security list-keychains -d user -s "$keychain" login.keychain
base64 --decode <<<"$CERTIFICATES" > "$RUNNER_TEMP/certificates.p12"
security import "$RUNNER_TEMP/certificates.p12" -k "$keychain" -P "$CERTIFICATES_PASSWORD" \
-T /usr/bin/codesign -T /usr/bin/pkgbuild
security set-key-partition-list -S apple-tool:,apple: -s -k "" "$keychain" >/dev/null
profiles="$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"
mkdir -p "$profiles"
base64 --decode <<<"$APP_PROFILE" > "$profiles/app.provisionprofile"
base64 --decode <<<"$EXTENSION_PROFILE" > "$profiles/extension.provisionprofile"
printf '%s' "$NOTARY_KEY" > "$RUNNER_TEMP/notary.p8"
xcrun notarytool store-credentials "$NOTARY_PROFILE" \
--key "$RUNNER_TEMP/notary.p8" --key-id "$NOTARY_KEY_ID" --issuer "$NOTARY_ISSUER"
- run: macos/build.sh
- uses: actions/upload-artifact@v7
with:
name: artifacts-macos-pkg
path: macos/build/Firewall.pkg
+12 -49
View File
@@ -211,14 +211,24 @@ jobs:
path: | path: |
${{ steps.cargo-dist.outputs.paths }} ${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }} ${{ env.BUILD_MANIFEST_NAME }}
custom-build-macos-pkg:
needs:
- plan
- build-local-artifacts
uses: ./.github/workflows/build-macos-pkg.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit
# Determines if we should publish/announce # Determines if we should publish/announce
host: host:
needs: needs:
- plan - plan
- build-local-artifacts - build-local-artifacts
- build-global-artifacts - build-global-artifacts
- custom-build-macos-pkg
# Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine) # Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.custom-build-macos-pkg.result == 'skipped' || needs.custom-build-macos-pkg.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: "ubuntu-22.04" runs-on: "ubuntu-22.04"
@@ -278,52 +288,6 @@ jobs:
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
publish-homebrew-formula:
needs:
- plan
- host
runs-on: "ubuntu-22.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PLAN: ${{ needs.plan.outputs.val }}
GITHUB_USER: "axo bot"
GITHUB_EMAIL: "[email protected]"
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: true
repository: "usestrix/homebrew-tap"
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
# So we have access to the formula
- name: Fetch homebrew formulae
uses: actions/download-artifact@v8
with:
pattern: artifacts-*
path: Formula/
merge-multiple: true
# This is extra complex because you can make your Formula name not match your app name
# so we need to find releases with a *.rb file, and publish with that filename.
- name: Commit formula files
run: |
git config --global user.name "${GITHUB_USER}"
git config --global user.email "${GITHUB_EMAIL}"
for release in $(echo "$PLAN" | jq --compact-output '.releases[] | select([.artifacts[] | endswith(".rb")] | any)'); do
filename=$(echo "$release" | jq '.artifacts[] | select(endswith(".rb"))' --raw-output)
name=$(echo "$filename" | sed "s/\.rb$//")
version=$(echo "$release" | jq .app_version --raw-output)
export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"
brew update
# We avoid reformatting user-provided data such as the app description and homepage.
brew style --except-cops FormulaAudit/Homepage,FormulaAudit/Desc,FormulaAuditStrict --fix "Formula/${filename}" || true
git add "Formula/${filename}"
git commit -m "${name} ${version}"
done
git push
custom-publish-crates: custom-publish-crates:
needs: needs:
- plan - plan
@@ -342,12 +306,11 @@ jobs:
needs: needs:
- plan - plan
- host - host
- publish-homebrew-formula
- custom-publish-crates - custom-publish-crates
# use "always() && ..." to allow us to wait for all publish jobs while # use "always() && ..." to allow us to wait for all publish jobs while
# still allowing individual publish jobs to skip themselves (for prereleases). # still allowing individual publish jobs to skip themselves (for prereleases).
# "host" however must run to completion, no skipping allowed! # "host" however must run to completion, no skipping allowed!
if: ${{ always() && needs.host.result == 'success' && (needs.publish-homebrew-formula.result == 'skipped' || needs.publish-homebrew-formula.result == 'success') && (needs.custom-publish-crates.result == 'skipped' || needs.custom-publish-crates.result == 'success') }} if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish-crates.result == 'skipped' || needs.custom-publish-crates.result == 'success') }}
runs-on: "ubuntu-22.04" runs-on: "ubuntu-22.04"
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -3370,7 +3370,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]] [[package]]
name = "strix-firewall" name = "strix-firewall"
version = "0.1.0" version = "0.1.0-rc.1"
dependencies = [ dependencies = [
"clap", "clap",
"core-foundation 0.10.1", "core-foundation 0.10.1",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "strix-firewall" name = "strix-firewall"
version = "0.1.0" version = "0.1.0-rc.1"
edition = "2021" edition = "2021"
license = "Apache-2.0" license = "Apache-2.0"
authors = ["Strix"] authors = ["Strix"]
+1
View File
@@ -0,0 +1 @@
use_small_heuristics = "Max"
+7 -1
View File
@@ -37,7 +37,13 @@ pub struct Manager {
pub env: &'static [(&'static str, &'static str)], pub env: &'static [(&'static str, &'static str)],
pub args: &'static [Args], pub args: &'static [Args],
/// Where the manager records registry URLs, which then carry our http:// override. /// Where the manager records registry URLs, which then carry our http:// override.
pub lockfile: Option<&'static str>, pub lockfile: Option<Lockfile>,
}
/// What to do after a run about http:// registry URLs left in the lockfile at this path.
pub enum Lockfile {
Restore(&'static str),
Warn(&'static str),
} }
pub static MANAGERS: &[&Manager] = pub static MANAGERS: &[&Manager] =
+2 -2
View File
@@ -1,4 +1,4 @@
use super::{Manager, PROXY}; use super::{Lockfile, Manager, PROXY};
pub static NPM: Manager = Manager { pub static NPM: Manager = Manager {
command: "npm", command: "npm",
@@ -8,5 +8,5 @@ pub static NPM: Manager = Manager {
("npm_config_registry", "http://registry.npmjs.org/"), ("npm_config_registry", "http://registry.npmjs.org/"),
], ],
args: &[], args: &[],
lockfile: Some("package-lock.json"), lockfile: Some(Lockfile::Restore("package-lock.json")),
}; };
+2 -2
View File
@@ -1,8 +1,8 @@
use super::{Manager, PROXY, PYPI_SIMPLE}; use super::{Lockfile, Manager, PROXY, PYPI_SIMPLE};
pub static UV: Manager = Manager { pub static UV: Manager = Manager {
command: "uv", command: "uv",
env: &[("HTTP_PROXY", PROXY), ("http_proxy", PROXY), ("UV_DEFAULT_INDEX", PYPI_SIMPLE)], env: &[("HTTP_PROXY", PROXY), ("http_proxy", PROXY), ("UV_DEFAULT_INDEX", PYPI_SIMPLE)],
args: &[], args: &[],
lockfile: Some("uv.lock"), lockfile: Some(Lockfile::Warn("uv.lock")),
}; };
+22 -3
View File
@@ -10,7 +10,7 @@ use crate::policies::registries::BUILT_IN;
use std::io::{Error, ErrorKind}; use std::io::{Error, ErrorKind};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use tracing::warn; use tracing::{info, warn};
pub struct Invocation { pub struct Invocation {
pub program: String, pub program: String,
@@ -54,8 +54,10 @@ pub fn run(port: u16, invocation: &Invocation, dir: Option<&Path>) -> std::io::R
let status = status.map_err(|error| windows::explain(error, &program)); let status = status.map_err(|error| windows::explain(error, &program));
let code = status?.code().unwrap_or(1); let code = status?.code().unwrap_or(1);
let manager = MANAGERS.iter().find(|manager| manager.command == invocation.program); let manager = MANAGERS.iter().find(|manager| manager.command == invocation.program);
if let Some(lockfile) = manager.and_then(|manager| manager.lockfile) { match manager.and_then(|manager| manager.lockfile.as_ref()) {
warn_if_plaintext(lockfile, dir); Some(Lockfile::Restore(lockfile)) => restore_https(lockfile, dir),
Some(Lockfile::Warn(lockfile)) => warn_if_plaintext(lockfile, dir),
None => {}
} }
Ok(code) Ok(code)
} }
@@ -72,3 +74,20 @@ fn warn_if_plaintext(lockfile: &str, dir: Option<&Path>) {
warn!("{lockfile} has http:// registry URLs; change them to https:// before committing"); warn!("{lockfile} has http:// registry URLs; change them to https:// before committing");
} }
} }
fn restore_https(lockfile: &str, dir: Option<&Path>) {
let path = dir.unwrap_or(Path::new(".")).join(lockfile);
let Ok(text) = std::fs::read_to_string(&path) else { return };
let mut fixed = text.clone();
for registry in BUILT_IN {
let host = registry.host();
fixed = fixed.replace(&format!("http://{host}/"), &format!("https://{host}/"));
}
if fixed == text {
return;
}
match std::fs::write(&path, fixed) {
Ok(()) => info!("changed {lockfile}'s registry URLs back to https://"),
Err(e) => warn!("could not change {lockfile}'s registry URLs back to https://: {e}"),
}
}
+4 -1
View File
@@ -83,7 +83,10 @@ fn record(
let holds = filter == "age" && watched; let holds = filter == "age" && watched;
// A repeat fetch of a held package waits on the same answer. // A repeat fetch of a held package waits on the same answer.
let waiting = blocks.held.iter().find(|(held, wake)| { let waiting = blocks.held.iter().find(|(held, wake)| {
wake.receiver_count() > 0 && held.eco == eco && held.name == pkg.name && held.version == pkg.version wake.receiver_count() > 0
&& held.eco == eco
&& held.name == pkg.name
&& held.version == pkg.version
}); });
if let (true, Some((_, wake))) = (holds, waiting) { if let (true, Some((_, wake))) = (holds, waiting) {
return Some(wake.subscribe()); return Some(wake.subscribe());
+7 -3
View File
@@ -86,10 +86,13 @@ impl FirewallConfig {
Err(e) if e.kind() == ErrorKind::NotFound => continue, Err(e) if e.kind() == ErrorKind::NotFound => continue,
Err(e) => return Err(Error::new(e.kind(), format!("{}: {e}", path.display()))), Err(e) => return Err(Error::new(e.kind(), format!("{}: {e}", path.display()))),
}; };
let invalid = |e| Error::new(ErrorKind::InvalidData, 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)?; let layer: Value = serde_json::from_str(&text).map_err(invalid)?;
let config = FirewallConfig::deserialize(&layer).map_err(invalid)?; let config = FirewallConfig::deserialize(&layer).map_err(invalid)?;
check(&config).map_err(|e| Error::new(ErrorKind::InvalidData, format!("{}: {e}", path.display())))?; check(&config).map_err(|e| {
Error::new(ErrorKind::InvalidData, format!("{}: {e}", path.display()))
})?;
let Value::Object(layer) = layer else { let Value::Object(layer) = layer else {
unreachable!("a config parses only from an object"); unreachable!("a config parses only from an object");
}; };
@@ -122,7 +125,8 @@ fn merge(base: &mut Map<String, Value>, top: Map<String, Value>) {
/// Adds an `allow` entry to the file, leaving the rest of it as written. /// Adds an `allow` entry to the file, leaving the rest of it as written.
pub fn allow(path: &Path, eco: &str, name: &str, version: &str) -> std::io::Result<()> { pub fn allow(path: &Path, eco: &str, name: &str, version: &str) -> std::io::Result<()> {
let invalid = |e: String| Error::new(ErrorKind::InvalidData, format!("{}: {e}", path.display())); let invalid =
|e: String| Error::new(ErrorKind::InvalidData, format!("{}: {e}", path.display()));
let mut config = match std::fs::read_to_string(path) { let mut config = match std::fs::read_to_string(path) {
Ok(text) => serde_json::from_str(&text).map_err(|e| invalid(e.to_string()))?, Ok(text) => serde_json::from_str(&text).map_err(|e| invalid(e.to_string()))?,
Err(e) if e.kind() == ErrorKind::NotFound => json!({}), Err(e) if e.kind() == ErrorKind::NotFound => json!({}),
+5 -1
View File
@@ -25,7 +25,11 @@ pub fn local_url(port: u16, path: &str) -> String {
} }
pub fn init_logging(default_level: &str) { pub fn init_logging(default_level: &str) {
fmt().with_env_filter(log_filter(default_level)).with_target(false).with_writer(std::io::stderr).init(); fmt()
.with_env_filter(log_filter(default_level))
.with_target(false)
.with_writer(std::io::stderr)
.init();
} }
pub(crate) fn log_filter(default_level: &str) -> EnvFilter { pub(crate) fn log_filter(default_level: &str) -> EnvFilter {
+1 -4
View File
@@ -58,10 +58,7 @@ fn serve() -> ! {
Ok(()) => exit(0), Ok(()) => exit(0),
Err(e) if e.kind() == ErrorKind::AddrInUse => { Err(e) if e.kind() == ErrorKind::AddrInUse => {
let location = config::path().map_or("the config".into(), |p| p.display().to_string()); let location = config::path().map_or("the config".into(), |p| p.display().to_string());
fail( fail("port", &format!("port {port} is in use, set another in {location}"))
"port",
&format!("port {port} is in use, set another in {location}"),
)
} }
Err(e) if e.kind() == ErrorKind::PermissionDenied => { Err(e) if e.kind() == ErrorKind::PermissionDenied => {
fail("port", &format!("not allowed to listen on port {port}")) fail("port", &format!("not allowed to listen on port {port}"))
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::http::{blocked, https_url}; use crate::http::{blocked, https_url};
use crate::{policies, telemetry};
use crate::Firewall; use crate::Firewall;
use crate::{policies, telemetry};
use rama::error::BoxError; use rama::error::BoxError;
use rama::http::{Request, Response}; use rama::http::{Request, Response};
use rama::{Layer, Service}; use rama::{Layer, Service};
+6 -3
View File
@@ -75,8 +75,8 @@ fn create() -> Result<SecKey, BoxError> {
.set_key_type(KeyType::ec_sec_prime_random()) .set_key_type(KeyType::ec_sec_prime_random())
.set_token(Token::SecureEnclave) .set_token(Token::SecureEnclave)
.set_access_control(access); .set_access_control(access);
let key = SecKey::new(&options) let key =
.map_err(|e| format!("secure enclave key generation failed: {e}"))?; SecKey::new(&options).map_err(|e| format!("secure enclave key generation failed: {e}"))?;
let blob = key let blob = key
.attributes() .attributes()
@@ -102,7 +102,10 @@ fn load(blob: &[u8]) -> Result<SecKey, BoxError> {
CFString::wrap_under_get_rule(kSecAttrKeyClass), CFString::wrap_under_get_rule(kSecAttrKeyClass),
CFString::wrap_under_get_rule(kSecAttrKeyClassPrivate).into_CFType(), CFString::wrap_under_get_rule(kSecAttrKeyClassPrivate).into_CFType(),
), ),
(CFString::from_static_string(TOKEN_OBJECT_ID), CFData::from_buffer(blob).into_CFType()), (
CFString::from_static_string(TOKEN_OBJECT_ID),
CFData::from_buffer(blob).into_CFType(),
),
]) ])
}; };
let mut error = ptr::null_mut(); let mut error = ptr::null_mut();
+5 -3
View File
@@ -5,8 +5,8 @@ mod uninstall;
pub use uninstall::uninstall; pub use uninstall::uninstall;
use crate::config::FirewallConfig; use crate::config::FirewallConfig;
use crate::{blocks, mitm, telemetry};
use crate::Firewall; use crate::Firewall;
use crate::{blocks, mitm, telemetry};
use rama::bytes::Bytes; use rama::bytes::Bytes;
use rama::error::BoxError; use rama::error::BoxError;
use rama::io::BridgeIo; use rama::io::BridgeIo;
@@ -107,7 +107,8 @@ pub fn system_config() -> Option<PathBuf> {
fn handler(exec: Executor) -> Result<Handler, BoxError> { fn handler(exec: Executor) -> Result<Handler, BoxError> {
telemetry::set_mode("extension"); telemetry::set_mode("extension");
telemetry::heartbeat(); telemetry::heartbeat();
let config = FirewallConfig::load_from(&[CONFIG_FILE.into()]).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 firewall = Arc::new(Firewall::new(config));
let hosts = firewall.hosts(); let hosts = firewall.hosts();
let key = enclave::EnclaveKey::load_or_create().inspect_err(|_| telemetry::error("enclave"))?; let key = enclave::EnclaveKey::load_or_create().inspect_err(|_| telemetry::error("enclave"))?;
@@ -125,7 +126,8 @@ fn init(_config: Option<&TransparentProxyInitConfig>) -> bool {
std::panic::set_hook(Box::new(|panic| tracing::error!("{panic}"))); std::panic::set_hook(Box::new(|panic| tracing::error!("{panic}")));
if let Ok(oslog) = OsLogLayer::new(BUNDLE_ID, "extension") { if let Ok(oslog) = OsLogLayer::new(BUNDLE_ID, "extension") {
let oslog = oslog.with_privacy(Privacy::Public); let oslog = oslog.with_privacy(Privacy::Public);
let _ = tracing_subscriber::registry().with(crate::log_filter("info")).with(oslog).try_init(); let _ =
tracing_subscriber::registry().with(crate::log_filter("info")).with(oslog).try_init();
} }
true true
} }
+2 -6
View File
@@ -37,9 +37,7 @@ pub fn load_or_create_ca(
None => { None => {
warn!(?hosts, "minting a new CA"); warn!(?hosts, "minting a new CA");
let ca = CertificateAuthority::generate(hosts, signer)?; let ca = CertificateAuthority::generate(hosts, signer)?;
LOCAL_MACHINE LOCAL_MACHINE.create(REGISTRY_KEY)?.set_string("ca", ca.to_blob(hosts))?;
.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. // 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 let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos() / 100
+ FILETIME_UNIX_EPOCH; + FILETIME_UNIX_EPOCH;
@@ -119,9 +117,7 @@ fn owned_by_admins(path: &Path) -> windows::core::Result<bool> {
.ok()? .ok()?
}; };
let admins = [WinBuiltinAdministratorsSid, WinLocalSystemSid]; let admins = [WinBuiltinAdministratorsSid, WinLocalSystemSid];
let owned = admins let owned = admins.into_iter().any(|sid| unsafe { IsWellKnownSid(owner, sid) }.as_bool());
.into_iter()
.any(|sid| unsafe { IsWellKnownSid(owner, sid) }.as_bool());
// `owner` points into the descriptor, so it is freed last. // `owner` points into the descriptor, so it is freed last.
unsafe { LocalFree(Some(HLOCAL(descriptor.0))) }; unsafe { LocalFree(Some(HLOCAL(descriptor.0))) };
Ok(owned) Ok(owned)
+5 -20
View File
@@ -48,9 +48,7 @@ pub fn system_config() -> Option<PathBuf> {
/// In the folder Windows reads it from, normally %SystemRoot%\System32\drivers\etc. /// In the folder Windows reads it from, normally %SystemRoot%\System32\drivers\etc.
fn hosts_file() -> PathBuf { fn hosts_file() -> PathBuf {
let key = r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters"; let key = r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters";
let configured = LOCAL_MACHINE let configured = LOCAL_MACHINE.open(key).and_then(|key| key.get_hstring("DataBasePath"));
.open(key)
.and_then(|key| key.get_hstring("DataBasePath"));
let dir = match configured { let dir = match configured {
Ok(dir) => expand_environment(&dir), Ok(dir) => expand_environment(&dir),
Err(_) => known_folder(&FOLDERID_System).join(r"drivers\etc"), Err(_) => known_folder(&FOLDERID_System).join(r"drivers\etc"),
@@ -71,17 +69,12 @@ fn expand_environment(path: &HSTRING) -> PathBuf {
let mut expanded = vec![0; len as usize]; let mut expanded = vec![0; len as usize];
let len = unsafe { ExpandEnvironmentStringsW(path, Some(&mut expanded)) }; let len = unsafe { ExpandEnvironmentStringsW(path, Some(&mut expanded)) };
// Less the terminating null. // Less the terminating null.
PathBuf::from(String::from_utf16_lossy( PathBuf::from(String::from_utf16_lossy(&expanded[..(len as usize).saturating_sub(1)]))
&expanded[..(len as usize).saturating_sub(1)],
))
} }
/// From 127.0.0.3, leaving 127.0.0.1 to whatever else serves locally. /// 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)> { fn loopback_addresses(hosts: &[&'static str]) -> Vec<(Ipv4Addr, &'static str)> {
(3..) (3..).zip(hosts).map(|(i, host)| (Ipv4Addr::new(127, 0, 0, i), *host)).collect()
.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 /// Points each host at its listener, replacing the lines we wrote before; with none, only removes
@@ -91,11 +84,7 @@ fn write_hosts_entries(hosts: &[&'static str]) -> std::io::Result<()> {
let old = std::fs::read(hosts_file())?; let old = std::fs::read(hosts_file())?;
let ours = |line: &&[u8]| line.trim_ascii_end().ends_with(TAG.as_bytes()); let ours = |line: &&[u8]| line.trim_ascii_end().ends_with(TAG.as_bytes());
let lines = old.split_inclusive(|&byte| byte == b'\n'); let lines = old.split_inclusive(|&byte| byte == b'\n');
let mut new: Vec<u8> = lines let mut new: Vec<u8> = lines.filter(|line| !ours(line)).flatten().copied().collect();
.filter(|line| !ours(line))
.flatten()
.copied()
.collect();
if !new.is_empty() && !new.ends_with(b"\n") { if !new.is_empty() && !new.ends_with(b"\n") {
new.extend_from_slice(b"\r\n"); new.extend_from_slice(b"\r\n");
} }
@@ -120,11 +109,7 @@ fn security_descriptor(sddl: &str) -> windows::core::Result<Owned<HLOCAL>> {
} }
fn read_registry_string(key: &str, value: &str) -> Result<Option<String>, BoxError> { fn read_registry_string(key: &str, value: &str) -> Result<Option<String>, BoxError> {
none_if_missing( none_if_missing(LOCAL_MACHINE.open(key).and_then(|key| key.get_string(value)))
LOCAL_MACHINE
.open(key)
.and_then(|key| key.get_string(value)),
)
} }
/// `None` for a missing key or value. /// `None` for a missing key or value.
+1 -4
View File
@@ -51,10 +51,7 @@ fn create(first: bool) -> Result<NamedPipeServer, BoxError> {
async fn answer(client: NamedPipeServer, firewall: &Firewall) -> 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).read_line(&mut request).await?;
.take(64 * 1024)
.read_line(&mut request)
.await?;
let request = request.trim_end(); let request = request.trim_end();
// Only the app can message the macOS extension; here any program could connect. // Only the app can message the macOS extension; here any program could connect.
let reply = if !client_is_tray(client.get_ref())? { let reply = if !client_is_tray(client.get_ref())? {
+4 -12
View File
@@ -79,9 +79,7 @@ fn run_until_stopped() -> Result<(), BoxError> {
// Answered once handled, so a failed pause fails the caller's ControlService. // Answered once handled, so a failed pause fails the caller's ControlService.
let (reply, handled) = tokio::sync::oneshot::channel(); let (reply, handled) = tokio::sync::oneshot::channel();
let _ = control_sender.send((control, reply)); let _ = control_sender.send((control, reply));
handled handled.blocking_recv().unwrap_or(ServiceControlHandlerResult::NoError)
.blocking_recv()
.unwrap_or(ServiceControlHandlerResult::NoError)
} }
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
_ => ServiceControlHandlerResult::NotImplemented, _ => ServiceControlHandlerResult::NotImplemented,
@@ -109,9 +107,7 @@ fn run_until_stopped() -> Result<(), BoxError> {
} }
Err(error) => { Err(error) => {
tracing::error!(%error, "could not rewrite the hosts file"); tracing::error!(%error, "could not rewrite the hosts file");
let code = error let code = error.raw_os_error().map_or(ERROR_GEN_FAILURE.0, |code| code as u32);
.raw_os_error()
.map_or(ERROR_GEN_FAILURE.0, |code| code as u32);
ServiceControlHandlerResult::Other(code) ServiceControlHandlerResult::Other(code)
} }
}; };
@@ -165,12 +161,8 @@ async fn start_screening(status: &ServiceStatusHandle) -> Result<Vec<&'static st
let service = mitm::mitm_service(firewall, &ca, signer, Some(crl_url), exec.clone()); let service = mitm::mitm_service(firewall, &ca, signer, Some(crl_url), exec.clone());
for (address, host) in loopback_addresses(&hosts) { for (address, host) in loopback_addresses(&hosts) {
for port in [80, 443] { for port in [80, 443] {
let upstream = RedirectedHost { let upstream =
host, RedirectedHost { host, port, resolver: resolver.clone(), service: service.clone() };
port,
resolver: resolver.clone(),
service: service.clone(),
};
let upstream = ConsumeErrLayer::trace_as_warning().into_layer(upstream); let upstream = ConsumeErrLayer::trace_as_warning().into_layer(upstream);
tokio::spawn(listen_on(address, port, &exec)?.serve(upstream)); tokio::spawn(listen_on(address, port, &exec)?.serve(upstream));
} }
+8 -27
View File
@@ -50,9 +50,7 @@ pub fn run() -> Result<(), BoxError> {
&MenuItem::with_id(MENU_QUIT, "Quit", true, None), &MenuItem::with_id(MENU_QUIT, "Quit", true, None),
])?; ])?;
let (logo, logo_size) = logo_mask()?; let (logo, logo_size) = logo_mask()?;
let icon = TrayIconBuilder::new() let icon = TrayIconBuilder::new().with_menu(Box::new(menu.clone())).build()?;
.with_menu(Box::new(menu.clone()))
.build()?;
let window = HWND(icon.window_handle()); let window = HWND(icon.window_handle());
let mut tray = Tray { let mut tray = Tray {
icon, icon,
@@ -84,11 +82,7 @@ pub fn run() -> Result<(), BoxError> {
} }
while let Ok(event) = TrayIconEvent::receiver().try_recv() { while let Ok(event) = TrayIconEvent::receiver().try_recv() {
// The menu opens on release, so this keeps it current. // The menu opens on release, so this keeps it current.
if let TrayIconEvent::Click { if let TrayIconEvent::Click { button_state: MouseButtonState::Down, .. } = event {
button_state: MouseButtonState::Down,
..
} = event
{
tray.update(); tray.update();
} }
} }
@@ -218,11 +212,7 @@ impl Tray {
} }
let _ = self.menu.insert(&self.blocked_separator, BLOCKED_AT); let _ = self.menu.insert(&self.blocked_separator, BLOCKED_AT);
for (i, package) in self.blocked.iter().enumerate() { for (i, package) in self.blocked.iter().enumerate() {
let verb = if package.malicious() { let verb = if package.malicious() { "Blocked" } else { "Held back" };
"Blocked"
} else {
"Held back"
};
let reason = if package.malicious() { let reason = if package.malicious() {
format!("Malicious: {}", package.reason) format!("Malicious: {}", package.reason)
} else { } else {
@@ -230,11 +220,8 @@ impl Tray {
}; };
let reason = MenuItem::new(reason, false, None); let reason = MenuItem::new(reason, false, None);
let once = MenuItem::with_id(format!("{ALLOW_ONCE}:{i}"), "Allow Once", true, None); let once = MenuItem::with_id(format!("{ALLOW_ONCE}:{i}"), "Allow Once", true, None);
let items: &[&dyn IsMenuItem] = if package.trustable() { let items: &[&dyn IsMenuItem] =
&[&reason, &once] if package.trustable() { &[&reason, &once] } else { &[&reason] };
} else {
&[&reason]
};
let label = format!("{verb} {}", package.label()); let label = format!("{verb} {}", package.label());
if let Ok(item) = Submenu::with_items(label, true, items) { 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);
@@ -316,10 +303,7 @@ fn allow_once(package: BlockedPackage, allowed: &Sender<BlockedPackage>) {
/// One request per connection, as the service answers. /// One request per connection, as the service answers.
fn ask_service(request: &str) -> Result<String, BoxError> { fn ask_service(request: &str) -> Result<String, BoxError> {
let mut pipe = std::fs::OpenOptions::new() let mut pipe = std::fs::OpenOptions::new().read(true).write(true).open(PIPE)?;
.read(true)
.write(true)
.open(PIPE)?;
// Any program can create the pipe if it gets there before the service. // Any program can create the pipe if it gets there before the service.
let mut server = 0; let mut server = 0;
unsafe { GetNamedPipeServerProcessId(HANDLE(pipe.as_raw_handle()), &mut server)? }; unsafe { GetNamedPipeServerProcessId(HANDLE(pipe.as_raw_handle()), &mut server)? };
@@ -345,9 +329,7 @@ fn show_error(error: BoxError) {
fn service_status() -> Option<ServiceStatus> { fn service_status() -> Option<ServiceStatus> {
let manager = let manager =
ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT).ok()?; ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT).ok()?;
let service = manager let service = manager.open_service(SERVICE_NAME, ServiceAccess::QUERY_STATUS).ok()?;
.open_service(SERVICE_NAME, ServiceAccess::QUERY_STATUS)
.ok()?;
service.query_status().ok() service.query_status().ok()
} }
@@ -356,8 +338,7 @@ fn light_taskbar() -> bool {
let Ok(key) = CURRENT_USER.open(personalize) else { let Ok(key) = CURRENT_USER.open(personalize) else {
return false; return false;
}; };
key.get_u32("SystemUsesLightTheme") key.get_u32("SystemUsesLightTheme").is_ok_and(|light| light == 1)
.is_ok_and(|light| light == 1)
} }
fn status_text(state: Option<ServiceState>) -> &'static str { fn status_text(state: Option<ServiceState>) -> &'static str {
+1 -3
View File
@@ -63,9 +63,7 @@ impl Filter for Denylist {
fn check<'a>(&'a self, registry: &'static Registry, pkg: &'a Package) -> VerdictFuture<'a> { fn check<'a>(&'a self, registry: &'static Registry, pkg: &'a Package) -> VerdictFuture<'a> {
Box::pin(async move { Box::pin(async move {
let eco = registry.osv_name; let eco = registry.osv_name;
let load = || async { let load = || async { RwLock::new(Arc::new(load(&self.source, eco).await)) };
RwLock::new(Arc::new(load(&self.source, eco).await))
};
let list = self.lists[eco].get_or_init(load).await; let list = self.lists[eco].get_or_init(load).await;
let list = Arc::clone(&list.read().expect("denylist lock")); let list = Arc::clone(&list.read().expect("denylist lock"));
match &*list { match &*list {
+4 -2
View File
@@ -41,8 +41,10 @@ impl Filter for Osv {
if hits.is_empty() { if hits.is_empty() {
Verdict::Allow Verdict::Allow
} else { } else {
let links: Vec<String> = let links: Vec<String> = hits
hits.iter().map(|id| format!("{id} https://osv.dev/vulnerability/{id}")).collect(); .iter()
.map(|id| format!("{id} https://osv.dev/vulnerability/{id}"))
.collect();
Verdict::Deny(format!("is flagged malicious by OSV: {}", links.join(", "))) Verdict::Deny(format!("is flagged malicious by OSV: {}", links.join(", ")))
} }
}) })
+1 -5
View File
@@ -23,11 +23,7 @@ static TELEMETRY: LazyLock<Option<Telemetry>> = LazyLock::new(|| {
return None; return None;
} }
let session = format!("{:016x}", rand::random::<u64>()); let session = format!("{:016x}", rand::random::<u64>());
Some(Telemetry { Some(Telemetry { key, session, events: Mutex::default() })
key,
session,
events: Mutex::default(),
})
}); });
/// How the firewall is running: "extension", "service", "wrapper" or "proxy". /// How the firewall is running: "extension", "service", "wrapper" or "proxy".
+10 -3
View File
@@ -14,7 +14,8 @@ async fn allowing_once_releases_the_held_download() {
let held = tokio::spawn(async move { block("npm", &pkg, "age", "is new").await }); let held = tokio::spawn(async move { block("npm", &pkg, "age", "is new").await });
let id = loop { let id = loop {
let blocks: serde_json::Value = serde_json::from_str(&answer(&firewall, config, "blocks")).unwrap(); let blocks: serde_json::Value =
serde_json::from_str(&answer(&firewall, config, "blocks")).unwrap();
if let Some(id) = blocks[0]["id"].as_u64() { if let Some(id) = blocks[0]["id"].as_u64() {
break id; break id;
} }
@@ -22,6 +23,12 @@ async fn allowing_once_releases_the_held_download() {
}; };
assert_eq!(answer(&firewall, config, &format!("allow-once {id}")), "ok"); assert_eq!(answer(&firewall, config, &format!("allow-once {id}")), "ok");
assert!(held.await.unwrap(), "the waiting download goes through"); assert!(held.await.unwrap(), "the waiting download goes through");
assert!(firewall.age_check_lifted("npm", |p| p.name == "left-pad"), "and the rest of the install"); assert!(
assert!(!block("npm", &Package { name: "evil".into(), version: None }, "denylist", "is malware").await); firewall.age_check_lifted("npm", |p| p.name == "left-pad"),
"and the rest of the install"
);
assert!(
!block("npm", &Package { name: "evil".into(), version: None }, "denylist", "is malware")
.await
);
} }
+17 -6
View File
@@ -49,8 +49,13 @@ fn a_later_file_wins_and_allow_lists_add_up() {
std::fs::create_dir_all(&dir).expect("temp dir"); std::fs::create_dir_all(&dir).expect("temp dir");
let user = dir.join("user.json"); let user = dir.join("user.json");
let system = dir.join("system.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(
std::fs::write(&system, r#"{"min_age_hours": 24, "allow": {"npm": [{"name": "b"}]}}"#).expect("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 missing = dir.join("missing.json");
let config = FirewallConfig::load_from(&[user, system, missing]).expect("loads"); let config = FirewallConfig::load_from(&[user, system, missing]).expect("loads");
std::fs::remove_dir_all(&dir).ok(); std::fs::remove_dir_all(&dir).ok();
@@ -67,9 +72,13 @@ async fn a_mirror_is_screened_like_the_registry_it_copies() {
assert!(firewall.hosts().contains(&"npm.example.com")); assert!(firewall.hosts().contains(&"npm.example.com"));
let url = "https://npm.example.com/left-pad/-/left-pad-1.3.0.tgz"; let url = "https://npm.example.com/left-pad/-/left-pad-1.3.0.tgz";
let (registry, rest) = firewall.match_on(url, |r| r.upstream).expect("the mirror"); let (registry, rest) = firewall.match_on(url, |r| r.upstream).expect("the mirror");
assert_eq!(firewall.resolve(registry, rest).pkg.map(|p| p.label()), Some("[email protected]".into())); assert_eq!(
firewall.resolve(registry, rest).pkg.map(|p| p.label()),
Some("[email protected]".into())
);
let unknown = std::env::temp_dir().join(format!("firewall-mirror-{}.json", std::process::id())); let unknown = std::env::temp_dir().join(format!("firewall-mirror-{}.json", std::process::id()));
std::fs::write(&unknown, r#"{"registries": {"nmp": ["https://npm.example.com/"]}}"#).expect("write"); std::fs::write(&unknown, r#"{"registries": {"nmp": ["https://npm.example.com/"]}}"#)
.expect("write");
let loaded = FirewallConfig::load_from(std::slice::from_ref(&unknown)); let loaded = FirewallConfig::load_from(std::slice::from_ref(&unknown));
std::fs::remove_file(&unknown).ok(); std::fs::remove_file(&unknown).ok();
assert!(loaded.is_err()); assert!(loaded.is_err());
@@ -80,7 +89,8 @@ async fn an_allowed_version_is_still_checked_for_malware() {
// The unreachable denylist shows that it still runs. // The unreachable denylist shows that it still runs.
let config = r#"{"allow": {"go": [{"name": "golang.org/x/net", "version": "v0.20.0"}]}, "denylist": {"base": "http://127.0.0.1:1"}}"#; let config = r#"{"allow": {"go": [{"name": "golang.org/x/net", "version": "v0.20.0"}]}, "denylist": {"base": "http://127.0.0.1:1"}}"#;
let firewall = Firewall::new(serde_json::from_str(config).expect("parses")); let firewall = Firewall::new(serde_json::from_str(config).expect("parses"));
let (registry, _) = firewall.match_on("/go/golang.org/x/net/@v/list", |r| r.prefix).expect("go"); let (registry, _) =
firewall.match_on("/go/golang.org/x/net/@v/list", |r| r.prefix).expect("go");
let pkg = Package { name: "golang.org/x/net".into(), version: Some("0.20.0".into()) }; let pkg = Package { name: "golang.org/x/net".into(), version: Some("0.20.0".into()) };
let verdict = consult(&firewall, registry, &pkg).await; let verdict = consult(&firewall, registry, &pkg).await;
assert_eq!(verdict.map(|(filter, _)| filter), Some("denylist")); assert_eq!(verdict.map(|(filter, _)| filter), Some("denylist"));
@@ -99,7 +109,8 @@ async fn failing_closed_blocks_what_could_not_be_checked() {
async fn lifting_the_age_check_still_checks_for_malware() { async fn lifting_the_age_check_still_checks_for_malware() {
let config = r#"{"denylist": {"base": "http://127.0.0.1:1"}}"#; let config = r#"{"denylist": {"base": "http://127.0.0.1:1"}}"#;
let firewall = Firewall::new(serde_json::from_str(config).expect("parses")); let firewall = Firewall::new(serde_json::from_str(config).expect("parses"));
let (registry, _) = firewall.match_on("/go/golang.org/x/net/@v/list", |r| r.prefix).expect("go"); let (registry, _) =
firewall.match_on("/go/golang.org/x/net/@v/list", |r| r.prefix).expect("go");
let pkg = Package { name: "golang.org/x/net".into(), version: Some("0.20.0".into()) }; let pkg = Package { name: "golang.org/x/net".into(), version: Some("0.20.0".into()) };
firewall.lift_age_check("go", AllowedPackage { name: pkg.name.clone(), version: None }, None); firewall.lift_age_check("go", AllowedPackage { name: pkg.name.clone(), version: None }, None);
let (filter, _) = consult(&firewall, registry, &pkg).await.expect("the denylist still runs"); let (filter, _) = consult(&firewall, registry, &pkg).await.expect("the denylist still runs");
+2 -1
View File
@@ -49,7 +49,8 @@ fn a_plain_http_upstream_is_not_upgraded() {
let port = aliasing::spawn(0, FirewallConfig::default()).expect("spawn"); let port = aliasing::spawn(0, FirewallConfig::default()).expect("spawn");
let mut proxy = TcpStream::connect(("127.0.0.1", port)).expect("connect"); let mut proxy = TcpStream::connect(("127.0.0.1", port)).expect("connect");
let request = format!("GET http://{addr}/x HTTP/1.1\r\nHost: {addr}\r\nConnection: close\r\n\r\n"); let request =
format!("GET http://{addr}/x HTTP/1.1\r\nHost: {addr}\r\nConnection: close\r\n\r\n");
proxy.write_all(request.as_bytes()).expect("write"); proxy.write_all(request.as_bytes()).expect("write");
let mut response = String::new(); let mut response = String::new();
proxy.read_to_string(&mut response).expect("read"); proxy.read_to_string(&mut response).expect("read");
+2 -1
View File
@@ -36,7 +36,8 @@ pub fn all(port: u16) -> Vec<(&'static str, Outcome)> {
fn run(probe: &Probe, port: u16) -> Outcome { fn run(probe: &Probe, port: u16) -> Outcome {
let args = probe.args.iter().map(|arg| arg.to_string()).collect(); let args = probe.args.iter().map(|arg| arg.to_string()).collect();
let dir = probe.setup.and_then(|prepare| prepare()); let dir = probe.setup.and_then(|prepare| prepare());
match aliasing::run(port, &Invocation::new(probe.manager.command.into(), args), dir.as_deref()) { match aliasing::run(port, &Invocation::new(probe.manager.command.into(), args), dir.as_deref())
{
Ok(0) => Outcome::Ok, Ok(0) => Outcome::Ok,
Ok(_) => Outcome::Failed, Ok(_) => Outcome::Failed,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Outcome::Absent, Err(e) if e.kind() == std::io::ErrorKind::NotFound => Outcome::Absent,
+3 -3
View File
@@ -4,10 +4,10 @@ members = ["cargo:core"]
[dist] [dist]
cargo-dist-version = "0.32.0" cargo-dist-version = "0.32.0"
ci = "github" ci = "github"
installers = ["shell", "powershell", "homebrew", "msi"] installers = ["shell", "powershell", "msi"]
# core/wix/main.wxs is edited to install the service. # core/wix/main.wxs is edited to install the service.
allow-dirty = ["msi"] allow-dirty = ["msi"]
features = ["machine-wide"] features = ["machine-wide"]
tap = "usestrix/homebrew-tap" publish-jobs = ["./publish-crates"]
publish-jobs = ["homebrew", "./publish-crates"] global-artifacts-jobs = ["./build-macos-pkg"]
targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"]
+1 -1
View File
@@ -48,7 +48,7 @@ build() {
xcodegen generate >/dev/null xcodegen generate >/dev/null
xcodebuild -project Firewall.xcodeproj -scheme StrixFirewall -configuration $CONFIGURATION \ xcodebuild -project Firewall.xcodeproj -scheme StrixFirewall -configuration $CONFIGURATION \
-destination generic/platform=macOS \ -destination generic/platform=macOS \
-derivedDataPath build CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO \ -derivedDataPath build CURRENT_PROJECT_VERSION="$(date +%s)" CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO \
CODE_SIGN_IDENTITY="" CODE_SIGN_ENTITLEMENTS="" build >/dev/null CODE_SIGN_IDENTITY="" CODE_SIGN_ENTITLEMENTS="" build >/dev/null
} }
+4
View File
@@ -19,6 +19,8 @@ settings:
ENABLE_DEBUG_DYLIB: "NO" ENABLE_DEBUG_DYLIB: "NO"
CODE_SIGN_STYLE: Automatic CODE_SIGN_STYLE: Automatic
CODE_SIGN_IDENTITY: Apple Development CODE_SIGN_IDENTITY: Apple Development
# build.sh sets a fresh one per build; macOS only replaces an installed extension whose version differs.
CURRENT_PROJECT_VERSION: "1"
packages: packages:
Rama: Rama:
@@ -42,6 +44,7 @@ targets:
properties: properties:
CFBundlePackageType: SYSX CFBundlePackageType: SYSX
CFBundleShortVersionString: "0.1" CFBundleShortVersionString: "0.1"
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
NSSystemExtensionUsageDescription: Firewall uses a network extension to screen package downloads before they install. NSSystemExtensionUsageDescription: Firewall uses a network extension to screen package downloads before they install.
NetworkExtension: NetworkExtension:
NEMachServiceName: ${TEAM}.com.usestrix.firewall.extension NEMachServiceName: ${TEAM}.com.usestrix.firewall.extension
@@ -102,6 +105,7 @@ targets:
path: App/Info.plist path: App/Info.plist
properties: properties:
CFBundleShortVersionString: "0.1" CFBundleShortVersionString: "0.1"
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
LSMinimumSystemVersion: $(MACOSX_DEPLOYMENT_TARGET) LSMinimumSystemVersion: $(MACOSX_DEPLOYMENT_TARGET)
NSSystemExtensionUsageDescription: Firewall uses a network extension to screen package downloads before they install. NSSystemExtensionUsageDescription: Firewall uses a network extension to screen package downloads before they install.
LSUIElement: true LSUIElement: true