Firewall Diagram

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:

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];
    }
  }
}
3 Likes

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.


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.


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):

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:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  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

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

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

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

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

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.

1 Like

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!