Your Firewall Rules, Explained by a Senior SME – On Demand

You are on call. A customer is down. The NSG is clean. You SSH into the VM, run sudo iptables-save, and get this:

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
-A INPUT -p tcp -m multiport --dports 22,80,443 -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -j f2b-sshd
-A f2b-sshd -s 203.0.113.47/32 -j REJECT --reject-with icmp-port-unreachable
-A f2b-sshd -j RETURN
COMMIT

You now need to answer: is the customer’s IP being blocked? What is f2b-sshd doing exactly? Is the ESTABLISHED,RELATED rule in the right position? What happens to traffic that does not match any of these rules?

If you are a senior iptables engineer, you can work through this in a few minutes. If you are not — and most engineers on call are not iptables specialists — you are opening browser tabs, reading man pages, and hoping you get the evaluation order right under pressure.

I built --explain and --explain-diff to close that gap.


The Problem

The knowledge gap shows up in three situations consistently:

On-call escalations. The engineer who catches the page may be a strong generalist but not a Netfilter specialist. Reading a ruleset correctly takes time they do not have. Misreading it — LOG interpreted as blocking, a RETURN misread as ACCEPT — leads to wrong hypotheses and longer outages.

Customer escalations. A customer reports that a connection is being refused. You need to confirm whether the OS firewall is the cause and explain your reasoning. Working through iptables-save output and narrating it live, under time pressure, is error-prone.

Change review. A script modified the firewall rules during a maintenance window. The diff is a wall of -A and -D lines. You need to understand what actually changed in terms of traffic impact — not line-by-line syntax.

--explain addresses the first two. --explain-diff addresses the third.


Why Not Just Paste the Output Into an LLM?

The natural first instinct when faced with an unfamiliar iptables ruleset is to paste it into a general-purpose LLM — ChatGPT, Claude, Gemini — and ask “what does this firewall do?”

This produces plausible-sounding but unreliable answers for a specific set of reasons:

General LLMs do not consistently apply first-match semantics. iptables evaluates rules top-to-bottom and stops at the first match. A general LLM asked to describe a chain will often describe every rule that could match a given packet, rather than identifying which rule fires first and which rules are therefore unreachable for that traffic class.

Terminal vs non-terminal verdicts are frequently confused. LOG, MARK, CONNMARK, and NFLOG are non-terminal — the packet continues to the next rule after they fire. A general LLM commonly describes a LOG rule as “this rule logs and blocks the packet,” which is incorrect. The LOG rule passes the packet through; only a subsequent DROP or REJECT stops it.

User-defined chain mechanics are described imprecisely. When a chain jump hits RETURN (or falls through), the packet returns to the calling chain and evaluation continues from the rule after the jump. A general LLM will often say the packet is “returned to the input chain” without specifying whether it continues or terminates — which is the critical detail.

ESTABLISHED/RELATED placement warnings are missed. A general LLM will rarely flag that an ESTABLISHED,RELATED ACCEPT rule placed after DROP or REJECT rules in the same chain is a correctness problem — one that causes established-session return traffic to be dropped before the ESTABLISHED,RELATED accept is reached.

Counter-less rules are misinterpreted. iptables-save output without the --counters flag omits packet and byte counts entirely. A general LLM will often interpret the absence of counter data as “no packets have matched this rule,” which is not what the absence means — it means no counter was captured.

Empty tables are misread. On hosts running the iptables-nft backend (Ubuntu 22.04+, Debian 11+), iptables-save may return no rules even while the host is actively filtering traffic via nftables. A general LLM asked to analyze this output has no way to recognise this condition and will report “no firewall rules are in effect,” which is incorrect.

The root cause is the same in all cases: a general-purpose LLM was not given the Netfilter evaluation model. It has absorbed some knowledge of iptables from training data, but that knowledge is inconsistent — sufficient to produce a description that sounds plausible and contains errors that are only visible to someone who already knows the answer.

Feeding structured JSON instead of raw text narrows the problem space. But the evaluation model still has to be explicitly encoded in the prompt, or the LLM will fall back to pattern-matching from training data. The system prompt is where that encoding lives.


How It Works

The pipeline has two steps: parse, then explain.

┌───────────────────────────────────────────────────────────────────┐
│                                                                   │
│   iptables-save text              nft --json list ruleset         │
│          │                                   │                    │
│          ▼                                   ▼                    │
│   iptables_parser.py           nftables_parser.py                 │
│          │                                   │                    │
│          └──────────────┬────────────────────┘                    │
│                         ▼                                         │
│              Structured JSON snapshot                             │
│              (tables, chains, rules, policies,                    │
│               counters, diagnostics)                              │
│                         │                                         │
│                         ▼                                         │
│         iptables_explain.py / nftables_explain.py                 │
│         LLM + framework-specific expert system prompt             │
│                         │                                         │
│                         ▼                                         │
│            Plain-English security analysis                        │
│                                                                   │
└───────────────────────────────────────────────────────────────────┘

