Compare commits

...
Author SHA1 Message Date
Ian b37f93936e Show why the extension failed to start 2026-10-21 15:40:40 -04:00
ian-at-strix bcd26b91b0 Rename the Installer app to StrixFirewall (#28)
* Rename the Installer app to Firewall

* Name the app Strix Firewall

* Name the app StrixFirewall
2026-10-21 15:28:19 -04:00
Ian 70e66280f6 Log uninstall only when something was removed 2026-10-21 15:28:19 -04:00
Ian 1879e9061e Uninstall through the bundled CLI 2026-10-21 15:28:19 -04:00
Ian dcee91ef04 Remove the CA from the app, not the extension 2026-10-21 15:28:19 -04:00
17 changed files with 124 additions and 57 deletions

No files matched your search

+13
View File
@@ -12,6 +12,10 @@ fn main() {
firewall::init_logging("info"); firewall::init_logging("info");
serve() serve()
} }
if matches.subcommand_matches("uninstall").is_some() {
firewall::init_logging("info");
uninstall()
}
let mut command = matches.get_many::<String>("COMMAND").unwrap().cloned(); let mut command = matches.get_many::<String>("COMMAND").unwrap().cloned();
let program = command.next().unwrap(); let program = command.next().unwrap();
firewall::init_logging("warn"); firewall::init_logging("warn");
@@ -40,6 +44,8 @@ fn options() -> Command {
Command::new("serve") Command::new("serve")
.about("Serve a local registry for package managers configured to use it"), .about("Serve a local registry for package managers configured to use it"),
) )
// Run as root by the macOS app's uninstall.
.subcommand(Command::new("uninstall").hide(true))
.arg( .arg(
Arg::new("COMMAND") Arg::new("COMMAND")
.help("A command to run with its package downloads screened") .help("A command to run with its package downloads screened")
@@ -69,6 +75,13 @@ fn serve() -> ! {
} }
} }
fn uninstall() -> ! {
match firewall::os::uninstall() {
Ok(()) => exit(0),
Err(e) => fail("uninstall", &format!("could not uninstall: {e}")),
}
}
fn run(invocation: Invocation) -> ! { fn run(invocation: Invocation) -> ! {
let command = &invocation.program; let command = &invocation.program;
let port = match aliasing::spawn(0, load()) { let port = match aliasing::spawn(0, load()) {
+3 -1
View File
@@ -60,10 +60,12 @@ pub fn crl(hosts: &[&str], signer: &dyn SigningKey) -> Result<Vec<u8>, BoxError>
Ok(list.signed_by(&Issuer::from_params(&ca, signer))?.der().to_vec()) Ok(list.signed_by(&Issuer::from_params(&ca, signer))?.der().to_vec())
} }
pub const COMMON_NAME: &str = "Strix Firewall";
/// Also used by the issuer, whose issuer name must match the CA's subject. /// Also used by the issuer, whose issuer name must match the CA's subject.
pub fn params(hosts: &[&str]) -> CertificateParams { pub fn params(hosts: &[&str]) -> CertificateParams {
let mut name = DistinguishedName::new(); let mut name = DistinguishedName::new();
name.push(DnType::CommonName, "Strix Firewall"); name.push(DnType::CommonName, COMMON_NAME);
name.push(DnType::OrganizationName, "Strix"); name.push(DnType::OrganizationName, "Strix");
let mut params = CertificateParams::default(); let mut params = CertificateParams::default();
+1 -1
View File
@@ -3,7 +3,7 @@ mod issuer;
mod route; mod route;
mod screen; mod screen;
pub use ca::{crl, CaSigner, CertificateAuthority}; pub use ca::{crl, CaSigner, CertificateAuthority, COMMON_NAME};
use crate::Firewall; use crate::Firewall;
use issuer::ConstrainedCertIssuer; use issuer::ConstrainedCertIssuer;
+3 -5
View File
@@ -2,6 +2,8 @@ mod enclave;
mod install; mod install;
mod uninstall; mod uninstall;
pub use uninstall::uninstall;
use crate::config::FirewallConfig; use crate::config::FirewallConfig;
use crate::{mitm, telemetry}; use crate::{mitm, telemetry};
use crate::Firewall; use crate::Firewall;
@@ -49,15 +51,11 @@ impl TransparentProxyHandler for Handler {
TransparentProxyConfig::new().with_rules(vec![tcp_rule(80), tcp_rule(443)]) TransparentProxyConfig::new().with_rules(vec![tcp_rule(80), tcp_rule(443)])
} }
/// Setup requests from the installer. Never empty: Swift cannot tell that from no reply. /// Setup requests from the app. Never empty: Swift cannot tell that from no reply.
async fn handle_app_message(&self, _exec: Executor, message: Bytes) -> Option<Bytes> { async fn handle_app_message(&self, _exec: Executor, message: Bytes) -> Option<Bytes> {
let message = String::from_utf8_lossy(&message).into_owned(); let message = String::from_utf8_lossy(&message).into_owned();
Some(Bytes::from(match message.as_str() { Some(Bytes::from(match message.as_str() {
"ca" => self.ca.pem.clone(), "ca" => self.ca.pem.clone(),
"uninstall" => match uninstall::certificate_authority(&self.ca) {
Ok(()) => "ok".to_owned(),
Err(error) => format!("error: {error}"),
},
other => format!("error: unknown message {other}"), other => format!("error: unknown message {other}"),
})) }))
} }
+28 -7
View File
@@ -1,26 +1,47 @@
use super::enclave::EnclaveKey; use super::enclave::EnclaveKey;
use super::{CA_FILE, KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE}; use super::{CA_FILE, KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE};
use crate::mitm::CertificateAuthority; use crate::mitm::COMMON_NAME;
use rama::error::BoxError; use rama::error::BoxError;
use rama::net::apple::networkextension::system_keychain; use rama::net::apple::networkextension::system_keychain;
use security_framework::item::{ItemClass, ItemSearchOptions, Limit};
use security_framework::os::macos::keychain::SecKeychain;
use security_framework_sys::base::errSecItemNotFound;
use std::io::ErrorKind; use std::io::ErrorKind;
use tracing::info; use tracing::info;
/// Installed by the pkg. /// Installed by the pkg.
const ENV_AGENT: &str = "/Library/LaunchAgents/com.usestrix.firewall.env.plist"; const ENV_AGENT: &str = "/Library/LaunchAgents/com.usestrix.firewall.env.plist";
/// Removes what only root can: the CA, its enclave key and exported copy, and the env /// Removes what only root can: every CA the extension minted, its enclave key and exported
/// agent. Trust settings need an authorization dialog, so the installer drops those itself. /// copy, and the env agent. Trust settings need an authorization dialog, so the app drops those.
pub fn certificate_authority(ca: &CertificateAuthority) -> Result<(), BoxError> { pub fn uninstall() -> Result<(), BoxError> {
system_keychain::uninstall_system_ca(&ca.crt.to_der()?)?; let deleted = ItemSearchOptions::new()
.class(ItemClass::certificate())
.keychains(&[SecKeychain::open("/Library/Keychains/System.keychain")?])
.label(COMMON_NAME)
.limit(Limit::All)
.delete();
let mut removed = match deleted {
Ok(()) => true,
Err(error) if error.code() == errSecItemNotFound => false,
Err(error) => return Err(error.into()),
};
if system_keychain::load_secret(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT)?.is_some() {
system_keychain::delete_secret(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT)?; system_keychain::delete_secret(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT)?;
removed = true;
}
EnclaveKey::delete()?; EnclaveKey::delete()?;
for path in [CA_FILE, ENV_AGENT] { for path in [CA_FILE, ENV_AGENT] {
match std::fs::remove_file(path) { match std::fs::remove_file(path) {
Err(error) if error.kind() != ErrorKind::NotFound => return Err(error.into()), Ok(()) => removed = true,
_ => {} Err(error) if error.kind() == ErrorKind::NotFound => {}
Err(error) => return Err(error.into()),
} }
} }
if removed {
info!("removed the CA from the system keychain"); info!("removed the CA from the system keychain");
} else {
info!("nothing to uninstall");
}
Ok(()) Ok(())
} }
+9
View File
@@ -1,2 +1,11 @@
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub mod macos; pub mod macos;
#[cfg(target_os = "macos")]
pub use macos::uninstall;
/// Elsewhere nothing is installed system-wide.
#[cfg(not(target_os = "macos"))]
pub fn uninstall() -> Result<(), rama::error::BoxError> {
Ok(())
}
+2 -2
View File
@@ -3,5 +3,5 @@ Firewall.xcodeproj
.env .env
Extension/Extension.entitlements Extension/Extension.entitlements
Extension/Info.plist Extension/Info.plist
Installer/Installer.entitlements App/App.entitlements
Installer/Info.plist App/Info.plist
@@ -1,7 +1,7 @@
import SwiftUI import SwiftUI
struct ContentView: View { struct ContentView: View {
@ObservedObject var model: InstallerModel @ObservedObject var model: FirewallModel
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
var body: some View { var body: some View {
@@ -1,8 +1,8 @@
import SwiftUI import SwiftUI
@main @main
struct InstallerApp: App { struct FirewallApp: App {
@StateObject private var model = InstallerModel() @StateObject private var model = FirewallModel()
var body: some Scene { var body: some Scene {
MenuBarExtra { MenuBarExtra {
@@ -31,7 +31,7 @@ enum SetupWindow {
/// An accessory app restores no windows, so setup is raised from the label, the one view built at launch. /// An accessory app restores no windows, so setup is raised from the label, the one view built at launch.
private struct MenuBarLabel: View { private struct MenuBarLabel: View {
@ObservedObject var model: InstallerModel @ObservedObject var model: FirewallModel
@Environment(\.openWindow) private var openWindow @Environment(\.openWindow) private var openWindow
@State private var raised = false @State private var raised = false
File renamed without changes.
@@ -23,22 +23,23 @@ enum StepStatus: Equatable {
} }
} }
enum InstallerError: Error, LocalizedError { enum FirewallError: Error, LocalizedError {
case extensionUnavailable case extensionUnavailable
case extensionRefused(String)
case screeningDidNotStart(NEVPNStatus) case screeningDidNotStart(NEVPNStatus)
case extensionStopped(String)
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
case .extensionUnavailable: return "The extension is not running" case .extensionUnavailable: return "The extension is not running"
case let .extensionRefused(reply): return "The extension replied \(reply)"
case let .screeningDidNotStart(status): return "Screening did not start (\(status.label))" case let .screeningDidNotStart(status): return "Screening did not start (\(status.label))"
case let .extensionStopped(reason):
return "The extension stopped: \(reason). Its log: log show --last 5m --predicate 'subsystem BEGINSWITH \"\(FirewallIDs.appBundleID)\"'"
} }
} }
} }
@MainActor @MainActor
final class InstallerModel: NSObject, ObservableObject { final class FirewallModel: NSObject, ObservableObject {
@Published private(set) var extensionStep = StepStatus.pending @Published private(set) var extensionStep = StepStatus.pending
@Published private(set) var screeningStep = StepStatus.pending @Published private(set) var screeningStep = StepStatus.pending
@Published private(set) var trustStep = StepStatus.pending @Published private(set) var trustStep = StepStatus.pending
@@ -124,9 +125,9 @@ final class InstallerModel: NSObject, ObservableObject {
log("Uninstall cancelled") log("Uninstall cancelled")
return return
} }
// The extension owns the private key, so ask it before tearing down the session that carries the request. // Stop screening first so the extension cannot mint a fresh CA after it is removed.
let strandedKey = await removeStoredCA()
await removeConfiguration() await removeConfiguration()
let strandedKey = removeStoredCA()
await deactivate() await deactivate()
if let strandedKey { trustStep = .failed(strandedKey) } if let strandedKey { trustStep = .failed(strandedKey) }
@@ -167,7 +168,7 @@ final class InstallerModel: NSObject, ObservableObject {
} }
} }
private func stage(_ step: ReferenceWritableKeyPath<InstallerModel, StepStatus>, private func stage(_ step: ReferenceWritableKeyPath<FirewallModel, StepStatus>,
running: String, running: String,
work: @MainActor () async throws -> String) async -> Bool { work: @MainActor () async throws -> String) async -> Bool {
self[keyPath: step] = .running(running) self[keyPath: step] = .running(running)
@@ -245,17 +246,18 @@ final class InstallerModel: NSObject, ObservableObject {
return true return true
} }
/// Only root can delete the key, so the extension does it; returns what to report if it survived. /// The extension wrote these as root, so the bundled CLI removes them behind an admin prompt; returns what to report if it failed.
private func removeStoredCA() async -> String? { private func removeStoredCA() -> String? {
do { let cli = Bundle.main.path(forAuxiliaryExecutable: "firewall") ?? ""
let reply = try await providerMessage("uninstall") let script = "do shell script quoted form of \"\(cli)\" & \" uninstall\" with administrator privileges"
guard reply == "ok" else { throw InstallerError.extensionRefused(reply) } var error: NSDictionary?
log("Removed the CA, its exported copy and the env agent") NSAppleScript(source: script)?.executeAndReturnError(&error)
return nil if let error {
} catch { log("Could not remove the CA: \(error[NSAppleScript.errorMessage] ?? error)")
log("Could not remove the CA: \(error.localizedDescription)")
return "The CA private key is still in the System keychain" return "The CA private key is still in the System keychain"
} }
log("Removed the CA, its exported copy and the env agent")
return nil
} }
// MARK: - System extension // MARK: - System extension
@@ -295,16 +297,22 @@ final class InstallerModel: NSObject, ObservableObject {
try await manager.loadFromPreferences() try await manager.loadFromPreferences()
watched = manager watched = manager
screeningStep = .running("Starting the extension")
try manager.connection.startVPNTunnel() try manager.connection.startVPNTunnel()
guard try await wait(for: .connected, on: manager.connection) else { guard try await wait(for: .connected, on: manager.connection) else {
throw InstallerError.screeningDidNotStart(manager.connection.status) do {
try await manager.connection.fetchLastDisconnectError()
} catch {
throw FirewallError.extensionStopped(error.localizedDescription)
}
throw FirewallError.screeningDidNotStart(manager.connection.status)
} }
return "Screening package downloads" return "Screening package downloads"
} }
private func restartScreening() async throws -> String { private func restartScreening() async throws -> String {
guard let manager = try await loadManager() else { guard let manager = try await loadManager() else {
throw InstallerError.extensionUnavailable throw FirewallError.extensionUnavailable
} }
manager.connection.stopVPNTunnel() manager.connection.stopVPNTunnel()
_ = try await wait(for: .disconnected, on: manager.connection) _ = try await wait(for: .disconnected, on: manager.connection)
@@ -312,7 +320,7 @@ final class InstallerModel: NSObject, ObservableObject {
} }
private func stopScreening() async throws -> String { private func stopScreening() async throws -> String {
guard let manager = try await loadManager() else { throw InstallerError.extensionUnavailable } guard let manager = try await loadManager() else { throw FirewallError.extensionUnavailable }
manager.isEnabled = false manager.isEnabled = false
try await manager.saveToPreferences() try await manager.saveToPreferences()
manager.connection.stopVPNTunnel() manager.connection.stopVPNTunnel()
@@ -320,9 +328,13 @@ final class InstallerModel: NSObject, ObservableObject {
return "Screening disabled" return "Screening disabled"
} }
/// Gives up early once a start has failed, which shows as connecting then disconnected.
private func wait(for status: NEVPNStatus, on connection: NEVPNConnection) async throws -> Bool { private func wait(for status: NEVPNStatus, on connection: NEVPNConnection) async throws -> Bool {
var connecting = false
for _ in 0 ..< 60 { for _ in 0 ..< 60 {
if connection.status == status { return true } if connection.status == status { return true }
if connection.status == .connecting { connecting = true }
if connecting && connection.status == .disconnected { return false }
try await Task.sleep(nanoseconds: 250_000_000) try await Task.sleep(nanoseconds: 250_000_000)
} }
return false return false
@@ -358,13 +370,13 @@ final class InstallerModel: NSObject, ObservableObject {
private func providerMessage(_ message: String) async throws -> String { private func providerMessage(_ message: String) async throws -> String {
guard let session = watched?.connection as? NETunnelProviderSession else { guard let session = watched?.connection as? NETunnelProviderSession else {
throw InstallerError.extensionUnavailable throw FirewallError.extensionUnavailable
} }
return try await withCheckedThrowingContinuation { continuation in return try await withCheckedThrowingContinuation { continuation in
do { do {
try session.sendProviderMessage(Data(message.utf8)) { response in try session.sendProviderMessage(Data(message.utf8)) { response in
guard let response, let reply = String(data: response, encoding: .utf8) else { guard let response, let reply = String(data: response, encoding: .utf8) else {
continuation.resume(throwing: InstallerError.extensionUnavailable) continuation.resume(throwing: FirewallError.extensionUnavailable)
return return
} }
continuation.resume(returning: reply) continuation.resume(returning: reply)
@@ -381,7 +393,7 @@ final class InstallerModel: NSObject, ObservableObject {
} }
} }
extension InstallerModel: OSSystemExtensionRequestDelegate { extension FirewallModel: OSSystemExtensionRequestDelegate {
nonisolated func request(_ request: OSSystemExtensionRequest, nonisolated func request(_ request: OSSystemExtensionRequest,
actionForReplacingExtension existing: OSSystemExtensionProperties, actionForReplacingExtension existing: OSSystemExtensionProperties,
withExtension replacement: OSSystemExtensionProperties) -> OSSystemExtensionRequest.ReplacementAction { withExtension replacement: OSSystemExtensionProperties) -> OSSystemExtensionRequest.ReplacementAction {
@@ -1,7 +1,7 @@
import SwiftUI import SwiftUI
struct MenuBarView: View { struct MenuBarView: View {
@ObservedObject var model: InstallerModel @ObservedObject var model: FirewallModel
@Environment(\.openWindow) private var openWindow @Environment(\.openWindow) private var openWindow
var body: some View { var body: some View {
@@ -9,7 +9,7 @@ struct MenuBarView: View {
Divider() Divider()
Button("Installer") { Button("Setup") {
SetupWindow.open(openWindow) SetupWindow.open(openWindow)
} }
File renamed without changes.
+9 -8
View File
@@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
# Builds and signs Firewall.app, installs it to /Applications, and packages it as build/Firewall.pkg. # Builds and signs StrixFirewall.app, installs it to /Applications, and packages it as build/Firewall.pkg.
# The package is notarized with the notarytool keychain profile in NOTARY_PROFILE; `build.sh debug` # The package is notarized with the notarytool keychain profile in NOTARY_PROFILE; `build.sh debug`
# makes a Debug build and skips notarization. # makes a Debug build and skips notarization.
# #
@@ -32,7 +32,7 @@ VERSION=0.1
INSTALLER_IDENTITY=${INSTALLER_IDENTITY:-Developer ID Installer} INSTALLER_IDENTITY=${INSTALLER_IDENTITY:-Developer ID Installer}
PROFILES="$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles" PROFILES="$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"
APP=build/Build/Products/$CONFIGURATION/Installer.app APP=build/Build/Products/$CONFIGURATION/StrixFirewall.app
EXT=$APP/Contents/Library/SystemExtensions/$EXT_ID.systemextension EXT=$APP/Contents/Library/SystemExtensions/$EXT_ID.systemextension
PKG_ROOT=build/pkg-root PKG_ROOT=build/pkg-root
@@ -47,7 +47,7 @@ main() {
build() { build() {
echo "==> Building" echo "==> Building"
xcodegen generate >/dev/null xcodegen generate >/dev/null
xcodebuild -project Firewall.xcodeproj -scheme Installer -configuration $CONFIGURATION \ xcodebuild -project Firewall.xcodeproj -scheme StrixFirewall -configuration $CONFIGURATION \
-derivedDataPath build CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO \ -derivedDataPath build 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
} }
@@ -56,18 +56,19 @@ sign() {
echo "==> Signing with $IDENTITY" echo "==> Signing with $IDENTITY"
cp "$(profile_for $APP_ID)" "$APP/Contents/embedded.provisionprofile" cp "$(profile_for $APP_ID)" "$APP/Contents/embedded.provisionprofile"
cp "$(profile_for $EXT_ID)" "$EXT/Contents/embedded.provisionprofile" cp "$(profile_for $EXT_ID)" "$EXT/Contents/embedded.provisionprofile"
codesign --force --options runtime --timestamp -s "$IDENTITY" "$APP/Contents/MacOS/firewall"
codesign --force --options runtime --timestamp \ codesign --force --options runtime --timestamp \
--entitlements Extension/Extension.entitlements -s "$IDENTITY" "$EXT" --entitlements Extension/Extension.entitlements -s "$IDENTITY" "$EXT"
codesign --force --options runtime --timestamp \ codesign --force --options runtime --timestamp \
--entitlements Installer/Installer.entitlements -s "$IDENTITY" "$APP" --entitlements App/App.entitlements -s "$IDENTITY" "$APP"
codesign --verify --deep --strict "$APP" codesign --verify --deep --strict "$APP"
} }
# The extension can only be activated from /Applications. # The extension can only be activated from /Applications.
install_app() { install_app() {
echo "==> Installing /Applications/Firewall.app" echo "==> Installing /Applications/StrixFirewall.app"
rm -rf /Applications/Firewall.app rm -rf /Applications/StrixFirewall.app
cp -R "$APP" /Applications/Firewall.app cp -R "$APP" /Applications/StrixFirewall.app
} }
# Marked non-relocatable so the installer never updates another copy with the same bundle id. # Marked non-relocatable so the installer never updates another copy with the same bundle id.
@@ -75,7 +76,7 @@ package() {
echo "==> Packaging build/Firewall.pkg" echo "==> Packaging build/Firewall.pkg"
rm -rf "$PKG_ROOT" rm -rf "$PKG_ROOT"
mkdir -p "$PKG_ROOT/Applications" "$PKG_ROOT/Library/LaunchAgents" mkdir -p "$PKG_ROOT/Applications" "$PKG_ROOT/Library/LaunchAgents"
cp -R "$APP" "$PKG_ROOT/Applications/Firewall.app" cp -R "$APP" "$PKG_ROOT/Applications/StrixFirewall.app"
cp LaunchAgents/com.usestrix.firewall.env.plist "$PKG_ROOT/Library/LaunchAgents/" cp LaunchAgents/com.usestrix.firewall.env.plist "$PKG_ROOT/Library/LaunchAgents/"
pkgbuild --analyze --root "$PKG_ROOT" build/components.plist >/dev/null pkgbuild --analyze --root "$PKG_ROOT" build/components.plist >/dev/null
+1 -1
View File
@@ -6,5 +6,5 @@ uid=$(id -u "$user")
agent=/Library/LaunchAgents/com.usestrix.firewall.env.plist agent=/Library/LaunchAgents/com.usestrix.firewall.env.plist
launchctl bootout "gui/$uid" "$agent" 2>/dev/null launchctl bootout "gui/$uid" "$agent" 2>/dev/null
launchctl bootstrap "gui/$uid" "$agent" launchctl bootstrap "gui/$uid" "$agent"
launchctl asuser "$uid" sudo -u "$user" open -a /Applications/Firewall.app --args --install launchctl asuser "$uid" sudo -u "$user" open -a /Applications/StrixFirewall.app --args --install
exit 0 exit 0
+15 -4
View File
@@ -87,25 +87,25 @@ targets:
$([ "$CONFIGURATION" = Debug ] || echo --release) $([ "$CONFIGURATION" = Debug ] || echo --release)
done done
Installer: StrixFirewall:
type: application type: application
platform: macOS platform: macOS
sources: sources:
- Installer - App
- path: ../assets/logo-transparent.png - path: ../assets/logo-transparent.png
buildPhase: resources buildPhase: resources
settings: settings:
base: base:
PRODUCT_BUNDLE_IDENTIFIER: com.usestrix.firewall PRODUCT_BUNDLE_IDENTIFIER: com.usestrix.firewall
info: info:
path: Installer/Info.plist path: App/Info.plist
properties: properties:
CFBundleShortVersionString: "0.1" CFBundleShortVersionString: "0.1"
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
entitlements: entitlements:
path: Installer/Installer.entitlements path: App/App.entitlements
properties: properties:
com.apple.application-identifier: ${TEAM}.com.usestrix.firewall com.apple.application-identifier: ${TEAM}.com.usestrix.firewall
com.apple.developer.team-identifier: ${TEAM} com.apple.developer.team-identifier: ${TEAM}
@@ -117,3 +117,14 @@ targets:
- target: Extension - target: Extension
embed: true embed: true
codeSign: true codeSign: true
# Uninstall runs the CLI as root to remove what the extension left.
postBuildScripts:
- name: Embed the CLI
basedOnDependencyAnalysis: false
script: |
export PATH="$HOME/.cargo/bin:$PATH"
unset CC CXX CFLAGS CXXFLAGS
export SDKROOT="$(xcrun --sdk macosx --show-sdk-path)"
cd "$SRCROOT/../core"
cargo build --bin firewall --target aarch64-apple-darwin $([ "$CONFIGURATION" = Debug ] || echo --release)
cp "target/aarch64-apple-darwin/$CARGO_PROFILE/firewall" "$TARGET_BUILD_DIR/$EXECUTABLE_FOLDER_PATH/"