Firewall rules
AiFw rules are stateful pf rules that live in a dedicated aifw anchor and are persisted in SQLite. The rule engine compiles each row into pf syntax, calls pfctl (or the in-memory mock on Linux/WSL), and never touches the system pf config. Match by interface, address family, protocol, source/destination with optional invert, attach a schedule, and reorder by drag-and-drop.
Quickstart
Open the Web UI and go to Firewall → Rules → Add rule. Choose an action (pass, block, block drop, or block return), a direction (in, out, any), an IP version (inet, inet6, both — default), a protocol, source and destination addresses with optional ports, and an optional schedule.
Create one from the API:
curl -X POST https://aifw.local/api/v1/rules \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"action": "pass",
"direction": "in",
"ip_version": "both",
"protocol": "tcp",
"rule_match": {
"src_addr": "any",
"dst_addr": "any",
"dst_port": "443"
},
"log": false,
"quick": true,
"label": "allow-https"
}'
Reorder rules in bulk:
curl -X PUT https://aifw.local/api/v1/rules/reorder \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"order": ["<uuid-1>", "<uuid-2>", "<uuid-3>"]}'
Push the new ruleset to pf:
curl -X POST https://aifw.local/api/v1/reload \
-H "Authorization: Bearer $TOKEN"
CLI
aifw rules add --action pass --direction in --proto tcp --dst-port 443 --label "allow-https"
aifw rules add --action block --direction in --proto tcp --dst-port 22 --src 10.0.0.0/8
aifw rules list
aifw rules remove <uuid>
aifw reload
API endpoints
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/rules |
List all AiFw rules |
GET |
/api/v1/rules/system |
Show the system pf rules outside the aifw anchor |
GET |
/api/v1/rules/{id} |
Get one rule |
POST |
/api/v1/rules |
Create a rule |
PUT |
/api/v1/rules/{id} |
Update a rule |
DELETE |
/api/v1/rules/{id} |
Delete a rule |
PUT |
/api/v1/rules/reorder |
Reorder rules by UUID array |
PUT |
/api/v1/rules/block-logging |
Toggle the global block-rules-log default |
POST |
/api/v1/reload |
Recompile and apply all rules + NAT to pf |
GET |
/api/v1/aliases |
List aliases |
POST |
/api/v1/aliases |
Create an alias |
PUT |
/api/v1/aliases/{id} |
Update an alias |
DELETE |
/api/v1/aliases/{id} |
Delete an alias |
GET |
/api/v1/schedules |
List schedules |
POST |
/api/v1/schedules |
Create a schedule |
Aliases
An alias is a named bag of addresses, networks, hosts, or ports that is referenced in the source or destination of a rule. Aliases compile to pf tables on reload, so adding an entry is one INSERT plus one reload — no rule edit required. Use them for “trusted admins”, “blocked countries”, “internal RFC1918”, etc. They keep the rule list short and let you bulk-update membership without touching policy.
Scheduling
A schedule is a named time window with two fields:
time_ranges— one or moreHH:MM-HH:MMranges, e.g."08:00-12:00,13:00-17:00".days_of_week— comma-separated short names, e.g."mon,tue,wed,thu,fri".
Attach a schedule_id to a rule and the rule is only loaded into pf during the active window. Outside the window the rule is dropped from the compiled ruleset.
Evaluation semantics:
- Windows are evaluated against the appliance’s local time (system timezone, including DST).
- Range ends are exclusive (
08:00-17:00deactivates at 17:00). A range that crosses midnight (22:00-06:00) belongs to the day it starts and runs into the next morning.00:00-00:00means the whole day. - The daemon re-evaluates schedules once per minute (the same cadence pfSense/OPNsense use) and reloads the ruleset when a rule crosses a window boundary; schedule edits via the API reload immediately.
- A disabled schedule, or a
schedule_idpointing at a deleted schedule, does not constrain the rule — it stays loaded as if unscheduled (fail-open is deliberate: rules can beblockas well aspass, and unloading a block rule on a dangling reference would open the firewall).
Policy routing (rule gateway)
A pass rule can reference a Multi-WAN gateway by id (the gateway field; a dropdown in the rule form). Matching traffic is compiled with pf route-to (interface next_hop) so it egresses via that gateway instead of the default route.
- Only
passrules route; the field is ignored on block rules. - The API rejects a gateway reference that isn’t an existing Multi-WAN gateway — it is never silently discarded.
- While the gateway is down (as judged by its health probes) the rule compiles without
route-toand traffic falls back to default routing; the daemon reloads the ruleset on gateway state transitions. Deleting a gateway unlinks it from rules. - For weighted balancing or failover groups, use Multi-WAN policies instead; the rule gateway pins traffic to a single gateway.
Traffic shaping
Queue policy lives alongside rules. Three queue disciplines are supported via the aifw queue subcommand:
- CoDel — default, low-latency AQM. Best for general LAN/WAN egress.
- HFSC — hierarchical share-based scheduler with bandwidth percentages. Use when you need strict per-class allocations (VoIP, interactive, default, bulk).
- PRIQ — strict priority queueing. Highest-priority class always preempts lower ones.
aifw queue add --name voip --interface em0 --type hfsc --bandwidth 1Gb --class voip --pct 20
Queues are a separate anchor; the firewall rule references the queue by name.
Configuration
| Field | Default | Notes |
|---|---|---|
| Anchor name | aifw |
All AiFw filter rules go here; the system pf ruleset just hooks the anchor |
quick |
true |
The first matching rule wins. Disable for traditional last-match semantics |
log |
false for pass; always on for block |
Block rules are forced to log so dropped traffic appears in pflog0 |
state_options.tracking |
keep state |
none, modulate state, and synproxy state are also supported |
ip_version |
both |
Set to inet or inet6 to scope a rule to a single family |
priority |
100 |
Used as the default sort key; UI drag-and-drop overrides this via /rules/reorder |
State tracking only applies to pass rules. Block rules in pf cannot keep state.