The parsers convert raw text to structured JSON. The explain engines take that JSON and feed it to an LLM with a detailed system prompt that encodes the Netfilter evaluation model for the relevant framework.

The system prompt is the core of the feature. It encodes:

  • Evaluation order: for iptables, inbound traffic passes through raw, mangle, and nat at PREROUTING before the routing decision, then mangle and filter at INPUT. For nftables, chains registered at the same hook run in priority order — lowest number first — regardless of table.
  • First-match semantics: the first rule in a chain that matches a packet determines its fate; subsequent rules in that chain are not evaluated for that packet.
  • Terminal vs non-terminal verdicts: ACCEPT, DROP, REJECT are terminal — chain traversal ends. LOG, MARK, NFLOG, CONNMARK are non-terminal — the packet continues to the next rule.
  • Jump and return mechanics: what happens when a user-defined chain is exhausted, what RETURN does in a user-defined chain vs a top-level chain, the difference between jump and goto in nftables.
  • The ESTABLISHED/RELATED pattern: when it is protective, when its position makes it ineffective, and how to detect the problem.
  • User-defined chain conventions: f2b-* is fail2ban, DOCKER/DOCKER-USER/DOCKER-ISOLATION-STAGE-* is Docker, ufw-* is Uncomplicated Firewall, KUBE-* is Kubernetes kube-proxy.
  • Counter interpretation: what packet_count: 0 means vs the absence of a counter expression entirely.
  • Scope boundaries: the prompt instructs the LLM to explicitly state what the ruleset cannot tell you — NSG state, routing table, conntrack table contents, application layer.

The System Prompt is the Product

The LLM provides the language generation. The system prompt provides the Netfilter expertise. Without the system prompt, the LLM falls back to the general-purpose behaviour described above — plausible text that contains evaluation order errors, missed placement issues, and misread verdicts.

Getting the system prompt right required iterating through real-world rulesets that expose the edge cases:

  • fail2ban chains with active bans, where the RETURN path needs to be traced back to INPUT to determine what happens to non-banned traffic
  • Docker’s chain structure: DOCKER-USER → DOCKER → DOCKER-ISOLATION-STAGE-1 → DOCKER-ISOLATION-STAGE-2, where isolation is enforced across two chained jumps, not a single rule
  • Kubernetes kube-proxy rulesets with hundreds of KUBE-SEP-* chains, one per service endpoint
  • CIS-hardened configurations with ESTABLISHED/RELATED as position 1, explicit port permits, and a DROP policy — where the ordering between ESTABLISHED/RELATED and the explicit permits matters
  • Rulesets with LOG rules immediately preceding DROP rules — non-terminal followed by terminal — where a naive reading treats the LOG as a block
  • Rulesets where ESTABLISHED/RELATED appears at position 5 in INPUT, after DROP rules at positions 2 and 3 — where the DROP fires first for established-session return traffic, causing the ESTABLISHED/RELATED accept to never be reached for those sessions

Each of these is encoded in the system prompt with explicit instructions on what to say, what to flag, and what the traffic impact is. The output consistently reflects these patterns because the instructions for handling them are explicit — not because the LLM is reasoning from general knowledge.


What This Tool Does — and Does Not — Cover

--explain and --explain-diff analyse the OS-layer firewall configuration only. The analysis tells you what iptables or nftables rules are in effect on the VM. It does not tell you anything about the layers above or below.

┌─────────────────────────────────────────────────────────────────────┐
│  CLOUD PLATFORM LAYER                                               │
│                                                                     │
│  NSG rules · Route tables · Service endpoints · Load balancers      │
│                                                                     │
│  ✗ NOT covered by --explain / --explain-diff                        │
│    Use: az CLI · Network Ghost Agent · Azure Network Watcher        │
└──────────────────────────────┬──────────────────────────────────────┘
                               │ packet permitted by platform
                               ▼
┌─────────────────────────────────────────────────────────────────────┐
│  OS LAYER — VM KERNEL                                               │
│                                                                     │
│  iptables chains and rules · nftables tables and chains             │
│  Chain policies · User-defined chains · Connection tracking config  │
│                                                                     │
│  ✓ COVERED by --explain / --explain-diff                            │
└──────────────────────────────┬──────────────────────────────────────┘
                               │ packet permitted by OS firewall
                               ▼
┌─────────────────────────────────────────────────────────────────────┐
│  APPLICATION LAYER                                                  │
│                                                                     │
│  Whether the application is listening · Application-level ACLs      │
│                                                                     │
│  ✗ NOT covered by --explain / --explain-diff                        │
└─────────────────────────────────────────────────────────────────────┘

