Crate networkextension
net-apple-networkextension only.Expand description
Apple Network Extension support for rama.
Scope: this crate has been developed and tested with macOS System Extensions as the primary target. Other use cases — macOS app extensions, iOS app extensions, and so on — may work but have not been tested and are not a current maintainer priority. If you have such a use case and run into issues, please open a feature request on GitHub.
Official Apple documentation about the Network Extension Framework can be consulted at: https://developer.apple.com/documentation/networkextension.
§Tech Notes
- Network Extension Provider Packaging
- TN3134: Network Extension provider deployment
- TN3120: Expected use cases for Network Extension packet tunnel providers
- iOS memory limits
- ~ 15 MiB for App/Dns proxy providers on iOS, no limit on MacOS
- Exporting a Developer ID Network Extension
- Debugging a Network Extension Provider
Below is relevant information communicated from some of the above sources.
§Terminology
As clarified by Quinn “The Eskimo!” from Apple Developer Technical Support:
When talking about extensions on Apple platforms, it’s important to get your terminology straight.
- The application in which the extension is embedded is called the container application.
- The host application is the application using the extension.
In this case, the host application isn’t actually an application, but rather the system itself.
§Communicating with Extensions
With an app extension there are two communication options:
- App-provider messages
- App groups
App-provider messages are supported by NE directly. In the container app,
send a message to the provider by calling sendProviderMessage(_:responseHandler:)
method. In the appex, receive that message by overriding the
handleAppMessage(_:completionHandler:) method.
For transparent proxy support provided by this crate this is on the Rust (sysext) side as easy as implementing the
TransparentProxyHandler::handle_app_messagetrait method.
An appex can also implement inter-process communication (IPC) using various system IPC primitives. Both the container app and the appex claim access to the app group via the com.apple.security.application-groups entitlement. They can then set up IPC using various APIs, as explain in the documentation for that entitlement.
With a system extension the story is very different.
App-provider messages are supported, but they are rarely used. Rather,
most products use XPC for their communication. In the sysex,
publish a named XPC endpoint by setting the NEMachServiceName property in
its Info.plist. Listen for XPC connections
on that endpoint using the XPC API of your choice.
Note For more information about the available XPC APIs, see XPC Resources.
In the container app, connect to that named XPC endpoint using the XPC Mach service name API.
For example, with NSXPCConnection, initialise the connection with init(machServiceName:options:),
passing in the string from NEMachServiceName. To maximise security, set the .privileged flag.
Note XPC Resources has a link to a post that explains why this flag is important.
Rama offers XPC support via the
rama-net-apple-xpccrate, which is also available asrama::net::apple::xpcwhen enabling thenet-apple-xpcfeature on Apple vendor targets.
If the container app is sandboxed — necessary if you ship on the Mac App Store — then the endpoint name must be prefixed by an app group ID that’s accessible to that app, lest the App Sandbox deny the connection. See the app groups documentation for the specifics.
When implementing an XPC listener in your sysex, keep in mind that:
Your sysex’s named XPC endpoint is registered in the global namespace. Any process on the system can open a connection to it
[1]. Your XPC listener must be prepared for this. If you want to restrict connections to just your container app, see XPC Resources for a link to a post that explains how to do that. Even if you restrict access in that way, it’s still possible for multiple instances of your container app to be running simultaneously, each with its own connection to your sysex. This happens, for example, if there are multiple GUI users logged in and different users run your container app. Design your XPC protocol with this in mind. Your sysex only gets one named XPC endpoint, and thus one XPC listener. If your sysex includes multiple NE providers, take that into account when you design your XPC protocol.
[1]Assuming that connection isn’t blocked by some other mechanism, like the App Sandbox.
§Wiring up XPC for a sysex NE provider in practice
The notes above are accurate but skip the practical setup. The recipe below is
distilled from the working transparent-proxy example shipped in this repository
(ffi/apple/examples/transparent_proxy) — follow it and you should not need
to repeat the trial-and-error we went through.
What you need:
-
An app group ID, shared by the container app and the sysex. This is the prefix macOS / launchd will accept as a Mach service name from a sandboxed or NE-style process — without it,
xpc_connection_create_mach_service(orNSXPCConnection) traffic is silently dropped, andlaunchdwill refuse to register the listener inside the sysex.- macOS: the legacy
<TEAM_ID>.<bundle-id-prefix>form is enough (e.g.ADPG6C355H.org.example.tproxy). It does not need to start withgroup.on macOS, and does not have to be created in the Apple Developer portal for local developer signing — Xcode automatic signing accepts<AppIdentifierPrefix><bundle-id>directly. - iOS (and macOS in distribution / App Store contexts where you cannot
rely on the legacy form): create a real App Group identifier in the
Apple Developer portal under
Certificates, Identifiers & Profiles → Identifiers → App Groups. These
identifiers must start with
group.(e.g.group.org.example.tproxy). Enable the App Groups capability on both App IDs (container and provider) and add the identifier to each.
- macOS: the legacy
-
The same app group ID listed in the entitlements of both binaries. Both the container app and the sysex must declare it under
com.apple.security.application-groups:<key>com.apple.security.application-groups</key> <array> <string>$(APP_GROUP_ID)</string> </array>If only one side declares it,
launchdwill allow the listener to come up but the peer will not be able to reach it: the connection appears to succeed (XPC is lazy) and then fails on the first send. -
NEMachServiceNamedeclared inside theNetworkExtensiondict of the sysex’sInfo.plist, prefixed by the app group ID. This is the single name thatsysextduses to generate the launchdMachServicesentry for the extension. The prefix-must-match-an-app-group rule applies here too — pick any unique suffix you like, but the value must start with the app group ID:<key>NetworkExtension</key> <dict> <key>NEProviderClasses</key> <dict> <key>com.apple.networkextension.app-proxy</key> <string>YourModule.YourProviderClass</string> </dict> <key>NEMachServiceName</key> <string>$(APP_GROUP_ID).provider</string> </dict>The container app should read the same value (do not re-derive it from
Bundle.main.bundleIdentifier, the two namespaces are different). The transparent-proxy example exposes it as aProviderMachServiceNamekey in the container’s ownInfo.plistso both bundles share one source of truth via theAPP_GROUP_IDbuild setting. -
A reinstall after any change to
NEMachServiceName.sysextdonly readsInfo.plistwhen the extension is (re)activated, and it only writes theMachServicesentry into the generated launchd job at that moment. EditingNEMachServiceNamein place and rebuilding is not enough; you must trigger a deactivate + reactivate cycle (in the example this isjust install-tproxy-dev-reset-profile). Confirm afterwards with:sudo launchctl print system/<sysex-bundle-id> | grep -A 5 -i machservicesA correctly registered listener shows up as e.g.:
MachServices = { ADPG6C355H.org.example.tproxy.provider => 0 }If the
MachServicesblock is empty or missing, the prefix does not match a declared app group, the entitlements were stripped during signing, orsysextdhas a stale registration — see the example’s Troubleshooting section for the full decision tree. -
A handshake-friendly XPC protocol.
XpcConnectionon the client side is lazy, so peer-requirement and prefix mismatches surface asXpcConnectionError::PeerRequirementFailed(or a silent disconnect) on the first send, not at construction. Send something cheap and idempotent early (a “ping” /updateSettingsstyle call) so misconfigurations fail loudly during development rather than the first time a real workload runs.
On the Rust side the only thing you need to know is that the same
NEMachServiceName string is what you pass to
rama::net::apple::xpc::XpcListenerConfig::new(service_name) — there is no
separate registration step. As long as the launchd MachServices entry above
exists, XpcListener::bind(...) will succeed and the container app’s
xpc_connection_create_mach_service(<same name>) will reach it. The
transparent-proxy example carries the service name from the container app to
the sysex through NETunnelProviderProtocol.providerConfiguration, which is
the simplest pattern when you want the sysex to learn its own name without
re-reading Info.plist.
§Inter-provider Communication
A sysex can include multiple types of NE providers. For example, a single sysex might include a content filter and a DNS proxy provider. In that case the system instantiates all of the NE providers in the same sysex process. These instances can communicate without using IPC, for example, by storing shared state in global variables (with suitable locking, of course).
It’s also possible for a single container app to contain multiple sysexen, each including a single NE provider. In that case the system instantiates the NE providers in separate processes, one for each sysex. If these providers need to communicate, they have to use IPC.
In the appex case, the system instantiates each provider in its own process. If two providers need to communicate, they have to use IPC.
§Managing Secrets
An appex runs in a user context and thus can store secrets, like VPN credentials, in the keychain. On macOS this includes both the data protection keychain and the file-based keychain. It can also use a keychain access group to share secrets with its container app. See Sharing access to keychain items among a collection of apps.
Note If you’re not familiar with the different types of keychain available on macOS, see TN3137 On Mac keychain APIs and implementations.
A sysex runs in the global context and thus doesn’t have access to user state. It also doesn’t have access to the data protection keychain. It must use the file-based keychain, and specifically the System keychain. That means there’s no good way to share secrets with the container app.
Instead, do all your keychain operations in the sysex.
If the container app needs to work with a secret, have it pass that request
to the sysex via IPC. For example, if the user wants to use a digital
identity as a VPN credential, have the container app get the PKCS#12
data and password and then pass that to the sysex so that it can import
the digital identity into the keychain.
This crate offers system keychain support via the
system_keychainmodule (only available on macOS).
Some keychain features require the data protection keychain, including:
- iCloud Keychain. See the kSecAttrSynchronizable attribute.
- Protecting an item with biometrics (Touch ID and Face ID).
- Protecting a keychain item with the Secure Enclave.
None of these are available to a sysex using the System Keychain.
However, a sysex can still use the Secure Enclave directly via Apple
CryptoKit’s SecureEnclave.P256.KeyAgreement.PrivateKey, which does not
go through the Data Protection Keychain. The
system_keychain::secure_enclave submodule wraps that path: mint a key
with kSecAttrAccessibleAlways accessibility (the only class that works
before login in a sysex daemon), persist its opaque blob anywhere, and
use it to encrypt arbitrary bytes. The Rust API is backed by the
RamaAppleSecureEnclave Swift product shipped from this repository’s
Package.swift; the consumer’s final binary must link it. See
https://developer.apple.com/forums/thread/804612 for the underlying
Apple guidance.
§Learn More
Learn more about rama:
- Github: https://github.com/plabayo/rama
- Book: https://ramaproxy.org/book/