Skip to content

feat(macos): run VPN in a packet tunnel system extension - #2354

Open
xNOTHlNGx wants to merge 14 commits into
hiddify:mainfrom
xNOTHlNGx:feat/macos-network-extension
Open

xNOTHlNGx wants to merge 14 commits into
hiddify:mainfrom
xNOTHlNGx:feat/macos-network-extension

Conversation

@xNOTHlNGx

Copy link
Copy Markdown
Contributor

feat(macos): run VPN in a packet tunnel system extension

Related to #187 and #1788.

Scope and behavior

This PR moves native macOS VPN mode into an OS-managed packet tunnel system extension. The Flutter app runs as the logged-in user, while macOS manages the tunnel lifecycle.

Quitting the app leaves the VPN running. Reopening the app restores its connection state and control client. Disconnect disables on-demand reconnection before stopping the tunnel, and changing profiles or connection modes stops the previous engine first. Proxy and system-proxy modes continue to use the foreground core.

Changes

  • Add the packet tunnel target, Swift provider/platform adapter, and Go core bridge.
  • Use SystemExtensions for extension activation and approval, and NetworkExtension for VPN configuration, start/stop, saved state, and reset.
  • Authenticate the extension’s loopback gRPC service with a random secret. Permit statistics, logs, URL tests, and proxy selection; keep lifecycle and configuration changes behind NetworkExtension.
  • Snapshot profile content and local certificate/key/rule-set resources so reconnecting does not depend on the app’s profile directory. The combined local-resource snapshot is limited to 32 MiB. Relative paths resolve against the core working directory; absolute paths are read from their specified locations.
  • Support Darwin process ownership lookup and network interface monitoring.
  • Present system extension approval as an expected setup step. System extension approval and VPN configuration approval are separate macOS permission flows.
  • Pin the tunnel core to the 4.1.0 release sources and Go 1.25.6.
  • Add unsigned macOS PR builds and signing/notarization steps for distributed artifacts.
  • Fix universal Flutter builds on affected Apple lipo versions by verifying each requested architecture separately.
  • Raise the minimum macOS version from 10.15 to 12.0.

Ordinary Debug builds default to proxy support with Network Extension disabled. Local VPN development requires a paid Apple Developer team and signing/provisioning for both targets; setup instructions are below.

The private SIP-disabled testing kit and ad hoc VPN signing exception are outside this PR. Android, iOS, Windows, and Linux retain their existing platform backends; shared Dart changes add macOS lifecycle integration and propagate configuration errors.

Maintainer setup required before distributing macOS builds

The notarization workflow is implemented, but release maintainers must supply the signing identities, provisioning profiles, and notarization credentials. The Developer ID distribution path has not been validated using the local ad hoc test build.

1. Configure Apple signing and capabilities

Configure these explicit App IDs under the same Apple Developer team:

Target Bundle identifier
Runner app.hiddify.com
HiddifyPacketTunnel app.hiddify.com.HiddifyPacketTunnel

Enable Network Extension for both App IDs and generate or regenerate their macOS Developer ID provisioning profiles. Both profiles must authorize the Developer ID Application certificate that will be imported into CI. The distribution entitlement is packet-tunnel-provider-systemextension.

Runner also requires com.apple.developer.system-extension.install. Both targets use this application group:

HIDDIFY_VPN_APP_GROUP = $(TeamIdentifierPrefix)app.hiddify.com.vpn

For a team prefix of YOURTEAMID., the resolved group is YOURTEAMID.app.hiddify.com.vpn. The provider’s NEMachServiceName is that same group followed by .HiddifyPacketTunnel.

Keep the application group identical in both targets and ensure its prefix matches the signing team. This macOS team-prefixed group does not require registration in the Developer portal; it is distinct from a group.-prefixed provisioned group. See Apple’s app group guidance and Network Extensions entitlement guidance.

Sign Runner and the embedded provider with Developer ID Application; sign the installer package with Developer ID Installer. Preserve hardened runtime, secure timestamps, and distribution entitlements without get-task-allow. Hardened runtime and Release/Profile base-entitlement settings are already configured in this branch. See Apple’s notarization requirements.

2. Prepare and supply the CI secrets

Add the following secrets in the repository that runs the workflow, under Settings → Secrets and variables → Actions → Repository secrets. Organization secrets are also suitable if that repository has access to them.

Secret Required value
APPLE_CERTIFICATE_P12 Base64-encoded PKCS#12 containing both Developer ID Application and Developer ID Installer identities, including their private keys.
APPLE_CERTIFICATE_P12_PASSWORD Password used when exporting that PKCS#12 archive.
APPLE_MOBILE_PROVISIONING_PROFILES_TARGZ_BASE64 Base64-encoded tar.gz containing the macOS Developer ID provisioning profiles for both targets, with the profile files at the archive root.
APPLE_MACOS_SIGNING_XCCONFIG Literal multiline xcconfig text containing the team, signing identity/style, and separate host/provider profile settings. Do not base64-encode this value.
APPLE_MACOS_INSTALLER_SIGN_IDENTITY Full installed certificate name, such as Developer ID Installer: Organization (TEAMID).
APPLE_NOTARIZATION_APPLE_ID Apple ID authorized to notarize software for the selected team.
APPLE_NOTARIZATION_TEAM_ID The same Apple Developer team ID used for signing.
APPLE_NOTARIZATION_PASSWORD App-specific password for the notarization Apple ID, rather than its normal account password.

