mirror of
https://github.com/usestrix/firewall.git
synced 2026-11-10 05:11:24 -10:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b37f93936e | ||
|
|
bcd26b91b0 | ||
|
|
70e66280f6 | ||
|
|
1879e9061e | ||
|
|
dcee91ef04 |
No files matched your search
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}"),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
system_keychain::delete_secret(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT)?;
|
.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)?;
|
||||||
|
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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
info!("removed the CA from the system keychain");
|
if removed {
|
||||||
|
info!("removed the CA from the system keychain");
|
||||||
|
} else {
|
||||||
|
info!("nothing to uninstall");
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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
|
||||||
File renamed without changes.
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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/"
|
||||||
Reference in new issue
Block a user