macOS Kernel & System Extensions · Lesson 3

The Swift Code

Two small files: the app that asks the OS to install the extension, and the provider that judges every network flow. All Swift, no C.

🎯
By the end you'll understand every line of the demo's main.swift and FilterDataProvider.swift — what runs in the app, what runs in the extension, and where a connection gets allowed or dropped. The files are in courses/macos-kernel-extensions/demo/netfilter/.

A content filter is two cooperating programs. The container app is a thin launcher: its whole job is to get the extension installed and switched on. The provider is the extension: a long-lived, sandboxed process the OS keeps alive and hands every new connection.

1The provider — FilterDataProvider.swift

This subclass of NEFilterDataProvider is the heart. Three overrides: start, stop, and the per-flow verdict.

demo/netfilter/Sources/Filter/FilterDataProvider.swift
import NetworkExtension
import os.log

final class FilterDataProvider: NEFilterDataProvider {

  // Called once when the filter is switched on. Install the rules.
  override func startFilter(completionHandler: @escaping (Error?) -> Void) {
    // Empty rule list + defaultAction .filterData => EVERY new flow
    // is routed to handleNewFlow(_:). The simplest "see everything".
    let settings = NEFilterSettings(rules: [], defaultAction: .filterData)
    apply(settings) { error in completionHandler(error) }
  }

  override func stopFilter(with reason: NEProviderStopReason,
                           completionHandler: @escaping () -> Void) {
    completionHandler()
  }

  // THE verdict. Called once per new connection. Return allow/drop/…
  override func handleNewFlow(_ flow: NEFilterFlow) -> NEFilterNewFlowVerdict {
    if let socket = flow as? NEFilterSocketFlow {
      let remote = socket.remoteEndpoint.map { String(describing: $0) } ?? "?"
      os_log("flow → %{public}@", remote)   // shows up in `log stream`
    }
    return .allow()   // change to .drop() to block
  }
}

What to notice

LineWhy it matters
defaultAction: .filterDataThe trick for a demo: with an empty rule array, every flow falls through to the default, and .filterData means "ask the provider" → handleNewFlow fires for all traffic. A real product ships specific NEFilterRules for efficiency.
handleNewFlow return valueA synchronous verdict per connection: .allow(), .drop(), .needRules(), or pause/peek-data variants. This is where a firewall makes its decision.
os_log(...)The extension is a background process with no console — os_log to a named subsystem is how you observe it (Lesson 5's log stream).
You never call FilterDataProvider()The framework instantiates it, by the name in NEProviderClasses. Your code only responds.
Block a site in one character

Change return .allow() to return .drop() and every connection dies. To block selectively, inspect socket.remoteEndpoint (host/port) or socket.remoteHostname and return .drop() only for matches. That's a content blocker in ~3 lines.

2The extension's entry point — Filter/main.swift

A System Extension is just an executable. Its main hands control to the framework, which then owns the provider's lifecycle:

demo/netfilter/Sources/Filter/main.swift
import NetworkExtension
autoreleasepool { NEProvider.startSystemExtensionMode() }
dispatchMain()

That's the whole entry point. startSystemExtensionMode() tells NetworkExtension "I'm a sysext — read my Info.plist, instantiate my provider class, and start calling it." dispatchMain() parks the thread so the process stays alive.

3The container app — App/main.swift

The app does two things in order: (a) activate the extension, then (b) configure the filter so it actually starts. Both are async with delegate/closure callbacks.

demo/netfilter/Sources/App/main.swift (abridged)
import SystemExtensions; import NetworkExtension

let extBundleID = "com.example.netfilterdemo.filter"

final class Controller: NSObject, OSSystemExtensionRequestDelegate {
  func activate() {
    let req = OSSystemExtensionRequest
      .activationRequest(forExtensionWithIdentifier: extBundleID, queue: .main)
    req.delegate = self
    OSSystemExtensionManager.shared.submitRequest(req)   // (a) install/activate
  }

  // The OS calls these back:
  func requestNeedsUserApproval(_ r: OSSystemExtensionRequest) {
    print("⏳ approve it in System Settings ▸ Login Items & Extensions")
  }
  func request(_ r: OSSystemExtensionRequest,
               didFinishWithResult result: OSSystemExtensionRequest.Result) {
    enableContentFilter()   // (b) now turn the filter on
  }
  func request(_ r: OSSystemExtensionRequest, didFailWithError error: Error) {
    print("❌ \(error)"); exit(1)
  }

  func enableContentFilter() {
    let mgr = NEFilterManager.shared()
    mgr.loadFromPreferences { _ in
      if mgr.providerConfiguration == nil {
        let cfg = NEFilterProviderConfiguration()
        cfg.filterSockets = true
        mgr.providerConfiguration = cfg; mgr.localizedDescription = "NetFilterDemo"
      }
      mgr.isEnabled = true
      mgr.saveToPreferences { _ in print("✅ filtering") }
    }
  }
}
Controller().activate(); dispatchMain()
Two different approvals

There are two system prompts. (a) OSSystemExtensionRequest installs the extension (approve under Network Extensions). (b) NEFilterManager.saveToPreferences turns on content filtering (a separate "…would like to filter network content" prompt). Both are required before handleNewFlow ever runs.

4Who calls what — the whole loop

StepProcessCall
1appsubmitRequest(activationRequest) → user approves
2appNEFilterManager … isEnabled = true; saveToPreferences → user approves
3OSlaunches the extension; calls NEProvider.startSystemExtensionMode()'s machinery → your startFilter
4extensionevery new connection → handleNewFlow → .allow() / .drop()

5Quick check

If you delete handleNewFlow entirely, what happens to traffic?

With defaultAction: .filterData the framework still routes flows to the provider, but the base class's default verdict applies — you'd lose your logging/decision point. The point of a data provider is to override handleNewFlow; without it there's no filter logic.

Why is the container app needed at all — can't the extension install itself?

No. Only an app calling OSSystemExtensionRequest (with the system-extension.install entitlement) can ask the OS to activate an extension, and only the user can approve it. The extension can't self-install.

Which file runs in which process?

App/main.swift → the container app process. Filter/main.swift + FilterDataProvider.swift → the separate, sandboxed extension process the OS manages.

💬 Ask your teacher. Want the verdict types (pause, peek-data, needRules) explained, or how to block a specific domain by name? Ask. Otherwise say "Lesson 4" — we compile and sign all of this from the CLI.