Specifically, the tool cannot tell you:

  • Whether an Azure NSG is blocking or permitting traffic. An NSG makes its allow/deny decision before the packet reaches the OS. A rule permitting port 443 in iptables does not guarantee the port is reachable if an NSG denies it upstream.
  • Whether a route table or UDR is directing traffic away from its expected path. Routing decisions happen in the kernel’s routing subsystem, not in Netfilter rules.
  • Whether IP forwarding is enabled. The FORWARD chain is only active if net.ipv4.ip_forward = 1. The tool explains what FORWARD rules say but cannot tell you whether forwarding is actually enabled.
  • The live state of the connection tracking table. ESTABLISHED and RELATED rules depend on conntrack entries that exist at the time the packet arrives. The tool analyses the rule configuration — whether the rule is present and correctly positioned — not the current conntrack table contents.
  • Whether the application behind the firewall is listening on the permitted port.

Every analysis output includes a scope note that makes these boundaries explicit. The tool is one layer of a multi-layer investigation — not a complete connectivity diagnosis.


–explain: Understanding What the Firewall Is Doing Right Now

sudo iptables-save | python3 iptables_parser.py --explain

For the ruleset from the opening of this post, the engine produces an analysis along these lines:

## Firewall State Analysis

**Overall posture: default-deny with targeted permits**

The INPUT chain has a DROP default policy — all inbound traffic is blocked
unless explicitly permitted by a rule.

**What is permitted inbound:**

1. Return traffic for established connections — the ESTABLISHED,RELATED
   ACCEPT rule is the first rule in INPUT, which is correct placement.
   It short-circuits evaluation for reply packets before any other
   filtering logic runs. Without this rule, the DROP policy would block
   reply packets for connections this host initiates outbound.

2. TCP ports 22 (SSH), 80 (HTTP), and 443 (HTTPS) — covered by a single
   multiport match rule.

3. Loopback interface traffic — all traffic on lo is accepted. Required
   for inter-process communication on the host.

**The f2b-sshd chain (fail2ban):**

After the explicit permits, INPUT jumps to the user-defined chain f2b-sshd.
This chain contains one active ban entry: 203.0.113.47/32 is REJECTed with
ICMP port-unreachable. The RETURN rule at the end of f2b-sshd means that
any packet not matching an active ban entry returns to INPUT, continuing
from the rule after the jump. There are no further rules in INPUT after the
f2b-sshd jump, so non-banned packets fall through to the INPUT DROP policy.

**FORWARD chain: DROP policy, no rules.** This host will not forward
traffic between interfaces.

**OUTPUT chain: ACCEPT policy, no rules.** All outbound traffic from
this host is permitted.

**Scope note:** This analysis covers iptables rules only. It does not
reflect Azure NSG rules, route table configuration, IP forwarding state,
or the current contents of the connection tracking table.

Notice specifically: the engine identifies the ESTABLISHED,RELATED rule placement as correct, explains what RETURN does in f2b-sshd and traces what happens to non-banned packets after RETURN, and scopes the analysis explicitly.


–explain-diff: Understanding What Changed and Why It Matters

Given two snapshots — before and after a change window — the diff engine produces a structured JSON change report. The explain engine then interprets that report in terms of traffic impact.

                    ┌─────────────┐
   before.json ─────►             │
                    │ diff engine │──► drift.json ──► explain engine ──► analysis
   after.json  ─────►             │
                    └─────────────┘
python3 iptables_parser.py before.json --explain-diff after.json

For a diff showing an INPUT policy change from ACCEPT to DROP, with three associated rule changes, the engine produces an analysis along these lines:

## Firewall Drift Analysis

**2 rules added, 1 rule removed, 1 policy change — one critical change.**

---

⚠ CRITICAL: INPUT chain default policy changed from ACCEPT to DROP

Previously, inbound traffic not matched by any rule was accepted by default.
Now it is silently dropped. Any service listening on a port without an
explicit ACCEPT rule is now unreachable. This policy change is the most
likely cause of a connectivity failure if one is being investigated.

---

Rule added: ESTABLISHED,RELATED ACCEPT (INPUT, position 1)

A conntrack-based ACCEPT for ESTABLISHED and RELATED traffic was added as
the first rule in INPUT. This is paired with the policy change above.
Without this rule, the DROP policy would block reply packets for outbound
connections initiated from this host. Its placement as position 1 is correct.

---

Rule added: TCP 443 ACCEPT (INPUT)

HTTPS is now explicitly permitted. This rule was not present in the baseline
and is required under the new DROP policy for HTTPS traffic to be accepted.

---

Rule removed: TCP 23 ACCEPT (INPUT)

