# Firewall Diagram

**URL:** <https://community.ipfire.org/t/firewall-diagram/15469>\
**Category:** Network\
**Created:** [9 February 2026 18:30 UTC](https://community.ipfire.org/t/firewall-diagram/15469 "2026-02-09T18:30:35Z")\
**Posts on this page:** 9\
**Page:** 1

<div class="post-metadata">

**Author:** ![hjkl](https://community.ipfire.org/user_avatar/community.ipfire.org/hjkl/32/8574_2.png) [@hjkl](https://community.ipfire.org/u/hjkl)\
**Post date:** [9 February 2026 18:30 UTC](https://community.ipfire.org/t/firewall-diagram/15469/1 "2026-02-09T18:30:35Z")

</div>

I am working to script that should generate a Firewall Diagram - the script generates a .dot file which is used to generate the .png

`dot -Tpng input.dot -o output.png`

At the moment the script does not investigate the chain relations (i.e. it does not follow the “-j”/jump directives). Therefore the diagram includes only the Chains with a Policy attached and their children chains.

It is possible to add arrows/links to reflect the dependencies but the amount of arrows will make the diagram hard to be read.

Any suggestion on additional layers (information) to be added (the script will generate the input.dot file automatically)

Current result:

 ![ipfire_unified_firewallcore197](https://community.ipfire.org/uploads/default/original/2X/f/fb2a64c4a3e5f03776c1e0a8a4a36f9009b6a89c.png)

```typescript
digraph UnifiedFW { rankdir=TB; node [shape=rect, style=filled, fillcolor=white];
graph [compound=true, ranksep=1.0, nodesep=0.5];
  subgraph "cluster_table_raw" { label="TABLE: raw"; style=filled; fillcolor=ivory; color=blue; penwidth=2;
    subgraph "cluster_raw_PREROUTING" { label="PREROUTING"; style=filled; fillcolor=lightgrey;
      "raw_PREROUTING_SYN_FLOOD_PROTECT" [label="SYN_FLOOD_PROTECT", fillcolor=white];
    }
    subgraph "cluster_raw_OUTPUT" { label="OUTPUT"; style=filled; fillcolor=lightgrey;
      "anchor_raw_OUTPUT" [label="", style=invis, width=0, height=0];
    }
  }
  subgraph "cluster_table_mangle" { label="TABLE: mangle"; style=filled; fillcolor=ivory; color=blue; penwidth=2;
    subgraph "cluster_mangle_PREROUTING" { label="PREROUTING"; style=filled; fillcolor=lightgrey;
      "mangle_PREROUTING_CONNMARK" [label="CONNMARK", fillcolor=white];
      "mangle_PREROUTING_NAT_DESTINATION" [label="NAT_DESTINATION", fillcolor=white];
      "mangle_PREROUTING_CONNMARK" -> "mangle_PREROUTING_NAT_DESTINATION" [color=blue, weight=10];
    }
    subgraph "cluster_mangle_INPUT" { label="INPUT"; style=filled; fillcolor=lightgrey;
      "mangle_INPUT_IPS_SCAN_IN" [label="IPS_SCAN_IN", fillcolor=white];
      "mangle_INPUT_IPS" [label="IPS", fillcolor=white];
      "mangle_INPUT_IPS_SCAN_IN" -> "mangle_INPUT_IPS" [color=blue, weight=10];
      "mangle_INPUT_IPS_CLEAR" [label="IPS_CLEAR", fillcolor=white];
      "mangle_INPUT_IPS" -> "mangle_INPUT_IPS_CLEAR" [color=blue, weight=10];
    }
    subgraph "cluster_mangle_FORWARD" { label="FORWARD"; style=filled; fillcolor=lightgrey;
      "mangle_FORWARD_IPS_SCAN_IN" [label="IPS_SCAN_IN", fillcolor=white];
      "mangle_FORWARD_IPS_SCAN_OUT" [label="IPS_SCAN_OUT", fillcolor=white];
      "mangle_FORWARD_IPS_SCAN_IN" -> "mangle_FORWARD_IPS_SCAN_OUT" [color=blue, weight=10];
      "mangle_FORWARD_IPS" [label="IPS", fillcolor=white];
      "mangle_FORWARD_IPS_SCAN_OUT" -> "mangle_FORWARD_IPS" [color=blue, weight=10];
      "mangle_FORWARD_IPS_CLEAR" [label="IPS_CLEAR", fillcolor=white];
      "mangle_FORWARD_IPS" -> "mangle_FORWARD_IPS_CLEAR" [color=blue, weight=10];
    }
    subgraph "cluster_mangle_OUTPUT" { label="OUTPUT"; style=filled; fillcolor=lightgrey;
      "mangle_OUTPUT_IPS_SCAN_OUT" [label="IPS_SCAN_OUT", fillcolor=white];
      "mangle_OUTPUT_IPS" [label="IPS", fillcolor=white];
      "mangle_OUTPUT_IPS_SCAN_OUT" -> "mangle_OUTPUT_IPS" [color=blue, weight=10];
      "mangle_OUTPUT_IPS_CLEAR" [label="IPS_CLEAR", fillcolor=white];
      "mangle_OUTPUT_IPS" -> "mangle_OUTPUT_IPS_CLEAR" [color=blue, weight=10];
    }
    subgraph "cluster_mangle_POSTROUTING" { label="POSTROUTING"; style=filled; fillcolor=lightgrey;
      "anchor_mangle_POSTROUTING" [label="", style=invis, width=0, height=0];
    }
  }
  subgraph "cluster_table_nat" { label="TABLE: nat"; style=filled; fillcolor=ivory; color=blue; penwidth=2;
    subgraph "cluster_nat_PREROUTING" { label="PREROUTING"; style=filled; fillcolor=lightgrey;
      "nat_PREROUTING_CUSTOMPREROUTING" [label="CUSTOMPREROUTING", fillcolor=white];
      "nat_PREROUTING_CAPTIVE_PORTAL" [label="CAPTIVE_PORTAL", fillcolor=white];
      "nat_PREROUTING_CUSTOMPREROUTING" -> "nat_PREROUTING_CAPTIVE_PORTAL" [color=blue, weight=10];
      "nat_PREROUTING_SQUID" [label="SQUID", fillcolor=white];
      "nat_PREROUTING_CAPTIVE_PORTAL" -> "nat_PREROUTING_SQUID" [color=blue, weight=10];
      "nat_PREROUTING_NAT_DESTINATION" [label="NAT_DESTINATION", fillcolor=white];
      "nat_PREROUTING_SQUID" -> "nat_PREROUTING_NAT_DESTINATION" [color=blue, weight=10];
    }
    subgraph "cluster_nat_INPUT" { label="INPUT"; style=filled; fillcolor=lightgrey;
      "anchor_nat_INPUT" [label="", style=invis, width=0, height=0];
    }
    subgraph "cluster_nat_OUTPUT" { label="OUTPUT"; style=filled; fillcolor=lightgrey;
      "nat_OUTPUT_NAT_DESTINATION" [label="NAT_DESTINATION", fillcolor=white];
    }
    subgraph "cluster_nat_POSTROUTING" { label="POSTROUTING"; style=filled; fillcolor=lightgrey;
      "nat_POSTROUTING_CUSTOMPOSTROUTING" [label="CUSTOMPOSTROUTING", fillcolor=white];
      "nat_POSTROUTING_WGNAT" [label="WGNAT", fillcolor=white];
      "nat_POSTROUTING_CUSTOMPOSTROUTING" -> "nat_POSTROUTING_WGNAT" [color=blue, weight=10];
      "nat_POSTROUTING_OVPNNAT" [label="OVPNNAT", fillcolor=white];
      "nat_POSTROUTING_WGNAT" -> "nat_POSTROUTING_OVPNNAT" [color=blue, weight=10];
      "nat_POSTROUTING_IPSECNAT" [label="IPSECNAT", fillcolor=white];
      "nat_POSTROUTING_OVPNNAT" -> "nat_POSTROUTING_IPSECNAT" [color=blue, weight=10];
      "nat_POSTROUTING_NAT_SOURCE" [label="NAT_SOURCE", fillcolor=white];
      "nat_POSTROUTING_IPSECNAT" -> "nat_POSTROUTING_NAT_SOURCE" [color=blue, weight=10];
      "nat_POSTROUTING_NAT_DESTINATION_FIX" [label="NAT_DESTINATION_FIX", fillcolor=white];
      "nat_POSTROUTING_NAT_SOURCE" -> "nat_POSTROUTING_NAT_DESTINATION_FIX" [color=blue, weight=10];
      "nat_POSTROUTING_REDNAT" [label="REDNAT", fillcolor=white];
      "nat_POSTROUTING_NAT_DESTINATION_FIX" -> "nat_POSTROUTING_REDNAT" [color=blue, weight=10];
    }
  }
  subgraph "cluster_table_filter" { label="TABLE: filter"; style=filled; fillcolor=ivory; color=blue; penwidth=2;
    subgraph "cluster_filter_INPUT" { label="INPUT"; style=filled; fillcolor=lightgrey;
      "filter_INPUT_BADTCP" [label="BADTCP", fillcolor=white];
      "filter_INPUT_CUSTOMINPUT" [label="CUSTOMINPUT", fillcolor=white];
      "filter_INPUT_BADTCP" -> "filter_INPUT_CUSTOMINPUT" [color=blue, weight=10];
      "filter_INPUT_HOSTILE" [label="HOSTILE", fillcolor=white];
      "filter_INPUT_CUSTOMINPUT" -> "filter_INPUT_HOSTILE" [color=blue, weight=10];
      "filter_INPUT_BLOCKLISTIN" [label="BLOCKLISTIN", fillcolor=white];
      "filter_INPUT_HOSTILE" -> "filter_INPUT_BLOCKLISTIN" [color=blue, weight=10];
      "filter_INPUT_GUARDIAN" [label="GUARDIAN", fillcolor=white];
      "filter_INPUT_BLOCKLISTIN" -> "filter_INPUT_GUARDIAN" [color=blue, weight=10];
      "filter_INPUT_WGBLOCK" [label="WGBLOCK", fillcolor=white];
      "filter_INPUT_GUARDIAN" -> "filter_INPUT_WGBLOCK" [color=blue, weight=10];
      "filter_INPUT_OVPNBLOCK" [label="OVPNBLOCK", fillcolor=white];
      "filter_INPUT_WGBLOCK" -> "filter_INPUT_OVPNBLOCK" [color=blue, weight=10];
      "filter_INPUT_IPTVINPUT" [label="IPTVINPUT", fillcolor=white];
      "filter_INPUT_OVPNBLOCK" -> "filter_INPUT_IPTVINPUT" [color=blue, weight=10];
      "filter_INPUT_ICMPINPUT" [label="ICMPINPUT", fillcolor=white];
      "filter_INPUT_IPTVINPUT" -> "filter_INPUT_ICMPINPUT" [color=blue, weight=10];
      "filter_INPUT_LOOPBACK" [label="LOOPBACK", fillcolor=white];
      "filter_INPUT_ICMPINPUT" -> "filter_INPUT_LOOPBACK" [color=blue, weight=10];
      "filter_INPUT_CAPTIVE_PORTAL" [label="CAPTIVE_PORTAL", fillcolor=white];
      "filter_INPUT_LOOPBACK" -> "filter_INPUT_CAPTIVE_PORTAL" [color=blue, weight=10];
      "filter_INPUT_CTINPUT" [label="CTINPUT", fillcolor=white];
      "filter_INPUT_CAPTIVE_PORTAL" -> "filter_INPUT_CTINPUT" [color=blue, weight=10];
      "filter_INPUT_DHCPGREENINPUT" [label="DHCPGREENINPUT", fillcolor=white];
      "filter_INPUT_CTINPUT" -> "filter_INPUT_DHCPGREENINPUT" [color=blue, weight=10];
      "filter_INPUT_DHCPBLUEINPUT" [label="DHCPBLUEINPUT", fillcolor=white];
      "filter_INPUT_DHCPGREENINPUT" -> "filter_INPUT_DHCPBLUEINPUT" [color=blue, weight=10];
      "filter_INPUT_TOR_INPUT" [label="TOR_INPUT", fillcolor=white];
      "filter_INPUT_DHCPBLUEINPUT" -> "filter_INPUT_TOR_INPUT" [color=blue, weight=10];
      "filter_INPUT_LOCATIONBLOCK" [label="LOCATIONBLOCK", fillcolor=white];
      "filter_INPUT_TOR_INPUT" -> "filter_INPUT_LOCATIONBLOCK" [color=blue, weight=10];
      "filter_INPUT_IPSECINPUT" [label="IPSECINPUT", fillcolor=white];
      "filter_INPUT_LOCATIONBLOCK" -> "filter_INPUT_IPSECINPUT" [color=blue, weight=10];
      "filter_INPUT_GUIINPUT" [label="GUIINPUT", fillcolor=white];
      "filter_INPUT_IPSECINPUT" -> "filter_INPUT_GUIINPUT" [color=blue, weight=10];
      "filter_INPUT_WIRELESSINPUT" [label="WIRELESSINPUT", fillcolor=white];
      "filter_INPUT_GUIINPUT" -> "filter_INPUT_WIRELESSINPUT" [color=blue, weight=10];
      "filter_INPUT_WGINPUT" [label="WGINPUT", fillcolor=white];
      "filter_INPUT_WIRELESSINPUT" -> "filter_INPUT_WGINPUT" [color=blue, weight=10];
      "filter_INPUT_OVPNINPUTRW" [label="OVPNINPUTRW", fillcolor=white];
      "filter_INPUT_WGINPUT" -> "filter_INPUT_OVPNINPUTRW" [color=blue, weight=10];
      "filter_INPUT_OVPNINPUTN2N" [label="OVPNINPUTN2N", fillcolor=white];
      "filter_INPUT_OVPNINPUTRW" -> "filter_INPUT_OVPNINPUTN2N" [color=blue, weight=10];
      "filter_INPUT_INPUTFW" [label="INPUTFW", fillcolor=white];
      "filter_INPUT_OVPNINPUTN2N" -> "filter_INPUT_INPUTFW" [color=blue, weight=10];
      "filter_INPUT_REDINPUT" [label="REDINPUT", fillcolor=white];
      "filter_INPUT_INPUTFW" -> "filter_INPUT_REDINPUT" [color=blue, weight=10];
      "filter_INPUT_POLICYIN" [label="POLICYIN", fillcolor=white];
      "filter_INPUT_REDINPUT" -> "filter_INPUT_POLICYIN" [color=blue, weight=10];
    }
    subgraph "cluster_filter_FORWARD" { label="FORWARD"; style=filled; fillcolor=lightgrey;
      "filter_FORWARD_BADTCP" [label="BADTCP", fillcolor=white];
      "filter_FORWARD_TCPMSS" [label="TCPMSS", fillcolor=white];
      "filter_FORWARD_BADTCP" -> "filter_FORWARD_TCPMSS" [color=blue, weight=10];
      "filter_FORWARD_CUSTOMFORWARD" [label="CUSTOMFORWARD", fillcolor=white];
      "filter_FORWARD_TCPMSS" -> "filter_FORWARD_CUSTOMFORWARD" [color=blue, weight=10];
      "filter_FORWARD_HOSTILE" [label="HOSTILE", fillcolor=white];
      "filter_FORWARD_CUSTOMFORWARD" -> "filter_FORWARD_HOSTILE" [color=blue, weight=10];
      "filter_FORWARD_BLOCKLISTIN" [label="BLOCKLISTIN", fillcolor=white];
      "filter_FORWARD_HOSTILE" -> "filter_FORWARD_BLOCKLISTIN" [color=blue, weight=10];
      "filter_FORWARD_BLOCKLISTOUT" [label="BLOCKLISTOUT", fillcolor=white];
      "filter_FORWARD_BLOCKLISTIN" -> "filter_FORWARD_BLOCKLISTOUT" [color=blue, weight=10];
      "filter_FORWARD_GUARDIAN" [label="GUARDIAN", fillcolor=white];
      "filter_FORWARD_BLOCKLISTOUT" -> "filter_FORWARD_GUARDIAN" [color=blue, weight=10];
      "filter_FORWARD_IPSECBLOCK" [label="IPSECBLOCK", fillcolor=white];
      "filter_FORWARD_GUARDIAN" -> "filter_FORWARD_IPSECBLOCK" [color=blue, weight=10];
      "filter_FORWARD_WGBLOCK" [label="WGBLOCK", fillcolor=white];
      "filter_FORWARD_IPSECBLOCK" -> "filter_FORWARD_WGBLOCK" [color=blue, weight=10];
      "filter_FORWARD_OVPNBLOCK" [label="OVPNBLOCK", fillcolor=white];
      "filter_FORWARD_WGBLOCK" -> "filter_FORWARD_OVPNBLOCK" [color=blue, weight=10];
      "filter_FORWARD_IPTVFORWARD" [label="IPTVFORWARD", fillcolor=white];
      "filter_FORWARD_OVPNBLOCK" -> "filter_FORWARD_IPTVFORWARD" [color=blue, weight=10];
      "filter_FORWARD_LOOPBACK" [label="LOOPBACK", fillcolor=white];
      "filter_FORWARD_IPTVFORWARD" -> "filter_FORWARD_LOOPBACK" [color=blue, weight=10];
      "filter_FORWARD_CAPTIVE_PORTAL" [label="CAPTIVE_PORTAL", fillcolor=white];
      "filter_FORWARD_LOOPBACK" -> "filter_FORWARD_CAPTIVE_PORTAL" [color=blue, weight=10];
      "filter_FORWARD_CTINPUT" [label="CTINPUT", fillcolor=white];
      "filter_FORWARD_CAPTIVE_PORTAL" -> "filter_FORWARD_CTINPUT" [color=blue, weight=10];
      "filter_FORWARD_LOCATIONBLOCK" [label="LOCATIONBLOCK", fillcolor=white];
      "filter_FORWARD_CTINPUT" -> "filter_FORWARD_LOCATIONBLOCK" [color=blue, weight=10];
      "filter_FORWARD_IPSECFORWARD" [label="IPSECFORWARD", fillcolor=white];
      "filter_FORWARD_LOCATIONBLOCK" -> "filter_FORWARD_IPSECFORWARD" [color=blue, weight=10];
      "filter_FORWARD_WIRELESSFORWARD" [label="WIRELESSFORWARD", fillcolor=white];
      "filter_FORWARD_IPSECFORWARD" -> "filter_FORWARD_WIRELESSFORWARD" [color=blue, weight=10];
      "filter_FORWARD_FORWARDFW" [label="FORWARDFW", fillcolor=white];
      "filter_FORWARD_WIRELESSFORWARD" -> "filter_FORWARD_FORWARDFW" [color=blue, weight=10];
      "filter_FORWARD_REDFORWARD" [label="REDFORWARD", fillcolor=white];
      "filter_FORWARD_FORWARDFW" -> "filter_FORWARD_REDFORWARD" [color=blue, weight=10];
      "filter_FORWARD_POLICYFWD" [label="POLICYFWD", fillcolor=white];
      "filter_FORWARD_REDFORWARD" -> "filter_FORWARD_POLICYFWD" [color=blue, weight=10];
    }
    subgraph "cluster_filter_OUTPUT" { label="OUTPUT"; style=filled; fillcolor=lightgrey;
      "filter_OUTPUT_CUSTOMOUTPUT" [label="CUSTOMOUTPUT", fillcolor=white];
      "filter_OUTPUT_HOSTILE" [label="HOSTILE", fillcolor=white];
      "filter_OUTPUT_CUSTOMOUTPUT" -> "filter_OUTPUT_HOSTILE" [color=blue, weight=10];
      "filter_OUTPUT_BLOCKLISTOUT" [label="BLOCKLISTOUT", fillcolor=white];
      "filter_OUTPUT_HOSTILE" -> "filter_OUTPUT_BLOCKLISTOUT" [color=blue, weight=10];
      "filter_OUTPUT_IPSECBLOCK" [label="IPSECBLOCK", fillcolor=white];
      "filter_OUTPUT_BLOCKLISTOUT" -> "filter_OUTPUT_IPSECBLOCK" [color=blue, weight=10];
      "filter_OUTPUT_LOOPBACK" [label="LOOPBACK", fillcolor=white];
      "filter_OUTPUT_IPSECBLOCK" -> "filter_OUTPUT_LOOPBACK" [color=blue, weight=10];
      "filter_OUTPUT_CTOUTPUT" [label="CTOUTPUT", fillcolor=white];
      "filter_OUTPUT_LOOPBACK" -> "filter_OUTPUT_CTOUTPUT" [color=blue, weight=10];
      "filter_OUTPUT_DHCPGREENOUTPUT" [label="DHCPGREENOUTPUT", fillcolor=white];
      "filter_OUTPUT_CTOUTPUT" -> "filter_OUTPUT_DHCPGREENOUTPUT" [color=blue, weight=10];
      "filter_OUTPUT_DHCPBLUEOUTPUT" [label="DHCPBLUEOUTPUT", fillcolor=white];
      "filter_OUTPUT_DHCPGREENOUTPUT" -> "filter_OUTPUT_DHCPBLUEOUTPUT" [color=blue, weight=10];
      "filter_OUTPUT_IPSECOUTPUT" [label="IPSECOUTPUT", fillcolor=white];
      "filter_OUTPUT_DHCPBLUEOUTPUT" -> "filter_OUTPUT_IPSECOUTPUT" [color=blue, weight=10];
      "filter_OUTPUT_TOR_OUTPUT" [label="TOR_OUTPUT", fillcolor=white];
      "filter_OUTPUT_IPSECOUTPUT" -> "filter_OUTPUT_TOR_OUTPUT" [color=blue, weight=10];
      "filter_OUTPUT_OUTGOINGFW" [label="OUTGOINGFW", fillcolor=white];
      "filter_OUTPUT_TOR_OUTPUT" -> "filter_OUTPUT_OUTGOINGFW" [color=blue, weight=10];
      "filter_OUTPUT_POLICYOUT" [label="POLICYOUT", fillcolor=white];
      "filter_OUTPUT_OUTGOINGFW" -> "filter_OUTPUT_POLICYOUT" [color=blue, weight=10];
    }
  }
  subgraph "cluster_table_security" { label="TABLE: security"; style=filled; fillcolor=ivory; color=blue; penwidth=2;
    subgraph "cluster_security_INPUT" { label="INPUT"; style=filled; fillcolor=lightgrey;
      "anchor_security_INPUT" [label="", style=invis, width=0, height=0];
    }
    subgraph "cluster_security_FORWARD" { label="FORWARD"; style=filled; fillcolor=lightgrey;
      "anchor_security_FORWARD" [label="", style=invis, width=0, height=0];
    }
    subgraph "cluster_security_OUTPUT" { label="OUTPUT"; style=filled; fillcolor=lightgrey;
      "anchor_security_OUTPUT" [label="", style=invis, width=0, height=0];
    }
  }
}

```

---

<div class="post-metadata">

**Author:** ![cfusco](https://community.ipfire.org/user_avatar/community.ipfire.org/cfusco/32/9213_2.png) [@cfusco](https://community.ipfire.org/u/cfusco)\
**Post date:** [10 February 2026 09:40 UTC](https://community.ipfire.org/t/firewall-diagram/15469/2 "2026-02-10T09:40:23Z")

</div>

I always wanted to take this project. Lack of skills and time made this impossible. I wish you all the best for completing this script. Here my wish list:

- Color coding by zone (GREEN=green, RED=red, etc.)

- Grouping related chains in subgraphs

- Legend showing what colors/shapes mean

Good luck.

---

<div class="post-metadata">

**Author:** ![cfusco](https://community.ipfire.org/user_avatar/community.ipfire.org/cfusco/32/9213_2.png) [@cfusco](https://community.ipfire.org/u/cfusco)\
**Post date:** [10 February 2026 10:33 UTC](https://community.ipfire.org/t/firewall-diagram/15469/3 "2026-02-10T10:33:43Z")

</div>

* * *

I’ve been developing an AI-assisted development methodology that I’ve successfully scaled to projects in the 10K LOC range. I want to share both the method and a draft spec in case either is useful to you.

**The process works like this:**

I start with a Socratic session with the LLM where I state my goals, the LLM asks clarifying questions, and we iteratively arrive at a stakeholder-directed spec document. I then transform that into a blueprint spec, where I decompose the project into units, each constrained to 150 lines of code or fewer, with only backward dependencies. From this blueprint I generate two things for each unit: a prompt to implement it, and a prompt to implement a robust test suite for it. I then manually verify each unit against its tests. Invariably, this stage reveals problems in the initial spec, so I iterate, fixing the spec and regenerating all downstream documents.

**This is not vibe-coding.** There’s no “generate and pray.” Every unit is small enough to review in full, every dependency flows in one direction, and the test suites provide a mechanical check at each step. The LLM is a drafting tool operating within tight structural constraints. The architectural decisions, the decomposition, and the validation are all human-driven. That’s how it scales: not by trusting the LLM with more, but by giving it less to get wrong at each step.

For this project, I have the initial spec, attached below in a separate message. It was written collaboratively using Claude Opus as the drafting partner, following the Socratic process described above. It is not yet ready for implementation: it still needs several decisions to introduce further constraints. But I’m including it in case it gives you some insight into the problem structure and how you might want to approach it.

Best of luck with the project.

* * *

---

<div class="post-metadata">

**Author:** ![cfusco](https://community.ipfire.org/user_avatar/community.ipfire.org/cfusco/32/9213_2.png) [@cfusco](https://community.ipfire.org/u/cfusco)\
**Post date:** [10 February 2026 10:41 UTC](https://community.ipfire.org/t/firewall-diagram/15469/4 "2026-02-10T10:41:43Z")

</div>

# IPFire Firewall Visualization — High-Level Specification

**Version:** 0.1-draft  
**Date:** 2025-10-12  
**Author:** wilya7  
**Status:** Proposal

* * *

## 1. Intent

### 1.1 Problem Statement

IPFire’s iptables ruleset is complex. A default installation spans five netfilter tables (raw, mangle, nat, filter, security), each containing multiple built-in chains, which in turn jump to dozens of user-defined chains. The ruleset changes dynamically depending on which features are enabled (IPS/IDS, captive portal, location blocking, VPN tunnels, QoS, etc.). Today, there is no canonical way to visualize the full packet path through all of these chains.

### 1.2 Goal

Produce a tool that generates a Graphviz `.dot` file representing the **complete IPv4 packet traversal path** through an IPFire iptables configuration. The output must:

1. Show the **netfilter hook order** (raw → conntrack → mangle → nat → filter → security) for all three packet paths (incoming-to-local, forwarded, locally-generated).

2. Show the **chain-jump sequence** within each built-in chain.

3. Show **routing decisions** and the **local process boundary** as explicit nodes.

4. Be renderable by standard Graphviz into SVG, PNG, or PDF.

### 1.3 Scope Constraints

- **IPv4 only.** ip6tables is out of scope.

- **Standalone CLI tool.** Not a WUI component. Produces a static file.

- **Invocation:** Manual execution or triggered at boot via init script.

- **Chain-level granularity by default.** Individual rules are not shown unless explicitly requested via `--expand`.

- **Not a replacement for `iptables -L` or `iptables-save`.**

* * *

## 2. Design Decisions and Tradeoffs

### 2.1 Fundamental Approach: Live Extraction vs. Static Template

#### Alternative A: Static Template

A hand-authored `.dot` file encoding IPFire’s known chain structure.

**Strengths:** Zero runtime dependencies; works offline; easy to version-control; no root required; directly usable as documentation.

**Weaknesses:** Drifts from reality the moment any feature is toggled or addon installed; requires manual updates every release; cannot represent per-system variation; completeness depends on the author tracing every code path.

#### Alternative B: Live Extraction

A script that parses `iptables-save` output to discover chains and generate the `.dot` programmatically.

**Strengths:** Always accurate; zero maintenance when chains change; detects anomalies (orphan chains, empty chains); handles per-system variation naturally.

**Weaknesses:** Requires root; requires a running IPFire system; output varies per system; parsing complexity; runtime dependency on bash/Python.

#### Evaluation

If the diagram is primarily **documentation** , a static template is simpler and sufficient. If it is primarily a **diagnostic aid** , live extraction is the only approach that delivers value.

**A hybrid approach is possible:** live extraction as the primary mode, with a `--snapshot` flag that dumps the intermediate representation to JSON for offline re-rendering and version control. A reference snapshot from a default install effectively becomes the “static template” with a provenance trail.

For the remainder of this spec, the architecture supports both modes.

### 2.2 Granularity

**Chain-level by default** , with an optional `--expand <chain>` flag that inlines rules as a sub-cluster.

### 2.3 Inter-Table Packet Flow

The backbone edges follow the netfilter packet traversal order (dashed red), visually separated from intra-chain jump edges (solid blue):

```auto
PREROUTING: raw → conntrack → mangle → nat
INPUT: mangle → filter → security
FORWARD: mangle → filter → security
OUTPUT: raw → conntrack → mangle → nat → filter → security
POSTROUTING: mangle → nat

```

### 2.4 Routing Decisions

Modeled as **diamond-shaped decision nodes** outside any table cluster, labeled “Routing Decision” and “Reroute Check”.

### 2.5 Empty Chains and Policies

Empty chains show a pass-through annotation. Policies are terminal octagon nodes (salmon for DROP, palegreen for ACCEPT).

### 2.6 Language

**Bash for extraction, Python for DOT generation** , with a pure-Bash fallback.

* * *

## 3. Architecture

Three-stage pipeline:

```auto
┌─────────────┐ ┌──────────────┐ ┌───────────────┐ ┌────────────┐
│ EXTRACT │────▶│ MODEL │────▶│ RENDER │────▶│ OUTPUT │
│ (bash/root)│ │ (structured)│ │ (DOT gen) │ │ (.dot/.svg)│
└─────────────┘ └──────────────┘ └───────────────┘ └────────────┘

```

### 3.1 Stage 1: Extract

Calls `iptables -t <table> -S` for all five tables. Parses output to extract chain names, policies, jump-target ordering, and rule counts. Emits JSON intermediate representation.

### 3.2 Stage 2: Model

Builds the netfilter backbone (kernel-invariant traversal order), attaches chain internals, inserts synthetic nodes (routing decisions, conntrack, local process), and tags edges with packet path membership.

### 3.3 Stage 3: Render

Emits valid Graphviz DOT syntax with the visual vocabulary below. Supports `--expand` and `--path` highlighting.

### 3.4 Visual Vocabulary

| Element | Representation | Meaning |
| --- | --- | --- |
| Table | cluster, blue border, ivory fill | iptables table |
| Built-in chain | sub-cluster, grey fill | Netfilter hook point |
| User chain | rect, white fill | Jump target |
| Policy | octagon, salmon/palegreen | Chain default policy |
| Routing decision | diamond, lightyellow | Kernel routing lookup |
| Conntrack | oval, lavender | Connection tracking |
| Local process | house, lightblue | Firewall’s own stack |
| Intra-chain edge | blue, solid | Jump ordering |
| Backbone edge | red, dashed | Inter-table flow |

* * *

## 4. Implementation — Pseudocode

### 4.1 Entry Point

```auto
PROGRAM ipfire-fw-graph:
  PARSE arguments:
    --output PATH, --format dot|svg|png, --snapshot PATH,
    --expand TABLE:CHAIN, --path incoming|forward|outgoing

  IF --snapshot file exists: intermediate ← READ_JSON(path)
  ELSE: intermediate ← EXTRACT(); optionally WRITE_JSON

  graph_model ← BUILD_MODEL(intermediate)
  IF --expand: EXPAND_CHAIN(graph_model, intermediate, table, chain)
  dot_output ← RENDER_DOT(graph_model, options)
  WRITE or PIPE through "dot -T{format}"

```

### 4.2 Extract

```auto
FUNCTION EXTRACT() → JSON:
  BUILTIN_TARGETS ← {"ACCEPT","DROP","REJECT","RETURN","LOG",
    "MARK","CONNMARK","DNAT","SNAT","MASQUERADE","REDIRECT",
    "NFQUEUE","NFLOG","CT","NOTRACK","TCPMSS","TPROXY",...}

  FOR table IN [raw, mangle, nat, filter, security]:
    raw_output ← EXEC("iptables -t {table} -S")

    Pass 1 — identify chains:
      "-P <CHAIN> <POLICY>" → builtin chain with policy
      "-N <CHAIN>" → user-defined chain

    Pass 2 — extract jump targets:
      "-A <CHAIN> ... -j <TARGET>" →
        IF TARGET in chains AND not in BUILTIN_TARGETS:
          append to chain's ordered jump_targets list

    Post-pass — flag orphan chains (defined but never referenced)

  RETURN { timestamp, hostname, tables }

```

### 4.3 Build Model

```auto
FUNCTION BUILD_MODEL(intermediate) → graph_model:
  Create synthetic nodes:
    packet_in, conntrack_pre, routing_in, local_process,
    conntrack_out, reroute_check, packet_out

  For each table/builtin-chain:
    Create cluster, add jump-target nodes in order,
    add policy terminal node, add anchor if empty

  CHAIN_ENTRY(table, chain) → first jump-target node or anchor
  CHAIN_EXIT(table, chain) → last jump-target node or anchor

  Wire netfilter backbone (kernel-invariant):
    PREROUTING (incoming+forward):
      packet_in → raw.PRE → conntrack → mangle.PRE → nat.PRE
      → routing_in

    Routing fan-out:
      routing_in → mangle.INPUT (dst=local, incoming path)
      routing_in → mangle.FORWARD (dst=remote, forward path)

    INPUT (incoming):
      mangle.IN → filter.IN → security.IN → local_process

    FORWARD (forward):
      mangle.FWD → filter.FWD → security.FWD → mangle.POST

    OUTPUT (outgoing):
      local_process → raw.OUT → conntrack → mangle.OUT
      → nat.OUT → filter.OUT → security.OUT → reroute_check

    POSTROUTING (forward+outgoing):
      reroute_check → mangle.POST → nat.POST → packet_out

```

### 4.4 Render DOT

```auto
FUNCTION RENDER_DOT(graph_model, options) → string:
  Emit digraph header (rankdir=TB, compound=true)
  Emit synthetic nodes outside clusters
  For each table cluster:
    For each chain sub-cluster:
      Emit nodes
  For each edge:
    Style by type (intra_chain=blue/solid, backbone=red/dashed,
                   policy=grey/dotted)
    If --path: highlight matching edges green, dim others
    If backbone crossing clusters: add ltail/lhead for compound edges

```

### 4.5 Chain Expansion

```auto
FUNCTION EXPAND_CHAIN(graph_model, intermediate, table, chain):
  Parse rules_raw into compact summaries:
    "src=X dst=Y tcp dpt=443 → ACCEPT"
  Replace the chain's single node with a vertical list of rule nodes
  Reconnect incoming/outgoing edges to first/last expanded node

```

* * *

## 5. Open Questions

1. **User-chain-to-user-chain jumps.** Some user chains jump to other user chains. Should we follow one level deep? All levels? Make it configurable?

2. **Connection tracking fast-path.** IPFire’s `ESTABLISHED,RELATED` accept rules short-circuit most traffic before later chains. Should we annotate this?

3. **Snapshot diffing.** Comparing two snapshots to show structural changes is a natural extension. Defer or include?

4. **Graphviz layout quality.** Large configs may need alternative layout hints or engines via `--layout`.

5. **Boot-time ordering.** Must execute after `firewall` and all VPN/addon services have started.

---

<div class="post-metadata">

**Author:** ![hjkl](https://community.ipfire.org/user_avatar/community.ipfire.org/hjkl/32/8574_2.png) [@hjkl](https://community.ipfire.org/u/hjkl)\
**Post date:** [10 February 2026 13:19 UTC](https://community.ipfire.org/t/firewall-diagram/15469/5 "2026-02-10T13:19:59Z")

</div>

> [@cfusco](#):
>
> - Color coding by zone (GREEN=green, RED=red, etc.)
> - Grouping related chains in subgraphs
> - Legend showing what colors/shapes mean

The colors depend on interfaces, and interfaces are listed as source or dest inside the rules.

In theory that can be achieved once the script will start to parse the rules : to detect the relationship between chains but also the interfaces.

Grouping is there and will continue to be there.

Legend - that is above my skills now.

Thank you for the inputs!

---

<div class="post-metadata">

**Author:** ![hjkl](https://community.ipfire.org/user_avatar/community.ipfire.org/hjkl/32/8574_2.png) [@hjkl](https://community.ipfire.org/u/hjkl)\
**Post date:** [28 May 2026 19:27 UTC](https://community.ipfire.org/t/firewall-diagram/15469/6 "2026-05-28T19:27:59Z")

</div>

The first attempt to automatically generate the links (arrows) based on [Linux netfilter Hacking HOWTO: Netfilter Architecture](https://www.netfilter.org/documentation/HOWTO/netfilter-hacking-HOWTO-3.html) and the [Iptables Tutorial 1.2.3](https://www.frozentux.net/iptables-tutorial/iptables-tutorial.html#TRAVERSINGOFTABLES) lead to an not so easy to read diagram:  
Top-Bottom:

 ![image](https://community.ipfire.org/uploads/default/original/2X/e/eaab7d84ac3d519b8dc38de757748348bebabf90.png)

Left\_Right:

 ![image](https://community.ipfire.org/uploads/default/original/2X/4/463044ee8dd1c64e49e37210a312174ce57a1991.png)

Therefore I tried different styles of representing the information by using the PCB routing design style where I tried to avoid intersections (each being additional cost in PCB design).  
I gave the PCB principles to an AI, my original 2015 diagram, additional constraints on what can be omitted and what is to be displayed and got from it 3 styles:

**Option 1** — two parallel rows collapsed into one thin horizontal strip.

 ![opt1](https://community.ipfire.org/uploads/default/original/2X/3/306a0857fdc4e20b41653c1ed1b2bf5d829cd4cf.png)

**Option 2** — closest to my 2015 diagram where I have split NAT table in parts to follow PREROUTING and POSTROUTING logic. Semi-readable but the diagonal paths defeat the PCB orthogonal-routing goal.

 ![opt2](https://community.ipfire.org/uploads/default/original/2X/2/2aa97d29d32ac030fc656a93c8c332a87b9216f5.png)

**Option 3** — BADTCP anchors the left, main shared spine flows right (P2PBLOCK → GUARDIAN → LOOPBACK → CONNTRACK → LOCATIONBLOCK), and the exclusive chains are small dashed side-branches above and below. Closest to PCB spine concept.

 ![opt3](https://community.ipfire.org/uploads/default/original/2X/e/efa33a85054f80e65b5045621fc755b7dd562e06.png)

I would like to use the poll functionality to get from the IPFire users their preferred option in displaying the Iptables Netfilter information:

_Poll ([view on site](https://community.ipfire.org/t/firewall-diagram/15469/6))_

---

<div class="post-metadata">

**Author:** ![hjkl](https://community.ipfire.org/user_avatar/community.ipfire.org/hjkl/32/8574_2.png) [@hjkl](https://community.ipfire.org/u/hjkl)\
**Post date:** [29 May 2026 10:35 UTC](https://community.ipfire.org/t/firewall-diagram/15469/7 "2026-05-29T10:35:55Z")

</div>

![ipfire_pcb_firewall_core200_v1.5.0_TB](https://community.ipfire.org/uploads/default/original/2X/5/53f265e17f1dd527a1843857775820f3a715fcbf.png)

Version 1.5.0 of the script delivers that  
Is it easy to be understood? (i.e. no need to review based on [Linux netfilter Hacking HOWTO: Netfilter Architecture](https://www.netfilter.org/documentation/HOWTO/netfilter-hacking-HOWTO-3.html) and the [Iptables Tutorial 1.2.3](https://www.frozentux.net/iptables-tutorial/iptables-tutorial.html#TRAVERSINGOFTABLES) to understand the flow of a packet through all the chains?)

---

<div class="post-metadata">

**Author:** ![hjkl](https://community.ipfire.org/user_avatar/community.ipfire.org/hjkl/32/8574_2.png) [@hjkl](https://community.ipfire.org/u/hjkl)\
**Post date:** [29 May 2026 13:35 UTC](https://community.ipfire.org/t/firewall-diagram/15469/8 "2026-05-29T13:35:17Z")

</div>

Tool is public at [Diagraph IPFire](https://github.com/horace-michael/diagraph-ipfire/releases/latest)

---

<div class="post-metadata">

**Author:** ![hjkl](https://community.ipfire.org/user_avatar/community.ipfire.org/hjkl/32/8574_2.png) [@hjkl](https://community.ipfire.org/u/hjkl)\
**Post date:** [31 May 2026 20:13 UTC](https://community.ipfire.org/t/firewall-diagram/15469/9 "2026-05-31T20:13:03Z")

</div>

Version [1.5.1](https://github.com/horace-michael/diagraph-ipfire/releases/tag/v1.5.1) is released - nat and mangle tables fully expanded. Newly visible chains include SQUID (transparent proxy in nat/PREROUTING), IPS/Suricata chains  
(in mangle INPUT/FORWARD/OUTPUT), and CAPTIVE\_PORTAL (nat/PREROUTING).

 ![ipfire_pcb_firewall_core200_v1.5.1_TB](https://community.ipfire.org/uploads/default/original/2X/0/00e49ca93c063780a270330892572a897214e5ef.png)
