diff --git a/client/platforms/macos/daemon/macosfirewall.cpp b/client/platforms/macos/daemon/macosfirewall.cpp index 0fe51f23..5211c440 100644 --- a/client/platforms/macos/daemon/macosfirewall.cpp +++ b/client/platforms/macos/daemon/macosfirewall.cpp @@ -43,8 +43,16 @@ namespace { #include "macosfirewall.h" -#define ResourceDir qApp->applicationDirPath() + "/pf" -#define DaemonDataDir qApp->applicationDirPath() + "/pf" +#include +#include + +// Read-only rules bundled with the application. +#define ResourceDir (qApp->applicationDirPath() + "/pf") + +// Writable location that does NOT live inside the signed bundle. Using a +// constant path under /Library/Application Support keeps the signature intact +// and is accessible to the root helper. +#define DaemonDataDir QStringLiteral("/Library/Application Support/AmneziaVPN/pf") #include @@ -121,6 +129,8 @@ void MacOSFirewall::install() logger.info() << "Installing PF root anchor"; installRootAnchors(); + // Ensure writable directory exists, then store the token there. + QDir().mkpath(DaemonDataDir); execute(QStringLiteral("pfctl -E 2>&1 | grep -F 'Token : ' | cut -c9- > '%1/pf.token'").arg(DaemonDataDir)); } diff --git a/deploy/build_macos.sh b/deploy/build_macos.sh index 3723fb14..a44e4a9b 100755 --- a/deploy/build_macos.sh +++ b/deploy/build_macos.sh @@ -35,7 +35,7 @@ DEPLOY_DATA_DIR=$PROJECT_DIR/deploy/data/macos # Search Qt if [ -z "${QT_VERSION+x}" ]; then -QT_VERSION=6.4.3; +QT_VERSION=6.8.3; QT_BIN_DIR=$HOME/Qt/$QT_VERSION/macos/bin fi @@ -71,14 +71,16 @@ rsync -av --exclude="$PLIST_NAME" --exclude=post_install.sh --exclude=post_unins if [ "${MAC_CERT_PW+x}" ]; then +# Path to the p12 that contains the Developer ID *Application* certificate CERTIFICATE_P12=$DEPLOY_DIR/PrivacyTechAppleCertDeveloperId.p12 -mkdir -p "$PKG_DIR" "$SCRIPTS_DIR" "$RESOURCES_DIR" "$UNINSTALL_SCRIPTS_DIR" -# Ensure launchd plist is present in the app bundle root -cp "$DEPLOY_DATA_DIR/$PLIST_NAME" "$BUNDLE_DIR/$PLIST_NAME" -pkgbuild --component "$BUNDLE_DIR" \ - --install-location "/Applications" \ - security find-identity -p codesigning +# Ensure launchd plist is bundled, but place it inside Resources so that +# the bundle keeps a valid structure (nothing but `Contents` at the root). +mkdir -p "$BUNDLE_DIR/Contents/Resources" +cp "$DEPLOY_DATA_DIR/$PLIST_NAME" "$BUNDLE_DIR/Contents/Resources/$PLIST_NAME" + +# Show available signing identities (useful for debugging) +security find-identity -p codesigning || true echo "Signing App bundle..." /usr/bin/codesign --deep --force --verbose --timestamp -o runtime --sign "$MAC_SIGNER_ID" "$BUNDLE_DIR" @@ -133,11 +135,35 @@ cp "$PROJECT_DIR/LICENSE" "$RESOURCES_DIR/LICENSE" APP_VERSION=$(grep -m1 -E 'project\(' "$PROJECT_DIR/CMakeLists.txt" | sed -E 's/.*VERSION ([0-9.]+).*/\1/') echo "Building component package $INSTALL_PKG ..." + +# Disable bundle relocation so the app always ends up in /Applications even if +# another copy is lying around somewhere. We do this by letting pkgbuild +# analyse the contents, flipping the BundleIsRelocatable flag to false for every +# bundle it discovers and then feeding that plist back to pkgbuild. + +COMPONENT_PLIST="$PKG_DIR/component.plist" +# Create the component description plist first +pkgbuild --analyze --root "$PKG_ROOT" "$COMPONENT_PLIST" + +# Turn all `BundleIsRelocatable` keys to false (PlistBuddy is available on all +# macOS systems). We first convert to xml1 to ensure predictable formatting. + +# Turn relocation off for every bundle entry in the plist. PlistBuddy cannot +# address keys that contain slashes without quoting, so we iterate through the +# top-level keys it prints. +plutil -convert xml1 "$COMPONENT_PLIST" +for bundle_key in $(/usr/libexec/PlistBuddy -c "Print" "$COMPONENT_PLIST" | awk '/^[ \t]*[A-Za-z0-9].*\.app/ {print $1}'); do + /usr/libexec/PlistBuddy -c "Set :'${bundle_key}':BundleIsRelocatable false" "$COMPONENT_PLIST" || true +done + +# Now build the real payload package with the edited plist so that the final +# PackageInfo contains relocatable="false". pkgbuild --root "$PKG_ROOT" \ --identifier "$APP_DOMAIN" \ --version "$APP_VERSION" \ --install-location "/" \ --scripts "$SCRIPTS_DIR" \ + --component-plist "$COMPONENT_PLIST" \ ${MAC_INSTALLER_SIGNER_ID:+--sign "$MAC_INSTALLER_SIGNER_ID"} \ "$INSTALL_PKG" diff --git a/deploy/data/macos/post_uninstall.sh b/deploy/data/macos/post_uninstall.sh index de7846db..83949552 100755 --- a/deploy/data/macos/post_uninstall.sh +++ b/deploy/data/macos/post_uninstall.sh @@ -32,3 +32,40 @@ sudo rm -rf "$LOG_FOLDER" # Remove any caches left behind rm -rf "$CACHES_FOLDER" + +# Remove PF data directory created by firewall helper, if present +sudo rm -rf "/Library/Application Support/${APP_NAME}/pf" + +# ---------------- PF firewall cleanup ---------------------- +# Rules are loaded under the anchor "amn" (see macosfirewall.cpp) +# Flush only that anchor to avoid destroying user/system rules. + +PF_ANCHOR="amn" + +### Flush all PF rules, NATs, and tables under our anchor and sub-anchors ### +anchors=$(sudo pfctl -s Anchors 2>/dev/null | awk '/^'"${PF_ANCHOR}"'/ {sub(/\*$/, "", $1); print $1}') +for anc in $anchors; do + echo "Flushing PF anchor $anc" + sudo pfctl -a "$anc" -F all 2>/dev/null || true + # flush tables under this anchor + tables=$(sudo pfctl -s Tables 2>/dev/null | awk '/^'"$anc"'/ {print}') + for tbl in $tables; do + echo "Killing PF table $tbl" + sudo pfctl -t "$tbl" -T kill 2>/dev/null || true + done +done + +### Reload default PF config to restore system rules ### +if [ -f /etc/pf.conf ]; then + echo "Restoring system PF config" + sudo pfctl -f /etc/pf.conf 2>/dev/null || true +fi + +### Disable PF if no rules remain ### +if sudo pfctl -s info 2>/dev/null | grep -q '^Status: Enabled' && \ + ! sudo pfctl -sr 2>/dev/null | grep -q .; then + echo "Disabling PF" + sudo pfctl -d 2>/dev/null || true +fi + +# -----------------------------------------------------------