Authorize the restricted entitlement, get the extension activated and approved, and watch your own code judge every connection on this Mac.
systemextensionsctl list as activated enabled, and log stream shows it logging real flows as you browse — then you'll flip one line to block a site.Lessons 1–4 were fully verified on this Mac. This lesson's two steps — provisioning (needs your Apple account) and approval (needs your click) — are yours to complete; I can't auto-run them. Below is exactly what to do and exactly what you'll see. Ask me the moment anything diverges.
From Lesson 4: a restricted entitlement loads only via a provisioning profile (SIP on) or developer mode (SIP off). You have a paid account, so take Door ① and keep SIP on.
Authorize the entitlement with a profile from your Developer account. No reboots, no lowered security. The way every shipping extension works. The rest of this lesson.
Boot to recoveryOS, csrutil disable, reboot, systemextensionsctl developer on. Then unprovisioned dev builds run. The fallback — see §6.
com.example.netfilterdemo and com.example.netfilterdemo.filter. On each, enable the Network Extensions capability. (It's self-service — no request form, unlike Endpoint Security.).provisionprofile files.embedded.provisionprofile — the profile is what AMFI checks to authorize the entitlement:
cp lines to build.sh just before the codesign calls.)app.entitlements / ext.entitlements so the signature lines up with the profile:
./build.sh. Same output as Lesson 4, but now the binary is provisioned.Hate the portal clicking? Open the demo as an Xcode project once and build with xcodebuild -allowProvisioningUpdates — Xcode auto-creates the App IDs, enables the capability, and downloads the profiles. After that you can go back to build.sh using the profiles it generated.
The app must live in /Applications when SIP is on. Copy it, then run it:
NEFilterManager.saveToPreferences step from Lesson 3.)Confirm it's live — the command from Lesson 1, now showing your extension:
A System Extension you wrote, built, and signed from the command line is now a live, sandboxed process — listed right next to Tailscale and Mac Monitor. That's the mission's core goal, met with SIP on.
Open two terminals. In the first, stream the extension's log; in the second, generate a flow:
Terminal 1 lights up — your os_log from handleNewFlow, one line per connection:
So far every verdict is .allow(). Turn the filter into a blocker by editing FilterDataProvider.swift:
Rebuild, bump CFBundleVersion, re-run the app (it sends a replacement activation — actionForReplacingExtension returns .replace), and now curl https://example.com hangs/fails while everything else works. You've written a firewall rule.
Boot to 1TR (hold the power button) ▸ Utilities ▸ Terminal ▸ csrutil disable ▸ reboot. Then systemextensionsctl developer on. Now the unprovisioned build from Lesson 4 activates (it can even run from outside /Applications). Re-enable with csrutil enable when done. This is the same security trade-off real kexts demand — which is exactly why Door ① exists.
A provisioning profile (embedded as Contents/embedded.provisionprofile) that authorizes the entitlement for your App ID + cert + device.
Two: one to activate the system extension (Network Extensions), one to enable content filtering (NEFilterManager). Plus a first-run Gatekeeper allow.
sudo log stream --predicate 'subsystem == "<your subsystem>"' — the extension is a headless process; os_log is its only voice.
In /Applications. (Developer mode relaxes this, but developer mode needs SIP off.)
Killed: 9, a profile-mismatch codesign error, or the approval toggle won't stick, paste the output and I'll debug it with you live. Ready to operate it day-to-day? Say "Lesson 6".