An explicit ACCEPT for Telnet (TCP 23) was present in the baseline and is
absent in the current state. Under the new DROP policy, Telnet is now
actively blocked. Telnet transmits credentials in cleartext; its removal
is consistent with the hardening intent of this change.

---

Overall: the policy change, the ESTABLISHED/RELATED rule addition, and the
explicit port permits form a consistent default-deny hardening pattern.
No rules were added that introduce new permitted access. The Telnet removal
closes a cleartext protocol. The change set is internally consistent.

Scope note: this analysis reflects changes to iptables rules only. Changes
to NSG rules, route tables, or other platform-layer controls during the
same window are not reflected here.

nftables: Different Syntax, Same Capability

The nftables version — nft --json list ruleset as input, nftables_explain.py as the engine — works with the same pattern but uses a framework-specific system prompt that encodes nftables evaluation semantics. These differ from iptables in ways that matter for correctness:

  iptables                          nftables
  ────────────────────────────      ────────────────────────────────
  Fixed table order:                No fixed table order.
  raw → mangle → nat → filter       Chains run in priority order at
                                    each hook. Lower number = earlier.
                                    Priority -100 runs before priority 0.

  User-defined chains: no           Regular chains: no hook, no policy.
  hook, no policy.                  Only reachable via jump or goto.
  RETURN → back to caller.          RETURN → back to caller (jump).
                                    RETURN after goto → caller's caller.

  No named sets in base syntax.     Named sets: @setname.
                                    A missing set definition fails
                                    silently at runtime.

The goto vs jump distinction is one that regularly surprises engineers reading nftables rulesets. With jump, when the target chain is exhausted or hits return, evaluation returns to the calling chain at the rule after the jump. With goto, it does not — it returns to the calling chain’s caller, skipping one level of the call stack. The explain engine surfaces this when a goto is present and traces what happens when the target chain is exhausted.

# Explain a live nftables ruleset
sudo nft --json list ruleset | python3 nftables_parser.py --explain

# Explain the diff between two nftables snapshots
python3 nftables_parser.py before.json --explain-diff after.json

Tangible Value for Network Engineers

Concretely, what this changes in daily work:

During an on-call incident: Run --explain on the VM’s current iptables/nftables state. You get a plain-English summary of what the OS firewall is permitting and blocking, whether any active bans are in place, and whether any configuration anomalies (wrong ESTABLISHED/RELATED placement, LOG-before-DROP, missing ACCEPT rules under a DROP policy) could explain the symptom. You do not need to know iptables evaluation order to get a correct answer.

During a customer escalation: Run --explain, copy the output into the ticket or the call. The analysis states what is permitted, what is blocked, and what the tool cannot determine — in language the customer can understand and you can stand behind.

After a maintenance window: Run --explain-diff against the pre-change baseline. The output tells you what changed, what the traffic impact of each change is, whether any critical changes (policy flips, DROP rules added for active traffic paths) require attention, and whether the change set is internally consistent.

When reviewing a configuration in your config management system: Pipe the saved rules file through --explain without touching the live VM. Flag anomalies before deployment.

For post-incident review: Explain what the firewall configuration was at the time of the incident using a saved backup or a Netfilter Inspector snapshot. No live VM access required.

For CI/CD validation: Parse a proposed ruleset configuration before deployment, explain what it will permit and block, and flag anomalies before they reach production.

What the tool does not replace: it does not replace understanding the full network path. When a connection fails, the OS firewall is one layer. The investigation should also cover NSG rules, route tables, service endpoints, and platform-layer controls. If that is the scope of your problem — a full cloud network diagnosis across both the Azure control plane and the OS layer — the Network Ghost Agent is built for that. It uses a suite of tools to diagnose cloud network faults end-to-end, of which the iptables and nftables parsers are one component. --explain and --explain-diff close the OS-layer part of that investigation; the Ghost Agent closes the rest.


Getting Started

Both explain engines are in the agentic-network-tools repository:

  • netfilter-inspector/iptables-parser/iptables_explain.py
  • netfilter-inspector/nftables-parser/nftables_explain.py

Exposed via --explain and --explain-diff on the respective parser CLIs:

# iptables — explain current state
sudo iptables-save | python3 iptables_parser.py --explain

# iptables — explain drift between two snapshots
python3 iptables_parser.py before.json --explain-diff after.json

# nftables — explain current state
sudo nft --json list ruleset | python3 nftables_parser.py --explain

# nftables — explain drift between two snapshots
python3 nftables_parser.py before.json --explain-diff after.json

Requires an LLM API key set as an environment variable. See .env.example in each parser directory for configuration details.

The next time someone on your team is staring at a wall of iptables rules during an incident, they should not have to be a Netfilter expert to understand what they are looking at — or to trust that the answer they are reading is correct.