macOS Kernel & System Extensions · Lesson 5 · the payoff

Cross the Gate & Watch It Filter

Authorize the restricted entitlement, get the extension activated and approved, and watch your own code judge every connection on this Mac.

🎯
By the end your filter is listed in 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.
Honesty note

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.

1Pick your door

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.

RECOMMENDED · SIP STAYS ON

① Provisioning profile

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.

NO ACCOUNT NEEDED · SIP OFF

② Developer mode

Boot to recoveryOS, csrutil disable, reboot, systemextensionsctl developer on. Then unprovisioned dev builds run. The fallback — see §6.

2Provision the capability (Door ①)

  1. Register two App IDs at developer.apple.com ▸ Identifiers: 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.)
  2. Create two macOS Development provisioning profiles (Profiles), one per App ID, tied to your Apple Development cert and this Mac. Download both .provisionprofile files.
  3. Drop them into the bundles as embedded.provisionprofile — the profile is what AMFI checks to authorize the entitlement:
    $ cp app.provisionprofile build/NetFilterDemo.app/Contents/embedded.provisionprofile $ cp ext.provisionprofile build/NetFilterDemo.app/Contents/Library/SystemExtensions/\ com.example.netfilterdemo.filter.systemextension/Contents/embedded.provisionprofile
    (Add these two cp lines to build.sh just before the codesign calls.)
  4. Add the matching keys to app.entitlements / ext.entitlements so the signature lines up with the profile:
    <key>com.apple.application-identifier</key><string>7LS573L8C4.com.example.netfilterdemo</string> <key>com.apple.developer.team-identifier</key><string>7LS573L8C4</string>
  5. Re-run ./build.sh. Same output as Lesson 4, but now the binary is provisioned.
Shortcut: let Xcode provision once

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.

3Activate — and approve

The app must live in /Applications when SIP is on. Copy it, then run it:

▶ run me$ cp -R build/NetFilterDemo.app /Applications/ $ /Applications/NetFilterDemo.app/Contents/MacOS/NetFilterDemo → submitting activation request for com.example.netfilterdemo.filter ⏳ NEEDS USER APPROVAL — open System Settings ▸ General ▸ Login Items & Extensions
  1. Approve the extension — System Settings ▸ General ▸ Login Items & Extensions ▸ Network Extensions ▸ enable NetFilterDemo. (You'll also get a Gatekeeper "allow" the first time.)
  2. Approve content filtering — a second prompt, "NetFilterDemo would like to filter network content." Click Allow. (This is the NEFilterManager.saveToPreferences step from Lesson 3.)

Confirm it's live — the command from Lesson 1, now showing your extension:

▶ run me$ systemextensionsctl list --- com.apple.system_extension.network_extension enabled active teamID bundleID (version) name [state] * * 7LS573L8C4 com.example.netfilterdemo.filter (1.0/1) NetFilterDemo [activated enabled]
✓ It's running on your Mac

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.

4Watch it see your traffic

Open two terminals. In the first, stream the extension's log; in the second, generate a flow:

▶ terminal 1$ sudo log stream --predicate 'subsystem == "com.example.netfilterdemo.filter"'
▶ terminal 2$ curl -s https://example.com > /dev/null

Terminal 1 lights up — your os_log from handleNewFlow, one line per connection:

… NetFilterDemo filter: startFilter: content filter starting up … NetFilterDemo filter: applySettings OK — now filtering all flows … NetFilterDemo filter: flow out example.com:443 … NetFilterDemo filter: flow out 17.253.144.10:443

5Make it actually block something

So far every verdict is .allow(). Turn the filter into a blocker by editing FilterDataProvider.swift:

override func handleNewFlow(_ flow: NEFilterFlow) -> NEFilterNewFlowVerdict { if let s = flow as? NEFilterSocketFlow, let host = s.remoteEndpoint.map({ String(describing: $0) }), host.contains("example.com") { os_log("BLOCKED %{public}@", host) return .drop() // ⟵ kill the connection } return .allow() }

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.

6Door ② — developer mode (no account, SIP off)

Only if you can't/won't provision

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.

7Flashcards

What single artifact lets a restricted-entitlement extension run with SIP on?

A provisioning profile (embedded as Contents/embedded.provisionprofile) that authorizes the entitlement for your App ID + cert + device.

How many user approvals does a content filter need, and for what?

Two: one to activate the system extension (Network Extensions), one to enable content filtering (NEFilterManager). Plus a first-run Gatekeeper allow.

How do you watch the extension at runtime?

sudo log stream --predicate 'subsystem == "<your subsystem>"' — the extension is a headless process; os_log is its only voice.

Where must the app live for activation with SIP on?

In /Applications. (Developer mode relaxes this, but developer mode needs SIP off.)

💬 Ask your teacher — especially here. Provisioning is the fiddliest part. If you hit 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".