⚠️ PROOF OF CONCEPT: DO NOT USE IN PRODUCTIONwallfacer generates nftables commands from natural language. It will not apply rules, roll back mistakes, or guarantee correctness on a live system. It exists to demonstrate a software architecture, not to manage a real firewall. Read the Limitations section before running any generated commands on a machine you care about.
A proof-of-concept firewall assistant that accepts plain-English policy
descriptions and responds with nft(8) commands, clarification requests, or
network inspection queries. The name is borrowed from Liu Cixin's
The Dark Forest: a Wallfacer must act on incomplete information and seek
the truth before acting, because the cost of a wrong assumption is extinction.
The same posture applies here. When a request is ambiguous, wallfacer asks for
clarification rather than guessing; when a request is potentially destructive,
wallfacer asks for the live chain state before generating commands.
$ wallfacer
Wallfacer 0.9.1 - nftables rule assistant
Describe your firewall policy in plain English.
Type 'help' for examples, 'exit' or Ctrl+D to quit.
wallfacer> allow ssh from 192.168.0.0/16
I'll allow SSH (TCP/22) from 192.168.0.0/16 inbound.
Run this to apply:
nft add table inet filter
nft add chain inet filter input { type filter hook input priority 0; }
nft add rule inet filter input ip saddr 192.168.0.0/16 tcp dport 22 accept
wallfacer> show my current rules
To view your current nftables ruleset:
nft list ruleset
wallfacer> rate limit ssh to 3 per minute per source
I'll rate-limit new SSH (TCP/22) connections to 3 per minute per source IP.
Run this to apply:
nft add table inet filter
nft add chain inet filter input { type filter hook input priority 0; }
nft add rule inet filter input tcp dport 22 meter ssh-limit { ip saddr limit rate 3/minute } accept
wallfacer> allow https from the internet
I'll allow HTTPS (TCP/443) inbound.
Run this to apply:
nft add table inet filter
nft add chain inet filter input { type filter hook input priority 0; }
nft add rule inet filter input tcp dport 443 accept
The reasoning core of wallfacer is written in CLIPS (C Language Integrated Production System), a forward-chaining production rule system originally developed at NASA's Johnson Space Center in the mid-1980s. CLIPS belongs to a family of tools inspired by the academic expert system work of that era: systems like MYCIN (medical diagnosis, Stanford, 1972) and XCON (VAX computer configuration, Digital Equipment Corporation, 1980) demonstrated that rule-based reasoning could encode expert knowledge in a form that a computer could apply reliably.
CLIPS is not a scripting language. Instead of writing a sequence of
instructions, you write production rules: if … then … statements that
operate over a shared working memory called the fact base. The CLIPS runtime
runs an inference cycle:
- Match: for each rule, check whether its conditions are satisfied by the current fact base.
- Select: choose which matching rule to fire (based on salience priority and a conflict-resolution strategy).
- Act: execute that rule's actions, which may assert new facts, retract old ones, or modify existing ones.
- Repeat: keep cycling until no rule can fire.
This model is well-suited to problems where the reasoning path is not fixed in advance: new facts trigger new rules, which produce new facts, and so on, until the system reaches a stable state. For firewall policy reasoning the fact base starts with a parsed user request and, after inference, contains a fully resolved nftables rule specification ready to emit as shell commands.
NASA used CLIPS extensively in Space Shuttle operations and flight-rules management. The Java rule engine Jess was directly modelled on CLIPS and was widely used in enterprise decision systems throughout the 2000s.
wallfacer has four layers, with a Python knowledge-acquisition module that translates live kernel state into CLIPS facts on demand:
User text
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Layer 1: NL Parser (parser/nl_parser.py) │
│ │
│ Tokenises input; strips temporal qualifiers; matches against │
│ grammar patterns. Returns a structured IR dict with all │
│ fields, including window-id when a time qualifier is present, │
│ or None + an ambiguity message on failure. │
└──────────────────────────┬──────────────────────────────────────┘
│ IR dict
▼
┌───────────────────────────────────────────────────────────────────┐
│ Layer 2: CLIPS Expert System (wallfacer/clips/*.clp) │
│ │
│ templates.clp : deftemplate declarations and helper │
│ deffunctions (ct-state-expr, action-expr) │
│ classifier_rules.clp: tier-2 escalation rules (salience 80) │
│ core_rules.clp : address family, reject-type, priority, │
│ readiness gate, rate-limit emitter │
│ conflict_rules.clp : anomaly detection (salience -5) │
│ temporal_rules.clp : interval-algebra reasoning (salience -3, │
│ -5) │
│ generated/ : rules produced at build time: │
│ vocabulary.clp service classifiers + action synonyms │
│ hook_matrix.clp direction × rule-type → nft hook │
│ emitter_rules.clp (proto × port? × src?) combinatoric │
│ temporal_composition.clp Allen 13×13 composition table │
└──────────────────────────┬────────────────────────────────────────┘
│ nft-command, rule-warning,
│ needs-ground-truth facts
▼
┌─────────────────────────────────────────────────────────────────┐
│ Layer 3: Python Driver (wallfacer/driver.py) │
│ │
│ Manages the CLIPS environment lifecycle: loads .clp files, │
│ maintains session state across queries (proposed-rule buffer, │
│ CIDR address index, time-window facts), asserts IR as CLIPS │
│ facts, runs inference, collects output facts, and renders them │
│ into nft shell command strings. │
└──────────────────────────┬──────────────────────────────────────┘
│ ProcessResult | FeedRequest | str
▼
┌─────────────────────────────────────────────────────────────────┐
│ Layer 4: Chat Session (wallfacer/chat.py, main.py) │
│ │
│ Classifies input (inspection query vs policy statement), │
│ manages feed lifecycle (FIFO creation, await_feed, retry), │
│ maintains conversation history, formats response types │
│ (commands / info / clarification / feed-request / error). │
└─────────────────────────────────────────────────────────────────┘
When a FeedRequest is returned, the chat session also calls into a
knowledge-acquisition module (parser/nft_json.py) that translates the
live chain state into CLIPS facts:
sudo nft -j list chain inet filter input
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ KA Layer: parser/nft_json.py │
│ │
│ Walks the nft JSON expression tree. Translatable rules become │
│ existing-rule + chain-policy + stateful-accept-exists facts. │
│ Untranslatable rules (named sets, verdict chains, interface │
│ matches, port ranges) become opaque-rule facts so that CLIPS │
│ can honestly report incomplete analysis rather than silently │
│ skipping them. CIDR relationships between proposed and │
│ existing addresses are pre-computed in Python and asserted as │
│ cidr-rel facts; CLIPS never does address arithmetic. │
└─────────────────────────────────────────────────────────────────┘
The CLIPS knowledge base contains two kinds of rules:
Hand-written rules (wallfacer/clips/core_rules.clp, wallfacer/clips/conflict_rules.clp,
wallfacer/clips/temporal_rules.clp) encode reasoning logic that requires judgment:
address family inference, reject-type defaulting, insertion-priority
assignment, the readiness gate, the rate-limit emitter, anomaly detection,
and temporal conflict checking.
Generated rules (wallfacer/clips/generated/*.clp) handle combinatorial tables:
- vocabulary.clp: one rule per service name (ssh → TCP/22, etc.) and one rule per action synonym (allow/permit/pass → accept, …).
- hook_matrix.clp: one rule per valid (direction, rule-type) → hook mapping.
- emitter_rules.clp: one rule per
(proto, has_dst_port, has_src_addr)combination (5 × 2 × 2 = 20 rules). - temporal_composition.clp: 169
(interval-composition r1 r2 result)deffacts encoding the full Allen (1983) 13×13 composition table, computed by exhaustive search and written bygenerators/gen_temporal.py.
Jinja2 templates (templates/*.j2) define the shape of generated rule
classes; Python generator modules (generators/gen_*.py) instantiate them.
Running python build.py or make generate regenerates all four files.
wallfacer applies a three-tier safety model to every incoming request:
-
Tier 0: the answer is correct regardless of existing chain state. Specific accept rules, table and chain creation, inspection queries, and rate-limit rules fall here. wallfacer generates commands immediately.
-
Tier 1: the answer depends only on what has been proposed in this session. wallfacer has already seen this information; no external feed is needed. Conflict detection between session-proposed rules operates at this tier.
-
Tier 2: the answer's safety depends on what is currently installed in the kernel. A wrong assumption here has real consequences: a broad
droprule without act state established acceptpreceding it will terminate the user's active SSH sessions. wallfacer pauses and asks the user to provide the live chain state before generating anything.
The tier-2 classifier runs at CLIPS salience 80, before all other rules.
It escalates when a proposed drop/reject is broad enough (any source, any
port, or any protocol) or targets a management-likely port (22, 80, 443,
3389) without a confirmed stateful accept rule in the chain. It is
silenced once system-state-loaded is asserted for the session.
When escalation is required, wallfacer prints the exact command the user should run and blocks on a session FIFO waiting for the output:
wallfacer> drop all from 203.0.113.0/24
I can generate that rule, but I need to inspect your current chain first.
Reason: broad drop/reject: need to confirm stateful accept rule precedes it in chain
Please run this in another terminal and I'll continue:
sudo nft -j list chain inet filter input | wallfacer --infeed /tmp/wallfacer-xxx/feed
(waiting for feed...)
The user never grants wallfacer privileged access to the kernel.
wallfacer --infeed is a separate, unprivileged sidecar process that reads
stdin (which the user wrote as root in their own shell) and pipes it to the
waiting REPL via a named FIFO.
Once chain state is loaded: or when checking rules proposed earlier in the same session: wallfacer detects five firewall anomaly types drawn from the taxonomy of Al-Shaer and Hamed (2004):
| Group | Anomaly | Description |
|---|---|---|
| F-1 | Broad drop without stateful guard | A broad drop/reject in a chain with no ct state established accept would also terminate currently-established connections. |
| F-2 | Accept shadowed by existing drop | A new accept rule will never fire because a drop already in the live chain covers its source address space. |
| F-3 | Accept shadowed by prior proposed drop | Same anomaly, but the shadowing drop was proposed earlier in this session. |
| F-4 | New drop shadows prior proposed accept | A new drop whose source is a superset of a prior session accept would nullify that accept if inserted before it. |
| F-5 | Opaque-rule incompleteness | The chain contains rules wallfacer cannot decode (named sets, verdict chains, interface matches). Conflict analysis is explicitly flagged as incomplete. |
Groups F-2 through F-4 use CIDR containment arithmetic, pre-computed in
Python as cidr-rel facts before CLIPS sees any addresses. CLIPS reasons
over symbolic relation tokens (contains, contained-by, equal,
overlaps, disjoint) and never performs address arithmetic itself.
wallfacer> allow SSH from 10.0.0.0/8
I'll allow SSH (TCP/22) from 10.0.0.0/8 inbound.
Run this to apply: ...
wallfacer> drop all from 10.0.0.0/8
Before I generate that rule, I need to look at your current firewall.
Why: this could cut off connections already in progress; I need to see
your current firewall first
In another terminal, run:
sudo nft -j list chain inet filter input | wallfacer --infeed /tmp/wallfacer-xxx/feed
(waiting...)
[feed provided]
I'll drop all traffic from 10.0.0.0/8 inbound.
Run this to apply: ...
Advisory:
⚠ Conflicts with 'allow SSH from 10.0.0.0/8' you proposed earlier: this
block covers 10.0.0.0/8, which includes the addresses that allow rule
permits. If applied first, that allow rule will never take effect.
See demo/session.py for a complete annotated demonstration.
Reference: E. Al-Shaer & H. Hamed, "Discovery of Policy Anomalies in Distributed Firewalls," Proceedings of IEEE INFOCOM 2004.
Time-windowed rules can be requested using natural language qualifiers:
wallfacer> allow SSH from 10.0.0.0/8 during business hours
wallfacer> drop telnet from any on weekdays
wallfacer> allow HTTP and HTTPS from 192.168.0.0/16 on weekends
When two rules proposed in the same session have overlapping time windows but contradictory verdicts, wallfacer warns that the rules may apply to the same traffic simultaneously.
Temporal reasoning is built on Allen's Interval Algebra (Allen, 1983), which defines 13 mutually exclusive and exhaustive relations between time intervals:
| Relation | Inverse | Meaning |
|---|---|---|
before |
after |
A ends before B starts |
meets |
met-by |
A ends exactly when B starts |
overlaps |
overlapped-by |
A starts before B; they overlap |
starts |
started-by |
A and B start together; A ends first |
during |
contains |
A is entirely inside B |
finishes |
finished-by |
A and B end together; B starts first |
equals |
equals |
A and B are identical |
Allen (1983) also defines a 13×13 composition table: given that A has
relation r₁ with B, and B has relation r₂ with C, the table gives the set of
possible relations for (A, C). Some compositions yield a single definite
result (e.g. before ∘ before = {before}); others yield a set of
possibilities (e.g. before ∘ during = {before, meets, overlaps, starts, during}).
wallfacer computes this table by exhaustive search over integer intervals
(implemented in wallfacer/temporal.py) and encodes it as CLIPS deffacts
at build time (wallfacer/clips/generated/temporal_composition.clp). The CLIPS
inference rule infer-interval-rel-transitively uses the table to derive
new interval relations when the composition is unambiguous: ambiguous
compositions are not asserted, because wallfacer cannot know which relation
actually holds.
Time is represented as a flat integer day_of_week × 1440 + minutes_since_midnight (Monday = 1, Sunday = 7). Python pre-computes the
Allen relation between any two named windows and asserts it as an
interval-rel fact; CLIPS reasons over the relation symbols rather than
doing calendar arithmetic.
| Name | Interval |
|---|---|
always |
Monday 00:00 – Sunday 24:00 |
business-hours |
Monday 09:00 – Friday 17:00 |
weekdays |
Monday 00:00 – Friday 24:00 |
weekends |
Saturday 00:00 – Sunday 24:00 |
Note: business-hours during weekdays is a provable relation under the
algebra; CLIPS derives it transitively from the pre-computed window facts.
during business hours, in business hours, during business-hours,
on weekdays, on weekday, during weekdays,
on weekends, on weekend, during weekends,
always, at all times.
All phrases are matched case-insensitively and must appear at the end of the input text. Time windows that span the week boundary (e.g. Friday evening to Monday morning) are not representable as a single interval in the current implementation.
Reference: J.F. Allen, "Maintaining Knowledge about Temporal Intervals," Communications of the ACM, 26(11):832–843, 1983.
Requires Python 3.11+. The clips PyPI package (clipspy) bundles the CLIPS
C library and compiles it on install; no system CLIPS package is needed at
runtime.
pip install -e .[dev]Optional development aids (Debian/Ubuntu):
apt install clips clips-docclips provides a standalone CLIPS REPL useful for interactively debugging
.clp files. clips-doc registers documentation metadata (the PDF files
themselves are not included in the Ubuntu package).
make generate # run all four CLIPS generators
make test # pytest with coverage
make all # both in sequence (safe to run from a clean state)
make clean # remove wallfacer/clips/generated/*.clpVerify that all Python and documentation dependencies are present:
python verify_deps.pyRun the annotated demo session:
python demo/session.pyShow CLIPS memory diagnostics at shutdown (useful when profiling the knowledge base: MemoryCalls grows with session length due to symbol interning):
wallfacer --debug| Pattern | Example |
|---|---|
| Action + service name | allow ssh |
| Action + protocol [from addr] | drop tcp from 10.0.0.0/8 |
| Action + service + from addr | allow https from 203.0.113.0/24 |
| Action + protocol + port [from addr] | accept tcp port 8080 |
| Stateful connection accept | accept established and related connections |
| Rate limit per source | rate limit ssh to 3 per minute per source |
| Multi-service [from/to addr] | allow http and https from 192.168.0.0/16 |
| Source address negation | drop all except from 192.168.0.0/16 |
| Masquerade (dynamic outbound SNAT) | masquerade outbound |
| Static SNAT | snat outbound to 203.0.113.1 |
| DNAT / port forward | redirect port 80 to 192.168.1.5:8080 |
| Time-windowed rule | allow ssh from 10.0.0.0/8 during business hours |
| Time-windowed rule | drop telnet from any on weekdays |
Where addr appears above it accepts an IP address, a CIDR prefix, or a
natural-language location phrase (see address synonyms below). Filler words
between the preposition and the address are tolerated, so
allow https from the internet, block ssh from any external host, and
drop tcp from untrusted networks all parse correctly.
from <phrase> sets direction inbound (source match);
to <phrase> sets direction outbound (destination match).
Action synonyms: allow / accept / permit / pass → accept;
block / drop / deny / discard / blackhole → drop;
reject / refuse / reset → reject.
Address synonyms: anywhere / everywhere / everyone / everybody / anyone /
anybody / everything / anything / universe / all / all-hosts / world /
internet / global / wan / public / external / outside / remote / untrusted /
unknown / open → any.
Recognised services: ssh, http, https, dns, dns-tcp, smtp, ntp, ftp, telnet, ldap, rdp, mysql, postgres, redis, amqp.
Network inspection queries (bypass CLIPS, return a diagnostic command):
show rules, show tables, show chains, show interfaces,
show addresses, show routing, show connections, show listening,
show dns.
wallfacer detects whether stdin is a TTY. When stdin is a pipe or file,
prompt_toolkit is not used; each input line is echoed as wallfacer> text
before its response so the output is self-documenting:
printf "allow SSH from 10.0.0.0/8\nallow HTTPS from any\n" | python -m wallfacer.mainwallfacer> allow SSH from 10.0.0.0/8
I'll allow SSH (TCP/22) from 10.0.0.0/8 inbound.
Run this to apply: ...
wallfacer> allow HTTPS from any
I'll allow HTTPS (TCP/443) from any inbound.
Run this to apply: ...
The feed-request path works the same way in both modes: the REPL blocks on
the FIFO and the --infeed sidecar writes to it from another process or
terminal. For scripted demonstrations that need to simulate a feed, driving
ChatSession directly (as demo/session.py does) avoids the FIFO entirely.
This is a proof of concept. A non-exhaustive list of things it does not do:
Correctness and safety
- wallfacer does not know your existing ruleset unless you explicitly provide it via the feed mechanism. Until you do, conflict detection operates only over rules proposed within the current session.
- Conflict detection covers five anomaly types (see above) and is bounded by what wallfacer can decode from the nft JSON. Rules involving named sets, verdict chains, interface matches, and port ranges are marked opaque; any "no conflicts found" verdict is conditional on those rules not interfering.
- Filter rules target the
inet filtertable; NAT rules targetinet nat. wallfacer does not check whether these tables exist with appropriate default policies, or whether rule position in the chain is meaningful relative to rules it has not seen. nft add ruleappends to the chain. Rule order matters in nftables. Conflict detection warns about ordering problems it can detect, but there is no mechanism to insert rules at the correct position using kernel handles.- Semantic advisories (reject + ICMP, drop + established) are always emitted. The commands are still generated; wallfacer does not refuse to produce a rule it considers questionable.
Language coverage
- Temporal reasoning covers the four predefined windows (
always,business-hours,weekdays,weekends) and detects overlapping-window conflicts between session-proposed rules. It does not yet generate nftables time-match expressions (meta hour, scheduled sets) or handle week-spanning windows. - Eleven filter/NAT input patterns are supported plus temporal qualifiers. Any phrasing that does not match returns a clarification response.
- Policies combining multiple source networks in a single rule, per-destination rules, and complex NAT (conntrack state matching, interface-specific NAT) are not supported.
- IPv6 is handled correctly for source and destination addresses; split parallel rules targeting both families in a single request are not supported.
Production use
- wallfacer does not apply rules. It prints commands for you to review and run manually. This is intentional, but it is not a substitute for understanding what those commands will do.
- There is no persistence, history replay, or rollback. If you run the
generated commands and something breaks,
nft flush rulesetwill remove everything. - Session state is in-process only. Restarting wallfacer starts a fresh session; previously proposed rules are not remembered.
Why CLIPS for a firewall tool? This project exists to demonstrate that a production rule system is a plausible architecture for network policy reasoning. The CLIPS inference cycle maps naturally onto the problem: a parsed request is a set of facts; reasoning rules progressively refine those facts (resolve address family, assign hook, classify service, compute priority); emitter rules read the fully-resolved facts and produce output. Adding a new matching dimension: a new protocol, a new action type: means adding a rule or extending a generator, not rewriting a decision tree.
The same architecture scales cleanly to the reasoning tasks added since the initial build: the tier-2 classifier, conflict detection, and temporal reasoning are all expressed as salience-stratified CLIPS rule groups that operate on the same shared fact base, without coupling to each other or to the emitter layer.
Why Python as the knowledge-acquisition layer?
CLIPS is a symbolic reasoning system. It has no native notion of whether
10.1.0.0/24 is a subnet of 10.0.0.0/8, whether two timestamps overlap,
or how to parse a JSON expression tree. Python does that work: translating
raw inputs into symbolic facts: before CLIPS sees anything. CLIPS then
reasons over relation tokens (contains, during, disjoint) rather than
addresses and timestamps. This is the traditional structure of expert
systems: a knowledge-acquisition layer that structures data, and an inference
engine that reasons over the structured representation.
Why not a language model?
A language model can paraphrase firewall policy fluently, but its outputs
are not auditable and its reasoning is not inspectable. The CLIPS knowledge
base is a complete, human-readable statement of every inference the system
can make. You can open wallfacer/clips/conflict_rules.clp and read exactly which
conditions trigger a shadow-anomaly warning, or open
wallfacer/clips/generated/temporal_composition.clp and verify the composition table
entry for before ∘ during. That auditability matters for security tooling.
BSD 3-Clause: Copyright © 2026 Russell Neches.
-
J.F. Allen, "Maintaining Knowledge about Temporal Intervals," Communications of the ACM, 26(11):832–843, 1983. The foundational paper defining the 13 Allen interval relations and the composition table used for transitive inference in
wallfacer/clips/temporal_rules.clp. -
E. Al-Shaer & H. Hamed, "Discovery of Policy Anomalies in Distributed Firewalls," Proceedings of IEEE INFOCOM 2004. Defines the five firewall rule anomaly types (shadow, correlation, redundancy, generalization, irrelevance) that structure
wallfacer/clips/conflict_rules.clp. -
CLIPS language overview and history: Wikipedia: CLIPS
-
Jess rule engine (Java, CLIPS-derived, widely deployed in enterprise systems): Wikipedia: Jess
-
MYCIN (1972–1980), the Stanford medical expert system that established the production-rule paradigm: Wikipedia: Mycin
-
XCON (1980), Digital Equipment Corporation's VAX configuration expert system; one of the first large-scale commercial deployments of rule-based AI: Wikipedia: XCON
-
nftables documentation and man page:
man nft(8),/usr/share/doc/nftables/ -
Liu Cixin, The Dark Forest (2008; English translation 2015): origin of the Wallfacer concept
-
clipspy (Python bindings for CLIPS): https://clipspy.readthedocs.io/