Warden
A declarative macOS firewall, built on this SDK as an example. The firewall
policy is a JSON/YAML document applied authoritatively — the document is the
complete, enforced state of the firewall.
Its architecture is modeled on Objective-See's LuLu
(the NEFilterDataProvider filtering, rule engine, XPC control channel, and process
attribution map onto LuLu's targets), but Warden is configuration-driven rather
than interactive. It demonstrates three capabilities of this SDK:
- NEFilterDataProvider subclassing in Go (
bindings/runtime/purego NewDelegate/RegisterClass),
- NSXPCConnection from Go (
bindings/runtime/purego XPCProtocol/XPCConn/XPCListener),
- idiomatic CGo libraries (
opinionated/idiomatic/libraries/libproc for process attribution).
Component layout (left: the LuLu target it's modeled on; right: this Go example):
| LuLu (ObjC original) |
Warden (this Go example) |
Shared/ (Rule, protos, consts) |
shared/ |
Extension/Rules |
rules/ |
Extension/FilterDataProvider |
extension/provider.go |
Extension/XPCListener/XPCDaemon |
extension/daemon.go |
Extension/Process/Binary |
extension/process.go (via libproc) |
Extension/main.m |
extension/run.go + cmd/wardend |
App/ |
app/ + cmd/warden |
Data flow
new outbound flow
→ extension: handleNewFlow:(NEFilterFlow*) (provider.go)
→ attribute to process via libproc proc_pidpath (process.go)
→ rules.Engine.Find(processKey, remoteAddr, port)
allow → NEFilterNewFlowVerdict allowVerdict
block → NEFilterNewFlowVerdict dropVerdict
none → DefaultAllow verdict (Warden is policy-driven; where LuLu would
prompt the user, Warden applies the configured default)
app ──XPC──► daemon (XPCDaemonProtocol): get/add/delete/toggle rules
(mutations rejected in config-managed mode)
Build
go build ./examples/warden/...
Two binaries:
cmd/warden — the controlling app/CLI (pure purego).
cmd/wardend — the network extension (links CGo libproc).
Run
The CLI control surface runs as an ordinary binary once a daemon is reachable:
warden list # list rules from the daemon (XPC)
warden allow /usr/bin/curl # add an allow rule for a process
warden block /usr/bin/nc 1.2.3.4
warden delete <key> <uuid>
warden activate # submit the system-extension activation request
The rule engine is plain Go and unit-testable in isolation.
Declarative configuration (JSON or YAML)
Firewall rules can be expressed as a declarative document and reconciled onto the
firewall, kubectl apply style. The same schema is authored in either JSON or
YAML (format chosen by file extension, or sniffed):
version: "1"
defaultAction: block
rules:
- name: curl to example.com
path: /usr/bin/curl
action: allow
endpoints:
- host: example.com
port: "443"
- name: netcat
path: /usr/bin/nc
action: block
Apply it two ways:
warden apply config/firewall.example.yaml # reconcile a running daemon over XPC
WARDEN_CONFIG=config/firewall.example.yaml wardend # daemon reconciles at startup
warden export current.yaml # dump the live rules to a document
Reconciliation (config package) is authoritative: the document is the
complete firewall state.
- Idempotent — rule identity is content-addressed (a hash of path + action +
host + port), so re-applying an unchanged document is a no-op; editing an
endpoint cleanly replaces the old rule.
- Authoritative / prune-all — any rule not in the document is removed,
regardless of who created it. A rule added out-of-band (interactively or by an
XPC client) cannot survive an apply, so the policy can't be bypassed by adding
local allow rules.
Managed mode (enforcement)
A one-shot apply only enforces at apply-time. When the daemon is governed by a
config (WARDEN_CONFIG), it runs in managed mode, which closes the bypass
window:
- Locked mutation surface — the daemon rejects rule mutations over XPC
(
addRule/deleteRule/toggle). Policy can only change by editing the
document. (This also disables the interactive alert→rule flow — the correct
behaviour for an enforced policy.)
- Continuous reconciliation — the daemon re-applies the config on an interval
(
reconcileInterval, 60s), so drift from direct tampering of the persisted
rules.json is reverted automatically.
Tradeoff: authoritative mode wipes ad-hoc/interactive rules on every apply.
That is what makes the policy enforceable, but it means you cannot mix a
declarative baseline with persistent interactive additions. Supporting both would
need a layered policy model (a locked base + a user overlay) — intentionally not
built here.
The engine-local path (config.Apply over a rules.Engine) is plain Go and is
covered by config/config_test.go (including a test that an out-of-band rule is
pruned by an authoritative apply); the XPC path drives the same reconciler.
Sample documents: config/firewall.example.{yaml,json}.
YAML support uses gopkg.in/yaml.v3 — the one place this example departs from the
SDK's otherwise dependency-free posture (JSON alone is stdlib).
What requires Apple provisioning (not doable with go build alone)
macOS only loads cmd/wardend as a system extension when it is packaged and
signed correctly — this is an OS constraint, not an SDK one:
- The extension binary must live in a host app bundle at
Warden.app/Contents/Library/SystemExtensions/com.example.warden.extension.systemextension/,
with the Info.plist here naming WardenFilterDataProvider as its
NSExtensionPrincipalClass (the Go daemon registers that exact ObjC class at
startup, subclassing NEFilterDataProvider).
- Both the app and the extension must be code-signed with the entitlements
in
Warden.entitlements / extension/WardenExtension.entitlements. The
com.apple.developer.networking.networkextension entitlement is restricted
and must be granted to your App ID through an Apple Developer provisioning
profile.
- The mach service name (
shared.DaemonMachServiceName) must match an
NEMachServiceName declared in the extension's entitlements/Info.plist and be
team-prefixed.
Without a Developer account + signing, the activation request is rejected by the
OS and the filter never loads — the firewall logic, rule engine, XPC wiring, and
process attribution are all exercised by the code here, but a live install is
gated on provisioning you control.
Note: running a NetworkExtension principal class implemented as a purego-
registered ObjC class is at the edge of what macOS supports; treat the live
system-extension load as experimental. The CLI, rules engine, and XPC layer are
the parts meant to run today.