In Keychain Access, export both Developer ID signing identities, including their private keys, together as a password-protected .p12 file. On macOS, this command copies its base64 representation to the clipboard for APPLE_CERTIFICATE_P12:

base64 -i /absolute/path/to/developer-id-identities.p12 | tr -d '\n' | pbcopy

Create the provisioning archive with the two files at its root. Replace the directory and filenames below with the downloaded profiles:

tar -czf /absolute/path/to/macos-developer-id-profiles.tar.gz \
  -C /absolute/path/to/profile-files \
  Runner.provisionprofile \
  HiddifyPacketTunnel.provisionprofile

base64 -i /absolute/path/to/macos-developer-id-profiles.tar.gz \
  | tr -d '\n' | pbcopy

Paste that clipboard value into APPLE_MOBILE_PROVISIONING_PROFILES_TARGZ_BASE64. CI extracts the archive directly into its provisioning-profile directory.

Example APPLE_MACOS_SIGNING_XCCONFIG:

DEVELOPMENT_TEAM = YOURTEAMID
HIDDIFY_CODE_SIGN_STYLE = Manual
HIDDIFY_CODE_SIGN_IDENTITY = Developer ID Application: Organization (YOURTEAMID)
HIDDIFY_HOST_PROVISIONING_PROFILE = YOUR_RUNNER_DEVELOPER_ID_PROFILE
HIDDIFY_TUNNEL_PROVISIONING_PROFILE = YOUR_TUNNEL_DEVELOPER_ID_PROFILE

Replace every placeholder. The two provisioning settings select the installed profiles by their internal Name or UUID, not their downloaded filenames. Each profile must match its target’s bundle identifier, team, capabilities, and imported Developer ID Application certificate.

PR builds use CODE_SIGNING_ALLOWED = NO and skip notarization. Every macOS workflow run producing uploaded artifacts requires this setup, including draft builds from pushes.

3. Validate the distribution path

Runner and the embedded provider must already be correctly signed before packaging. macos/scripts/notarize-release.sh signs the installer package and notarizes/staples the distribution artifacts; it does not sign or repair the application bundle.

After packaging, the script:

  • Signs the PKG with the Developer ID Installer identity and a secure timestamp.
  • Submits both DMG and PKG using notarytool.
  • Requires an Accepted result for each submission.
  • Staples and validates their notarization tickets before upload.

An invalid submission stops that macOS artifact pipeline.

Before releasing, maintainers should validate the signed, notarized installation on a Mac with SIP enabled: installation in /Applications, system extension and VPN configuration approval, connection, quit/reopen, disconnect, and reconnect. Local ad hoc testing does not verify this distribution path.

Local VPN development

For local VPN development, copy the existing signing example from the repository root:

cp macos/Runner/Configs/NetworkExtension.local.xcconfig.example \
  macos/Runner/Configs/NetworkExtension.local.xcconfig

Replace YOURTEAMID in the copied file with your paid Apple Developer team ID and configure Apple Development signing/provisioning for both Runner and HiddifyPacketTunnel.

The example enables the extension in Debug builds, selects Apple Development signing, and uses the host’s VPN development entitlements. Ordinary Debug builds keep the extension disabled until these overrides are supplied.

Apple Development signing is for local VPN development. Developer ID signing and notarization are required for distributed Release/Profile builds.

Local testing

Test machine: Apple M4 MacBook Air, arm64, macOS 27.2 (26B5091g), with SIP disabled. Build tools: Xcode 27.0 (27A266a), Flutter 3.38.5, Dart 3.10.4, and Go 1.25.6 for the tunnel core.

Demo

I used a separate private source snapshot with distinct local bundle IDs and an ad hoc Debug signing patch. The normal repository retains its Apple-team signing requirement for VPN builds. On that local test build, I manually tested VPN connection, quitting and reopening the app, and reconnecting.

Compilation and stability validation:

  • All 34 Flutter tests passed.
  • All four Go test functions passed, including authentication, privileged RPC restrictions, and TCP/UDP process lookup.
  • Changed Dart files passed analysis and formatting; Go formatting, shell syntax, and diff whitespace checks passed.
  • A universal Release build with CODE_SIGNING_ALLOWED = NO passed. Both arm64 and x86_64 slices were verified in the app, embedded provider, Flutter/Dart frameworks, and desktop core.
  • The private arm64 Debug build and bundle signature verification passed.

Runtime testing was on arm64. Intel runtime behavior and Developer ID signing/notarization with SIP enabled remain for maintainer validation.

@hiddify-com

Copy link
Copy Markdown
Contributor

thank you for your contribution

we will take. it. into consideration sooon

Preserve the macOS managed VPN lifecycle while adopting upstream logging and dependency changes. Avoid restarting a tunnel already restored by macOS on app startup.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants